diff --git a/CMakeLists.txt b/CMakeLists.txt index 24130ada9d..2c76bfb520 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1032,7 +1032,8 @@ if(NOT LIBRETRO) if(ANDROID OR IOS) cmrc_add_resources(flycast-resources WHENCE resources - resources/picture/buttons.png) + resources/picture/buttons.png + resources/picture/buttons-arcade.png) endif() endif() @@ -1617,6 +1618,10 @@ if(NOT LIBRETRO) target_sources(${PROJECT_NAME} PRIVATE shell/android-studio/flycast/src/main/jni/src/Android.cpp shell/android-studio/flycast/src/main/jni/src/android_gamepad.h + shell/android-studio/flycast/src/main/jni/src/android_storage.h + shell/android-studio/flycast/src/main/jni/src/http_client.h + shell/android-studio/flycast/src/main/jni/src/jni_util.h + shell/android-studio/flycast/src/main/jni/src/android_input.cpp shell/android-studio/flycast/src/main/jni/src/android_keyboard.h) target_link_libraries(${PROJECT_NAME} PRIVATE android log) diff --git a/core/input/gamepad.h b/core/input/gamepad.h index 88bce6fe7b..e4fdc049bd 100644 --- a/core/input/gamepad.h +++ b/core/input/gamepad.h @@ -52,6 +52,7 @@ enum DreamcastKey EMU_BTN_SAVESTATE, EMU_BTN_BYPASS_KB, EMU_BTN_SCREENSHOT, + EMU_BTN_SRVMODE, // used internally by virtual gamepad // Real axes DC_AXIS_TRIGGERS = 0x1000000, diff --git a/core/input/virtual_gamepad.h b/core/input/virtual_gamepad.h new file mode 100644 index 0000000000..69adc28285 --- /dev/null +++ b/core/input/virtual_gamepad.h @@ -0,0 +1,117 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#pragma once +#include "gamepad_device.h" +#include "ui/vgamepad.h" + +class VirtualGamepad : public GamepadDevice +{ +public: + VirtualGamepad(const char *api_name, int maple_port = 0) + : GamepadDevice(maple_port, api_name, false) + { + _name = "Virtual Gamepad"; + _unique_id = "virtual_gamepad_uid"; + input_mapper = std::make_shared(); + // hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted + + leftTrigger = DC_AXIS_LT; + rightTrigger = DC_AXIS_RT; + } + + bool is_virtual_gamepad() override { + return true; + }; + + // normalized coordinates [-1, 1] + void joystickInput(float x, float y) + { + vgamepad::setAnalogStick(x, y); + int joyx = std::round(x * 32767.f); + int joyy = std::round(y * 32767.f); + if (joyx >= 0) + gamepad_axis_input(DC_AXIS_RIGHT, joyx); + else + gamepad_axis_input(DC_AXIS_LEFT, -joyx); + if (joyy >= 0) + gamepad_axis_input(DC_AXIS_DOWN, joyy); + else + gamepad_axis_input(DC_AXIS_UP, -joyy); + } + + void releaseAll() + { + for (int i = 0; i < 32; i++) + if (buttonState & (1 << i)) + gamepad_btn_input(1 << i, false); + buttonState = 0; + joystickInput(0, 0); + gamepad_axis_input(DC_AXIS_LT, 0); + gamepad_axis_input(DC_AXIS_RT, 0); + if (previousFastForward) + gamepad_btn_input(EMU_BTN_FFORWARD, false); + previousFastForward = false; + } + + virtual bool handleButtonInput(u32& state, u32 key, bool pressed) { + // can be overridden in derived classes to handle specific key combos + // (iOS up+down or left+right) + return false; + } + + void buttonInput(vgamepad::ControlId controlId, bool pressed) + { + u32 kcode = vgamepad::controlToDcKey(controlId); + if (kcode == 0) + return; + if (handleButtonInput(buttonState, kcode, pressed)) + return; + if (kcode == DC_AXIS_LT) { + gamepad_axis_input(DC_AXIS_LT, pressed ? 0x7fff : 0); + } + else if (kcode == DC_AXIS_RT) { + gamepad_axis_input(DC_AXIS_RT, pressed ? 0x7fff : 0); + } + else if (kcode == EMU_BTN_SRVMODE) { + if (pressed) + vgamepad::toggleServiceMode(); + } + else + { + if (pressed) + buttonState |= kcode; + else + buttonState &= ~kcode; + if ((kcode & (DC_DPAD_LEFT | DC_DPAD_RIGHT)) != 0 + && (kcode & (DC_DPAD_UP | DC_DPAD_DOWN)) != 0) + { + // diagonals + gamepad_btn_input(kcode & (DC_DPAD_LEFT | DC_DPAD_RIGHT), pressed); + gamepad_btn_input(kcode & (DC_DPAD_UP | DC_DPAD_DOWN), pressed); + } + else { + gamepad_btn_input(kcode, pressed); + } + } + } + +private: + u32 buttonState = 0; + bool previousFastForward = false; +}; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 6d6ceb4943..e826a6b77f 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -1467,7 +1467,7 @@ static void gamepadSettingsPopup(const std::shared_ptr& gamepad) #if defined(__ANDROID__) || defined(TARGET_IPHONE) vgamepad::ImguiVGamepadTexture tex; - ImGui::Image(tex.getId(), ScaledVec2(300, 112.5f), ImVec2(0, 1), ImVec2(1, 0.25f)); + ImGui::Image(tex.getId(), ScaledVec2(300.f, 150.f), ImVec2(0, 1), ImVec2(1, 0)); #endif const char *gamepadPngTitle = "Select a PNG file"; if (ImGui::Button("Choose Image...", ScaledVec2(150, 30))) diff --git a/core/ui/vgamepad.cpp b/core/ui/vgamepad.cpp index b4d7e1d586..53867fdf51 100644 --- a/core/ui/vgamepad.cpp +++ b/core/ui/vgamepad.cpp @@ -31,14 +31,13 @@ #include "cfg/cfg.h" #include "input/gamepad.h" #include "hw/naomi/naomi_cart.h" +#include "hw/naomi/card_reader.h" #include "hw/maple/maple_devs.h" #include namespace vgamepad { -static void loadLayout(); - struct Control { Control() = default; @@ -53,9 +52,11 @@ struct Control }; static Control Controls[_Count]; static bool Visible = true; +static bool serviceMode; static float AlphaTrans = 1.f; static ImVec2 StickPos; // analog stick position [-1, 1] constexpr char const *BTN_PATH = "picture/buttons.png"; +constexpr char const *BTN_PATH_ARCADE = "picture/buttons-arcade.png"; constexpr char const *CFG_SECTION = "vgamepad"; void displayCommands() @@ -87,6 +88,14 @@ void displayCommands() ImGui::End(); } +static const char *getButtonsResPath() { + return settings.platform.isConsole() ? BTN_PATH : BTN_PATH_ARCADE; +} + +static const char *getButtonsCfgName() { + return settings.platform.isConsole() ? "image" : "image_arcade"; +} + static bool loadOSDButtons(const std::string& path) { if (path.empty()) @@ -102,7 +111,7 @@ static bool loadOSDButtons(const std::string& path) if (image_data == nullptr) return false; try { - imguiDriver->updateTexture(BTN_PATH, image_data, width, height, false); + imguiDriver->updateTexture(getButtonsResPath(), image_data, width, height, false); } catch (...) { // vulkan can throw during resizing } @@ -115,25 +124,28 @@ static ImTextureID loadOSDButtons() { ImTextureID id{}; // custom image - std::string path = cfgLoadStr(CFG_SECTION, "image", ""); + std::string path = cfgLoadStr(CFG_SECTION, getButtonsCfgName(), ""); if (loadOSDButtons(path)) return id; - // legacy buttons.png in data folder - if (loadOSDButtons(get_readonly_data_path("buttons.png"))) - return id; - // also try the home folder (android) - if (loadOSDButtons(get_readonly_config_path("buttons.png"))) - return id; + if (settings.platform.isConsole()) + { + // legacy buttons.png in data folder + if (loadOSDButtons(get_readonly_data_path("buttons.png"))) + return id; + // also try the home folder (android) + if (loadOSDButtons(get_readonly_config_path("buttons.png"))) + return id; + } // default in resource size_t size; - std::unique_ptr data = resource::load(BTN_PATH, size); + std::unique_ptr data = resource::load(getButtonsResPath(), size); stbi_set_flip_vertically_on_load(1); int width, height, n; u8 *image_data = stbi_load_from_memory(data.get(), (int)size, &width, &height, &n, STBI_rgb_alpha); if (image_data != nullptr) { try { - id = imguiDriver->updateTexture(BTN_PATH, image_data, width, height, false); + id = imguiDriver->updateTexture(getButtonsResPath(), image_data, width, height, false); } catch (...) { // vulkan can throw during resizing } @@ -144,42 +156,100 @@ static ImTextureID loadOSDButtons() ImTextureID ImguiVGamepadTexture::getId() { - ImTextureID id = imguiDriver->getTexture(BTN_PATH); + ImTextureID id = imguiDriver->getTexture(getButtonsResPath()); if (id == ImTextureID()) id = loadOSDButtons(); return id; } -constexpr float vjoy_sz[2][_Count] = { - // L U R D X Y B A St LT RT Ana Stck FF LU RU LD RD - { 64,64,64,64, 64,64,64,64, 64, 90,90, 128, 64, 64, 64,64,64,64 }, - { 64,64,64,64, 64,64,64,64, 64, 64,64, 128, 64, 64, 64,64,64,64 }, +constexpr float vjoy_tex[_Count][4] = { + // L + { 0, 0, 64, 64 }, + // U + { 64, 0, 64, 64 }, + // R + { 128, 0, 64, 64 }, + // D + { 192, 0, 64, 64 }, + // Y, btn3 + { 256, 0, 64, 64 }, + // X, btn2 + { 320, 0, 64, 64 }, + // B, btn1 + { 384, 0, 64, 64 }, + // A, btn0 + { 448, 0, 64, 64 }, + + // Start + { 0, 64, 64, 64 }, + // LT + { 64, 64, 90, 64 }, + // RT + { 154, 64, 90, 64 }, + // Analog + { 244, 64, 128, 128 }, + // Stick + { 372, 64, 64, 64 }, + // Fast forward + { 436, 64, 64, 64 }, + + // C, btn4 + { 0, 128, 64, 64 }, + // Z, btn5 + { 64, 128, 64, 64 }, + + // service mode + { 0, 192, 64, 64 }, + // insert card + { 64, 192, 64, 64 }, + + // Special controls + // service + { 128, 128, 64, 64 }, + // coin + { 384, 128, 64, 64 }, + // test + { 448, 128, 64, 64 }, }; +static ImVec2 coinUV0, coinUV1; +static ImVec2 serviceUV0, serviceUV1; +static ImVec2 testUV0, testUV1; + constexpr float OSD_TEX_W = 512.f; constexpr float OSD_TEX_H = 256.f; static void setUV() { - float u = 0; - float v = 0; int i = 0; for (auto& control : Controls) { - control.uv0.x = (u + 1) / OSD_TEX_W; - control.uv0.y = 1.f - (v + 1) / OSD_TEX_H; - control.uv1.x = (u + vjoy_sz[0][i] - 1) / OSD_TEX_W; - control.uv1.y = 1.f - (v + vjoy_sz[1][i] - 1) / OSD_TEX_H; - - u += vjoy_sz[0][i]; - if (u >= OSD_TEX_W) { - u -= OSD_TEX_W; - v += vjoy_sz[1][i]; - } + control.uv0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + control.uv0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + control.uv1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + control.uv1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; i++; + if (i >= _VisibleCount) + break; } + serviceUV0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + serviceUV0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + serviceUV1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + serviceUV1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; + i++; + coinUV0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + coinUV0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + coinUV1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + coinUV1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; + i++; + testUV0.x = (vjoy_tex[i][0] + 1) / OSD_TEX_W; + testUV0.y = 1.f - (vjoy_tex[i][1] + 1) / OSD_TEX_H; + testUV1.x = (vjoy_tex[i][0] + vjoy_tex[i][2] - 1) / OSD_TEX_W; + testUV1.y = 1.f - (vjoy_tex[i][1] + vjoy_tex[i][3] - 1) / OSD_TEX_H; + i++; + } static OnLoad _(&setUV); @@ -191,18 +261,6 @@ void hide() { Visible = false; } -void setPosition(ControlId id, float x, float y, float w, float h) -{ - verify(id >= 0 && id < _VisibleCount); - auto& control = Controls[id]; - control.pos.x = x; - control.pos.y = y; - if (w != 0) - control.size.x = w; - if (h != 0) - control.size.y = h; -} - ControlId hitTest(float x, float y) { for (const auto& control : Controls) @@ -215,16 +273,17 @@ ControlId hitTest(float x, float y) u32 controlToDcKey(ControlId control) { + const bool arcade = settings.platform.isArcade(); switch (control) { case Left: return DC_DPAD_LEFT; case Up: return DC_DPAD_UP; case Right: return DC_DPAD_RIGHT; case Down: return DC_DPAD_DOWN; - case X: return DC_BTN_X; - case Y: return DC_BTN_Y; - case B: return DC_BTN_B; - case A: return DC_BTN_A; + case X: return serviceMode ? DC_DPAD2_DOWN : arcade ? DC_BTN_C : DC_BTN_X; + case Y: return arcade ? DC_BTN_X : DC_BTN_Y; + case B: return serviceMode ? DC_DPAD2_UP : DC_BTN_B; + case A: return serviceMode ? DC_BTN_D : DC_BTN_A; case Start: return DC_BTN_START; case LeftTrigger: return DC_AXIS_LT; case RightTrigger: return DC_AXIS_RT; @@ -233,6 +292,11 @@ u32 controlToDcKey(ControlId control) case RightUp: return DC_DPAD_RIGHT | DC_DPAD_UP; case LeftDown: return DC_DPAD_LEFT | DC_DPAD_DOWN; case RightDown: return DC_DPAD_RIGHT | DC_DPAD_DOWN; + // Arcade + case Btn4: return DC_BTN_Y; + case Btn5: return DC_BTN_Z; + case InsertCard: return DC_BTN_INSERT_CARD; + case ServiceMode: return EMU_BTN_SRVMODE; default: return 0; } } @@ -246,6 +310,19 @@ float getControlWidth(ControlId control) { return Controls[control].size.x; } +void toggleServiceMode() +{ + serviceMode = !serviceMode; + if (serviceMode) { + Controls[A].disabled = false; + Controls[B].disabled = false; + Controls[X].disabled = false; + } + else { + startGame(); + } +} + static void drawButtonDim(ImDrawList *drawList, const Control& control, int state) { if (control.disabled) @@ -255,15 +332,37 @@ static void drawButtonDim(ImDrawList *drawList, const Control& control, int stat ImVec2 pos = control.pos * scale_h; ImVec2 size = control.size * scale_h; pos.x += offs_x; - if (static_cast(&control - &Controls[0]) == AnalogStick) + ControlId controlId = static_cast(&control - &Controls[0]); + if (controlId == AnalogStick) pos += StickPos * size; float col = (0.5f - 0.25f * state / 255) * AlphaTrans; float alpha = (100.f - config::VirtualGamepadTransparency) / 100.f * AlphaTrans; ImVec4 color(col, col, col, alpha); + const ImVec2* uv0 = &control.uv0; + const ImVec2* uv1 = &control.uv1; + if (serviceMode) + switch (controlId) + { + case A: + uv0 = &coinUV0; + uv1 = &coinUV1; + break; + case B: + uv0 = &serviceUV0; + uv1 = &serviceUV1; + break; + case X: + uv0 = &testUV0; + uv1 = &testUV1; + break; + default: + break; + } + ImguiVGamepadTexture tex; - tex.draw(drawList, pos, size, control.uv0, control.uv1, color); + tex.draw(drawList, pos, size, *uv0, *uv1, color); } static void drawButton(ImDrawList *drawList, const Control& control, bool state) { @@ -272,7 +371,6 @@ static void drawButton(ImDrawList *drawList, const Control& control, bool state) void draw() { -#ifndef __ANDROID__ if (Controls[Left].pos.x == 0.f) { loadLayout(); @@ -280,7 +378,6 @@ void draw() // mark done Controls[Left].pos.x = 1e-12f; } -#endif ImDrawList *drawList = ImGui::GetBackgroundDrawList(); drawButton(drawList, Controls[Left], kcode[0] & DC_DPAD_LEFT); @@ -288,10 +385,10 @@ void draw() drawButton(drawList, Controls[Right], kcode[0] & DC_DPAD_RIGHT); drawButton(drawList, Controls[Down], kcode[0] & DC_DPAD_DOWN); - drawButton(drawList, Controls[X], kcode[0] & (settings.platform.isConsole() ? DC_BTN_X : DC_BTN_C)); + drawButton(drawList, Controls[X], kcode[0] & (serviceMode ? DC_DPAD2_DOWN : settings.platform.isConsole() ? DC_BTN_X : DC_BTN_C)); drawButton(drawList, Controls[Y], kcode[0] & (settings.platform.isConsole() ? DC_BTN_Y : DC_BTN_X)); - drawButton(drawList, Controls[B], kcode[0] & DC_BTN_B); - drawButton(drawList, Controls[A], kcode[0] & DC_BTN_A); + drawButton(drawList, Controls[B], kcode[0] & (serviceMode ? DC_DPAD2_UP : DC_BTN_B)); + drawButton(drawList, Controls[A], kcode[0] & (serviceMode ? DC_BTN_D : DC_BTN_A)); drawButton(drawList, Controls[Start], kcode[0] & DC_BTN_START); @@ -303,6 +400,12 @@ void draw() drawButton(drawList, Controls[AnalogStick], false); drawButton(drawList, Controls[FastForward], false); + + drawButton(drawList, Controls[Btn4], kcode[0] & DC_BTN_Y); + drawButton(drawList, Controls[Btn5], kcode[0] & DC_BTN_Z); + drawButton(drawList, Controls[ServiceMode], !serviceMode); + drawButton(drawList, Controls[InsertCard], kcode[0] & DC_BTN_INSERT_CARD); + AlphaTrans += ((float)Visible - AlphaTrans) / 2; } @@ -349,6 +452,7 @@ struct LayoutElement void reset() { + applyUiScale(); scale = 1.f; const float dcw = 480.f * (float)settings.display.width / settings.display.height; const float uiscale = getUIScale(); @@ -374,6 +478,11 @@ static LayoutElement Layout[] { { "RT", -32.f,-240.f, 90.f, 64.f }, { "analog", 40.f,-320.f, 128.f, 128.f }, { "fforward", -24.f, 24.f, 64.f, 64.f }, + + { "btn4", -24.f,-216.f, 64.f, 64.f }, + { "btn5", -152.f,-216.f, 64.f, 64.f }, + { "service", -24.f, 96.f, 64.f, 64.f }, + { "inscard", 40.f,-250.f, 64.f, 64.f }, }; static void applyLayout() @@ -440,6 +549,26 @@ static void applyLayout() scale = Layout[Elem_FForward].scale * uiscale; Controls[FastForward].pos = { Layout[Elem_FForward].x * dcw - dx, Layout[Elem_FForward].y * 480.f }; Controls[FastForward].size = { Layout[Elem_FForward].dw * scale, Layout[Elem_FForward].dh * scale }; + + // ARCADE + // Button 4 + scale = Layout[Elem_Btn4].scale * uiscale; + Controls[Btn4].pos = { Layout[Elem_Btn4].x * dcw - dx, Layout[Elem_Btn4].y * 480.f }; + Controls[Btn4].size = { Layout[Elem_Btn4].dw * scale, Layout[Elem_Btn4].dh * scale }; + // Button 5 + scale = Layout[Elem_Btn5].scale * uiscale; + Controls[Btn5].pos = { Layout[Elem_Btn5].x * dcw - dx, Layout[Elem_Btn5].y * 480.f }; + Controls[Btn5].size = { Layout[Elem_Btn5].dw * scale, Layout[Elem_Btn5].dh * scale }; + + // Service Mode + scale = Layout[Elem_ServiceMode].scale * uiscale; + Controls[ServiceMode].pos = { Layout[Elem_ServiceMode].x * dcw - dx, Layout[Elem_ServiceMode].y * 480.f }; + Controls[ServiceMode].size = { Layout[Elem_ServiceMode].dw * scale, Layout[Elem_ServiceMode].dh * scale }; + + // Insert Card + scale = Layout[Elem_InsertCard].scale * uiscale; + Controls[InsertCard].pos = { Layout[Elem_InsertCard].x * dcw - dx, Layout[Elem_InsertCard].y * 480.f }; + Controls[InsertCard].size = { Layout[Elem_InsertCard].dw * scale, Layout[Elem_InsertCard].dh * scale }; } void applyUiScale() { @@ -447,7 +576,7 @@ void applyUiScale() { element.applyUiScale(); } -static void loadLayout() +void loadLayout() { for (auto& element : Layout) { element.reset(); @@ -456,7 +585,7 @@ static void loadLayout() applyLayout(); } -static void saveLayout() +void saveLayout() { cfgSetAutoSave(false); for (auto& element : Layout) @@ -500,11 +629,11 @@ void scaleElement(Element element, float factor) void loadImage(const std::string& path) { if (path.empty()) { - cfgSaveStr(CFG_SECTION, "image", ""); + cfgSaveStr(CFG_SECTION, getButtonsCfgName(), ""); loadOSDButtons(); } else if (loadOSDButtons(path)) { - cfgSaveStr(CFG_SECTION, "image", path); + cfgSaveStr(CFG_SECTION, getButtonsCfgName(), path); } } @@ -554,8 +683,13 @@ static void disableControl(ControlId ctrlId) void startGame() { enableAllControls(); + serviceMode = false; if (settings.platform.isConsole()) { + disableControl(Btn4); + disableControl(Btn5); + disableControl(ServiceMode); + disableControl(InsertCard); switch (config::MapleMainDevices[0]) { case MDT_LightGun: @@ -590,9 +724,17 @@ void startGame() else { // arcade game - // FIXME RT is used as mod key for coin, test, service (ABX) - // FIXME RT and LT are buttons 4 & 5 in arcade mode - // TODO insert card button for card games + if (!card_reader::readerAvailable()) + disableControl(InsertCard); + if (settings.platform.isAtomiswave()) { + disableControl(Btn5); + } + else if (settings.platform.isSystemSP()) + { + disableControl(Y); + disableControl(Btn4); + disableControl(Btn5); + } if (NaomiGameInputs != nullptr) { bool fullAnalog = false; @@ -618,6 +760,14 @@ void startGame() } if (!fullAnalog) disableControl(AnalogArea); + if (!lt) + disableControl(LeftTrigger); + else + disableControl(Btn5); + if (!rt) + disableControl(RightTrigger); + else + disableControl(Btn4); u32 usedButtons = 0; for (const auto& button : NaomiGameInputs->buttons) { @@ -627,21 +777,17 @@ void startGame() } if (settings.platform.isAtomiswave()) { - // button order: A B X Y RT - /* these ones are always needed for now + // button order: A B X Y B4 if ((usedButtons & AWAVE_BTN0_KEY) == 0) disableControl(A); if ((usedButtons & AWAVE_BTN1_KEY) == 0) disableControl(B); if ((usedButtons & AWAVE_BTN2_KEY) == 0) disableControl(X); - if ((usedButtons & AWAVE_BTN4_KEY) == 0 && !rt) - disableControl(RightTrigger); - */ if ((usedButtons & AWAVE_BTN3_KEY) == 0) disableControl(Y); - if (!lt) - disableControl(LeftTrigger); + if ((usedButtons & AWAVE_BTN4_KEY) == 0) + disableControl(Btn4); if ((usedButtons & AWAVE_UP_KEY) == 0) disableControl(Up); if ((usedButtons & AWAVE_DOWN_KEY) == 0) @@ -650,10 +796,30 @@ void startGame() disableControl(Left); if ((usedButtons & AWAVE_RIGHT_KEY) == 0) disableControl(Right); + if ((usedButtons & AWAVE_START_KEY) == 0) + disableControl(Start); + } + else if (settings.platform.isSystemSP()) + { + if ((usedButtons & DC_BTN_A) == 0) + disableControl(A); + if ((usedButtons & DC_BTN_B) == 0) + disableControl(B); + if ((usedButtons & DC_BTN_C) == 0) + disableControl(X); + if ((usedButtons & DC_DPAD_UP) == 0) + disableControl(Up); + if ((usedButtons & DC_DPAD_DOWN) == 0) + disableControl(Down); + if ((usedButtons & DC_DPAD_LEFT) == 0) + disableControl(Left); + if ((usedButtons & DC_DPAD_RIGHT) == 0) + disableControl(Right); + if ((usedButtons & DC_BTN_START) == 0) + disableControl(Start); } else { - /* these ones are always needed for now if ((usedButtons & NAOMI_BTN0_KEY) == 0) disableControl(A); if ((usedButtons & NAOMI_BTN1_KEY) == 0) @@ -661,16 +827,15 @@ void startGame() if ((usedButtons & NAOMI_BTN2_KEY) == 0) // C disableControl(X); - if ((usedButtons & NAOMI_BTN4_KEY) == 0 && !rt) - // Y - disableControl(RightTrigger); - */ if ((usedButtons & NAOMI_BTN3_KEY) == 0) // X disableControl(Y); - if ((usedButtons & NAOMI_BTN5_KEY) == 0 && !lt) + if ((usedButtons & NAOMI_BTN4_KEY) == 0) + // Y + disableControl(Btn4); + if ((usedButtons & NAOMI_BTN5_KEY) == 0) // Z - disableControl(LeftTrigger); + disableControl(Btn5); if ((usedButtons & NAOMI_UP_KEY) == 0) disableControl(Up); if ((usedButtons & NAOMI_DOWN_KEY) == 0) @@ -679,28 +844,41 @@ void startGame() disableControl(Left); if ((usedButtons & NAOMI_RIGHT_KEY) == 0) disableControl(Right); + if ((usedButtons & NAOMI_START_KEY) == 0) + disableControl(Start); } } - else if (settings.input.lightgunGame) - { - disableControl(Y); - disableControl(AnalogArea); - disableControl(LeftTrigger); - disableControl(Up); - disableControl(Down); - disableControl(Left); - disableControl(Right); - } else { - // all analog games *should* have an input description - disableControl(AnalogArea); + if (settings.input.lightgunGame) + { + // TODO enable mouse? + disableControl(A); + disableControl(X); + disableControl(Y); + disableControl(Btn4); + disableControl(Btn5); + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(RightTrigger); + disableControl(Up); + disableControl(Down); + disableControl(Left); + disableControl(Right); + } + else + { + // all analog games *should* have an input description + disableControl(AnalogArea); + disableControl(LeftTrigger); + disableControl(RightTrigger); + } } } - bool enabledState[_Count]; - for (int i = 0; i < _Count; i++) - enabledState[i] = !Controls[i].disabled; - setEnabledControls(enabledState); +} + +void resetEditing() { + resetLayout(); } #ifndef __ANDROID__ @@ -721,13 +899,6 @@ void stopEditing(bool canceled) saveLayout(); } -void resetEditing() { - resetLayout(); -} - -void setEnabledControls(bool enabled[_Count]) { -} - #endif } // namespace vgamepad diff --git a/core/ui/vgamepad.h b/core/ui/vgamepad.h index 86b099aa77..d97fb21f0d 100644 --- a/core/ui/vgamepad.h +++ b/core/ui/vgamepad.h @@ -40,13 +40,18 @@ enum ControlId AnalogStick, FastForward, + Btn4, + Btn5, + ServiceMode, + InsertCard, + LeftUp, RightUp, LeftDown, RightDown, _Count, - _VisibleCount = FastForward + 1, + _VisibleCount = LeftUp, }; enum Element @@ -59,6 +64,10 @@ enum Element Elem_RT, Elem_Analog, Elem_FForward, + Elem_Btn4, + Elem_Btn5, + Elem_ServiceMode, + Elem_InsertCard, }; class ImguiVGamepadTexture : public ImguiTexture @@ -69,8 +78,6 @@ class ImguiVGamepadTexture : public ImguiTexture #if defined(__ANDROID__) || defined(TARGET_IPHONE) -void setPosition(ControlId id, float x, float y, float w = 0.f, float h = 0.f); // Legacy android -void setEnabledControls(bool enabled[_Count]); // Legacy android void enableAllControls(); void show(); void hide(); @@ -87,11 +94,14 @@ ControlId hitTest(float x, float y); u32 controlToDcKey(ControlId control); void setAnalogStick(float x, float y); float getControlWidth(ControlId); +void toggleServiceMode(); void applyUiScale(); Element layoutHitTest(float x, float y); void translateElement(Element element, float dx, float dy); void scaleElement(Element element, float factor); +void loadLayout(); +void saveLayout(); #else diff --git a/resources/picture/buttons-arcade.png b/resources/picture/buttons-arcade.png new file mode 100644 index 0000000000..980212cffc Binary files /dev/null and b/resources/picture/buttons-arcade.png differ diff --git a/resources/picture/buttons-arcade.svg b/resources/picture/buttons-arcade.svg new file mode 100644 index 0000000000..db982d2ffb --- /dev/null +++ b/resources/picture/buttons-arcade.svg @@ -0,0 +1,2279 @@ + + + +124356S$T111111LTRT diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java index 4766949585..01505f39c9 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java @@ -49,7 +49,6 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat. private static final int AUDIO_PERM_REQUEST = 1002; protected SharedPreferences prefs; - protected float[][] vjoy_d_cached; // Used for VJoy editing private AudioBackend audioBackend; protected Handler handler = new Handler(); private boolean audioPermissionRequested = false; @@ -406,7 +405,7 @@ else if (requestCode == STORAGE_PERM_REQUEST) { } //setup mic - if (Emulator.micPluggedIn()) + if (InputDeviceManager.isMicPluggedIn()) requestRecordAudioPermission(); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java index 28d8ba14e2..5a089d2bf0 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/Emulator.java @@ -10,7 +10,8 @@ import androidx.appcompat.app.AppCompatDelegate; import com.flycast.emulator.config.Config; -import com.flycast.emulator.emu.JNIdc; +import com.flycast.emulator.emu.VGamepad; +import com.flycast.emulator.periph.InputDeviceManager; public class Emulator extends Application { private static Context context; @@ -18,32 +19,14 @@ public class Emulator extends Application { private WifiManager wifiManager = null; private WifiManager.MulticastLock multicastLock = null; - // see MapleDeviceType in hw/maple/maple_devs.h - public static final int MDT_Microphone = 2; - public static final int MDT_None = 8; - public static int vibrationPower = 80; - public static int[] maple_devices = { - MDT_None, - MDT_None, - MDT_None, - MDT_None - }; - public static int[][] maple_expansion_devices = { - { MDT_None, MDT_None }, - { MDT_None, MDT_None }, - { MDT_None, MDT_None }, - { MDT_None, MDT_None }, - }; - /** * Load the settings from native code * */ public void getConfigurationPrefs() { - Emulator.vibrationPower = JNIdc.getVirtualGamepadVibration(); - JNIdc.getControllers(maple_devices, maple_expansion_devices); + Emulator.vibrationPower = VGamepad.getVibrationPower(); } /** @@ -54,26 +37,17 @@ public void SaveAndroidSettings(String homeDirectory) { Log.i("flycast", "SaveAndroidSettings: saving preferences"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - Emulator.vibrationPower = JNIdc.getVirtualGamepadVibration(); - JNIdc.getControllers(maple_devices, maple_expansion_devices); + Emulator.vibrationPower = VGamepad.getVibrationPower(); prefs.edit() .putString(Config.pref_home, homeDirectory).apply(); - if (micPluggedIn() && currentActivity instanceof BaseGLActivity) { + if (InputDeviceManager.isMicPluggedIn() && currentActivity instanceof BaseGLActivity) { + Log.i("flycast", "SaveAndroidSettings: MIC PLUGGED IN"); ((BaseGLActivity)currentActivity).requestRecordAudioPermission(); } } - public static boolean micPluggedIn() { - JNIdc.getControllers(maple_devices, maple_expansion_devices); - for (int[] maple_expansion_device : maple_expansion_devices) - if (maple_expansion_device[0] == MDT_Microphone - || maple_expansion_device[1] == MDT_Microphone) - return true; - return false; - } - @Override public void onCreate() { super.onCreate(); diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java index 4f84b6e2b5..a729a92b65 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/NativeGLActivity.java @@ -16,10 +16,8 @@ import androidx.annotation.Nullable; -import com.flycast.emulator.emu.JNIdc; import com.flycast.emulator.emu.NativeGLView; import com.flycast.emulator.periph.InputDeviceManager; -import com.flycast.emulator.periph.VJoy; public final class NativeGLActivity extends BaseGLActivity { @@ -61,38 +59,35 @@ public boolean isSurfaceReady() { return mView != null && mView.isSurfaceReady(); } - // Called from native code - private void VJoyStartEditing() { - vjoy_d_cached = VJoy.readCustomVjoyValues(getApplicationContext()); - JNIdc.showVirtualGamepad(); - mView.setEditVjoyMode(true); + @Override + public void onGameStateChange(boolean started) { + super.onGameStateChange(started); + runOnUiThread(new Runnable() { + public void run() { + if (started) + mView.showVGamepad(); + } + }); } + // Called from native code - private void VJoyResetEditing() { - VJoy.resetCustomVjoyValues(getApplicationContext()); - mView.readCustomVjoyValues(); - mView.resetEditMode(); + private void VJoyStartEditing() { handler.post(new Runnable() { @Override public void run() { - mView.requestLayout(); + mView.setEditVjoyMode(true); } }); } // Called from native code - private void VJoyStopEditing(final boolean canceled) { + private void VJoyStopEditing() { handler.post(new Runnable() { @Override public void run() { - if (canceled) - mView.restoreCustomVjoyValues(vjoy_d_cached); mView.setEditVjoyMode(false); } }); } - private void VJoyEnableControls(boolean[] state) { - mView.enableVjoy(state); - } // On-screen keyboard borrowed from SDL core android code class ShowTextInputTask implements Runnable { diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java new file mode 100644 index 0000000000..422dfd1456 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/EditVirtualJoystickDelegate.java @@ -0,0 +1,111 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +package com.flycast.emulator.emu; + +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +import androidx.annotation.NonNull; + +public class EditVirtualJoystickDelegate implements TouchEventHandler +{ + private View view; + private ScaleGestureDetector scaleGestureDetector; + private int currentElement = -1; + private float lastX, lastY; + + public EditVirtualJoystickDelegate(View view) { + this.view = view; + scaleGestureDetector = new ScaleGestureDetector(view.getContext(), new EditVirtualJoystickDelegate.ScaleGestureListener()); + } + + @Override + public void stop() { + } + @Override + public void show() { + VGamepad.show(); + } + + @Override + public boolean onTouchEvent(MotionEvent event, int width, int height) + { + scaleGestureDetector.onTouchEvent(event); + if (scaleGestureDetector.isInProgress()) + return true; + + int actionMasked = event.getActionMasked(); + int actionIndex = event.getActionIndex(); + switch (actionMasked) + { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + currentElement = -1; + break; + + case MotionEvent.ACTION_DOWN: + lastX = event.getX(actionIndex) / view.getWidth(); + lastY = event.getY(actionIndex) / view.getHeight(); + currentElement = VGamepad.layoutHitTest(lastX, lastY); + return currentElement != -1; + + case MotionEvent.ACTION_MOVE: + if (currentElement != -1 && event.getPointerCount() == 1) + { + float x = event.getX(actionIndex) / view.getWidth(); + float y = event.getY(actionIndex) / view.getHeight(); + VGamepad.translateElement(currentElement, x - lastX, y - lastY); + lastX = x; + lastY = y; + return true; + } + break; + + default: + break; + } + + return false; + } + + private class ScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener { + private int elemId = -1; + @Override + public boolean onScaleBegin(@NonNull ScaleGestureDetector detector) + { + elemId = VGamepad.layoutHitTest(detector.getFocusX() / view.getWidth(), detector.getFocusY() / view.getHeight()); + return elemId != -1; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) + { + if (elemId == -1) + return false; + VGamepad.scaleElement(elemId, detector.getScaleFactor()); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + elemId = -1; + } + } +} diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java index 45aecceea3..ff06fd2376 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/JNIdc.java @@ -18,12 +18,7 @@ public final class JNIdc public static native void rendinitNative(Surface surface, int w, int h); - public static native void vjoy(int id, float x, float y, float w, float h); - - public static native void getControllers(int[] controllers, int[][] peripherals); - public static native void setupMic(SipEmulator sip); - public static native int getVirtualGamepadVibration(); public static native void screenCharacteristics(float screenDpi, float refreshRate); public static native void guiOpenSettings(); @@ -31,6 +26,4 @@ public final class JNIdc public static native boolean guiIsContentBrowser(); public static native void guiSetInsets(int left, int right, int top, int bottom); - public static native void showVirtualGamepad(); - public static native void hideVirtualGamepad(); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java index b70cda441c..da10864e1c 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/NativeGLView.java @@ -10,6 +10,7 @@ import android.util.Log; import android.view.Display; import android.view.DisplayCutout; +import android.view.InputDevice; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -24,11 +25,7 @@ public class NativeGLView extends SurfaceView implements SurfaceHolder.Callback { private boolean surfaceReady = false; private boolean paused = false; - VirtualJoystickDelegate vjoyDelegate; - - public void restoreCustomVjoyValues(float[][] vjoy_d_cached) { - vjoyDelegate.restoreCustomVjoyValues(vjoy_d_cached); - } + private TouchEventHandler vjoyDelegate = null; public NativeGLView(Context context) { this(context, null); @@ -63,20 +60,21 @@ public void onSystemUiVisibilityChange(int visibility) { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - vjoyDelegate = new VirtualJoystickDelegate(this); + if (InputDeviceManager.getInstance().hasTouchscreen()) + vjoyDelegate = new VirtualJoystickDelegate(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - vjoyDelegate.stop(); + if (vjoyDelegate != null) + vjoyDelegate.stop(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - vjoyDelegate.layout(getWidth(), getHeight()); DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); Display d; @@ -101,10 +99,6 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto } } - public void resetEditMode() { - vjoyDelegate.resetEditMode(); - } - @Override public boolean onTouchEvent(final MotionEvent event) { @@ -113,13 +107,13 @@ public boolean onTouchEvent(final MotionEvent event) InputDeviceManager.getInstance().mouseEvent(Math.round(event.getX()), Math.round(event.getY()), event.getButtonState()); return true; } - else + if (vjoyDelegate != null && (event.getSource() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN) return vjoyDelegate.onTouchEvent(event, getWidth(), getHeight()); + return false; } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { - } @Override @@ -179,14 +173,18 @@ public void onWindowFocusChanged(boolean hasFocus) { } } - public void readCustomVjoyValues() { - vjoyDelegate.readCustomVjoyValues(); + public void setEditVjoyMode(boolean editVjoyMode) + { + if (!InputDeviceManager.getInstance().hasTouchscreen()) + return; + if (editVjoyMode && !(vjoyDelegate instanceof EditVirtualJoystickDelegate)) + vjoyDelegate = new EditVirtualJoystickDelegate(this); + else if (!editVjoyMode && !(vjoyDelegate instanceof VirtualJoystickDelegate)) + vjoyDelegate = new VirtualJoystickDelegate(this); } - public void setEditVjoyMode(boolean editVjoyMode) { - vjoyDelegate.setEditVjoyMode(editVjoyMode); - } - public void enableVjoy(boolean[] state) { - vjoyDelegate.enableVjoy(state); + public void showVGamepad() { + if (vjoyDelegate != null) + vjoyDelegate.show(); } } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/TouchEventHandler.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/TouchEventHandler.java new file mode 100644 index 0000000000..40f3513371 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/TouchEventHandler.java @@ -0,0 +1,27 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +package com.flycast.emulator.emu; + +import android.view.MotionEvent; + +public interface TouchEventHandler { + boolean onTouchEvent(MotionEvent event, int width, int height); + void stop(); + void show(); +} diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VGamepad.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VGamepad.java new file mode 100644 index 0000000000..4b85c8ce68 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VGamepad.java @@ -0,0 +1,35 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +package com.flycast.emulator.emu; + +public class VGamepad +{ + static { System.loadLibrary("flycast"); } + + public static native int getVibrationPower(); + + public static native void show(); + public static native void hide(); + public static native int hitTest(float x, float y); + public static native float getControlWidth(int controlId); + + public static native int layoutHitTest(float x, float y); + public static native void scaleElement(int elemId, float scale); + public static native void translateElement(int elemId, float x, float y); +} diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java index 813ef79815..a0276225f7 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/emu/VirtualJoystickDelegate.java @@ -1,409 +1,273 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ package com.flycast.emulator.emu; import android.content.Context; -import android.content.res.Configuration; import android.os.Handler; -import android.view.InputDevice; import android.view.MotionEvent; -import android.view.ScaleGestureDetector; import android.view.View; import com.flycast.emulator.periph.InputDeviceManager; -import com.flycast.emulator.periph.VJoy; import com.flycast.emulator.periph.VibratorThread; -import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; -public class VirtualJoystickDelegate { - private VibratorThread vibratorThread; - - private boolean editVjoyMode = false; - private int selectedVjoyElement = VJoy.ELEM_NONE; - private ScaleGestureDetector scaleGestureDetector; +public class VirtualJoystickDelegate implements TouchEventHandler +{ + private static final int CTLID_ANARING = 11; + private static final int CTLID_ANASTICK = 12; + private VibratorThread vibratorThread; private Handler handler = new Handler(); private Runnable hideVGamepadRunnable = new Runnable() { @Override public void run() { - JNIdc.hideVirtualGamepad(); + VGamepad.hide(); } }; - - private float[][] vjoy_d_custom; - private boolean[] vjoy_enabled; - - private static final float[][] vjoy = VJoy.baseVJoy(); - private Context context; private View view; + private int joyPointerId = -1; + private float joyBiasX, joyBiasY; + private Map pidToControlId = new HashMap<>(); + private int mouseButtons = 0; + private int[] mousePos = { -32768, -32768 }; + private int mousePid = -1; public VirtualJoystickDelegate(View view) { this.view = view; this.context = view.getContext(); vibratorThread = VibratorThread.getInstance(); - - readCustomVjoyValues(); - vjoy_enabled = new boolean[VJoy.VJoyCount + 4]; // include diagonals - Arrays.fill(vjoy_enabled, true); - scaleGestureDetector = new ScaleGestureDetector(context, new OscOnScaleGestureListener()); } + @Override public void stop() { vibratorThread.stopThread(); vibratorThread = null; } - public void readCustomVjoyValues() { - vjoy_d_custom = VJoy.readCustomVjoyValues(context); - } - - public void restoreCustomVjoyValues(float[][] vjoy_d_cached) { - vjoy_d_custom = vjoy_d_cached; - VJoy.writeCustomVjoyValues(vjoy_d_cached, context); - - resetEditMode(); - view.requestLayout(); - } - - private void reset_analog() - { - - int j=11; - vjoy[j+1][0]=vjoy[j][0]+vjoy[j][2]/2-vjoy[j+1][2]/2; - vjoy[j+1][1]=vjoy[j][1]+vjoy[j][3]/2-vjoy[j+1][3]/2; - JNIdc.vjoy(j+1, vjoy[j+1][0], vjoy[j+1][1], vjoy[j+1][2], vjoy[j+1][3]); - } - - private int get_anal(int j, int axis) - { - return (int) (((vjoy[j+1][axis]+vjoy[j+1][axis+2]/2) - vjoy[j][axis] - vjoy[j][axis+2]/2)*254/vjoy[j][axis+2]); - } - - private float vbase(float p, float m, float scl) - { - return (int) ( m - (m -p)*scl); - } - - private float vbase(float p, float scl) + private boolean touchMouseEvent(MotionEvent event) { - return (int) (p*scl ); - } - - private boolean isTablet() { - return (context.getResources().getConfiguration().screenLayout - & Configuration.SCREENLAYOUT_SIZE_MASK) - >= Configuration.SCREENLAYOUT_SIZE_LARGE; - } - - public void layout(int width, int height) - { - //dcpx/cm = dcpx/px * px/cm - float magic = isTablet() ? 0.8f : 0.7f; - float scl = 480.0f / height * context.getResources().getDisplayMetrics().density * magic; - float scl_dc = height / 480.0f; - float tx = (width - 640.0f * scl_dc) / 2 / scl_dc; - - float a_x = -tx + 24 * scl; - float a_y = -24 * scl; - - // Not sure how this can happen - if (vjoy_d_custom == null) - return; - - float[][] vjoy_d = VJoy.getVjoy_d(vjoy_d_custom); - - for (int i=0;i vjoy[j][0] && x <= (vjoy[j][0] + vjoy[j][2]) - && y > vjoy[j][1] && y <= (vjoy[j][1] + vjoy[j][3])) - { - if (vjoy[j][4] >= VJoy.BTN_RTRIG) { - // Not for analog - if (vjoy[j][5] == 0) - if (!editVjoyMode) { - vibratorThread.click(); - } - vjoy[j][5] = 2; - } - - - if (vjoy[j][4] == VJoy.BTN_ANARING) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_ANALOG; - resetEditMode(); - } else { - vjoy[j + 1][0] = x - vjoy[j + 1][2] / 2; - vjoy[j + 1][1] = y - vjoy[j + 1][3] / 2; - - JNIdc.vjoy(j + 1, vjoy[j + 1][0], vjoy[j + 1][1], vjoy[j + 1][2], vjoy[j + 1][3]); - anal_id = event.getPointerId(i); - } - } else if (vjoy[j][4] != VJoy.BTN_ANAPOINT) { - if (vjoy[j][4] == VJoy.BTN_LTRIG) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_LTRIG; - resetEditMode(); - } else { - left_trigger = 255; - lt_id = event.getPointerId(i); - } - } else if (vjoy[j][4] == VJoy.BTN_RTRIG) { - if (editVjoyMode) { - selectedVjoyElement = VJoy.ELEM_RTRIG; - resetEditMode(); - } else { - right_trigger = 255; - rt_id = event.getPointerId(i); - } - } else { - if (editVjoyMode) { - selectedVjoyElement = getElementIdFromButtonId(j); - resetEditMode(); - } else if (vjoy[j][4] == VJoy.key_CONT_FFORWARD) - fastForward = true; - else - rv &= ~(int)vjoy[j][4]; - } - } - } - } - } else if (vjoy_enabled[11]) { - // Analog stick - if (x < vjoy[11][0]) - x = vjoy[11][0]; - else if (x > (vjoy[11][0] + vjoy[11][2])) - x = vjoy[11][0] + vjoy[11][2]; - - if (y < vjoy[11][1]) - y = vjoy[11][1]; - else if (y > (vjoy[11][1] + vjoy[11][3])) - y = vjoy[11][1] + vjoy[11][3]; - - int j = 11; - vjoy[j + 1][0] = x - vjoy[j + 1][2] / 2; - vjoy[j + 1][1] = y - vjoy[j + 1][3] / 2; - - JNIdc.vjoy(j + 1, vjoy[j + 1][0], vjoy[j + 1][1], vjoy[j + 1][2], vjoy[j + 1][3]); - - } - } - - for (int j = 0; j < vjoy.length; j++) { - if (vjoy[j][5] == 2) - vjoy[j][5] = 1; - else if (vjoy[j][5] == 1) - vjoy[j][5] = 0; - } - } - - switch(aid) + int actionIndex = event.getActionIndex(); + switch (event.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - selectedVjoyElement = -1; - reset_analog(); - anal_id = -1; - rv = 0xFFFFFFFF; - fastForward = false; - right_trigger = 0; - left_trigger = 0; - lt_id = -1; - rt_id = -1; - for (int j= 0 ;j < vjoy.length; j++) - vjoy[j][5] = 0; - mouse_btns = 0; + // Release all + pidToControlId.clear(); + joyPointerId = -1; + InputDeviceManager.getInstance().virtualReleaseAll(); break; - case MotionEvent.ACTION_POINTER_UP: - if (event.getPointerId(event.getActionIndex())==anal_id) - { - reset_analog(); - anal_id = -1; - } - else if (event.getPointerId(event.getActionIndex())==lt_id) - { - left_trigger = 0; - lt_id = -1; - } - else if (event.getPointerId(event.getActionIndex())==rt_id) + case MotionEvent.ACTION_DOWN: + // First release all + pidToControlId.clear(); + joyPointerId = -1; + InputDeviceManager.getInstance().virtualReleaseAll(); + // Release the mouse too + mousePid = -1; + mouseButtons = 0; + InputDeviceManager.getInstance().mouseEvent(mousePos[0], mousePos[1], mouseButtons); + // Then fall through + case MotionEvent.ACTION_POINTER_DOWN: + { + Point p = new Point(event.getX(actionIndex), event.getY(actionIndex)); + p = translateCoords(p, new Point(width, height)); + int control = VGamepad.hitTest(p.x, p.y); + if (control != -1) { - right_trigger = 0; - rt_id = -1; + int pid = event.getPointerId(actionIndex); + if (control == CTLID_ANARING || control == CTLID_ANASTICK) + { + if (joyPointerId == -1) + { + // Analog stick down + joyPointerId = pid; + joyBiasX = p.x; + joyBiasY = p.y; + InputDeviceManager.getInstance().virtualJoystick(0, 0); + return true; + } + } + else + { + // Button down + InputDeviceManager.getInstance().virtualButtonInput(control, true); + pidToControlId.put(pid, control); + vibratorThread.click(); + return true; + } } break; + } - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_DOWN: - if (event.getPointerCount() != 1) - { - mouse_btns = 0; - } - else + case MotionEvent.ACTION_MOVE: + for (int i = 0; i < event.getPointerCount(); i++) { - mouse_pos[0] = Math.round(event.getX()); - mouse_pos[1] = Math.round(event.getY()); - mouse_btns = MotionEvent.BUTTON_PRIMARY; // Mouse left button down + int pid = event.getPointerId(i); + Point p = new Point(event.getX(i), event.getY(i)); + p = translateCoords(p, new Point(width, height)); + if (joyPointerId == pid) + { + // Analog stick + float dx = p.x - joyBiasX; + float dy = p.y - joyBiasY; + float sz = VGamepad.getControlWidth(CTLID_ANASTICK); + dx = Math.max(Math.min(1.f, dx / sz), -1.f); + dy = Math.max(Math.min(1.f, dy / sz), -1.f); + InputDeviceManager.getInstance().virtualJoystick(dx, dy); + continue; + } + // Buttons + int control = VGamepad.hitTest(p.x, p.y); + int oldControl = pidToControlId.containsKey(pid) ? pidToControlId.get(pid) : -1; + if (oldControl == control) + // same button still pressed, or none at all + continue; + if (oldControl != -1) { + // Previous button up + InputDeviceManager.getInstance().virtualButtonInput(oldControl, false); + pidToControlId.remove(pid); + } + if (control != -1 && control != CTLID_ANARING && control != CTLID_ANASTICK) + { + // New button down + InputDeviceManager.getInstance().virtualButtonInput(control, true); + pidToControlId.put(pid, control); + vibratorThread.click(); + } } break; - case MotionEvent.ACTION_MOVE: - if (event.getPointerCount() == 1) + case MotionEvent.ACTION_POINTER_UP: + { + int pid = event.getPointerId(actionIndex); + if (joyPointerId == pid) + { + // Analog up + InputDeviceManager.getInstance().virtualJoystick(0, 0); + joyPointerId = -1; + return true; + } + if (pidToControlId.containsKey(pid)) { - mouse_pos[0] = Math.round(event.getX()); - mouse_pos[1] = Math.round(event.getY()); + // Button up + int controlId = pidToControlId.get(pid); + InputDeviceManager.getInstance().virtualButtonInput(controlId, false); + return true; } break; - } - int joyx = get_anal(11, 0); - int joyy = get_anal(11, 1); - InputDeviceManager.getInstance().virtualGamepadEvent(rv, joyx, joyy, left_trigger, right_trigger, fastForward); - // Only register the mouse event if no virtual gamepad button is down - if (!editVjoyMode && ((rv == 0xFFFFFFFF && left_trigger == 0 && right_trigger == 0 && joyx == 0 && joyy == 0 && !fastForward) - || JNIdc.guiIsOpen())) - InputDeviceManager.getInstance().mouseEvent(mouse_pos[0], mouse_pos[1], mouse_btns); - return(true); - } - - public void setEditVjoyMode(boolean editVjoyMode) { - this.editVjoyMode = editVjoyMode; - selectedVjoyElement = -1; - if (editVjoyMode) { - this.handler.removeCallbacks(hideVGamepadRunnable); - Arrays.fill(vjoy_enabled, true); - } - resetEditMode(); - } - - public void enableVjoy(boolean[] state) { - vjoy_enabled = state; - } - - private class OscOnScaleGestureListener extends - ScaleGestureDetector.SimpleOnScaleGestureListener { - - @Override - public boolean onScale(ScaleGestureDetector detector) { - if (editVjoyMode && selectedVjoyElement != -1) { - vjoy_d_custom[selectedVjoyElement][2] *= detector.getScaleFactor(); - view.requestLayout(); - - return true; } - - return false; } + return touchMouseEvent(event); + } - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - selectedVjoyElement = -1; - } + @Override + public void show() + { + VGamepad.show(); + this.handler.removeCallbacks(hideVGamepadRunnable); + this.handler.postDelayed(hideVGamepadRunnable, 10000); } } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java index a8078f0634..09947d9440 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/InputDeviceManager.java @@ -24,6 +24,8 @@ public final class InputDeviceManager implements InputManager.InputDeviceListene private InputManager inputManager; private int maple_port = 0; + private boolean hasTouchscreen = false; + private static class VibrationParams { float power; float inclination; @@ -39,9 +41,10 @@ public InputDeviceManager() public void startListening(Context applicationContext) { maple_port = 0; - if (applicationContext.getPackageManager().hasSystemFeature("android.hardware.touchscreen")) - joystickAdded(VIRTUAL_GAMEPAD_ID, "Virtual Gamepad", 0, "virtual_gamepad_uid", - new int[0], new int[0], getVibrator(VIRTUAL_GAMEPAD_ID) != null); + hasTouchscreen = applicationContext.getPackageManager().hasSystemFeature("android.hardware.touchscreen"); + if (hasTouchscreen) + joystickAdded(VIRTUAL_GAMEPAD_ID, null, 0, null, + null, null, getVibrator(VIRTUAL_GAMEPAD_ID) != null); int[] ids = InputDevice.getDeviceIds(); for (int id : ids) onInputDeviceAdded(id); @@ -202,12 +205,18 @@ public void stopRumble() } } + public boolean hasTouchscreen() { + return hasTouchscreen; + } + public static InputDeviceManager getInstance() { return INSTANCE; } public native void init(); - public native void virtualGamepadEvent(int kcode, int joyx, int joyy, int lt, int rt, boolean fastForward); + public native void virtualReleaseAll(); + public native void virtualJoystick(float x, float y); + public native void virtualButtonInput(int key, boolean pressed); public native boolean joystickButtonEvent(int id, int button, boolean pressed); public native boolean joystickAxisEvent(int id, int button, int value); public native void mouseEvent(int xpos, int ypos, int buttons); @@ -216,4 +225,5 @@ public static InputDeviceManager getInstance() { private native void joystickRemoved(int id); public native boolean keyboardEvent(int key, boolean pressed); public native void keyboardText(int c); + public static native boolean isMicPluggedIn(); } diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VJoy.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VJoy.java deleted file mode 100644 index c94950aed8..0000000000 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/periph/VJoy.java +++ /dev/null @@ -1,219 +0,0 @@ -package com.flycast.emulator.periph; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -public class VJoy { - - public static final int key_CONT_C = 0x0001; - public static final int key_CONT_B = 0x0002; - public static final int key_CONT_A = 0x0004; - public static final int key_CONT_START = 0x0008; - public static final int key_CONT_DPAD_UP = 0x0010; - public static final int key_CONT_DPAD_DOWN = 0x0020; - public static final int key_CONT_DPAD_LEFT = 0x0040; - public static final int key_CONT_DPAD_RIGHT = 0x0080; - public static final int key_CONT_Y = 0x0200; - public static final int key_CONT_X = 0x0400; - public static final int key_CONT_FFORWARD = 0x3000002; - - public static final int BTN_LTRIG = -1; - public static final int BTN_RTRIG = -2; - public static final int BTN_ANARING = -3; - public static final int BTN_ANAPOINT = -4; - - public static final int ELEM_NONE = -1; - public static final int ELEM_DPAD = 0; - public static final int ELEM_BUTTONS = 1; - public static final int ELEM_START = 2; - public static final int ELEM_LTRIG = 3; - public static final int ELEM_RTRIG = 4; - public static final int ELEM_ANALOG = 5; - public static final int ELEM_FFORWARD = 6; - - public static int VJoyCount = 14; - - public static float[][] baseVJoy() { - return new float[][] { - new float[] { 24, 24+64, 64,64, key_CONT_DPAD_LEFT, 0}, - new float[] { 24+64, 24, 64,64, key_CONT_DPAD_UP, 0}, - new float[] { 24+128, 24+64, 64,64, key_CONT_DPAD_RIGHT, 0}, - new float[] { 24+64, 24+128, 64,64, key_CONT_DPAD_DOWN, 0}, - - new float[] { 440, 280+64, 64,64, key_CONT_X, 0}, - new float[] { 440+64, 280, 64,64, key_CONT_Y, 0}, - new float[] { 440+128, 280+64, 64,64, key_CONT_B, 0}, - new float[] { 440+64, 280+128,64,64, key_CONT_A, 0}, - - new float[] { 320-32, 360+32, 64,64, key_CONT_START, 0}, - - new float[] { 440, 200, 90,64, BTN_LTRIG, 0}, // LT - new float[] { 542, 200, 90,64, BTN_RTRIG, 0}, // RT - - new float[] { 0, 128+224,128,128,BTN_ANARING, 0}, // Analog ring - new float[] { 32, 128+256,64,64, BTN_ANAPOINT, 0}, // Analog point - - new float[] { 320-32, 12, 64,64, key_CONT_FFORWARD, 0}, // Fast-forward - - new float[] { 20, 288, 64,64, key_CONT_DPAD_LEFT|key_CONT_DPAD_UP, 0}, // DPad diagonals - new float[] { 20+128, 288, 64,64, key_CONT_DPAD_RIGHT|key_CONT_DPAD_UP, 0}, - new float[] { 20, 288+128,64,64, key_CONT_DPAD_LEFT|key_CONT_DPAD_DOWN, 0}, - new float[] { 20+128, 288+128,64,64, key_CONT_DPAD_RIGHT|key_CONT_DPAD_DOWN, 0}, - }; - } - - public static float[][] readCustomVjoyValues(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - return new float[][] { - // x-shift, y-shift, sizing-factor - new float[] { prefs.getFloat("touch_x_shift_dpad", 0), - prefs.getFloat("touch_y_shift_dpad", 0), - prefs.getFloat("touch_scale_dpad", 1) - }, // DPAD - new float[] { prefs.getFloat("touch_x_shift_buttons", 0), - prefs.getFloat("touch_y_shift_buttons", 0), - prefs.getFloat("touch_scale_buttons", 1) - }, // X, Y, B, A Buttons - new float[] { prefs.getFloat("touch_x_shift_start", 0), - prefs.getFloat("touch_y_shift_start", 0), - prefs.getFloat("touch_scale_start", 1) - }, // Start - new float[] { prefs.getFloat("touch_x_shift_left_trigger", 0), - prefs.getFloat("touch_y_shift_left_trigger", 0), - prefs.getFloat("touch_scale_left_trigger", 1) - }, // Left Trigger - new float[] { prefs.getFloat("touch_x_shift_right_trigger", 0), - prefs.getFloat("touch_y_shift_right_trigger", 0), - prefs.getFloat("touch_scale_right_trigger", 1) - }, // Right Trigger - new float[] { prefs.getFloat("touch_x_shift_analog", 0), - prefs.getFloat("touch_y_shift_analog", 0), - prefs.getFloat("touch_scale_analog", 1) - }, // Analog Stick - new float[] { prefs.getFloat("touch_x_shift_fforward", 0), - prefs.getFloat("touch_y_shift_fforward", 0), - prefs.getFloat("touch_scale_fforward", 1) - } // Fast-forward - }; - } - - public static float[][] getVjoy_d(float[][] vjoy_d_custom) { - return new float[][] { - // LEFT, UP, RIGHT, DOWN - new float[] { 20+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_LEFT}, - new float[] { 20+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_UP}, - new float[] { 20+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_RIGHT}, - new float[] { 20+64*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_DOWN}, - - // X, Y, B, A - new float[] { 448+0*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_X}, - new float[] { 448+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+0*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_Y}, - new float[] { 448+128*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_B}, - new float[] { 448+64*vjoy_d_custom[1][2]+vjoy_d_custom[1][0], 288+128*vjoy_d_custom[1][2]+vjoy_d_custom[1][1], - 64*vjoy_d_custom[1][2],64*vjoy_d_custom[1][2], key_CONT_A}, - - // START - new float[] { 320-32+vjoy_d_custom[2][0], 288+128+vjoy_d_custom[2][1], - 64*vjoy_d_custom[2][2],64*vjoy_d_custom[2][2], key_CONT_START}, - - // LT, RT - new float[] { 440+vjoy_d_custom[3][0], 200+vjoy_d_custom[3][1], - 90*vjoy_d_custom[3][2],64*vjoy_d_custom[3][2], -1}, - new float[] { 542+vjoy_d_custom[4][0], 200+vjoy_d_custom[4][1], - 90*vjoy_d_custom[4][2],64*vjoy_d_custom[4][2], -2}, - - // Analog ring and point - new float[] { 16+vjoy_d_custom[5][0], 24+32+vjoy_d_custom[5][1], - 128*vjoy_d_custom[5][2],128*vjoy_d_custom[5][2],-3}, - new float[] { 48+vjoy_d_custom[5][0], 24+64+vjoy_d_custom[5][1], - 64*vjoy_d_custom[5][2],64*vjoy_d_custom[5][2], -4}, - - // Fast-forward - new float[] { 320-32+vjoy_d_custom[6][0], 12+vjoy_d_custom[6][1], - 64*vjoy_d_custom[6][2],64*vjoy_d_custom[6][2], -5}, - - // DPad diagonals - new float[] { 20+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_LEFT|key_CONT_DPAD_UP}, - new float[] { 20+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_RIGHT|key_CONT_DPAD_UP}, - new float[] { 20+0*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_LEFT|key_CONT_DPAD_DOWN}, - new float[] { 20+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][0], 288+128*vjoy_d_custom[0][2]+vjoy_d_custom[0][1], - 64*vjoy_d_custom[0][2],64*vjoy_d_custom[0][2], key_CONT_DPAD_RIGHT|key_CONT_DPAD_DOWN}, - }; - } - - public static void writeCustomVjoyValues(float[][] vjoy_d_custom, Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - prefs.edit().putFloat("touch_x_shift_dpad", vjoy_d_custom[0][0]).apply(); - prefs.edit().putFloat("touch_y_shift_dpad", vjoy_d_custom[0][1]).apply(); - prefs.edit().putFloat("touch_scale_dpad", vjoy_d_custom[0][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_buttons", vjoy_d_custom[1][0]).apply(); - prefs.edit().putFloat("touch_y_shift_buttons", vjoy_d_custom[1][1]).apply(); - prefs.edit().putFloat("touch_scale_buttons", vjoy_d_custom[1][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_start", vjoy_d_custom[2][0]).apply(); - prefs.edit().putFloat("touch_y_shift_start", vjoy_d_custom[2][1]).apply(); - prefs.edit().putFloat("touch_scale_start", vjoy_d_custom[2][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_left_trigger", vjoy_d_custom[3][0]).apply(); - prefs.edit().putFloat("touch_y_shift_left_trigger", vjoy_d_custom[3][1]).apply(); - prefs.edit().putFloat("touch_scale_left_trigger", vjoy_d_custom[3][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_right_trigger", vjoy_d_custom[4][0]).apply(); - prefs.edit().putFloat("touch_y_shift_right_trigger", vjoy_d_custom[4][1]).apply(); - prefs.edit().putFloat("touch_scale_right_trigger", vjoy_d_custom[4][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_analog", vjoy_d_custom[5][0]).apply(); - prefs.edit().putFloat("touch_y_shift_analog", vjoy_d_custom[5][1]).apply(); - prefs.edit().putFloat("touch_scale_analog", vjoy_d_custom[5][2]).apply(); - - prefs.edit().putFloat("touch_x_shift_fforward", vjoy_d_custom[6][0]).apply(); - prefs.edit().putFloat("touch_y_shift_fforward", vjoy_d_custom[6][1]).apply(); - prefs.edit().putFloat("touch_scale_fforward", vjoy_d_custom[6][2]).apply(); - } - - public static void resetCustomVjoyValues(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - prefs.edit().remove("touch_x_shift_dpad").apply(); - prefs.edit().remove("touch_y_shift_dpad").apply(); - prefs.edit().remove("touch_scale_dpad").apply(); - - prefs.edit().remove("touch_x_shift_buttons").apply(); - prefs.edit().remove("touch_y_shift_buttons").apply(); - prefs.edit().remove("touch_scale_buttons").apply(); - - prefs.edit().remove("touch_x_shift_start").apply(); - prefs.edit().remove("touch_y_shift_start").apply(); - prefs.edit().remove("touch_scale_start").apply(); - - prefs.edit().remove("touch_x_shift_left_trigger").apply(); - prefs.edit().remove("touch_y_shift_left_trigger").apply(); - prefs.edit().remove("touch_scale_left_trigger").apply(); - - prefs.edit().remove("touch_x_shift_right_trigger").apply(); - prefs.edit().remove("touch_y_shift_right_trigger").apply(); - prefs.edit().remove("touch_scale_right_trigger").apply(); - - prefs.edit().remove("touch_x_shift_analog").apply(); - prefs.edit().remove("touch_y_shift_analog").apply(); - prefs.edit().remove("touch_scale_analog").apply(); - - prefs.edit().remove("touch_x_shift_fforward").apply(); - prefs.edit().remove("touch_y_shift_fforward").apply(); - prefs.edit().remove("touch_scale_fforward").apply(); - } -} diff --git a/shell/android-studio/flycast/src/main/jni/src/Android.cpp b/shell/android-studio/flycast/src/main/jni/src/Android.cpp index 3fa0fabc87..50bbd45e36 100644 --- a/shell/android-studio/flycast/src/main/jni/src/Android.cpp +++ b/shell/android-studio/flycast/src/main/jni/src/Android.cpp @@ -1,12 +1,8 @@ #include "types.h" -#include "hw/maple/maple_cfg.h" -#include "hw/maple/maple_devs.h" -#include "hw/maple/maple_if.h" #include "hw/naomi/naomi_cart.h" #include "audio/audiostream.h" #include "imgread/common.h" #include "ui/gui.h" -#include "ui/vgamepad.h" #include "rend/osd.h" #include "cfg/cfg.h" #include "log/LogManager.h" @@ -39,24 +35,14 @@ namespace jni thread_local JVMAttacher jvm_attacher; } -#include "android_gamepad.h" -#include "android_keyboard.h" #include "http_client.h" -extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_JNIdc_getVirtualGamepadVibration(JNIEnv *env, jobject obj) -{ - return (jint)config::VirtualGamepadVibration; -} - extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_screenCharacteristics(JNIEnv *env, jobject obj, jfloat screenDpi, jfloat refreshRate) { settings.display.dpi = screenDpi; settings.display.refreshRate = refreshRate; } -std::shared_ptr mouse; -std::shared_ptr keyboard; - static bool game_started; //stuff for saving prefs @@ -65,12 +51,10 @@ jmethodID saveAndroidSettingsMid; static ANativeWindow *g_window = 0; // Activity -static jobject g_activity; -static jmethodID VJoyStartEditingMID; -static jmethodID VJoyStopEditingMID; -static jmethodID VJoyResetEditingMID; -static jmethodID VJoyEnableControlsMID; -static jmethodID showScreenKeyboardMid; +jobject g_activity; +extern jmethodID VJoyStartEditingMID; +extern jmethodID VJoyStopEditingMID; +extern jmethodID showScreenKeyboardMid; static jmethodID onGameStateChangeMid; static void emuEventCallback(Event event, void *) @@ -353,42 +337,6 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_rendinitNa } } -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_vjoy(JNIEnv * env, jobject obj,int id,float x, float y, float w, float h) -{ - vgamepad::setPosition(static_cast(id), x, y, w, h); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_showVirtualGamepad(JNIEnv * env, jobject obj) -{ - vgamepad::show(); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_hideVirtualGamepad(JNIEnv * env, jobject obj) -{ - vgamepad::hide(); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_getControllers(JNIEnv *env, jobject obj, jintArray controllers, jobjectArray peripherals) -{ - // might be called before JNIdc.initEnvironment() - if (g_jvm == NULL) - env->GetJavaVM(&g_jvm); - - jni::IntArray jcontrollers(controllers, false); - std::vector devs; - for (u32 i = 0; i < config::MapleMainDevices.size(); i++) - devs.push_back((MapleDeviceType)config::MapleMainDevices[i]); - jcontrollers.setData(devs.data()); - - jni::ObjectArray jperipherals(peripherals, false); - int obj_len = jperipherals.size(); - for (int i = 0; i < obj_len; ++i) - { - std::vector devs { (MapleDeviceType)config::MapleExpansionDevices[i][0], (MapleDeviceType)config::MapleExpansionDevices[i][1] }; - jperipherals[i].setData(devs.data()); - } -} - extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_JNIdc_guiOpenSettings(JNIEnv *env, jobject obj) { gui_open_settings(); @@ -495,92 +443,6 @@ void SaveAndroidSettings() jni::env()->CallVoidMethod(g_emulator, saveAndroidSettingsMid, (jstring)homeDirectory); } -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_init(JNIEnv *env, jobject obj) -{ - input_device_manager = env->NewGlobalRef(obj); - input_device_manager_rumble = env->GetMethodID(env->GetObjectClass(obj), "rumble", "(IFFI)Z"); - // FIXME Don't connect it by default or any screen touch will register as button A press - mouse = std::make_shared(-1); - GamepadDevice::Register(mouse); - keyboard = std::make_shared(); - GamepadDevice::Register(keyboard); - gui_setOnScreenKeyboardCallback([](bool show) { - if (g_activity != nullptr) - jni::env()->CallVoidMethod(g_activity, showScreenKeyboardMid, show); - }); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAdded(JNIEnv *env, jobject obj, jint id, jstring name, - jint maple_port, jstring junique_id, jintArray fullAxes, jintArray halfAxes, jboolean hasRumble) -{ - std::string joyname = jni::String(name, false); - std::string unique_id = jni::String(junique_id, false); - std::vector full = jni::IntArray(fullAxes, false); - std::vector half = jni::IntArray(halfAxes, false); - - std::shared_ptr gamepad = std::make_shared(maple_port, id, joyname.c_str(), unique_id.c_str(), full, half); - AndroidGamepadDevice::AddAndroidGamepad(gamepad); - gamepad->setRumbleEnabled(hasRumble); -} -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickRemoved(JNIEnv *env, jobject obj, jint id) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); - if (device != NULL) - AndroidGamepadDevice::RemoveAndroidGamepad(device); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualGamepadEvent(JNIEnv *env, jobject obj, jint kcode, jint joyx, jint joyy, jint lt, jint rt, jboolean fastForward) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(AndroidGamepadDevice::VIRTUAL_GAMEPAD_ID); - if (device != NULL) - device->virtual_gamepad_event(kcode, joyx, joyy, lt, rt, fastForward); -} - -extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickButtonEvent(JNIEnv *env, jobject obj, jint id, jint key, jboolean pressed) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); - if (device != NULL) - return device->gamepad_btn_input(key, pressed); - else - return false; - -} - -extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardEvent(JNIEnv *env, jobject obj, jint key, jboolean pressed) -{ - keyboard->input(key, pressed); - return true; -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardText(JNIEnv *env, jobject obj, jint c) -{ - gui_keyboard_input((u16)c); -} - -static std::map, jint> previous_axis_values; - -extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAxisEvent(JNIEnv *env, jobject obj, jint id, jint key, jint value) -{ - std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); - if (device != nullptr) - return device->gamepad_axis_input(key, value); - else - return false; -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseEvent(JNIEnv *env, jobject obj, jint xpos, jint ypos, jint buttons) -{ - mouse->setAbsPos(xpos, ypos, settings.display.width, settings.display.height); - mouse->setButton(Mouse::LEFT_BUTTON, (buttons & 1) != 0); - mouse->setButton(Mouse::RIGHT_BUTTON, (buttons & 2) != 0); - mouse->setButton(Mouse::MIDDLE_BUTTON, (buttons & 4) != 0); -} - -extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseScrollEvent(JNIEnv *env, jobject obj, jint scrollValue) -{ - mouse->setWheel(scrollValue); -} - extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_BaseGLActivity_register(JNIEnv *env, jobject obj, jobject activity) { if (g_activity != nullptr) { @@ -592,40 +454,12 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_BaseGLActivity_regis g_activity = env->NewGlobalRef(activity); jclass actClass = env->GetObjectClass(activity); VJoyStartEditingMID = env->GetMethodID(actClass, "VJoyStartEditing", "()V"); - VJoyStopEditingMID = env->GetMethodID(actClass, "VJoyStopEditing", "(Z)V"); - VJoyResetEditingMID = env->GetMethodID(actClass, "VJoyResetEditing", "()V"); - VJoyEnableControlsMID = env->GetMethodID(actClass, "VJoyEnableControls", "([Z)V"); + VJoyStopEditingMID = env->GetMethodID(actClass, "VJoyStopEditing", "()V"); showScreenKeyboardMid = env->GetMethodID(actClass, "showScreenKeyboard", "(Z)V"); onGameStateChangeMid = env->GetMethodID(actClass, "onGameStateChange", "(Z)V"); } } -namespace vgamepad -{ - -void startEditing() { - enableAllControls(); - jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); -} -void pauseEditing() { - stopEditing(false); -} -void resetEditing() { - jni::env()->CallVoidMethod(g_activity, VJoyResetEditingMID); -} -void stopEditing(bool canceled) { - jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID, canceled); -} - -void setEnabledControls(bool enabled[_Count]) -{ - jni::BooleanArray jb{_Count}; - jb.setData(enabled, 0, _Count); - jni::env()->CallVoidMethod(g_activity, VJoyEnableControlsMID, (jbooleanArray)jb); -} - -} - void enableNetworkBroadcast(bool enable) { JNIEnv *env = jni::env(); diff --git a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h index 2208b741c1..79d1cf9850 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_gamepad.h @@ -20,6 +20,7 @@ #include "input/gamepad_device.h" #include "input/mouse.h" +#include "input/virtual_gamepad.h" #include "jni_util.h" #include @@ -97,26 +98,18 @@ class AndroidGamepadDevice : public GamepadDevice public: AndroidGamepadDevice(int maple_port, int id, const char *name, const char *unique_id, const std::vector& fullAxes, const std::vector& halfAxes) - : GamepadDevice(maple_port, "Android", id != VIRTUAL_GAMEPAD_ID), android_id(id), + : GamepadDevice(maple_port, "Android"), android_id(id), fullAxes(fullAxes), halfAxes(halfAxes) { _name = name; _unique_id = unique_id; INFO_LOG(INPUT, "Android: Opened joystick %d on port %d: '%s' descriptor '%s'", id, maple_port, _name.c_str(), _unique_id.c_str()); - if (id == VIRTUAL_GAMEPAD_ID) - { - input_mapper = std::make_shared(); - // hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted - } - else - { - loadMapping(); - save_mapping(); - hasAnalogStick = !fullAxes.empty(); - } + + loadMapping(); + save_mapping(); + hasAnalogStick = !fullAxes.empty(); } - ~AndroidGamepadDevice() override - { + ~AndroidGamepadDevice() override { INFO_LOG(INPUT, "Android: Joystick '%s' on port %d disconnected", _name.c_str(), maple_port()); } @@ -247,66 +240,6 @@ class AndroidGamepadDevice : public GamepadDevice GamepadDevice::Unregister(gamepad); }; - void virtual_gamepad_event(int kcode, int joyx, int joyy, int lt, int rt, bool fastForward) - { - // No virtual gamepad when the GUI is open: touch events only - if (gui_is_open() && gui_state != GuiState::VJoyEdit) - { - kcode = 0xffffffff; - joyx = joyy = rt = lt = 0; - } - if (settings.platform.isArcade()) - { - if (rt > 0) - { - if ((kcode & DC_BTN_A) == 0) - // RT + A -> D (coin) - kcode &= ~DC_BTN_D; - if ((kcode & DC_BTN_B) == 0) - // RT + B -> Service - kcode &= ~DC_DPAD2_UP; - if ((kcode & DC_BTN_X) == 0) - // RT + X -> Test - kcode &= ~DC_DPAD2_DOWN; - } - // arcade mapping: X -> btn2, Y -> btn3 - if ((kcode & DC_BTN_X) == 0) - { - kcode &= ~DC_BTN_C; - kcode |= DC_BTN_X; - } - if ((kcode & DC_BTN_Y) == 0) - { - kcode &= ~DC_BTN_X; - kcode |= DC_BTN_Y; - } - if (rt > 0) - // naomi btn4 - kcode &= ~DC_BTN_Y; - if (lt > 0) - // naomi btn5 - kcode &= ~DC_BTN_Z; - } - u32 changes = kcode ^ previous_kcode; - for (int i = 0; i < 32; i++) - if (changes & (1 << i)) - gamepad_btn_input(1 << i, (kcode & (1 << i)) == 0); - if (joyx >= 0) - gamepad_axis_input(DC_AXIS_RIGHT, joyx | (joyx << 8)); - else - gamepad_axis_input(DC_AXIS_LEFT, -joyx | (-joyx << 8)); - if (joyy >= 0) - gamepad_axis_input(DC_AXIS_DOWN, joyy | (joyy << 8)); - else - gamepad_axis_input(DC_AXIS_UP, -joyy | (-joyy << 8)); - gamepad_axis_input(DC_AXIS_LT, lt == 0 ? 0 : 0x7fff); - gamepad_axis_input(DC_AXIS_RT, rt == 0 ? 0 : 0x7fff); - previous_kcode = kcode; - if (fastForward != previousFastForward) - gamepad_btn_input(EMU_BTN_FFORWARD, fastForward); - previousFastForward = fastForward; - } - void rumble(float power, float inclination, u32 duration_ms) override { power *= rumblePower / 100.f; @@ -317,8 +250,6 @@ class AndroidGamepadDevice : public GamepadDevice this->rumbleEnabled = rumbleEnabled; } - bool is_virtual_gamepad() override { return android_id == VIRTUAL_GAMEPAD_ID; } - bool hasHalfAxis(int axis) const { return std::find(halfAxes.begin(), halfAxes.end(), axis) != halfAxes.end(); } bool hasFullAxis(int axis) const { return std::find(fullAxes.begin(), fullAxes.end(), axis) != fullAxes.end(); } @@ -336,13 +267,9 @@ class AndroidGamepadDevice : public GamepadDevice input_mapper = std::make_shared>(*this); } - static const int VIRTUAL_GAMEPAD_ID = 0x12345678; // must match the Java definition - private: int android_id; static std::map> android_gamepads; - u32 previous_kcode = 0xffffffff; - bool previousFastForward = false; std::vector fullAxes; std::vector halfAxes; }; @@ -481,3 +408,19 @@ class AndroidMouse : public SystemMouse } }; +class AndroidVirtualGamepad : public VirtualGamepad +{ +public: + AndroidVirtualGamepad(bool rumbleEnabled) : VirtualGamepad("Android") { + this->rumbleEnabled = rumbleEnabled; + } + + void rumble(float power, float inclination, u32 duration_ms) override + { + power *= rumblePower / 100.f; + jboolean has_vibrator = jni::env()->CallBooleanMethod(input_device_manager, input_device_manager_rumble, GAMEPAD_ID, power, inclination, duration_ms); + rumbleEnabled = has_vibrator; + } + + static constexpr int GAMEPAD_ID = 0x12345678; // must match the Java definition +}; diff --git a/shell/android-studio/flycast/src/main/jni/src/android_input.cpp b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp new file mode 100644 index 0000000000..2fde7d1a73 --- /dev/null +++ b/shell/android-studio/flycast/src/main/jni/src/android_input.cpp @@ -0,0 +1,225 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "android_gamepad.h" +#include "android_keyboard.h" +#include "ui/vgamepad.h" +#include "cfg/option.h" +#include "hw/maple/maple_if.h" + +std::shared_ptr mouse; +std::shared_ptr keyboard; +std::shared_ptr virtualGamepad; + +extern jobject g_activity; +jmethodID VJoyStartEditingMID; +jmethodID VJoyStopEditingMID; +jmethodID VJoyEnableControlsMID; +jmethodID showScreenKeyboardMid; + +// +// VGamepad +// +extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_VGamepad_getVibrationPower(JNIEnv *env, jobject obj) { + return (jint)config::VirtualGamepadVibration; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_show(JNIEnv * env, jobject obj) { + vgamepad::show(); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_hide(JNIEnv * env, jobject obj) { + vgamepad::hide(); +} + +extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_VGamepad_hitTest(JNIEnv * env, jobject obj, + jfloat x, jfloat y) { + return vgamepad::hitTest(x, y); +} + +extern "C" JNIEXPORT jfloat JNICALL Java_com_flycast_emulator_emu_VGamepad_getControlWidth(JNIEnv * env, jobject obj, + jint controlId) { + return vgamepad::getControlWidth(static_cast(controlId)); +} + +extern "C" JNIEXPORT jint JNICALL Java_com_flycast_emulator_emu_VGamepad_layoutHitTest(JNIEnv * env, jobject obj, + jfloat x, jfloat y) { + return vgamepad::layoutHitTest(x, y); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_scaleElement(JNIEnv * env, jobject obj, + jint elemId, jfloat scale) { + vgamepad::scaleElement(static_cast(elemId), scale); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_emu_VGamepad_translateElement(JNIEnv * env, jobject obj, + jint elemId, jfloat x, jfloat y) { + vgamepad::translateElement(static_cast(elemId), x, y); +} + +namespace vgamepad +{ + +void startEditing() +{ + // FIXME code dup with vgamepad.cpp + enableAllControls(); + show(); + jni::env()->CallVoidMethod(g_activity, VJoyStartEditingMID); +} +void pauseEditing() { + // needed? could be used by iOS to avoid relying on gui state + jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID); +} +void stopEditing(bool canceled) +{ + // FIXME code dup with vgamepad.cpp + jni::env()->CallVoidMethod(g_activity, VJoyStopEditingMID); + if (canceled) + loadLayout(); + else + saveLayout(); +} + +} + +// +// InputDeviceManager +// +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_isMicPluggedIn(JNIEnv *env, jobject obj) +{ + for (const auto& devices : config::MapleExpansionDevices) + if (static_cast(devices[0]) == MDT_Microphone + || static_cast(devices[1]) == MDT_Microphone) + return true; + + return false; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_init(JNIEnv *env, jobject obj) +{ + input_device_manager = env->NewGlobalRef(obj); + input_device_manager_rumble = env->GetMethodID(env->GetObjectClass(obj), "rumble", "(IFFI)Z"); + // FIXME Don't connect it by default or any screen touch will register as button A press + mouse = std::make_shared(-1); + GamepadDevice::Register(mouse); + keyboard = std::make_shared(); + GamepadDevice::Register(keyboard); + gui_setOnScreenKeyboardCallback([](bool show) { + if (g_activity != nullptr) + jni::env()->CallVoidMethod(g_activity, showScreenKeyboardMid, show); + }); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAdded(JNIEnv *env, jobject obj, + jint id, jstring name, jint maple_port, jstring junique_id, jintArray fullAxes, jintArray halfAxes, jboolean hasRumble) +{ + if (id == AndroidVirtualGamepad::GAMEPAD_ID) { + virtualGamepad = std::make_shared(hasRumble); + GamepadDevice::Register(virtualGamepad); + } + else + { + std::string joyname = jni::String(name, false); + std::string unique_id = jni::String(junique_id, false); + std::vector full = jni::IntArray(fullAxes, false); + std::vector half = jni::IntArray(halfAxes, false); + + std::shared_ptr gamepad = std::make_shared(maple_port, id, joyname.c_str(), unique_id.c_str(), full, half); + AndroidGamepadDevice::AddAndroidGamepad(gamepad); + gamepad->setRumbleEnabled(hasRumble); + } +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickRemoved(JNIEnv *env, jobject obj, + jint id) +{ + if (id == AndroidVirtualGamepad::GAMEPAD_ID) { + GamepadDevice::Unregister(virtualGamepad); + virtualGamepad.reset(); + } + else { + std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); + if (device) + AndroidGamepadDevice::RemoveAndroidGamepad(device); + } +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualReleaseAll(JNIEnv *env, jobject obj) { + if (virtualGamepad) + virtualGamepad->releaseAll(); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualJoystick(JNIEnv *env, jobject obj, + jfloat x, jfloat y) { + if (virtualGamepad) + virtualGamepad->joystickInput(x, y); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_virtualButtonInput(JNIEnv *env, jobject obj, + jint controlId, jboolean pressed) { + if (virtualGamepad) + virtualGamepad->buttonInput(static_cast(controlId), pressed); +} + +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickButtonEvent(JNIEnv *env, jobject obj, + jint id, jint key, jboolean pressed) +{ + std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); + if (device != NULL) + return device->gamepad_btn_input(key, pressed); + else + return false; +} + +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardEvent(JNIEnv *env, jobject obj, + jint key, jboolean pressed) { + keyboard->input(key, pressed); + return true; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_keyboardText(JNIEnv *env, jobject obj, + jint c) { + gui_keyboard_input((u16)c); +} + +static std::map, jint> previous_axis_values; + +extern "C" JNIEXPORT jboolean JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_joystickAxisEvent(JNIEnv *env, jobject obj, + jint id, jint key, jint value) +{ + std::shared_ptr device = AndroidGamepadDevice::GetAndroidGamepad(id); + if (device != nullptr) + return device->gamepad_axis_input(key, value); + else + return false; +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseEvent(JNIEnv *env, jobject obj, + jint xpos, jint ypos, jint buttons) +{ + mouse->setAbsPos(xpos, ypos, settings.display.width, settings.display.height); + mouse->setButton(Mouse::LEFT_BUTTON, (buttons & 1) != 0); + mouse->setButton(Mouse::RIGHT_BUTTON, (buttons & 2) != 0); + mouse->setButton(Mouse::MIDDLE_BUTTON, (buttons & 4) != 0); +} + +extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_periph_InputDeviceManager_mouseScrollEvent(JNIEnv *env, jobject obj, + jint scrollValue) { + mouse->setWheel(scrollValue); +} diff --git a/shell/android-studio/flycast/src/main/jni/src/jni_util.h b/shell/android-studio/flycast/src/main/jni/src/jni_util.h index 2fc0b992a6..73e57034d7 100644 --- a/shell/android-studio/flycast/src/main/jni/src/jni_util.h +++ b/shell/android-studio/flycast/src/main/jni/src/jni_util.h @@ -107,7 +107,7 @@ class Object bool isNull() const { return object == nullptr; } operator jobject() const { return object; } - Class getClass() const; + inline Class getClass() const; template T globalRef() { diff --git a/shell/apple/emulator-ios/emulator/PadViewController.mm b/shell/apple/emulator-ios/emulator/PadViewController.mm index 19b678ee59..97dedb34fd 100644 --- a/shell/apple/emulator-ios/emulator/PadViewController.mm +++ b/shell/apple/emulator-ios/emulator/PadViewController.mm @@ -65,7 +65,7 @@ - (void)showController:(UIView *)parentView - (void)hideController { - [self resetTouch]; + [self resetAnalog]; [hideTimer invalidate]; [self.view removeFromSuperview]; } @@ -89,12 +89,10 @@ -(void)hideTimer { vgamepad::hide(); } -- (void)resetTouch +- (void)resetAnalog { joyTouch = nil; - virtualGamepad->gamepad_axis_input(DC_AXIS_LEFT, 0); - virtualGamepad->gamepad_axis_input(DC_AXIS_UP, 0); - vgamepad::setAnalogStick(0, 0); + virtualGamepad->joystickInput(0, 0); } static CGPoint translateCoords(const CGPoint& pos, const CGSize& size) @@ -114,20 +112,20 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; CGPoint point = [touch locationInView:self.view]; point = translateCoords(point, self.view.bounds.size); vgamepad::ControlId control = vgamepad::hitTest(point.x, point.y); - if (joyTouch == nil && control == vgamepad::AnalogArea) + if (joyTouch == nil && (control == vgamepad::AnalogArea || control == vgamepad::AnalogStick)) { - [self resetTouch]; + [self resetAnalog]; joyTouch = touch; joyBias = point; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; if (control != vgamepad::None && control != vgamepad::AnalogArea - && touchToButton[key] == nil) + && control != vgamepad::AnalogStick && touchToButton[key] == nil) { touchToButton[key] = [NSNumber numberWithInt:control]; // button down - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey(control), true); + virtualGamepad->buttonInput(control, true); } } [super touchesBegan:touches withEvent:event]; @@ -138,7 +136,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; for (UITouch *touch in touches) { if (touch == joyTouch) { - [self resetTouch]; + [self resetAnalog]; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; @@ -146,7 +144,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; if (control != nil) { [touchToButton removeObjectForKey:key]; // button up - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)control.intValue), false); + virtualGamepad->buttonInput(static_cast(control.intValue), false); } } [super touchesEnded:touches withEvent:event]; @@ -166,17 +164,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; double sz = vgamepad::getControlWidth(vgamepad::AnalogStick); point.x = std::max(std::min(1.0, point.x / sz), -1.0); point.y = std::max(std::min(1.0, point.y / sz), -1.0); - vgamepad::setAnalogStick(point.x, point.y); - point.x *= 32767.0; - point.y *= 32767.0; - if (point.x >= 0) - virtualGamepad->gamepad_axis_input(DC_AXIS_RIGHT, (int)std::round(point.x)); - else - virtualGamepad->gamepad_axis_input(DC_AXIS_LEFT, -(int)std::round(point.x)); - if (point.y >= 0) - virtualGamepad->gamepad_axis_input(DC_AXIS_DOWN, (int)std::round(point.y)); - else - virtualGamepad->gamepad_axis_input(DC_AXIS_UP, -(int)std::round(point.y)); + virtualGamepad->joystickInput(point.x, point.y); continue; } vgamepad::ControlId control = vgamepad::hitTest(point.x, point.y); @@ -186,10 +174,10 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; continue; if (prevControl != nil && prevControl.intValue != vgamepad::None && prevControl.intValue != vgamepad::AnalogArea) { // button up - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)prevControl.intValue), false); + virtualGamepad->buttonInput(static_cast(prevControl.intValue), false); } // button down - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey(control), true); + virtualGamepad->buttonInput(control, true); touchToButton[key] = [NSNumber numberWithInt:control]; } [super touchesMoved:touches withEvent:event]; @@ -199,7 +187,7 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; { for (UITouch *touch in touches) { if (touch == joyTouch) { - [self resetTouch]; + [self resetAnalog]; continue; } NSValue *key = [NSValue valueWithPointer:(const void *)touch]; @@ -207,7 +195,7 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; if (control != nil) { [touchToButton removeObjectForKey:key]; // button up - virtualGamepad->gamepad_btn_input(vgamepad::controlToDcKey((vgamepad::ControlId)control.intValue), false); + virtualGamepad->buttonInput(static_cast(control.intValue), false); } } [super touchesCancelled:touches withEvent:event]; diff --git a/shell/apple/emulator-ios/emulator/ios_gamepad.h b/shell/apple/emulator-ios/emulator/ios_gamepad.h index 214744c3ea..e3aa0683a7 100644 --- a/shell/apple/emulator-ios/emulator/ios_gamepad.h +++ b/shell/apple/emulator-ios/emulator/ios_gamepad.h @@ -23,6 +23,7 @@ #include #include "input/gamepad_device.h" #include "input/mouse.h" +#include "input/virtual_gamepad.h" #include "ui/gui.h" enum IOSButton { @@ -480,110 +481,30 @@ class IOSGamepad : public GamepadDevice static std::map> controllers; }; -class IOSVirtualGamepad : public GamepadDevice +class IOSVirtualGamepad : public VirtualGamepad { public: - IOSVirtualGamepad() : GamepadDevice(0, "iOS", false) { - _name = "Virtual Gamepad"; - _unique_id = "ios-virtual-gamepad"; - input_mapper = std::make_shared(); - //hasAnalogStick = true; // TODO has an analog stick but input mapping isn't persisted + IOSVirtualGamepad() : VirtualGamepad("iOS") { } - bool is_virtual_gamepad() override { return true; } - - std::shared_ptr getDefaultMapping() override { - return std::make_shared>(); - } - - bool gamepad_btn_input(u32 code, bool pressed) override + bool handleButtonInput(u32& state, u32 key, bool pressed) override { - if (pressed) - buttonState |= code; - else - buttonState &= ~code; - switch (code) + if (!pressed + || (key != DC_DPAD_UP && key != DC_DPAD_DOWN && key != DC_DPAD_LEFT && key != DC_DPAD_RIGHT)) + return false; + if (((state | key) & (DC_DPAD_UP | DC_DPAD_DOWN)) == (DC_DPAD_UP | DC_DPAD_DOWN) + || ((state | key) & (DC_DPAD_LEFT | DC_DPAD_RIGHT)) == (DC_DPAD_LEFT | DC_DPAD_RIGHT)) { - case DC_AXIS_LT: - gamepad_axis_input(DC_AXIS_LT, pressed ? 0x7fff : 0); - if (settings.platform.isArcade()) - GamepadDevice::gamepad_btn_input(DC_BTN_Z, pressed); // btn5 - return true; - case DC_AXIS_RT: - if (!pressed && maple_port() >= 0 && maple_port() <= 3) - kcode[maple_port()] |= DC_DPAD2_UP | DC_BTN_D | DC_DPAD2_DOWN; - gamepad_axis_input(DC_AXIS_RT, pressed ? 0x7fff : 0); - if (settings.platform.isArcade()) - GamepadDevice::gamepad_btn_input(DC_BTN_Y, pressed); // btn4 - return true; - default: - if ((buttonState & (DC_DPAD_UP | DC_DPAD_DOWN)) == (DC_DPAD_UP | DC_DPAD_DOWN) - || (buttonState & (DC_DPAD_LEFT | DC_DPAD_RIGHT)) == (DC_DPAD_LEFT | DC_DPAD_RIGHT)) - { - GamepadDevice::gamepad_btn_input(DC_DPAD_UP, false); - GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, false); - GamepadDevice::gamepad_btn_input(DC_DPAD_LEFT, false); - GamepadDevice::gamepad_btn_input(DC_DPAD_RIGHT, false); - buttonState = 0; - gui_open_settings(); - return true; - } - if (settings.platform.isArcade() && maple_port() >= 0 && maple_port() <= 3) - { - u32& keycode = kcode[maple_port()]; - if ((buttonState & DC_AXIS_RT) != 0) - { - switch (code) { - case DC_BTN_A: - // RT + A -> D (coin) - keycode = pressed ? keycode & ~DC_BTN_D : keycode | DC_BTN_D; - break; - case DC_BTN_B: - // RT + B -> Service - keycode = pressed ? keycode & ~DC_DPAD2_UP : keycode | DC_DPAD2_UP; - break; - case DC_BTN_X: - // RT + X -> Test - keycode = pressed ? keycode & ~DC_DPAD2_DOWN : keycode | DC_DPAD2_DOWN; - break; - default: - break; - } - } - // arcade mapping: X -> btn2, Y -> btn3 - if (code == DC_BTN_X) - code = DC_BTN_C; // btn2 - if (code == DC_BTN_Y) - code = DC_BTN_X; // btn3 - } - switch (code) - { - case DC_DPAD_UP | DC_DPAD_RIGHT: - GamepadDevice::gamepad_btn_input(DC_DPAD_UP, pressed); - code = DC_DPAD_RIGHT; - break; - case DC_DPAD_DOWN | DC_DPAD_RIGHT: - GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, pressed); - code = DC_DPAD_RIGHT; - break; - case DC_DPAD_DOWN | DC_DPAD_LEFT: - GamepadDevice::gamepad_btn_input(DC_DPAD_DOWN, pressed); - code = DC_DPAD_LEFT; - break; - case DC_DPAD_UP | DC_DPAD_LEFT: - GamepadDevice::gamepad_btn_input(DC_DPAD_UP, pressed); - code = DC_DPAD_LEFT; - break; - default: - break; - } - - return GamepadDevice::gamepad_btn_input(code, pressed); + gamepad_btn_input(DC_DPAD_UP, false); + gamepad_btn_input(DC_DPAD_DOWN, false); + gamepad_btn_input(DC_DPAD_LEFT, false); + gamepad_btn_input(DC_DPAD_RIGHT, false); + state = 0; + gui_open_settings(); + return true; } + return false; } - -private: - u32 buttonState = 0; }; class IOSTouchMouse : public SystemMouse