Godot port: Redot 26.2 -> Godot 4.6.3 (standard build) + XR foundation stack #5

Merged
merith-tk merged 12 commits from muse-lantern/da-game:godot-mcp-plugin into main 2026-10-22 17:43:44 +00:00
Member

What this is

The full stack, rebased as one PR because the XR foundation work was never PR'd separately: XR foundation (vendored godot-xr-tools 4.5.2, rig on XRToolsPlayerBody, controller pose fix), the engine port (Redot 26.2 -> Godot 4.6.3, standard build — see docs/engine-migration.md), the DA Debug Bridge as an editor plugin (localhost TCP + bundled stdlib MCP server), and the 1.2x arm-reach solver multiplier.

Arms: the corrected fix (2026-10-22) — replaces the earlier account

The first version of this PR described a "mirror correction" to the PSX arms asset (commit 1dd3cf2). That diagnosis was wrong and that fix is reverted by e908981 in this stack. The asset was never mislabeled: its bone names agree with its skinned geometry (verified in Blender — bone and weighted-geometry positions match, and the mesh vertex data was byte-identical across the bad commit; only armature bone positions had been flipped, leaving the bind incoherent, while bone-position-only tests kept passing).

The real defect was a frame mismatch in the mapping pipeline: the model is authored facing +Z (glTF convention — its elbows bow toward its back, -Z) while the player faces -Z (Godot). Fix, at the layer where it belongs:

  • .blend / .glb restored to the authored state; READMEs document the facing contract and the history.
  • scenes/player_rig.tscn: ArmsView carries a 180-degree yaw, landing the model's own left on the player's left. update_arms() converts controller world transforms through the skeleton's global transform, so the yaw is accounted for automatically.
  • tests/arms_check.gd: side guard now asserts world-space rest positions, plus a new bone/geometry coherence guard (each Hand bone must sit next to the geometry skinned to it). Negative control: against the old flipped asset, all four new assertions fail.

Debug Cam (new in this update)

The bridge gains a debug_cam method / MCP tool: a free Camera3D under the persistent Game root, independent of the player camera, placeable by orbit (target + yaw/pitch/distance) or explicit position + look_at. While enabled it is the viewport camera, so screenshot renders from it. Also, solve_arm now makes the view model visible (it hides whenever no controller is tracked — i.e. always in flat/headless runs). Verification workflow is now: pose with solve_arm, frame with debug_cam, check the screenshot with eyes — validated in-session with posed screenshots (wave / double-reach / T-pose, all uncrossed, hands on their own sides).

Verified

  • Import clean on the standard 4.6.3 binary; smoke_menu / arms_check / addon_check PASS.
  • Bridge e2e PASS (Xvfb): pause proof via pausable tick counter, solve_arm lands Hand within tolerance and shows the model, debug_cam orbit/explicit/snapshot/disable, screenshot from the debug cam, MCP handshake with 13 tools.
  • One-time, per checkout: delete .godot/ before the first import — Redot-stamped import cache (engine 26.2 / format 6) is refused by Godot.

Still owed (yours)

  • In-headset check: uncrossed arms, 1.2x reach/elbow feel, hand-orientation constants (still a first guess).
  • TwoBoneIK3D remains unverified on Godot 4.6.3 (spike produced zero movement in every configuration; the hand-rolled solver stays the IK of record).

Merge is yours, as always.

Validation correction (2026-10-22, Zach's review): the first demo captures were shot from behind the model (orbit-yaw front/back was documented inverted — now fixed) and solve_arm's default hand basis pinned the fingers skeleton-up, twisting the wrists in the captures. The bridge now defaults to a rest-relative hand basis (~7° wrist angle in a forward reach vs ~51° before), the snapshot reports fingertip positions, and the e2e asserts the wrist-angle invariant numerically. Corrected front-view captures and the full account are on issue #6. The arms fix itself (facing yaw + restored asset) was not implicated.

## What this is The full stack, rebased as one PR because the XR foundation work was never PR'd separately: XR foundation (vendored godot-xr-tools 4.5.2, rig on XRToolsPlayerBody, controller pose fix), the engine port (Redot 26.2 -> **Godot 4.6.3, standard build** — see `docs/engine-migration.md`), the DA Debug Bridge as an editor plugin (localhost TCP + bundled stdlib MCP server), and the 1.2x arm-reach solver multiplier. ## Arms: the corrected fix (2026-10-22) — replaces the earlier account The first version of this PR described a "mirror correction" to the PSX arms asset (commit `1dd3cf2`). **That diagnosis was wrong and that fix is reverted by `e908981` in this stack.** The asset was never mislabeled: its bone names agree with its skinned geometry (verified in Blender — bone and weighted-geometry positions match, and the mesh vertex data was byte-identical across the bad commit; only armature bone positions had been flipped, leaving the bind incoherent, while bone-position-only tests kept passing). The real defect was a frame mismatch in the mapping pipeline: the model is authored facing **+Z** (glTF convention — its elbows bow toward its back, -Z) while the player faces **-Z** (Godot). Fix, at the layer where it belongs: - `.blend` / `.glb` restored to the authored state; READMEs document the facing contract and the history. - `scenes/player_rig.tscn`: **ArmsView carries a 180-degree yaw**, landing the model's own left on the player's left. `update_arms()` converts controller world transforms through the skeleton's global transform, so the yaw is accounted for automatically. - `tests/arms_check.gd`: side guard now asserts **world-space** rest positions, plus a new **bone/geometry coherence guard** (each Hand bone must sit next to the geometry skinned to it). Negative control: against the old flipped asset, all four new assertions fail. ## Debug Cam (new in this update) The bridge gains a `debug_cam` method / MCP tool: a free Camera3D under the persistent Game root, independent of the player camera, placeable by orbit (target + yaw/pitch/distance) or explicit position + look_at. While enabled it is the viewport camera, so `screenshot` renders from it. Also, `solve_arm` now makes the view model visible (it hides whenever no controller is tracked — i.e. always in flat/headless runs). Verification workflow is now: pose with `solve_arm`, frame with `debug_cam`, check the screenshot with eyes — validated in-session with posed screenshots (wave / double-reach / T-pose, all uncrossed, hands on their own sides). ## Verified - Import clean on the standard 4.6.3 binary; smoke_menu / arms_check / addon_check PASS. - Bridge e2e PASS (Xvfb): pause proof via pausable tick counter, solve_arm lands Hand within tolerance and shows the model, debug_cam orbit/explicit/snapshot/disable, screenshot from the debug cam, MCP handshake with 13 tools. - **One-time, per checkout: delete `.godot/` before the first import** — Redot-stamped import cache (engine 26.2 / format 6) is refused by Godot. ## Still owed (yours) - In-headset check: uncrossed arms, 1.2x reach/elbow feel, hand-orientation constants (still a first guess). - TwoBoneIK3D remains unverified on Godot 4.6.3 (spike produced zero movement in every configuration; the hand-rolled solver stays the IK of record). Merge is yours, as always. **Validation correction (2026-10-22, Zach's review):** the first demo captures were shot from behind the model (orbit-yaw front/back was documented inverted — now fixed) and `solve_arm`'s default hand basis pinned the fingers skeleton-up, twisting the wrists in the captures. The bridge now defaults to a rest-relative hand basis (~7° wrist angle in a forward reach vs ~51° before), the snapshot reports fingertip positions, and the e2e asserts the wrist-angle invariant numerically. Corrected front-view captures and the full account are on issue #6. The arms fix itself (facing yaw + restored asset) was not implicated.
Low-level XR plumbing now comes from the maintained toolkit (MIT,
addons/godot-xr-tools, unmodified — see VENDORED.md); DEAD AIR builds
its own systems on top.

- PlayerRig: XRToolsPlayerBody capsule (child of Origin, as upstream
  assembles it) + XRToolsMovementDirect (left stick, 2.2 m/s carried
  over) + XRToolsMovementTurn (right stick). Hand-rolled provisional
  locomotion and the Pointer/Beam nodes are gone. Anchored (menu) and
  flat modes switch body/providers off and menu pointers on via
  _apply_state(); the rig's public API (place_at, set_anchored,
  set_flat_mode, hands, arms_view) is unchanged for Game/smoke tests.
- MenuPanel: collision is now a StaticBody3D on physics layer 21 (the
  layer xr-tools pointers raycast against) with a tiny body script
  forwarding XRToolsPointerEvents into the panel's existing _feed()
  translation; mouse/flat path unchanged. Pointer hover/lock behavior
  comes from the toolkit pointer.
- project.godot: XRToolsUserSettings + XRToolsRumbleManager autoloads
  (the addon's providers/pointers require them, as in upstream's own
  project).
- docs/external-resources.md: xr-tools entry + attribution ledger
  updated for actual vendored use.

Verified headless on Redot 26.2 Mono: import clean; smoke_menu,
arms_check, addon_check all PASS. In-headset check is the remaining
verification: menu lasers + trigger clicks, body collision/crouch in
the sandbox, snap turn feel.
Opening the project in the editor re-serializes project.godot and
drops comments, which leaves a permanent dirty diff on any machine
that runs the editor. Commit the stripped form so the file is stable.

The two explanations that lived in those comments, preserved here:
- driver/driver_name pins: kept for the Forward+ path — if the
  renderer returns to Forward+, the driver must be Vulkan
  (per-platform defaults drift to D3D12 on Windows).
- xr/openxr/default_action_map is REQUIRED for any controller input:
  without a default action map the runtime gets no interaction
  bindings — controllers never activate (no poses, buttons, or
  thumbsticks). Head tracking is unaffected. tests/smoke_menu.gd
  guards the setting.

No values changed.
The pose ACTION in the action map is named aim_pose, but the pose
the tracker exposes strips the suffix: controllers must ask for
"aim" (every reference project — godot-demos, godot-xr-tools
staging/scenes — uses "aim"/"grip" only). With "aim_pose" the
controllers never activated: frozen at the origin, function-pointer
lasers rendering at floor level, no tracking, buttons, or sticks.
Head tracking was unaffected (different path), which is why the
view always worked. Present since the skeleton — earlier headset
tests were always blocked earlier (session failures, then
render-only), so it never surfaced until now.

Adds a smoke-test guard: each rig controller's pose must resolve
against the action map's pose actions (suffix-stripped), so this
silent failure class is caught headless. Also documents the
--script harness's early-compile noise (autoload identifiers
register after the first compile pass; editor/game runs don't do
this) in the test header.
Issue 1 (crossed arms): the bone NAMES are the contract, so the
fix corrects the geometry, not the names. The PSX source model's
sides are authored in Blender space and the Blender->glTF axis
conversion flips them — the chain named .L rested on the
character's right (+X). The rig + mesh in source/player_arms.blend
were mirrored across the sagittal plane (scale X -1 applied, mesh
normals re-made consistent, signed volume identical) and
re-exported; names, weights, hierarchy untouched. Engine-verified:
Upper_Arm.L rests at x=-0.197, Hand.L at -0.660, facing and heights
unchanged. arms_check now asserts the invariant (.L at -X, .R at
+X) so a future model's conversion flip fails the check instead
of shipping. README documents the correction.

Issue 4 (hands stop short): ARM_LENGTH_SCALE = 1.2 in ArmsView
(Zach's Resonite arm-length configuration). The solver treats
both segments as 1.2x their rest lengths and the posed bone
scales follow via _aimed_global/_set_pose_from_global (which no
longer forces pose scale to 1), so the mesh stretches with the
reach. The 0.999 clamp still applies on the scaled reach, so
the elbow never fully locks. arms_check: a target at 110% of
rest reach is now reached, and the upper-arm pose scale reads
1.2 after a solve.

All suites pass headless: arms_check, smoke_menu, addon_check.
Zach's call after the sourced Godot-vs-Redot re-weigh (2026-10-21):
DEAD AIR is XR-first and XR is where Redot is structurally weakest
(no XR team, selective cherry-picks onto a Godot 4.5-generation
base, >=9 months measured lag on the IK stack); the migration is at
its lifetime cheapest while the codebase is a rig, a menu, and an
arm solver. Full record: docs/engine-migration.md.

Conversion surface was minimal:
- project.godot features rewritten to Godot's canonical
  ("4.6", "GL Compatibility"); no other settings changed.
- Every existing checkout needs ONE manual step: delete .godot/
  before the first Godot import — Redot's importer stamps converted
  resources with engine version 26.2 / format 6 and Godot refuses
  them. Committed sources are engine-neutral; only the local
  import cache is stamped. (Documented in the migration doc.)
- Vendored godot-xr-tools 4.5.2 runs as-is on 4.6.3 (VENDORED.md
  updated); re-syncing to upstream's 4.6.x line is a separate
  deliberate step.

Verified headless on Godot 4.6.3 Mono: import clean; smoke_menu,
arms_check, addon_check all PASS, suites unchanged.

Honest open item (in the migration doc): the 'working engine IK'
gain is NOT first-hand verified — TwoBoneIK3D produced zero bone
movement from code-driven configuration on 4.6.3 and 4.7.2 in
every tested setup (real + synthetic skeletons, headless + Xvfb)
while a probe modifier proved the pipeline runs. ArmsView's
hand-rolled IK remains the IK of record.
Zach's call after the sourced native-vs-.NET comparison: the
codebase contains zero C# (no .cs/.csproj/.sln anywhere), and
.NET's benefits are subsystem-local while its costs are
project-wide (.NET editor + SDK + MSBuild + export templates for
every contributor, Web export lost, mobile experimental, GC
pauses against the VR frame budget, marshalling on engine-call-
dense code). Nothing here needs it, so it goes.

- project.godot: [dotnet] section removed.
- README: pin is now 'Godot 4.6.3, standard build' (no .NET SDK
  required to open/run/test); the Mono rule is replaced by a
  scripting rule stating the .NET adoption bar (profiled compute
  subsystem in .NET data, or a named load-bearing NuGet library)
  and pointing native hot kernels at GDExtension, which works in
  the standard build.
- docs/engine-migration.md: follow-up section recording the
  reasoning.

Verified on the standard Godot 4.6.3 binary (no .NET SDK in the
environment): import clean with no mono cache generated;
smoke_menu, arms_check, addon_check all PASS.
Ports the validated MCP debug-bridge prototype into the editor:
enabling the plugin registers the DebugBridge autoload and adds a
'DA Bridge' bottom dock that can start/stop the bundled MCP server.
The project.godot autoload + editor_plugins entries committed here
are exactly the state the editor produces with the plugin enabled
(headless import initializes plugins but does not persist the
autoload registration, so it is seeded; the plugin's _enter_tree
re-asserts it and _exit_tree removes it when disabled in the GUI).

The bridge itself is unchanged from the prototype: opt-in via
DA_DEBUG_BRIDGE=1 / --debug-bridge, inert otherwise (no socket, no
processing); TCP 127.0.0.1 newline-JSON protocol; methods ping,
pause, resume, set_time_scale, snapshot, set_mode, place_rig,
solve_arm, release_arms, set_curl, screenshot, quit. The MCP server
(mcp_server/server.py, stdlib-only) forwards 12 tools 1:1.

Verified on Godot 4.6.3: mcp_server/test_e2e.py E2E PASS (game
under Xvfb through the plugin autoload; pause proof via the
pausable tick counter, solve_arm lands the Hand bone on target,
real 1152x648 screenshot, MCP initialize/list/call round-trip) and
smoke_menu / arms_check / addon_check all PASS with the plugin
present and the bridge inert.
Default ENGINE_BIN is now the standard (non-.NET) 4.6.3 binary,
and the .NET SDK environment vestiges (DOTNET_ROOT/PATH injection
from the Mono-editor era) are gone — the standard build needs no
SDK. E2E PASS on the standard binary; smoke suite PASS.
muse-lantern force-pushed godot-mcp-plugin from 05e71966a0 to 527c071e7e 2026-10-22 14:48:44 +00:00 Compare
A free Camera3D controlled through the bridge, independent of the
player camera. It lives under the persistent Game root (survives
mode/level swaps), becomes the viewport camera while enabled - so
screenshot renders from it - and hands the viewport back when
disabled. Placement is by orbit (target + yaw/pitch/distance) or
explicit position + look_at; state is reported in every snapshot.

This closes the verification hole behind the 1dd3cf2 arms mistake:
the bridge could pose arms and report bone positions, but there was
no way to LOOK at a posed model from a chosen angle. Now the
workflow is: solve_arm both arms, debug_cam to a front three-quarter
view, screenshot, eyeball it. E2E covers enable/orbit/explicit/
snapshot/disable plus a screenshot rendered from the debug cam;
MCP tool count is now 13.
ArmsView.update_arms hides the view model whenever no controller
is tracked, which is always in the flat/headless runs the bridge
exists for - so a bridge-posed arm was invisible in screenshots.
solve_arm now sets the view visible; release_arms hands control
back and the normal per-frame rules resume. E2E asserts the model
reports visible after a solve.
Zach's review of the demo screenshots caught two real defects in
the validation captures (the arms fix itself was fine):

1. Camera direction: the demo shots were taken from BEHIND the
   model. The orbit helper's doc claimed 'yaw 0 = in front of a
   -Z-facing target' - inverted: yaw is measured from the target's
   +Z side, and a Godot character faces -Z, so yaw 0 is its back
   and yaw 180 its front. Docs in the bridge and the MCP server
   now say so plainly. (Sides conclusions were unaffected - from
   behind, screen-left is still the model's left - but 'front'
   captions and palm readings were wrong.)

2. Twisted hands: solve_arm defaulted the Hand bone's global
   basis to Basis.IDENTITY in skeleton space, pinning the fingers
   to skeleton-up in every pose (~51 deg off the forearm in a
   forward reach - jazz hands, not a zombie reach). Default is
   now 'rest': a rest-relative basis (the hand's rest orientation
   rotated by the forearm's solve arc, two-pass), which measures
   ~7 deg off the forearm - the wrist stays straight, as at rest.
   'identity' remains available for reproducing old captures, and
   euler_deg still overrides.

Snapshot now includes Middle3 (fingertip) per side, and the e2e
asserts the wrist-angle invariant numerically (rest < 20 deg,
identity > 35 deg) instead of trusting screenshots alone.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dead-air/engine!5
No description provided.