Skip to content

Fix/destselect touch version - #1

Merged
deivid22srk merged 31 commits into
mainfrom
fix/destselect-touch-version
Aug 30, 2026
Merged

deivid22srk merged 31 commits into
mainfrom
fix/destselect-touch-version

Conversation

@deivid22srk

Copy link
Copy Markdown
Owner

No description provided.

…ot-plug auto-hide, layout editor

- touchhud.{h,cpp}: per-frame context publishing (Hidden/OnFoot/Climbing) + JNI input bridge
- HudBridge/HudController/TouchHudView: overlay with fades, multi-touch, safe areas,
  layout editor (long-press), opacity/low-contrast, haptics, JSON persistence
- Physical gamepad hot-plug hides/shows HUD automatically (real-gamepad source check)
- Stable committed keystore for debug+release (no more signature churn between CI runs)
The real PSX WORLDPTS door scripts (shadowing the compiled reconstructions
at runtime) use top-level opcode 0x3E with a humanoid-hash operand to walk
the player into the doorway via NisControl. The recomp only knew 0x3E as a
DoorFunc sub-command, so the interpreter hit the unknown-opcode path and
blocked forever at the same scriptPtr — door never opened (stuck NIS, actor
reload loop). Implement it as AttachToDoor with the door taken from
g_codeSnipThing (set by SetCodeSnip when the door collision triggers the
NIS).
…, title tap = Start

- AndroidInput: first-finger touchscreen tracking feeds androidbridge touch state
- glesDisplay (Android): IsMouseButtonDown/GetMousePosition return touch state, so
  PlatformInput/ActionInput/the engine's own menu code handle touch natively
  (entry hover, click-to-activate, sliders, scrollbars - full PC mouse parity)
- touchhud: tap on the 'press any button' title screen pulses Start
- HudController: FLAG_NOT_TOUCHABLE pass-through in Menu/Hidden contexts (taps
  reach the engine); gameplay contexts keep the overlay controls
…Input touch handling, glesDisplay mouse stubs)
…DEX_MASK (0xff00) instead of ACTION_MASK (0xff)
…t taps already activate, prevents double Confirm
…engine, movie skip button

- Multi-touch slots in androidbridge (4 pointers) driven by AndroidInput
- touchcontrols module: contextual controls (OnFoot full / Climbing reduced),
  fade transitions, virtual gamepad via androidbridge (same path as physical pad)
- Render via ScreenDraw + TextManager labels in Display::EndFrame (on top)
- Movie cutscene skip button (Start pulse) rendered+processed in PlayMovie loop
…orrect render-target state), center labels via TextAlign_Center/SCREEN_SCALE
Root cause of the invisible 'Pular' / A / B / X / Y labels:
ScreenDraw::DrawFilledCircle takes (centerX, centerY, radiusX, radiusY,
u0, v0, u1, v1, segments, r, g, b, a). The previous call passed
(x, y, r, r, 255, 255, 255, alpha) — the trailing 4 args landed in the
u0/v0/u1/v1 UV slots, leaving the color at its default (255,255,255,255)
(opaque white). White text on an opaque white circle was camouflaged,
so the labels looked like they were rendered behind the button.

Fixes:
- Add TouchColor struct + dark translucent palette (kFill, kRing, kKnob,
  kSkipFill, kSkipRing, kText) plus FadeAlpha() helper that scales the
  alpha channel by the current HUD fade.
- Add FillCircle()/RingCircle() wrappers that pass all 13 positional
  args explicitly, so the color actually reaches the (r,g,b,a) tail.
- Render() and RenderMovieSkip() now draw a dark filled circle + light
  ring + white label on top, matching the PPSSPP/AetherSX2 overlay look.
- Render() reorganized into two passes: pass 1 draws every visible
  control's background (filled circle + ring + joystick knob); pass 2
  draws the labels. Guarantees consistent ordering regardless of how
  the text backend's texture changes trigger pending-batch flushes.

Missing HUD on Destination Select (and other menus):
- touchcontrols::Update() only set s_visible for OnFoot / Climbing.
  GameState::LocationMenu -> HudContext::Menu -> HUD hidden.
- IsControlInteractive(ci, climbing) refactored to take HudContext;
  Menu context now exposes joystick (d-pad nav) + A (confirm) + B
  (back) + Start (pause/title-confirm). X/Y/LB/RB are hidden.
- Update() now includes HudContext::Menu in wantVisible, so the
  contextual HUD appears on the title screen and destination select.
Root causes found from user screenshots + log:

1. Character moved by itself / buttons dropped while held:
   LoadTouchPoints() returns a COMPACTED array (only active points), so
   indices shift whenever another finger lifts. Tracking joystick/buttons
   by array index made the joystick grab the wrong finger after a lift
   (e.g. the A button finger -> character walks alone) and made held
   buttons release spontaneously. Track pointers by their stable Android
   pointer id (TouchPoint.id) instead.

2. Skip button looked like an empty 'ball' during cutscenes:
   during PlayMovie the FE fonts are usually not loaded, so the text-only
   'Pular' label never rendered, leaving a bare circle. The button now
   draws a font-independent '>>' fast-forward glyph (two batched
   triangles) inside the circle, with 'Pular' as an optional caption
   under it when a font is available (MeasureString > 0 guard).

3. Skip button shown on non-skippable movies:
   PlayMovie(skippable=0) (logos, credits) rendered a fake skip button.
   Add SetMovieSkippable()/EndMovieSkip(); the button only shows and
   feeds input for skippable movies.

4. Virtual Start stuck down after a skip:
   DoSkip() posted Start down and never released it, leaking into the
   next state (instant pause / skipping the following movie). EndMovieSkip()
   releases it after the movie loop. A finger that already skipped is
   ignored until it lifts (no re-trigger while held).

5. Start button showed a broken '?' glyph:
   the '≡' label is not in the PSX bitmap font. Replace it with a 3-bar
   hamburger glyph drawn as batched rects (no font dependency).

6. Layout: Y button was clipped at the right edge (0.965 -> 0.950);
   L1 moved from mid-left screen to the action cluster (0.845, 0.455)
   above the diamond so combat inputs sit together; Start slightly
   larger (0.042 -> 0.048); skip button lowered (0.05 -> 0.075) so the
   circle is not cut off by the top edge.
The engine was treating the finger's touch-as-mouse position as
'desktop input', which cleared gamepadActive every frame the user
dragged a finger — including on the virtual joystick itself. The
virtual gamepad and the phantom 'keyboard/mouse' fought each other:
analog controls froze, prompts flipped to keyboard glyphs, buttons
got scrambled by the control-map remap, and menu drags switched to
cursor mode and gated pad navigation off.

- inputaction: on Android there is no desktop to switch to — the
  virtual gamepad is the primary device; touch-as-mouse no longer
  counts as keyboard/mouse input
- fecustommenumgr: mouse hover/cursor mode permanently off on
  touch-first devices (taps via ProcessTouchTaps, nav via pad)
- androidbridge: virtual pad connected from boot so analog mode and
  gamepad prompts are active from the first frame
- main loop: touch controls feed the pad BEFORE ServiceInput so
  touch hits land in the same frame, not one frame later
- AndroidInput: require full AINPUT_SOURCE_TOUCHSCREEN source so
  pointer-class devices (BT mouse/stylus) are not treated as fingers
Root cause of the intermittent 'analog stuck + all buttons dead':
FindTouchSlot() searched the COMPACTED LoadTouchPoints() array, so when
slots had holes a MOVE wrote a finger into the WRONG bridge slot,
duplicating its pointer id; the leftover copy stayed active forever
(zombie). Zombies held s_joyPointer (joystick frozen at its last
deflection = character moving alone) and filled all 4 slots until new
touches were silently dropped (every button dead).

- androidbridge: new real-slot helpers FindTouchSlotById /
  AcquireTouchSlot (self-heals stale same-id entries) /
  ClearAllTouchPoints; TouchPoint gains a per-acquire 'seq' so a
  recycled Android pointer id can never keep holding a control.
- AndroidInput: DOWN/MOVE/UP now address real slots; MOVE self-heals
  missed DOWNs; ACTION_CANCEL clears the WHOLE gesture (it aborts all
  pointers, not one).
- AndroidShell: reset touch state on PAUSE/STOP/LOST_FOCUS/TERM_WINDOW
  (Android never delivers the missing UPs when fingers are taken away).
- touchcontrols: match controls by (pointer id, seq); dedup duplicated
  ids defensively; movie-skip pointer also seq-guarded.
It was defined inside 'namespace {' (internal linkage) so AndroidShell.cpp
could not resolve it. Now declared up front and defined at file scope.
The PSX original ran every texture through SetTransparentTim
(LOADERS.CPP:460) at load time, which rewrote the magenta key colour
(0x7C1F) in the CLUTs to 0x0000 so the GPU skipped those texels in
every draw mode. That step was never ported, so raw magenta leaked to
the screen wherever textures use magenta as the transparency key:
- semi-transparent world geometry (windows, banners, decals, pickup
  sprites): the 3D shader only discarded magenta on opaque draws
  (uUseZeroTexelKey==0); blended draws used zero-texel keying only
- decoded VRAM pages used by BackG sprites (PsxToRGBA keyed only 0x0000)
- CELL images / TIM images / bitmap font palettes
  (PsxAbgr1555ToRgba8888 set alpha=255 for every colour != 0)

Fix: discard/key 0x7C1F (either STP bit) unconditionally in all four
VRAM/CLUT shaders (GLES + desktop GL, colour + shadow depth passes),
in PsxToRGBA (world.cpp) and in PsxAbgr1555ToRgba8888
(psxcolor_helpers.h), reproducing the hardware behaviour.
The Game Over screen (GameState::EndGameLoop, 'Press A to continue') had
no touch path: the HUD context was Hidden (no virtual buttons) and the
touch-as-mouse bridge was disabled on Android, so touch-only players
were stuck and needed a physical gamepad.

- ComputeContext: EndGameLoop now maps to HudContext::Menu, so the
  virtual joystick/A/B/Start set is available on the Game Over screen.
- PublishFrame: generalizes the title 'tap = Start' pulse into a
  'tap = the button this screen waits for' pulse:
    * TitleLoop            -> Start (unchanged)
    * EndGameLoop          -> A    (Game Over continue)
    * Intro legal splash   -> A    (AnyJustPressed skip; PC skips with
                                   any mouse button, touch had nothing)
  The confirm pulse is gated on the fade state so taps during the
  Game Over fade-out don't queue dead input.
- PublishFrame drains the pending position-carrying tap whenever no
  custom menu is active: previously it stayed queued and resurfaced as
  a phantom click in the next menu to open (taps on the Game Over
  screen clicking a Location menu entry right after the fade, HUD taps
  during gameplay clicking the pause menu opened later).
- game.h: public IsGameOverFading()/IsIntroSplashActive() queries.
Connecting a Bluetooth/USB pad now takes over as THE input: the on-screen
touch HUD (joystick, action buttons, movie-skip button) hides, and its
virtual gamepad input path is suspended so palm brushes on the glass
can't fight the real pad. When the last pad disconnects, the HUD comes
right back.

Detection (reliable connect AND disconnect):
- GameActivity now implements InputDevice.InputDeviceListener, registered
  in onCreate (rescan in onResume, unregistered in onDestroy). Every
  add/remove/change rescans all devices for real gamepads
  (SOURCE_GAMEPAD / SOURCE_JOYSTICK with actual stick axes; virtual
  devices, keyboards and DPAD-only TV remotes are excluded) and pushes
  the result through HudBridge.nativeSetPhysicalGamepad -> JNI ->
  androidbridge::SetPhysicalPadConnected (new atomic flag).
- Native self-confirm: gamepad-SOURCE key/motion events in
  AndroidInput.cpp also set the flag (DPAD-source remote navigation is
  excluded on purpose - remotes can't play analog, the HUD must stay).

Engine consumption:
- touchcontrols::Update(): wantVisible is gated on
  !IsPhysicalPadConnected(); ResetAll() now runs on EVERY visibility
  transition so fingers holding the joystick/buttons release immediately
  when the pad connects (instead of driving the game through the
  fade-out), and touch processing is fully skipped while hidden.
- UpdateMovieSkip()/RenderMovieSkip(): the on-screen skip button is part
  of the HUD - hidden and inert while a pad is connected (Start on the
  pad skips the movie).
- touchhud::PublishFrame(): the tap-to-continue pulses (title 'press
  start', Game Over 'press A', legal splash skip) are suspended while a
  pad is connected; the real pad's own Start/A drives those screens.
  Direct menu taps stay enabled with a pad connected (mixed input is
  convenient and harmless). [TouchDiag] log now includes physPad=.

The new flag is completely independent from PostGamepadConnected: the
engine's gamepad MODE still stays on (touch-first boot default), only
the HUD visibility follows the physical pad.
The device listener API lives on android.hardware.input.InputManager
(instance obtained via getSystemService(INPUT_SERVICE)), not on
android.view.InputDevice — the first CI run failed to find
InputDevice.InputDeviceListener / registerInputDeviceListener.

Same registration pattern as the (disabled) HudController: register in
onCreate with a null handler (callbacks on the main-thread Looper),
rescan in onResume, unregister in onDestroy.

Java now syntax/semantics-checked locally with ecj against minimal
Android API stubs (scripts/javastub) before pushing.
Xiaomi/MIUI exposes fingerprint readers (uinput-fpc, uinput-goodix,
uinput-synaptics, uinput-elan, uinput-vfs, uinput-atrus) as input
devices whose sources mask lies: SOURCE_GAMEPAD|SOURCE_JOYSTICK are set
while isVirtual() returns false (documented in Godot #47656, libgdx
#5596, Unity forums, GameMaker on Redmi). The pad-presence scan saw one
of these at boot on every Xiaomi/Redmi/POCO phone, so
IsPhysicalPadConnected() stuck true and the contextual touch HUD never
appeared.

Fix, engine + Java working together:
- GameActivity: skip devices named uinput-* in the physical-pad scan
  (the filter the whole industry converged on); push the ghost device
  ids into the engine alongside the presence flag.
- androidbridge: lock-free ghost-id blacklist (single Java writer,
  ids-then-count release ordering; input thread reads a consistent
  prefix).
- AndroidInput: swallow events from ghost device ids entirely - they
  can no longer confirm pad presence nor inject phantom buttons/axes
  (fingerprint swipe gestures would otherwise drive the virtual stick).
- HudBridge/touchhud: nativeSetGhostPadDeviceIds JNI bridge.

Real Bluetooth/USB pads are unaffected; TVs remotes/keyboards still
don't count; virtual pads (scrcpy-style streaming) still confirm
presence via events, which is correct.
…ame Over/title

The Level Results screen ("Press [A] to continue", GameState::EndLevelLoop)
had no touch path: it fell into ComputeContext()'s default -> HudContext::
Hidden, so the virtual buttons never showed and touch-only players were
stuck the same way they were on the Game Over screen before.

- touchhud ComputeContext: EndLevelLoop -> HudContext::Menu. The tally
  already accepts exactly what the navigation set provides - A held/
  pressed = fast-forward the count (ACTION_MENU_CONFIRM, hdTally::Update)
  and A/Start = confirm the "done" prompt (hdTally::DoDoneStuff) - and
  this state renders through MenuDraw -> MenuRender -> HUD::Display, so
  the on-screen controls draw automatically.
- touchhud PublishFrame: tap-anywhere = A pulse on EndLevelLoop, matching
  the Game Over / legal-splash behaviour (tap during the count fast-
  forwards it, tap on the prompt leaves the screen).
- game.cpp gsEndGameLoopState: call touchcontrols::Render() on Android.
  The Game Over screen renders on its own path (no HUD::Display), so the
  virtual A/Start buttons were ACTIVE but INVISIBLE; touch players only
  had the blind tap-anywhere pulse.
- game.cpp gsTitleLoopState (title menu active): same - the title menu
  renders via its own path, draw the touch controls explicitly like the
  pause menu does via CustomMenuRender.

Full state audit (all 27 GameStates + hdTally/hdDestSelect/movie loops):
every other screen already has a touch path - custom menus take direct
taps, PlayMovie has the skip button, press-start/Game Over/splash have
tap pulses, loads and transitions take no input. ErrorLoop is a latent
(non-touch) dead-end, unreachable through normal play.
Tapping an adjustable row (Frame Rate, Anti-Aliasing, Language, volume
sliders, ...) ran Confirm(), which is a no-op for most List bindings -
the value could only be changed with the virtual D-pad.

Adjustable rows now render their value flanked by '<' and '>' arrows.
The arrows' screen positions are measured during RenderCurrentPage()
(TextManager::MeasureString) and recorded as tap zones that
ProcessTouchTaps() consumes: tapping '<' steps the value down, '>'
steps it up (same Adjust() path as MENU_LEFT/RIGHT, including the
staged commit flow for Resolution/ScreenMode/MSAA). Tapping elsewhere
on the row keeps the previous select+confirm behaviour.

Applies to every adjustable row across Display, Sound, Controller and
Cheats pages (List, Slider and Toggle entries). Desktop/other builds
render and behave exactly as before.
Custom menu pages that direct touch already covers (title, options,
settings, cheats, confirms, pause, location) no longer show the
on-screen joystick/A/B/Start overlay - taps and < value > steppers
drive everything, so the pad was pure visual clutter.

The pad stays on pages that taps cannot fully operate: Key Bindings,
Mods and the save-slot tables (scrolling + non-generic row layouts
need virtual D-pad navigation), plus Changelog on updater builds.

Non-interactive Menu-context states (press-start, Game Over, Level
Results, transitions) also hide the pad: the existing tap-anywhere
Start/A pulses cover them, and a physical pad is unaffected.
Anti-Aliasing in Display Settings always reverted to Off on Android
because glesDisplay::SetMSAA was a no-op and GetMSAA() always returned
0; Display::SetMSAA() reads the value back, so every commit reset the
setting (and saved 0 to the config).

The EGL config cannot change while the GL context lives (that would
destroy every GL object), so AA is now done through an offscreen
multisampled framebuffer: each frame renders into a window-sized
multisampled FBO (RGBA8 + depth24/stencil8 renderbuffers) and is
resolved onto the window framebuffer with a blit right before the swap.
Sample count changes apply instantly from the settings menu; the
request is clamped to GL_MAX_SAMPLES with graceful fallback (halve
samples, then off). GetMSAA() reports the count actually applied so the
menu and the saved config stay honest.

Also tags the app as a game (android:appCategory="game" +
android:isGame) so launchers, Game Mode and Play Games recognise it.
…ings

Anti-Aliasing was one of the three staged bindings (Resolution, Screen
Mode, MSAA): the arrows only staged a pending value and it took a
separate confirm tap on the value text to commit. Touch users never
discovered the confirm step - they tapped the arrow, saw the new value,
and watched it snap back to Off as soon as they moved to another row or
backed out of the page, because every staging cleanup path
(MoveCursor/SetPage/row-tap/mouse-leave) discards pending values.

The staging made sense when changing AA could not take effect at runtime,
but the GLES backend now switches its multisample framebuffer on the fly
(safely clamped to what the device supports), so MSAA is applied the
moment the arrow is tapped - exactly like Frame Rate and V-Sync. The
displayed value is the read-back effective sample count, so the row
always shows what is really active, and ApplyValue persists it to the
config immediately. Resolution and Screen Mode keep their staged flow
(those changes can blank the display and still deserve a confirm step).

Desktop is unaffected beyond sharing the menu code: glDisplay::SetMSAA
only toggles GL_MULTISAMPLE.
Three fixes for the fresh-install experience (data cleared, no save):

1. Destination select (Level N / grade / dragon grid) is now tappable:
   the page is fully custom-drawn and its single Confirm entry is
   invisible, so the generic hit-test left a thin uncued strip in the
   middle of the panel - touch players read the screen as dead and got
   stuck (no pad shown either). A tap anywhere on the panel now selects;
   the bottom prompt row works as drawn (right half, where the 'B Back'
   prompt sits, backs out to the hub).

2. The contextual HUD never appeared on a fresh save's first hub visit.
   gsOpenFEState called Activate(MenuPage_Frontend) and then handed off
   through FE -> OpenLocationMenu -> QueueLevelLoad -> PrePlay -> Play
   without ever deactivating the menu; nothing in that chain Invokes it
   (its only Invoke sites are Intro/TitleLoop/Menu/LocationMenu). The
   stale 'active' flag made ComputeContext report the menu overlay, and
   since the menu-hiding rework that means Hidden - no HUD, no virtual
   joystick, hub unplayable until a pause/resume cycle. (Continues never
   hit this: the pending-load branch skips the Activate.) The dead
   Activate is removed, and ComputeContext now ignores an active menu in
   states that never Invoke one, so no future leak can hide the pad
   during gameplay.

3. Version 1.0.2 (versionCode 2) in gradle + GAME_VERSION.
@deivid22srk
deivid22srk merged commit a898685 into main Aug 30, 2026
2 checks passed
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