Implement C++ Support for Controller Configuration

This commit adds support to the C++ end of things for controller configuration. It isn't targeting being 1:1 to HOS for controller assignment but is rather based on intuition of how things should be.
This commit is contained in:
◱ PixelyIon 2020-08-15 19:21:23 +05:30 committed by ◱ PixelyIon
parent 75d485a9a7
commit 6a931b95b0
18 changed files with 564 additions and 257 deletions

View File

@ -10,12 +10,12 @@ jobs:
steps: steps:
- name: Git Checkout - name: Git Checkout
uses: actions/checkout@v1 uses: actions/checkout@v2
with: with:
submodules: true submodules: true
- name: Restore Gradle Cache - name: Restore Gradle Cache
uses: actions/cache@v1.0.3 uses: actions/cache@v2
with: with:
path: /root/.gradle/ path: /root/.gradle/
key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }} key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }}
@ -23,7 +23,7 @@ jobs:
${{ runner.os }}-gradle- ${{ runner.os }}-gradle-
- name: Restore CXX Cache - name: Restore CXX Cache
uses: actions/cache@v1.0.3 uses: actions/cache@v2
with: with:
path: app/.cxx/ path: app/.cxx/
key: ${{ runner.os }}-cxx-${{ hashFiles('**/CMakeLists.txt') }} key: ${{ runner.os }}-cxx-${{ hashFiles('**/CMakeLists.txt') }}
@ -37,7 +37,7 @@ jobs:
run: ./gradlew --stacktrace lint run: ./gradlew --stacktrace lint
- name: Upload Lint Report - name: Upload Lint Report
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v2
with: with:
name: lint-result.html name: lint-result.html
path: app/build/reports/lint-results.html path: app/build/reports/lint-results.html
@ -46,19 +46,19 @@ jobs:
run: ./gradlew assemble run: ./gradlew assemble
- name: Upload Debug APK - name: Upload Debug APK
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v2
with: with:
name: app-debug.apk name: app-debug.apk
path: app/build/outputs/apk/debug/ path: app/build/outputs/apk/debug/
- name: Upload Release APK - name: Upload Release APK
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v2
with: with:
name: app-release.apk name: app-release.apk
path: app/build/outputs/apk/release/ path: app/build/outputs/apk/release/
- name: Upload R8 Mapping - name: Upload R8 Mapping
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v2
with: with:
name: mapping.txt name: mapping.txt
path: app/build/outputs/mapping/release/ path: app/build/outputs/mapping/release/

View File

@ -36,7 +36,7 @@ This can also be done by using `Ctrl + Alt + L` on Windows, `Ctrl + Shift + Alt
* Parameters: `camelCase` * Parameters: `camelCase`
* Files and Directories: `snake_case` except for when they correspond to a HOS structure (EG: Services, Kernel Objects) * 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 ### Comments
Use doxygen style comments for: Use doxygen style comments for:

View File

@ -97,14 +97,31 @@ extern "C" JNIEXPORT jfloat Java_emu_skyline_EmulationActivity_getFrametime(JNIE
return static_cast<float>(frametime) / 100; return static_cast<float>(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<skyline::input::NpadControllerType>(type), static_cast<skyline::i8>(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) { if (input) {
skyline::input::NpadButton button{.raw = static_cast<skyline::u64>(id)}; auto device = input->npad.controllers[index].device;
input->npad.at(skyline::input::NpadId::Player1).SetButtonState(button, static_cast<skyline::input::NpadButtonState>(state)); skyline::input::NpadButton button{.raw = static_cast<skyline::u64>(mask)};
if (device)
device->SetButtonState(button, static_cast<skyline::input::NpadButtonState>(state));
} }
} }
extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setAxisValue(JNIEnv *, jobject, jint id, jint value) { extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setAxisValue(JNIEnv *, jobject, jint index, jint axis, jint value) {
if (input) if (input) {
input->npad.at(skyline::input::NpadId::Player1).SetAxisValue(static_cast<skyline::input::NpadAxisId>(id), value); auto device = input->npad.controllers[index].device;
if (device)
device->SetAxisValue(static_cast<skyline::input::NpadAxisId>(axis), value);
}
} }

View File

@ -5,28 +5,95 @@
#include "npad.h" #include "npad.h"
namespace skyline::input { namespace skyline::input {
NpadManager::NpadManager(const DeviceState &state, input::HidSharedMemory *hid) : state(state), npads NpadManager::NpadManager(const DeviceState &state, input::HidSharedMemory *hid) : state(state), npads
{NpadDevice{hid->npad[0], NpadId::Player1}, {hid->npad[1], NpadId::Player2}, {NpadDevice{*this, hid->npad[0], NpadId::Player1}, {*this, hid->npad[1], NpadId::Player2},
{hid->npad[2], NpadId::Player3}, {hid->npad[3], NpadId::Player4}, {*this, hid->npad[2], NpadId::Player3}, {*this, hid->npad[3], NpadId::Player4},
{hid->npad[4], NpadId::Player5}, {hid->npad[5], NpadId::Player6}, {*this, hid->npad[4], NpadId::Player5}, {*this, hid->npad[5], NpadId::Player6},
{hid->npad[6], NpadId::Player7}, {hid->npad[7], NpadId::Player8}, {*this, hid->npad[6], NpadId::Player7}, {*this, hid->npad[7], NpadId::Player8},
{hid->npad[8], NpadId::Unknown}, {hid->npad[9], NpadId::Handheld}, {*this, hid->npad[8], NpadId::Unknown}, {*this, hid->npad[9], NpadId::Handheld},
} {} } {}
void NpadManager::Activate() { void NpadManager::Update(bool host) {
if (styles.raw == 0) { if (host)
styles.proController = true; updated = true;
styles.joyconHandheld = true; else
} while (!updated)
asm("yield");
at(NpadId::Player1).Connect(state.settings->GetBool("operation_mode") ? NpadControllerType::Handheld : NpadControllerType::ProController); if (!activated)
} return;
void NpadManager::Deactivate() {
styles.raw = 0;
for (auto &npad : npads) for (auto &npad : npads)
npad.Disconnect(); 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;
} }
} }

View File

@ -6,24 +6,32 @@
#include "npad_device.h" #include "npad_device.h"
namespace skyline::input { 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 { class NpadManager {
private: private:
const DeviceState &state; //!< The state of the device const DeviceState &state; //!< The state of the device
std::array<NpadDevice, constant::NpadCount> npads; //!< An array of all the NPad devices std::array<NpadDevice, constant::NpadCount> npads; //!< An array of all the NPad devices
bool activated{false}; //!< If this NpadManager is activated or not
std::atomic<bool> 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 * @brief This translates an NPad's ID into it's index in the array
* @param id The ID of the NPad to translate * @param id The ID of the NPad to translate
* @return The corresponding index of the NPad in the array * @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) { switch (id) {
case NpadId::Unknown:
return 8;
case NpadId::Handheld: case NpadId::Handheld:
return 8;
case NpadId::Unknown:
return 9; return 9;
default: default:
return static_cast<size_t>(id); return static_cast<size_t>(id);
@ -31,7 +39,9 @@ namespace skyline::input {
} }
public: public:
NpadStyleSet styles{}; //!< The styles that are supported in accordance to the host input std::array<GuestController, constant::ControllerCount> controllers; //!< An array of all the available guest controllers
std::vector<NpadId> 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 NpadJoyOrientation orientation{}; //!< The Joy-Con orientation to use
/** /**
@ -43,7 +53,7 @@ namespace skyline::input {
* @param id The ID of the NPad to return * @param id The ID of the NPad to return
* @return A reference to the NPad with the specified ID * @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)); return npads.at(Translate(id));
} }
@ -51,17 +61,23 @@ namespace skyline::input {
* @param id The ID of the NPad to return * @param id The ID of the NPad to return
* @return A reference to the NPad with the specified ID * @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)); 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(); void Activate();
/** /**
* @brief This deactivates the controllers * @brief This disables any activate mappings from guest controllers -> players till Activate has been called
*/ */
void Deactivate(); void Deactivate();
}; };

View File

@ -2,9 +2,10 @@
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) // Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include "npad_device.h" #include "npad_device.h"
#include "npad.h"
namespace skyline::input { namespace skyline::input {
NpadDevice::NpadDevice(NpadSection &section, NpadId id) : section(section), id(id) {} NpadDevice::NpadDevice(NpadManager &manager, NpadSection &section, NpadId id) : manager(manager), section(section), id(id) {}
void NpadDevice::Connect(NpadControllerType type) { void NpadDevice::Connect(NpadControllerType type) {
section.header.type = NpadControllerType::None; section.header.type = NpadControllerType::None;
@ -15,59 +16,121 @@ namespace skyline::input {
connectionState.connected = true; connectionState.connected = true;
switch (type) { 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: case NpadControllerType::Handheld:
section.header.type = NpadControllerType::Handheld; section.header.type = NpadControllerType::Handheld;
section.deviceType.handheldLeft = true; section.deviceType.handheldLeft = true;
section.deviceType.handheldRight = 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.plusButtonCapability = true;
section.systemProperties.minusButtonCapability = true; section.systemProperties.minusButtonCapability = true;
connectionState.handheld = true; connectionState.handheld = true;
connectionState.leftJoyconConnected = true;
connectionState.rightJoyconConnected = true;
connectionState.leftJoyconHandheld = true; connectionState.leftJoyconHandheld = true;
connectionState.rightJoyconHandheld = true; connectionState.rightJoyconHandheld = true;
break; break;
case NpadControllerType::ProController:
section.header.type = NpadControllerType::ProController; case NpadControllerType::JoyconDual:
section.deviceType.fullKey = true; section.header.type = NpadControllerType::JoyconDual;
section.deviceType.joyconLeft = true;
section.deviceType.joyconRight = true; section.deviceType.joyconRight = true;
section.header.assignment = NpadJoyAssignment::Single; section.header.assignment = NpadJoyAssignment::Dual;
section.systemProperties.ABXYButtonOriented = true;
section.systemProperties.directionalButtonsSupported = true;
section.systemProperties.abxyButtonsOriented = true;
section.systemProperties.plusButtonCapability = true; section.systemProperties.plusButtonCapability = true;
section.systemProperties.minusButtonCapability = true; section.systemProperties.minusButtonCapability = true;
connectionState.connected = true;
connectionState.leftJoyconConnected = true;
connectionState.rightJoyconConnected = true;
break; 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: default:
throw exception("Unsupported controller type: {}", type); 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; case NpadControllerType::Handheld:
section.header.singleColor = {0, 0}; 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; case NpadControllerType::None:
section.header.leftColor = {0, 0}; //TODO: make these configurable break;
section.header.rightColor = {0, 0}; }
section.batteryLevel[0] = NpadBatteryLevel::Full; section.singleBatteryLevel = NpadBatteryLevel::Full;
section.batteryLevel[1] = NpadBatteryLevel::Full; section.leftBatteryLevel = NpadBatteryLevel::Full;
section.batteryLevel[2] = NpadBatteryLevel::Full; section.rightBatteryLevel = NpadBatteryLevel::Full;
this->type = type;
controllerInfo = &GetControllerInfo();
SetButtonState(NpadButton{}, NpadButtonState::Released); SetButtonState(NpadButton{}, NpadButtonState::Released);
} }
void NpadDevice::Disconnect() { void NpadDevice::Disconnect() {
connectionState.connected = false; section = {};
explicitAssignment = false;
globalTimestamp = 0;
GetNextEntry(GetControllerInfo()); type = NpadControllerType::None;
GetNextEntry(section.systemExtController); controllerInfo = nullptr;
} }
NpadControllerInfo &NpadDevice::GetControllerInfo() { NpadControllerInfo &NpadDevice::GetControllerInfo() {
switch (controllerType) { switch (type) {
case NpadControllerType::ProController: case NpadControllerType::ProController:
return section.fullKeyController; return section.fullKeyController;
case NpadControllerType::Handheld: case NpadControllerType::Handheld:
@ -79,15 +142,13 @@ namespace skyline::input {
case NpadControllerType::JoyconRight: case NpadControllerType::JoyconRight:
return section.rightController; return section.rightController;
default: default:
throw exception("Cannot find corresponding section for ControllerType: {}", controllerType); throw exception("Cannot find corresponding section for ControllerType: {}", type);
} }
} }
NpadControllerState &NpadDevice::GetNextEntry(NpadControllerInfo &info) { NpadControllerState &NpadDevice::GetNextEntry(NpadControllerInfo &info) {
auto &lastEntry = info.state.at(info.header.currentEntry); 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.currentEntry = (info.header.currentEntry != constant::HidEntryCount - 1) ? info.header.currentEntry + 1 : 0;
info.header.timestamp = util::GetTimeTicks(); info.header.timestamp = util::GetTimeTicks();
@ -106,44 +167,118 @@ namespace skyline::input {
} }
void NpadDevice::SetButtonState(NpadButton mask, NpadButtonState state) { void NpadDevice::SetButtonState(NpadButton mask, NpadButtonState state) {
if (!connectionState.connected) if (!connectionState.connected || !controllerInfo)
return; return;
for (NpadControllerInfo &controllerInfo : {std::ref(GetControllerInfo()), std::ref(section.systemExtController)}) { auto &entry = GetNextEntry(*controllerInfo);
auto &entry = GetNextEntry(controllerInfo);
if (state == NpadButtonState::Pressed) if (state == NpadButtonState::Pressed)
entry.buttons.raw |= mask.raw; entry.buttons.raw |= mask.raw;
else else
entry.buttons.raw &= ~(mask.raw); 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++; globalTimestamp++;
} }
void NpadDevice::SetAxisValue(NpadAxisId axis, i32 value) { void NpadDevice::SetAxisValue(NpadAxisId axis, i32 value) {
if (!connectionState.connected) if (!connectionState.connected || !controllerInfo)
return; return;
for (NpadControllerInfo &controllerInfo : {std::ref(GetControllerInfo()), std::ref(section.systemExtController)}) { auto &controllerEntry = GetNextEntry(*controllerInfo);
auto &entry = GetNextEntry(controllerInfo); auto& defaultEntry = GetNextEntry(section.defaultController);
if (manager.orientation == NpadJoyOrientation::Vertical && (type != NpadControllerType::JoyconLeft && type != NpadControllerType::JoyconRight)) {
switch (axis) { switch (axis) {
case NpadAxisId::LX: case NpadAxisId::LX:
entry.leftX = value; controllerEntry.leftX = value;
defaultEntry.leftX = value;
break; break;
case NpadAxisId::LY: case NpadAxisId::LY:
entry.leftY = -value; // Invert Y axis controllerEntry.leftY = value;
defaultEntry.leftY = value;
break; break;
case NpadAxisId::RX: case NpadAxisId::RX:
entry.rightX = value; controllerEntry.rightX = value;
defaultEntry.rightX = value;
break; break;
case NpadAxisId::RY: 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; 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++; globalTimestamp++;
} }
} }

View File

@ -9,8 +9,8 @@ namespace skyline::input {
/** /**
* @brief This enumerates all the orientations of the Joy-Con(s) * @brief This enumerates all the orientations of the Joy-Con(s)
*/ */
enum class NpadJoyOrientation : u64 { enum class NpadJoyOrientation : i64 {
Vertical = 0, //!< The Joy-Con is held vertically Vertical = 0, //!< The Joy-Con is held vertically (Default)
Horizontal = 1, //!< The Joy-Con is held horizontally 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) * @brief This holds all the NPad styles (https://switchbrew.org/wiki/HID_services#NpadStyleTag)
*/ */
union NpadStyleSet { union NpadStyleSet {
u32 raw;
struct { struct {
bool proController : 1; //!< The Pro Controller bool proController : 1; //!< The Pro Controller
bool joyconHandheld : 1; //!< Joy-Cons in handheld mode bool joyconHandheld : 1; //!< Joy-Cons in handheld mode
@ -30,7 +31,6 @@ namespace skyline::input {
bool nesHandheld : 1; //!< NES controller in handheld mode bool nesHandheld : 1; //!< NES controller in handheld mode
bool snes : 1; //!< SNES controller bool snes : 1; //!< SNES controller
}; };
u32 raw;
}; };
static_assert(sizeof(NpadStyleSet) == 0x4); static_assert(sizeof(NpadStyleSet) == 0x4);
@ -46,10 +46,10 @@ namespace skyline::input {
* @brief This enumerates all of the axis on NPad controllers * @brief This enumerates all of the axis on NPad controllers
*/ */
enum class NpadAxisId { enum class NpadAxisId {
LX, //!< Left Stick X
LY, //!< Left Stick Y
RX, //!< Right Stick X RX, //!< Right Stick X
RY, //!< Right Stick Y RY, //!< Right Stick Y
LX, //!< Left Stick X
LY //!< Left Stick Y
}; };
/** /**
@ -68,14 +68,17 @@ namespace skyline::input {
Handheld = 0x20 //!< Handheld mode 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 { class NpadDevice {
private: private:
NpadId id; //!< The ID of this controller NpadManager &manager; //!< The manager responsible for managing this NpadDevice
NpadControllerType controllerType{}; //!< The type of this controller
u8 globalTimestamp{}; //!< The global timestamp of the state entries
NpadConnectionState connectionState{}; //!< The state of the connection
NpadSection &section; //!< The section in HID shared memory for this controller NpadSection &section; //!< 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 * @brief This updates the headers and creates a new entry in HID Shared Memory
@ -90,21 +93,37 @@ namespace skyline::input {
NpadControllerInfo &GetControllerInfo(); NpadControllerInfo &GetControllerInfo();
public: 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 &section, NpadId id); NpadDevice(NpadManager &manager, NpadSection &section, NpadId id);
/** /**
* @brief This sets a Joy-Con's Assignment Mode * @brief This sets a Joy-Con's Assignment Mode
* @param assignment The assignment mode to set this controller to * @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) { inline void SetAssignment(NpadJoyAssignment assignment, bool isDefault) {
section.header.assignment = assignment; 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 * @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); void Connect(NpadControllerType type);

View File

@ -10,6 +10,7 @@ namespace skyline::input {
* @brief This enumerates all the modifier keys that can be used * @brief This enumerates all the modifier keys that can be used
*/ */
union ModifierKey { union ModifierKey {
u64 raw;
struct { struct {
bool LControl : 1; //!< Left Control Key bool LControl : 1; //!< Left Control Key
bool LShift : 1; //!< Left Shift Key bool LShift : 1; //!< Left Shift Key
@ -23,7 +24,6 @@ namespace skyline::input {
bool ScrLock : 1; //!< Scroll-Lock Key bool ScrLock : 1; //!< Scroll-Lock Key
bool NumLock : 1; //!< Num-Lock Key bool NumLock : 1; //!< Num-Lock Key
}; };
u64 raw;
}; };
/** /**

View File

@ -24,7 +24,7 @@ namespace skyline::input {
* @brief This enumerates all the possible assignments of the Joy-Con(s) * @brief This enumerates all the possible assignments of the Joy-Con(s)
*/ */
enum class NpadJoyAssignment : u32 { enum class NpadJoyAssignment : u32 {
Dual = 0, //!< Dual Joy-Cons Dual = 0, //!< Dual Joy-Cons (Default)
Single = 1, //!< Single Joy-Con 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) * @brief This is a bit-field of all the buttons on an NPad (https://switchbrew.org/wiki/HID_Shared_Memory#NpadButton)
*/ */
union NpadButton { union NpadButton {
u64 raw;
struct { struct {
bool a : 1; //!< The A button bool a : 1; //!< The A button
bool b : 1; //!< The B button bool b : 1; //!< The B button
@ -91,12 +92,11 @@ namespace skyline::input {
bool rightStickUp : 1; //!< Right stick up bool rightStickUp : 1; //!< Right stick up
bool rightStickRight : 1; //!< Right stick right bool rightStickRight : 1; //!< Right stick right
bool rightStickDown : 1; //!< Right stick down 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 leftSr : 1; //!< Left Joy-Con SR button
bool rightSl : 1; //!< Right Joy-Con SL button bool rightSl : 1; //!< Right Joy-Con SL button
bool rightSr : 1; //!< Right Joy-Con SR button bool rightSr : 1; //!< Right Joy-Con SR button
}; };
u64 raw;
}; };
static_assert(sizeof(NpadButton) == 0x8); 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 * @brief This structure holds data about the state of the connection with the controller
*/ */
union NpadConnectionState { union NpadConnectionState {
u64 raw;
struct { 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 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 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 bool rightJoyconHandheld : 1; //!< If the right Joy-Con is handheld
}; };
u64 raw;
}; };
static_assert(sizeof(NpadConnectionState) == 0x8); 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) * @brief This is a bit-field of all the device types (https://switchbrew.org/wiki/HID_services#DeviceType)
*/ */
union NpadDeviceType { union NpadDeviceType {
u32 raw;
struct { struct {
bool fullKey : 1; //!< Pro/GC controller bool fullKey : 1; //!< Pro/GC controller
bool debugPad : 1; //!< Debug controller bool debugPad : 1; //!< Debug controller
@ -204,7 +205,6 @@ namespace skyline::input {
u32 _unk_ : 15; u32 _unk_ : 15;
bool system : 1; //!< Generic controller bool system : 1; //!< Generic controller
}; };
u32 raw;
}; };
static_assert(sizeof(NpadDeviceType) == 0x4); 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) * @brief This structure holds the system properties of this NPad (https://switchbrew.org/wiki/HID_Shared_Memory#NpadSystemProperties)
*/ */
union 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; 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); 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) * @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 { union NpadSystemButtonProperties {
u32 raw;
struct { struct {
bool unintendedHomeButtonInputProtectionEnabled : 1; //!< If the Unintended Home Button Input Protection is enabled or not bool unintendedHomeButtonInputProtectionEnabled : 1; //!< If the Unintended Home Button Input Protection is enabled or not
}; };
u32 raw;
}; };
static_assert(sizeof(NpadSystemButtonProperties) == 0x4); static_assert(sizeof(NpadSystemButtonProperties) == 0x4);
@ -262,11 +262,11 @@ namespace skyline::input {
NpadControllerInfo fullKeyController; //!< The Pro/GC controller data NpadControllerInfo fullKeyController; //!< The Pro/GC controller data
NpadControllerInfo handheldController; //!< The Handheld controller data NpadControllerInfo handheldController; //!< The Handheld controller data
NpadControllerInfo dualController; //!< The Dual Joy-Con 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 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 NpadControllerInfo rightController; //!< The Right Joy-Con controller data (Only in Single Mode, no input rotation based on rotation)
NpadControllerInfo palmaController; //!< The Poké Ball Plus controller data NpadControllerInfo digitalController; //!< The Default Digital controller data (Same as Default but Analog Sticks are converted into 8-directional Digital Sticks)
NpadControllerInfo systemExtController; //!< The System External controller data 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 fullKeySixaxis; //!< The Pro/GC IMU data
NpadSixaxisInfo handheldSixaxis; //!< The Handheld IMU data NpadSixaxisInfo handheldSixaxis; //!< The Handheld IMU data
@ -281,7 +281,9 @@ namespace skyline::input {
NpadSystemProperties systemProperties; //!< The system properties of this controller NpadSystemProperties systemProperties; //!< The system properties of this controller
NpadSystemButtonProperties buttonProperties; //!< The system button 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]; u32 _pad1_[0x395];
}; };

View File

@ -9,7 +9,8 @@
namespace skyline { namespace skyline {
namespace constant { namespace constant {
constexpr u8 HidEntryCount = 17; //!< The amount of entries in each HID device 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 constexpr u32 NpadBatteryFull = 2; //!< The full battery state of an npad
} }
@ -19,9 +20,9 @@ namespace skyline {
*/ */
struct CommonHeader { struct CommonHeader {
u64 timestamp; //!< The timestamp of the latest entry in ticks 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 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); static_assert(sizeof(CommonHeader) == 0x20);
} }

View File

@ -89,11 +89,9 @@ namespace skyline {
try { try {
while (true) { while (true) {
std::lock_guard guard(JniMtx); std::lock_guard guard(JniMtx);
if (!Halt) {
if (Halt) state.gpu->Loop();
break; }
state.gpu->Loop();
} }
} catch (const std::exception &e) { } catch (const std::exception &e) {
state.logger->Error(e.what()); state.logger->Error(e.what());

View File

@ -17,9 +17,7 @@ namespace skyline::service::hid {
{0x7A, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingleByDefault)}, {0x7A, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingleByDefault)},
{0x7B, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingle)}, {0x7B, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingle)},
{0x7C, SFUNC(IHidServer::SetNpadJoyAssignmentModeDual)} {0x7C, SFUNC(IHidServer::SetNpadJoyAssignmentModeDual)}
}) { }) {}
state.input->npad.Activate();
}
void IHidServer::CreateAppletResource(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { void IHidServer::CreateAppletResource(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
manager.RegisterService(SRVREG(IAppletResource), session, response); manager.RegisterService(SRVREG(IAppletResource), session, response);
@ -36,13 +34,15 @@ namespace skyline::service::hid {
const auto &buffer = request.inputBuf.at(0); const auto &buffer = request.inputBuf.at(0);
u64 address = buffer.address; u64 address = buffer.address;
size_t size = buffer.size / sizeof(NpadId); size_t size = buffer.size / sizeof(NpadId);
std::vector<NpadId> supportedIds;
for (size_t i = 0; i < size; i++) { for (size_t i = 0; i < size; i++) {
auto id = state.process->GetObject<NpadId>(address); supportedIds.push_back(state.process->GetObject<NpadId>(address));
state.input->npad.at(id).supported = true;
address += sizeof(NpadId); address += sizeof(NpadId);
} }
state.input->npad.supportedIds = supportedIds;
state.input->npad.Update();
} }
void IHidServer::ActivateNpad(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { 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) { void IHidServer::SetNpadJoyAssignmentModeSingleByDefault(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
auto id = request.Pop<NpadId>(); auto id = request.Pop<NpadId>();
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) { void IHidServer::SetNpadJoyAssignmentModeSingle(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
auto id = request.Pop<NpadId>(); auto id = request.Pop<NpadId>();
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) { void IHidServer::SetNpadJoyAssignmentModeDual(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
auto id = request.Pop<NpadId>(); auto id = request.Pop<NpadId>();
state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Dual); state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Dual, false);
} }
} }

View File

@ -15,9 +15,7 @@ import android.util.Log
import android.view.* import android.view.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import emu.skyline.input.AxisId import emu.skyline.input.*
import emu.skyline.input.ButtonId
import emu.skyline.input.ButtonState
import emu.skyline.loader.getRomFormat import emu.skyline.loader.getRomFormat
import kotlinx.android.synthetic.main.emu_activity.* import kotlinx.android.synthetic.main.emu_activity.*
import java.io.File import java.io.File
@ -38,14 +36,26 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
*/ */
private lateinit var preferenceFd : ParcelFileDescriptor 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 * The surface object used for displaying frames
*/ */
@Volatile
private var surface : Surface? = null private var surface : Surface? = null
/** /**
* A boolean flag denoting if the emulation thread should call finish() or not * A boolean flag denoting if the emulation thread should call finish() or not
*/ */
@Volatile
private var shouldFinish : Boolean = true private var shouldFinish : Boolean = true
/** /**
@ -89,14 +99,63 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
private external fun getFrametime() : Float 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 * 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")!! romFd = contentResolver.openFileDescriptor(rom, "r")!!
emulationThread = Thread { emulationThread = Thread {
while ((surface == null)) while (surface == null)
Thread.yield() Thread.yield()
runOnUiThread { initializeControllers() }
executeApplication(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, applicationContext.filesDir.canonicalPath + "/") executeApplication(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, applicationContext.filesDir.canonicalPath + "/")
if (shouldFinish) if (shouldFinish)
@ -142,6 +203,8 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
or View.SYSTEM_UI_FLAG_FULLSCREEN) or View.SYSTEM_UI_FLAG_FULLSCREEN)
} }
input = InputManager(this)
val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml") val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml")
preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE) preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE)
@ -150,16 +213,20 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
if (sharedPreferences.getBoolean("perf_stats", false)) { if (sharedPreferences.getBoolean("perf_stats", false)) {
lateinit var perfRunnable : Runnable val perfRunnable = object : Runnable {
override fun run() {
perfRunnable = Runnable { perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms"
perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms" perf_stats.postDelayed(this, 250)
perf_stats.postDelayed(perfRunnable, 250) }
} }
perf_stats.postDelayed(perfRunnable, 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!!) 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 { override fun dispatchKeyEvent(event : KeyEvent) : Boolean {
if (event.repeatCount != 0)
return super.dispatchKeyEvent(event)
val action : ButtonState = when (event.action) { val action : ButtonState = when (event.action) {
KeyEvent.ACTION_DOWN -> ButtonState.Pressed KeyEvent.ACTION_DOWN -> ButtonState.Pressed
KeyEvent.ACTION_UP -> ButtonState.Released KeyEvent.ACTION_UP -> ButtonState.Released
else -> return false else -> return super.dispatchKeyEvent(event)
} }
val buttonMap : Map<Int, ButtonId> = mapOf( return when (val guestEvent = input.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
KeyEvent.KEYCODE_BUTTON_A to ButtonId.A, is ButtonGuestEvent -> {
KeyEvent.KEYCODE_BUTTON_B to ButtonId.B, if (guestEvent.button != ButtonId.Menu)
KeyEvent.KEYCODE_BUTTON_X to ButtonId.X, setButtonState(guestEvent.id, guestEvent.button.value(), action.ordinal)
KeyEvent.KEYCODE_BUTTON_Y to ButtonId.Y, true
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 try { is AxisGuestEvent -> {
setButtonState(buttonMap.getValue(event.keyCode).value(), action.ordinal) setAxisValue(guestEvent.id, guestEvent.axis.ordinal, (if (action == ButtonState.Pressed) if (guestEvent.polarity) Short.MAX_VALUE else Short.MIN_VALUE else 0).toInt())
true true
} catch (ignored : NoSuchElementException) { }
super.dispatchKeyEvent(event)
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<Float>(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 { override fun onGenericMotionEvent(event : MotionEvent) : Boolean {
if ((event.source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD || if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE) {
(event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) { val hat = Pair(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y))
val hatXMap : Map<Float, ButtonId> = mapOf(
-1.0f to ButtonId.DpadLeft,
+1.0f to ButtonId.DpadRight)
val hatYMap : Map<Float, ButtonId> = mapOf( if (hat == oldHat) {
-1.0f to ButtonId.DpadUp, for (axisItem in MotionHostEvent.axes.withIndex()) {
+1.0f to ButtonId.DpadDown) val axis = axisItem.value
var value = event.getAxisValue(axis)
if (controllerHatX != event.getAxisValue(MotionEvent.AXIS_HAT_X)) { if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) {
if (event.getAxisValue(MotionEvent.AXIS_HAT_X) == 0.0f) var polarity = value >= 0
setButtonState(hatXMap.getValue(controllerHatX).value(), ButtonState.Released.ordinal)
else
setButtonState(hatXMap.getValue(event.getAxisValue(MotionEvent.AXIS_HAT_X)).value(), ButtonState.Pressed.ordinal)
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)) { is AxisGuestEvent -> {
if (event.getAxisValue(MotionEvent.AXIS_HAT_Y) == 0.0f) value = guestEvent.value(value)
setButtonState(hatYMap.getValue(controllerHatY).value(), ButtonState.Released.ordinal) value = if (polarity) abs(value) else -abs(value)
else value = if (guestEvent.axis == AxisId.LX || guestEvent.axis == AxisId.RX) value else -value // TODO: Test this
setButtonState(hatYMap.getValue(event.getAxisValue(MotionEvent.AXIS_HAT_Y)).value(), ButtonState.Pressed.ordinal)
controllerHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y) setAxisValue(guestEvent.id, guestEvent.axis.ordinal, (value * Short.MAX_VALUE).toInt())
}
}
}
return true axesHistory[axisItem.index] = value
}
}
if ((event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.action == MotionEvent.ACTION_MOVE) {
val axisMap : Map<Int, AxisId> = 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)
} }
return true
} else {
oldHat = hat
} }
return true
} }
return super.dispatchGenericMotionEvent(event) return super.onGenericMotionEvent(event)
} }
} }

View File

@ -14,12 +14,12 @@ import java.io.Serializable
* @param stringRes The string resource of the controller's name * @param stringRes The string resource of the controller's name
* @param firstController If the type only applies to the first controller * @param firstController If the type only applies to the first controller
*/ */
enum class ControllerType(val stringRes : Int, val firstController : Boolean, val sticks : Array<StickId> = arrayOf(), val buttons : Array<ButtonId> = arrayOf()) { enum class ControllerType(val stringRes : Int, val firstController : Boolean, val sticks : Array<StickId> = arrayOf(), val buttons : Array<ButtonId> = arrayOf(), val id : Int) {
None(R.string.none, false), None(R.string.none, false, id=0b0),
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), 0b1),
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)), 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)), 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)), 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),
} }
/** /**

View File

@ -63,10 +63,10 @@ enum class ButtonState(val state : Boolean) {
* This enumerates all of the axes on a controller that the emulator recognizes * This enumerates all of the axes on a controller that the emulator recognizes
*/ */
enum class AxisId { enum class AxisId {
RX,
RY,
LX, LX,
LY, LY,
RX,
RY,
} }
/** /**

View File

@ -32,6 +32,9 @@ abstract class HostEvent(val descriptor : String = "") : Serializable {
abstract override fun hashCode() : Int 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) { class KeyHostEvent(descriptor : String = "", val keyCode : Int) : HostEvent(descriptor) {
/** /**
* This returns the string representation of [keyCode] * 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) 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) { 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] * This returns the string representation of [axis] combined with [polarity]
*/ */

View File

@ -197,7 +197,7 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
context.axisMap[(guestEvent as AxisGuestEvent).axis]?.update() 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) } context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }

View File

@ -17,6 +17,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.ControllerStickItem import emu.skyline.adapter.ControllerStickItem
import emu.skyline.input.* import emu.skyline.input.*
import emu.skyline.input.MotionHostEvent.Companion.axes
import kotlinx.android.synthetic.main.stick_dialog.* import kotlinx.android.synthetic.main.stick_dialog.*
import java.util.* import java.util.*
import kotlin.math.abs 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 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 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 { stick_next.setOnClickListener {
gotoStage(1) gotoStage(1)
@ -304,11 +301,7 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
} }
is AxisGuestEvent -> { is AxisGuestEvent -> {
val coefficient = if (event.action == KeyEvent.ACTION_DOWN) { val coefficient = if (event.action == KeyEvent.ACTION_DOWN) if (guestEvent.polarity) 1 else -1 else 0
if (guestEvent.polarity) 1 else -1
} else {
0
}
if (guestEvent.axis == item.stick.xAxis) { if (guestEvent.axis == item.stick.xAxis) {
stick_container?.translationX = dipToPixels(16.5f) * coefficient 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<Float>(axes.size) // The last recorded value of an axis, this is used to eliminate any stagnant axes val axesHistory = arrayOfNulls<Float>(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) 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 -> 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 // 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 hat = Pair(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y))
val dpadY = 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 // 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) { 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 // 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()) { for (axisItem in axes.withIndex()) {
@ -592,8 +584,7 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
true true
} else { } else {
oldDpadX = dpadX oldHat = hat
oldDpadY = dpadY
false false
} }