diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6004e4a..be84dc12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,12 +10,12 @@ jobs: steps: - name: Git Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 with: submodules: true - name: Restore Gradle Cache - uses: actions/cache@v1.0.3 + uses: actions/cache@v2 with: path: /root/.gradle/ key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }} @@ -23,7 +23,7 @@ jobs: ${{ runner.os }}-gradle- - name: Restore CXX Cache - uses: actions/cache@v1.0.3 + uses: actions/cache@v2 with: path: app/.cxx/ key: ${{ runner.os }}-cxx-${{ hashFiles('**/CMakeLists.txt') }} @@ -37,7 +37,7 @@ jobs: run: ./gradlew --stacktrace lint - name: Upload Lint Report - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: name: lint-result.html path: app/build/reports/lint-results.html @@ -46,19 +46,19 @@ jobs: run: ./gradlew assemble - name: Upload Debug APK - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: name: app-debug.apk path: app/build/outputs/apk/debug/ - name: Upload Release APK - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: name: app-release.apk path: app/build/outputs/apk/release/ - name: Upload R8 Mapping - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v2 with: name: mapping.txt path: app/build/outputs/mapping/release/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 429f1c8c..54e9f7dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ This can also be done by using `Ctrl + Alt + L` on Windows, `Ctrl + Shift + Alt * Parameters: `camelCase` * Files and Directories: `snake_case` except for when they correspond to a HOS structure (EG: Services, Kernel Objects) -**(1)** Except when the whole name is an abbreviation such as `OS` and `NCE` but not `JVMManager` +**(1)** Except when the whole name is an abbreviation use UPPERCASE such as `OS` and `NCE` but not `JVMManager` ### Comments Use doxygen style comments for: diff --git a/app/src/main/cpp/emu_jni.cpp b/app/src/main/cpp/emu_jni.cpp index 4a463fb9..42d02354 100644 --- a/app/src/main/cpp/emu_jni.cpp +++ b/app/src/main/cpp/emu_jni.cpp @@ -97,14 +97,31 @@ extern "C" JNIEXPORT jfloat Java_emu_skyline_EmulationActivity_getFrametime(JNIE return static_cast(frametime) / 100; } -extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setButtonState(JNIEnv *, jobject, jlong id, jint state) { +extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setController(JNIEnv *, jobject, jint index, jint type, jint partnerIndex) { + while (input == nullptr) + asm("yield"); + input->npad.controllers[index] = skyline::input::GuestController{static_cast(type), static_cast(partnerIndex)}; +} + +extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_updateControllers(JNIEnv *, jobject) { + while (input == nullptr) + asm("yield"); + input->npad.Update(true); +} + +extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setButtonState(JNIEnv *, jobject, jint index, jlong mask, jint state) { if (input) { - skyline::input::NpadButton button{.raw = static_cast(id)}; - input->npad.at(skyline::input::NpadId::Player1).SetButtonState(button, static_cast(state)); + auto device = input->npad.controllers[index].device; + skyline::input::NpadButton button{.raw = static_cast(mask)}; + if (device) + device->SetButtonState(button, static_cast(state)); } } -extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setAxisValue(JNIEnv *, jobject, jint id, jint value) { - if (input) - input->npad.at(skyline::input::NpadId::Player1).SetAxisValue(static_cast(id), value); +extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setAxisValue(JNIEnv *, jobject, jint index, jint axis, jint value) { + if (input) { + auto device = input->npad.controllers[index].device; + if (device) + device->SetAxisValue(static_cast(axis), value); + } } diff --git a/app/src/main/cpp/skyline/input/npad.cpp b/app/src/main/cpp/skyline/input/npad.cpp index 5045ad85..83a0a33c 100644 --- a/app/src/main/cpp/skyline/input/npad.cpp +++ b/app/src/main/cpp/skyline/input/npad.cpp @@ -5,28 +5,95 @@ #include "npad.h" namespace skyline::input { - NpadManager::NpadManager(const DeviceState &state, input::HidSharedMemory *hid) : state(state), npads - {NpadDevice{hid->npad[0], NpadId::Player1}, {hid->npad[1], NpadId::Player2}, - {hid->npad[2], NpadId::Player3}, {hid->npad[3], NpadId::Player4}, - {hid->npad[4], NpadId::Player5}, {hid->npad[5], NpadId::Player6}, - {hid->npad[6], NpadId::Player7}, {hid->npad[7], NpadId::Player8}, - {hid->npad[8], NpadId::Unknown}, {hid->npad[9], NpadId::Handheld}, + {NpadDevice{*this, hid->npad[0], NpadId::Player1}, {*this, hid->npad[1], NpadId::Player2}, + {*this, hid->npad[2], NpadId::Player3}, {*this, hid->npad[3], NpadId::Player4}, + {*this, hid->npad[4], NpadId::Player5}, {*this, hid->npad[5], NpadId::Player6}, + {*this, hid->npad[6], NpadId::Player7}, {*this, hid->npad[7], NpadId::Player8}, + {*this, hid->npad[8], NpadId::Unknown}, {*this, hid->npad[9], NpadId::Handheld}, } {} - void NpadManager::Activate() { - if (styles.raw == 0) { - styles.proController = true; - styles.joyconHandheld = true; - } + void NpadManager::Update(bool host) { + if (host) + updated = true; + else + while (!updated) + asm("yield"); - at(NpadId::Player1).Connect(state.settings->GetBool("operation_mode") ? NpadControllerType::Handheld : NpadControllerType::ProController); - } - - void NpadManager::Deactivate() { - styles.raw = 0; + if (!activated) + return; for (auto &npad : npads) npad.Disconnect(); + + for (auto &controller : controllers) + controller.device = nullptr; + + for (auto &id : supportedIds) { + if (id == NpadId::Unknown) + continue; + + auto &device = at(id); + + for (auto &controller : controllers) { + if (controller.device) + continue; + + NpadStyleSet style{}; + if (id != NpadId::Handheld) { + if (controller.type == NpadControllerType::ProController) + style.proController = true; + else if (controller.type == NpadControllerType::JoyconLeft) + style.joyconLeft = true; + else if (controller.type == NpadControllerType::JoyconRight) + style.joyconRight = true; + if (controller.type == NpadControllerType::JoyconDual || controller.partnerIndex != -1) + style.joyconDual = true; + } else if (controller.type == NpadControllerType::Handheld) { + style.joyconHandheld = true; + } + style = NpadStyleSet{.raw = style.raw & styles.raw}; + + if (style.raw) { + if (style.proController) { + device.Connect(NpadControllerType::ProController); + controller.device = &device; + } else if (style.joyconHandheld) { + device.Connect(NpadControllerType::Handheld); + controller.device = &device; + } else if (style.joyconDual && device.GetAssignment() == NpadJoyAssignment::Dual) { + device.Connect(NpadControllerType::JoyconDual); + controller.device = &device; + controllers.at(controller.partnerIndex).device = &device; + } else if (style.joyconLeft || style.joyconRight) { + device.Connect(controller.type); + controller.device = &device; + } else { + continue; + } + break; + } + } + } + } + + void NpadManager::Activate() { + supportedIds = {NpadId::Handheld, NpadId::Player1, NpadId::Player2, NpadId::Player3, NpadId::Player4, NpadId::Player5, NpadId::Player6, NpadId::Player7, NpadId::Player8}; + styles = {.proController = true, .joyconHandheld = true, .joyconDual = true, .joyconLeft = true, .joyconRight = true}; + activated = true; + + Update(); + } + + void NpadManager::Deactivate() { + supportedIds = {}; + styles = {}; + activated = false; + + for (auto &npad : npads) + npad.Disconnect(); + + for (auto &controller : controllers) + controller.device = nullptr; } } diff --git a/app/src/main/cpp/skyline/input/npad.h b/app/src/main/cpp/skyline/input/npad.h index ba1403c3..b1c091c7 100644 --- a/app/src/main/cpp/skyline/input/npad.h +++ b/app/src/main/cpp/skyline/input/npad.h @@ -6,24 +6,32 @@ #include "npad_device.h" namespace skyline::input { + struct GuestController { + NpadControllerType type{}; //!< The type of the controller + i8 partnerIndex{-1}; //!< The index of a Joy-Con partner, if this has one + NpadDevice *device{nullptr}; //!< A pointer to the NpadDevice that all events from this are redirected to + }; + /** - * @brief This class is used to + * @brief This class is used to manage all NPad devices and their allocations to Player objects */ class NpadManager { private: const DeviceState &state; //!< The state of the device std::array npads; //!< An array of all the NPad devices + bool activated{false}; //!< If this NpadManager is activated or not + std::atomic updated{false}; //!< If this NpadManager has been updated by the guest /** * @brief This translates an NPad's ID into it's index in the array * @param id The ID of the NPad to translate * @return The corresponding index of the NPad in the array */ - constexpr inline size_t Translate(NpadId id) { + static constexpr inline size_t Translate(NpadId id) { switch (id) { - case NpadId::Unknown: - return 8; case NpadId::Handheld: + return 8; + case NpadId::Unknown: return 9; default: return static_cast(id); @@ -31,7 +39,9 @@ namespace skyline::input { } public: - NpadStyleSet styles{}; //!< The styles that are supported in accordance to the host input + std::array controllers; //!< An array of all the available guest controllers + std::vector supportedIds; //!< The NpadId(s) that are supported by the application + NpadStyleSet styles; //!< The styles that are supported by the application NpadJoyOrientation orientation{}; //!< The Joy-Con orientation to use /** @@ -43,7 +53,7 @@ namespace skyline::input { * @param id The ID of the NPad to return * @return A reference to the NPad with the specified ID */ - constexpr inline NpadDevice& at(NpadId id) { + constexpr inline NpadDevice &at(NpadId id) { return npads.at(Translate(id)); } @@ -51,17 +61,23 @@ namespace skyline::input { * @param id The ID of the NPad to return * @return A reference to the NPad with the specified ID */ - constexpr inline NpadDevice& operator[](NpadId id) noexcept { + constexpr inline NpadDevice &operator[](NpadId id) noexcept { return npads.operator[](Translate(id)); } /** - * @brief This activates the controllers + * @brief This deduces all the mappings from guest controllers -> players based on the configuration supplied by HID services and available controllers + * @param host If the update is host-initiated rather than the guest */ + void Update(bool host = false); + + /** + * @brief This activates the mapping between guest controllers -> players, a call to this is required for function + */ void Activate(); /** - * @brief This deactivates the controllers + * @brief This disables any activate mappings from guest controllers -> players till Activate has been called */ void Deactivate(); }; diff --git a/app/src/main/cpp/skyline/input/npad_device.cpp b/app/src/main/cpp/skyline/input/npad_device.cpp index 9197ebb4..8c072c75 100644 --- a/app/src/main/cpp/skyline/input/npad_device.cpp +++ b/app/src/main/cpp/skyline/input/npad_device.cpp @@ -2,9 +2,10 @@ // Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) #include "npad_device.h" +#include "npad.h" namespace skyline::input { - NpadDevice::NpadDevice(NpadSection §ion, NpadId id) : section(section), id(id) {} + NpadDevice::NpadDevice(NpadManager &manager, NpadSection §ion, NpadId id) : manager(manager), section(section), id(id) {} void NpadDevice::Connect(NpadControllerType type) { section.header.type = NpadControllerType::None; @@ -15,59 +16,121 @@ namespace skyline::input { connectionState.connected = true; switch (type) { + case NpadControllerType::ProController: + section.header.type = NpadControllerType::ProController; + section.deviceType.fullKey = true; + + section.systemProperties.directionalButtonsSupported = true; + section.systemProperties.abxyButtonsOriented = true; + section.systemProperties.plusButtonCapability = true; + section.systemProperties.minusButtonCapability = true; + + connectionState.connected = true; + break; + case NpadControllerType::Handheld: section.header.type = NpadControllerType::Handheld; section.deviceType.handheldLeft = true; section.deviceType.handheldRight = true; - section.header.assignment = NpadJoyAssignment::Dual; - section.systemProperties.ABXYButtonOriented = true; + + section.systemProperties.directionalButtonsSupported = true; + section.systemProperties.abxyButtonsOriented = true; section.systemProperties.plusButtonCapability = true; section.systemProperties.minusButtonCapability = true; connectionState.handheld = true; - connectionState.leftJoyconConnected = true; - connectionState.rightJoyconConnected = true; connectionState.leftJoyconHandheld = true; connectionState.rightJoyconHandheld = true; break; - case NpadControllerType::ProController: - section.header.type = NpadControllerType::ProController; - section.deviceType.fullKey = true; + + case NpadControllerType::JoyconDual: + section.header.type = NpadControllerType::JoyconDual; + section.deviceType.joyconLeft = true; section.deviceType.joyconRight = true; - section.header.assignment = NpadJoyAssignment::Single; - section.systemProperties.ABXYButtonOriented = true; + section.header.assignment = NpadJoyAssignment::Dual; + + section.systemProperties.directionalButtonsSupported = true; + section.systemProperties.abxyButtonsOriented = true; section.systemProperties.plusButtonCapability = true; section.systemProperties.minusButtonCapability = true; + + connectionState.connected = true; + connectionState.leftJoyconConnected = true; + connectionState.rightJoyconConnected = true; break; + + case NpadControllerType::JoyconLeft: + section.header.type = NpadControllerType::JoyconLeft; + section.deviceType.joyconLeft = true; + section.header.assignment = NpadJoyAssignment::Single; + + section.systemProperties.directionalButtonsSupported = true; + section.systemProperties.slSrButtonOriented = true; + section.systemProperties.minusButtonCapability = true; + + connectionState.connected = true; + connectionState.leftJoyconConnected = true; + break; + + case NpadControllerType::JoyconRight: + section.header.type = NpadControllerType::JoyconRight; + section.deviceType.joyconRight = true; + section.header.assignment = NpadJoyAssignment::Single; + + section.systemProperties.abxyButtonsOriented = true; + section.systemProperties.slSrButtonOriented = true; + section.systemProperties.plusButtonCapability = true; + + connectionState.connected = true; + connectionState.rightJoyconConnected = true; + break; + default: throw exception("Unsupported controller type: {}", type); } - controllerType = type; + switch (type) { + case NpadControllerType::ProController: + case NpadControllerType::JoyconLeft: + case NpadControllerType::JoyconRight: + section.header.singleColorStatus = NpadColorReadStatus::Success; + section.header.dualColorStatus = NpadColorReadStatus::Disconnected; + section.header.singleColor = {0, 0}; + break; - section.header.singleColorStatus = NpadColorReadStatus::Success; - section.header.singleColor = {0, 0}; + case NpadControllerType::Handheld: + case NpadControllerType::JoyconDual: + section.header.singleColorStatus = NpadColorReadStatus::Disconnected; + section.header.dualColorStatus = NpadColorReadStatus::Success; + section.header.leftColor = {0, 0}; + section.header.rightColor = {0, 0}; + break; - section.header.dualColorStatus = NpadColorReadStatus::Success; - section.header.leftColor = {0, 0}; //TODO: make these configurable - section.header.rightColor = {0, 0}; + case NpadControllerType::None: + break; + } - section.batteryLevel[0] = NpadBatteryLevel::Full; - section.batteryLevel[1] = NpadBatteryLevel::Full; - section.batteryLevel[2] = NpadBatteryLevel::Full; + section.singleBatteryLevel = NpadBatteryLevel::Full; + section.leftBatteryLevel = NpadBatteryLevel::Full; + section.rightBatteryLevel = NpadBatteryLevel::Full; + + this->type = type; + controllerInfo = &GetControllerInfo(); SetButtonState(NpadButton{}, NpadButtonState::Released); } void NpadDevice::Disconnect() { - connectionState.connected = false; + section = {}; + explicitAssignment = false; + globalTimestamp = 0; - GetNextEntry(GetControllerInfo()); - GetNextEntry(section.systemExtController); + type = NpadControllerType::None; + controllerInfo = nullptr; } NpadControllerInfo &NpadDevice::GetControllerInfo() { - switch (controllerType) { + switch (type) { case NpadControllerType::ProController: return section.fullKeyController; case NpadControllerType::Handheld: @@ -79,15 +142,13 @@ namespace skyline::input { case NpadControllerType::JoyconRight: return section.rightController; default: - throw exception("Cannot find corresponding section for ControllerType: {}", controllerType); + throw exception("Cannot find corresponding section for ControllerType: {}", type); } } NpadControllerState &NpadDevice::GetNextEntry(NpadControllerInfo &info) { auto &lastEntry = info.state.at(info.header.currentEntry); - info.header.entryCount = constant::HidEntryCount; - info.header.maxEntry = constant::HidEntryCount - 1; info.header.currentEntry = (info.header.currentEntry != constant::HidEntryCount - 1) ? info.header.currentEntry + 1 : 0; info.header.timestamp = util::GetTimeTicks(); @@ -106,44 +167,118 @@ namespace skyline::input { } void NpadDevice::SetButtonState(NpadButton mask, NpadButtonState state) { - if (!connectionState.connected) + if (!connectionState.connected || !controllerInfo) return; - for (NpadControllerInfo &controllerInfo : {std::ref(GetControllerInfo()), std::ref(section.systemExtController)}) { - auto &entry = GetNextEntry(controllerInfo); + auto &entry = GetNextEntry(*controllerInfo); - if (state == NpadButtonState::Pressed) - entry.buttons.raw |= mask.raw; - else - entry.buttons.raw &= ~(mask.raw); + if (state == NpadButtonState::Pressed) + entry.buttons.raw |= mask.raw; + else + entry.buttons.raw &= ~mask.raw; + + if ((type == NpadControllerType::JoyconLeft || type == NpadControllerType::JoyconRight) && manager.orientation == NpadJoyOrientation::Horizontal) { + NpadButton orientedMask{}; + + if (mask.dpadUp) + orientedMask.dpadLeft = true; + if (mask.dpadDown) + orientedMask.dpadRight = true; + if (mask.dpadLeft) + orientedMask.dpadDown = true; + if (mask.dpadRight) + orientedMask.dpadUp = true; + + if (mask.leftSl || mask.rightSl) + orientedMask.l = true; + if (mask.leftSr || mask.rightSr) + orientedMask.r = true; + + orientedMask.a = mask.a; + orientedMask.b = mask.b; + orientedMask.x = mask.x; + orientedMask.y = mask.y; + orientedMask.leftStick = mask.leftStick; + orientedMask.rightStick = mask.rightStick; + orientedMask.plus = mask.plus; + orientedMask.minus = mask.minus; + orientedMask.leftSl = mask.leftSl; + orientedMask.leftSr = mask.leftSr; + orientedMask.rightSl = mask.rightSl; + orientedMask.rightSr = mask.rightSr; + + mask = orientedMask; } + for (NpadControllerState& controllerEntry : {std::ref(GetNextEntry(section.defaultController)), std::ref(GetNextEntry(section.digitalController))}) + if (state == NpadButtonState::Pressed) + controllerEntry.buttons.raw |= mask.raw; + else + controllerEntry.buttons.raw &= ~mask.raw; + globalTimestamp++; } void NpadDevice::SetAxisValue(NpadAxisId axis, i32 value) { - if (!connectionState.connected) + if (!connectionState.connected || !controllerInfo) return; - for (NpadControllerInfo &controllerInfo : {std::ref(GetControllerInfo()), std::ref(section.systemExtController)}) { - auto &entry = GetNextEntry(controllerInfo); + auto &controllerEntry = GetNextEntry(*controllerInfo); + auto& defaultEntry = GetNextEntry(section.defaultController); + if (manager.orientation == NpadJoyOrientation::Vertical && (type != NpadControllerType::JoyconLeft && type != NpadControllerType::JoyconRight)) { switch (axis) { case NpadAxisId::LX: - entry.leftX = value; + controllerEntry.leftX = value; + defaultEntry.leftX = value; break; case NpadAxisId::LY: - entry.leftY = -value; // Invert Y axis + controllerEntry.leftY = value; + defaultEntry.leftY = value; break; case NpadAxisId::RX: - entry.rightX = value; + controllerEntry.rightX = value; + defaultEntry.rightX = value; break; case NpadAxisId::RY: - entry.rightY = -value; // Invert Y axis + controllerEntry.rightY = value; + defaultEntry.rightY = value; + break; + } + } else { + switch (axis) { + case NpadAxisId::LX: + controllerEntry.leftX = value; + defaultEntry.leftY = value; + break; + case NpadAxisId::LY: + controllerEntry.leftY = value; + defaultEntry.leftX = -value; + break; + case NpadAxisId::RX: + controllerEntry.rightX = value; + defaultEntry.rightY = value; + break; + case NpadAxisId::RY: + controllerEntry.rightY = value; + defaultEntry.rightX = -value; break; } } + auto& digitalEntry = GetNextEntry(section.digitalController); + constexpr i16 threshold = 3276; // A 10% deadzone for the stick + + digitalEntry.buttons.leftStickUp = defaultEntry.leftY >= threshold; + digitalEntry.buttons.leftStickDown = defaultEntry.leftY <= -threshold; + digitalEntry.buttons.leftStickLeft = defaultEntry.leftX <= -threshold; + digitalEntry.buttons.leftStickRight = defaultEntry.leftX >= threshold; + + digitalEntry.buttons.rightStickUp = defaultEntry.rightY >= threshold; + digitalEntry.buttons.rightStickDown = defaultEntry.rightY <= -threshold; + digitalEntry.buttons.rightStickLeft = defaultEntry.rightX <= -threshold; + digitalEntry.buttons.rightStickRight = defaultEntry.rightX >= threshold; + globalTimestamp++; } } diff --git a/app/src/main/cpp/skyline/input/npad_device.h b/app/src/main/cpp/skyline/input/npad_device.h index 94a84285..c303f06e 100644 --- a/app/src/main/cpp/skyline/input/npad_device.h +++ b/app/src/main/cpp/skyline/input/npad_device.h @@ -9,8 +9,8 @@ namespace skyline::input { /** * @brief This enumerates all the orientations of the Joy-Con(s) */ - enum class NpadJoyOrientation : u64 { - Vertical = 0, //!< The Joy-Con is held vertically + enum class NpadJoyOrientation : i64 { + Vertical = 0, //!< The Joy-Con is held vertically (Default) Horizontal = 1, //!< The Joy-Con is held horizontally }; @@ -18,6 +18,7 @@ namespace skyline::input { * @brief This holds all the NPad styles (https://switchbrew.org/wiki/HID_services#NpadStyleTag) */ union NpadStyleSet { + u32 raw; struct { bool proController : 1; //!< The Pro Controller bool joyconHandheld : 1; //!< Joy-Cons in handheld mode @@ -30,7 +31,6 @@ namespace skyline::input { bool nesHandheld : 1; //!< NES controller in handheld mode bool snes : 1; //!< SNES controller }; - u32 raw; }; static_assert(sizeof(NpadStyleSet) == 0x4); @@ -46,10 +46,10 @@ namespace skyline::input { * @brief This enumerates all of the axis on NPad controllers */ enum class NpadAxisId { + LX, //!< Left Stick X + LY, //!< Left Stick Y RX, //!< Right Stick X RY, //!< Right Stick Y - LX, //!< Left Stick X - LY //!< Left Stick Y }; /** @@ -68,14 +68,17 @@ namespace skyline::input { Handheld = 0x20 //!< Handheld mode }; + class NpadManager; + + /** + * @brief This class abstracts a single NPad device that controls it's own state and shared memory section + */ class NpadDevice { private: - NpadId id; //!< The ID of this controller - NpadControllerType controllerType{}; //!< The type of this controller - u8 globalTimestamp{}; //!< The global timestamp of the state entries - - NpadConnectionState connectionState{}; //!< The state of the connection + NpadManager &manager; //!< The manager responsible for managing this NpadDevice NpadSection §ion; //!< The section in HID shared memory for this controller + NpadControllerInfo *controllerInfo; //!< The controller info for this controller's type + u64 globalTimestamp{}; //!< The global timestamp of the state entries /** * @brief This updates the headers and creates a new entry in HID Shared Memory @@ -90,21 +93,37 @@ namespace skyline::input { NpadControllerInfo &GetControllerInfo(); public: - bool supported{false}; //!< If this specific NpadId was marked by the application as supported + NpadId id; //!< The ID of this controller + NpadControllerType type{}; //!< The type of this controller + NpadConnectionState connectionState{}; //!< The state of the connection + bool explicitAssignment{false}; //!< If an assignment has explicitly been set or is the default for this controller - NpadDevice(NpadSection §ion, NpadId id); + NpadDevice(NpadManager &manager, NpadSection §ion, NpadId id); /** * @brief This sets a Joy-Con's Assignment Mode * @param assignment The assignment mode to set this controller to + * @param isDefault If this is setting the default assignment mode of the controller */ - inline void SetAssignment(NpadJoyAssignment assignment) { - section.header.assignment = assignment; + inline void SetAssignment(NpadJoyAssignment assignment, bool isDefault) { + if (!isDefault) { + section.header.assignment = assignment; + explicitAssignment = true; + } else if (!explicitAssignment) { + section.header.assignment = assignment; + } + } + + /** + * @return The assignment mode of this Joy-Con + */ + inline NpadJoyAssignment GetAssignment() { + return section.header.assignment; } /** * @brief This connects this controller to the guest - * @param type The type of controller to connect + * @param type The type of controller to connect as */ void Connect(NpadControllerType type); diff --git a/app/src/main/cpp/skyline/input/sections/Keyboard.h b/app/src/main/cpp/skyline/input/sections/Keyboard.h index ba023143..22bcac5b 100644 --- a/app/src/main/cpp/skyline/input/sections/Keyboard.h +++ b/app/src/main/cpp/skyline/input/sections/Keyboard.h @@ -10,6 +10,7 @@ namespace skyline::input { * @brief This enumerates all the modifier keys that can be used */ union ModifierKey { + u64 raw; struct { bool LControl : 1; //!< Left Control Key bool LShift : 1; //!< Left Shift Key @@ -23,7 +24,6 @@ namespace skyline::input { bool ScrLock : 1; //!< Scroll-Lock Key bool NumLock : 1; //!< Num-Lock Key }; - u64 raw; }; /** diff --git a/app/src/main/cpp/skyline/input/sections/Npad.h b/app/src/main/cpp/skyline/input/sections/Npad.h index f6f9ae16..ea338562 100644 --- a/app/src/main/cpp/skyline/input/sections/Npad.h +++ b/app/src/main/cpp/skyline/input/sections/Npad.h @@ -24,7 +24,7 @@ namespace skyline::input { * @brief This enumerates all the possible assignments of the Joy-Con(s) */ enum class NpadJoyAssignment : u32 { - Dual = 0, //!< Dual Joy-Cons + Dual = 0, //!< Dual Joy-Cons (Default) Single = 1, //!< Single Joy-Con }; @@ -66,6 +66,7 @@ namespace skyline::input { * @brief This is a bit-field of all the buttons on an NPad (https://switchbrew.org/wiki/HID_Shared_Memory#NpadButton) */ union NpadButton { + u64 raw; struct { bool a : 1; //!< The A button bool b : 1; //!< The B button @@ -91,12 +92,11 @@ namespace skyline::input { bool rightStickUp : 1; //!< Right stick up bool rightStickRight : 1; //!< Right stick right bool rightStickDown : 1; //!< Right stick down - bool leftSL : 1; //!< Left Joy-Con SL button + bool leftSl : 1; //!< Left Joy-Con SL button bool leftSr : 1; //!< Left Joy-Con SR button bool rightSl : 1; //!< Right Joy-Con SL button bool rightSr : 1; //!< Right Joy-Con SR button }; - u64 raw; }; static_assert(sizeof(NpadButton) == 0x8); @@ -104,15 +104,15 @@ namespace skyline::input { * @brief This structure holds data about the state of the connection with the controller */ union NpadConnectionState { + u64 raw; struct { - bool connected : 1; //!< If the controller is connected + bool connected : 1; //!< If the controller is connected (Not in handheld mode) bool handheld : 1; //!< If the controller is in handheld mode - bool leftJoyconConnected : 1; //!< If the left Joy-Con is connected + bool leftJoyconConnected : 1; //!< If the left Joy-Con is connected (Not in handheld mode) bool leftJoyconHandheld : 1; //!< If the left Joy-Con is handheld - bool rightJoyconConnected : 1; //!< If the right Joy-Con is connected + bool rightJoyconConnected : 1; //!< If the right Joy-Con is connected (Not in handheld mode) bool rightJoyconHandheld : 1; //!< If the right Joy-Con is handheld }; - u64 raw; }; static_assert(sizeof(NpadConnectionState) == 0x8); @@ -184,6 +184,7 @@ namespace skyline::input { * @brief This is a bit-field of all the device types (https://switchbrew.org/wiki/HID_services#DeviceType) */ union NpadDeviceType { + u32 raw; struct { bool fullKey : 1; //!< Pro/GC controller bool debugPad : 1; //!< Debug controller @@ -204,7 +205,6 @@ namespace skyline::input { u32 _unk_ : 15; bool system : 1; //!< Generic controller }; - u32 raw; }; static_assert(sizeof(NpadDeviceType) == 0x4); @@ -212,23 +212,23 @@ namespace skyline::input { * @brief This structure holds the system properties of this NPad (https://switchbrew.org/wiki/HID_Shared_Memory#NpadSystemProperties) */ union NpadSystemProperties { - struct { - bool singleCharging : 1; //!< Info 0 Charging - bool leftCharging : 1; //!< Info 1 Charging - bool rightCharging : 1; //!< Info 2 Charging - bool singlePowerConnected : 1; //!< Info 0 Connected - bool leftPowerConnected : 1; //!< Info 1 Connected - bool rightPowerConnected : 1; //!< Info 2 Connected - u64 _unk_ : 3; - bool unsupportedButtonPressedSystem : 1; //!< Unsupported buttons are pressed on system controller - bool unsupportedButtonPressedSystemExt : 1; //!< Unsupported buttons are pressed on system external controller - bool ABXYButtonOriented : 1; //!< Are the ABXY Buttons oriented - bool SLSRuttonOriented : 1; //!< Are the SLSR Buttons oriented - bool plusButtonCapability : 1; //!< Does the + button exist - bool minusButtonCapability : 1; //!< Does the - button exist - bool directionalButtonsSupported : 1; //!< Does the controller have a D-Pad - }; u64 raw; + struct { + bool singleCharging : 1; //!< If a single unit is charging (Handheld, Pro-Con) + bool leftCharging : 1; //!< If the left Joy-Con is charging + bool rightCharging : 1; //!< If the right Joy-Con is charging + bool singlePowerConnected : 1; //!< If a single unit is connected to a power source (Handheld, Pro-Con) + bool leftPowerConnected : 1; //!< If the left Joy-Con is connected to a power source + bool rightPowerConnected : 1; //!< If the right Joy-Con is connected to a power source + u64 _unk_ : 3; + bool unsupportedButtonPressedSystem : 1; //!< If an unsupported buttons was pressed on system controller + bool unsupportedButtonPressedSystemExt : 1; //!< If an unsupported buttons was pressed on system external controller + bool abxyButtonsOriented : 1; //!< If the ABXY Buttons oriented + bool slSrButtonOriented : 1; //!< If the SL/SR Buttons oriented + bool plusButtonCapability : 1; //!< If the + button exists + bool minusButtonCapability : 1; //!< If the - button exists + bool directionalButtonsSupported : 1; //!< If the controller has a D-Pad + }; }; static_assert(sizeof(NpadSystemProperties) == 0x8); @@ -236,10 +236,10 @@ namespace skyline::input { * @brief This structure holds data about the System Buttons (Home, Sleep and Capture) on an NPad (https://switchbrew.org/wiki/HID_Shared_Memory#NpadSystemButtonProperties) */ union NpadSystemButtonProperties { + u32 raw; struct { bool unintendedHomeButtonInputProtectionEnabled : 1; //!< If the Unintended Home Button Input Protection is enabled or not }; - u32 raw; }; static_assert(sizeof(NpadSystemButtonProperties) == 0x4); @@ -262,11 +262,11 @@ namespace skyline::input { NpadControllerInfo fullKeyController; //!< The Pro/GC controller data NpadControllerInfo handheldController; //!< The Handheld controller data - NpadControllerInfo dualController; //!< The Dual Joy-Con controller data - NpadControllerInfo leftController; //!< The Left Joy-Con controller data - NpadControllerInfo rightController; //!< The Right Joy-Con controller data - NpadControllerInfo palmaController; //!< The Poké Ball Plus controller data - NpadControllerInfo systemExtController; //!< The System External controller data + NpadControllerInfo dualController; //!< The Dual Joy-Con controller data (Only in Dual Mode, no input rotation based on rotation) + NpadControllerInfo leftController; //!< The Left Joy-Con controller data (Only in Single Mode, no input rotation based on rotation) + NpadControllerInfo rightController; //!< The Right Joy-Con controller data (Only in Single Mode, no input rotation based on rotation) + NpadControllerInfo digitalController; //!< The Default Digital controller data (Same as Default but Analog Sticks are converted into 8-directional Digital Sticks) + NpadControllerInfo defaultController; //!< The Default controller data (Inputs are rotated based on orientation and SL/SR are mapped to L/R incase it is a single JC) NpadSixaxisInfo fullKeySixaxis; //!< The Pro/GC IMU data NpadSixaxisInfo handheldSixaxis; //!< The Handheld IMU data @@ -281,7 +281,9 @@ namespace skyline::input { NpadSystemProperties systemProperties; //!< The system properties of this controller NpadSystemButtonProperties buttonProperties; //!< The system button properties of this controller - NpadBatteryLevel batteryLevel[3]; //!< The battery level of this controller + NpadBatteryLevel singleBatteryLevel; //!< The battery level of a single unit (Handheld, Pro-Con) + NpadBatteryLevel leftBatteryLevel; //!< The battery level of the left Joy-Con + NpadBatteryLevel rightBatteryLevel; //!< The battery level of the right Joy-Con u32 _pad1_[0x395]; }; diff --git a/app/src/main/cpp/skyline/input/sections/common.h b/app/src/main/cpp/skyline/input/sections/common.h index 2e4589fc..70f679b7 100644 --- a/app/src/main/cpp/skyline/input/sections/common.h +++ b/app/src/main/cpp/skyline/input/sections/common.h @@ -9,7 +9,8 @@ namespace skyline { namespace constant { constexpr u8 HidEntryCount = 17; //!< The amount of entries in each HID device - constexpr u8 NpadCount = 10; //!< The number of npads in shared memory + constexpr u8 NpadCount = 10; //!< The amount of NPads in shared memory + constexpr u8 ControllerCount = 8; //!< The maximum amount of host controllers constexpr u32 NpadBatteryFull = 2; //!< The full battery state of an npad } @@ -19,9 +20,9 @@ namespace skyline { */ struct CommonHeader { u64 timestamp; //!< The timestamp of the latest entry in ticks - u64 entryCount; //!< The number of entries (17) + u64 entryCount{constant::HidEntryCount}; //!< The number of entries (17) u64 currentEntry; //!< The index of the latest entry - u64 maxEntry; //!< The maximum entry index (16) + u64 maxEntry{constant::HidEntryCount - 1}; //!< The maximum entry index (16) }; static_assert(sizeof(CommonHeader) == 0x20); } diff --git a/app/src/main/cpp/skyline/nce.cpp b/app/src/main/cpp/skyline/nce.cpp index d21aa17c..84e5b6fa 100644 --- a/app/src/main/cpp/skyline/nce.cpp +++ b/app/src/main/cpp/skyline/nce.cpp @@ -89,11 +89,9 @@ namespace skyline { try { while (true) { std::lock_guard guard(JniMtx); - - if (Halt) - break; - - state.gpu->Loop(); + if (!Halt) { + state.gpu->Loop(); + } } } catch (const std::exception &e) { state.logger->Error(e.what()); diff --git a/app/src/main/cpp/skyline/services/hid/IHidServer.cpp b/app/src/main/cpp/skyline/services/hid/IHidServer.cpp index 7ee56e21..f65cef17 100644 --- a/app/src/main/cpp/skyline/services/hid/IHidServer.cpp +++ b/app/src/main/cpp/skyline/services/hid/IHidServer.cpp @@ -17,9 +17,7 @@ namespace skyline::service::hid { {0x7A, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingleByDefault)}, {0x7B, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingle)}, {0x7C, SFUNC(IHidServer::SetNpadJoyAssignmentModeDual)} - }) { - state.input->npad.Activate(); - } + }) {} void IHidServer::CreateAppletResource(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { manager.RegisterService(SRVREG(IAppletResource), session, response); @@ -36,13 +34,15 @@ namespace skyline::service::hid { const auto &buffer = request.inputBuf.at(0); u64 address = buffer.address; size_t size = buffer.size / sizeof(NpadId); + std::vector supportedIds; for (size_t i = 0; i < size; i++) { - auto id = state.process->GetObject(address); - state.input->npad.at(id).supported = true; - + supportedIds.push_back(state.process->GetObject(address)); address += sizeof(NpadId); } + + state.input->npad.supportedIds = supportedIds; + state.input->npad.Update(); } void IHidServer::ActivateNpad(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { @@ -60,16 +60,17 @@ namespace skyline::service::hid { void IHidServer::SetNpadJoyAssignmentModeSingleByDefault(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { auto id = request.Pop(); - state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Single); + state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Single, true); + state.input->npad.Update(); } void IHidServer::SetNpadJoyAssignmentModeSingle(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { auto id = request.Pop(); - state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Single); + state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Single, false); } void IHidServer::SetNpadJoyAssignmentModeDual(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { auto id = request.Pop(); - state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Dual); + state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Dual, false); } } diff --git a/app/src/main/java/emu/skyline/EmulationActivity.kt b/app/src/main/java/emu/skyline/EmulationActivity.kt index 81714724..3e7530d7 100644 --- a/app/src/main/java/emu/skyline/EmulationActivity.kt +++ b/app/src/main/java/emu/skyline/EmulationActivity.kt @@ -15,9 +15,7 @@ import android.util.Log import android.view.* import androidx.appcompat.app.AppCompatActivity import androidx.preference.PreferenceManager -import emu.skyline.input.AxisId -import emu.skyline.input.ButtonId -import emu.skyline.input.ButtonState +import emu.skyline.input.* import emu.skyline.loader.getRomFormat import kotlinx.android.synthetic.main.emu_activity.* import java.io.File @@ -38,14 +36,26 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { */ private lateinit var preferenceFd : ParcelFileDescriptor + /** + * The [InputManager] class handles loading/saving the input data + */ + lateinit var input : InputManager + + /** + * A boolean flag denoting the current operation mode of the emulator (Docked = true/Handheld = false) + */ + private var operationMode : Boolean = true + /** * The surface object used for displaying frames */ + @Volatile private var surface : Surface? = null /** * A boolean flag denoting if the emulation thread should call finish() or not */ + @Volatile private var shouldFinish : Boolean = true /** @@ -89,14 +99,63 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { private external fun getFrametime() : Float /** - * This sets the state of a specific button + * This initializes a guest controller in libskyline + * + * @param index The arbitrary index of the controller, this is to handle matching with a partner Joy-Con + * @param type The type of the host controller + * @param partnerIndex The index of a partner Joy-Con if there is one */ - private external fun setButtonState(id : Long, state : Int) + private external fun setController(index : Int, type : Int, partnerIndex : Int = -1) /** - * This sets the value of a specific axis + * This flushes the controller updates on the guest */ - private external fun setAxisValue(id : Int, value : Int) + private external fun updateControllers() + + /** + * This sets the state of the buttons specified in the mask on a specific controller + * + * @param index The index of the controller this is directed to + * @param mask The mask of the button that are being set + * @param state The state to set the button to + */ + private external fun setButtonState(index : Int, mask : Long, state : Int) + + /** + * This sets the value of a specific axis on a specific controller + * + * @param index The index of the controller this is directed to + * @param axis The ID of the axis that is being modified + * @param value The value to set the axis to + */ + private external fun setAxisValue(index : Int, axis : Int, value : Int) + + /** + * This initializes all of the controllers from [input] on the guest + */ + private fun initializeControllers() { + for (entry in input.controllers) { + val controller = entry.value + + if (controller.type != ControllerType.None) { + val type : Int = when (controller.type) { + ControllerType.None -> throw IllegalArgumentException() + ControllerType.HandheldProController -> if (operationMode) ControllerType.ProController.id else ControllerType.HandheldProController.id + ControllerType.ProController, ControllerType.JoyConLeft, ControllerType.JoyConRight -> controller.type.id + } + + val partnerIndex : Int? = when (controller) { + is JoyConLeftController -> controller.partnerId + is JoyConRightController -> controller.partnerId + else -> null + } + + setController(entry.key, type, partnerIndex ?: -1) + } + } + + updateControllers() + } /** * This executes the specified ROM, [preferenceFd] is assumed to be valid beforehand @@ -108,9 +167,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { romFd = contentResolver.openFileDescriptor(rom, "r")!! emulationThread = Thread { - while ((surface == null)) + while (surface == null) Thread.yield() + runOnUiThread { initializeControllers() } + executeApplication(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, applicationContext.filesDir.canonicalPath + "/") if (shouldFinish) @@ -142,6 +203,8 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { or View.SYSTEM_UI_FLAG_FULLSCREEN) } + input = InputManager(this) + val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml") preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE) @@ -150,16 +213,20 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) if (sharedPreferences.getBoolean("perf_stats", false)) { - lateinit var perfRunnable : Runnable - - perfRunnable = Runnable { - perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms" - perf_stats.postDelayed(perfRunnable, 250) + val perfRunnable = object : Runnable { + override fun run() { + perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms" + perf_stats.postDelayed(this, 250) + } } perf_stats.postDelayed(perfRunnable, 250) } + operationMode = sharedPreferences.getBoolean("operation_mode", operationMode) + + windowManager.defaultDisplay.supportedModes.maxBy { it.refreshRate + (it.physicalHeight * it.physicalWidth) }?.let { window.attributes.preferredDisplayModeId = it.modeId } + executeApplication(intent.data!!) } @@ -222,111 +289,91 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { } /** - * This handles passing on any key events to libskyline + * This handles translating any [KeyHostEvent]s to a [GuestEvent] that is passed into libskyline */ override fun dispatchKeyEvent(event : KeyEvent) : Boolean { + if (event.repeatCount != 0) + return super.dispatchKeyEvent(event) + val action : ButtonState = when (event.action) { KeyEvent.ACTION_DOWN -> ButtonState.Pressed KeyEvent.ACTION_UP -> ButtonState.Released - else -> return false + else -> return super.dispatchKeyEvent(event) } - val buttonMap : Map = mapOf( - KeyEvent.KEYCODE_BUTTON_A to ButtonId.A, - KeyEvent.KEYCODE_BUTTON_B to ButtonId.B, - KeyEvent.KEYCODE_BUTTON_X to ButtonId.X, - KeyEvent.KEYCODE_BUTTON_Y to ButtonId.Y, - KeyEvent.KEYCODE_BUTTON_THUMBL to ButtonId.LeftStick, - KeyEvent.KEYCODE_BUTTON_THUMBR to ButtonId.RightStick, - KeyEvent.KEYCODE_BUTTON_L1 to ButtonId.L, - KeyEvent.KEYCODE_BUTTON_R1 to ButtonId.R, - KeyEvent.KEYCODE_BUTTON_L2 to ButtonId.ZL, - KeyEvent.KEYCODE_BUTTON_R2 to ButtonId.ZR, - KeyEvent.KEYCODE_BUTTON_START to ButtonId.Plus, - KeyEvent.KEYCODE_BUTTON_SELECT to ButtonId.Minus, - KeyEvent.KEYCODE_DPAD_DOWN to ButtonId.DpadDown, - KeyEvent.KEYCODE_DPAD_UP to ButtonId.DpadUp, - KeyEvent.KEYCODE_DPAD_LEFT to ButtonId.DpadLeft, - KeyEvent.KEYCODE_DPAD_RIGHT to ButtonId.DpadRight) + return when (val guestEvent = input.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) { + is ButtonGuestEvent -> { + if (guestEvent.button != ButtonId.Menu) + setButtonState(guestEvent.id, guestEvent.button.value(), action.ordinal) + true + } - return try { - setButtonState(buttonMap.getValue(event.keyCode).value(), action.ordinal) - true - } catch (ignored : NoSuchElementException) { - super.dispatchKeyEvent(event) + is AxisGuestEvent -> { + setAxisValue(guestEvent.id, guestEvent.axis.ordinal, (if (action == ButtonState.Pressed) if (guestEvent.polarity) Short.MAX_VALUE else Short.MIN_VALUE else 0).toInt()) + true + } + + else -> super.dispatchKeyEvent(event) } } /** - * This is the controller HAT X value + * The last value of the axes so the stagnant axes can be eliminated to not wastefully look them up */ - private var controllerHatX : Float = 0.0f + private val axesHistory = arrayOfNulls(MotionHostEvent.axes.size) /** - * This is the controller HAT Y value + * The last value of the HAT axes so it can be ignored in [onGenericMotionEvent] so they are handled by [dispatchKeyEvent] instead */ - private var controllerHatY : Float = 0.0f + private var oldHat = Pair(0.0f, 0.0f) /** - * This handles passing on any motion events to libskyline + * This handles translating any [MotionHostEvent]s to a [GuestEvent] that is passed into libskyline */ - override fun dispatchGenericMotionEvent(event : MotionEvent) : Boolean { - if ((event.source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || - (event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) { - val hatXMap : Map = mapOf( - -1.0f to ButtonId.DpadLeft, - +1.0f to ButtonId.DpadRight) + override fun onGenericMotionEvent(event : MotionEvent) : Boolean { + if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE) { + val hat = Pair(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y)) - val hatYMap : Map = mapOf( - -1.0f to ButtonId.DpadUp, - +1.0f to ButtonId.DpadDown) + if (hat == oldHat) { + for (axisItem in MotionHostEvent.axes.withIndex()) { + val axis = axisItem.value + var value = event.getAxisValue(axis) - if (controllerHatX != event.getAxisValue(MotionEvent.AXIS_HAT_X)) { - if (event.getAxisValue(MotionEvent.AXIS_HAT_X) == 0.0f) - setButtonState(hatXMap.getValue(controllerHatX).value(), ButtonState.Released.ordinal) - else - setButtonState(hatXMap.getValue(event.getAxisValue(MotionEvent.AXIS_HAT_X)).value(), ButtonState.Pressed.ordinal) + if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) { + var polarity = value >= 0 - controllerHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X) + val guestEvent = input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) { + polarity = false + input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] + } else { + null + } - return true - } + when (guestEvent) { + is ButtonGuestEvent -> { + if (guestEvent.button != ButtonId.Menu) + setButtonState(guestEvent.id, guestEvent.button.value(), if (abs(value) >= guestEvent.threshold) ButtonState.Pressed.ordinal else ButtonState.Released.ordinal) + } - if (controllerHatY != event.getAxisValue(MotionEvent.AXIS_HAT_Y)) { - if (event.getAxisValue(MotionEvent.AXIS_HAT_Y) == 0.0f) - setButtonState(hatYMap.getValue(controllerHatY).value(), ButtonState.Released.ordinal) - else - setButtonState(hatYMap.getValue(event.getAxisValue(MotionEvent.AXIS_HAT_Y)).value(), ButtonState.Pressed.ordinal) + is AxisGuestEvent -> { + value = guestEvent.value(value) + value = if (polarity) abs(value) else -abs(value) + value = if (guestEvent.axis == AxisId.LX || guestEvent.axis == AxisId.RX) value else -value // TODO: Test this - controllerHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y) + setAxisValue(guestEvent.id, guestEvent.axis.ordinal, (value * Short.MAX_VALUE).toInt()) + } + } + } - return true - } - } - - if ((event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.action == MotionEvent.ACTION_MOVE) { - val axisMap : Map = mapOf( - MotionEvent.AXIS_X to AxisId.LX, - MotionEvent.AXIS_Y to AxisId.LY, - MotionEvent.AXIS_Z to AxisId.RX, - MotionEvent.AXIS_RZ to AxisId.RY) - - //TODO: Digital inputs based off of analog sticks - event.device.motionRanges.forEach { - if (axisMap.containsKey(it.axis)) { - var axisValue : Float = event.getAxisValue(it.axis) - if (abs(axisValue) <= it.flat) - axisValue = 0.0f - - val ratio : Float = axisValue / (it.max - it.min) - val rangedAxisValue : Int = (ratio * (Short.MAX_VALUE - Short.MIN_VALUE)).toInt() - - setAxisValue(axisMap.getValue(it.axis).ordinal, rangedAxisValue) + axesHistory[axisItem.index] = value } + + return true + } else { + oldHat = hat } - return true } - return super.dispatchGenericMotionEvent(event) + return super.onGenericMotionEvent(event) } } diff --git a/app/src/main/java/emu/skyline/input/Controller.kt b/app/src/main/java/emu/skyline/input/Controller.kt index 843acb77..ffda7fe0 100644 --- a/app/src/main/java/emu/skyline/input/Controller.kt +++ b/app/src/main/java/emu/skyline/input/Controller.kt @@ -14,12 +14,12 @@ import java.io.Serializable * @param stringRes The string resource of the controller's name * @param firstController If the type only applies to the first controller */ -enum class ControllerType(val stringRes : Int, val firstController : Boolean, val sticks : Array = arrayOf(), val buttons : Array = arrayOf()) { - None(R.string.none, false), - HandheldProController(R.string.handheld_procon, true, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus)), - ProController(R.string.procon, false, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus)), - JoyConLeft(R.string.ljoycon, false, arrayOf(StickId.Left), arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.ZL, ButtonId.Minus, ButtonId.LeftSL, ButtonId.LeftSR)), - JoyConRight(R.string.rjoycon, false, arrayOf(StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.R, ButtonId.ZR, ButtonId.Plus, ButtonId.RightSL, ButtonId.RightSR)), +enum class ControllerType(val stringRes : Int, val firstController : Boolean, val sticks : Array = arrayOf(), val buttons : Array = arrayOf(), val id : Int) { + None(R.string.none, false, id=0b0), + ProController(R.string.procon, false, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus), 0b1), + HandheldProController(R.string.handheld_procon, true, arrayOf(StickId.Left, StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR, ButtonId.Plus, ButtonId.Minus), 0b10), + JoyConLeft(R.string.ljoycon, false, arrayOf(StickId.Left), arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight, ButtonId.L, ButtonId.ZL, ButtonId.Minus, ButtonId.LeftSL, ButtonId.LeftSR), 0b1000), + JoyConRight(R.string.rjoycon, false, arrayOf(StickId.Right), arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y, ButtonId.R, ButtonId.ZR, ButtonId.Plus, ButtonId.RightSL, ButtonId.RightSR), 0b10000), } /** diff --git a/app/src/main/java/emu/skyline/input/GuestEvent.kt b/app/src/main/java/emu/skyline/input/GuestEvent.kt index 0bbfa3b6..1a939ff6 100644 --- a/app/src/main/java/emu/skyline/input/GuestEvent.kt +++ b/app/src/main/java/emu/skyline/input/GuestEvent.kt @@ -63,10 +63,10 @@ enum class ButtonState(val state : Boolean) { * This enumerates all of the axes on a controller that the emulator recognizes */ enum class AxisId { - RX, - RY, LX, LY, + RX, + RY, } /** diff --git a/app/src/main/java/emu/skyline/input/HostEvent.kt b/app/src/main/java/emu/skyline/input/HostEvent.kt index a77d81fd..10e6fe18 100644 --- a/app/src/main/java/emu/skyline/input/HostEvent.kt +++ b/app/src/main/java/emu/skyline/input/HostEvent.kt @@ -32,6 +32,9 @@ abstract class HostEvent(val descriptor : String = "") : Serializable { abstract override fun hashCode() : Int } +/** + * This class represents all events on the host that arise from a [KeyEvent] + */ class KeyHostEvent(descriptor : String = "", val keyCode : Int) : HostEvent(descriptor) { /** * This returns the string representation of [keyCode] @@ -49,7 +52,17 @@ class KeyHostEvent(descriptor : String = "", val keyCode : Int) : HostEvent(desc override fun hashCode() : Int = Objects.hash(descriptor, keyCode) } +/** + * This class represents all events on the host that arise from a [MotionEvent] + */ class MotionHostEvent(descriptor : String = "", val axis : Int, val polarity : Boolean) : HostEvent(descriptor) { + companion object { + /** + * This is an array of all the axes that are checked during a [MotionEvent] + */ + val axes = arrayOf(MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER, MotionEvent.AXIS_THROTTLE, MotionEvent.AXIS_RUDDER, MotionEvent.AXIS_WHEEL, MotionEvent.AXIS_GAS, MotionEvent.AXIS_BRAKE).plus(IntRange(MotionEvent.AXIS_GENERIC_1, MotionEvent.AXIS_GENERIC_16).toList()) + } + /** * This returns the string representation of [axis] combined with [polarity] */ diff --git a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt index f08de67e..4bcd6ac5 100644 --- a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt @@ -197,7 +197,7 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment( context.axisMap[(guestEvent as AxisGuestEvent).axis]?.update() } - guestEvent = ButtonGuestEvent(controller.id, item.button) + guestEvent = ButtonGuestEvent(controller.id, item.button, threshold) context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) } diff --git a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt index 4757c9a2..4a61f677 100644 --- a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt @@ -17,6 +17,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import emu.skyline.R import emu.skyline.adapter.ControllerStickItem import emu.skyline.input.* +import emu.skyline.input.MotionHostEvent.Companion.axes import kotlinx.android.synthetic.main.stick_dialog.* import java.util.* import kotlin.math.abs @@ -265,10 +266,6 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment() var axisPolarity = false // The polarity of the axis for the currently selected event var axisRunnable : Runnable? = null // The Runnable that is used for counting down till an axis is selected - // The last values of the HAT axes so that they can be ignored in [View.OnGenericMotionListener] so they are passed onto [DialogInterface.OnKeyListener] as [KeyEvent]s - var oldDpadX = 0.0f - var oldDpadY = 0.0f - stick_next.setOnClickListener { gotoStage(1) @@ -304,11 +301,7 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment() } is AxisGuestEvent -> { - val coefficient = if (event.action == KeyEvent.ACTION_DOWN) { - if (guestEvent.polarity) 1 else -1 - } else { - 0 - } + val coefficient = if (event.action == KeyEvent.ACTION_DOWN) if (guestEvent.polarity) 1 else -1 else 0 if (guestEvent.axis == item.stick.xAxis) { stick_container?.translationX = dipToPixels(16.5f) * coefficient @@ -402,18 +395,17 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment() } } - val axes = arrayOf(MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER, MotionEvent.AXIS_THROTTLE, MotionEvent.AXIS_RUDDER, MotionEvent.AXIS_WHEEL, MotionEvent.AXIS_GAS, MotionEvent.AXIS_BRAKE).plus(IntRange(MotionEvent.AXIS_GENERIC_1, MotionEvent.AXIS_GENERIC_16).toList()) - val axesHistory = arrayOfNulls(axes.size) // The last recorded value of an axis, this is used to eliminate any stagnant axes val axesMax = Array(axes.size) { 0f } // The maximum recorded value of the axis, this is to scale the axis to a stick accordingly (The value is also checked at runtime, so it's fine if this isn't the true maximum) + var oldHat = Pair(0.0f, 0.0f) // The last values of the HAT axes so that they can be ignored in [View.OnGenericMotionListener] so they are passed onto [DialogInterface.OnKeyListener] as [KeyEvent]s + view?.setOnGenericMotionListener { _, event -> // We retrieve the value of the HAT axes so that we can check for change and ignore any input from them so it'll be passed onto the [KeyEvent] handler - val dpadX = event.getAxisValue(MotionEvent.AXIS_HAT_X) - val dpadY = event.getAxisValue(MotionEvent.AXIS_HAT_Y) + val hat = Pair(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y)) // We want all input events from Joysticks and Buttons that are [MotionEvent.ACTION_MOVE] and not from the D-pad - if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE && dpadX == oldDpadX && dpadY == oldDpadY) { + if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE && hat == oldHat) { if (stage == DialogStage.Stick) { // When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly for (axisItem in axes.withIndex()) { @@ -592,8 +584,7 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment() true } else { - oldDpadX = dpadX - oldDpadY = dpadY + oldHat = hat false }