# Track note: arm (the player's first-person cybernetic arm) Workplace: the one project `D:\HADAL - The Descent\TheDescentUE` (the copy `.wt\arm` is no longer written to). Written by the Coder of the arm, 2026-10-05. **Everything under "Results" was run on this PC in this project.** What was guessed, not seen or not run is listed at the end. **2026-10-11: the arm is off the character's view.** The player's view is now the Twinblast body (`Docs/status/player_body.md`); on the character the arm stays in its Hidden pose and is not started for the jack-in. Switch it back with `descent.Body.OldArm 1` (console variable, default in `Core/Tuning/DescentTune_Body.h`) or `Descent.Body.View arm`; `arm_character` does that as its first step. Nothing of the arm was removed. ## WHERE THE WORK STANDS (read this first after a cut-off) | Task | State | |---|---| | A. The original brief: export, import, viewmodel component, poses, reach solver, jack-in, debug rig, commands, scenarios, tests | DONE | | B. The arm on `ADescentCharacter` (foot and swim, gone in a Tender, no clipping) + scenario `arm_character` | DONE | | C. The whole chain (build, import, all tests, every scenario) | DONE: last full run 2026-10-05 23:36-23:46, after the last source change; everything passes (the two self-tests fail on purpose) | ## Results | Chain step | Result | |---|---| | `Tools\export_arm.ps1` | PASS, 1.1-1.8 s (last run 23:20). The user's `.blend` is untouched (same SHA-256 before and after; size 367,515, modified 2026-10-04 21:13:45) | | `Tools\build.ps1` | PASS, 0 warnings | | `Tools\import_assets.ps1` | PASS, 8 s | | `Tools\run_tests.ps1` | **67 of 67 PASS** (59 older ones + 8 `Descent.Arm.*`) | | Scenarios | **37 PASS**: `arm_showcase`, `arm_jack`, `arm_character`, and the 34 older ones (`smoke`, `core_sim`, `kazah_showcase`, `kazah_closeups`, `look_bloom`, `look_calibrate`, `look_deep_dark`, `look_deep_lamp`, `look_dry_mask`, `look_near_wall`, `look_rooms`, `look_sweep`, `look_throat`, `m12_boot`, `m1_carve`, `m1_commands`, `m1_terrain_tour`, `m1_world_slices`, `m2_dry`, `m2_exterior`, `m2_moving_base`, `m2_walk`, `m3_carousel`, `m3_cockpit`, `m3_crush`, `m3_launch_dock`, `m3_no_walls`, `m3_rotor`, `m3_swim`, `m3_ui`, `ui_dash`, `ui_gallery`, `ui_hud_layout`, `ui_windows`). FAIL on purpose (2): `selftest_expect`, `selftest_timeout` | The chain was run three times this evening (23:10, 23:21, 23:36), each time with all 37 scenarios passing. **In the 23:21 run `import_assets.ps1` FAILED** (exit 1, 40 errors): the user's Unreal editor had the project open and held the material files (`MoveFile was unable to move ... MI_*.uasset`), so the commandlet could not save them. The import script itself reported PASS and the 20 arm meshes were re-imported and matched their bounds (the export had been re-run, and an FBX carries its creation time, so its hash changes on every export). With the editor closed (23:10 and 23:36) the import passes. **While the editor has the project open, `import_assets.ps1` cannot pass**; that is the editor's file lock, not a fault of the scripts. The on-foot pictures of the older scenarios now have the hand in the lower right corner (seen in `m2_walk_room_bridge`; the other rooms' pictures were not looked at one by one); the luminance checks of `look_rooms` and `m2_walk` still pass. ### The eight tests (`Tests\ArmTests.cpp`), with the numbers they print | Test | What it proves | |---|---| | `Descent.Arm.Layout` | 21 parts in the order of `EDescentArmPart`, each on the parent the brief names, 20 with a mesh; **every child's pivot lies on its parent** (inside the parent's mesh box, 1 cm slack; the upper arm's pivot is the shoulder point); the chain built from the local numbers lands every pivot where the model has it (worst < 0.01 cm); 30.0 + 29.2 + 19.44 = 78.64 cm; a right arm (thumb root at -Y) | | `Descent.Arm.Joints` | over the 7 named poses and 24 random ones: no pivot moves, every hinge / roll / finger turns about its own joint axis only, the rod only slides along its axis inside its stroke and follows the elbow monotonically; the senses of every joint; **each finger curls toward the palm** and a fist brings the tip back to the palm; the thumb closes toward the palm's centre; curling one finger moves that finger alone | | `Descent.Arm.PoseBlend` | 7 named poses, names parse, all differ, all inside the limits; blending is **bit-for-bit repeatable**, depends on the time passed and not on the frame split (0.2 s in 6 steps = in 24 steps to 1e-9), arrives exactly, is a smoothstep (no step above 1.5 x the mean), re-targeting in mid-blend does not move the arm | | `Descent.Arm.Reach` | **1029 of 1029 targets inside the range hit, worst error 7.2e-11 cm** (stated accuracy 0.05 cm): hand pivot, palm and fingertip, 60-degree cone in front of the shoulder, every distance from the folded to the straight arm. Direction and palm twist also exact in 882 of them. 576 fingertip targets with fingers forward and 4 palm twists in a 30 x 50 x 50 cm box: every one within the arm's length is hit. **Out of range is reported**: 85 / 120 / 300 cm away gives `bReachable` false, `bLimited` true, `ErrorCm` = the true shortfall, the arm straight; 2 mm inside the far edge is reached, 5 mm outside is not; too near (4 cm) is refused too. The reported error always equals the forward-kinematics error of the returned pose. Deterministic. The named poses put the hand pivot exactly where their recipes say, elbow below and outside | | `Descent.Arm.Jack` | the design's times (0.7 / 1.0 / 4.1 / 4.8 s); six phases in order; a pure function of time; at 120 Hz over the whole sequence against the rig's port (70.5 cm from the shoulder): **fingertip on the socket within 1.5e-14 cm for the whole 3.1 s feed**, hand along the port's axis (0.000 deg), plug and withdrawal on the axis and never past the socket, **largest joint step 5.8 degrees** (no jump); a port 40 cm too far is reported unreachable | | `Descent.Arm.Component` | in a throw-away world: 21 components attached in the model's chain, 20 with meshes; the scene components are where the pose maths says in every pose and in mid-blend (< 0.01 cm); `SetFingerCurl`; `ReachToWorld` puts the real fingertip component on a world point; first-person mounting (camera's child at the right shoulder, first-person primitives, no shadow, stencil 1, channels 0 and 1, no collision, the rig's FOV 72); a left arm is the mirror; the whole jack-in at 30 Hz: 4.8 s, plugged at 1.0 s, both events once, the fingertip COMPONENT within 0.0000 cm of the port in the world, a pose request during it ignored, a second start refused, abort works | | `Descent.Arm.FirstPersonFit` | over every named pose and every blend between two of them: the deepest point of the arm is 67.3 cm from the eye and is **drawn at 30.3 cm** (first-person scale 0.45), a wall's face is never nearer than 34.0 cm; the nearest point on screen is 29.2 cm away and is **drawn at 13.2 cm**, the near plane is at 10.0 cm | | `Descent.Arm.Clearance` | boxes of non-joined segments do not overlap (2 mm of skin ignored): in every named pose; each finger over its whole curl; the four fingers closing together; the fist (thumb curl 0.30; it is clear of the closed fingers up to 0.48); elbow fully bent with the wrist at both limits. **Not covered by boxes: the thumb against the palm** (its box and the palm's overlap although the parts do not); that pair was looked at in pictures instead | ### The three scenarios (`Debug\Scenarios_Arm.cpp`) and what the pictures show (`Saved\Shots\`, looked at by the Coder) * **`arm_showcase`** (PASS, 34 s): the arm alone on the rig's stand, 6 km outside the claim and 400 m down, so the background is black. `zero_outer/_inner` (all joints at zero), `hidden`, `idle`, `reach`, `grip`, `point`, `plug`, `hold_tool` each `_outer` (from the right, front, above: the back of the hand) and `_inner` (from the left, front, below: the palm and the red hoses), `blend_idle_grip_*` (half way, weight 0.5 asserted), `curl_fan_*` (thumb 0.5, index 0.25, middle 0.5, ring 0.75, pinky 1), `fist_*`, `fist_front`, `fist_palm`, `thumb_half_*`, `thumb_tucked_*`, `reach_command_*` (`Descent.Arm.Reach 60 -10 15 1 0 0 90`: error 0.0000 cm, direction and twist error 0.00 deg). A reach to 1.5 m is asserted "out of range". **Seen** (looked at: `zero_outer`, `zero_inner`, `hidden_outer`, `idle_outer`, `reach_outer`, `grip_outer`, `grip_inner`, `point_inner`, `plug_inner`, `curl_fan_inner`, `fist_inner`, `fist_palm`, `thumb_tucked_front`, `thumb_tucked_palm`, `reach_command_outer`; the other shots of the 30 were not opened): the arm is whole in each (shoulder cap with its red disc, upper arm, elbow hub, forearm with rod and red hoses, wrist bellows, hand, 15 phalanges), nothing detached or inside out; fingers curl toward the palm, more with more curl; the fist's four fingertips rest on the palm, the thumb beside them; the fully tucked thumb lies under the palm with a visible gap (`thumb_tucked_front`). With a commanded thumb-up reach the elbow goes up and out to the side. * **`arm_jack`** (PASS, 15 s): the rig in first person (eye 1.65 m, FOV 72, looking at a port 1.45 m up on a wall 0.55 m away), the arm mounted on the camera. `0_before` (wall and port, no arm), `1_raise` (the hand comes up from the lower right), `2_reach`, `3_plug`, `4_feed` (the fingers inside the port's collar, the collar's near rim in front of the fingertips), `4_feed_side`, `4_feed_below` (from other cameras: four fingers straight into the collar along its axis), `5_withdraw`, `6_lower`. Asserted at the feed: fingertip **0.0000 cm** from the socket in the world, hand direction error 0.000 deg. Then the sequence once more on the game's own clock: **4.800 s**, 93 feed frames, worst error 0.0000 cm. **Seen** (`1_raise`, `2_reach`, `3_plug`, `4_feed`, `4_feed_side`, `6_lower`; not opened: `0_before`, `4_feed_below`, `5_withdraw`): it reads as one's own right arm reaching to a wall port, at a believable size (the forearm fills the lower middle of the picture, as an arm stretched to 70 cm does). The thumb stands out to the left of the hand rather than lying against it. * **`arm_character`** (PASS, 21 s): the real character. `foot_idle`, `foot_idle_bridge`, `foot_idle_look_down`, `foot_grip`, `foot_hold_tool`, `foot_point`, `foot_reach` (galley, one in the bridge); `foot_wall_reach`, `foot_wall_idle` walked into the galley's port-side wall until the capsule stopped (carrier-local y -9.498, speed 0): wall face 34.0 cm from the eye, the longest pose **drawn to 29.0 cm**; `swim_idle` outside the hull with the lamp on the hull; `tender_docked` (mode Docked): arm pose `hidden`, no segment visible (asserted), the picture shows the cockpit and no arm. **Seen** (all but `foot_wall_idle` and the last run's `foot_grip`, `foot_reach`): on foot the hand sits in the lower right of the picture, back of the hand to the viewer; `foot_point` points with the index, `foot_hold_tool` is a closed hand turned thumb up; against the wall the whole hand is visible, nothing cut; swimming, the lamp's spot on the hull has no arm shadow in it. ## What was built (files) New: `Player/Arm/DescentArmLayout.*` + `ArmLayout.inl` (generated), `DescentArmPose.*` (the pose value, joint maths, forward kinematics, the blender), `DescentArmReach.*` (the solver, `PoseFromHandAndElbow`), `DescentArmJack.*` (the sequence as a pure function of time), `DescentArmViewmodelComponent.*` (components, time, rendering, mounting; state section `arm`), `DescentArmDebugRig.*`; `Core/Tuning/DescentTune_Arm.h`; `Debug/DescentCommands_Arm.cpp`, `Debug/Scenarios_Arm.cpp`; `Tests/ArmTests.cpp`; `Tools/blender/export_arm.py`, `Tools/export_arm.ps1`, `Tools/ue/assets_12_arm.py`; `SourceArt/Arm/*` (20 FBX + manifest), `Content/Player/Arm/*` (20 meshes, 1 material, 7 instances). Changed (someone else's files, the smallest edits; see "Shared-file changes"): `Player/DescentCharacter.h`, `Player/DescentCharacter.cpp`. Commands (`Descent.Arm.`; they act on the debug rig's arm when a rig exists, otherwise on the character's): `Rig `, `Pose [blend s]`, `Curl <0..1> [blend s]`, `Reach x y z [dx dy dz [twist]]` (arm space, cm), `ReachWorld x y z [dx dy dz]` (world, m), `Jack [x y z dx dy dz | abort]` (world m; no arguments: the rig's port), `Recipe handX handY handZ elbowOut wristRoll wristFlex` (the six numbers of a pose recipe, for tuning a pose while looking at it). State section `arm`: `count`, `items[]` = `owner`, `side`, `mounted_on_camera`, `first_person_rendering`, `in_dry_air`, `pose` (a name or `custom`), `parts_built`, `parts_with_mesh`, `blend{blending, progress, weight}`, `joints{shoulder_yaw/pitch/roll_deg, elbow_flex_deg, wrist_roll/flex_deg, rod_slide_cm, finger_curl[5]}`, `target_pose_distance`, `reach{source, target_arm_m, error_cm, reachable, limited, distance_cm, max_reach_cm, hand_dir_error_deg, twist_error_deg, tolerance_cm}`, `jack{active, phase, time_s, feeding, reach_error_cm}`, `fingertip_world_m`, `shoulder_world_m`. ## How the arm works (for whoever builds on it) * **A pose is a value** (`FDescentArmPose`): shoulder yaw / pitch / roll, elbow flexion, wrist roll, wrist flex, five finger curls, a shift of the shoulder. `ComputeLocal` turns it into one transform per part: the part's fixed offset on its parent, its rest rotation, then **one rotation about its joint axis** (the rod: one slide along its axis, `-5.5 cm x elbow / 150`). Nothing else ever moves a part. * **Named poses** are recipes in `DescentTune_Arm.h`: where the hand pivot is, how far the elbow swings out from "straight below", wrist roll and flex, curls. * **Reach solver** (`SolveReach`): the arm has six joints for six numbers (where, which way the hand points, how the palm is twisted). The wanted hand orientation fixes the hand pivot; the triangle shoulder - elbow - pivot is closed-form; the elbow goes to the one or two points of its circle where the forearm is square to the hand's hinge (only there can roll + flex give the orientation exactly). If no such point exists or a limit holds a joint, it solves **position first**: wrist held, effector put on the target in closed form, and the one free turn about the line shoulder - target searched (10 degrees, then 1 degree) for the nearest orientation. The result says what happened (`bReachable`, `ErrorCm`, `HandDirErrorDeg`, `TwistErrorDeg`, `bLimited`). **Stated accuracy: 0.05 cm** (`ReachToleranceCm`); measured 7e-11 cm inside the range. * **Jack-in** (`EvaluateJack(T, input)`, 4.8 s): raise 0-0.3 (joint space, from the pose it had), reach 0.3-0.7 (fingertips on a straight line to a standoff 14 cm out on the port's axis, hand closing to the plug shape), plug 0.7-1.0 (straight in), feed 1.0-4.1 (held on the socket; `OnJackPlugged` fires at 1.0), withdraw 4.1-4.45 (straight out), lower 4.45-4.8 (joint space, back to the named pose it started in; `OnJackFinished`). The socket is given in the world and converted to arm space every frame, so the hand tracks it while the body moves. ## How to mount this on the character (it IS mounted; this is what was done, and what later milestones call) ```cpp // DescentCharacter.h UPROPERTY(VisibleAnywhere) TObjectPtr Arm; // constructor Arm = CreateDefaultSubobject(TEXT("Arm")); Arm->SetupAttachment(Camera); // BeginPlay Arm->MountOnCamera(Camera, EDescentArmSide::Right); // shoulder at (-5, +19, -24) cm from the eye; first-person settings; camera scale // Tick, before anything else: Idle when the mode is Foot or Swim and the body is not seated, Hidden otherwise. ``` * The character decides only between **Idle and Hidden**. While up, a pose someone else set stays: the carry component (M5) calls `Arm->SetPose(EDescentArmPose::Grip)` on pick-up and `SetPose(Idle)` on put-down; a hand tool (M4 / M8) `SetPose(HoldTool)`. * **Jack-in (M8, mode `Jack`)**: `Arm->StartJackIn(SocketWorldCm, -PortOutwardNormal)` at the Foot > Jack transition, with the eye about 55 cm from the wall (the port must be within 79 cm of the shoulder; check `GetLastJackFrame().bReachable`). Bind `OnJackPlugged` (start the feed, ring, sound) and `OnJackFinished` (back to Foot). `IsJackFeeding()` is true from 1.0 s to 4.1 s. `AbortJackIn()` if the mode is left early. The port anchors of the layout are 14 cm proud of the wall (`port_1` at carrier-local (24, -9.76, -1.55)): the real fixture needs a recess or a collar the fingers can enter; with the fixture stub as it is the fingers would go into its box. * A left arm: `MountOnCamera(Camera, EDescentArmSide::Left)` (the same model mirrored). ## Shared-file changes (files that are not the arm's) * `Player/DescentCharacter.h`: a forward declaration and the member `Arm`. * `Player/DescentCharacter.cpp`: one include; two lines in the constructor; one line in `BeginPlay`; one block of 12 lines at the top of `Tick` (Idle / Hidden by mode). * `Player/DescentPlayerController.*`: **not edited**. * `Tools/build.ps1`: one block. The script only looked whether an editor holds the plain `UnrealEditor-TheDescent.dll`. On 2026-10-05 at 23:29 the user's editor was started after a numbered build and so held `UnrealEditor-TheDescent-0001.dll`; the next build failed with "Unable to delete hot-reload file". A locked numbered DLL now counts as "held" too and the build links the next number (this one linked `-0002`). `-Rebuild` is refused in that case as well. * Nothing else outside the arm's own files was edited. ## Deviations from the brief and the design, decisions, and why 1. **Jack-in timing.** The design gives 0.7 (approach ends), 1.0 (feed begins), 4.1 (feed ends), 4.8 (release). The brief asks for raise, reach, plug, hold, withdraw, lower. The split inside the design's boundaries is mine: raise to 0.3, reach to 0.7, plug to 1.0, withdraw to 4.45, lower to 4.8. 2. **First-person scale 0.45, true depth during the jack-in.** Drawn at true depth the hand goes into a wall the body is pressed against (the reach pose is 64 cm deep, the capsule 34 cm). Unreal's first-person scale draws the arm at the same place and size on screen but nearer in depth. During the jack-in the scale is 1 (true depth), because the fingers must go into the port and behind its rim; the switch moves nothing on screen. 3. **The arm is drawn only in its owner's view** (`SetOnlyOwnerSee`): a Tender's camera, the map and scenario cameras do not see an arm floating at the character. 4. **The arm is up (Idle) whenever the mode is Foot or Swim.** The design mentions the arm only for the jack-in and for the dry stencil; this is the Administrator's instruction for task B. Swimming uses the same Idle pose (there is no swim stroke). 5. **Shoulder limit** is a cone (the upper arm within 150 degrees of straight ahead) with free roll, not separate yaw / pitch / roll boxes: the boxes of the first version forbade ordinary places (a hand low in front of the hip). **Wrist back-bend** is limited to -40 degrees (at -65 with the elbow fully bent the hand entered the upper arm). **Finger closed angles** were taken back until the fingertips' boxes stop short of the palm. 6. **Named poses are recipes of hand place + elbow swing + wrist**, not of hand direction: with a wrist that only rolls and bends, asking for a hand direction put the elbow inside the body (shoulder yaw -31 degrees in the first version). 7. **The debug rig has no exposure or fog of its own** (the environment actor is the only authority) and uses the game's unitless lights. It stands far outside the claim so the showcase background is dark. 8. **`Descent.Arm.*` commands prefer the debug rig's arm** once a rig is spawned, otherwise they drive the character's. 9. The model has 26,936 triangles and 7 materials (the brief says 17k and 3). 10. **Lighting channels.** The arm is on channels 0 and 1 like the body (design 5.1), but while its camera is in dry air it drops channel 0: design 7.4 says the sea's light never falls on anything inside the hull. Asserted by `arm_character` (off on foot, on while swimming). It made no visible difference in the shots. ## Guessed, not seen, not run (stated plainly) * **Guesses, all in `DescentTune_Arm.h` and marked there:** the shoulder's place relative to the eye (-5, +19, -24 cm); every pose recipe; finger and thumb closed angles; the rod's stroke (5.5 cm); the jack-in's ready point, standoff (14 cm) and palm twist (20 degrees); blend times (0.35 s, 0.5 s for hidden); the rig's wall distance (55 cm). * **The hand's colour does not follow the room's.** In the blue bridge (`arm_character_foot_idle_bridge`, also `m2_walk_room_bridge`) the hand is tan while every wall is blue; in the warm galley it matches. Not the sun (channel 0 is off there). **Probable cause, not proven:** the rooms get their colour from the ambient term of their own material (`M_DescentSurface`), the lamps are faint and shadowless (they reach through floors and walls), and the arm has its own plain lit material (`M_Arm_Opaque`), so it shows only whatever lamps reach it. The likely fix is to parent the arm's seven instances to `M_DescentSurface` in `Tools\ue\assets_12_arm.py`. **Not done:** it needs `import_assets.ps1`, which cannot save while the user's editor has the project open (see "Results"). * **The thumb against the palm** is not proven by a test, only looked at in two pictures. * **Clearance is tested with boxes**, which is stronger than needed where it passes but says nothing about the blends between poses (only the first-person fit is tested over the blends) nor about arbitrary reach results. * **Not seen moving.** Every picture is a still; the blends and the jack-in were checked as numbers (no jump above 5.8 degrees per 1/120 s) and as stills at each phase, not watched as motion. The raise is the fastest part (0.3 s). * **The grip pose is not on a box** (there is no carry system before M5) and the hold-tool pose holds nothing; both are hand shapes at a plausible place. * **Not run:** a jack-in on the real character against a real port fixture (mode `Jack` is M8; the fixture is a stub box); the left arm in a picture (only the mirror transform is tested); the arm with a moving, pitching carrier; a non-unity build; pressing Play in an editor window. * On foot the camera's FOV is 72 degrees horizontal (the M3 note's open question): if it becomes 72 vertical the arm shows more of the forearm and `Descent.Arm.FirstPersonFit` must be re-run (it reads `Tune::Player::FovDeg` and assumes 16:9). * Unreal's first-person scale changes the depth the arm is lit and shadowed at (it is nearer the eye than it looks); no difference was visible in the shots. ## Facts measured by the export (from the geometry, not from the file's comments) * It is a **RIGHT arm**. Evidence: fingers point +X; the grip pads (RA_Rubber faces on the phalanges) sit on the -Z side and a positive rotation about a finger's joint axis moves the tip toward -Z, so **the palm faces -Z (down)** (the two palm estimates agree, dot 0.93); the thumb root is on +Y. In Blender's right-handed frame (fingers x palm) . thumb = +0.995 > 0 means right hand. In Unreal axes (`ue = (bx, -by, bz)`) the thumb is on -Y, the left of the arm, which is where a right hand's thumb is with the palm down. A left arm is this one mirrored in Y (negative Y scale on the component). * Chain: RA_Root (empty, the shoulder) > UpperArm > Forearm (elbow, hinge about local Y) > Wrist (roll about X) > Hand (flex about Y) > 5 fingers x 3 phalanges (curl about local Y); PistonRod is a child of the Forearm and slides along its local X. 21 parts, 20 meshes, **26,936 triangles**. * Lengths: shoulder to elbow 30.0 cm, elbow to hand pivot 29.2 cm, hand pivot to fingertip 19.44 cm, shoulder to fingertip 78.64 cm. * Materials: RA_ArmorWhite, RA_Rubber, RA_Frame, RA_Steel, RA_HoseRed, RA_Brass, RA_ArmorGrey. No emission, no textures. * The pivot of every child lies within 1 cm of its parent's mesh bounds (checked in the export; the export aborts otherwise). * The chain rebuilt from the exported local numbers reproduces every Blender world pivot to under 0.01 mm (checked in the export). * Skipped objects: `Cube`, `Camera`, `Light`, `RA_Cam`, `RA_Sun`. ## Conventions the C++ follows * Joint angle sign = the model's own: `FQuat(JointAxis, +angle)` curls fingers and flexes the hand toward the palm. Elbow flexion is the opposite sense (the forearm folds toward the back of the hand), so `ElbowFlexDeg` is applied as `-angle`. Positive wrist roll = supination (thumb up). * Arm space = the component's space: origin at the shoulder, +X along the arm at rest, +Y right, +Z up, centimetres. ## Findings on the way * The classic FBX importer with the Kazah options maps Blender to Unreal as `(-by, -bx, bz)` whatever forward axis the exporter says, so a model lying along +X arrived turned 90 degrees (caught by the bounds check on the first import). Fix in `export_arm.py`: each copy is turned -90 degrees about Z before the FBX is written. The result is `(bx, -by, bz)`; 20 of 20 parts then match to 0.0000 cm. * A material created in Blender with the canonical name of an existing material gets renamed `.001`; the exporter reuses the file's own datablock. * The table's quaternions have 7 decimals; a pose's rotation is normalised before use (an angle read from an unnormalised quaternion is off by 0.04 degrees). * A candidate score that only weighted position against orientation took the hand off reachable targets to point better; position now wins strictly. * The pearl ore of the "abyss" floor shows magenta behind anything photographed there. * A state snapshot must be held in a local while its parts are read (`C.Snapshot()` returns a temporary).