diff --git a/android/app/src/main/assets/Config/Supermodel.ini b/android/app/src/main/assets/Config/Supermodel.ini index 8230f24..602d55a 100644 --- a/android/app/src/main/assets/Config/Supermodel.ini +++ b/android/app/src/main/assets/Config/Supermodel.ini @@ -51,6 +51,7 @@ InputCoin1 = KEY_5,JOY1_BUTTON7 InputStart1 = KEY_1,JOY1_BUTTON8 InputServiceA = KEY_F1,JOY1_BUTTON13 InputTestA = KEY_F2 +InputUISelectCrosshairs = KEY_F8 InputJoyUp = KEY_UP,JOY1_POV1_UP,JOY1_YAXIS_NEG InputJoyDown = KEY_DOWN,JOY1_POV1_DOWN,JOY1_YAXIS_POS InputJoyLeft = KEY_LEFT,JOY1_POV1_LEFT,JOY1_XAXIS_NEG diff --git a/android/app/src/main/cpp/android_input_system.cpp b/android/app/src/main/cpp/android_input_system.cpp index 646ecd0..3ae5490 100644 --- a/android/app/src/main/cpp/android_input_system.cpp +++ b/android/app/src/main/cpp/android_input_system.cpp @@ -42,6 +42,10 @@ void AndroidInputSystem::SetGunTouchEnabled(bool enabled) m_gunTouchEnabled = enabled; m_gunFingerActive = false; m_gunFinger = 0; + m_mousePadActive = false; + m_mousePadFinger = 0; + m_mousePadLastX = 0.0f; + m_mousePadLastY = 0.0f; m_mouseX = 0; m_mouseY = 0; m_mouseWheelDir = 0; @@ -49,6 +53,16 @@ void AndroidInputSystem::SetGunTouchEnabled(bool enabled) std::memset(m_mouseButtonPulseUntilMs, 0, sizeof(m_mouseButtonPulseUntilMs)); } +void AndroidInputSystem::SetGunStickAimEnabled(bool enabled) +{ + if (m_gunStickAimEnabled == enabled) + return; + m_gunStickAimEnabled = enabled; + m_lastGunAimTickMs = 0; + if (m_gunStickAimEnabled) + CenterMouse(); +} + void AndroidInputSystem::SetVirtualWheelEnabled(bool enabled) { if (m_virtualWheelEnabled == enabled) @@ -270,6 +284,8 @@ void AndroidInputSystem::ApplyConfig(const Util::Config::Node& config) m_touchSkiPollRight.a = keySc(get("InputSkiPollRight", "KEY_S"), SDL_SCANCODE_S); m_touchSkiSelect1.a = keySc(get("InputSkiSelect1", "KEY_Q"), SDL_SCANCODE_Q); m_touchSkiSelect2.a = keySc(get("InputSkiSelect2", "KEY_W"), SDL_SCANCODE_W); + + m_touchCrosshairs.a = keySc(get("InputUISelectCrosshairs", "KEY_F8"), SDL_SCANCODE_F8); } void AndroidInputSystem::EnsureGameControllerMappingsLoaded() @@ -307,8 +323,13 @@ bool AndroidInputSystem::InitializeSystem() std::memset(m_mouseButtonPulseUntilMs, 0, sizeof(m_mouseButtonPulseUntilMs)); m_gunFingerActive = false; m_gunFinger = 0; + m_mousePadActive = false; + m_mousePadFinger = 0; + m_mousePadLastX = 0.0f; + m_mousePadLastY = 0.0f; m_suppressGunTriggerUntilMs = 0; m_pendingReloadTriggerAtMs = 0; + m_lastGunAimTickMs = 0; m_wheelFingerActive = false; m_wheelFinger = 0; m_virtualJoyX.store(0, std::memory_order_relaxed); @@ -354,6 +375,7 @@ bool AndroidInputSystem::Poll() SDL_JoystickUpdate(); const uint32_t now = SDL_GetTicks(); + UpdateGunStickAim(now); if (m_pendingReloadTriggerAtMs != 0 && now >= m_pendingReloadTriggerAtMs) { // Manual reload helper when the core's InputAutoTrigger is disabled: @@ -503,9 +525,36 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) case 1141: SetKeys(m_touchSkiPollRight, down); return; case 1142: SetKeys(m_touchSkiSelect1, down); return; case 1143: SetKeys(m_touchSkiSelect2, down); return; + case 1119: // Crosshairs toggle + if (down) PulseKeys(m_touchCrosshairs, 120); + return; default: break; } + // Virtual mouse pad (relative mouse movement). + if (tf.fingerId == 1125) + { + if (down) + { + if (!m_mousePadActive) + { + m_mousePadActive = true; + m_mousePadFinger = tf.fingerId; + m_mousePadLastX = tf.x; + m_mousePadLastY = tf.y; + } + } + else + { + if (m_mousePadActive && tf.fingerId == m_mousePadFinger) + { + m_mousePadActive = false; + m_mousePadFinger = 0; + } + } + return; + } + // Virtual fighting stick: 8-way directional based on encoded x/y in [0..1], keyed by a fixed fingerId. if (tf.fingerId == 1114) { @@ -790,6 +839,30 @@ void AndroidInputSystem::HandleTouchMotion(const SDL_TouchFingerEvent& tf) return; } + if (m_mousePadActive && tf.fingerId == m_mousePadFinger) + { + const float dx = tf.x - m_mousePadLastX; + const float dy = tf.y - m_mousePadLastY; + m_mousePadLastX = tf.x; + m_mousePadLastY = tf.y; + + if (dx == 0.0f && dy == 0.0f) + return; + + const int w = (m_dispW > 0) ? (int)m_dispW : 496; + const int h = (m_dispH > 0) ? (int)m_dispH : 384; + const int x0 = (int)m_dispX; + const int y0 = (int)m_dispY; + constexpr float sensitivity = 1.0f; + m_mouseX += (int)std::lround(dx * (float)w * sensitivity); + m_mouseY += (int)std::lround(dy * (float)h * sensitivity); + const int maxX = x0 + w - 1; + const int maxY = y0 + h - 1; + m_mouseX = std::clamp(m_mouseX, x0, maxX); + m_mouseY = std::clamp(m_mouseY, y0, maxY); + return; + } + if (m_virtualShifterShift4 && tf.fingerId == 1108) { // Avoid accidental neutral when passing through center during motion. @@ -888,7 +961,7 @@ int AndroidInputSystem::GetMouseAxisValue(int mseNum, int axisNum) // This lets: // - touch aim drive MOUSE_* mappings while held, and // - physical controller aim drive JOY* mappings when not touching. - if (m_virtualAnalogGunEnabled && (axisNum == AXIS_X || axisNum == AXIS_Y) && !m_gunFingerActive) + if (m_virtualAnalogGunEnabled && (axisNum == AXIS_X || axisNum == AXIS_Y) && !m_gunFingerActive && !m_gunStickAimEnabled) { const unsigned extent = (axisNum == AXIS_X) ? m_dispW : m_dispH; const unsigned origin = (axisNum == AXIS_X) ? m_dispX : m_dispY; @@ -959,6 +1032,93 @@ void AndroidInputSystem::SetMousePosFromNormalized(float x, float y) m_mouseY = std::clamp(py, 0, h - 1); } +void AndroidInputSystem::CenterMouse() +{ + const int w = (m_dispW > 0) ? (int)m_dispW : 496; + const int h = (m_dispH > 0) ? (int)m_dispH : 384; + const int x0 = (int)m_dispX; + const int y0 = (int)m_dispY; + m_mouseX = x0 + w / 2; + m_mouseY = y0 + h / 2; +} + +void AndroidInputSystem::UpdateGunStickAim(uint32_t nowMs) +{ + if (!m_gunStickAimEnabled || m_controllers.empty() || m_gunFingerActive || m_mousePadActive) + { + m_lastGunAimTickMs = nowMs; + return; + } + + float dt = 0.0f; + if (m_lastGunAimTickMs != 0) + { + const uint32_t delta = nowMs - m_lastGunAimTickMs; + dt = (float)delta / 1000.0f; + if (dt > 0.05f) + dt = 0.05f; + } + m_lastGunAimTickMs = nowMs; + if (dt <= 0.0f) + return; + + auto normAxis = [](int v) { + if (v <= -32768) return -1.0f; + if (v >= 32767) return 1.0f; + return (float)v / 32767.0f; + }; + + auto axisAny = [&](int axisNum) { + int best = 0; + for (const auto& c : m_controllers) + { + const int v = AxisValueFor(c, axisNum); + if (std::abs(v) > std::abs(best)) + best = v; + } + return best; + }; + + int ax = axisAny(AXIS_RX); + int ay = axisAny(AXIS_RY); + float fx = normAxis(ax); + float fy = normAxis(ay); + + constexpr float deadzone = 0.18f; + float mag = std::sqrt(fx * fx + fy * fy); + if (mag < deadzone) + { + ax = axisAny(AXIS_X); + ay = axisAny(AXIS_Y); + fx = normAxis(ax); + fy = normAxis(ay); + mag = std::sqrt(fx * fx + fy * fy); + } + + if (mag < deadzone) + return; + + float scaled = (mag - deadzone) / (1.0f - deadzone); + scaled = std::clamp(scaled, 0.0f, 1.0f); + const float invMag = 1.0f / mag; + fx = fx * invMag * scaled; + fy = fy * invMag * scaled; + + const int w = (m_dispW > 0) ? (int)m_dispW : 496; + const int h = (m_dispH > 0) ? (int)m_dispH : 384; + const int x0 = (int)m_dispX; + const int y0 = (int)m_dispY; + const float baseSpeed = (float)std::max(w, h) * 1.6f; + + m_mouseX += (int)std::lround(fx * baseSpeed * dt); + m_mouseY += (int)std::lround(fy * baseSpeed * dt); + + const int maxX = x0 + w - 1; + const int maxY = y0 + h - 1; + m_mouseX = std::clamp(m_mouseX, x0, maxX); + m_mouseY = std::clamp(m_mouseY, y0, maxY); +} + void AndroidInputSystem::CloseControllers() { for (auto& c : m_controllers) @@ -1237,6 +1397,20 @@ bool AndroidInputSystem::ButtonPressedFor(const ControllerState& c, int butNum) int AndroidInputSystem::GetJoyAxisValue(int joyNum, int axisNum) { + if (m_gunStickAimEnabled && m_virtualAnalogJoystickEnabled && !UseVirtualJoystick() && + (axisNum == AXIS_X || axisNum == AXIS_Y || axisNum == AXIS_RX || axisNum == AXIS_RY)) + { + return 0; + } + + // Analog-gun games often map the gun axis to JOY1_X/Y, which can override our + // mouse/touch/stick-aim path. Suppress raw joystick axes so the gun uses MOUSE_*. + if (m_gunStickAimEnabled && m_virtualAnalogGunEnabled && !UseVirtualJoystick() && + (axisNum == AXIS_X || axisNum == AXIS_Y || axisNum == AXIS_RX || axisNum == AXIS_RY)) + { + return 0; + } + if (UseVirtualJoystick()) { if (axisNum == AXIS_X) diff --git a/android/app/src/main/cpp/android_input_system.h b/android/app/src/main/cpp/android_input_system.h index 1f72ffe..675f348 100644 --- a/android/app/src/main/cpp/android_input_system.h +++ b/android/app/src/main/cpp/android_input_system.h @@ -22,6 +22,7 @@ public: ~AndroidInputSystem() override; void SetGunTouchEnabled(bool enabled); + void SetGunStickAimEnabled(bool enabled); void SetVirtualWheelEnabled(bool enabled); void SetVirtualShifterMode(bool shift4, bool shiftUpDown); void SetVirtualAnalogGunEnabled(bool enabled); @@ -113,6 +114,8 @@ private: void SetMouseButton(int butNum, bool down); void PulseMouseButton(int butNum, uint32_t durationMs); void SetMousePosFromNormalized(float x, float y); + void CenterMouse(); + void UpdateGunStickAim(uint32_t nowMs); SDL_Scancode ScancodeFromSupermodelKeyName(const char* keyName) const; static SDL_Scancode ParseFirstKeyboardScancode(const std::string& mapping, const AndroidInputSystem& self); @@ -167,6 +170,8 @@ private: DualScancode m_touchFishingReel{SDL_SCANCODE_SPACE, SDL_SCANCODE_UNKNOWN}; DualScancode m_touchFishingTension{SDL_SCANCODE_T, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchCrosshairs{SDL_SCANCODE_F8, SDL_SCANCODE_UNKNOWN}; + DualScancode m_touchMagPedal1{SDL_SCANCODE_A, SDL_SCANCODE_UNKNOWN}; DualScancode m_touchMagPedal2{SDL_SCANCODE_S, SDL_SCANCODE_UNKNOWN}; @@ -176,8 +181,14 @@ private: DualScancode m_touchSkiSelect2{SDL_SCANCODE_W, SDL_SCANCODE_UNKNOWN}; bool m_gunTouchEnabled = false; + bool m_gunStickAimEnabled = false; + uint32_t m_lastGunAimTickMs = 0; SDL_FingerID m_gunFinger = 0; bool m_gunFingerActive = false; + bool m_mousePadActive = false; + SDL_FingerID m_mousePadFinger = 0; + float m_mousePadLastX = 0.0f; + float m_mousePadLastY = 0.0f; bool m_virtualWheelEnabled = false; SDL_FingerID m_wheelFinger = 0; diff --git a/android/app/src/main/cpp/gles_presenter.cpp b/android/app/src/main/cpp/gles_presenter.cpp index 8bdcf22..5cb2403 100644 --- a/android/app/src/main/cpp/gles_presenter.cpp +++ b/android/app/src/main/cpp/gles_presenter.cpp @@ -60,6 +60,13 @@ bool GlesPresenter::Init() return false; if (!CreateGeometry()) return false; + if (!CreateCrosshairProgram() || !CreateCrosshairGeometry()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GlesPresenter: crosshair renderer disabled"); + if (m_crossProgram) { glDeleteProgram(m_crossProgram); m_crossProgram = 0; } + if (m_crossVbo) { glDeleteBuffers(1, &m_crossVbo); m_crossVbo = 0; } + if (m_crossVao) { glDeleteVertexArrays(1, &m_crossVao); m_crossVao = 0; } + m_uCrossColor = -1; + } glGenTextures(1, &m_tex); glBindTexture(GL_TEXTURE_2D, m_tex); @@ -78,7 +85,11 @@ void GlesPresenter::Shutdown() if (m_vbo) { glDeleteBuffers(1, &m_vbo); m_vbo = 0; } if (m_vao) { glDeleteVertexArrays(1, &m_vao); m_vao = 0; } if (m_program) { glDeleteProgram(m_program); m_program = 0; } + if (m_crossVbo) { glDeleteBuffers(1, &m_crossVbo); m_crossVbo = 0; } + if (m_crossVao) { glDeleteVertexArrays(1, &m_crossVao); m_crossVao = 0; } + if (m_crossProgram) { glDeleteProgram(m_crossProgram); m_crossProgram = 0; } m_uTex = -1; + m_uCrossColor = -1; m_uploadRGBA.clear(); m_outputW = m_outputH = m_srcW = m_srcH = 0; } @@ -161,6 +172,74 @@ bool GlesPresenter::CreateGeometry() return true; } +bool GlesPresenter::CreateCrosshairProgram() +{ + static const char* vs = R"glsl(#version 300 es + precision highp float; + in vec2 aPos; + void main() { + gl_Position = vec4(aPos, 0.0, 1.0); + } + )glsl"; + + static const char* fs = R"glsl(#version 300 es + precision mediump float; + uniform vec4 uColor; + layout(location=0) out vec4 oColor; + void main() { + oColor = uColor; + } + )glsl"; + + GLuint v = Compile(GL_VERTEX_SHADER, vs); + GLuint f = Compile(GL_FRAGMENT_SHADER, fs); + if (!v || !f) + { + if (v) glDeleteShader(v); + if (f) glDeleteShader(f); + return false; + } + + m_crossProgram = glCreateProgram(); + glAttachShader(m_crossProgram, v); + glAttachShader(m_crossProgram, f); + glBindAttribLocation(m_crossProgram, 0, "aPos"); + glLinkProgram(m_crossProgram); + glDeleteShader(v); + glDeleteShader(f); + + GLint ok = 0; + glGetProgramiv(m_crossProgram, GL_LINK_STATUS, &ok); + if (!ok) + { + char log[2048]; + GLsizei n = 0; + glGetProgramInfoLog(m_crossProgram, (GLsizei)sizeof(log), &n, log); + log[(n >= (GLsizei)sizeof(log)) ? ((GLsizei)sizeof(log) - 1) : n] = '\0'; + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GlesPresenter: crosshair program link failed:\n%s", log); + glDeleteProgram(m_crossProgram); + m_crossProgram = 0; + return false; + } + + m_uCrossColor = glGetUniformLocation(m_crossProgram, "uColor"); + return true; +} + +bool GlesPresenter::CreateCrosshairGeometry() +{ + glGenVertexArrays(1, &m_crossVao); + glGenBuffers(1, &m_crossVbo); + glBindVertexArray(m_crossVao); + glBindBuffer(GL_ARRAY_BUFFER, m_crossVbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 24, nullptr, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (void*)0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + return true; +} + void GlesPresenter::Resize(int outputW, int outputH) { m_outputW = std::max(1, outputW); @@ -281,3 +360,66 @@ void GlesPresenter::Render(bool alphaBlend) glBindTexture(GL_TEXTURE_2D, 0); glUseProgram(0); } + +void GlesPresenter::RenderCrosshair(float xNorm, float yNorm, float r, float g, float b, float aspect, + int viewX, int viewY, int viewW, int viewH) +{ + if (!m_crossProgram || !m_crossVao || viewW <= 0 || viewH <= 0) + return; + + auto toNdc = [](float x, float y, float& outX, float& outY) { + outX = (x * 2.0f) - 1.0f; + outY = 1.0f - (y * 2.0f); + }; + + const float base = 0.01f; + const float height = 0.02f; + const float dist = 0.004f; + const float bx = base * 0.5f; + const float hy = (dist + height) * aspect; + const float by = bx * aspect; + + float verts[24]; + int idx = 0; + auto emit = [&](float x, float y) { + float nx, ny; + toNdc(x, y, nx, ny); + verts[idx++] = nx; + verts[idx++] = ny; + }; + + // Bottom triangle + emit(xNorm, yNorm + dist); + emit(xNorm + bx, yNorm + hy); + emit(xNorm - bx, yNorm + hy); + // Top triangle + emit(xNorm, yNorm - dist); + emit(xNorm - bx, yNorm - hy); + emit(xNorm + bx, yNorm - hy); + // Left triangle + emit(xNorm - dist, yNorm); + emit(xNorm - dist - height, yNorm + by); + emit(xNorm - dist - height, yNorm - by); + // Right triangle + emit(xNorm + dist, yNorm); + emit(xNorm + dist + height, yNorm - by); + emit(xNorm + dist + height, yNorm + by); + + glViewport(viewX, viewY, viewW, viewH); + glDisable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_STENCIL_TEST); + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glUseProgram(m_crossProgram); + glUniform4f(m_uCrossColor, r, g, b, 1.0f); + glBindVertexArray(m_crossVao); + glBindBuffer(GL_ARRAY_BUFFER, m_crossVbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(verts), verts); + glDrawArrays(GL_TRIANGLES, 0, 12); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + glUseProgram(0); +} diff --git a/android/app/src/main/cpp/gles_presenter.h b/android/app/src/main/cpp/gles_presenter.h index f6b04d3..59766d5 100644 --- a/android/app/src/main/cpp/gles_presenter.h +++ b/android/app/src/main/cpp/gles_presenter.h @@ -18,10 +18,13 @@ public: // pixelsARGB: 0xAARRGGBB packed. void UpdateFrameARGB(const uint32_t* pixelsARGB, int width, int height); void Render(bool alphaBlend); + void RenderCrosshair(float xNorm, float yNorm, float r, float g, float b, float aspect, int viewX, int viewY, int viewW, int viewH); private: bool CreateProgram(); bool CreateGeometry(); + bool CreateCrosshairProgram(); + bool CreateCrosshairGeometry(); void UpdateQuadVerts(); int m_outputW = 0; @@ -36,6 +39,11 @@ private: unsigned m_tex = 0; int m_uTex = -1; + unsigned m_crossProgram = 0; + unsigned m_crossVao = 0; + unsigned m_crossVbo = 0; + int m_uCrossColor = -1; + // Interleaved: pos.xy, uv.xy (4 verts) float m_verts[16]{}; diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index 7fc19f6..e958f8d 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -197,6 +197,12 @@ static void AndroidTileBlit(const uint32_t* pixelsARGB, int width, int height, b g_presenter->Render(alphaBlend); } +static void GunToViewCoords(float& x, float& y) +{ + x = (x - 150.0f) / (651.0f - 150.0f); + y = (y - 80.0f) / (465.0f - 80.0f); +} + struct Super3Host { static constexpr int32_t STATE_FILE_VERSION = 3; Util::Config::Node config{"Global"}; @@ -324,6 +330,7 @@ struct Super3Host { config.Set("YResolution", "384"); config.Set("Throttle", true); config.Set("RefreshRate", "60.0"); + config.Set("Crosshairs", 0); // Lightgun games: reload immediately when tapping the off-screen button. config.Set("InputAutoTrigger", "1"); @@ -342,12 +349,13 @@ struct Super3Host { config.Set("InputSteeringLeft", "KEY_LEFT"); config.Set("InputSteeringRight", "KEY_RIGHT"); config.Set("InputAccelerator", "KEY_W"); - config.Set("InputBrake", "KEY_S"); - config.Set("UISaveState", "KEY_F5"); - config.Set("UIChangeSlot", "KEY_F6"); - config.Set("UILoadState", "KEY_F7"); - inputSystem.ApplyConfig(config); - } + config.Set("InputBrake", "KEY_S"); + config.Set("UISaveState", "KEY_F5"); + config.Set("UIChangeSlot", "KEY_F6"); + config.Set("UILoadState", "KEY_F7"); + config.Set("InputUISelectCrosshairs", "KEY_F8"); + inputSystem.ApplyConfig(config); + } void ApplyAndroidHardOverrides() { @@ -439,6 +447,7 @@ struct Super3Host { ensureKeyboardFallback("InputTwinJoyShot2", "KEY_S"); ensureKeyboardFallback("InputTwinJoyTurbo1", "KEY_Z"); ensureKeyboardFallback("InputTwinJoyTurbo2", "KEY_X"); + ensureKeyboardFallback("InputUISelectCrosshairs", "KEY_F8"); inputSystem.ApplyConfig(config); } @@ -516,6 +525,7 @@ struct Super3Host { (game.inputs & (Game::INPUT_GUN1 | Game::INPUT_GUN2 | Game::INPUT_ANALOG_GUN1 | Game::INPUT_ANALOG_GUN2)) != 0; const bool analogJoystickGame = (game.inputs & Game::INPUT_ANALOG_JOYSTICK) != 0; inputSystem.SetGunTouchEnabled(gunGame || analogJoystickGame); + inputSystem.SetGunStickAimEnabled(gunGame || analogJoystickGame); const bool analogGunGame = (game.inputs & (Game::INPUT_ANALOG_GUN1 | Game::INPUT_ANALOG_GUN2)) != 0; inputSystem.SetVirtualAnalogGunEnabled(analogGunGame); @@ -657,6 +667,19 @@ struct Super3Host { // it mainly keeps the input system in a sane state. inputs.Poll(&game, 0, 0, 496, 384); + if (inputs.uiSelectCrosshairs && inputs.uiSelectCrosshairs->Pressed()) + { + const bool hasGunInputs = + (game.inputs & (Game::INPUT_GUN1 | Game::INPUT_GUN2 | Game::INPUT_ANALOG_GUN1 | Game::INPUT_ANALOG_GUN2)) != 0 || + game.name == "lostwsga"; + if (hasGunInputs) + { + unsigned crosshairs = config["Crosshairs"].ValueAsDefault(0); + crosshairs = (crosshairs + 1) & 3u; + config.Set("Crosshairs", crosshairs); + } + } + // If a physical keyboard is attached, allow the canonical hotkeys too. if (!threadsPausedByMenu) { if (inputs.uiSaveState && inputs.uiSaveState->Pressed()) { @@ -790,6 +813,79 @@ static std::optional FindFirstExisting(const std::vector(0) & 3u; + if (crosshairs == 0) + return; + + const uint32_t inputs = host.game.inputs; + bool hasGunInputs = (inputs & (Game::INPUT_GUN1 | Game::INPUT_GUN2 | Game::INPUT_ANALOG_GUN1 | Game::INPUT_ANALOG_GUN2)) != 0; + if (host.game.name == "lostwsga") + hasGunInputs = true; + if (!hasGunInputs) + return; + + float x[2] = {0.0f, 0.0f}; + float y[2] = {0.0f, 0.0f}; + bool offscreen[2] = {false, false}; + bool have[2] = {false, false}; + + if (inputs & Game::INPUT_ANALOG_GUN1) + { + x[0] = host.inputs.analogGunX[0]->value / 255.0f; + y[0] = (255.0f - host.inputs.analogGunY[0]->value) / 255.0f; + offscreen[0] = host.inputs.analogTriggerLeft[0]->value || host.inputs.analogTriggerRight[0]->value; + have[0] = true; + } + else if (inputs & Game::INPUT_GUN1) + { + x[0] = (float)host.inputs.gunX[0]->value; + y[0] = (float)host.inputs.gunY[0]->value; + GunToViewCoords(x[0], y[0]); + offscreen[0] = host.inputs.trigger[0]->offscreenValue > 0; + have[0] = true; + } + + if (inputs & Game::INPUT_ANALOG_GUN2) + { + x[1] = host.inputs.analogGunX[1]->value / 255.0f; + y[1] = (255.0f - host.inputs.analogGunY[1]->value) / 255.0f; + offscreen[1] = host.inputs.analogTriggerLeft[1]->value || host.inputs.analogTriggerRight[1]->value; + have[1] = true; + } + else if (inputs & Game::INPUT_GUN2) + { + x[1] = (float)host.inputs.gunX[1]->value; + y[1] = (float)host.inputs.gunY[1]->value; + GunToViewCoords(x[1], y[1]); + offscreen[1] = host.inputs.trigger[1]->offscreenValue > 0; + have[1] = true; + } + + const float aspect = (yRes > 0) ? (float)xRes / (float)yRes : 1.0f; + if ((crosshairs & 1u) && have[0] && !offscreen[0]) + { + presenter.RenderCrosshair(std::clamp(x[0], 0.0f, 1.0f), std::clamp(y[0], 0.0f, 1.0f), + 1.0f, 0.0f, 0.0f, aspect, + (int)xOff, (int)yOff, (int)xRes, (int)yRes); + } + if ((crosshairs & 2u) && have[1] && !offscreen[1]) + { + presenter.RenderCrosshair(std::clamp(x[1], 0.0f, 1.0f), std::clamp(y[1], 0.0f, 1.0f), + 0.0f, 1.0f, 0.0f, aspect, + (int)xOff, (int)yOff, (int)xRes, (int)yRes); + } +} + // SDL entry point; currently a stub until the emulator core is hooked up. extern "C" int SDL_main(int argc, char* argv[]) { (void)argc; @@ -1088,6 +1184,10 @@ extern "C" int SDL_main(int argc, char* argv[]) { } } + if (state == 1) { + RenderCrosshairsIfNeeded(host, presenter, xOff, yOff, xRes, yRes); + } + SDL_GL_SwapWindow(window); const bool throttle = host.config["Throttle"].ValueAsDefault(true); 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 e504e26..677403f 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt @@ -222,7 +222,8 @@ class Super3Activity : SDLActivity() { ) val showFightButtons = isFighting || isSpikeout || isSoccer || isTwinJoysticks || isFishing || isSki - val showFightStick = showFightButtons || isMagTruck || needsAimStickAndButtons + val showAimPad = isGunGame || needsAimStickAndButtons + val showFightStick = (showFightButtons || isMagTruck) && !showAimPad val showExtraButtons = showFightButtons || needsAimStickAndButtons val shifterEnabled = getSharedPreferences("super3_prefs", MODE_PRIVATE) @@ -241,12 +242,30 @@ class Super3Activity : SDLActivity() { overlay.findViewById(R.id.overlay_view)?.visibility = if (isRacing && (hasViewChange || hasVr4)) View.VISIBLE else View.GONE - overlay.findViewById(R.id.overlay_reload)?.visibility = - if (isGunGame) View.VISIBLE else View.GONE + val replaceReloadWithCross = isGunGame && needsAimStickAndButtons + val reloadButton = overlay.findViewById(R.id.overlay_reload) + val crossButton = overlay.findViewById(R.id.overlay_crosshair) + + reloadButton?.visibility = if (isGunGame && !replaceReloadWithCross) View.VISIBLE else View.GONE + crossButton?.visibility = if (isGunGame) View.VISIBLE else View.GONE + + if (replaceReloadWithCross && crossButton != null) { + val density = resources.displayMetrics.density + val margin = (16f * density + 0.5f).toInt() + val lp = crossButton.layoutParams as? FrameLayout.LayoutParams + if (lp != null) { + lp.bottomMargin = margin + lp.marginEnd = margin + crossButton.layoutParams = lp + } + } overlay.findViewById(R.id.overlay_fight_stick)?.visibility = if (showFightStick) View.VISIBLE else View.GONE + overlay.findViewById(R.id.overlay_mouse_pad)?.visibility = + if (showAimPad) View.VISIBLE else View.GONE + overlay.findViewById(R.id.overlay_fight_buttons)?.visibility = if (showExtraButtons) View.VISIBLE else View.GONE @@ -342,7 +361,10 @@ class Super3Activity : SDLActivity() { 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 (!replaceReloadWithCross) { + bindMomentary(R.id.overlay_reload, fingerId = 1109, x = 0.90f, y = 0.90f) + } + bindMomentary(R.id.overlay_crosshair, fingerId = 1119, x = 0.90f, y = 0.78f) } if (showExtraButtons) { @@ -417,6 +439,40 @@ class Super3Activity : SDLActivity() { overlay.findViewById(R.id.overlay_fight_stick)?.setOnTouchListener(null) } + if (showAimPad) { + val pad = overlay.findViewById(R.id.overlay_mouse_pad) + pad?.setOnTouchListener { v, ev -> + val w = v.width.toFloat().coerceAtLeast(1f) + val h = v.height.toFloat().coerceAtLeast(1f) + val nx = (ev.x / w).coerceIn(0f, 1f) + val ny = (ev.y / h).coerceIn(0f, 1f) + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + v.alpha = 0.90f + nativeTouch(MotionEvent.ACTION_DOWN, 1125, nx, ny) + true + } + MotionEvent.ACTION_MOVE -> { + nativeTouch(MotionEvent.ACTION_MOVE, 1125, nx, ny) + true + } + MotionEvent.ACTION_UP -> { + v.alpha = 1.0f + nativeTouch(MotionEvent.ACTION_UP, 1125, nx, ny) + true + } + MotionEvent.ACTION_CANCEL -> { + v.alpha = 1.0f + nativeTouch(MotionEvent.ACTION_UP, 1125, nx, ny) + true + } + else -> true + } + } + } else { + overlay.findViewById(R.id.overlay_mouse_pad)?.setOnTouchListener(null) + } + applyOverlayControlsEnabled(overlayControlsEnabled, persist = false) if (isRacing || isMagTruck) { diff --git a/android/app/src/main/res/drawable/overlay_mouse_pad.xml b/android/app/src/main/res/drawable/overlay_mouse_pad.xml new file mode 100644 index 0000000..6c3af2d --- /dev/null +++ b/android/app/src/main/res/drawable/overlay_mouse_pad.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/layout/overlay_controls.xml b/android/app/src/main/res/layout/overlay_controls.xml index 846ca57..9d05bbc 100644 --- a/android/app/src/main/res/layout/overlay_controls.xml +++ b/android/app/src/main/res/layout/overlay_controls.xml @@ -140,6 +140,25 @@ app:rippleColor="@color/overlay_ripple" app:strokeWidth="2dp" /> + + + +