From 2c018ea82f7795919a9014fd86b993133865c52c Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Thu, 25 Dec 2025 00:53:41 -0500 Subject: [PATCH] added virtual joystick and buttons for games that didn't have controls --- .../app/src/main/cpp/android_input_system.cpp | 110 +++++++++++++- .../app/src/main/cpp/android_input_system.h | 17 ++- android/app/src/main/cpp/native-lib.cpp | 8 ++ .../com/izzy2lost/super3/Super3Activity.kt | 78 ++++++++++ .../res/drawable/overlay_joystick_base.xml | 5 + .../res/drawable/overlay_joystick_knob.xml | 5 + .../src/main/res/layout/overlay_controls.xml | 134 ++++++++++++++++++ 7 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 android/app/src/main/res/drawable/overlay_joystick_base.xml create mode 100644 android/app/src/main/res/drawable/overlay_joystick_knob.xml diff --git a/android/app/src/main/cpp/android_input_system.cpp b/android/app/src/main/cpp/android_input_system.cpp index 0e856b0..7e8c53b 100644 --- a/android/app/src/main/cpp/android_input_system.cpp +++ b/android/app/src/main/cpp/android_input_system.cpp @@ -165,6 +165,16 @@ void AndroidInputSystem::ApplyConfig(const Util::Config::Node& config) m_touchShift3.a = keySc(get("InputGearShift3", "KEY_9"), SDL_SCANCODE_9); m_touchShift4.a = keySc(get("InputGearShift4", "KEY_0"), SDL_SCANCODE_0); m_touchShiftN.a = keySc(get("InputGearShiftN", "KEY_6"), SDL_SCANCODE_6); + + m_touchPunch.a = keySc(get("InputPunch", "KEY_A"), SDL_SCANCODE_A); + m_touchKick.a = keySc(get("InputKick", "KEY_S"), SDL_SCANCODE_S); + m_touchGuard.a = keySc(get("InputGuard", "KEY_D"), SDL_SCANCODE_D); + m_touchEscape.a = keySc(get("InputEscape", "KEY_F"), SDL_SCANCODE_F); + + m_touchSpikeShift.a = keySc(get("InputShift", "KEY_A"), SDL_SCANCODE_A); + m_touchSpikeBeat.a = keySc(get("InputBeat", "KEY_S"), SDL_SCANCODE_S); + m_touchSpikeCharge.a = keySc(get("InputCharge", "KEY_D"), SDL_SCANCODE_D); + m_touchSpikeJump.a = keySc(get("InputJump", "KEY_F"), SDL_SCANCODE_F); } bool AndroidInputSystem::InitializeSystem() @@ -319,6 +329,88 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) const float x = tf.x; const float y = tf.y; + // Fighting game action buttons (fingerId encoded from Java). + switch (tf.fingerId) + { + case 1110: SetKeys(m_touchPunch, down); return; + case 1111: SetKeys(m_touchKick, down); return; + case 1112: SetKeys(m_touchGuard, down); return; + case 1113: SetKeys(m_touchEscape, down); return; + case 1115: SetKeys(m_touchSpikeShift, down); return; + case 1116: SetKeys(m_touchSpikeBeat, down); return; + case 1117: SetKeys(m_touchSpikeCharge, down); return; + case 1118: SetKeys(m_touchSpikeJump, down); return; + default: break; + } + + // Virtual fighting stick: 8-way directional based on encoded x/y in [0..1], keyed by a fixed fingerId. + if (tf.fingerId == 1114) + { + auto releaseHeld = [&]() { + auto it = m_fingerHeldDir.find(tf.fingerId); + if (it != m_fingerHeldDir.end()) + { + SetKeys(it->second.primary, false); + SetKeys(it->second.secondary, false); + m_fingerHeldDir.erase(it); + } + }; + + if (!down) + { + releaseHeld(); + return; + } + + const float sx = std::clamp((x - 0.5f) * 2.0f, -1.0f, 1.0f); + const float sy = std::clamp((y - 0.5f) * 2.0f, -1.0f, 1.0f); + constexpr float deadzone = 0.25f; + if (std::abs(sx) < deadzone && std::abs(sy) < deadzone) + { + releaseHeld(); + return; + } + + constexpr float kPi = 3.14159265358979323846f; + float angle = std::atan2(-sy, sx); // y is down in normalized coords; invert for math-y up. + if (angle < 0.0f) + angle += 2.0f * kPi; + + const int oct = (int)std::floor((angle + (kPi / 8.0f)) / (kPi / 4.0f)) & 7; + DualScancode h{}; + DualScancode v{}; + switch (oct) + { + case 0: h = m_touchJoyRight; break; + case 1: h = m_touchJoyRight; v = m_touchJoyUp; break; + case 2: v = m_touchJoyUp; break; + case 3: h = m_touchJoyLeft; v = m_touchJoyUp; break; + case 4: h = m_touchJoyLeft; break; + case 5: h = m_touchJoyLeft; v = m_touchJoyDown; break; + case 6: v = m_touchJoyDown; break; + case 7: h = m_touchJoyRight; v = m_touchJoyDown; break; + default: break; + } + + releaseHeld(); + HeldDirKeys held{}; + if (h.a != SDL_SCANCODE_UNKNOWN || h.b != SDL_SCANCODE_UNKNOWN) + held.primary = h; + if (v.a != SDL_SCANCODE_UNKNOWN || v.b != SDL_SCANCODE_UNKNOWN) + { + if (held.primary.a == SDL_SCANCODE_UNKNOWN && held.primary.b == SDL_SCANCODE_UNKNOWN) + held.primary = v; + else + held.secondary = v; + } + if (held.primary.a != SDL_SCANCODE_UNKNOWN || held.primary.b != SDL_SCANCODE_UNKNOWN) + SetKeys(held.primary, true); + if (held.secondary.a != SDL_SCANCODE_UNKNOWN || held.secondary.b != SDL_SCANCODE_UNKNOWN) + SetKeys(held.secondary, true); + m_fingerHeldDir[tf.fingerId] = held; + return; + } + // Virtual manual shifter (racing games): encoded in tf.x/tf.y from Java and keyed by a fixed fingerId. if ((m_virtualShifterShift4 || m_virtualShifterUpDown) && tf.fingerId == 1108) { @@ -487,7 +579,8 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) auto it = m_fingerHeldDir.find(tf.fingerId); if (it != m_fingerHeldDir.end()) { - SetKeys(it->second, false); + SetKeys(it->second.primary, false); + SetKeys(it->second.secondary, false); m_fingerHeldDir.erase(it); } return; @@ -507,13 +600,21 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) if (dir.a != SDL_SCANCODE_UNKNOWN || dir.b != SDL_SCANCODE_UNKNOWN) { - m_fingerHeldDir[tf.fingerId] = dir; - SetKeys(dir, true); + HeldDirKeys held{}; + held.primary = dir; + m_fingerHeldDir[tf.fingerId] = held; + SetKeys(held.primary, true); } } void AndroidInputSystem::HandleTouchMotion(const SDL_TouchFingerEvent& tf) { + if (tf.fingerId == 1114) + { + HandleTouch(tf, true); + return; + } + if (m_virtualShifterShift4 && tf.fingerId == 1108) { // Avoid accidental neutral when passing through center during motion. @@ -582,7 +683,8 @@ void AndroidInputSystem::HandleTouchMotion(const SDL_TouchFingerEvent& tf) } // Release previous direction and compute a new one. - SetKeys(it->second, false); + SetKeys(it->second.primary, false); + SetKeys(it->second.secondary, false); m_fingerHeldDir.erase(it); HandleTouch(tf, true); } diff --git a/android/app/src/main/cpp/android_input_system.h b/android/app/src/main/cpp/android_input_system.h index 7c9721a..a39010f 100644 --- a/android/app/src/main/cpp/android_input_system.h +++ b/android/app/src/main/cpp/android_input_system.h @@ -73,6 +73,11 @@ private: SDL_Scancode b = SDL_SCANCODE_UNKNOWN; }; + struct HeldDirKeys { + DualScancode primary{}; + DualScancode secondary{}; + }; + void RefreshControllers(); void CloseControllers(); @@ -126,6 +131,16 @@ private: DualScancode m_touchShift4{SDL_SCANCODE_0, SDL_SCANCODE_UNKNOWN}; DualScancode m_touchShiftN{SDL_SCANCODE_6, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchPunch{SDL_SCANCODE_A, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchKick{SDL_SCANCODE_S, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchGuard{SDL_SCANCODE_D, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchEscape{SDL_SCANCODE_F, SDL_SCANCODE_UNKNOWN}; + + DualScancode m_touchSpikeShift{SDL_SCANCODE_A, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchSpikeBeat{SDL_SCANCODE_S, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchSpikeCharge{SDL_SCANCODE_D, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchSpikeJump{SDL_SCANCODE_F, SDL_SCANCODE_UNKNOWN}; + bool m_gunTouchEnabled = false; SDL_FingerID m_gunFinger = 0; bool m_gunFingerActive = false; @@ -152,7 +167,7 @@ private: std::vector m_controllers; std::vector m_keys; // indexed by SDL_Scancode - std::unordered_map m_fingerHeldDir; + std::unordered_map m_fingerHeldDir; std::unordered_map m_fingerHeldKey; std::unordered_map m_pulseUntilMs; }; diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index c9aa4b7..1533e19 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -267,6 +267,14 @@ struct Super3Host { ensureKeyboardFallback("InputSteeringRight", "KEY_RIGHT"); ensureKeyboardFallback("InputAccelerator", "KEY_W"); ensureKeyboardFallback("InputBrake", "KEY_S"); + ensureKeyboardFallback("InputPunch", "KEY_A"); + ensureKeyboardFallback("InputKick", "KEY_S"); + ensureKeyboardFallback("InputGuard", "KEY_D"); + ensureKeyboardFallback("InputEscape", "KEY_F"); + ensureKeyboardFallback("InputShift", "KEY_A"); + ensureKeyboardFallback("InputBeat", "KEY_S"); + ensureKeyboardFallback("InputCharge", "KEY_D"); + ensureKeyboardFallback("InputJump", "KEY_F"); inputSystem.ApplyConfig(config); } diff --git a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt index d8e11b7..1c718ea 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt @@ -13,6 +13,7 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageButton import android.widget.LinearLayout import android.widget.RelativeLayout @@ -139,6 +140,16 @@ class Super3Activity : SDLActivity() { gamesXml.isNotBlank() && GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("gun1", "gun2", "analog_gun1", "analog_gun2")) + val isFighting = + game.isNotBlank() && + gamesXml.isNotBlank() && + GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("fighting")) + + val isSpikeout = + game.isNotBlank() && + gamesXml.isNotBlank() && + GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("spikeout")) + val shifterEnabled = getSharedPreferences("super3_prefs", MODE_PRIVATE) .getBoolean("overlay_shifter_enabled", false) val showShifter = shifterEnabled && (hasShift4 || hasShiftUpDown) @@ -155,6 +166,12 @@ class Super3Activity : SDLActivity() { overlay.findViewById(R.id.overlay_reload)?.visibility = if (isGunGame) View.VISIBLE else View.GONE + overlay.findViewById(R.id.overlay_fight_stick)?.visibility = + if (isFighting || isSpikeout) View.VISIBLE else View.GONE + + overlay.findViewById(R.id.overlay_fight_buttons)?.visibility = + if (isFighting || isSpikeout) View.VISIBLE else View.GONE + fun nativeTouch(action: Int, fingerId: Int, x: Float, y: Float, p: Float = 1.0f) { SDLActivity.onNativeTouch(0, fingerId, action, x, y, p) } @@ -216,6 +233,67 @@ class Super3Activity : SDLActivity() { bindMomentary(R.id.overlay_reload, fingerId = 1109, x = 0.90f, y = 0.90f) } + if (isFighting || isSpikeout) { + val baseId = + if (isSpikeout) { + 1115 + } else { + 1110 + } + + bindHeld(R.id.overlay_fight_punch, fingerId = baseId + 0, x = 0.90f, y = 0.55f) + bindHeld(R.id.overlay_fight_kick, fingerId = baseId + 1, x = 0.82f, y = 0.65f) + bindHeld(R.id.overlay_fight_guard, fingerId = baseId + 2, x = 0.90f, y = 0.45f) + bindHeld(R.id.overlay_fight_escape, fingerId = baseId + 3, x = 0.82f, y = 0.35f) + + val stick = overlay.findViewById(R.id.overlay_fight_stick) + val knob = overlay.findViewById(R.id.overlay_fight_stick_knob) + stick?.setOnTouchListener { v, ev -> + val w = v.width.toFloat().coerceAtLeast(1f) + val h = v.height.toFloat().coerceAtLeast(1f) + val cx = w / 2f + val cy = h / 2f + val dx = ((ev.x - cx) / cx).coerceIn(-1f, 1f) + val dy = ((ev.y - cy) / cy).coerceIn(-1f, 1f) + + val radiusPx = (w.coerceAtMost(h) * 0.32f).coerceAtLeast(1f) + knob?.translationX = dx * radiusPx + knob?.translationY = dy * radiusPx + + val encodedX = ((dx + 1f) / 2f).coerceIn(0f, 1f) + val encodedY = ((dy + 1f) / 2f).coerceIn(0f, 1f) + + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + v.alpha = 0.90f + nativeTouch(MotionEvent.ACTION_DOWN, 1114, encodedX, encodedY) + true + } + MotionEvent.ACTION_MOVE -> { + nativeTouch(MotionEvent.ACTION_MOVE, 1114, encodedX, encodedY) + true + } + MotionEvent.ACTION_UP -> { + v.alpha = 1.0f + knob?.translationX = 0f + knob?.translationY = 0f + nativeTouch(MotionEvent.ACTION_UP, 1114, 0.5f, 0.5f) + true + } + MotionEvent.ACTION_CANCEL -> { + v.alpha = 1.0f + knob?.translationX = 0f + knob?.translationY = 0f + nativeTouch(MotionEvent.ACTION_UP, 1114, 0.5f, 0.5f) + true + } + else -> true + } + } + } else { + overlay.findViewById(R.id.overlay_fight_stick)?.setOnTouchListener(null) + } + if (isRacing) { // Match the native pedal zone (right-middle), independent of UI placement. bindHeld(R.id.overlay_gas, fingerId = 1103, x = 0.85f, y = 0.35f) diff --git a/android/app/src/main/res/drawable/overlay_joystick_base.xml b/android/app/src/main/res/drawable/overlay_joystick_base.xml new file mode 100644 index 0000000..1672cef --- /dev/null +++ b/android/app/src/main/res/drawable/overlay_joystick_base.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/drawable/overlay_joystick_knob.xml b/android/app/src/main/res/drawable/overlay_joystick_knob.xml new file mode 100644 index 0000000..e0af797 --- /dev/null +++ b/android/app/src/main/res/drawable/overlay_joystick_knob.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/layout/overlay_controls.xml b/android/app/src/main/res/layout/overlay_controls.xml index 6ce907a..69ea219 100644 --- a/android/app/src/main/res/layout/overlay_controls.xml +++ b/android/app/src/main/res/layout/overlay_controls.xml @@ -197,4 +197,138 @@ android:src="@drawable/gaspedal" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +