/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * RE aid: a controller whose state comes from a FILE, not from a device. ****************************************************************************** */ #ifndef XENIA_HID_FILE_FILE_INPUT_DRIVER_H_ #define XENIA_HID_FILE_FILE_INPUT_DRIVER_H_ // Why this exists // --------------- // The scripted-input tool used for reverse engineering created its pad through // `/dev/uinput`. Input devices are NOT namespaced by the kernel, so a uinput // device created inside a container is registered with the HOST's input stack: // every trigger hold and button press is delivered to whatever on the host reads // gamepads, not only to the emulator. That is a real leak, and it was noticed the // hard way. // // This driver takes the kernel out of the loop entirely. The pad state lives in // an ordinary text file that only this container can see, and `GetState` reads it // (re-parsing only when the file changes). Nothing is registered with the host, // no X server is involved either, and — a bonus for RE — the analogue values are // exact rather than whatever a virtual stick quantises to. // // File format: one or more whitespace/newline separated `key=value` pairs. // // press=A,START buttons by name (see kButtonNames), comma separated // buttons=0x1010 or the raw XINPUT mask, if you prefer // lt=0 rt=255 triggers, 0..255 // lx=0 ly=0 left thumb, -32768..32767 // rx=0 ry=0 right thumb, -32768..32767 // // Anything absent is neutral, so `press=A` alone is a valid file. An empty or // missing file means "no input" — which is also the safe default if the file is // deleted mid-run. // // Path: `--pad_file=`, default `/tmp/xenia_pad.txt`. Only user 0 is // connected; other slots report no device, as a single-pad console would. #include #include #include #include #include #include #include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/hid/input_driver.h" DECLARE_string(pad_file); namespace xe { namespace hid { namespace filepad { struct NamedButton { const char* name; uint16_t mask; }; static constexpr NamedButton kButtonNames[] = { {"UP", X_INPUT_GAMEPAD_DPAD_UP}, {"DOWN", X_INPUT_GAMEPAD_DPAD_DOWN}, {"LEFT", X_INPUT_GAMEPAD_DPAD_LEFT}, {"RIGHT", X_INPUT_GAMEPAD_DPAD_RIGHT}, {"START", X_INPUT_GAMEPAD_START}, {"BACK", X_INPUT_GAMEPAD_BACK}, {"LS", X_INPUT_GAMEPAD_LEFT_THUMB}, {"RS", X_INPUT_GAMEPAD_RIGHT_THUMB}, {"LB", X_INPUT_GAMEPAD_LEFT_SHOULDER}, {"RB", X_INPUT_GAMEPAD_RIGHT_SHOULDER}, {"A", X_INPUT_GAMEPAD_A}, {"B", X_INPUT_GAMEPAD_B}, {"X", X_INPUT_GAMEPAD_X}, {"Y", X_INPUT_GAMEPAD_Y}, }; class FileInputDriver final : public InputDriver { public: FileInputDriver(xe::ui::Window* window, size_t window_z_order) : InputDriver(window, window_z_order) {} ~FileInputDriver() override = default; X_STATUS Setup() override { XELOGI("[file-pad] reading controller state from {}", cvars::pad_file); return X_STATUS_SUCCESS; } X_RESULT GetCapabilities(uint32_t user_index, uint32_t flags, X_INPUT_CAPABILITIES* out_caps) override { if (user_index != 0) { return X_ERROR_DEVICE_NOT_CONNECTED; } std::memset(out_caps, 0, sizeof(*out_caps)); out_caps->type = 0x01; // XINPUT_DEVTYPE_GAMEPAD out_caps->sub_type = 0x01; // XINPUT_DEVSUBTYPE_GAMEPAD out_caps->flags = 0; out_caps->gamepad.buttons = 0xFFFF; out_caps->gamepad.left_trigger = 0xFF; out_caps->gamepad.right_trigger = 0xFF; out_caps->gamepad.thumb_lx = static_cast(0xFFFFu); out_caps->gamepad.thumb_ly = static_cast(0xFFFFu); out_caps->gamepad.thumb_rx = static_cast(0xFFFFu); out_caps->gamepad.thumb_ry = static_cast(0xFFFFu); return X_ERROR_SUCCESS; } X_RESULT GetState(uint32_t user_index, X_INPUT_STATE* out_state) override { if (user_index != 0) { return X_ERROR_DEVICE_NOT_CONNECTED; } Refresh(); std::memset(out_state, 0, sizeof(*out_state)); out_state->packet_number = packet_; out_state->gamepad.buttons = buttons_; out_state->gamepad.left_trigger = lt_; out_state->gamepad.right_trigger = rt_; out_state->gamepad.thumb_lx = lx_; out_state->gamepad.thumb_ly = ly_; out_state->gamepad.thumb_rx = rx_; out_state->gamepad.thumb_ry = ry_; return X_ERROR_SUCCESS; } X_RESULT SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) override { return user_index == 0 ? X_ERROR_SUCCESS : X_ERROR_DEVICE_NOT_CONNECTED; } X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags, X_INPUT_KEYSTROKE* out_keystroke) override { return X_ERROR_EMPTY; } InputType GetInputType() const override { return InputType::Controller; } private: // Re-parse only when the file actually changed: `GetState` is polled every // frame and a stat is far cheaper than a read+parse. void Refresh() { struct stat st; if (::stat(cvars::pad_file.c_str(), &st) != 0) { Neutral(); return; } if (st.st_mtime == mtime_ && st.st_size == size_) { return; } mtime_ = st.st_mtime; size_ = st.st_size; std::FILE* f = std::fopen(cvars::pad_file.c_str(), "rb"); if (!f) { Neutral(); return; } char buf[512] = {0}; size_t n = std::fread(buf, 1, sizeof(buf) - 1, f); std::fclose(f); buf[n] = '\0'; Parse(buf); ++packet_; } void Neutral() { buttons_ = 0; lt_ = rt_ = 0; lx_ = ly_ = rx_ = ry_ = 0; } void Parse(const char* text) { Neutral(); std::string s(text); size_t pos = 0; while (pos < s.size()) { size_t end = s.find_first_of(" \t\r\n", pos); if (end == std::string::npos) { end = s.size(); } std::string tok = s.substr(pos, end - pos); pos = end + 1; size_t eq = tok.find('='); if (eq == std::string::npos) { continue; } std::string key = tok.substr(0, eq), val = tok.substr(eq + 1); if (key == "press") { size_t p = 0; while (p < val.size()) { size_t c = val.find(',', p); if (c == std::string::npos) { c = val.size(); } std::string name = val.substr(p, c - p); p = c + 1; for (const auto& b : kButtonNames) { if (name == b.name) { buttons_ |= b.mask; break; } } } } else if (key == "buttons") { buttons_ |= static_cast(std::strtoul(val.c_str(), nullptr, 0)); } else if (key == "lt") { lt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0)); } else if (key == "rt") { rt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0)); } else if (key == "lx") { lx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); } else if (key == "ly") { ly_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); } else if (key == "rx") { rx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); } else if (key == "ry") { ry_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); } } } static uint8_t Clamp8(long v) { return static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); } static int16_t Clamp16(long v) { return static_cast(v < -32768 ? -32768 : (v > 32767 ? 32767 : v)); } uint16_t buttons_ = 0; uint8_t lt_ = 0, rt_ = 0; int16_t lx_ = 0, ly_ = 0, rx_ = 0, ry_ = 0; uint32_t packet_ = 1; time_t mtime_ = 0; off_t size_ = -1; }; inline std::unique_ptr Create(xe::ui::Window* window, size_t window_z_order) { return std::make_unique(window, window_z_order); } } // namespace filepad } // namespace hid } // namespace xe #endif // XENIA_HID_FILE_FILE_INPUT_DRIVER_H_