Skip to content

BeaconColorPicker: fix VR/controller compatibility (panel flashing, d-pad, VR keyboard) - #1

Open
Gugarion wants to merge 1 commit into
shikyo13:masterfrom
Gugarion:vr-controller-fixes
Open

BeaconColorPicker: fix VR/controller compatibility (panel flashing, d-pad, VR keyboard)#1
Gugarion wants to merge 1 commit into
shikyo13:masterfrom
Gugarion:vr-controller-fixes

Conversation

@Gugarion

Copy link
Copy Markdown

Summary

Three independent bugs found and fixed while getting this mod working in a VR + gamepad co-op setup, plus controller navigation added to the picker panel itself once fixing one of them exposed it had none:

  1. Panel flashes shut immediately when opened in VR or with a controller. Cursor.lockState == Locked is a mouse-specific proxy for "player clicked back into the game world, close this menu" — it's already true from ordinary gameplay in VR (cursor is effectively always locked there) and for controller input (there's no mouse-look capture/release cycle for a controller press to trigger), so the panel closed within a frame of ever opening for either.

  2. The "+" button is invisible to d-pad/controller navigation. uGUI_PingEntry.GetSelectables() builds its navigable list directly and only from colorSelectors[], so a control that isn't a genuine member of that array is unreachable by controller no matter how its click handler is wired. Fixed by keeping the clone as a Toggle (instead of replacing it with a Button) and appending it onto colorSelectors, so the game's own unmodified GetSelectables() picks it up automatically. As a side benefit this also replaces RemoveAllListeners() — which the existing code comment already flagged as unreliable for clearing persistent/prefab-serialized listeners — with assigning a brand-new ToggleEvent, which is guaranteed clean.

  3. Clicking the hex field doesn't open a VR virtual keyboard. SubmersedVR (and likely other VR mods) hook their keyboard support as a Harmony postfix on TMPro.TMP_InputField.ActivateInputField specifically — the legacy InputField's ActivateInputField never triggers it, so the field still accepted clicks and physical-keyboard input (looked like it worked) but never opened a VR keyboard. Fixed by switching to TMP_InputField, the same primitive the base game's own UI and SubmersedVR's hook both expect.

Fixing #2 exposed that the panel's own controls (sliders, hex field, Apply/Close) had no controller navigation once opened at all, so this PR also adds:

  • A minimal uGUI_INavigableIconGrid (ColorPickerPanel.NavigableGrid) over the panel's controls, seized/restored around Show()/Hide(). GamepadInputModule already dispatches value changes based on the selected item's runtime type (right stick onto a Slider, UISubmit onto anything IPointerClickHandler), so this only needs to move focus between controls, not manipulate their values.
  • Self-healing grid re-assertion in Update(): uGUI_PDA.OnSelect resets GamepadInputModule's current grid on every PDA focus change (e.g. simply reopening the PDA while this panel is still up), with no awareness the panel might be open on top of it. Re-asserting every frame turned out more robust than trying to patch every place that might reset it.
  • uGUI_PDA_OnDeselect_Patch: closes the panel automatically when the PDA itself closes, rather than leaving it open with lost controller focus.
  • An explicit focus-ring indicator (ShowFocusRing/CreateFocusRing), since Unity's own Selectable ColorTint transition turned out unreliable as a focus indicator here — targetGraphic is assigned after AddComponent<Button>()/<Slider>() in CreateButton/CreateSlider, so the very first automatic state transition (which fires immediately on enable) runs against a null targetGraphic and never visibly recovers even once targetGraphic is set.

Every fix was diagnosed against this exact codebase, not guessed — several took 2-4 rounds each, mostly because a first theory got disproven by adding logging and reading the actual BepInEx log from a live session rather than trusting static analysis alone. Happy to share more detail on any of them if useful for review.

Caveat

Shared/ColorPicker/ColorPickerPanel.cs is linked into both BeaconColorPicker and BeaconColorPicker.BZ. I added the two new references (Unity.TextMeshPro, UnityEngine.VRModule) to both .csproj files for symmetry, and this compiles cleanly against a real Subnautica (non-BZ) install, but I don't have Below Zero installed to verify against — worth a check before merging if BZ is still maintained.

Test plan

  • Builds clean (dotnet build -c Release) against a real Subnautica install, 0 warnings/errors
  • Live-tested in-game against Subnautica v5.4.23.5 + BepInEx 5 + SubmersedVR + a physical Xbox controller: panel no longer flashes shut in VR or with a controller; "+" button reachable and usable via d-pad; hex field opens the SteamVR keyboard in VR; panel's own sliders/hex field/Apply/Close all navigable and usable via controller, with a visible focus indicator; panel closes cleanly with the PDA
  • Below Zero build not verified (see caveat above)

🤖 Generated with Claude Code

Three independent bugs, found and fixed while getting this mod working
in a VR + gamepad co-op setup:

1. Panel flashes shut immediately when opened in VR or with a
   controller. Cursor.lockState == Locked is a mouse-specific proxy for
   "player clicked back into the game world, close this menu" -- it's
   already true from ordinary gameplay in VR (cursor effectively always
   locked there) and for controller input (no mouse-look capture/release
   cycle for a controller press to trigger), so the panel closed within
   a frame of ever opening for either.

2. The "+" button is invisible to d-pad/controller navigation.
   uGUI_PingEntry.GetSelectables() builds its navigable list directly
   and only from colorSelectors[], so a control that isn't a genuine
   member of that array is unreachable by controller no matter how it's
   wired up otherwise. Fixed by keeping the clone as a Toggle (instead
   of replacing it with a Button) and appending it onto colorSelectors,
   so the game's own unmodified GetSelectables() picks it up
   automatically. As a side benefit, this also replaces the previous
   RemoveAllListeners() (which the code's own comment already flagged as
   unreliable for clearing persistent/prefab-serialized listeners) with
   assigning a brand-new ToggleEvent, which is guaranteed clean.

3. Clicking the hex field doesn't open a VR virtual keyboard.
   SubmersedVR (and likely other VR mods) hook their keyboard support as
   a Harmony postfix on TMPro.TMP_InputField.ActivateInputField
   specifically -- the legacy InputField's ActivateInputField never
   triggers it, so the field still accepted clicks/physical-keyboard
   input (looked like it worked) but never opened a VR keyboard. Fixed
   by switching to TMP_InputField, the same primitive the base game's
   own UI and SubmersedVR's hook both expect.

Also added, since fixing #2 exposed that the panel's own controls
(sliders, hex field, Apply/Close) had no controller navigation at all
once opened:

- A minimal uGUI_INavigableIconGrid (ColorPickerPanel.NavigableGrid)
  over the panel's controls, seized/restored around Show()/Hide().
  GamepadInputModule already dispatches value changes based on the
  selected item's runtime type (right stick onto a Slider, UISubmit
  onto anything IPointerClickHandler), so this only needs to move focus
  between controls, not manipulate their values.
- Self-healing grid re-assertion in Update(): uGUI_PDA.OnSelect resets
  GamepadInputModule's current grid on every PDA focus change (e.g.
  simply reopening the PDA while this panel is still up) with no
  awareness the panel might be open on top of it. Re-asserting every
  frame is more robust than trying to patch every place that might
  reset it.
- uGUI_PDA_OnDeselect_Patch: closes the panel automatically when the
  PDA itself closes, rather than leaving it open with lost focus.
- An explicit focus-ring indicator (ShowFocusRing/CreateFocusRing),
  since Unity's own Selectable ColorTint transition turned out
  unreliable here -- targetGraphic is assigned after
  AddComponent<Button>/<Slider>, so the very first automatic state
  transition (which fires immediately on enable) runs against a null
  targetGraphic and never visibly recovers even once targetGraphic is
  set later.

Every fix was diagnosed against this exact codebase (not guessed) --
several went through 2-4 rounds each, most of them because a first
theory was disproven by adding logging and reading BepInEx's own log
output from a live session rather than trusting static analysis alone.
Happy to share more detail on any of them if useful.

One caveat: Shared/ColorPicker/ColorPickerPanel.cs is linked into both
BeaconColorPicker and BeaconColorPicker.BZ. I added the two new
references (Unity.TextMeshPro, UnityEngine.VRModule) to both csproj
files for symmetry, and this compiles cleanly against a real
Subnautica (non-BZ) install, but I don't have Below Zero to verify
against -- worth a check before merging if BZ is still maintained.

Tested against Subnautica v5.4.23.5 with BepInEx 5, SubmersedVR, and a
physical Xbox controller.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Gugarion

Copy link
Copy Markdown
Author

Hope this helps! Tested with Submersed/Nitrox/Buddy System using flat mode (vrmode -none) using a controller or a mouse and with VR mode (vrmode -openvr).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant