committed by
Narr the Reg
8 changed files with 848 additions and 827 deletions
-
6src/input_common/CMakeLists.txt
-
419src/input_common/drivers/gc_adapter.cpp
-
128src/input_common/drivers/gc_adapter.h
-
320src/input_common/drivers/tas_input.cpp
-
200src/input_common/drivers/tas_input.h
-
168src/input_common/gcadapter/gc_adapter.h
-
356src/input_common/gcadapter/gc_poller.cpp
-
78src/input_common/gcadapter/gc_poller.h
@ -0,0 +1,128 @@ |
|||
// Copyright 2014 Dolphin Emulator Project |
|||
// Licensed under GPLv2+ |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <mutex> |
|||
#include <stop_token> |
|||
#include <thread> |
|||
|
|||
#include "input_common/input_engine.h" |
|||
|
|||
struct libusb_context; |
|||
struct libusb_device; |
|||
struct libusb_device_handle; |
|||
|
|||
namespace InputCommon { |
|||
|
|||
class LibUSBContext; |
|||
class LibUSBDeviceHandle; |
|||
|
|||
class GCAdapter : public InputCommon::InputEngine { |
|||
public: |
|||
explicit GCAdapter(const std::string input_engine_); |
|||
~GCAdapter(); |
|||
|
|||
bool SetRumble(const PadIdentifier& identifier, |
|||
const Input::VibrationStatus vibration) override; |
|||
|
|||
/// Used for automapping features |
|||
std::vector<Common::ParamPackage> GetInputDevices() const override; |
|||
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override; |
|||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override; |
|||
std::string GetUIName(const Common::ParamPackage& params) const override; |
|||
|
|||
private: |
|||
enum class PadButton { |
|||
Undefined = 0x0000, |
|||
ButtonLeft = 0x0001, |
|||
ButtonRight = 0x0002, |
|||
ButtonDown = 0x0004, |
|||
ButtonUp = 0x0008, |
|||
TriggerZ = 0x0010, |
|||
TriggerR = 0x0020, |
|||
TriggerL = 0x0040, |
|||
ButtonA = 0x0100, |
|||
ButtonB = 0x0200, |
|||
ButtonX = 0x0400, |
|||
ButtonY = 0x0800, |
|||
ButtonStart = 0x1000, |
|||
}; |
|||
|
|||
enum class PadAxes : u8 { |
|||
StickX, |
|||
StickY, |
|||
SubstickX, |
|||
SubstickY, |
|||
TriggerLeft, |
|||
TriggerRight, |
|||
Undefined, |
|||
}; |
|||
|
|||
enum class ControllerTypes { |
|||
None, |
|||
Wired, |
|||
Wireless, |
|||
}; |
|||
|
|||
struct GCController { |
|||
ControllerTypes type = ControllerTypes::None; |
|||
PadIdentifier identifier{}; |
|||
bool enable_vibration = false; |
|||
u8 rumble_amplitude{}; |
|||
std::array<u8, 6> axis_origin{}; |
|||
u8 reset_origin_counter{}; |
|||
}; |
|||
|
|||
using AdapterPayload = std::array<u8, 37>; |
|||
|
|||
void UpdatePadType(std::size_t port, ControllerTypes pad_type); |
|||
void UpdateControllers(const AdapterPayload& adapter_payload); |
|||
void UpdateStateButtons(std::size_t port, u8 b1, u8 b2); |
|||
void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload); |
|||
|
|||
void AdapterInputThread(std::stop_token stop_token); |
|||
|
|||
void AdapterScanThread(std::stop_token stop_token); |
|||
|
|||
bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size); |
|||
|
|||
/// For use in initialization, querying devices to find the adapter |
|||
bool Setup(); |
|||
|
|||
/// Returns true if we successfully gain access to GC Adapter |
|||
bool CheckDeviceAccess(); |
|||
|
|||
/// Captures GC Adapter endpoint address |
|||
/// Returns true if the endpoint was set correctly |
|||
bool GetGCEndpoint(libusb_device* device); |
|||
|
|||
/// Returns true if there is a device connected to port |
|||
bool DeviceConnected(std::size_t port) const; |
|||
|
|||
/// For shutting down, clear all data, join all threads, release usb |
|||
void Reset(); |
|||
|
|||
void UpdateVibrations(); |
|||
// Updates vibration state of all controllers |
|||
void SendVibrations(); |
|||
std::unique_ptr<LibUSBDeviceHandle> usb_adapter_handle; |
|||
std::array<GCController, 4> pads; |
|||
|
|||
std::jthread adapter_input_thread; |
|||
std::jthread adapter_scan_thread; |
|||
bool restart_scan_thread{}; |
|||
|
|||
std::unique_ptr<LibUSBContext> libusb_ctx; |
|||
|
|||
u8 input_endpoint{0}; |
|||
u8 output_endpoint{0}; |
|||
u8 input_error_counter{0}; |
|||
u8 output_error_counter{0}; |
|||
int vibration_counter{0}; |
|||
|
|||
bool rumble_enabled{true}; |
|||
bool vibration_changed{true}; |
|||
}; |
|||
} // namespace InputCommon |
|||
@ -0,0 +1,320 @@ |
|||
// Copyright 2021 yuzu Emulator Project
|
|||
// Licensed under GPLv2+
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <cstring>
|
|||
#include <regex>
|
|||
#include <fmt/format.h>
|
|||
|
|||
#include "common/fs/file.h"
|
|||
#include "common/fs/fs_types.h"
|
|||
#include "common/fs/path_util.h"
|
|||
#include "common/logging/log.h"
|
|||
#include "common/settings.h"
|
|||
#include "input_common/drivers/tas_input.h"
|
|||
|
|||
namespace InputCommon::TasInput { |
|||
|
|||
enum TasAxes : u8 { |
|||
StickX, |
|||
StickY, |
|||
SubstickX, |
|||
SubstickY, |
|||
Undefined, |
|||
}; |
|||
|
|||
// Supported keywords and buttons from a TAS file
|
|||
constexpr std::array<std::pair<std::string_view, TasButton>, 20> text_to_tas_button = { |
|||
std::pair{"KEY_A", TasButton::BUTTON_A}, |
|||
{"KEY_B", TasButton::BUTTON_B}, |
|||
{"KEY_X", TasButton::BUTTON_X}, |
|||
{"KEY_Y", TasButton::BUTTON_Y}, |
|||
{"KEY_LSTICK", TasButton::STICK_L}, |
|||
{"KEY_RSTICK", TasButton::STICK_R}, |
|||
{"KEY_L", TasButton::TRIGGER_L}, |
|||
{"KEY_R", TasButton::TRIGGER_R}, |
|||
{"KEY_PLUS", TasButton::BUTTON_PLUS}, |
|||
{"KEY_MINUS", TasButton::BUTTON_MINUS}, |
|||
{"KEY_DLEFT", TasButton::BUTTON_LEFT}, |
|||
{"KEY_DUP", TasButton::BUTTON_UP}, |
|||
{"KEY_DRIGHT", TasButton::BUTTON_RIGHT}, |
|||
{"KEY_DDOWN", TasButton::BUTTON_DOWN}, |
|||
{"KEY_SL", TasButton::BUTTON_SL}, |
|||
{"KEY_SR", TasButton::BUTTON_SR}, |
|||
{"KEY_CAPTURE", TasButton::BUTTON_CAPTURE}, |
|||
{"KEY_HOME", TasButton::BUTTON_HOME}, |
|||
{"KEY_ZL", TasButton::TRIGGER_ZL}, |
|||
{"KEY_ZR", TasButton::TRIGGER_ZR}, |
|||
}; |
|||
|
|||
Tas::Tas(const std::string input_engine_) : InputCommon::InputEngine(input_engine_) { |
|||
for (size_t player_index = 0; player_index < PLAYER_NUMBER; player_index++) { |
|||
PadIdentifier identifier{ |
|||
.guid = Common::UUID{}, |
|||
.port = player_index, |
|||
.pad = 0, |
|||
}; |
|||
PreSetController(identifier); |
|||
} |
|||
ClearInput(); |
|||
if (!Settings::values.tas_enable) { |
|||
needs_reset = true; |
|||
return; |
|||
} |
|||
LoadTasFiles(); |
|||
} |
|||
|
|||
Tas::~Tas() { |
|||
Stop(); |
|||
}; |
|||
|
|||
void Tas::LoadTasFiles() { |
|||
script_length = 0; |
|||
for (size_t i = 0; i < commands.size(); i++) { |
|||
LoadTasFile(i); |
|||
if (commands[i].size() > script_length) { |
|||
script_length = commands[i].size(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void Tas::LoadTasFile(size_t player_index) { |
|||
if (!commands[player_index].empty()) { |
|||
commands[player_index].clear(); |
|||
} |
|||
std::string file = |
|||
Common::FS::ReadStringFromFile(Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / |
|||
fmt::format("script0-{}.txt", player_index + 1), |
|||
Common::FS::FileType::BinaryFile); |
|||
std::stringstream command_line(file); |
|||
std::string line; |
|||
int frame_no = 0; |
|||
while (std::getline(command_line, line, '\n')) { |
|||
if (line.empty()) { |
|||
continue; |
|||
} |
|||
std::smatch m; |
|||
|
|||
std::stringstream linestream(line); |
|||
std::string segment; |
|||
std::vector<std::string> seglist; |
|||
|
|||
while (std::getline(linestream, segment, ' ')) { |
|||
seglist.push_back(segment); |
|||
} |
|||
|
|||
if (seglist.size() < 4) { |
|||
continue; |
|||
} |
|||
|
|||
while (frame_no < std::stoi(seglist.at(0))) { |
|||
commands[player_index].push_back({}); |
|||
frame_no++; |
|||
} |
|||
|
|||
TASCommand command = { |
|||
.buttons = ReadCommandButtons(seglist.at(1)), |
|||
.l_axis = ReadCommandAxis(seglist.at(2)), |
|||
.r_axis = ReadCommandAxis(seglist.at(3)), |
|||
}; |
|||
commands[player_index].push_back(command); |
|||
frame_no++; |
|||
} |
|||
LOG_INFO(Input, "TAS file loaded! {} frames", frame_no); |
|||
} |
|||
|
|||
void Tas::WriteTasFile(std::u8string file_name) { |
|||
std::string output_text; |
|||
for (size_t frame = 0; frame < record_commands.size(); frame++) { |
|||
const TASCommand& line = record_commands[frame]; |
|||
output_text += fmt::format("{} {} {} {} {}\n", frame, WriteCommandButtons(line.buttons), |
|||
WriteCommandAxis(line.l_axis), WriteCommandAxis(line.r_axis)); |
|||
} |
|||
const auto bytes_written = Common::FS::WriteStringToFile( |
|||
Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / file_name, |
|||
Common::FS::FileType::TextFile, output_text); |
|||
if (bytes_written == output_text.size()) { |
|||
LOG_INFO(Input, "TAS file written to file!"); |
|||
} else { |
|||
LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written, |
|||
output_text.size()); |
|||
} |
|||
} |
|||
|
|||
void Tas::RecordInput(u32 buttons, TasAnalog left_axis, TasAnalog right_axis) { |
|||
last_input = { |
|||
.buttons = buttons, |
|||
.l_axis = FlipAxisY(left_axis), |
|||
.r_axis = FlipAxisY(right_axis), |
|||
}; |
|||
} |
|||
|
|||
TasAnalog Tas::FlipAxisY(TasAnalog old) { |
|||
return { |
|||
.x = old.x, |
|||
.y = -old.y, |
|||
}; |
|||
} |
|||
|
|||
std::tuple<TasState, size_t, size_t> Tas::GetStatus() const { |
|||
TasState state; |
|||
if (is_recording) { |
|||
return {TasState::Recording, 0, record_commands.size()}; |
|||
} |
|||
|
|||
if (is_running) { |
|||
state = TasState::Running; |
|||
} else { |
|||
state = TasState::Stopped; |
|||
} |
|||
|
|||
return {state, current_command, script_length}; |
|||
} |
|||
|
|||
void Tas::UpdateThread() { |
|||
if (!Settings::values.tas_enable) { |
|||
if (is_running) { |
|||
Stop(); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
if (is_recording) { |
|||
record_commands.push_back(last_input); |
|||
} |
|||
if (needs_reset) { |
|||
current_command = 0; |
|||
needs_reset = false; |
|||
LoadTasFiles(); |
|||
LOG_DEBUG(Input, "tas_reset done"); |
|||
} |
|||
|
|||
if (!is_running) { |
|||
ClearInput(); |
|||
return; |
|||
} |
|||
if (current_command < script_length) { |
|||
LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length); |
|||
size_t frame = current_command++; |
|||
for (size_t player_index = 0; player_index < commands.size(); player_index++) { |
|||
TASCommand command{}; |
|||
if (frame < commands[player_index].size()) { |
|||
command = commands[player_index][frame]; |
|||
} |
|||
|
|||
PadIdentifier identifier{ |
|||
.guid = Common::UUID{}, |
|||
.port = player_index, |
|||
.pad = 0, |
|||
}; |
|||
for (std::size_t i = 0; i < sizeof(command.buttons); ++i) { |
|||
const bool button_status = (command.buttons & (1U << i)) != 0; |
|||
const int button = static_cast<int>(i); |
|||
SetButton(identifier, button, button_status); |
|||
} |
|||
SetAxis(identifier, TasAxes::StickX, command.l_axis.x); |
|||
SetAxis(identifier, TasAxes::StickY, command.l_axis.y); |
|||
SetAxis(identifier, TasAxes::SubstickX, command.r_axis.x); |
|||
SetAxis(identifier, TasAxes::SubstickY, command.r_axis.y); |
|||
} |
|||
} else { |
|||
is_running = Settings::values.tas_loop.GetValue(); |
|||
current_command = 0; |
|||
ClearInput(); |
|||
} |
|||
} |
|||
|
|||
void Tas::ClearInput() { |
|||
ResetButtonState(); |
|||
ResetAnalogState(); |
|||
} |
|||
|
|||
TasAnalog Tas::ReadCommandAxis(const std::string& line) const { |
|||
std::stringstream linestream(line); |
|||
std::string segment; |
|||
std::vector<std::string> seglist; |
|||
|
|||
while (std::getline(linestream, segment, ';')) { |
|||
seglist.push_back(segment); |
|||
} |
|||
|
|||
const float x = std::stof(seglist.at(0)) / 32767.0f; |
|||
const float y = std::stof(seglist.at(1)) / 32767.0f; |
|||
|
|||
return {x, y}; |
|||
} |
|||
|
|||
u32 Tas::ReadCommandButtons(const std::string& data) const { |
|||
std::stringstream button_text(data); |
|||
std::string line; |
|||
u32 buttons = 0; |
|||
while (std::getline(button_text, line, ';')) { |
|||
for (auto [text, tas_button] : text_to_tas_button) { |
|||
if (text == line) { |
|||
buttons |= static_cast<u32>(tas_button); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
return buttons; |
|||
} |
|||
|
|||
std::string Tas::WriteCommandButtons(u32 buttons) const { |
|||
std::string returns = ""; |
|||
for (auto [text_button, tas_button] : text_to_tas_button) { |
|||
if ((buttons & static_cast<u32>(tas_button)) != 0) |
|||
returns += fmt::format("{};", text_button.substr(4)); |
|||
} |
|||
return returns.empty() ? "NONE" : returns.substr(2); |
|||
} |
|||
|
|||
std::string Tas::WriteCommandAxis(TasAnalog analog) const { |
|||
return fmt::format("{};{}", analog.x * 32767, analog.y * 32767); |
|||
} |
|||
|
|||
void Tas::StartStop() { |
|||
if (!Settings::values.tas_enable) { |
|||
return; |
|||
} |
|||
if (is_running) { |
|||
Stop(); |
|||
} else { |
|||
is_running = true; |
|||
} |
|||
} |
|||
|
|||
void Tas::Stop() { |
|||
is_running = false; |
|||
} |
|||
|
|||
void Tas::Reset() { |
|||
if (!Settings::values.tas_enable) { |
|||
return; |
|||
} |
|||
needs_reset = true; |
|||
} |
|||
|
|||
bool Tas::Record() { |
|||
if (!Settings::values.tas_enable) { |
|||
return true; |
|||
} |
|||
is_recording = !is_recording; |
|||
return is_recording; |
|||
} |
|||
|
|||
void Tas::SaveRecording(bool overwrite_file) { |
|||
if (is_recording) { |
|||
return; |
|||
} |
|||
if (record_commands.empty()) { |
|||
return; |
|||
} |
|||
WriteTasFile(u8"record.txt"); |
|||
if (overwrite_file) { |
|||
WriteTasFile(u8"script0-1.txt"); |
|||
} |
|||
needs_reset = true; |
|||
record_commands.clear(); |
|||
} |
|||
|
|||
} // namespace InputCommon::TasInput
|
|||
@ -0,0 +1,200 @@ |
|||
// Copyright 2020 yuzu Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <array> |
|||
|
|||
#include "common/common_types.h" |
|||
#include "common/settings_input.h" |
|||
#include "input_common/input_engine.h" |
|||
#include "input_common/main.h" |
|||
|
|||
/* |
|||
To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below |
|||
Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt |
|||
for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players). |
|||
|
|||
A script file has the same format as TAS-nx uses, so final files will look like this: |
|||
|
|||
1 KEY_B 0;0 0;0 |
|||
6 KEY_ZL 0;0 0;0 |
|||
41 KEY_ZL;KEY_Y 0;0 0;0 |
|||
43 KEY_X;KEY_A 32767;0 0;0 |
|||
44 KEY_A 32767;0 0;0 |
|||
45 KEY_A 32767;0 0;0 |
|||
46 KEY_A 32767;0 0;0 |
|||
47 KEY_A 32767;0 0;0 |
|||
|
|||
After placing the file at the correct location, it can be read into Yuzu with the (default) hotkey |
|||
CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file |
|||
has. Playback can be started or stopped using CTRL+F5. |
|||
|
|||
However, for playback to actually work, the correct input device has to be selected: In the Controls |
|||
menu, select TAS from the device list for the controller that the script should be played on. |
|||
|
|||
Recording a new script file is really simple: Just make sure that the proper device (not TAS) is |
|||
connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke |
|||
again (CTRL+F7). The new script will be saved at the location previously selected, as the filename |
|||
record.txt. |
|||
|
|||
For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller |
|||
P1). |
|||
*/ |
|||
|
|||
namespace InputCommon::TasInput { |
|||
|
|||
constexpr size_t PLAYER_NUMBER = 10; |
|||
|
|||
enum class TasButton : u32 { |
|||
BUTTON_A = 1U << 0, |
|||
BUTTON_B = 1U << 1, |
|||
BUTTON_X = 1U << 2, |
|||
BUTTON_Y = 1U << 3, |
|||
STICK_L = 1U << 4, |
|||
STICK_R = 1U << 5, |
|||
TRIGGER_L = 1U << 6, |
|||
TRIGGER_R = 1U << 7, |
|||
TRIGGER_ZL = 1U << 8, |
|||
TRIGGER_ZR = 1U << 9, |
|||
BUTTON_PLUS = 1U << 10, |
|||
BUTTON_MINUS = 1U << 11, |
|||
BUTTON_LEFT = 1U << 12, |
|||
BUTTON_UP = 1U << 13, |
|||
BUTTON_RIGHT = 1U << 14, |
|||
BUTTON_DOWN = 1U << 15, |
|||
BUTTON_SL = 1U << 16, |
|||
BUTTON_SR = 1U << 17, |
|||
BUTTON_HOME = 1U << 18, |
|||
BUTTON_CAPTURE = 1U << 19, |
|||
}; |
|||
|
|||
struct TasAnalog { |
|||
float x{}; |
|||
float y{}; |
|||
}; |
|||
|
|||
enum class TasState { |
|||
Running, |
|||
Recording, |
|||
Stopped, |
|||
}; |
|||
|
|||
class Tas final : public InputCommon::InputEngine { |
|||
public: |
|||
explicit Tas(const std::string input_engine_); |
|||
~Tas(); |
|||
|
|||
/** |
|||
* Changes the input status that will be stored in each frame |
|||
* @param buttons: bitfield with the status of the buttons |
|||
* @param left_axis: value of the left axis |
|||
* @param right_axis: value of the right axis |
|||
*/ |
|||
void RecordInput(u32 buttons, TasAnalog left_axis, TasAnalog right_axis); |
|||
|
|||
// Main loop that records or executes input |
|||
void UpdateThread(); |
|||
|
|||
// Sets the flag to start or stop the TAS command excecution and swaps controllers profiles |
|||
void StartStop(); |
|||
|
|||
// Stop the TAS and reverts any controller profile |
|||
void Stop(); |
|||
|
|||
// Sets the flag to reload the file and start from the begining in the next update |
|||
void Reset(); |
|||
|
|||
/** |
|||
* Sets the flag to enable or disable recording of inputs |
|||
* @return Returns true if the current recording status is enabled |
|||
*/ |
|||
bool Record(); |
|||
|
|||
/** |
|||
* Saves contents of record_commands on a file |
|||
* @param overwrite_file: Indicates if player 1 should be overwritten |
|||
*/ |
|||
void SaveRecording(bool overwrite_file); |
|||
|
|||
/** |
|||
* Returns the current status values of TAS playback/recording |
|||
* @return Tuple of |
|||
* TasState indicating the current state out of Running ; |
|||
* Current playback progress ; |
|||
* Total length of script file currently loaded or being recorded |
|||
*/ |
|||
std::tuple<TasState, size_t, size_t> GetStatus() const; |
|||
|
|||
private: |
|||
struct TASCommand { |
|||
u32 buttons{}; |
|||
TasAnalog l_axis{}; |
|||
TasAnalog r_axis{}; |
|||
}; |
|||
|
|||
/// Loads TAS files from all players |
|||
void LoadTasFiles(); |
|||
|
|||
/** Loads TAS file from the specified player |
|||
* @param player_index: player number where data is going to be stored |
|||
*/ |
|||
void LoadTasFile(size_t player_index); |
|||
|
|||
/** Writes a TAS file from the recorded commands |
|||
* @param file_name: name of the file to be written |
|||
*/ |
|||
void WriteTasFile(std::u8string file_name); |
|||
|
|||
/** Inverts the Y axis polarity |
|||
* @param old: value of the axis |
|||
* @return new value of the axis |
|||
*/ |
|||
TasAnalog FlipAxisY(TasAnalog old); |
|||
|
|||
/** |
|||
* Parses a string containing the axis values. X and Y have a range from -32767 to 32767 |
|||
* @param line: string containing axis values with the following format "x;y" |
|||
* @return Returns a TAS analog object with axis values with range from -1.0 to 1.0 |
|||
*/ |
|||
TasAnalog ReadCommandAxis(const std::string& line) const; |
|||
|
|||
/** |
|||
* Parses a string containing the button values. Each button is represented by it's text format |
|||
* specified in text_to_tas_button array |
|||
* @param line: string containing button name with the following format "a;b;c;d..." |
|||
* @return Returns a u32 with each bit representing the status of a button |
|||
*/ |
|||
u32 ReadCommandButtons(const std::string& line) const; |
|||
|
|||
/** |
|||
* Reset state of all players |
|||
*/ |
|||
void ClearInput(); |
|||
|
|||
/** |
|||
* Converts an u32 containing the button status into the text equivalent |
|||
* @param buttons: bitfield with the status of the buttons |
|||
* @return Returns a string with the name of the buttons to be written to the file |
|||
*/ |
|||
std::string WriteCommandButtons(u32 buttons) const; |
|||
|
|||
/** |
|||
* Converts an TAS analog object containing the axis status into the text equivalent |
|||
* @param data: value of the axis |
|||
* @return A string with the value of the axis to be written to the file |
|||
*/ |
|||
std::string WriteCommandAxis(TasAnalog data) const; |
|||
|
|||
size_t script_length{0}; |
|||
bool is_old_input_saved{false}; |
|||
bool is_recording{false}; |
|||
bool is_running{false}; |
|||
bool needs_reset{false}; |
|||
std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{}; |
|||
std::vector<TASCommand> record_commands{}; |
|||
size_t current_command{0}; |
|||
TASCommand last_input{}; // only used for recording |
|||
}; |
|||
} // namespace InputCommon::TasInput |
|||
@ -1,168 +0,0 @@ |
|||
// Copyright 2014 Dolphin Emulator Project |
|||
// Licensed under GPLv2+ |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
#include <algorithm> |
|||
#include <functional> |
|||
#include <mutex> |
|||
#include <thread> |
|||
#include <unordered_map> |
|||
#include "common/common_types.h" |
|||
#include "common/threadsafe_queue.h" |
|||
#include "input_common/main.h" |
|||
|
|||
struct libusb_context; |
|||
struct libusb_device; |
|||
struct libusb_device_handle; |
|||
|
|||
namespace GCAdapter { |
|||
|
|||
enum class PadButton { |
|||
Undefined = 0x0000, |
|||
ButtonLeft = 0x0001, |
|||
ButtonRight = 0x0002, |
|||
ButtonDown = 0x0004, |
|||
ButtonUp = 0x0008, |
|||
TriggerZ = 0x0010, |
|||
TriggerR = 0x0020, |
|||
TriggerL = 0x0040, |
|||
ButtonA = 0x0100, |
|||
ButtonB = 0x0200, |
|||
ButtonX = 0x0400, |
|||
ButtonY = 0x0800, |
|||
ButtonStart = 0x1000, |
|||
// Below is for compatibility with "AxisButton" type |
|||
Stick = 0x2000, |
|||
}; |
|||
|
|||
enum class PadAxes : u8 { |
|||
StickX, |
|||
StickY, |
|||
SubstickX, |
|||
SubstickY, |
|||
TriggerLeft, |
|||
TriggerRight, |
|||
Undefined, |
|||
}; |
|||
|
|||
enum class ControllerTypes { |
|||
None, |
|||
Wired, |
|||
Wireless, |
|||
}; |
|||
|
|||
struct GCPadStatus { |
|||
std::size_t port{}; |
|||
|
|||
PadButton button{PadButton::Undefined}; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits |
|||
|
|||
PadAxes axis{PadAxes::Undefined}; |
|||
s16 axis_value{}; |
|||
u8 axis_threshold{50}; |
|||
}; |
|||
|
|||
struct GCController { |
|||
ControllerTypes type{}; |
|||
bool enable_vibration{}; |
|||
u8 rumble_amplitude{}; |
|||
u16 buttons{}; |
|||
PadButton last_button{}; |
|||
std::array<s16, 6> axis_values{}; |
|||
std::array<u8, 6> axis_origin{}; |
|||
u8 reset_origin_counter{}; |
|||
}; |
|||
|
|||
class Adapter { |
|||
public: |
|||
Adapter(); |
|||
~Adapter(); |
|||
|
|||
/// Request a vibration for a controller |
|||
bool RumblePlay(std::size_t port, u8 amplitude); |
|||
|
|||
/// Used for polling |
|||
void BeginConfiguration(); |
|||
void EndConfiguration(); |
|||
|
|||
Common::SPSCQueue<GCPadStatus>& GetPadQueue(); |
|||
const Common::SPSCQueue<GCPadStatus>& GetPadQueue() const; |
|||
|
|||
GCController& GetPadState(std::size_t port); |
|||
const GCController& GetPadState(std::size_t port) const; |
|||
|
|||
/// Returns true if there is a device connected to port |
|||
bool DeviceConnected(std::size_t port) const; |
|||
|
|||
/// Used for automapping features |
|||
std::vector<Common::ParamPackage> GetInputDevices() const; |
|||
InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const; |
|||
InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const; |
|||
|
|||
private: |
|||
using AdapterPayload = std::array<u8, 37>; |
|||
|
|||
void UpdatePadType(std::size_t port, ControllerTypes pad_type); |
|||
void UpdateControllers(const AdapterPayload& adapter_payload); |
|||
void UpdateYuzuSettings(std::size_t port); |
|||
void UpdateStateButtons(std::size_t port, u8 b1, u8 b2); |
|||
void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload); |
|||
void UpdateVibrations(); |
|||
|
|||
void AdapterInputThread(); |
|||
|
|||
void AdapterScanThread(); |
|||
|
|||
bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size); |
|||
|
|||
// Updates vibration state of all controllers |
|||
void SendVibrations(); |
|||
|
|||
/// For use in initialization, querying devices to find the adapter |
|||
void Setup(); |
|||
|
|||
/// Resets status of all GC controller devices to a disconnected state |
|||
void ResetDevices(); |
|||
|
|||
/// Resets status of device connected to a disconnected state |
|||
void ResetDevice(std::size_t port); |
|||
|
|||
/// Returns true if we successfully gain access to GC Adapter |
|||
bool CheckDeviceAccess(); |
|||
|
|||
/// Captures GC Adapter endpoint address |
|||
/// Returns true if the endpoint was set correctly |
|||
bool GetGCEndpoint(libusb_device* device); |
|||
|
|||
/// For shutting down, clear all data, join all threads, release usb |
|||
void Reset(); |
|||
|
|||
// Join all threads |
|||
void JoinThreads(); |
|||
|
|||
// Release usb handles |
|||
void ClearLibusbHandle(); |
|||
|
|||
libusb_device_handle* usb_adapter_handle = nullptr; |
|||
std::array<GCController, 4> pads; |
|||
Common::SPSCQueue<GCPadStatus> pad_queue; |
|||
|
|||
std::thread adapter_input_thread; |
|||
std::thread adapter_scan_thread; |
|||
bool adapter_input_thread_running; |
|||
bool adapter_scan_thread_running; |
|||
bool restart_scan_thread; |
|||
|
|||
libusb_context* libusb_ctx; |
|||
|
|||
u8 input_endpoint{0}; |
|||
u8 output_endpoint{0}; |
|||
u8 input_error_counter{0}; |
|||
u8 output_error_counter{0}; |
|||
int vibration_counter{0}; |
|||
|
|||
bool configuring{false}; |
|||
bool rumble_enabled{true}; |
|||
bool vibration_changed{true}; |
|||
}; |
|||
} // namespace GCAdapter |
|||
@ -1,356 +0,0 @@ |
|||
// Copyright 2020 yuzu Emulator Project
|
|||
// Licensed under GPLv2 or any later version
|
|||
// Refer to the license.txt file included.
|
|||
|
|||
#include <atomic>
|
|||
#include <list>
|
|||
#include <mutex>
|
|||
#include <utility>
|
|||
#include "common/assert.h"
|
|||
#include "common/threadsafe_queue.h"
|
|||
#include "input_common/gcadapter/gc_adapter.h"
|
|||
#include "input_common/gcadapter/gc_poller.h"
|
|||
|
|||
namespace InputCommon { |
|||
|
|||
class GCButton final : public Input::ButtonDevice { |
|||
public: |
|||
explicit GCButton(u32 port_, s32 button_, const GCAdapter::Adapter* adapter) |
|||
: port(port_), button(button_), gcadapter(adapter) {} |
|||
|
|||
~GCButton() override; |
|||
|
|||
bool GetStatus() const override { |
|||
if (gcadapter->DeviceConnected(port)) { |
|||
return (gcadapter->GetPadState(port).buttons & button) != 0; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private: |
|||
const u32 port; |
|||
const s32 button; |
|||
const GCAdapter::Adapter* gcadapter; |
|||
}; |
|||
|
|||
class GCAxisButton final : public Input::ButtonDevice { |
|||
public: |
|||
explicit GCAxisButton(u32 port_, u32 axis_, float threshold_, bool trigger_if_greater_, |
|||
const GCAdapter::Adapter* adapter) |
|||
: port(port_), axis(axis_), threshold(threshold_), trigger_if_greater(trigger_if_greater_), |
|||
gcadapter(adapter) {} |
|||
|
|||
bool GetStatus() const override { |
|||
if (gcadapter->DeviceConnected(port)) { |
|||
const float current_axis_value = gcadapter->GetPadState(port).axis_values.at(axis); |
|||
const float axis_value = current_axis_value / 128.0f; |
|||
if (trigger_if_greater) { |
|||
// TODO: Might be worthwile to set a slider for the trigger threshold. It is
|
|||
// currently always set to 0.5 in configure_input_player.cpp ZL/ZR HandleClick
|
|||
return axis_value > threshold; |
|||
} |
|||
return axis_value < -threshold; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private: |
|||
const u32 port; |
|||
const u32 axis; |
|||
float threshold; |
|||
bool trigger_if_greater; |
|||
const GCAdapter::Adapter* gcadapter; |
|||
}; |
|||
|
|||
GCButtonFactory::GCButtonFactory(std::shared_ptr<GCAdapter::Adapter> adapter_) |
|||
: adapter(std::move(adapter_)) {} |
|||
|
|||
GCButton::~GCButton() = default; |
|||
|
|||
std::unique_ptr<Input::ButtonDevice> GCButtonFactory::Create(const Common::ParamPackage& params) { |
|||
const auto button_id = params.Get("button", 0); |
|||
const auto port = static_cast<u32>(params.Get("port", 0)); |
|||
|
|||
constexpr s32 PAD_STICK_ID = static_cast<s32>(GCAdapter::PadButton::Stick); |
|||
|
|||
// button is not an axis/stick button
|
|||
if (button_id != PAD_STICK_ID) { |
|||
return std::make_unique<GCButton>(port, button_id, adapter.get()); |
|||
} |
|||
|
|||
// For Axis buttons, used by the binary sticks.
|
|||
if (button_id == PAD_STICK_ID) { |
|||
const int axis = params.Get("axis", 0); |
|||
const float threshold = params.Get("threshold", 0.25f); |
|||
const std::string direction_name = params.Get("direction", ""); |
|||
bool trigger_if_greater; |
|||
if (direction_name == "+") { |
|||
trigger_if_greater = true; |
|||
} else if (direction_name == "-") { |
|||
trigger_if_greater = false; |
|||
} else { |
|||
trigger_if_greater = true; |
|||
LOG_ERROR(Input, "Unknown direction {}", direction_name); |
|||
} |
|||
return std::make_unique<GCAxisButton>(port, axis, threshold, trigger_if_greater, |
|||
adapter.get()); |
|||
} |
|||
|
|||
return nullptr; |
|||
} |
|||
|
|||
Common::ParamPackage GCButtonFactory::GetNextInput() const { |
|||
Common::ParamPackage params; |
|||
GCAdapter::GCPadStatus pad; |
|||
auto& queue = adapter->GetPadQueue(); |
|||
while (queue.Pop(pad)) { |
|||
// This while loop will break on the earliest detected button
|
|||
params.Set("engine", "gcpad"); |
|||
params.Set("port", static_cast<s32>(pad.port)); |
|||
if (pad.button != GCAdapter::PadButton::Undefined) { |
|||
params.Set("button", static_cast<u16>(pad.button)); |
|||
} |
|||
|
|||
// For Axis button implementation
|
|||
if (pad.axis != GCAdapter::PadAxes::Undefined) { |
|||
params.Set("axis", static_cast<u8>(pad.axis)); |
|||
params.Set("button", static_cast<u16>(GCAdapter::PadButton::Stick)); |
|||
params.Set("threshold", "0.25"); |
|||
if (pad.axis_value > 0) { |
|||
params.Set("direction", "+"); |
|||
} else { |
|||
params.Set("direction", "-"); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
return params; |
|||
} |
|||
|
|||
void GCButtonFactory::BeginConfiguration() { |
|||
polling = true; |
|||
adapter->BeginConfiguration(); |
|||
} |
|||
|
|||
void GCButtonFactory::EndConfiguration() { |
|||
polling = false; |
|||
adapter->EndConfiguration(); |
|||
} |
|||
|
|||
class GCAnalog final : public Input::AnalogDevice { |
|||
public: |
|||
explicit GCAnalog(u32 port_, u32 axis_x_, u32 axis_y_, bool invert_x_, bool invert_y_, |
|||
float deadzone_, float range_, const GCAdapter::Adapter* adapter) |
|||
: port(port_), axis_x(axis_x_), axis_y(axis_y_), invert_x(invert_x_), invert_y(invert_y_), |
|||
deadzone(deadzone_), range(range_), gcadapter(adapter) {} |
|||
|
|||
float GetAxis(u32 axis) const { |
|||
if (gcadapter->DeviceConnected(port)) { |
|||
std::lock_guard lock{mutex}; |
|||
const auto axis_value = |
|||
static_cast<float>(gcadapter->GetPadState(port).axis_values.at(axis)); |
|||
return (axis_value) / (100.0f * range); |
|||
} |
|||
return 0.0f; |
|||
} |
|||
|
|||
std::pair<float, float> GetAnalog(u32 analog_axis_x, u32 analog_axis_y) const { |
|||
float x = GetAxis(analog_axis_x); |
|||
float y = GetAxis(analog_axis_y); |
|||
if (invert_x) { |
|||
x = -x; |
|||
} |
|||
if (invert_y) { |
|||
y = -y; |
|||
} |
|||
// Make sure the coordinates are in the unit circle,
|
|||
// otherwise normalize it.
|
|||
float r = x * x + y * y; |
|||
if (r > 1.0f) { |
|||
r = std::sqrt(r); |
|||
x /= r; |
|||
y /= r; |
|||
} |
|||
|
|||
return {x, y}; |
|||
} |
|||
|
|||
std::tuple<float, float> GetStatus() const override { |
|||
const auto [x, y] = GetAnalog(axis_x, axis_y); |
|||
const float r = std::sqrt((x * x) + (y * y)); |
|||
if (r > deadzone) { |
|||
return {x / r * (r - deadzone) / (1 - deadzone), |
|||
y / r * (r - deadzone) / (1 - deadzone)}; |
|||
} |
|||
return {0.0f, 0.0f}; |
|||
} |
|||
|
|||
std::tuple<float, float> GetRawStatus() const override { |
|||
const float x = GetAxis(axis_x); |
|||
const float y = GetAxis(axis_y); |
|||
return {x, y}; |
|||
} |
|||
|
|||
Input::AnalogProperties GetAnalogProperties() const override { |
|||
return {deadzone, range, 0.5f}; |
|||
} |
|||
|
|||
bool GetAnalogDirectionStatus(Input::AnalogDirection direction) const override { |
|||
const auto [x, y] = GetStatus(); |
|||
const float directional_deadzone = 0.5f; |
|||
switch (direction) { |
|||
case Input::AnalogDirection::RIGHT: |
|||
return x > directional_deadzone; |
|||
case Input::AnalogDirection::LEFT: |
|||
return x < -directional_deadzone; |
|||
case Input::AnalogDirection::UP: |
|||
return y > directional_deadzone; |
|||
case Input::AnalogDirection::DOWN: |
|||
return y < -directional_deadzone; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private: |
|||
const u32 port; |
|||
const u32 axis_x; |
|||
const u32 axis_y; |
|||
const bool invert_x; |
|||
const bool invert_y; |
|||
const float deadzone; |
|||
const float range; |
|||
const GCAdapter::Adapter* gcadapter; |
|||
mutable std::mutex mutex; |
|||
}; |
|||
|
|||
/// An analog device factory that creates analog devices from GC Adapter
|
|||
GCAnalogFactory::GCAnalogFactory(std::shared_ptr<GCAdapter::Adapter> adapter_) |
|||
: adapter(std::move(adapter_)) {} |
|||
|
|||
/**
|
|||
* Creates analog device from joystick axes |
|||
* @param params contains parameters for creating the device: |
|||
* - "port": the nth gcpad on the adapter |
|||
* - "axis_x": the index of the axis to be bind as x-axis |
|||
* - "axis_y": the index of the axis to be bind as y-axis |
|||
*/ |
|||
std::unique_ptr<Input::AnalogDevice> GCAnalogFactory::Create(const Common::ParamPackage& params) { |
|||
const auto port = static_cast<u32>(params.Get("port", 0)); |
|||
const auto axis_x = static_cast<u32>(params.Get("axis_x", 0)); |
|||
const auto axis_y = static_cast<u32>(params.Get("axis_y", 1)); |
|||
const auto deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, 1.0f); |
|||
const auto range = std::clamp(params.Get("range", 1.0f), 0.50f, 1.50f); |
|||
const std::string invert_x_value = params.Get("invert_x", "+"); |
|||
const std::string invert_y_value = params.Get("invert_y", "+"); |
|||
const bool invert_x = invert_x_value == "-"; |
|||
const bool invert_y = invert_y_value == "-"; |
|||
|
|||
return std::make_unique<GCAnalog>(port, axis_x, axis_y, invert_x, invert_y, deadzone, range, |
|||
adapter.get()); |
|||
} |
|||
|
|||
void GCAnalogFactory::BeginConfiguration() { |
|||
polling = true; |
|||
adapter->BeginConfiguration(); |
|||
} |
|||
|
|||
void GCAnalogFactory::EndConfiguration() { |
|||
polling = false; |
|||
adapter->EndConfiguration(); |
|||
} |
|||
|
|||
Common::ParamPackage GCAnalogFactory::GetNextInput() { |
|||
GCAdapter::GCPadStatus pad; |
|||
Common::ParamPackage params; |
|||
auto& queue = adapter->GetPadQueue(); |
|||
while (queue.Pop(pad)) { |
|||
if (pad.button != GCAdapter::PadButton::Undefined) { |
|||
params.Set("engine", "gcpad"); |
|||
params.Set("port", static_cast<s32>(pad.port)); |
|||
params.Set("button", static_cast<u16>(pad.button)); |
|||
return params; |
|||
} |
|||
if (pad.axis == GCAdapter::PadAxes::Undefined || |
|||
std::abs(static_cast<float>(pad.axis_value) / 128.0f) < 0.1f) { |
|||
continue; |
|||
} |
|||
// An analog device needs two axes, so we need to store the axis for later and wait for
|
|||
// a second input event. The axes also must be from the same joystick.
|
|||
const u8 axis = static_cast<u8>(pad.axis); |
|||
if (axis == 0 || axis == 1) { |
|||
analog_x_axis = 0; |
|||
analog_y_axis = 1; |
|||
controller_number = static_cast<s32>(pad.port); |
|||
break; |
|||
} |
|||
if (axis == 2 || axis == 3) { |
|||
analog_x_axis = 2; |
|||
analog_y_axis = 3; |
|||
controller_number = static_cast<s32>(pad.port); |
|||
break; |
|||
} |
|||
|
|||
if (analog_x_axis == -1) { |
|||
analog_x_axis = axis; |
|||
controller_number = static_cast<s32>(pad.port); |
|||
} else if (analog_y_axis == -1 && analog_x_axis != axis && |
|||
controller_number == static_cast<s32>(pad.port)) { |
|||
analog_y_axis = axis; |
|||
break; |
|||
} |
|||
} |
|||
if (analog_x_axis != -1 && analog_y_axis != -1) { |
|||
params.Set("engine", "gcpad"); |
|||
params.Set("port", controller_number); |
|||
params.Set("axis_x", analog_x_axis); |
|||
params.Set("axis_y", analog_y_axis); |
|||
params.Set("invert_x", "+"); |
|||
params.Set("invert_y", "+"); |
|||
analog_x_axis = -1; |
|||
analog_y_axis = -1; |
|||
controller_number = -1; |
|||
return params; |
|||
} |
|||
return params; |
|||
} |
|||
|
|||
class GCVibration final : public Input::VibrationDevice { |
|||
public: |
|||
explicit GCVibration(u32 port_, GCAdapter::Adapter* adapter) |
|||
: port(port_), gcadapter(adapter) {} |
|||
|
|||
u8 GetStatus() const override { |
|||
return gcadapter->RumblePlay(port, 0); |
|||
} |
|||
|
|||
bool SetRumblePlay(f32 amp_low, [[maybe_unused]] f32 freq_low, f32 amp_high, |
|||
[[maybe_unused]] f32 freq_high) const override { |
|||
const auto mean_amplitude = (amp_low + amp_high) * 0.5f; |
|||
const auto processed_amplitude = |
|||
static_cast<u8>((mean_amplitude + std::pow(mean_amplitude, 0.3f)) * 0.5f * 0x8); |
|||
|
|||
return gcadapter->RumblePlay(port, processed_amplitude); |
|||
} |
|||
|
|||
private: |
|||
const u32 port; |
|||
GCAdapter::Adapter* gcadapter; |
|||
}; |
|||
|
|||
/// An vibration device factory that creates vibration devices from GC Adapter
|
|||
GCVibrationFactory::GCVibrationFactory(std::shared_ptr<GCAdapter::Adapter> adapter_) |
|||
: adapter(std::move(adapter_)) {} |
|||
|
|||
/**
|
|||
* Creates a vibration device from a joystick |
|||
* @param params contains parameters for creating the device: |
|||
* - "port": the nth gcpad on the adapter |
|||
*/ |
|||
std::unique_ptr<Input::VibrationDevice> GCVibrationFactory::Create( |
|||
const Common::ParamPackage& params) { |
|||
const auto port = static_cast<u32>(params.Get("port", 0)); |
|||
|
|||
return std::make_unique<GCVibration>(port, adapter.get()); |
|||
} |
|||
|
|||
} // namespace InputCommon
|
|||
@ -1,78 +0,0 @@ |
|||
// Copyright 2020 yuzu Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <memory> |
|||
#include "core/frontend/input.h" |
|||
#include "input_common/gcadapter/gc_adapter.h" |
|||
|
|||
namespace InputCommon { |
|||
|
|||
/** |
|||
* A button device factory representing a gcpad. It receives gcpad events and forward them |
|||
* to all button devices it created. |
|||
*/ |
|||
class GCButtonFactory final : public Input::Factory<Input::ButtonDevice> { |
|||
public: |
|||
explicit GCButtonFactory(std::shared_ptr<GCAdapter::Adapter> adapter_); |
|||
|
|||
/** |
|||
* Creates a button device from a button press |
|||
* @param params contains parameters for creating the device: |
|||
* - "code": the code of the key to bind with the button |
|||
*/ |
|||
std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override; |
|||
|
|||
Common::ParamPackage GetNextInput() const; |
|||
|
|||
/// For device input configuration/polling |
|||
void BeginConfiguration(); |
|||
void EndConfiguration(); |
|||
|
|||
bool IsPolling() const { |
|||
return polling; |
|||
} |
|||
|
|||
private: |
|||
std::shared_ptr<GCAdapter::Adapter> adapter; |
|||
bool polling = false; |
|||
}; |
|||
|
|||
/// An analog device factory that creates analog devices from GC Adapter |
|||
class GCAnalogFactory final : public Input::Factory<Input::AnalogDevice> { |
|||
public: |
|||
explicit GCAnalogFactory(std::shared_ptr<GCAdapter::Adapter> adapter_); |
|||
|
|||
std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override; |
|||
Common::ParamPackage GetNextInput(); |
|||
|
|||
/// For device input configuration/polling |
|||
void BeginConfiguration(); |
|||
void EndConfiguration(); |
|||
|
|||
bool IsPolling() const { |
|||
return polling; |
|||
} |
|||
|
|||
private: |
|||
std::shared_ptr<GCAdapter::Adapter> adapter; |
|||
int analog_x_axis = -1; |
|||
int analog_y_axis = -1; |
|||
int controller_number = -1; |
|||
bool polling = false; |
|||
}; |
|||
|
|||
/// A vibration device factory creates vibration devices from GC Adapter |
|||
class GCVibrationFactory final : public Input::Factory<Input::VibrationDevice> { |
|||
public: |
|||
explicit GCVibrationFactory(std::shared_ptr<GCAdapter::Adapter> adapter_); |
|||
|
|||
std::unique_ptr<Input::VibrationDevice> Create(const Common::ParamPackage& params) override; |
|||
|
|||
private: |
|||
std::shared_ptr<GCAdapter::Adapter> adapter; |
|||
}; |
|||
|
|||
} // namespace InputCommon |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue