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:
- 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/

View File

@ -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:

View File

@ -97,14 +97,31 @@ extern "C" JNIEXPORT jfloat Java_emu_skyline_EmulationActivity_getFrametime(JNIE
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) {
skyline::input::NpadButton button{.raw = static_cast<skyline::u64>(id)};
input->npad.at(skyline::input::NpadId::Player1).SetButtonState(button, static_cast<skyline::input::NpadButtonState>(state));
auto device = input->npad.controllers[index].device;
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) {
if (input)
input->npad.at(skyline::input::NpadId::Player1).SetAxisValue(static_cast<skyline::input::NpadAxisId>(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<skyline::input::NpadAxisId>(axis), value);
}
}

View File

@ -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;
}
}

View File

@ -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<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
* @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<size_t>(id);
@ -31,7 +39,9 @@ namespace skyline::input {
}
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
/**
@ -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();
};

View File

@ -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 &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) {
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++;
}
}

View File

@ -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 &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
@ -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 &section, NpadId id);
NpadDevice(NpadManager &manager, NpadSection &section, 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);

View File

@ -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;
};
/**

View File

@ -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];
};

View File

@ -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);
}

View File

@ -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());

View File

@ -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<NpadId> supportedIds;
for (size_t i = 0; i < size; i++) {
auto id = state.process->GetObject<NpadId>(address);
state.input->npad.at(id).supported = true;
supportedIds.push_back(state.process->GetObject<NpadId>(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<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) {
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) {
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 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<Int, ButtonId> = 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<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 {
if ((event.source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD ||
(event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) {
val hatXMap : Map<Float, ButtonId> = 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<Float, ButtonId> = 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<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)
axesHistory[axisItem.index] = value
}
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 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()) {
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<StickId> = arrayOf(), val buttons : Array<ButtonId> = 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),
}
/**

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
*/
enum class AxisId {
RX,
RY,
LX,
LY,
RX,
RY,
}
/**

View File

@ -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]
*/

View File

@ -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) }

View File

@ -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<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)
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
}