/* 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 . */ #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 #include #include #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> GamepadDevice::_gamepads; std::mutex GamepadDevice::_gamepads_mutex; #ifdef TEST_AUTOMATION #include "hw/sh4/sh4_sched.h" #include static FILE *record_input; #endif bool GamepadDevice::handleButtonInput(int port, DreamcastKey key, bool pressed) { if (key == EMU_BTN_NONE) return false; if (key <= DC_BTN_BITMAPPED_LAST) { if (port >= 0) { 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_ESCAPE: if (pressed) dc_exit(); break; case EMU_BTN_MENU: if (pressed) gui_open_settings(); 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_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(port, key, pressed, joyy[port]); break; case DC_AXIS_LEFT: case DC_AXIS_RIGHT: buttonToAnalogInput(port, key, pressed, joyx[port]); break; case DC_AXIS2_UP: case DC_AXIS2_DOWN: buttonToAnalogInput(port, key, pressed, joyry[port]); break; case DC_AXIS2_LEFT: case DC_AXIS2_RIGHT: buttonToAnalogInput(port, key, pressed, joyrx[port]); break; case DC_AXIS3_UP: case DC_AXIS3_DOWN: buttonToAnalogInput(port, key, pressed, joy3y[port]); break; case DC_AXIS3_LEFT: case DC_AXIS3_RIGHT: buttonToAnalogInput(port, key, pressed, joy3x[port]); break; default: return false; } } DEBUG_LOG(INPUT, "%d: BUTTON %s %d. kcode=%x", port, pressed ? "down" : "up", key, port >= 0 ? kcode[port] : 0); 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 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 && _detecting_button && 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; } } // // value must be >= -32768 and <= 32767 for full axes // and 0 to 32767 for half axes/triggers // bool GamepadDevice::gamepad_axis_input(u32 code, int value) { const bool positive = value >= 0; const InputMapping::InputDef inputDef = InputMapping::InputDef::from_axis(code, positive); const InputMapping::InputDef inverseInputDef = InputMapping::InputDef::from_axis(code, !positive); if (_input_detected != nullptr && getTimeMs() >= _detection_start_time) { if (_detecting_axis && std::abs(value) >= AXIS_ACTIVATION_VALUE) { // 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); return true; } _input_detected(code, true, positive); _input_detected = nullptr; return true; } // When in combo detection, track button releases for axes too else if (_detecting_combo && std::abs(value) < AXIS_DEACTIVATION_VALUE) { // 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); } } } if (!input_mapper || _maple_port < 0 || _maple_port > 4) return false; auto handle_axis = [&](u32 port, DreamcastKey key, int v) { if ((key & DC_BTN_GROUP_MASK) == DC_AXIS_TRIGGERS) // Triggers { //printf("T-AXIS %d Mapped to %d -> %d\n", key, value, std::min(std::abs(v) >> 7, 255)); if (key == DC_AXIS_LT) lt[port] = std::min(std::abs(v) << 1, 0xffff); else if (key == DC_AXIS_RT) rt[port] = std::min(std::abs(v) << 1, 0xffff); else if (key == DC_AXIS_LT2) lt2[port] = std::min(std::abs(v) << 1, 0xffff); else if (key == DC_AXIS_RT2) rt2[port] = std::min(std::abs(v) << 1, 0xffff); 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? int threshold = AXIS_ACTIVATION_VALUE; if (isHalfAxis(code)) threshold = 100; if (std::abs(v) < threshold) kcode[port] |= key; // button released else kcode[port] &= ~key; // button pressed } else if ((key & DC_BTN_GROUP_MASK) == EMU_BUTTONS) // Map triggers to emu buttons { int lastValue = lastAxisValue[port][key]; 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] = newValue; } else return false; return true; }; bool rc = false; if (_maple_port == 4) { for (u32 port = 0; port < 4; port++) { DreamcastKey key = input_mapper->get_axis_id(port, code, !positive); // Reset opposite axis to 0 handle_axis(port, key, 0); key = input_mapper->get_axis_id(port, code, positive); rc = handle_axis(port, key, value) || rc; } } else { DreamcastKey key = input_mapper->get_axis_id(0, code, !positive); // Reset opposite axis to 0 handle_axis(_maple_port, key, 0); key = input_mapper->get_axis_id(0, code, positive); rc = handle_axis(_maple_port, key, value); } // Update axis press tracking for button combinations const int absValue = std::abs(value); if (absValue < AXIS_DEACTIVATION_VALUE || absValue >= AXIS_ACTIVATION_VALUE) { // Reset opposite axis to 0 rc = handleButtonInputDef(inverseInputDef, false) || rc; rc = handleButtonInputDef(inputDef, (absValue >= AXIS_ACTIVATION_VALUE)) || rc; } return rc; } void GamepadDevice::load_system_mappings() { for (int i = 0; i < GetGamepadCount(); i++) { std::shared_ptr gamepad = GetGamepad(i); if (!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 */) { 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(*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::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(*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); } } static void updateVibration(u32 port, float power, float inclination, u32 duration_ms) { int i = GamepadDevice::GetGamepadCount() - 1; for ( ; i >= 0; i--) { std::shared_ptr gamepad = GamepadDevice::GetGamepad(i); if (gamepad != NULL && gamepad->maple_port() == (int)port && gamepad->is_rumble_enabled()) gamepad->rumble(power, inclination, duration_ms); } } void GamepadDevice::detect_btn_input(input_detected_cb button_pressed) { detectInput(true, false, false, button_pressed); } void GamepadDevice::detect_axis_input(input_detected_cb axis_moved) { detectInput(false, true, false, axis_moved); } void GamepadDevice::detectButtonOrAxisInput(input_detected_cb input_changed) { detectInput(true, true, true, input_changed); } void GamepadDevice::detectInput(bool button, bool axis, bool combo, input_detected_cb input_changed) { if (button || axis || combo) { _input_detected = input_changed; _detecting_button = button; _detecting_axis = axis; _detecting_combo = combo; _detection_start_time = getTimeMs() + 200; detectionInputs.clear(); } } #ifdef TEST_AUTOMATION static FILE *get_record_input(bool write) { if (write && !cfgLoadBool("record", "record_input", false)) return NULL; if (!write && !cfgLoadBool("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& gamepad) { int maple_port = cfgLoadInt("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& 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 gamepad = GamepadDevice::GetGamepad(i); if (gamepad != NULL && !gamepad->unique_id().empty()) cfgSaveInt("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(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(1 << axis); if ((rampAnalogState[port] & negDir) == 0) // axis not active continue; DigAnalog posDir = static_cast(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