Implement Rumble Support for Controllers and Device Vibrators

This commit is contained in:
◱ PixelyIon 2020-09-05 04:36:07 +05:30 committed by ◱ PixelyIon
parent d8ccdd723e
commit 1a58a2e967
26 changed files with 397 additions and 51 deletions

View File

@ -98,6 +98,7 @@ add_library(skyline SHARED
${source_DIR}/skyline/services/am/applet/ILibraryAppletAccessor.cpp
${source_DIR}/skyline/services/hid/IHidServer.cpp
${source_DIR}/skyline/services/hid/IAppletResource.cpp
${source_DIR}/skyline/services/hid/IActiveVibrationDeviceList.cpp
${source_DIR}/skyline/services/timesrv/IStaticService.cpp
${source_DIR}/skyline/services/timesrv/ISystemClock.cpp
${source_DIR}/skyline/services/timesrv/ISteadyClock.cpp

View File

@ -116,7 +116,7 @@ extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setButtonSt
auto device = input->npad.controllers[index].device;
if (device)
device->SetButtonState(skyline::input::NpadButton{.raw = static_cast<skyline::u64>(mask)}, pressed);
} catch (const std::bad_weak_ptr&) {
} catch (const std::bad_weak_ptr &) {
// We don't mind if we miss button updates while input hasn't been initialized
}
}
@ -127,7 +127,7 @@ extern "C" JNIEXPORT void JNICALL Java_emu_skyline_EmulationActivity_setAxisValu
auto device = input->npad.controllers[index].device;
if (device)
device->SetAxisValue(static_cast<skyline::input::NpadAxisId>(axis), value);
} catch (const std::bad_weak_ptr&) {
} catch (const std::bad_weak_ptr &) {
// We don't mind if we miss axis updates while input hasn't been initialized
}
}

View File

@ -5,6 +5,7 @@
#include <map>
#include <unordered_map>
#include <span>
#include <vector>
#include <fstream>
#include <syslog.h>

View File

@ -4,7 +4,6 @@
#pragma once
#include <common.h>
#include <span>
#include <queue>
#include "engines/engine.h"
#include "engines/gpfifo.h"
@ -177,4 +176,4 @@ namespace skyline::gpu {
void Push(std::span<GpEntry> entries);
};
}
}
}

View File

@ -3,7 +3,6 @@
#pragma once
#include <span>
#include <common.h>
namespace skyline {

View File

@ -50,9 +50,13 @@ namespace skyline::input {
if (style.raw) {
if (style.proController || style.joyconHandheld || style.joyconLeft || style.joyconRight) {
device.Connect(controller.type);
device.index = static_cast<size_t>(&controller - controllers.data());
device.partnerIndex = -1;
controller.device = &device;
} else if (style.joyconDual && orientation == NpadJoyOrientation::Vertical && device.GetAssignment() == NpadJoyAssignment::Dual) {
device.Connect(NpadControllerType::JoyconDual);
device.index = static_cast<size_t>(&controller - controllers.data());
device.partnerIndex = controller.partnerIndex;
controller.device = &device;
controllers.at(controller.partnerIndex).device = &device;
} else {

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <jvm.h>
#include "npad_device.h"
#include "npad.h"
@ -155,6 +156,9 @@ namespace skyline::input {
section = {};
globalTimestamp = 0;
index = -1;
partnerIndex = -1;
type = NpadControllerType::None;
controllerInfo = nullptr;
@ -347,4 +351,91 @@ namespace skyline::input {
globalTimestamp++;
}
void NpadDevice::VibrateDevice(i8 vibrateIndex, const NpadVibrationValue &value) {
std::array<jlong, 3> timings;
std::array<jint, 3> amplitudes;
jlong periodLow = 1000 / value.frequencyLow;
jlong periodHigh = 1000 / value.frequencyHigh;
jint amplitudeLow = value.amplitudeLow * 127;
jint amplitudeHigh = value.amplitudeHigh * 127;
if (amplitudeLow + amplitudeHigh == 0 || periodLow + periodHigh == 0) {
manager.state.jvm->ClearVibrationDevice(vibrateIndex);
return;
}
if (periodLow == periodHigh) {
timings = {periodLow, periodHigh, 0};
amplitudes = {std::min(amplitudeLow + amplitudeHigh, 255), 0, 0};
} else if (periodLow < periodHigh) {
timings = {periodLow, periodHigh - periodLow, periodHigh};
amplitudes = {std::min(amplitudeLow + amplitudeHigh, 255), amplitudeHigh, 0};
} else if (periodHigh < periodLow) {
timings = {periodHigh, periodLow - periodHigh, periodLow};
amplitudes = {std::min(amplitudeHigh + amplitudeLow, 255), amplitudeLow, 0};
}
manager.state.jvm->VibrateDevice(vibrateIndex, timings, amplitudes);
}
void NpadDevice::Vibrate(bool isRight, const NpadVibrationValue &value) {
if (isRight)
vibrationRight = value;
else
vibrationLeft = value;
if (vibrationRight)
Vibrate(vibrationLeft, *vibrationRight);
else
VibrateDevice(index, value);
}
void NpadDevice::Vibrate(const NpadVibrationValue &left, const NpadVibrationValue &right) {
if (partnerIndex == -1) {
std::array<jlong, 5> timings;
std::array<jint, 5> amplitudes;
std::array<std::pair<jlong, jint>, 4> vibrations{std::pair<jlong, jint>{1000 / left.frequencyLow, left.amplitudeLow * 64},
{1000 / left.frequencyHigh, left.amplitudeHigh * 64},
{1000 / right.frequencyLow, right.amplitudeLow * 64},
{1000 / right.frequencyHigh, right.amplitudeHigh * 64},
};
jlong totalTime{};
std::sort(vibrations.begin(), vibrations.end(), [](const std::pair<jlong, jint> &a, const std::pair<jlong, jint> &b) {
return a.first < b.first;
});
jint totalAmplitude{};
for (const auto &vibration : vibrations)
totalAmplitude += vibration.second;
if (totalAmplitude == 0 || vibrations[3].first == 0) {
manager.state.jvm->ClearVibrationDevice(index);
return;
}
for (u8 i{0}; i < vibrations.size(); i++) {
const auto &vibration = vibrations[i];
auto time = vibration.first - totalTime;
timings[i] = time;
totalTime += time;
amplitudes[i] = std::min(totalAmplitude, 255);
totalAmplitude -= vibration.second;
}
timings[4] = totalTime;
amplitudes[4] = 0;
manager.state.jvm->VibrateDevice(index, timings, amplitudes);
} else {
VibrateDevice(index, left);
VibrateDevice(partnerIndex, right);
}
}
}

View File

@ -61,6 +61,50 @@ namespace skyline::input {
Handheld = 0x20,
};
/**
* @brief A handle to a specific device addressed by it's ID and type
* @note This is used by both Six-Axis and Vibration
*/
union __attribute__((__packed__)) NpadDeviceHandle {
u32 raw;
struct {
u8 type;
NpadId id : 8;
bool isRight : 1; //!< If this is a right Joy-Con (Both) or right LRA in the Pro-Controller (Vibration)
bool isSixAxisSingle : 1; //!< If the Six-Axis device is a single unit, either Handheld or Pro-Controller
};
constexpr NpadControllerType GetType() {
switch (type) {
case 3:
return NpadControllerType::ProController;
case 4:
return NpadControllerType::Handheld;
case 5:
return NpadControllerType::JoyconDual;
case 6:
return NpadControllerType::JoyconLeft;
case 7:
return NpadControllerType::JoyconRight;
}
return NpadControllerType::None;
}
};
/**
* @brief The parameters to produce a vibration using an LRA
* @note The vibration is broken into a frequency band with the lower and high range supplied
* @note Amplitude is in arbitrary units from 0f to 1f
* @note Frequency is in Hertz
*/
struct NpadVibrationValue {
float amplitudeLow;
float frequencyLow;
float amplitudeHigh;
float frequencyHigh;
};
static_assert(sizeof(NpadVibrationValue) == 0x10);
class NpadManager;
/**
@ -85,8 +129,14 @@ namespace skyline::input {
*/
NpadControllerInfo &GetControllerInfo();
void VibrateDevice(i8 index, const NpadVibrationValue &value);
public:
NpadId id;
i8 index{-1}; //!< The index of the device assigned to this player
i8 partnerIndex{-1}; //!< The index of a partner device, if present
NpadVibrationValue vibrationLeft; //!< Vibration for the left Joy-Con (Handheld/Pair), left LRA in a Pro-Controller or individual Joy-Cons
std::optional<NpadVibrationValue> vibrationRight; //!< Vibration for the right Joy-Con (Handheld/Pair) or right LRA in a Pro-Controller
NpadControllerType type{};
NpadConnectionState connectionState{};
std::shared_ptr<kernel::type::KEvent> updateEvent; //!< This event is triggered on the controller's style changing
@ -132,5 +182,9 @@ namespace skyline::input {
* @param value The value to set
*/
void SetAxisValue(NpadAxisId axis, i32 value);
void Vibrate(bool isRight, const NpadVibrationValue &value);
void Vibrate(const NpadVibrationValue &left, const NpadVibrationValue &right);
};
}

View File

@ -147,38 +147,38 @@ namespace skyline::input {
/**
* @brief This structure is used to hold a single sample of 3D data from the IMU
*/
struct SixaxisVector {
struct SixAxisVector {
float x; //!< The data in the X-axis
float y; //!< The data in the Y-axis
float z; //!< The data in the Z-axis
};
static_assert(sizeof(SixaxisVector) == 0xC);
static_assert(sizeof(SixAxisVector) == 0xC);
/**
* @brief This structure contains data about the state of the controller's IMU (Sixaxis) (https://switchbrew.org/wiki/HID_Shared_Memory#NpadSixAxisSensorHandheldState)
* @brief This structure contains data about the state of the controller's IMU (Six-Axis) (https://switchbrew.org/wiki/HID_Shared_Memory#NpadSixAxisSensorHandheldState)
*/
struct NpadSixaxisState {
struct NpadSixAxisState {
u64 globalTimestamp; //!< The global timestamp in samples
u64 _unk0_;
u64 localTimestamp; //!< The local timestamp in samples
SixaxisVector accelerometer;
SixaxisVector gyroscope;
SixaxisVector rotation;
std::array<SixaxisVector, 3> orientation; //!< The orientation basis data as a matrix
SixAxisVector accelerometer;
SixAxisVector gyroscope;
SixAxisVector rotation;
std::array<SixAxisVector, 3> orientation; //!< The orientation basis data as a matrix
u64 _unk2_; //!< This is always 1
};
static_assert(sizeof(NpadSixaxisState) == 0x68);
static_assert(sizeof(NpadSixAxisState) == 0x68);
/**
* @brief This structure contains header and entries for the IMU (Sixaxis) data
* @brief This structure contains header and entries for the IMU (Six-Axis) data
*/
struct NpadSixaxisInfo {
struct NpadSixAxisInfo {
CommonHeader header;
std::array<NpadSixaxisState, constant::HidEntryCount> state;
std::array<NpadSixAxisState, constant::HidEntryCount> state;
};
static_assert(sizeof(NpadSixaxisInfo) == 0x708);
static_assert(sizeof(NpadSixAxisInfo) == 0x708);
/**
* @brief This is a bit-field of all the device types (https://switchbrew.org/wiki/HID_services#DeviceType)
@ -268,12 +268,12 @@ namespace skyline::input {
NpadControllerInfo palmaController; //!< The Poké Ball Plus 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 handheldSixaxis; //!< The Handheld IMU data
NpadSixaxisInfo dualLeftSixaxis; //!< The Left Joy-Con in dual mode's IMU data
NpadSixaxisInfo dualRightSixaxis; //!< The Left Joy-Con in dual mode's IMU data
NpadSixaxisInfo leftSixaxis; //!< The Left Joy-Con IMU data
NpadSixaxisInfo rightSixaxis; //!< The Right Joy-Con IMU data
NpadSixAxisInfo fullKeySixAxis; //!< The Pro/GC IMU data
NpadSixAxisInfo handheldSixAxis; //!< The Handheld IMU data
NpadSixAxisInfo dualLeftSixAxis; //!< The Left Joy-Con in dual mode's IMU data
NpadSixAxisInfo dualRightSixAxis; //!< The Left Joy-Con in dual mode's IMU data
NpadSixAxisInfo leftSixAxis; //!< The Left Joy-Con IMU data
NpadSixAxisInfo rightSixAxis; //!< The Right Joy-Con IMU data
NpadDeviceType deviceType;

View File

@ -6,12 +6,17 @@
thread_local JNIEnv *env;
namespace skyline {
JvmManager::JvmManager(JNIEnv *environ, jobject instance) : instance(instance), instanceClass(reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))), initializeControllersId(environ->GetMethodID(instanceClass, "initializeControllers", "()V")) {
JvmManager::JvmManager(JNIEnv *environ, jobject instance) : instance(environ->NewGlobalRef(instance)), instanceClass(reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))), initializeControllersId(environ->GetMethodID(instanceClass, "initializeControllers", "()V")), vibrateDeviceId(environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")), clearVibrationDeviceId(environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")) {
env = environ;
if (env->GetJavaVM(&vm) < 0)
throw exception("Cannot get JavaVM from environment");
}
JvmManager::~JvmManager() {
env->DeleteGlobalRef(instanceClass);
env->DeleteGlobalRef(instance);
}
void JvmManager::AttachThread() {
if (!env)
vm->AttachCurrentThread(&env, nullptr);
@ -41,4 +46,20 @@ namespace skyline {
void JvmManager::InitializeControllers() {
env->CallVoidMethod(instance, initializeControllersId);
}
void JvmManager::VibrateDevice(jint index, const std::span<jlong> &timings, const std::span<jint> &amplitudes) {
auto jTimings = env->NewLongArray(timings.size());
env->SetLongArrayRegion(jTimings, 0, timings.size(), timings.data());
auto jAmplitudes = env->NewIntArray(amplitudes.size());
env->SetIntArrayRegion(jAmplitudes, 0, amplitudes.size(), amplitudes.data());
env->CallVoidMethod(instance, vibrateDeviceId, index, jTimings, jAmplitudes);
env->DeleteLocalRef(jTimings);
env->DeleteLocalRef(jAmplitudes);
}
void JvmManager::ClearVibrationDevice(jint index) {
env->CallVoidMethod(instance, clearVibrationDeviceId, index);
}
}

View File

@ -22,6 +22,8 @@ namespace skyline {
*/
JvmManager(JNIEnv *env, jobject instance);
~JvmManager();
/**
* @brief Attach the current thread to the Java VM
*/
@ -92,7 +94,19 @@ namespace skyline {
*/
void InitializeControllers();
/**
* @brief A call to EmulationActivity.vibrateDevice in Kotlin
*/
void VibrateDevice(jint index, const std::span<jlong> &timings, const std::span<jint> &amplitudes);
/**
* @brief A call to EmulationActivity.clearVibrationDevice in Kotlin
*/
void ClearVibrationDevice(jint index);
private:
jmethodID initializeControllersId;
jmethodID vibrateDeviceId;
jmethodID clearVibrationDeviceId;
};
}

View File

@ -16,6 +16,7 @@ extern skyline::GroupMutex JniMtx;
namespace skyline {
void NCE::KernelThread(pid_t thread) {
state.jvm->AttachThread();
try {
state.thread = state.process->threads.at(thread);
state.ctx = reinterpret_cast<ThreadContext *>(state.thread->ctxMemory->kernel.address);
@ -76,6 +77,8 @@ namespace skyline {
state.os->KillThread(thread);
}
}
state.jvm->DetachThread();
}
NCE::NCE(DeviceState &state) : state(state) {}

View File

@ -1,7 +1,6 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <span>
#include <kernel/types/KProcess.h>
#include "IManagerForApplication.h"
#include "IProfile.h"

View File

@ -52,6 +52,7 @@ namespace skyline::service {
audio_IAudioDevice,
hid_IHidServer,
hid_IAppletResource,
hid_IActiveVibrationDeviceList,
timesrv_IStaticService,
timesrv_ISystemClock,
timesrv_ITimeZoneService,

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <input.h>
#include "IActiveVibrationDeviceList.h"
using namespace skyline::input;
namespace skyline::service::hid {
IActiveVibrationDeviceList::IActiveVibrationDeviceList(const DeviceState &state, ServiceManager &manager) : BaseService(state, manager, Service::hid_IActiveVibrationDeviceList, "hid:IActiveVibrationDeviceList", {
{0x0, SFUNC(IActiveVibrationDeviceList::ActivateVibrationDevice)}
}) {}
void IActiveVibrationDeviceList::ActivateVibrationDevice(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
auto handle = request.Pop<NpadDeviceHandle>();
if (!handle.isRight)
state.input->npad.at(handle.id).vibrationRight = NpadVibrationValue{};
}
}

View File

@ -0,0 +1,23 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#pragma once
#include <kernel/types/KProcess.h>
#include <services/base_service.h>
#include <services/serviceman.h>
namespace skyline::service::hid {
/**
* @brief IActiveVibrationDeviceList is used to activate vibration on certain HID devices (https://switchbrew.org/wiki/HID_services#IActiveVibrationDeviceList)
*/
class IActiveVibrationDeviceList : public BaseService {
public:
IActiveVibrationDeviceList(const DeviceState &state, ServiceManager &manager);
/**
* @brief Activates a vibration device with the specified #VibrationDeviceHandle (https://switchbrew.org/wiki/HID_services#ActivateVibrationDevice)
*/
void ActivateVibrationDevice(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
};
}

View File

@ -3,6 +3,7 @@
#include <input.h>
#include "IHidServer.h"
#include "IActiveVibrationDeviceList.h"
using namespace skyline::input;
@ -20,7 +21,9 @@ namespace skyline::service::hid {
{0x79, SFUNC(IHidServer::GetNpadJoyHoldType)},
{0x7A, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingleByDefault)},
{0x7B, SFUNC(IHidServer::SetNpadJoyAssignmentModeSingle)},
{0x7C, SFUNC(IHidServer::SetNpadJoyAssignmentModeDual)}
{0x7C, SFUNC(IHidServer::SetNpadJoyAssignmentModeDual)},
{0xCB, SFUNC(IHidServer::CreateActiveVibrationDeviceList)},
{0xCE, SFUNC(IHidServer::SendVibrationValues)}
}) {}
void IHidServer::CreateAppletResource(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
@ -105,4 +108,34 @@ namespace skyline::service::hid {
state.input->npad.at(id).SetAssignment(NpadJoyAssignment::Dual);
state.input->npad.Update();
}
void IHidServer::CreateActiveVibrationDeviceList(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
manager.RegisterService(SRVREG(IActiveVibrationDeviceList), session, response);
}
void IHidServer::SendVibrationValues(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
request.Skip<u64>(); // appletResourceUserId
auto &handleBuf = request.inputBuf.at(0);
std::span handles(reinterpret_cast<NpadDeviceHandle *>(handleBuf.address), handleBuf.size / sizeof(NpadDeviceHandle));
auto &valueBuf = request.inputBuf.at(1);
std::span values(reinterpret_cast<NpadVibrationValue *>(valueBuf.address), valueBuf.size / sizeof(NpadVibrationValue));
for (int i = 0; i < handles.size(); ++i) {
auto &handle = handles[i];
auto &device = state.input->npad.at(handle.id);
if (device.type == handle.GetType()) {
if (i + 1 != handles.size() && handles[i + 1].id == handle.id && handles[i + 1].isRight && !handle.isRight) {
state.logger->Info("Vibration #{}&{} - Handle: 0x{:02X} (0b{:05b}), Vibration: {:.2f}@{:.2f}Hz, {:.2f}@{:.2f}Hz - {:.2f}@{:.2f}Hz, {:.2f}@{:.2f}Hz", i, i + 1, u8(handle.id), u8(handle.type), values[i].amplitudeLow, values[i].frequencyLow, values[i].amplitudeHigh, values[i].frequencyHigh, values[i + 1].amplitudeLow, values[i + 1].frequencyLow, values[i + 1].amplitudeHigh, values[i + 1].frequencyHigh);
device.Vibrate(values[i], values[i + 1]);
i++;
} else {
auto &value = values[i];
state.logger->Info("Vibration #{} - Handle: 0x{:02X} (0b{:05b}), Vibration: {:.2f}@{:.2f}Hz, {:.2f}@{:.2f}Hz", i, u8(handle.id), u8(handle.type), value.amplitudeLow, value.frequencyLow, value.amplitudeHigh, value.frequencyHigh);
device.Vibrate(handle.isRight, value);
}
}
}
}
}

View File

@ -79,5 +79,15 @@ namespace skyline::service::hid {
* @brief Sets the Joy-Con assignment mode to Dual (https://switchbrew.org/wiki/HID_services#SetNpadJoyAssignmentModeDual)
*/
void SetNpadJoyAssignmentModeDual(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Returns an instance of #IActiveVibrationDeviceList (https://switchbrew.org/wiki/HID_services#CreateActiveVibrationDeviceList)
*/
void CreateActiveVibrationDeviceList(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Send vibration values to an NPad (https://switchbrew.org/wiki/HID_services#SendVibrationValues)
*/
void SendVibrationValues(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
};
}

View File

@ -1,7 +1,6 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <span>
#include <os.h>
#include <gpu/gpfifo.h>
#include <kernel/types/KProcess.h>

View File

@ -6,12 +6,10 @@
package emu.skyline
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.ConditionVariable
import android.os.ParcelFileDescriptor
import android.os.*
import android.util.Log
import android.view.*
import androidx.appcompat.app.AppCompatActivity
@ -42,10 +40,15 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
*/
private lateinit var input : InputManager
/**
* A map of [Vibrator]s that correspond to [InputManager.controllers]
*/
private var vibrators = HashMap<Int, Vibrator>()
/**
* A boolean flag denoting the current operation mode of the emulator (Docked = true/Handheld = false)
*/
private var operationMode : Boolean = true
private var operationMode = true
/**
* The surface object used for displaying frames
@ -260,7 +263,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
shouldFinish = false
setHalt(true)
emulationThread.join()
emulationThread.join(1000)
vibrators.forEach { (_, vibrator) -> vibrator.cancel() }
vibrators.clear()
romFd.close()
preferenceFd.close()
@ -383,4 +389,35 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
return super.onGenericMotionEvent(event)
}
@SuppressLint("WrongConstant")
fun vibrateDevice(index : Int, timing : LongArray, amplitude : IntArray) {
val vibrator = if (vibrators[index] != null) {
vibrators[index]!!
} else {
input.controllers[index]?.rumbleDeviceDescriptor?.let {
if (it == "builtin") {
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
vibrators[index] = vibrator
vibrator
} else {
for (id in InputDevice.getDeviceIds()) {
val device = InputDevice.getDevice(id)
if (device.descriptor == input.controllers[index]?.rumbleDeviceDescriptor) {
vibrators[index] = device.vibrator
device.vibrator
}
}
}
}
return
}
val effect = VibrationEffect.createWaveform(timing, amplitude, 0)
vibrator.vibrate(effect)
}
fun clearVibrationDevice(index : Int) {
vibrators[index]?.cancel()
}
}

View File

@ -83,7 +83,8 @@ class ControllerGeneralItem(val context : ControllerActivity, val type : General
else
context.getString(R.string.none)
}
GeneralType.RumbleDevice -> controller.rumbleDevice?.second ?: context.getString(R.string.none)
GeneralType.RumbleDevice -> controller.rumbleDeviceName ?: context.getString(R.string.none)
}
}
}

View File

@ -15,7 +15,7 @@ import java.io.Serializable
* @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(), val id : Int) {
None(R.string.none, false, id=0b0),
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),
@ -38,9 +38,10 @@ enum class GeneralType(val stringRes : Int, val compatibleControllers : Array<Co
*
* @param id The ID of the controller
* @param type The type of the controller
* @param rumbleDevice The device descriptor and the name of the device rumble/force-feedback will be passed onto
* @param rumbleDeviceDescriptor The device descriptor of the device rumble/force-feedback will be passed onto
* @param rumbleDeviceName The name of the device rumble/force-feedback will be passed onto
*/
open class Controller(val id : Int, var type : ControllerType, var rumbleDevice : Pair<String, String>? = null) : Serializable {
open class Controller(val id : Int, var type : ControllerType, var rumbleDeviceDescriptor : String? = null, var rumbleDeviceName : String? = null) : Serializable {
/**
* The current version of this class so that different versions won't be deserialized mistakenly
*/

View File

@ -6,8 +6,10 @@
package emu.skyline.input.dialog
import android.animation.LayoutTransition
import android.content.Context
import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.*
import android.view.animation.LinearInterpolator
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -50,12 +52,26 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed
rumble_reset.setOnClickListener {
controller.rumbleDevice = null
controller.rumbleDeviceDescriptor = null
controller.rumbleDeviceName = null
item.update()
dismiss()
}
if (context.id == 0) {
rumble_builtin.visibility = View.VISIBLE
if (!(context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).hasVibrator())
rumble_builtin.isEnabled = false
rumble_builtin.setOnClickListener {
controller.rumbleDeviceDescriptor = "builtin"
controller.rumbleDeviceName = getString(R.string.builtin_vibrator)
item.update()
dismiss()
}
}
// Ensure that layout animations are proper
rumble_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
rumble_controller.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
@ -80,7 +96,8 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
rumble_controller_supported.text = getString(R.string.not_supported)
rumble_title.text = getString(R.string.press_any_button)
dialog?.setOnKeyListener { _, _, _ -> false }
rumble_reset.requestFocus()
}
rumble_controller_icon.animate().apply {
@ -97,16 +114,16 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
vibrator.hasVibrator() -> {
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
controller.rumbleDevice = Pair(event.device.descriptor, event.device.name)
controller.rumbleDeviceDescriptor = event.device.descriptor
controller.rumbleDeviceName = event.device.name
item.update()
dismiss()
}
// If the currently selected device doesn't have a vibrator then dismiss the dialog entirely
else -> {
dismiss()
return@setOnKeyListener false
}
}
}

View File

@ -63,13 +63,29 @@
</RelativeLayout>
<Button
android:id="@+id/rumble_reset"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="10dp"
android:text="@string/reset" />
android:gravity="end"
android:orientation="horizontal">
<Button
android:id="@+id/rumble_builtin"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/builtin"
android:visibility="gone" />
<Button
android:id="@+id/rumble_reset"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/reset" />
</LinearLayout>
</LinearLayout>

View File

@ -62,6 +62,8 @@
<string name="not_supported">Not Supported</string>
<string name="press_any_button">Press any button on a controller</string>
<string name="confirm_button_again">Confirm choice by pressing a button again</string>
<string name="builtin">Built-in</string>
<string name="builtin_vibrator">Built-in Vibrator</string>
<string name="reset">Reset</string>
<string name="buttons">Buttons</string>
<string name="use_button_axis">Use any button or axis on a controller</string>

View File

@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong