Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
215c314
Contextual HUD: touch overlay with game-context adaptation, gamepad h…
deivid22srk Aug 25, 2026
cbe3cef
HUD: use TYPE_APPLICATION_PANEL window (NativeActivity takeInputQueue…
deivid22srk Aug 25, 2026
221b1e4
Diagnostics: dump stuck Director script stream, DoorFunc unknown toke…
deivid22srk Aug 25, 2026
a899391
Fix stuck door NIS: implement top-level opcode 0x3E (AttachThingToDoor)
deivid22srk Aug 25, 2026
4b5c748
Engine-side touch for menus: touchscreen emulates mouse (hover+click)…
deivid22srk Aug 25, 2026
23b69e9
Disable Java HUD overlay; keep native touch->mouse for menus (Android…
deivid22srk Aug 25, 2026
eb2017a
Touch diagnostics: log touch->mouse chain state in PublishFrame (thro…
deivid22srk Aug 25, 2026
faea257
Fix menu touch: activate mouse mode when a button/finger is held down…
deivid22srk Aug 25, 2026
c11d01b
Add MenuMouse diagnostic: hit-test geometry and leftClick state in ge…
deivid22srk Aug 25, 2026
b3e917e
Direct touch for menus: tap position hit-tests entries and activates …
deivid22srk Aug 25, 2026
3393b7b
Fix touch ACTION_UP not processed: switch was masking with POINTER_IN…
deivid22srk Aug 25, 2026
2d25c3a
Touch menus: don't emulate mouse button (position-only hover) — direc…
deivid22srk Aug 25, 2026
9349e75
Fix title Start pulse: also set PostGamepadConnected(true) so the pul…
deivid22srk Aug 25, 2026
03c1dfe
Engine touch controls: virtual joystick + action buttons rendered in-…
deivid22srk Aug 25, 2026
0cd9d6b
Fix touch overlay visibility + skip button: render in HUD::Display (c…
deivid22srk Aug 25, 2026
3c4431e
Fix contextual HUD: visible labels + Menu context support
deivid22srk Aug 26, 2026
6e6a10b
Fix touch HUD: pointer-id tracking, movie skip glyph, Start bars
deivid22srk Aug 26, 2026
19f5503
Fix input device fight: touch misdetected as keyboard/mouse
deivid22srk Aug 26, 2026
0a0f100
Fix touch slot corruption: zombie pointers freezing the HUD
deivid22srk Aug 26, 2026
4409e97
Fix link error: move RechanAndroidResetTouch out of anonymous namespace
deivid22srk Aug 26, 2026
28a2bb1
Fix magenta chroma-key leak: port PSX SetTransparentTim behaviour
deivid22srk Aug 26, 2026
86f9d63
Add touch support to press-button screens (Game Over, legal splash)
deivid22srk Aug 26, 2026
47dede8
Hide contextual touch HUD while a physical gamepad is connected
deivid22srk Aug 26, 2026
330f1e2
Fix Java build: InputManager.InputDeviceListener, not InputDevice
deivid22srk Aug 26, 2026
d32f6cb
Filter Xiaomi ghost gamepads so the contextual HUD shows
deivid22srk Aug 27, 2026
d83a24d
Add touch support to Level Results screen (tally) + show buttons on G…
deivid22srk Aug 27, 2026
f500c11
Add touch steppers (< value >) to settings menus
deivid22srk Aug 27, 2026
4576956
Hide redundant virtual gamepad on touch-operable menus
deivid22srk Aug 27, 2026
bf71068
Implement runtime MSAA on GLES + mark app as game
deivid22srk Aug 27, 2026
32e313b
Apply Anti-Aliasing immediately instead of staging it in Display Sett…
deivid22srk Aug 27, 2026
1017ef1
Touch-select the destination screen, fix HUD on fresh save, bump 1.0.2
deivid22srk Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: build

on:
push:
branches: [main]
branches: [main, 'feature/**']
pull_request:
workflow_dispatch:

Expand Down
21 changes: 16 additions & 5 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,23 @@ android {
// Matches AGP 8.9's default and the preinstalled default on GitHub runners.
ndkVersion = "27.0.12077973"

// Stable sideload signing: the keystore is committed so APKs from any
// CI run (and local builds) share one signature and update in place.
signingConfigs {
create("sideload") {
storeFile = file("rechan.keystore")
storePassword = "rechan"
keyAlias = "rechan"
keyPassword = "rechan"
}
}

defaultConfig {
applicationId = "com.deivid22srk.rechan"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0.1"
versionCode = 2
versionName = "1.0.2"

ndk {
abiFilters += listOf("arm64-v8a")
Expand All @@ -57,12 +68,12 @@ android {
buildTypes {
debug {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("sideload")
}
release {
isMinifyEnabled = false
// Sideload distribution: sign releases with the debug key so the
// APK installs without extra setup.
signingConfig = signingConfigs.getByName("debug")
// Sideload distribution: same stable key for both build types.
signingConfig = signingConfigs.getByName("sideload")
}
}

Expand Down
Binary file added android/app/rechan.keystore
Binary file not shown.
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:appCategory="game"
android:isGame="true"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme">
Expand Down
167 changes: 167 additions & 0 deletions android/app/src/main/cpp/AndroidInput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
#include "pddi/gles/AndroidPlatform.h"
#include "pddi/pddidev.h"

// Defined at file scope below (after the anonymous namespace) so that
// AndroidShell.cpp can link against it.
void RechanAndroidResetTouch();

namespace {

constexpr int32_t kSourceGamepadMask =
Expand Down Expand Up @@ -70,6 +74,111 @@ int HatToButtons(float hatX, float hatY) {
return buttons;
}

// Pointer id of the first finger driving the emulated mouse (-1 = none).
// The multi-touch slots (androidbridge) track all fingers; the primary
// index is the first slot that was assigned (used for menu hover).
int32_t s_touchPointer = -1;

static void AdoptPrimaryFromSlots() {
// Primary finger went away — either adopt another live finger or reset
// the emulated mouse. Runs on the input thread only.
androidbridge::TouchPoint pts[androidbridge::kMaxTouchPoints];
const int32_t cnt = androidbridge::LoadTouchPoints(pts, androidbridge::kMaxTouchPoints);
if (cnt > 0) {
s_touchPointer = pts[0].id;
androidbridge::SetTouchMouse(pts[0].x, pts[0].y, false);
}
else {
s_touchPointer = -1;
androidbridge::SetTouchMouse(0.0f, 0.0f, false);
}
}

// (RechanAndroidResetTouch is defined at file scope below, after the
// anonymous namespace, so AndroidShell.cpp can link against it.)

static void HandleTouchEvent(AInputEvent* event) {
const int32_t action = AMotionEvent_getAction(event);
const int32_t actionCode = action & AMOTION_EVENT_ACTION_MASK;
const int32_t idx = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
>> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
const int32_t pointerId = AMotionEvent_getPointerId(event, idx);
const float x = AMotionEvent_getX(event, idx);
const float y = AMotionEvent_getY(event, idx);

switch (actionCode) {
case AMOTION_EVENT_ACTION_DOWN:
case AMOTION_EVENT_ACTION_POINTER_DOWN: {
// AcquireTouchSlot indexes the real bridge slots (reusing any
// stale same-id entry) — never the compacted LoadTouchPoints()
// view, which used to send a finger's data into the WRONG slot.
if (androidbridge::AcquireTouchSlot(pointerId, x, y) >= 0) {
if (s_touchPointer == -1) {
s_touchPointer = pointerId;
androidbridge::SetTouchMouse(x, y, false);
androidbridge::QueueTouchTap();
}
}
break;
}
case AMOTION_EVENT_ACTION_MOVE: {
const int32_t pointerCount = AMotionEvent_getPointerCount(event);
for (int32_t i = 0; i < pointerCount; ++i) {
const int32_t pid = AMotionEvent_getPointerId(event, i);
const float px = AMotionEvent_getX(event, i);
const float py = AMotionEvent_getY(event, i);
int32_t slot = androidbridge::FindTouchSlotById(pid);
if (slot < 0) {
// Missed the DOWN (slot pressure at the time): self-heal
// by claiming a slot now instead of dropping the finger.
slot = androidbridge::AcquireTouchSlot(pid, px, py);
if (slot < 0) continue;
}
androidbridge::SetTouchPoint(slot, pid, px, py, true);
}
// Keep the emulated mouse on the primary finger.
if (s_touchPointer >= 0) {
androidbridge::TouchPoint pts[androidbridge::kMaxTouchPoints];
const int32_t cnt = androidbridge::LoadTouchPoints(pts, androidbridge::kMaxTouchPoints);
bool found = false;
for (int32_t i = 0; i < cnt; ++i) {
if (pts[i].id == s_touchPointer) {
androidbridge::SetTouchMouse(pts[i].x, pts[i].y, false);
found = true;
break;
}
}
if (!found) {
AdoptPrimaryFromSlots();
}
}
break;
}
case AMOTION_EVENT_ACTION_UP:
case AMOTION_EVENT_ACTION_POINTER_UP: {
const int32_t slot = androidbridge::FindTouchSlotById(pointerId);
if (slot >= 0) {
androidbridge::QueueTouchTapPos(x, y);
androidbridge::ClearTouchPoint(slot);
if (pointerId == s_touchPointer) {
AdoptPrimaryFromSlots();
}
}
break;
}
case AMOTION_EVENT_ACTION_CANCEL: {
// CANCEL aborts the entire gesture — every finger is up as far as
// the app is concerned. Clearing only the indexed pointer used to
// strand the others as "zombie" slots (joystick stuck dragging,
// buttons dead once the zombies filled all 4 slots).
RechanAndroidResetTouch();
break;
}
default:
break;
}
}

void ApplyHatButtons(int32_t deviceId, int wanted) {
// Per-device diff state: a pad without HAT axes keeps emitting 0 and must
// not clear the D-pad held on a different controller.
Expand Down Expand Up @@ -117,6 +226,14 @@ bool HandleKeyEvent(AInputEvent* event) {
const int32_t keyCode = AKeyEvent_getKeyCode(event);
const bool down = AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_DOWN;

// Ghost devices (Xiaomi uinput fingerprint readers posing as gamepads,
// see AndroidPlatform.h): swallow their events entirely — no phantom
// buttons, no pad-presence confirm. The id blacklist is pushed by
// GameActivity's InputDevice scan.
if (androidbridge::IsGhostPadDeviceId(AInputEvent_getDeviceId(event))) {
return true;
}

if (IsMenuKey(keyCode)) {
androidbridge::PostGamepadConnected(true);
androidbridge::PostGamepadButton(GamepadButton::Start, down);
Expand All @@ -125,6 +242,13 @@ bool HandleKeyEvent(AInputEvent* event) {

if (const KeyButtonEntry* entry = FindKeyEntry(keyCode)) {
androidbridge::PostGamepadConnected(true);
// Gamepad-sourced button keys confirm a physical pad is present.
// Source must be GAMEPAD (not DPAD/KEYBOARD): TV remotes and the
// device's own hardware keys also send DPAD_* codes, but they can't
// drive analog gameplay and must not hide the touch HUD.
if ((AInputEvent_getSource(event) & AINPUT_SOURCE_GAMEPAD) != 0) {
androidbridge::SetPhysicalPadConnected(true);
}
if (entry->pddiButton >= 0) {
androidbridge::PostGamepadButton(entry->pddiButton, down);
}
Expand All @@ -143,12 +267,42 @@ bool HandleKeyEvent(AInputEvent* event) {

bool HandleMotionEvent(AInputEvent* event) {
const int32_t source = AInputEvent_getSource(event);

// Touchscreen: feed the mouse POSITION for hover only (button state is
// intentionally NOT emulated — direct taps handled by the menu's
// ProcessTouchTaps would otherwise double-activate alongside a mouse
// left-click Confirm). Multi-touch slots drive the on-screen controls.
// Require BOTH source bits (class-pointer + touchscreen): mice, styluses
// and touchpads share the pointer class bit, and treating their events
// as finger input made the engine "recognize a mouse" that doesn't
// exist while the virtual gamepad was also active.
if ((source & AINPUT_SOURCE_TOUCHSCREEN) == AINPUT_SOURCE_TOUCHSCREEN) {
HandleTouchEvent(event);
return true;
}

// Ghost devices (Xiaomi uinput fingerprint readers posing as gamepads/
// joysticks, see AndroidPlatform.h): swallow — their gesture events
// must neither confirm pad presence nor drive the virtual stick
// (swipes on the fingerprint sensor would walk the character by
// themselves).
if (androidbridge::IsGhostPadDeviceId(AInputEvent_getDeviceId(event))) {
return true;
}

if ((source & kSourceGamepadMask) == 0) {
return false;
}

androidbridge::PostGamepadConnected(true);

// Stick/trigger motion confirms a physical pad is present. DPAD-source
// motion (TV remote navigation) is excluded for the same reason as
// above: it can't play, so the touch HUD must stay available.
if ((source & (AINPUT_SOURCE_GAMEPAD | AINPUT_SOURCE_JOYSTICK)) != 0) {
androidbridge::SetPhysicalPadConnected(true);
}

const float lx = ClampAxis(AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_X, 0));
const float ly = ClampAxis(AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_Y, 0));
const float rx = ClampAxis(AMotionEvent_getAxisValue(event, AMOTION_EVENT_AXIS_Z, 0));
Expand Down Expand Up @@ -179,6 +333,19 @@ bool HandleMotionEvent(AInputEvent* event) {

} // namespace

// Drops every finger at once. ACTION_CANCEL aborts the WHOLE gesture (not
// just one pointer), and lifecycle events (pause, lost focus, surface going
// away) mean Android will never deliver the missing UPs — leaving the slots
// set would freeze the joystick at its last deflection and keep "ghost"
// fingers hogging every slot, which is exactly how the HUD died before.
// Defined at file scope (external linkage): AndroidShell.cpp calls this from
// its command handler.
void RechanAndroidResetTouch() {
s_touchPointer = -1;
androidbridge::ClearAllTouchPoints();
androidbridge::SetTouchMouse(0.0f, 0.0f, false);
}

bool RechanAndroidHandleInputEvent(AInputEvent* event) {
switch (AInputEvent_getType(event)) {
case AINPUT_EVENT_TYPE_KEY:
Expand Down
15 changes: 14 additions & 1 deletion android/app/src/main/cpp/AndroidShell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

extern int GameMain(int argc, char** argv);
bool RechanAndroidHandleInputEvent(AInputEvent* event);
void RechanAndroidResetTouch();

// Belt-and-suspenders anchor: referencing glue's entry symbol here guarantees
// the linker pulls native_app_glue's object out of the static archive (the
Expand Down Expand Up @@ -140,6 +141,10 @@ void HandleCommand(android_app* app, int32_t cmd) {
}
break;
case APP_CMD_TERM_WINDOW:
// Fingers still down when the surface goes away will never get
// their UP events — drop them or the virtual joystick stays
// deflected and ghost fingers hog the touch slots forever.
RechanAndroidResetTouch();
androidbridge::SetNativeWindow(nullptr);
// The EGL surface keeps its own strong reference to the window, so
// dropping ours here is safe; it only guards the handoff window.
Expand All @@ -149,9 +154,17 @@ void HandleCommand(android_app* app, int32_t cmd) {
}
break;
case APP_CMD_PAUSE:
case APP_CMD_STOP:
case APP_CMD_LOST_FOCUS:
// Same story as TERM_WINDOW: the system takes the fingers away
// (shade pulled, home gesture, incoming call) without per-pointer
// UPs. Reset the touch state so the HUD starts clean on resume.
RechanAndroidResetTouch();
// Backgrounded: silence the output device (the game loop parks on
// its own once the surface goes away).
AudioEngine::Suspend();
if (cmd == APP_CMD_PAUSE) {
AudioEngine::Suspend();
}
break;
case APP_CMD_RESUME:
AudioEngine::Resume();
Expand Down
Loading
Loading