Files
flycast/core/input/gamepad_device.cpp
Joe Mattiello 5c86428c04 input: stop the emulator while saving an in-RAM savestate (#2554)
The in-RAM save hotkey called gui_saveState(false, true), which runs
dc_serialize() without stopping the emulator. With threaded rendering
on (the default), the SH4 and AICA keep running on the emulator thread
while the input thread serializes, so the state can be torn: memory and
registers from different moments.

Stop and restart around the save like the disk save hotkey and the
in-RAM load hotkey already do.

Co-authored-by: Claude Opus 5.5 <[email protected]>
[ci skip]
2026-10-16 20:11:22 +02:00

957 lines
25 KiB
C++

/*
Copyright 2019 flyinghead
This file is part of reicast.
reicast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
reicast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with reicast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "gamepad_device.h"
#include "cfg/cfg.h"
#include "stdclass.h"
#include "ui/gui.h"
#include "emulator.h"
#include "hw/maple/maple_devs.h"
#include "mouse.h"
#include <algorithm>
#include <mutex>
#include <vector>
#define MAPLE_PORT_CFG_PREFIX "maple_"
// Gamepads
u32 kcode[4] = { ~0u, ~0u, ~0u, ~0u };
s16 joyx[4];
s16 joyy[4];
s16 joyrx[4];
s16 joyry[4];
s16 joy3x[4];
s16 joy3y[4];
u16 rt[4];
u16 lt[4];
u16 lt2[4];
u16 rt2[4];
// Keyboards
u8 kb_shift[MAPLE_PORTS]; // shift keys pressed (bitmask)
u8 kb_key[MAPLE_PORTS][6]; // normal keys pressed
std::vector<std::shared_ptr<GamepadDevice>> GamepadDevice::_gamepads;
std::mutex GamepadDevice::_gamepads_mutex;
#ifdef TEST_AUTOMATION
#include "hw/sh4/sh4_sched.h"
#include <cstdio>
static FILE *record_input;
#endif
GamepadDevice::GamepadDevice(int maple_port, const char *api_name, bool remappable)
: _api_name(api_name), _maple_port(maple_port), _remappable(remappable),
digitalToAnalogState{}
{
EventManager::listen(Event::LocaleChange, emuEvent, this);
// Initialize pressedButtons sets
currentInputs.clear();
}
GamepadDevice::~GamepadDevice() {
EventManager::unlisten(Event::LocaleChange, emuEvent, this);
}
void GamepadDevice::emuEvent(Event event, void *arg) {
((GamepadDevice *)arg)->refreshName();
}
bool GamepadDevice::handleButtonInput(int port, DreamcastKey key, bool pressed)
{
if (key == EMU_BTN_NONE)
return false;
if (buttonListener != nullptr)
buttonListener(port, key, pressed);
DEBUG_LOG(INPUT, "%d: BUTTON %s %d. kcode=%x", port, pressed ? "down" : "up", key, port >= 0 ? kcode[port] : 0);
if (key <= DC_BTN_BITMAPPED_LAST)
{
if (port < 0)
return false;
if (pressed)
kcode[port] &= ~key;
else
kcode[port] |= key;
#ifdef TEST_AUTOMATION
if (record_input != NULL)
fprintf(record_input, "%ld button %x %04x\n", sh4_sched_now64(), port, kcode[port]);
#endif
}
else
{
switch (key)
{
case EMU_BTN_MENU:
if (pressed)
gui_open_settings();
break;
case EMU_BTN_ESCAPE:
if (pressed)
dc_exit();
break;
case EMU_BTN_PAUSE:
if (pressed)
gui_togglePause();
break;
case EMU_BTN_FFORWARD:
if (pressed && !gui_is_open())
settings.input.fastForwardMode = !settings.input.fastForwardMode && !settings.network.online && !settings.naomi.multiboard;
break;
case EMU_BTN_LOADSTATE:
if (pressed)
gui_loadState();
break;
case EMU_BTN_SAVESTATE:
if (pressed)
gui_saveState();
break;
case EMU_BTN_NEXTSLOT:
if (pressed)
gui_cycleSaveStateSlot(1);
break;
case EMU_BTN_PREVSLOT:
if (pressed)
gui_cycleSaveStateSlot(-1);
break;
case EMU_BTN_LOADSTATE_RAM:
if (pressed)
gui_loadState(true);
break;
case EMU_BTN_SAVESTATE_RAM:
if (pressed)
gui_saveState(true, true);
break;
case EMU_BTN_SCREENSHOT:
if (pressed)
gui_takeScreenshot();
break;
case DC_AXIS_LT:
if (port >= 0)
lt[port] = pressed ? 0xffff : 0;
break;
case DC_AXIS_RT:
if (port >= 0)
rt[port] = pressed ? 0xffff : 0;
break;
case DC_AXIS_LT2:
if (port >= 0)
lt2[port] = pressed ? 0xffff : 0;
break;
case DC_AXIS_RT2:
if (port >= 0)
rt2[port] = pressed ? 0xffff : 0;
break;
case DC_AXIS_UP:
case DC_AXIS_DOWN:
buttonToAnalogInput<DC_AXIS_UP, DIGANA_UP, DIGANA_DOWN>(port, key, pressed, joyy[port]);
break;
case DC_AXIS_LEFT:
case DC_AXIS_RIGHT:
buttonToAnalogInput<DC_AXIS_LEFT, DIGANA_LEFT, DIGANA_RIGHT>(port, key, pressed, joyx[port]);
break;
case DC_AXIS2_UP:
case DC_AXIS2_DOWN:
buttonToAnalogInput<DC_AXIS2_UP, DIGANA2_UP, DIGANA2_DOWN>(port, key, pressed, joyry[port]);
break;
case DC_AXIS2_LEFT:
case DC_AXIS2_RIGHT:
buttonToAnalogInput<DC_AXIS2_LEFT, DIGANA2_LEFT, DIGANA2_RIGHT>(port, key, pressed, joyrx[port]);
break;
case DC_AXIS3_UP:
case DC_AXIS3_DOWN:
buttonToAnalogInput<DC_AXIS3_UP, DIGANA3_UP, DIGANA3_DOWN>(port, key, pressed, joy3y[port]);
break;
case DC_AXIS3_LEFT:
case DC_AXIS3_RIGHT:
buttonToAnalogInput<DC_AXIS3_LEFT, DIGANA3_LEFT, DIGANA3_RIGHT>(port, key, pressed, joy3x[port]);
break;
default:
return false;
}
}
return true;
}
bool GamepadDevice::handleButtonInputDef(const InputMapping::InputDef& inputDef, bool pressed)
{
if (!input_mapper || _maple_port > (int)std::size(kcode))
return false;
// Update button press tracking
std::list<DreamcastKey> mappedKeys;
if (pressed)
{
// Add to triggered inputs
if (currentInputs.insert_back(inputDef))
{
// Handle keys activated by this new input
DreamcastKey mappedKey = input_mapper->get_button_id(0, currentInputs);
if (mappedKey != EMU_BTN_NONE) {
mappedKeys.push_back(mappedKey);
currentKeys.push_back(mappedKey);
}
}
}
else
{
// Remove from triggered inputs
if (currentInputs.remove(inputDef) > 0)
{
// Handle keys deactivated by this new input
mappedKeys = input_mapper->get_button_released_ids(0, currentKeys, inputDef);
for (const DreamcastKey& mappedKey : mappedKeys)
currentKeys.remove(mappedKey);
}
}
bool rc = false;
for (const DreamcastKey& mappedKey : mappedKeys)
rc = handleButtonInput(_maple_port == 4 ? 0 : _maple_port, mappedKey, pressed) || rc;
// Handle inputs for ports 1 to 3 in all-ports mode
// Combos are only detected on port 0 in this mode
if (_maple_port == 4)
{
const InputMapping::InputSet simpleInput{inputDef};
for (int port = 1; port < 4; port++) {
DreamcastKey mappedKey = input_mapper->get_button_id(port, simpleInput);
rc = handleButtonInput(port, mappedKey, pressed) || rc;
}
}
return rc;
}
bool GamepadDevice::gamepad_btn_input(u32 code, bool pressed)
{
const InputMapping::InputDef inputDef = InputMapping::InputDef::from_button(code);
// When detecting input for button mapping
if (_input_detected != nullptr && getTimeMs() >= _detection_start_time)
{
if (pressed)
{
// Button pressed - add to mapping and tracking
_input_detected(code, false, false);
detectionInputs.insert_back(inputDef);
// If we're not in combo detection mode, stop detecting after first button
if (!_detecting_combo)
{
_input_detected = nullptr;
detectionInputs.clear();
}
}
else if (_detecting_combo && detectionInputs.contains(inputDef))
{
// Button released - if this is a button we pressed during detection, end detection
_input_detected = nullptr;
detectionInputs.clear();
DEBUG_LOG(INPUT, "Ending combo detection on button release: %d", code);
}
return true;
}
return handleButtonInputDef(inputDef, pressed);
}
static DreamcastKey getOppositeAxis(DreamcastKey key)
{
switch (key)
{
case DC_AXIS_RIGHT: return DC_AXIS_LEFT;
case DC_AXIS_LEFT: return DC_AXIS_RIGHT;
case DC_AXIS_UP: return DC_AXIS_DOWN;
case DC_AXIS_DOWN: return DC_AXIS_UP;
case DC_AXIS2_RIGHT: return DC_AXIS2_LEFT;
case DC_AXIS2_LEFT: return DC_AXIS2_RIGHT;
case DC_AXIS2_UP: return DC_AXIS2_DOWN;
case DC_AXIS2_DOWN: return DC_AXIS2_UP;
case DC_AXIS3_RIGHT: return DC_AXIS3_LEFT;
case DC_AXIS3_LEFT: return DC_AXIS3_RIGHT;
case DC_AXIS3_UP: return DC_AXIS3_DOWN;
case DC_AXIS3_DOWN: return DC_AXIS3_UP;
default: return key;
}
}
bool GamepadDevice::detectAxis(u32 code, int value)
{
if (_input_detected == nullptr || getTimeMs() < _detection_start_time)
return false;
const auto& r = detectingAxes.try_emplace(code, value);
DetectingAxis& axis = r.first->second;
if (!r.second)
axis.setValue(value);
if (!axis.detected)
return false;
const bool isTrigger = axis.start != DetectingAxis::Zero;
const bool positive = isTrigger || axis.end == DetectingAxis::Positive;
const InputMapping::InputDef inputDef = InputMapping::InputDef::from_axis(code, positive);
// When in combo detection, track button releases for axes too
if (_detecting_combo && axis.released)
{
// If this is an axis we previously detected as pressed, end detection
if (detectionInputs.contains(inputDef))
{
_input_detected = nullptr;
detectionInputs.clear();
DEBUG_LOG(INPUT, "Ending combo detection on axis release: %d", code);
}
return false;
}
if (input_mapper != nullptr)
{
if (!isTrigger)
input_mapper->deleteTrigger(code);
else
input_mapper->addTrigger(code, axis.start == DetectingAxis::Positive);
}
// If we're in combo detection mode, add this axis to tracking but don't end detection
if (_detecting_combo)
{
// Track this axis as a "button" for combo detection
if (detectionInputs.insert_back(inputDef))
_input_detected(code, true, positive);
}
else
{
_input_detected(code, true, positive);
_input_detected = nullptr;
}
return true;
}
//
// value must be >= -32768 and <= 32767 for all axes
//
bool GamepadDevice::gamepad_axis_input(u32 code, int value)
{
if (detectAxis(code, value))
return true;
if (!input_mapper || _maple_port < 0 || _maple_port > 4)
return false;
const bool positive = input_mapper->isTrigger(code) || value >= 0;
const InputMapping::InputDef inputDef = InputMapping::InputDef::from_axis(code, positive);
auto handle_axis = [&](u32 port, DreamcastKey key, int v, u32 code)
{
if ((key & DC_BTN_GROUP_MASK) == DC_AXIS_TRIGGERS) // Triggers
{
int mv;
if (input_mapper->isTrigger(code))
{
mv = v + 32768;
if (input_mapper->isReverseTrigger(code))
mv = 65535 - mv;
}
else {
mv = std::min(std::abs(v) * 2, 0xffff);
}
//printf("T-AXIS %d Mapped to %d -> %d\n", key, v, mv);
if (key == DC_AXIS_LT)
lt[port] = mv;
else if (key == DC_AXIS_RT)
rt[port] = mv;
else if (key == DC_AXIS_LT2)
lt2[port] = mv;
else if (key == DC_AXIS_RT2)
rt2[port] = mv;
else
return false;
}
else if ((key & DC_BTN_GROUP_MASK) == DC_AXIS_STICKS) // Analog axes
{
//printf("AXIS %d Mapped to %d -> %d\n", key, value, v);
s16 *this_axis;
int otherAxisValue;
int axisDirection = -1;
switch (key)
{
case DC_AXIS_RIGHT:
axisDirection = 1;
[[fallthrough]];
case DC_AXIS_LEFT:
this_axis = &joyx[port];
otherAxisValue = lastAxisValue[port][DC_AXIS_UP];
break;
case DC_AXIS_DOWN:
axisDirection = 1;
[[fallthrough]];
case DC_AXIS_UP:
this_axis = &joyy[port];
otherAxisValue = lastAxisValue[port][DC_AXIS_LEFT];
break;
case DC_AXIS2_RIGHT:
axisDirection = 1;
[[fallthrough]];
case DC_AXIS2_LEFT:
this_axis = &joyrx[port];
otherAxisValue = lastAxisValue[port][DC_AXIS2_UP];
break;
case DC_AXIS2_DOWN:
axisDirection = 1;
[[fallthrough]];
case DC_AXIS2_UP:
this_axis = &joyry[port];
otherAxisValue = lastAxisValue[port][DC_AXIS2_LEFT];
break;
case DC_AXIS3_RIGHT:
axisDirection = 1;
[[fallthrough]];
case DC_AXIS3_LEFT:
this_axis = &joy3x[port];
otherAxisValue = lastAxisValue[port][DC_AXIS3_UP];
break;
case DC_AXIS3_DOWN:
axisDirection = 1;
[[fallthrough]];
case DC_AXIS3_UP:
this_axis = &joy3y[port];
otherAxisValue = lastAxisValue[port][DC_AXIS3_LEFT];
break;
default:
return false;
}
int& lastValue = lastAxisValue[port][key];
int& lastOpValue = lastAxisValue[port][getOppositeAxis(key)];
if (lastValue != v || lastOpValue != v)
{
lastValue = lastOpValue = v;
// Lightgun with left analog stick
if (key == DC_AXIS_RIGHT || key == DC_AXIS_LEFT)
mo_x_abs[port] = (std::abs(v) * axisDirection + 32768) * 639 / 65535;
else if (key == DC_AXIS_UP || key == DC_AXIS_DOWN)
mo_y_abs[port] = (std::abs(v) * axisDirection + 32768) * 479 / 65535;
}
// Radial dead zone
// FIXME compute both axes at the same time
const float nv = std::abs(v) / 32768.f;
const float r2 = nv * nv + otherAxisValue * otherAxisValue / 32768.f / 32768.f;
if (r2 < input_mapper->dead_zone * input_mapper->dead_zone || r2 == 0.f)
{
*this_axis = 0;
}
else
{
float pdz = nv * input_mapper->dead_zone / std::sqrt(r2);
// there's a dead angular zone at 45° with saturation > 1 (both axes are saturated)
v = std::round((nv - pdz) / (1 - pdz) * 32768.f * input_mapper->saturation);
*this_axis = std::clamp(v * axisDirection, -32768, 32767);
}
}
else if (key != EMU_BTN_NONE && key <= DC_BTN_BITMAPPED_LAST) // Map triggers to digital buttons
{
//printf("B-AXIS %d Mapped to %d -> %d\n", key, value, v);
// TODO hysteresis?
bool pressed = false;
if (input_mapper->isTrigger(code))
{
if (!input_mapper->isReverseTrigger(code))
pressed = v >= -32768 + 100; // positive range -32768 -> 32767
else
pressed = v <= 32767 - 100; // negative range 32767 -> -32768
}
else {
pressed = std::abs(v) >= AXIS_ACTIVATION_VALUE;
}
if (pressed)
kcode[port] &= ~key; // button pressed
else
kcode[port] |= key; // button released
}
else if ((key & DC_BTN_GROUP_MASK) == EMU_BUTTONS) // Map triggers to emu buttons
{
int lastValue = lastAxisValue[port][key];
if (input_mapper->isTrigger(code))
{
// 0 is the midpoint for half axes
if ((v >= 0) != (lastValue >= 0))
handleButtonInput(port, key, input_mapper->isReverseTrigger(code) == (v < 0));
}
else
{
lastValue = std::abs(lastValue);
int newValue = std::abs(v);
if ((lastValue < AXIS_ACTIVATION_VALUE && newValue >= AXIS_ACTIVATION_VALUE) || (lastValue >= AXIS_ACTIVATION_VALUE && newValue < AXIS_ACTIVATION_VALUE))
handleButtonInput(port, key, newValue >= AXIS_ACTIVATION_VALUE);
}
lastAxisValue[port][key] = v;
}
else
return false;
return true;
};
bool rc = false;
if (_maple_port == 4)
{
for (u32 port = 0; port < 4; port++)
{
if (!input_mapper->isTrigger(code)) {
DreamcastKey key = input_mapper->get_axis_id(port, code, !positive);
// Reset opposite axis to 0
handle_axis(port, key, 0, code);
}
DreamcastKey key = input_mapper->get_axis_id(port, code, positive);
rc = handle_axis(port, key, value, code) || rc;
}
}
else
{
if (!input_mapper->isTrigger(code)) {
DreamcastKey key = input_mapper->get_axis_id(0, code, !positive);
// Reset opposite axis to 0
handle_axis(_maple_port, key, 0, code);
}
DreamcastKey key = input_mapper->get_axis_id(0, code, positive);
rc = handle_axis(_maple_port, key, value, code);
}
// Update axis press tracking for button combinations
bool pressed = false;
bool released = false;
if (input_mapper->isTrigger(code))
{
if (!input_mapper->isReverseTrigger(code))
pressed = value >= -32768 + 100; // normal range -32768 -> 32767
else
pressed = value <= 32767 - 100; // reverse range 32767 -> -32768
released = !pressed;
}
else {
pressed = std::abs(value) >= AXIS_ACTIVATION_VALUE;
released = std::abs(value) < AXIS_ACTIVATION_VALUE;
}
if (pressed || released)
{
// Reset opposite axis to 0
if (!input_mapper->isTrigger(code)) {
const InputMapping::InputDef inverseInputDef = InputMapping::InputDef::from_axis(code, !positive);
rc = handleButtonInputDef(inverseInputDef, false) || rc;
}
rc = handleButtonInputDef(inputDef, pressed) || rc;
}
return rc;
}
void GamepadDevice::load_system_mappings()
{
for (int i = 0; i < GetGamepadCount(); i++)
{
std::shared_ptr<GamepadDevice> gamepad = GetGamepad(i);
if (gamepad != nullptr && !gamepad->find_mapping())
gamepad->resetMappingToDefault(settings.platform.isArcade(), true);
}
}
std::string GamepadDevice::make_mapping_filename(bool instance, int system, bool perGame /* = false */)
{
std::string mapping_file = api_name() + "_" + name();
if (instance)
mapping_file += "-" + _unique_id;
if (perGame && !settings.content.gameId.empty())
mapping_file += "_" + settings.content.gameId;
if (system != DC_PLATFORM_DREAMCAST)
mapping_file += "_arcade";
std::replace(mapping_file.begin(), mapping_file.end(), '/', '-');
std::replace(mapping_file.begin(), mapping_file.end(), '\\', '-');
std::replace(mapping_file.begin(), mapping_file.end(), ':', '-');
std::replace(mapping_file.begin(), mapping_file.end(), '?', '-');
std::replace(mapping_file.begin(), mapping_file.end(), '*', '-');
std::replace(mapping_file.begin(), mapping_file.end(), '|', '-');
std::replace(mapping_file.begin(), mapping_file.end(), '"', '-');
std::replace(mapping_file.begin(), mapping_file.end(), '<', '-');
std::replace(mapping_file.begin(), mapping_file.end(), '>', '-');
mapping_file += ".cfg";
return mapping_file;
}
bool GamepadDevice::find_mapping(int system /* = settings.platform.system */)
{
// FIXME this is called on Event::Start in the game loader thread
// race condition with the UI thread?
if (!_remappable)
return true;
instanceMapping = false;
bool cloneMapping = false;
while (true)
{
bool perGame = !settings.content.gameId.empty();
while (true)
{
std::string mapping_file = make_mapping_filename(true, system, perGame);
input_mapper = InputMapping::LoadMapping(mapping_file);
if (!input_mapper)
{
mapping_file = make_mapping_filename(false, system, perGame);
input_mapper = InputMapping::LoadMapping(mapping_file);
}
else
{
instanceMapping = true;
}
if (!!input_mapper)
{
if (cloneMapping)
input_mapper = std::make_shared<InputMapping>(*input_mapper);
perGameMapping = perGame;
rumblePower = input_mapper->rumblePower;
return true;
}
if (!perGame)
break;
perGame = false;
}
if (system == DC_PLATFORM_DREAMCAST)
break;
system = DC_PLATFORM_DREAMCAST;
cloneMapping = true;
}
return false;
}
int GamepadDevice::GetGamepadCount() {
Lock _(_gamepads_mutex);
return _gamepads.size();
}
std::shared_ptr<GamepadDevice> GamepadDevice::GetGamepad(int index)
{
Lock _(_gamepads_mutex);
if (index >= 0 && index < (int)_gamepads.size())
return _gamepads[index];
else
return nullptr;
}
void GamepadDevice::save_mapping(int system /* = settings.platform.system */)
{
if (!input_mapper)
return;
std::string filename = make_mapping_filename(instanceMapping, system, perGameMapping);
InputMapping::SaveMapping(filename, input_mapper);
}
void GamepadDevice::setPerGameMapping(bool enabled)
{
perGameMapping = enabled;
if (enabled)
input_mapper = std::make_shared<InputMapping>(*input_mapper);
else
{
auto deleteMapping = [this](bool instance, int system) {
std::string filename = make_mapping_filename(instance, system, true);
InputMapping::DeleteMapping(filename);
};
deleteMapping(false, DC_PLATFORM_DREAMCAST);
deleteMapping(false, DC_PLATFORM_NAOMI);
deleteMapping(true, DC_PLATFORM_DREAMCAST);
deleteMapping(true, DC_PLATFORM_NAOMI);
}
}
void GamepadDevice::clearButtonMapping(u32 port, DreamcastKey key)
{
if (input_mapper == nullptr)
return;
// Release the button
if (key <= DC_BTN_BITMAPPED_LAST && port >= 0 && port < std::size(kcode))
kcode[port] |= key;
input_mapper->clear_button(port, key);
}
void GamepadDevice::clearAxisMapping(u32 port, DreamcastKey key)
{
if (input_mapper == nullptr)
return;
// Reset the axis value
if (port >= 0 && port < MAPLE_PORTS)
{
switch (key)
{
case DC_AXIS_UP:
case DC_AXIS_DOWN:
joyy[port] = 0;
break;
case DC_AXIS_LEFT:
case DC_AXIS_RIGHT:
joyx[port] = 0;
break;
case DC_AXIS2_UP:
case DC_AXIS2_DOWN:
joyry[port] = 0;
break;
case DC_AXIS2_LEFT:
case DC_AXIS2_RIGHT:
joyrx[port] = 0;
break;
case DC_AXIS3_UP:
case DC_AXIS3_DOWN:
joy3y[port] = 0;
break;
case DC_AXIS3_LEFT:
case DC_AXIS3_RIGHT:
joy3x[port] = 0;
break;
case DC_AXIS_RT:
rt[port] = 0;
break;
case DC_AXIS_LT:
lt[port] = 0;
break;
case DC_AXIS_RT2:
rt2[port] = 0;
break;
case DC_AXIS_LT2:
lt2[port] = 0;
break;
default:
break;
}
}
input_mapper->clear_axis(port, key);
}
static void updateVibration(u32 port, float power, float inclination, u32 duration_ms)
{
int i = GamepadDevice::GetGamepadCount() - 1;
for ( ; i >= 0; i--)
{
std::shared_ptr<GamepadDevice> gamepad = GamepadDevice::GetGamepad(i);
if (gamepad != NULL && gamepad->maple_port() == (int)port && gamepad->is_rumble_enabled())
gamepad->rumble(power, inclination, duration_ms);
}
}
void GamepadDevice::detectInput(bool combo, input_detected_cb input_changed)
{
_input_detected = input_changed;
_detecting_combo = combo;
_detection_start_time = getTimeMs() + 200;
detectionInputs.clear();
detectingAxes.clear();
}
#ifdef TEST_AUTOMATION
static FILE *get_record_input(bool write)
{
if (write && !config::loadBool("record", "record_input", false))
return NULL;
if (!write && !config::loadBool("record", "replay_input", false))
return NULL;
std::string game_dir = settings.content.path;
size_t slash = game_dir.find_last_of("/");
size_t dot = game_dir.find_last_of(".");
std::string input_file = "scripts/" + game_dir.substr(slash + 1, dot - slash) + "input";
return nowide::fopen(input_file.c_str(), write ? "w" : "r");
}
#endif
void GamepadDevice::Register(const std::shared_ptr<GamepadDevice>& gamepad)
{
int maple_port = config::loadInt("input",
MAPLE_PORT_CFG_PREFIX + gamepad->unique_id(), 12345);
if (maple_port != 12345)
gamepad->set_maple_port(maple_port);
#ifdef TEST_AUTOMATION
if (record_input == NULL)
{
record_input = get_record_input(true);
if (record_input != NULL)
setbuf(record_input, NULL);
}
#endif
Lock _(_gamepads_mutex);
_gamepads.push_back(gamepad);
MapleConfigMap::UpdateVibration = updateVibration;
gamepad->_is_registered = true;
gamepad->registered();
}
void GamepadDevice::Unregister(const std::shared_ptr<GamepadDevice>& gamepad)
{
Lock _(_gamepads_mutex);
for (auto it = _gamepads.begin(); it != _gamepads.end(); it++)
if (*it == gamepad) {
_gamepads.erase(it);
break;
}
}
void GamepadDevice::SaveMaplePorts()
{
for (int i = 0; i < GamepadDevice::GetGamepadCount(); i++)
{
std::shared_ptr<GamepadDevice> gamepad = GamepadDevice::GetGamepad(i);
if (gamepad != NULL && !gamepad->unique_id().empty())
config::saveInt("input", MAPLE_PORT_CFG_PREFIX + gamepad->unique_id(), gamepad->maple_port());
}
}
s16 (&GamepadDevice::getTargetArray(DigAnalog axis))[4]
{
switch (axis)
{
case DIGANA_LEFT:
case DIGANA_RIGHT:
return joyx;
case DIGANA_UP:;
case DIGANA_DOWN:
return joyy;
case DIGANA2_LEFT:
case DIGANA2_RIGHT:
return joyrx;
case DIGANA2_UP:
case DIGANA2_DOWN:
return joyry;
case DIGANA3_LEFT:
case DIGANA3_RIGHT:
return joy3x;
case DIGANA3_UP:
case DIGANA3_DOWN:
return joy3y;
default:
die("unknown axis");
}
}
void GamepadDevice::rampAnalog()
{
Lock _(rampMutex);
if (lastAnalogUpdate == 0)
// also used as a flag that no analog ramping is needed on this device (yet)
return;
const u64 now = getTimeMs();
const int delta = std::round(static_cast<float>(now - lastAnalogUpdate) * AnalogRamp);
lastAnalogUpdate = now;
for (unsigned port = 0; port < std::size(digitalToAnalogState); port++)
{
for (int axis = 0; axis < 12; axis += 2) // 3 sticks with 2 axes each
{
DigAnalog negDir = static_cast<DigAnalog>(1 << axis);
if ((rampAnalogState[port] & negDir) == 0)
// axis not active
continue;
DigAnalog posDir = static_cast<DigAnalog>(1 << (axis + 1));
const int socd = digitalToAnalogState[port] & (negDir | posDir);
s16& axisValue = getTargetArray(negDir)[port];
if (socd != 0 && socd != (negDir | posDir))
{
// One axis is pressed => ramp up
if (socd == posDir)
axisValue = std::min(32767, axisValue + delta);
else
axisValue = std::max(-32768, axisValue - delta);
}
else
{
// No axis is pressed (or both) => ramp down
if (axisValue > 0)
axisValue = std::max(0, axisValue - delta);
else if (axisValue < 0)
axisValue = std::min(0, axisValue + delta);
else
rampAnalogState[port] &= ~negDir;
}
}
}
}
void GamepadDevice::RampAnalog()
{
Lock _(_gamepads_mutex);
for (auto& gamepad : _gamepads)
gamepad->rampAnalog();
}
#ifdef TEST_AUTOMATION
#include "cfg/option.h"
static bool replay_inited;
FILE *replay_file;
u64 next_event;
u32 next_port;
u32 next_kcode;
bool do_screenshot;
void replay_input()
{
if (!replay_inited)
{
replay_file = get_record_input(false);
replay_inited = true;
}
u64 now = sh4_sched_now64();
if (config::UseReios)
{
// Account for the swirl time
if (config::Broadcast == 0)
now = std::max((int64_t)now - 2152626532L, 0L);
else
now = std::max((int64_t)now - 2191059108L, 0L);
}
if (replay_file == NULL)
{
if (next_event > 0 && now - next_event > SH4_MAIN_CLOCK * 5)
die("Automation time-out after 5 s\n");
return;
}
while (next_event <= now)
{
if (next_event > 0)
kcode[next_port] = next_kcode;
char action[32];
if (fscanf(replay_file, "%ld %s %x %x\n", &next_event, action, &next_port, &next_kcode) != 4)
{
fclose(replay_file);
replay_file = NULL;
NOTICE_LOG(INPUT, "Input replay terminated");
do_screenshot = true;
break;
}
}
}
#endif