diff --git a/android/app/src/main/cpp/android_input_system.cpp b/android/app/src/main/cpp/android_input_system.cpp index 8da02d0..0e856b0 100644 --- a/android/app/src/main/cpp/android_input_system.cpp +++ b/android/app/src/main/cpp/android_input_system.cpp @@ -57,6 +57,13 @@ void AndroidInputSystem::SetVirtualWheelEnabled(bool enabled) m_wheelFingerActive = false; m_wheelFinger = 0; m_virtualJoyX = 0; + m_virtualJoyY = 0; + if (enabled && !m_virtualAnalogGunEnabled) + { + m_virtualJoyDetails.hasAxis[AXIS_Y] = false; + std::strncpy(m_virtualJoyDetails.name, "Touch Wheel", MAX_NAME_LENGTH); + m_virtualJoyDetails.name[MAX_NAME_LENGTH] = '\0'; + } } void AndroidInputSystem::SetVirtualShifterMode(bool shift4, bool shiftUpDown) @@ -66,11 +73,33 @@ void AndroidInputSystem::SetVirtualShifterMode(bool shift4, bool shiftUpDown) m_lastVirtualGear = -1; } +void AndroidInputSystem::SetVirtualAnalogGunEnabled(bool enabled) +{ + if (m_virtualAnalogGunEnabled == enabled) + return; + m_virtualAnalogGunEnabled = enabled; + m_virtualJoyDetails.hasAxis[AXIS_Y] = enabled; + if (enabled && !m_virtualWheelEnabled) + std::strncpy(m_virtualJoyDetails.name, "Touch Gun", MAX_NAME_LENGTH); + else if (!enabled && m_virtualWheelEnabled) + std::strncpy(m_virtualJoyDetails.name, "Touch Wheel", MAX_NAME_LENGTH); + else + std::strncpy(m_virtualJoyDetails.name, "Touch Controls", MAX_NAME_LENGTH); + m_virtualJoyDetails.name[MAX_NAME_LENGTH] = '\0'; + m_virtualJoyX = 0; + m_virtualJoyY = 0; +} + bool AndroidInputSystem::UseVirtualWheel() const { return m_virtualWheelEnabled && m_controllers.empty(); } +bool AndroidInputSystem::UseVirtualJoystick() const +{ + return m_controllers.empty() && (m_virtualWheelEnabled || m_virtualAnalogGunEnabled); +} + void AndroidInputSystem::SetVirtualSteerFromEncoded(float encodedX) { // encodedX comes from Java as [(steer+1)/2], where steer is in [-1,1]. @@ -87,6 +116,14 @@ void AndroidInputSystem::SetVirtualSteerFromEncoded(float encodedX) m_virtualJoyX = (int)std::lround(std::clamp(signedScaled, -1.0f, 1.0f) * 32767.0f); } +void AndroidInputSystem::SetVirtualJoyFromNormalized(float x, float y) +{ + 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); + m_virtualJoyX = (int)std::lround(sx * 32767.0f); + m_virtualJoyY = (int)std::lround(sy * 32767.0f); +} + void AndroidInputSystem::ApplyConfig(const Util::Config::Node& config) { auto get = [&](const char* key, const char* fallback) -> std::string { @@ -146,6 +183,7 @@ bool AndroidInputSystem::InitializeSystem() m_wheelFingerActive = false; m_wheelFinger = 0; m_virtualJoyX = 0; + m_virtualJoyY = 0; m_lastVirtualGear = -1; SDL_GameControllerEventState(SDL_ENABLE); @@ -372,6 +410,21 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) if (x > 0.75f && y < 0.25f) { PulseKeys(m_touchTest, 120); return; } } + // Lightgun reload button: treat a dedicated synthetic fingerId as offscreen/reload, + // independent of the aiming touch. + if (m_gunTouchEnabled && tf.fingerId == 1109) + { + if (down) + { + // Most lightgun games treat "reload" as offscreen + trigger. + // If the player is already holding the trigger (aim finger active), only pulse offscreen. + PulseMouseButton(2, 140); // right = reload/offscreen + if (!m_gunFingerActive) + PulseMouseButton(0, 80); // left = trigger pulse (only when not already held) + } + return; + } + // Lightgun/analog-gun games: use the touchscreen as an absolute "mouse" so // existing MOUSE_XAXIS/MOUSE_YAXIS + MOUSE_LEFT_BUTTON mappings work without // a physical mouse. @@ -384,13 +437,10 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) m_gunFingerActive = true; m_gunFinger = tf.fingerId; SetMousePosFromNormalized(x, y); + if (m_virtualAnalogGunEnabled) + SetVirtualJoyFromNormalized(x, y); SetMouseButton(0, true); // left = trigger (held) } - else - { - // Second touch while aiming: treat as offscreen/reload (right button pulse). - PulseMouseButton(2, 120); - } } else { @@ -509,6 +559,8 @@ void AndroidInputSystem::HandleTouchMotion(const SDL_TouchFingerEvent& tf) if (m_gunFingerActive && tf.fingerId == m_gunFinger) { SetMousePosFromNormalized(tf.x, tf.y); + if (m_virtualAnalogGunEnabled) + SetVirtualJoyFromNormalized(tf.x, tf.y); } return; } @@ -556,6 +608,15 @@ int AndroidInputSystem::GetMouseAxisValue(int mseNum, int axisNum) if (mseNum != ANY_MOUSE && mseNum != 0) return 0; + // For analog-gun games, prefer the virtual joystick (JOY1_XAXIS/JOY1_YAXIS). + // Keep the virtual mouse centered so MOUSE_XAXIS/MOUSE_YAXIS mappings don't override JOY mappings. + if (m_virtualAnalogGunEnabled && (axisNum == AXIS_X || axisNum == AXIS_Y)) + { + const unsigned extent = (axisNum == AXIS_X) ? m_dispW : m_dispH; + const unsigned origin = (axisNum == AXIS_X) ? m_dispX : m_dispY; + return (int)(origin + extent / 2); + } + switch (axisNum) { case AXIS_X: return m_mouseX; @@ -684,14 +745,14 @@ void AndroidInputSystem::RefreshControllers() int AndroidInputSystem::GetNumJoysticks() { - if (UseVirtualWheel()) + if (UseVirtualJoystick()) return 1; return static_cast(m_controllers.size()); } const JoyDetails* AndroidInputSystem::GetJoyDetails(int joyNum) { - if (UseVirtualWheel()) + if (UseVirtualJoystick()) { if (joyNum == ANY_JOYSTICK || joyNum == 0) return &m_virtualJoyDetails; @@ -766,10 +827,12 @@ bool AndroidInputSystem::ButtonPressedFor(const ControllerState& c, int butNum) int AndroidInputSystem::GetJoyAxisValue(int joyNum, int axisNum) { - if (UseVirtualWheel()) + if (UseVirtualJoystick()) { if (axisNum == AXIS_X) return m_virtualJoyX; + if (axisNum == AXIS_Y && m_virtualJoyDetails.hasAxis[AXIS_Y]) + return m_virtualJoyY; return 0; } diff --git a/android/app/src/main/cpp/android_input_system.h b/android/app/src/main/cpp/android_input_system.h index d57f23d..7c9721a 100644 --- a/android/app/src/main/cpp/android_input_system.h +++ b/android/app/src/main/cpp/android_input_system.h @@ -23,6 +23,7 @@ public: void SetGunTouchEnabled(bool enabled); void SetVirtualWheelEnabled(bool enabled); void SetVirtualShifterMode(bool shift4, bool shiftUpDown); + void SetVirtualAnalogGunEnabled(bool enabled); // Called from the SDL event loop (main thread). void HandleEvent(const SDL_Event& ev); @@ -76,7 +77,9 @@ private: void CloseControllers(); bool UseVirtualWheel() const; + bool UseVirtualJoystick() const; void SetVirtualSteerFromEncoded(float encodedX); + void SetVirtualJoyFromNormalized(float x, float y); void SetKey(SDL_Scancode sc, bool down); void PulseKey(SDL_Scancode sc, uint32_t durationMs); @@ -131,12 +134,15 @@ private: SDL_FingerID m_wheelFinger = 0; bool m_wheelFingerActive = false; int m_virtualJoyX = 0; + int m_virtualJoyY = 0; JoyDetails m_virtualJoyDetails{}; bool m_virtualShifterShift4 = false; bool m_virtualShifterUpDown = false; int m_lastVirtualGear = -1; + bool m_virtualAnalogGunEnabled = false; + MouseDetails m_mouseDetails{}; int m_mouseX = 0; int m_mouseY = 0; diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index 9c773bd..54c98f0 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -316,6 +316,9 @@ struct Super3Host { (game.inputs & (Game::INPUT_GUN1 | Game::INPUT_GUN2 | Game::INPUT_ANALOG_GUN1 | Game::INPUT_ANALOG_GUN2)) != 0; inputSystem.SetGunTouchEnabled(gunGame); + const bool analogGunGame = (game.inputs & (Game::INPUT_ANALOG_GUN1 | Game::INPUT_ANALOG_GUN2)) != 0; + inputSystem.SetVirtualAnalogGunEnabled(analogGunGame); + const bool vehicleGame = (game.inputs & (Game::INPUT_VEHICLE | Game::INPUT_HARLEY)) != 0; inputSystem.SetVirtualWheelEnabled(vehicleGame); diff --git a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt index e52b408..31f35e3 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/MainActivity.kt @@ -125,6 +125,8 @@ class MainActivity : AppCompatActivity() { btnResolutionMatchDevice = headerView.findViewById(R.id.btn_resolution_match_device) btnWidescreen = headerView.findViewById(R.id.btn_widescreen) btnWideBackground = headerView.findViewById(R.id.btn_wide_background) + val btnShowTouchControls: MaterialButton = headerView.findViewById(R.id.btn_show_touch_controls) + val btnShowShifterOverlay: MaterialButton = headerView.findViewById(R.id.btn_show_shifter_overlay) gamesAdapter = GamesAdapter { item -> if (!item.launchable) { @@ -146,6 +148,28 @@ class MainActivity : AppCompatActivity() { bindVideoSettingsUi() + btnShowTouchControls.isChecked = prefs.getBoolean("overlay_controls_enabled", true) + btnShowTouchControls.setOnClickListener { + val enabled = btnShowTouchControls.isChecked + prefs.edit().putBoolean("overlay_controls_enabled", enabled).apply() + Toast.makeText( + this, + if (enabled) "Touch controls enabled" else "Touch controls hidden", + Toast.LENGTH_SHORT, + ).show() + } + + btnShowShifterOverlay.isChecked = prefs.getBoolean("overlay_shifter_enabled", false) + btnShowShifterOverlay.setOnClickListener { + val enabled = btnShowShifterOverlay.isChecked + prefs.edit().putBoolean("overlay_shifter_enabled", enabled).apply() + Toast.makeText( + this, + if (enabled) "Shifter overlay enabled" else "Shifter overlay disabled", + Toast.LENGTH_SHORT, + ).show() + } + runCatching { searchView.setupWithSearchBar(searchBar) } searchBar.setOnClickListener { searchView.show() 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 0428e4a..8d8336a 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.widget.ImageButton import android.widget.LinearLayout import android.widget.RelativeLayout +import com.google.android.material.button.MaterialButton import java.io.File import kotlin.concurrent.thread import org.libsdl.app.SDLActivity @@ -42,6 +43,10 @@ class Super3Activity : SDLActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val overlaysEnabled = getSharedPreferences("super3_prefs", MODE_PRIVATE) + .getBoolean("overlay_controls_enabled", true) + if (!overlaysEnabled) return + val root = SDLActivity.getContentView() as? RelativeLayout ?: return if (overlayView != null) return @@ -72,7 +77,14 @@ class Super3Activity : SDLActivity() { gamesXml.isNotBlank() && GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("shiftupdown")) - val showShifter = hasShift4 || hasShiftUpDown + val isGunGame = + game.isNotBlank() && + gamesXml.isNotBlank() && + GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("gun1", "gun2", "analog_gun1", "analog_gun2")) + + val shifterEnabled = getSharedPreferences("super3_prefs", MODE_PRIVATE) + .getBoolean("overlay_shifter_enabled", false) + val showShifter = shifterEnabled && (hasShift4 || hasShiftUpDown) overlay.findViewById(R.id.overlay_pedals)?.visibility = if (isRacing) View.VISIBLE else View.GONE @@ -83,6 +95,9 @@ class Super3Activity : SDLActivity() { overlay.findViewById(R.id.overlay_shifter)?.visibility = if (isRacing && showShifter) View.VISIBLE else View.GONE + overlay.findViewById(R.id.overlay_reload)?.visibility = + if (isGunGame) 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) } @@ -140,6 +155,9 @@ class Super3Activity : SDLActivity() { bindMomentary(R.id.overlay_start, fingerId = 1102, x = 0.50f, y = 0.90f) bindMomentary(R.id.overlay_service, fingerId = 1105, x = 0.10f, y = 0.10f) bindMomentary(R.id.overlay_test, fingerId = 1106, x = 0.90f, y = 0.10f) + if (isGunGame) { + bindMomentary(R.id.overlay_reload, fingerId = 1109, x = 0.90f, y = 0.90f) + } if (isRacing) { // Match the native pedal zone (right-middle), independent of UI placement. @@ -185,7 +203,8 @@ class Super3Activity : SDLActivity() { } val shifter = overlay.findViewById(R.id.overlay_shifter) - shifter?.setOnTouchListener { v, ev -> + if (showShifter) { + shifter?.setOnTouchListener { v, ev -> val w = v.width.toFloat().coerceAtLeast(1f) val h = v.height.toFloat().coerceAtLeast(1f) val cx = w / 2f @@ -229,6 +248,9 @@ class Super3Activity : SDLActivity() { else -> true } } + } else { + shifter?.setOnTouchListener(null) + } } } diff --git a/android/app/src/main/res/layout/nav_header.xml b/android/app/src/main/res/layout/nav_header.xml index 6518485..d6e0f2c 100644 --- a/android/app/src/main/res/layout/nav_header.xml +++ b/android/app/src/main/res/layout/nav_header.xml @@ -101,6 +101,33 @@ android:text="Wide background" style="?attr/materialButtonElevatedStyle" /> + + + + + + + + + android:layout_height="6dp" /> - + - + - + + + +