/* Copyright (c) 2012-2024 R. Danbrook Copyright (c) 2020-2024 Rupert Carmichael All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include "uiadapter.h" #include "inputmanager.h" #include "logdriver.h" #include "jg/jg_nes.h" namespace { jg_inputstate_t coreinput[5]{}; jg_inputinfo_t *inputinfo[5]{nullptr}; jg_inputstate_t uistate; jg_inputinfo_t uiinfo; constexpr size_t NDEFS_UI = 14; const char *defs_ui[NDEFS_UI] = { "ResetSoft", "ResetHard", "FDSNextSide", "FDSInsertEject", "QuickSave1", "QuickSave2", "QuickLoad1", "QuickLoad2", "Fullscreen", "Pause", "Mute", "FastForward", "Screenshot", "Quit" }; const int ui_defaults[NDEFS_UI - 1] = { 0xffbd + 1, 0xffbd + 2, 0xffbd + 3, 0xffbd + 4, 0xffbd + 5, 0xffbd + 6, 0xffbd + 7, 0xffbd + 8, 'f', 'p', 'm', '`', 0xffbd + 9 }; bool uiprev[NDEFS_UI]{}; uint8_t undef8{}; uint16_t undef16{}; constexpr int DEADZONE = 5120; constexpr size_t MAXPORTS = 4; SDL_Joystick *joystick[MAXPORTS]{nullptr}; int jsports[MAXPORTS]{}; int jsiid[MAXPORTS]{}; int jstrig[MAXPORTS]{}; bool conflict{false}; } InputManager::InputManager(JGManager& jgm, SettingManager& setmgr) : jgm(jgm), setmgr(setmgr) { for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) { jg_set_inputstate(&coreinput[i], i); } // Set up UI inputs uiinfo.type = JG_INPUT_EXTERNAL; uiinfo.index = 21; uiinfo.name = "ui"; uiinfo.fname = "User Interface"; uiinfo.defs = defs_ui; uiinfo.numaxes = 0; uiinfo.numbuttons = NDEFS_UI; uistate.button = (uint8_t*)calloc(uiinfo.numbuttons, sizeof(uint8_t)); } InputManager::~InputManager() { unassign(); free(uistate.button); } void InputManager::reassign() { unassign(); assign(); } void InputManager::assign() { // Allocate memory for input states for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) { inputinfo[i] = jgm.get_inputinfo(i); coreinput[i].axis = (int16_t*)calloc(inputinfo[i]->numaxes, sizeof(int16_t)); coreinput[i].button = (uint8_t*)calloc(inputinfo[i]->numbuttons, sizeof(uint8_t)); // There are always X, Y, and Z coords coreinput[i].coord = (int32_t*)calloc(3, sizeof(int32_t)); // Magic Number // There is always X and Y relative motion coreinput[i].rel = (int32_t*)calloc(2, sizeof(int32_t)); // Magic Number } remap_kb(); remap_js(); } void InputManager::unassign() { jxmap.clear(); jamap.clear(); jbmap.clear(); jhmap.clear(); kbmap.clear(); msmap.clear(); lightgun = false; for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) { if (coreinput[i].axis) { free(coreinput[i].axis); coreinput[i].axis = nullptr; } if (coreinput[i].button) { free(coreinput[i].button); coreinput[i].button = nullptr; } if (coreinput[i].coord) { free(coreinput[i].coord); coreinput[i].coord = nullptr; } if (coreinput[i].rel) { free(coreinput[i].rel); coreinput[i].rel = nullptr; } } } void InputManager::remap_kb() { kbmap.clear(); msmap.clear(); // -1 to prevent "Quit" from being defined by default for (size_t i = 0; i < NDEFS_UI - 1; ++i) { std::string val = setmgr.get_input("ui", uiinfo.defs[i]); if (val.empty()) { setmgr.set_input("ui", uiinfo.defs[i], std::to_string(ui_defaults[i])); kbmap[ui_defaults[i]] = &uistate.button[i]; } else { if (kbmap[std::stoi(val)] == nullptr) { kbmap[std::stoi(val)] = &uistate.button[i]; } else { LogDriver::log(LogLevel::Warn, std::string{"Input configuration conflict: "} + "ui, " + uiinfo.defs[i]); } } } // If "Quit" was defined, apply the definition std::string val = setmgr.get_input("ui", uiinfo.defs[NDEFS_UI - 1]); if (!val.empty()) { kbmap[std::stoi(val)] = &uistate.button[NDEFS_UI - 1]; } for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) { inputinfo[i] = jgm.get_inputinfo(i); if (inputinfo[i]->type == JG_INPUT_POINTER || inputinfo[i]->type == JG_INPUT_GUN) { msmap[0] = &coreinput[i].coord[0]; msmap[1] = &coreinput[i].coord[1]; } if (inputinfo[i]->type == JG_INPUT_GUN) { lightgun = true; } for (size_t j = 0; j < inputinfo[i]->numbuttons; ++j) { // Keyboard/Mouse const char *idef = inputinfo[i]->defs[j + inputinfo[i]->numaxes]; std::string val = setmgr.get_input(inputinfo[i]->name, idef); if (!val.empty()) { if (kbmap[std::stoi(val)] == nullptr) { kbmap[std::stoi(val)] = &coreinput[i].button[j]; } else { LogDriver::log(LogLevel::Warn, std::string{"Input configuration conflict: "} + inputinfo[i]->name + ", " + idef); } } } } } void InputManager::remap_js() { jxmap.clear(); jamap.clear(); jbmap.clear(); jhmap.clear(); for (size_t i = 0; i < jgm.get_coreinfo()->numinputs; ++i) { inputinfo[i] = jgm.get_inputinfo(i); // Pattern for joystick axis definitions mapped to emulated axes std::regex pattern{"j[0-9][a]\\d+"}; for (size_t j = 0; j < inputinfo[i]->numaxes; ++j) { std::string val = setmgr.get_input(std::string(inputinfo[i]->name) + "j", inputinfo[i]->defs[j]); if (val.empty()) { continue; } if (std::regex_match(val, pattern)) { int port = val[1] - '0'; int inum = std::stoi(std::string(&val[3])); jxmap[(port * 100) + (inum / 2)] = &coreinput[i].axis[j]; } else { LogDriver::log(LogLevel::Warn, std::string("Malformed input code: ") + val.c_str()); } } // Reset pattern for axes acting as buttons, buttons, and hats pattern = "j[0-9][abh]\\d+"; for (size_t j = 0; j < inputinfo[i]->numbuttons; ++j) { // Joystick std::string val = setmgr.get_input(std::string(inputinfo[i]->name) + "j", inputinfo[i]->defs[j + inputinfo[i]->numaxes]); if (val.empty()) { continue; } if (std::regex_match(val, pattern)) { int port = val[1] - '0'; int inum = std::stoi(std::string(&val[3])); if (val[2] == 'b') { jbmap[(port * 100) + inum] = &coreinput[i].button[j]; } else if (val[2] == 'h') { jhmap[(port * 100) + inum] = &coreinput[i].button[j]; } else if (val[2] == 'a') { jamap[(port * 100) + inum] = &coreinput[i].button[j]; } } else { LogDriver::log(LogLevel::Warn, std::string("Malformed input code: ") + val.c_str()); } } } for (size_t i = 0; i < NDEFS_UI; ++i) { std::string val = setmgr.get_input("uij", uiinfo.defs[i]); if (!val.empty()) { if (val[0] == 'j') { int port = val[1] - '0'; int inum = std::stoi(std::string(&val[3])); if (val[2] == 'b') { jbmap[(port * 100) + inum] = &uistate.button[i]; } else if (val[2] == 'h') { jhmap[(port * 100) + inum] = &uistate.button[i]; } else if (val[2] == 'a') { jamap[(port * 100) + inum] = &uistate.button[i]; } } } } } void InputManager::set_inputdef(SDL_Event& evt) { // Check if an axis is being assigned bool axis = false; if ((cfg_name == "arkanoid" || cfg_name == "pachinko") && cfg_defnum < 1) { axis = true; } switch (evt.type) { case SDL_JOYBUTTONDOWN: { if (axis) { LogDriver::log(LogLevel::Warn, "Tried to configure an axis as a button"); break; } SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jbutton.which); int port = SDL_JoystickGetPlayerIndex(js); int btn = evt.jbutton.button; if (jbmap[(port * 100) + btn] == nullptr) { std::string bstr{"j" + std::to_string(port) + "b" + std::to_string(btn)}; setmgr.set_input(cfg_name + "j", cfg_def, bstr); } else { conflict = true; } set_cfg_running(false); break; } case SDL_JOYHATMOTION: { if (axis) { LogDriver::log(LogLevel::Warn, "Tried to configure an axis as a hat"); break; } SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which); int port = SDL_JoystickGetPlayerIndex(js); int hat = 0; if (evt.jhat.value & SDL_HAT_UP) { hat = 0; } else if (evt.jhat.value & SDL_HAT_DOWN) { hat = 1; } else if (evt.jhat.value & SDL_HAT_LEFT) { hat = 2; } else if (evt.jhat.value & SDL_HAT_RIGHT) { hat = 3; } if (jhmap[(port * 100) + hat] == nullptr) { std::string hstr{"j" + std::to_string(port) + "h" + std::to_string(hat)}; setmgr.set_input(cfg_name + "j", cfg_def, hstr); } else { conflict = true; } set_cfg_running(false); break; } case SDL_JOYAXISMOTION: { SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which); int port = SDL_JoystickGetPlayerIndex(js); int jaxis = evt.jaxis.axis * 2 + (evt.jaxis.value > 0 ? 1 : 0); if (jstrig[port] & (1 << evt.jaxis.axis) && evt.jaxis.value < 0) { break; } if (abs(evt.jaxis.value) >= DEADZONE) { if (jamap[(port * 100) + jaxis] == nullptr) { std::string astr{"j" + std::to_string(port) + "a" + std::to_string(jaxis)}; setmgr.set_input(cfg_name + "j", cfg_def, astr); } else { conflict = true; } set_cfg_running(false); } break; } } // Early return if nothing new was defined if (cfg_running) { return; } // Remap the joysticks remap_js(); } void InputManager::event(SDL_Event& evt) { if (cfg_running) { set_inputdef(evt); return; } switch (evt.type) { case SDL_JOYDEVICEADDED: { int port = 0; // Choose next unplugged port for (int i = 0; i < MAXPORTS; ++i) { if (!jsports[i]) { joystick[i] = SDL_JoystickOpen(evt.jdevice.which); SDL_JoystickSetPlayerIndex(joystick[i], i); jsports[i] = 1; jsiid[i] = SDL_JoystickInstanceID(joystick[i]); port = i; jstrig[i] = 0; for (int j = 0; j < SDL_JoystickNumAxes(joystick[i]); ++j) { if (SDL_JoystickGetAxis(joystick[i], j) <= -(DEADZONE)) { jstrig[i] |= 1 << j; // it's a trigger } } break; } } LogDriver::log(LogLevel::Info, std::string("Joystick ") + std::to_string(SDL_JoystickGetPlayerIndex(joystick[port]) + 1) + " Connected: " + SDL_JoystickName(joystick[port]) + " (Instance ID: " + std::to_string(jsiid[port]) + ")"); break; } case SDL_JOYDEVICEREMOVED: { int id = evt.jdevice.which; for (int i = 0; i < MAXPORTS; ++i) { if (jsiid[i] == id) { jsports[i] = 0; LogDriver::log(LogLevel::Info, std::string("Joystick ") + std::to_string(i + 1) + " Disconnected (Instance ID: " + std::to_string(id) + ")"); SDL_JoystickClose(joystick[i]); break; } } break; } case SDL_JOYBUTTONUP: { SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jbutton.which); int port = SDL_JoystickGetPlayerIndex(js); int btn = (port * 100) + evt.jbutton.button; if (jbmap[btn] != nullptr) { *jbmap[btn] = 0; } break; } case SDL_JOYBUTTONDOWN: { SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jbutton.which); int port = SDL_JoystickGetPlayerIndex(js); int btn = (port * 100) + evt.jbutton.button; if (jbmap[btn] != nullptr) { *jbmap[btn] = 1; } break; } case SDL_JOYHATMOTION: { SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which); int port = SDL_JoystickGetPlayerIndex(js); int hat = (port * 100); if (jhmap[hat + 0] != nullptr) { *jhmap[hat + 0] = evt.jhat.value & SDL_HAT_UP; } if (jhmap[hat + 1] != nullptr) { *jhmap[hat + 1] = (evt.jhat.value & SDL_HAT_DOWN) >> 2; } if (jhmap[hat + 2] != nullptr) { *jhmap[hat + 2] = (evt.jhat.value & SDL_HAT_LEFT) >> 3; } if (jhmap[hat + 3] != nullptr) { *jhmap[hat + 3] = (evt.jhat.value & SDL_HAT_RIGHT) >> 1; } break; } case SDL_JOYAXISMOTION: { SDL_Joystick *js = SDL_JoystickFromInstanceID(evt.jhat.which); int port = SDL_JoystickGetPlayerIndex(js); int axis = (port * 100) + (evt.jaxis.axis * 2); // Buttons if (jamap[axis] != nullptr) { *jamap[axis] = evt.jaxis.value < -16384; } if (jamap[axis + 1] != nullptr) { *jamap[axis + 1] = evt.jaxis.value > 16384; } // Analogue axis = (port * 100) + evt.jaxis.axis; if (jxmap[axis] != nullptr) { *jxmap[axis] = abs(evt.jaxis.value) > DEADZONE ? evt.jaxis.value : 0; } break; } } ui_events(); } void InputManager::event(int key, bool pressed) { if (kbmap[key] != nullptr) { if (*kbmap[key] && pressed) { return; } *kbmap[key] = pressed; } } void InputManager::event(int x, int y) { if (msmap[0] != nullptr) { *msmap[0] = x; *msmap[1] = y; } } void InputManager::ui_events() { // Process any UI events - need to make sure it went from true to false to // emulate a "keyup" event for (size_t i = 0; i < NDEFS_UI; ++i) { if (uiprev[i] && !uistate.button[i]) { switch (i) { case 0: // ResetSoft jgm.reset(0); break; case 1: // ResetHard jgm.reset(1); break; case 2: // FDSInsertEject jgm.media_insert(); break; case 3: // FDSNextSide jgm.media_select(); break; case 4: // QuickSave1 jgm.state_qsave(0); break; case 5: // QuickSave2 jgm.state_qsave(1); break; case 6: // QuickLoad1 jgm.state_qload(0); break; case 7: // QuickLoad2 jgm.state_qload(1); break; case 8: // Fullscreen UiAdapter::fullscreen(); break; case 9: // Pause UiAdapter::pause(); break; case 10: // Mute UiAdapter::mute(); break; case 11: // FastForward UiAdapter::fastforward(false); break; case 12: // Screenshot UiAdapter::screenshot(); break; case 13: // Quit UiAdapter::quit(); break; } } uiprev[i] = uistate.button[i]; } if (uistate.button[11]) { UiAdapter::fastforward(true); } } std::vector InputManager::get_inputinfo() { std::vector input_info{}; input_info.push_back(jg_nes_inputinfo(0, JG_NES_PAD1)); input_info.push_back(jg_nes_inputinfo(1, JG_NES_PAD2)); input_info.push_back(jg_nes_inputinfo(2, JG_NES_PAD3)); input_info.push_back(jg_nes_inputinfo(3, JG_NES_PAD4)); input_info.push_back(jg_nes_inputinfo(4, JG_NES_ZAPPER)); input_info.push_back(jg_nes_inputinfo(5, JG_NES_ARKANOID)); input_info.push_back(jg_nes_inputinfo(6, JG_NES_POWERPAD)); input_info.push_back(jg_nes_inputinfo(7, JG_NES_POWERGLOVE)); input_info.push_back(jg_nes_inputinfo(8, JG_NES_FAMILYTRAINER)); input_info.push_back(jg_nes_inputinfo(9, JG_NES_PACHINKO)); input_info.push_back(jg_nes_inputinfo(10, JG_NES_OEKAKIDSTABLET)); input_info.push_back(jg_nes_inputinfo(11, JG_NES_KONAMIHYPERSHOT)); input_info.push_back(jg_nes_inputinfo(12, JG_NES_BANDAIHYPERSHOT)); input_info.push_back(jg_nes_inputinfo(13, JG_NES_CRAZYCLIMBER)); input_info.push_back(jg_nes_inputinfo(14, JG_NES_MAHJONG)); input_info.push_back(jg_nes_inputinfo(15, JG_NES_EXCITINGBOXING)); input_info.push_back(jg_nes_inputinfo(16, JG_NES_TOPRIDER)); input_info.push_back(jg_nes_inputinfo(17, JG_NES_POKKUNMOGURAA)); input_info.push_back(jg_nes_inputinfo(18, JG_NES_PARTYTAP)); input_info.push_back(jg_nes_inputinfo(19, JG_NES_VSSYS)); input_info.push_back(jg_nes_inputinfo(20, JG_NES_KARAOKESTUDIO)); input_info.push_back(uiinfo); // Special case return input_info; } std::string InputManager::get_inputdef(std::string device, std::string def) { std::string ret = setmgr.get_input(device, def); return ret; } void InputManager::clear_inputdef() { setmgr.set_input(cfg_name, cfg_def, std::string{}); setmgr.set_input(cfg_name + "j", cfg_def, std::string{}); remap_kb(); remap_js(); } void InputManager::set_inputcfg(std::string name, std::string def, int defnum) { set_cfg_running(true); cfg_name = name; cfg_def = def; cfg_defnum = defnum; } void InputManager::set_inputdef(int val) { // Check for mapping conflicts to avoid overwriting an active definition if (kbmap[val] != nullptr) { conflict = true; return; } setmgr.set_input(cfg_name, cfg_def, std::to_string(val)); // Remap all keyboard definitions remap_kb(); } void InputManager::set_cfg_running(bool running) { cfg_running = running; if (!running) { UiAdapter::show_inputmsg(conflict ? 2 : 0); conflict = false; } }