From 870168083a1a342e2ca69da0a6fd9b5b66324beb Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 14:52:44 +0800 Subject: [PATCH 01/11] feat: add Gamescope controller CMake scaffolding and C API - Extract VkToEvdev.h to shared Common include - Add CMake build scaffolding for MaaGamescopeControlUnit - Declare GamescopeControlUnitAPI C header and implement exports - Update MaaController.h, ControlUnitAPI.h for Gamescope support - Add module export in MaaFramework.cppm --- CMakeLists.txt | 6 ++++ include/MaaControlUnit/ControlUnitAPI.h | 10 ++++++ .../MaaControlUnit/GamescopeControlUnitAPI.h | 20 +++++++++++ include/MaaFramework/Instance/MaaController.h | 13 +++++++ source/CMakeLists.txt | 4 +++ source/MaaFramework/API/MaaFramework.cpp | 27 ++++++++++++++ .../API/GamescopeControlUnitAPI.cpp | 35 +++++++++++++++++++ source/MaaGamescopeControlUnit/CMakeLists.txt | 28 +++++++++++++++ .../Client => include/Common}/VkToEvdev.h | 0 source/modules/MaaFramework.cppm | 1 + 10 files changed, 144 insertions(+) create mode 100644 include/MaaControlUnit/GamescopeControlUnitAPI.h create mode 100644 source/MaaGamescopeControlUnit/API/GamescopeControlUnitAPI.cpp create mode 100644 source/MaaGamescopeControlUnit/CMakeLists.txt rename source/{MaaWlRootsControlUnit/Client => include/Common}/VkToEvdev.h (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f5d5c5be6b..fb87045c11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(WITH_CUSTOM_CONTROLLER "build with custom controller" ON) option(WITH_PLAYCOVER_CONTROLLER "build with PlayCover controller for macOS" ON) option(WITH_GAMEPAD_CONTROLLER "build with virtual gamepad controller for Windows" ON) option(WITH_WLROOTS_CONTROLLER "build with wlroots controller for Linux" ON) +option(WITH_GAMESCOPE_CONTROLLER "build with gamescope controller for Linux" ON) option(WITH_RECORD_CONTROLLER "build with record controller for recording" ON) option(WITH_REPLAY_CONTROLLER "build with replay controller" ON) option(WITH_DBG_CONTROLLER "build with debug controller" OFF) @@ -80,6 +81,11 @@ if(WITH_WLROOTS_CONTROLLER AND (NOT LINUX OR ANDROID)) set(WITH_WLROOTS_CONTROLLER OFF) endif() +if(WITH_GAMESCOPE_CONTROLLER AND (NOT LINUX OR ANDROID)) + message(STATUS "Not on Linux, disable WITH_GAMESCOPE_CONTROLLER") + set(WITH_GAMESCOPE_CONTROLLER OFF) +endif() + if(WITH_MAA_AGENT) find_package(cppzmq REQUIRED) endif() diff --git a/include/MaaControlUnit/ControlUnitAPI.h b/include/MaaControlUnit/ControlUnitAPI.h index bb14219834..af1456331a 100644 --- a/include/MaaControlUnit/ControlUnitAPI.h +++ b/include/MaaControlUnit/ControlUnitAPI.h @@ -123,6 +123,15 @@ class WlRootsControlUnitAPI virtual ~WlRootsControlUnitAPI() = default; }; +class GamescopeControlUnitAPI + : public ControlUnitAPI + , public ScrollableUnit + , public RelativeMovableUnit +{ +public: + virtual ~GamescopeControlUnitAPI() = default; +}; + class CustomControlUnitAPI : public ControlUnitAPI , public ScrollableUnit @@ -156,6 +165,7 @@ using MaaAdbControlUnitHandle = MAA_CTRL_UNIT_NS::AdbControlUnitAPI*; using MaaWin32ControlUnitHandle = MAA_CTRL_UNIT_NS::Win32ControlUnitAPI*; using MaaMacOSControlUnitHandle = MAA_CTRL_UNIT_NS::MacOSControlUnitAPI*; using MaaWlRootsControlUnitHandle = MAA_CTRL_UNIT_NS::WlRootsControlUnitAPI*; +using MaaGamescopeControlUnitHandle = MAA_CTRL_UNIT_NS::GamescopeControlUnitAPI*; using MaaGamepadControlUnitHandle = MAA_CTRL_UNIT_NS::GamepadControlUnitAPI*; using MaaCustomControlUnitHandle = MAA_CTRL_UNIT_NS::CustomControlUnitAPI*; using MaaReplayControlUnitHandle = MAA_CTRL_UNIT_NS::FullControlUnitAPI*; diff --git a/include/MaaControlUnit/GamescopeControlUnitAPI.h b/include/MaaControlUnit/GamescopeControlUnitAPI.h new file mode 100644 index 0000000000..9727646bb5 --- /dev/null +++ b/include/MaaControlUnit/GamescopeControlUnitAPI.h @@ -0,0 +1,20 @@ +#pragma once + +#include "MaaControlUnit/ControlUnitAPI.h" +#include "MaaFramework/MaaDef.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + MAA_CONTROL_UNIT_API const char* MaaGamescopeControlUnitGetVersion(); + + MAA_CONTROL_UNIT_API MaaGamescopeControlUnitHandle + MaaGamescopeControlUnitCreate(uint32_t node_id, const char* eis_socket_path, MaaBool use_win32_vk_code); + + MAA_CONTROL_UNIT_API void MaaGamescopeControlUnitDestroy(MaaGamescopeControlUnitHandle handle); + +#ifdef __cplusplus +} +#endif diff --git a/include/MaaFramework/Instance/MaaController.h b/include/MaaFramework/Instance/MaaController.h index 1716135650..018c5e1b5a 100644 --- a/include/MaaFramework/Instance/MaaController.h +++ b/include/MaaFramework/Instance/MaaController.h @@ -128,6 +128,19 @@ extern "C" */ MAA_FRAMEWORK_API MaaController* MaaWlRootsControllerCreate(const char* wlr_socket_path, MaaBool use_win32_vk_code); + /** + * @brief Create a Gamescope controller for Linux. + * + * @param node_id PipeWire node ID for the gamescope output stream. + * @param eis_socket_path EIS socket path for libei keyboard/mouse emulation. + * @param use_win32_vk_code If true, key codes are Win32 VK codes translated to evdev internally. + * @return The controller handle, or nullptr on failure. + * + * @note This controller is designed for Gamescope on Linux. + * @note Requires libpipewire >= 0.3.50 and libei >= 1.0. + */ + MAA_FRAMEWORK_API MaaController* MaaGamescopeControllerCreate(uint32_t node_id, const char* eis_socket_path, MaaBool use_win32_vk_code); + /** * @brief Create a virtual gamepad controller for Windows. * diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 3569c4cea3..24bb958e73 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -42,6 +42,10 @@ if(WITH_WLROOTS_CONTROLLER) add_subdirectory(MaaWlRootsControlUnit) endif() +if(WITH_GAMESCOPE_CONTROLLER) + add_subdirectory(MaaGamescopeControlUnit) +endif() + add_subdirectory(LibraryHolder) add_subdirectory(MaaFramework) add_subdirectory(MaaToolkit) diff --git a/source/MaaFramework/API/MaaFramework.cpp b/source/MaaFramework/API/MaaFramework.cpp index f77b85b317..6c6c7ca173 100644 --- a/source/MaaFramework/API/MaaFramework.cpp +++ b/source/MaaFramework/API/MaaFramework.cpp @@ -283,6 +283,33 @@ MaaController* MaaWlRootsControllerCreate(const char* wlr_socket_path, MaaBool u #endif } +MaaController* MaaGamescopeControllerCreate(uint32_t node_id, const char* eis_socket_path, MaaBool use_win32_vk_code) +{ + LogFunc << VAR(node_id) << VAR(eis_socket_path) << VAR(use_win32_vk_code); + +#ifndef __linux__ + + LogError << "This API " << __FUNCTION__ << " is only available on Linux"; + return nullptr; + +#else + + if (!eis_socket_path) { + LogError << "eis_socket_path is null"; + return nullptr; + } + + auto control_unit = MAA_NS::GamescopeControlUnitLibraryHolder::create_control_unit(node_id, eis_socket_path, use_win32_vk_code); + + if (!control_unit) { + LogError << "Failed to create control unit"; + return nullptr; + } + + return new MAA_CTRL_NS::ControllerAgent(std::move(control_unit)); +#endif +} + void MaaControllerDestroy(MaaController* ctrl) { LogFunc << VAR_VOIDP(ctrl); diff --git a/source/MaaGamescopeControlUnit/API/GamescopeControlUnitAPI.cpp b/source/MaaGamescopeControlUnit/API/GamescopeControlUnitAPI.cpp new file mode 100644 index 0000000000..78e959ab96 --- /dev/null +++ b/source/MaaGamescopeControlUnit/API/GamescopeControlUnitAPI.cpp @@ -0,0 +1,35 @@ +#include "MaaControlUnit/GamescopeControlUnitAPI.h" + +#include "MaaUtils/Logger.h" +#include "Manager/GamescopeControlUnitMgr.h" + +const char* MaaGamescopeControlUnitGetVersion() +{ +#pragma message("MaaGamescopeControlUnit MAA_VERSION: " MAA_VERSION) + + return MAA_VERSION; +} + +MaaGamescopeControlUnitHandle MaaGamescopeControlUnitCreate(uint32_t node_id, const char* eis_socket_path, MaaBool use_win32_vk_code) +{ + using namespace MAA_CTRL_UNIT_NS; + + LogFunc << VAR(node_id) << VAR(eis_socket_path) << VAR(use_win32_vk_code); + + if (!eis_socket_path) { + LogError << "eis_socket_path is null or empty"; + return nullptr; + } + + auto unit_mgr = std::make_unique(node_id, eis_socket_path, use_win32_vk_code); + return unit_mgr.release(); +} + +void MaaGamescopeControlUnitDestroy(MaaGamescopeControlUnitHandle handle) +{ + LogFunc << VAR_VOIDP(handle); + + if (handle) { + delete handle; + } +} diff --git a/source/MaaGamescopeControlUnit/CMakeLists.txt b/source/MaaGamescopeControlUnit/CMakeLists.txt new file mode 100644 index 0000000000..ef378f6618 --- /dev/null +++ b/source/MaaGamescopeControlUnit/CMakeLists.txt @@ -0,0 +1,28 @@ +file(GLOB_RECURSE maa_gamescope_control_unit_src *.h *.hpp *.cpp) +file(GLOB_RECURSE maa_gamescope_control_unit_header ${MAA_PUBLIC_INC}/MaaControlUnit/GamescopeControlUnitAPI.h ${MAA_PUBLIC_INC}/MaaControlUnit/ControlUnitAPI.h) + +add_library(MaaGamescopeControlUnit SHARED ${maa_gamescope_control_unit_src} ${maa_gamescope_control_unit_header}) + +target_include_directories(MaaGamescopeControlUnit + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${MAA_PRIVATE_INC} ${MAA_PUBLIC_INC}) + +find_path(PipeWire_INCLUDE_DIR pipewire/pipewire.h PATH_SUFFIXES pipewire-0.3) +find_path(Spa_INCLUDE_DIR spa/param/video/format-utils.h PATH_SUFFIXES spa-0.2) +find_library(PipeWire_LIBS pipewire-0.3) +find_path(LibEI_INCLUDE_DIR libei.h PATH_SUFFIXES libei-1.0) +find_library(LibEI_LIBS ei) + +target_include_directories(MaaGamescopeControlUnit SYSTEM PRIVATE ${PipeWire_INCLUDE_DIR} ${Spa_INCLUDE_DIR} ${LibEI_INCLUDE_DIR}) +target_link_libraries(MaaGamescopeControlUnit PRIVATE MaaUtils HeaderOnlyLibraries ${OpenCV_LIBS} ${PipeWire_LIBS} ${LibEI_LIBS}) + +target_compile_definitions(MaaGamescopeControlUnit PRIVATE MAA_CONTROL_UNIT_EXPORTS) + +add_dependencies(MaaGamescopeControlUnit MaaUtils) + +install( + TARGETS MaaGamescopeControlUnit + RUNTIME DESTINATION bin + LIBRARY DESTINATION bin +) + +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${maa_gamescope_control_unit_src}) diff --git a/source/MaaWlRootsControlUnit/Client/VkToEvdev.h b/source/include/Common/VkToEvdev.h similarity index 100% rename from source/MaaWlRootsControlUnit/Client/VkToEvdev.h rename to source/include/Common/VkToEvdev.h diff --git a/source/modules/MaaFramework.cppm b/source/modules/MaaFramework.cppm index fbf48d3d55..e0042b2dd3 100644 --- a/source/modules/MaaFramework.cppm +++ b/source/modules/MaaFramework.cppm @@ -160,6 +160,7 @@ export using ::MaaReplayControllerCreate; export using ::MaaRecordControllerCreate; export using ::MaaPlayCoverControllerCreate; export using ::MaaWlRootsControllerCreate; +export using ::MaaGamescopeControllerCreate; export using ::MaaGamepadControllerCreate; export using ::MaaControllerDestroy; export using ::MaaControllerAddSink; From b0b09bd90ec5c067601a05b2e608fff34fec255c Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 14:52:51 +0800 Subject: [PATCH 02/11] feat: implement PipeWireScreencap for Gamescope controller PipeWire-based screen capture using pw_thread_loop, supporting BGR/RGB/RGBA/BGRA/YUY2 formats with SPA pod negotiation. --- .../Client/PipeWireScreencap.cpp | 314 ++++++++++++++++++ .../Client/PipeWireScreencap.h | 57 ++++ 2 files changed, 371 insertions(+) create mode 100644 source/MaaGamescopeControlUnit/Client/PipeWireScreencap.cpp create mode 100644 source/MaaGamescopeControlUnit/Client/PipeWireScreencap.h diff --git a/source/MaaGamescopeControlUnit/Client/PipeWireScreencap.cpp b/source/MaaGamescopeControlUnit/Client/PipeWireScreencap.cpp new file mode 100644 index 0000000000..f8c3351bd8 --- /dev/null +++ b/source/MaaGamescopeControlUnit/Client/PipeWireScreencap.cpp @@ -0,0 +1,314 @@ +#include "PipeWireScreencap.h" + +#include + +#include + +#include +#include + +#include "MaaUtils/Logger.h" + +MAA_CTRL_UNIT_NS_BEGIN + +struct SpaFormatInfo +{ + enum spa_video_format format; + int channels; + int cv_conversion; +}; + +static constexpr SpaFormatInfo kSpaFormatTable[] = { + { SPA_VIDEO_FORMAT_BGRA, 4, cv::COLOR_BGRA2BGR }, { SPA_VIDEO_FORMAT_BGRx, 4, cv::COLOR_BGRA2BGR }, + { SPA_VIDEO_FORMAT_RGBA, 4, cv::COLOR_RGBA2BGR }, { SPA_VIDEO_FORMAT_RGBx, 4, cv::COLOR_RGBA2BGR }, + { SPA_VIDEO_FORMAT_RGB, 3, cv::COLOR_RGB2BGR }, { SPA_VIDEO_FORMAT_BGR, 3, -1 }, + { SPA_VIDEO_FORMAT_YUY2, 2, cv::COLOR_YUV2BGR_YUY2 }, +}; + +static const SpaFormatInfo* spa_format_info(enum spa_video_format format) +{ + for (const auto& info : kSpaFormatTable) { + if (info.format == format) { + return &info; + } + } + return nullptr; +} + +static const struct spa_pod* build_format_pod(struct spa_pod_builder* b) +{ + static constexpr struct spa_rectangle kDefRect = { 1280, 720 }; + static constexpr struct spa_rectangle kMinRect = { 1, 1 }; + static constexpr struct spa_rectangle kMaxRect = { 4096, 4096 }; + static constexpr struct spa_fraction kDefFps = { 30, 1 }; + static constexpr struct spa_fraction kMinFps = { 0, 1 }; + static constexpr struct spa_fraction kMaxFps = { 1000, 1 }; + + return (const struct spa_pod*)spa_pod_builder_add_object( + b, + SPA_TYPE_OBJECT_Format, + SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, + SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, + SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, + SPA_POD_CHOICE_ENUM_Id( + 7, + SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGBA, + SPA_VIDEO_FORMAT_RGBx, + SPA_VIDEO_FORMAT_BGR, + SPA_VIDEO_FORMAT_BGRA, + SPA_VIDEO_FORMAT_BGRx, + SPA_VIDEO_FORMAT_YUY2), + SPA_FORMAT_VIDEO_size, + SPA_POD_CHOICE_RANGE_Rectangle(&kDefRect, &kMinRect, &kMaxRect), + SPA_FORMAT_VIDEO_framerate, + SPA_POD_CHOICE_RANGE_Fraction(&kDefFps, &kMinFps, &kMaxFps), + 0); +} + +static const struct spa_pod* build_buffer_pod(struct spa_pod_builder* b) +{ + int32_t data_types = (1 << SPA_DATA_MemFd) | (1 << SPA_DATA_MemPtr); + return (const struct spa_pod*)spa_pod_builder_add_object( + b, + SPA_TYPE_OBJECT_ParamBuffers, + SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_dataType, + SPA_POD_Int(data_types), + 0); +} + +PipeWireScreencap::PipeWireScreencap(uint32_t node_id) + : node_id_(node_id) +{ + LogFunc << VAR(node_id_); +} + +PipeWireScreencap::~PipeWireScreencap() +{ + LogFunc; + shutdown(); +} + +bool PipeWireScreencap::init() +{ + LogFunc; + + pw_init(nullptr, nullptr); + + tloop_ = pw_thread_loop_new("maa-gs-screencap", nullptr); + if (!tloop_) { + LogError << "pw_thread_loop_new failed"; + return false; + } + + struct pw_loop* loop = pw_thread_loop_get_loop(tloop_); + + uint8_t pod_buf[4096]; + spa_pod_builder b = SPA_POD_BUILDER_INIT(pod_buf, sizeof(pod_buf)); + const struct spa_pod* fmt_pod = build_format_pod(&b); + const struct spa_pod* buf_pod = build_buffer_pod(&b); + + const struct spa_pod* stream_params[] = { fmt_pod, buf_pod }; + struct pw_properties* stream_props = + pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr); + + stream_ = pw_stream_new_simple(loop, "maa-gs-screencap", stream_props, &stream_events_, this); + if (!stream_) { + LogError << "pw_stream_new_simple failed"; + return false; + } + + int rc = pw_stream_connect( + stream_, + PW_DIRECTION_INPUT, + node_id_, + static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), + stream_params, + 2); + if (rc < 0) { + LogError << "pw_stream_connect failed" << VAR(rc); + return false; + } + + pw_thread_loop_lock(tloop_); + + if (pw_thread_loop_start(tloop_) < 0) { + pw_thread_loop_unlock(tloop_); + LogError << "pw_thread_loop_start failed"; + return false; + } + + pw_thread_loop_timed_wait(tloop_, 3); + if (width_ == 0) { + pw_thread_loop_unlock(tloop_); + LogError << "format negotiation timed out"; + return false; + } + + pw_thread_loop_unlock(tloop_); + + connected_ = true; + LogInfo << "PipeWire screencap ready" << VAR(width_) << VAR(height_); + return true; +} + +bool PipeWireScreencap::screencap(cv::Mat& image, std::chrono::milliseconds timeout) +{ + if (!tloop_) { + LogError << "not initialized"; + return false; + } + + pw_thread_loop_lock(tloop_); + + capture_requested_ = true; + + int timeout_sec = static_cast((timeout.count() + 999) / 1000); + pw_thread_loop_timed_wait(tloop_, timeout_sec); + + if (capture_requested_) { + capture_requested_ = false; + pw_thread_loop_unlock(tloop_); + LogWarn << "screencap timeout"; + return false; + } + + cv::Mat frame = std::move(latest_frame_); + pw_thread_loop_unlock(tloop_); + + const SpaFormatInfo* fmt_info = spa_format_info(video_format_); + if (!fmt_info) { + LogError << "unsupported video format" << VAR(video_format_); + return false; + } + + if (fmt_info->cv_conversion < 0) { + image = std::move(frame); + } + else { + cv::cvtColor(frame, image, fmt_info->cv_conversion); + } + return true; +} + +void PipeWireScreencap::shutdown() +{ + if (!tloop_) { + return; + } + + pw_thread_loop_lock(tloop_); + pw_thread_loop_signal(tloop_, false); + pw_thread_loop_unlock(tloop_); + + pw_thread_loop_stop(tloop_); + + if (stream_) { + pw_stream_destroy(stream_); + stream_ = nullptr; + } + + pw_thread_loop_destroy(tloop_); + tloop_ = nullptr; + + pw_deinit(); + LogInfo << "PipeWire shutdown complete"; +} + +void PipeWireScreencap::on_param_changed(void* data, uint32_t id, const struct spa_pod* param) +{ + if (id != SPA_PARAM_Format || param == nullptr) { + return; + } + + auto* self = static_cast(data); + + struct spa_video_info info = {}; + if (spa_format_video_raw_parse(param, &info.info.raw) < 0) { + LogError << "spa_format_video_raw_parse failed"; + return; + } + + const SpaFormatInfo* fmt_info = spa_format_info(info.info.raw.format); + if (!fmt_info) { + LogError << "Unsupported video format" << VAR(info.info.raw.format); + return; + } + + self->width_ = info.info.raw.size.width; + self->height_ = info.info.raw.size.height; + self->video_format_ = info.info.raw.format; + self->channels_ = fmt_info->channels; + + pw_thread_loop_signal(self->tloop_, false); + + LogInfo << "format negotiated" << VAR(self->width_) << VAR(self->height_) + << VAR(spa_debug_type_find_name(spa_type_video_format, self->video_format_)); +} + +void PipeWireScreencap::on_process(void* data) +{ + auto* self = static_cast(data); + + struct pw_buffer* latest = nullptr; + while (struct pw_buffer* b = pw_stream_dequeue_buffer(self->stream_)) { + if (latest) { + pw_stream_queue_buffer(self->stream_, latest); + } + latest = b; + } + + if (!latest) { + return; + } + + if (!self->capture_requested_) { + pw_stream_queue_buffer(self->stream_, latest); + return; + } + + struct spa_buffer* spa_buf = latest->buffer; + if (!spa_buf || spa_buf->n_datas < 1) { + pw_stream_queue_buffer(self->stream_, latest); + return; + } + + struct spa_data* plane = &spa_buf->datas[0]; + if (!plane->data || plane->chunk->size == 0) { + LogWarn << "PipeWire screencap: empty buffer data"; + pw_stream_queue_buffer(self->stream_, latest); + return; + } + + struct spa_meta_header* header = + static_cast(spa_buffer_find_meta_data(spa_buf, SPA_META_Header, sizeof(*header))); + if (header && (header->flags & SPA_META_HEADER_FLAG_CORRUPTED)) { + LogWarn << "PipeWire screencap: corrupted frame"; + pw_stream_queue_buffer(self->stream_, latest); + return; + } + + int row_bytes = self->width_ * self->channels_; + int stride = plane->chunk->stride > 0 ? static_cast(plane->chunk->stride) : row_bytes; + self->latest_frame_.create(self->height_, self->width_, CV_8UC(self->channels_)); + + auto* src = static_cast(plane->data); + if (stride == row_bytes) { + std::memcpy(self->latest_frame_.data, src, self->height_ * stride); + } + else { + for (int row = 0; row < self->height_; ++row) { + std::memcpy(self->latest_frame_.ptr(row), src + row * stride, row_bytes); + } + } + + self->capture_requested_ = false; + pw_stream_queue_buffer(self->stream_, latest); + pw_thread_loop_signal(self->tloop_, false); +} + +MAA_CTRL_UNIT_NS_END diff --git a/source/MaaGamescopeControlUnit/Client/PipeWireScreencap.h b/source/MaaGamescopeControlUnit/Client/PipeWireScreencap.h new file mode 100644 index 0000000000..da53960b17 --- /dev/null +++ b/source/MaaGamescopeControlUnit/Client/PipeWireScreencap.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "Common/Conf.h" + +MAA_CTRL_UNIT_NS_BEGIN + +class PipeWireScreencap +{ +public: + explicit PipeWireScreencap(uint32_t node_id); + ~PipeWireScreencap(); + + PipeWireScreencap(const PipeWireScreencap&) = delete; + PipeWireScreencap& operator=(const PipeWireScreencap&) = delete; + + bool init(); + bool screencap(cv::Mat& image, std::chrono::milliseconds timeout = std::chrono::seconds(5)); + void shutdown(); + + bool connected() const { return connected_; } + + int width() const { return width_; } + + int height() const { return height_; } + +private: + static void on_param_changed(void* data, uint32_t id, const struct spa_pod* param); + static void on_process(void* data); + + uint32_t node_id_; + + struct pw_thread_loop* tloop_ = nullptr; + struct pw_stream* stream_ = nullptr; + + struct pw_stream_events stream_events_ = { .version = PW_VERSION_STREAM_EVENTS, + .param_changed = on_param_changed, + .process = on_process }; + + bool connected_ = false; + + int width_ = 0; + int height_ = 0; + enum spa_video_format video_format_ = SPA_VIDEO_FORMAT_UNKNOWN; + int channels_ = 0; + + cv::Mat latest_frame_; + bool capture_requested_ = false; +}; + +MAA_CTRL_UNIT_NS_END From ec0506717e0beabd5628868f2ced0ad6a7d4e91a Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 14:53:37 +0800 Subject: [PATCH 03/11] feat: implement EiInput for Gamescope controller libei-based input sender supporting pointer, keyboard, and scroll events via ei_device_* primitives with synchronous poll_and_dispatch loop. --- .../Client/EiInput.cpp | 238 ++++++++++++++++++ .../MaaGamescopeControlUnit/Client/EiInput.h | 62 +++++ 2 files changed, 300 insertions(+) create mode 100644 source/MaaGamescopeControlUnit/Client/EiInput.cpp create mode 100644 source/MaaGamescopeControlUnit/Client/EiInput.h diff --git a/source/MaaGamescopeControlUnit/Client/EiInput.cpp b/source/MaaGamescopeControlUnit/Client/EiInput.cpp new file mode 100644 index 0000000000..e09cde2616 --- /dev/null +++ b/source/MaaGamescopeControlUnit/Client/EiInput.cpp @@ -0,0 +1,238 @@ +#include "EiInput.h" + +#include +#include +#include + +#include + +#include "MaaUtils/Logger.h" + +MAA_CTRL_UNIT_NS_BEGIN + +EiInput::EiInput(std::string eis_socket_path) + : eis_socket_path_(std::move(eis_socket_path)) +{ + LogFunc << VAR(eis_socket_path_); +} + +EiInput::~EiInput() +{ + LogFunc; + shutdown(); +} + +bool EiInput::init() +{ + LogFunc; + + auto cleanup = [this]() { + if (device_) { + ei_device_unref(device_); + device_ = nullptr; + } + if (ei_) { + ei_unref(ei_); + ei_ = nullptr; + } + return false; + }; + + ei_ = ei_new_sender(nullptr); + if (!ei_) { + LogError << "ei_new_sender failed"; + return false; + } + + ei_configure_name(ei_, "MaaFramework"); + + int rc = ei_setup_backend_socket(ei_, eis_socket_path_.c_str()); + if (rc < 0) { + LogError << "ei_setup_backend_socket failed" << VAR(rc) << VAR(eis_socket_path_); + return cleanup(); + } + + ei_fd_ = ei_get_fd(ei_); + if (ei_fd_ < 0) { + LogError << "ei_get_fd failed"; + return cleanup(); + } + + constexpr int kPollMs = 100; + constexpr int kMaxAttempts = 30; + + for (int i = 0; i < kMaxAttempts && !connected_; ++i) { + int n = poll_and_dispatch(kPollMs); + if (n < 0) { + LogError << "poll failed during init"; + return cleanup(); + } + } + + if (!connected_) { + LogError << (device_ ? "EiInput init timed out" : "No device with keyboard and pointer capabilities"); + return cleanup(); + } + + ei_device_start_emulating(device_, ++emul_seq_); + ei_device_frame(device_, ei_now(ei_)); + + LogInfo << "EiInput ready"; + return true; +} + +void EiInput::shutdown() +{ + if (!ei_) { + return; + } + + connected_ = false; + if (device_) { + ei_device_stop_emulating(device_); + ei_device_unref(device_); + } + device_ = nullptr; + ei_disconnect(ei_); + ei_unref(ei_); + ei_ = nullptr; + ei_fd_ = -1; +} + +int EiInput::poll_and_dispatch(int timeout_ms) +{ + pollfd pfd = { ei_fd_, POLLIN, 0 }; + int n = poll(&pfd, 1, timeout_ms); + while (n < 0 && errno == EINTR) { + n = poll(&pfd, 1, timeout_ms); + } + if (n > 0 && (pfd.revents & POLLIN)) { + ei_dispatch(ei_); + while (auto* event = ei_get_event(ei_)) { + handle_event(event); + ei_event_unref(event); + } + } + return n; +} + +void EiInput::handle_event(struct ei_event* event) +{ + switch (ei_event_get_type(event)) { + case EI_EVENT_CONNECT: + LogInfo << "EI_EVENT_CONNECT"; + break; + case EI_EVENT_SEAT_ADDED: { + struct ei_seat* seat = ei_event_get_seat(event); + ei_seat_bind_capabilities( + seat, + EI_DEVICE_CAP_POINTER, + EI_DEVICE_CAP_POINTER_ABSOLUTE, + EI_DEVICE_CAP_KEYBOARD, + EI_DEVICE_CAP_BUTTON, + EI_DEVICE_CAP_SCROLL, + EI_DEVICE_CAP_TEXT, + nullptr); + break; + } + case EI_EVENT_DEVICE_ADDED: { + struct ei_device* dev = ei_event_get_device(event); + if (ei_device_has_capability(dev, EI_DEVICE_CAP_KEYBOARD) && ei_device_has_capability(dev, EI_DEVICE_CAP_POINTER)) { + if (device_) { + ei_device_unref(device_); + } + device_ = ei_device_ref(dev); + } + break; + } + case EI_EVENT_DEVICE_RESUMED: + if (ei_event_get_device(event) == device_) { + connected_ = true; + } + LogInfo << "EI_EVENT_DEVICE_RESUMED"; + break; + case EI_EVENT_KEYBOARD_MODIFIERS: + break; + case EI_EVENT_DEVICE_PAUSED: + LogInfo << "EI_EVENT_DEVICE_PAUSED"; + break; + case EI_EVENT_DEVICE_REMOVED: + if (ei_event_get_device(event) == device_) { + ei_device_unref(device_); + device_ = nullptr; + connected_ = false; + } + LogInfo << "EI_EVENT_DEVICE_REMOVED"; + break; + case EI_EVENT_SEAT_REMOVED: + LogInfo << "EI_EVENT_SEAT_REMOVED"; + break; + case EI_EVENT_DISCONNECT: + LogWarn << "EI_EVENT_DISCONNECT"; + if (device_) { + ei_device_unref(device_); + device_ = nullptr; + } + connected_ = false; + break; + default: + LogTrace << "Unhandled ei event" << VAR(ei_event_get_type(event)); + break; + } +} + +template +bool EiInput::send(F&& emit) +{ + if (!device_ || !ei_) { + LogError << "send called without a valid device/ei"; + return false; + } + while (poll_and_dispatch(0) > 0) { + } + emit(device_); + ei_device_frame(device_, ei_now(ei_)); + return true; +} + +bool EiInput::pointer(EventPhase phase, int x, int y, int contact) +{ + int btn = (contact == 1) ? BTN_LEFT : (contact == 2) ? BTN_RIGHT : (contact == 3) ? BTN_MIDDLE : BTN_LEFT; + + return send([phase, x, y, btn](struct ei_device* d) { + switch (phase) { + case EventPhase::Began: + ei_device_pointer_motion_absolute(d, static_cast(x), static_cast(y)); + ei_device_button_button(d, btn, true); + break; + case EventPhase::Moved: + ei_device_pointer_motion_absolute(d, static_cast(x), static_cast(y)); + break; + case EventPhase::Ended: + ei_device_button_button(d, btn, false); + break; + } + }); +} + +bool EiInput::keyboard_key(EventPhase phase, int evdev_key) +{ + return send([phase, evdev_key](struct ei_device* d) { ei_device_keyboard_key(d, evdev_key, phase != EventPhase::Ended); }); +} + +bool EiInput::text_utf8(const std::string& text) +{ + return send([&text](struct ei_device* d) { ei_device_text_utf8(d, text.c_str()); }); +} + +bool EiInput::relative_move(int dx, int dy) +{ + return send([dx, dy](struct ei_device* d) { ei_device_pointer_motion(d, static_cast(dx), static_cast(dy)); }); +} + +bool EiInput::scroll(int dx, int dy) +{ + return send([dx, dy](struct ei_device* d) { ei_device_scroll_delta(d, static_cast(dx), static_cast(dy)); }); +} + +MAA_CTRL_UNIT_NS_END diff --git a/source/MaaGamescopeControlUnit/Client/EiInput.h b/source/MaaGamescopeControlUnit/Client/EiInput.h new file mode 100644 index 0000000000..fecce1741c --- /dev/null +++ b/source/MaaGamescopeControlUnit/Client/EiInput.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include "Common/Conf.h" + +struct ei; +struct ei_seat; +struct ei_device; +struct ei_event; + +MAA_CTRL_UNIT_NS_BEGIN + +class EiInput +{ +public: + enum class EventPhase + { + Began, + Moved, + Ended + }; + + explicit EiInput(std::string eis_socket_path); + ~EiInput(); + + EiInput(const EiInput&) = delete; + EiInput& operator=(const EiInput&) = delete; + + bool init(); + + bool connected() const { return connected_; } + + bool pointer(EventPhase phase, int x, int y, int contact); + bool keyboard_key(EventPhase phase, int evdev_key); + bool text_utf8(const std::string& text); + bool relative_move(int dx, int dy); + bool scroll(int dx, int dy); + + void shutdown(); + +private: + int poll_and_dispatch(int timeout_ms); + void handle_event(struct ei_event* event); + + template + bool send(F&& emit); + + struct ei* ei_ = nullptr; + struct ei_device* device_ = nullptr; + + int ei_fd_ = -1; + + std::string eis_socket_path_; + + bool connected_ = false; + + uint64_t emul_seq_ = 0; +}; + +MAA_CTRL_UNIT_NS_END From bb40537d9b04583686835fb5345ed911bb6d8d3d Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 14:53:46 +0800 Subject: [PATCH 04/11] feat: implement GamescopeControlUnitMgr with LibraryHolder factory Control unit manager that composes PipeWireScreencap and EiInput, registered through the LibraryHolder factory pattern. --- source/LibraryHolder/CMakeLists.txt | 4 + .../LibraryHolder/ControlUnit/ControlUnit.cpp | 33 +++ .../Manager/GamescopeControlUnitMgr.cpp | 212 ++++++++++++++++++ .../Manager/GamescopeControlUnitMgr.h | 65 ++++++ .../Manager/WlRootsControlUnitMgr.cpp | 2 +- source/include/LibraryHolder/ControlUnit.h | 14 ++ 6 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.cpp create mode 100644 source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.h diff --git a/source/LibraryHolder/CMakeLists.txt b/source/LibraryHolder/CMakeLists.txt index a3c66ffb10..d4b606fdf6 100644 --- a/source/LibraryHolder/CMakeLists.txt +++ b/source/LibraryHolder/CMakeLists.txt @@ -51,4 +51,8 @@ if(WITH_WLROOTS_CONTROLLER) add_dependencies(LibraryHolder MaaWlRootsControlUnit) endif() +if(WITH_GAMESCOPE_CONTROLLER) + add_dependencies(LibraryHolder MaaGamescopeControlUnit) +endif() + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${library_holder_src}) diff --git a/source/LibraryHolder/ControlUnit/ControlUnit.cpp b/source/LibraryHolder/ControlUnit/ControlUnit.cpp index e671207cfa..ee0674f86f 100644 --- a/source/LibraryHolder/ControlUnit/ControlUnit.cpp +++ b/source/LibraryHolder/ControlUnit/ControlUnit.cpp @@ -7,6 +7,7 @@ #include "MaaControlUnit/CustomControlUnitAPI.h" #include "MaaControlUnit/DbgControlUnitAPI.h" #include "MaaControlUnit/GamepadControlUnitAPI.h" +#include "MaaControlUnit/GamescopeControlUnitAPI.h" #include "MaaControlUnit/MacOSControlUnitAPI.h" #include "MaaControlUnit/PlayCoverControlUnitAPI.h" #include "MaaControlUnit/RecordControlUnitAPI.h" @@ -394,4 +395,36 @@ std::shared_ptr MacOSControlUnitLibraryHo return std::shared_ptr(control_unit_handle, destroy_control_unit_func); } +std::shared_ptr + GamescopeControlUnitLibraryHolder::create_control_unit(uint32_t node_id, const char* eis_socket_path, MaaBool use_win32_vk_code) +{ + if (!load_library(library_dir() / libname_)) { + LogError << "Failed to load library" << VAR(library_dir()) << VAR(libname_); + return nullptr; + } + + check_version(version_func_name_); + + auto create_control_unit_func = get_function(create_func_name_); + if (!create_control_unit_func) { + LogError << "Failed to get function create_control_unit"; + return nullptr; + } + + auto destroy_control_unit_func = get_function(destroy_func_name_); + if (!destroy_control_unit_func) { + LogError << "Failed to get function destroy_control_unit"; + return nullptr; + } + + auto control_unit_handle = create_control_unit_func(node_id, eis_socket_path, use_win32_vk_code); + + if (!control_unit_handle) { + LogError << "Failed to create control unit"; + return nullptr; + } + + return std::shared_ptr(control_unit_handle, destroy_control_unit_func); +} + MAA_NS_END diff --git a/source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.cpp b/source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.cpp new file mode 100644 index 0000000000..c2e204c516 --- /dev/null +++ b/source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.cpp @@ -0,0 +1,212 @@ +#include "GamescopeControlUnitMgr.h" + +#include +#include + +#include "Client/EiInput.h" +#include "Client/PipeWireScreencap.h" +#include "Common/VkToEvdev.h" +#include "MaaUtils/Logger.h" + +MAA_CTRL_UNIT_NS_BEGIN + +GamescopeControlUnitMgr::GamescopeControlUnitMgr(uint32_t node_id, std::string eis_socket_path, bool use_win32_vk_code) + : screencap_(std::make_unique(node_id)) + , input_(std::make_unique(eis_socket_path)) + , eis_socket_path_(std::move(eis_socket_path)) + , node_id_(node_id) + , use_win32_vk_code_(use_win32_vk_code) +{ + LogFunc << VAR(node_id_) << VAR(eis_socket_path_) << VAR(use_win32_vk_code_); +} + +GamescopeControlUnitMgr::~GamescopeControlUnitMgr() +{ + LogFunc; +} + +bool GamescopeControlUnitMgr::connect() +{ + if (!screencap_->init()) { + LogError << "PipeWire screencap init failed"; + return false; + } + if (!input_->init()) { + LogError << "EiInput init failed"; + return false; + } + return true; +} + +bool GamescopeControlUnitMgr::connected() const +{ + return screencap_ && screencap_->connected() && input_ && input_->connected(); +} + +bool GamescopeControlUnitMgr::request_uuid(std::string& uuid) +{ + std::error_code ec; + auto mtime = std::filesystem::last_write_time(eis_socket_path_, ec); + uuid = std::format("gamescope-{}-{}", node_id_, ec ? 0 : mtime.time_since_epoch().count()); + return true; +} + +MaaControllerFeature GamescopeControlUnitMgr::get_features() const +{ + return MaaControllerFeature( + MaaControllerFeature_UseMouseDownAndUpInsteadOfClick | MaaControllerFeature_UseKeyboardDownAndUpInsteadOfClick); +} + +bool GamescopeControlUnitMgr::start_app(const std::string& /*intent*/) +{ + return false; +} + +bool GamescopeControlUnitMgr::stop_app(const std::string& /*intent*/) +{ + return false; +} + +bool GamescopeControlUnitMgr::screencap(cv::Mat& image) +{ + if (!screencap_) { + LogError << "screencap_ is null"; + return false; + } + return screencap_->screencap(image); +} + +int GamescopeControlUnitMgr::translate_key(int key) const +{ + if (!use_win32_vk_code_) { + return key; + } + int ev = vk_to_evdev(key); + if (ev == 0) { + LogWarn << "No evdev mapping for VK" << VAR(key); + } + return ev; +} + +bool GamescopeControlUnitMgr::click(int x, int y) +{ + LogError << "deprecated: get_features() returns MaaControllerFeature_UseMouseDownAndUpInsteadOfClick, " + "use touch_down/touch_up instead" + << VAR(x) << VAR(y); + return false; +} + +bool GamescopeControlUnitMgr::swipe(int x1, int y1, int x2, int y2, int duration) +{ + LogError << "deprecated: get_features() returns MaaControllerFeature_UseMouseDownAndUpInsteadOfClick, " + "use touch_down/touch_move/touch_up instead" + << VAR(x1) << VAR(y1) << VAR(x2) << VAR(y2) << VAR(duration); + return false; +} + +bool GamescopeControlUnitMgr::touch_down(int contact, int x, int y, int pressure) +{ + std::ignore = pressure; + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->pointer(EiInput::EventPhase::Began, x, y, contact); +} + +bool GamescopeControlUnitMgr::touch_move(int contact, int x, int y, int pressure) +{ + std::ignore = pressure; + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->pointer(EiInput::EventPhase::Moved, x, y, contact); +} + +bool GamescopeControlUnitMgr::touch_up(int contact) +{ + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->pointer(EiInput::EventPhase::Ended, 0, 0, contact); +} + +bool GamescopeControlUnitMgr::click_key(int key) +{ + LogError << "deprecated: get_features() returns MaaControllerFeature_UseKeyboardDownAndUpInsteadOfClick, " + "use key_down/key_up instead" + << VAR(key); + return false; +} + +bool GamescopeControlUnitMgr::input_text(const std::string& text) +{ + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->text_utf8(text); +} + +bool GamescopeControlUnitMgr::key_down(int key) +{ + int ev = translate_key(key); + if (!ev) { + return false; + } + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->keyboard_key(EiInput::EventPhase::Began, ev); +} + +bool GamescopeControlUnitMgr::key_up(int key) +{ + int ev = translate_key(key); + if (!ev) { + return false; + } + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->keyboard_key(EiInput::EventPhase::Ended, ev); +} + +bool GamescopeControlUnitMgr::relative_move(int dx, int dy) +{ + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->relative_move(dx, dy); +} + +bool GamescopeControlUnitMgr::scroll(int dx, int dy) +{ + if (!input_) { + LogError << "input_ is null"; + return false; + } + return input_->scroll(dx, dy); +} + +bool GamescopeControlUnitMgr::inactive() +{ + return true; +} + +json::object GamescopeControlUnitMgr::get_info() const +{ + return { + { "type", "gamescope" }, + { "node_id", static_cast(node_id_) }, + { "eis_socket_path", eis_socket_path_ }, + { "use_win32_vk_code", use_win32_vk_code_ }, + }; +} + +MAA_CTRL_UNIT_NS_END diff --git a/source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.h b/source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.h new file mode 100644 index 0000000000..dee05f5010 --- /dev/null +++ b/source/MaaGamescopeControlUnit/Manager/GamescopeControlUnitMgr.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +#include "MaaControlUnit/ControlUnitAPI.h" +#include "MaaFramework/MaaDef.h" + +#include "Common/Conf.h" + +MAA_CTRL_UNIT_NS_BEGIN + +class PipeWireScreencap; +class EiInput; + +class GamescopeControlUnitMgr : public GamescopeControlUnitAPI +{ +public: + GamescopeControlUnitMgr(uint32_t node_id, std::string eis_socket_path, bool use_win32_vk_code); + virtual ~GamescopeControlUnitMgr() override; + +public: + virtual bool connect() override; + virtual bool connected() const override; + + virtual bool request_uuid(std::string& uuid) override; + virtual MaaControllerFeature get_features() const override; + + virtual bool start_app(const std::string& intent) override; + virtual bool stop_app(const std::string& intent) override; + + virtual bool screencap(cv::Mat& image) override; + + virtual bool click(int x, int y) override; + virtual bool swipe(int x1, int y1, int x2, int y2, int duration) override; + + virtual bool touch_down(int contact, int x, int y, int pressure) override; + virtual bool touch_move(int contact, int x, int y, int pressure) override; + virtual bool touch_up(int contact) override; + + virtual bool click_key(int key) override; + virtual bool input_text(const std::string& text) override; + + virtual bool key_down(int key) override; + virtual bool key_up(int key) override; + + virtual bool relative_move(int dx, int dy) override; + virtual bool scroll(int dx, int dy) override; + + virtual bool inactive() override; + + virtual json::object get_info() const override; + +private: + int translate_key(int key) const; + + std::unique_ptr screencap_; + std::unique_ptr input_; + std::string eis_socket_path_; + uint32_t node_id_; + bool use_win32_vk_code_; +}; + +MAA_CTRL_UNIT_NS_END diff --git a/source/MaaWlRootsControlUnit/Manager/WlRootsControlUnitMgr.cpp b/source/MaaWlRootsControlUnit/Manager/WlRootsControlUnitMgr.cpp index ee99c28780..be24a0887f 100644 --- a/source/MaaWlRootsControlUnit/Manager/WlRootsControlUnitMgr.cpp +++ b/source/MaaWlRootsControlUnit/Manager/WlRootsControlUnitMgr.cpp @@ -5,8 +5,8 @@ #include -#include "Client/VkToEvdev.h" #include "Client/WaylandClient.h" +#include "Common/VkToEvdev.h" #include "MaaUtils/Logger.h" #include "MaaUtils/Platform.h" diff --git a/source/include/LibraryHolder/ControlUnit.h b/source/include/LibraryHolder/ControlUnit.h index 69cc7e9180..e35560bf82 100644 --- a/source/include/LibraryHolder/ControlUnit.h +++ b/source/include/LibraryHolder/ControlUnit.h @@ -16,6 +16,7 @@ class MacOSControlUnitAPI; class GamepadControlUnitAPI; class CustomControlUnitAPI; class WlRootsControlUnitAPI; +class GamescopeControlUnitAPI; class FullControlUnitAPI; class AndroidNativeControlUnitAPI; MAA_CTRL_UNIT_NS_END @@ -169,4 +170,17 @@ class WlRootsControlUnitLibraryHolder : public LibraryHolder +{ +public: + static std::shared_ptr + create_control_unit(uint32_t node_id, const char* eis_socket_path, MaaBool use_win32_vk_code); + +private: + inline static const std::filesystem::path libname_ = MAA_NS::path("MaaGamescopeControlUnit"); + inline static const std::string version_func_name_ = "MaaGamescopeControlUnitGetVersion"; + inline static const std::string create_func_name_ = "MaaGamescopeControlUnitCreate"; + inline static const std::string destroy_func_name_ = "MaaGamescopeControlUnitDestroy"; +}; + MAA_NS_END From f2f2fa04f7a233a798178e892782ab849fc7b6ce Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 14:53:54 +0800 Subject: [PATCH 05/11] feat: add Python and NodeJS bindings for Gamescope controller --- source/binding/NodeJS/src/apis/controller.cpp | 22 +++++++++ .../binding/NodeJS/src/apis/controller.d.ts | 18 +++++++ source/binding/NodeJS/src/apis/controller.h | 16 +++++- source/binding/NodeJS/src/apis/ext.h | 4 +- source/binding/NodeJS/src/apis/loader.cpp | 1 + source/binding/NodeJS/src/apis/loader.h | 1 + source/binding/Python/maa/controller.py | 49 +++++++++++++++++++ 7 files changed, 108 insertions(+), 3 deletions(-) diff --git a/source/binding/NodeJS/src/apis/controller.cpp b/source/binding/NodeJS/src/apis/controller.cpp index c21e547fed..66d4436012 100644 --- a/source/binding/NodeJS/src/apis/controller.cpp +++ b/source/binding/NodeJS/src/apis/controller.cpp @@ -777,6 +777,28 @@ maajs::ValueType load_wlroots_controller(maajs::EnvType env) return ctor; } +GamescopeControllerImpl* GamescopeControllerImpl::ctor(const maajs::CallbackInfo& info) +{ + auto [node_id, eis_socket_path, use_win32_vk_code] = maajs::UnWrapArgs(info); + auto ctrl = MaaGamescopeControllerCreate(node_id, eis_socket_path.c_str(), use_win32_vk_code); + if (!ctrl) { + return nullptr; + } + return new GamescopeControllerImpl(ctrl, true); +} + +void GamescopeControllerImpl::init_proto(maajs::ObjectType, maajs::FunctionType) +{ +} + +maajs::ValueType load_gamescope_controller(maajs::EnvType env) +{ + maajs::FunctionType ctor; + maajs::NativeClass::init(env, ctor, &ExtContext::get(env)->controllerCtor); + ExtContext::get(env)->gamescopeControllerCtor = maajs::PersistentFunction(ctor); + return ctor; +} + CustomControllerContext::~CustomControllerContext() { for (const auto& [_, ctx] : callbacks) { diff --git a/source/binding/NodeJS/src/apis/controller.d.ts b/source/binding/NodeJS/src/apis/controller.d.ts index b9e302fa84..bcaee6a5ab 100644 --- a/source/binding/NodeJS/src/apis/controller.d.ts +++ b/source/binding/NodeJS/src/apis/controller.d.ts @@ -322,6 +322,24 @@ declare global { static find(): Promise } + /** + * Gamescope controller for Linux (Gamescope compositor / Steam Deck). + * Uses Ei (Emulated Input) for input injection and PipeWire for screencap. + * + * @param node_id The PipeWire node ID for screencap. + * @param eis_socket_path The EIS socket path for emulated input injection. + * @param use_win32_vk_code When true, key codes passed to click_key / key_down / key_up are + * interpreted as Win32 Virtual-Key codes (VK_*) and translated to Linux evdev codes internally. + * When false, key codes are treated as raw evdev codes. + */ + class GamescopeController extends Controller { + constructor( + node_id: number, + eis_socket_path: string, + use_win32_vk_code: boolean, + ) + } + interface CustomControllerActor { connect?(): maa.MaybePromise // connected?(): maa.MaybePromise diff --git a/source/binding/NodeJS/src/apis/controller.h b/source/binding/NodeJS/src/apis/controller.h index 17a9fe04d8..2117067291 100644 --- a/source/binding/NodeJS/src/apis/controller.h +++ b/source/binding/NodeJS/src/apis/controller.h @@ -22,9 +22,9 @@ struct ImageJobImpl : public JobImpl struct ControllerImpl : public maajs::NativeClassBase { - MaaController* controller { }; + MaaController* controller {}; bool own = false; - std::map sinks { }; + std::map sinks {}; ControllerImpl() = default; ControllerImpl(MaaController* ctrl, bool own); @@ -212,6 +212,18 @@ struct WlRootsControllerImpl : public ControllerImpl static void init_proto(maajs::ObjectType proto, maajs::FunctionType ctor); }; +using GamescopeControllerCtorParam = std::tuple; + +struct GamescopeControllerImpl : public ControllerImpl +{ + using ControllerImpl::ControllerImpl; + + constexpr static char name[] = "GamescopeController"; + + static GamescopeControllerImpl* ctor(const maajs::CallbackInfo&); + static void init_proto(maajs::ObjectType proto, maajs::FunctionType ctor); +}; + struct CustomControllerContext { std::map callbacks; diff --git a/source/binding/NodeJS/src/apis/ext.h b/source/binding/NodeJS/src/apis/ext.h index 28fa3d46dd..99d24d9001 100644 --- a/source/binding/NodeJS/src/apis/ext.h +++ b/source/binding/NodeJS/src/apis/ext.h @@ -95,6 +95,7 @@ struct ExtContext : public maajs::NativeClassBase maajs::FunctionRefType recordControllerCtor; maajs::FunctionRefType gamepadControllerCtor; maajs::FunctionRefType wlrootsControllerCtor; + maajs::FunctionRefType gamescopeControllerCtor; maajs::FunctionRefType customControllerCtor; maajs::FunctionRefType taskJobCtor; maajs::FunctionRefType taskerCtor; @@ -122,6 +123,7 @@ struct ExtContext : public maajs::NativeClassBase marker(recordControllerCtor.Value()); marker(gamepadControllerCtor.Value()); marker(wlrootsControllerCtor.Value()); + marker(gamescopeControllerCtor.Value()); marker(customControllerCtor.Value()); marker(taskJobCtor.Value()); marker(taskerCtor.Value()); @@ -144,7 +146,7 @@ struct ExtContext : public maajs::NativeClassBase #ifdef MAA_JS_IMPL_IS_NODEJS auto ptr = env.GetInstanceData(); if (!ptr) { - ptr = new ExtContext { }; + ptr = new ExtContext {}; env.SetInstanceData(ptr); } return ptr; diff --git a/source/binding/NodeJS/src/apis/loader.cpp b/source/binding/NodeJS/src/apis/loader.cpp index 836e6be9a2..fc6c01a81d 100644 --- a/source/binding/NodeJS/src/apis/loader.cpp +++ b/source/binding/NodeJS/src/apis/loader.cpp @@ -27,6 +27,7 @@ static maajs::ObjectType init(maajs::EnvType env) maajs::BindValue(maa, "RecordController", load_record_controller(env)); maajs::BindValue(maa, "GamepadController", load_gamepad_controller(env)); maajs::BindValue(maa, "WlRootsController", load_wlroots_controller(env)); + maajs::BindValue(maa, "GamescopeController", load_gamescope_controller(env)); maajs::BindValue(maa, "CustomController", load_custom_controller(env)); maajs::BindValue(maa, "TaskJob", load_task_job(env)); maajs::BindValue(maa, "Tasker", load_tasker(env)); diff --git a/source/binding/NodeJS/src/apis/loader.h b/source/binding/NodeJS/src/apis/loader.h index 907ce17a05..33e212a6d2 100644 --- a/source/binding/NodeJS/src/apis/loader.h +++ b/source/binding/NodeJS/src/apis/loader.h @@ -22,6 +22,7 @@ maajs::ValueType load_replay_controller(maajs::EnvType env); maajs::ValueType load_record_controller(maajs::EnvType env); maajs::ValueType load_gamepad_controller(maajs::EnvType env); maajs::ValueType load_wlroots_controller(maajs::EnvType env); +maajs::ValueType load_gamescope_controller(maajs::EnvType env); maajs::ValueType load_custom_controller(maajs::EnvType env); maajs::ValueType load_task_job(maajs::EnvType env); diff --git a/source/binding/Python/maa/controller.py b/source/binding/Python/maa/controller.py index a563984d00..0efc48687b 100644 --- a/source/binding/Python/maa/controller.py +++ b/source/binding/Python/maa/controller.py @@ -21,6 +21,7 @@ "Win32Controller", "GamepadController", "WlRootsController", + "GamescopeController", "AndroidNativeController", "CustomController", ] @@ -1063,6 +1064,54 @@ def _set_wlroots_api_properties(self): ] +class GamescopeController(Controller): + """Gamescope 控制器 / Gamescope controller + + 用于在 Linux 上控制 Gamescope 微合成器中运行的应用 + For controlling apps running in Gamescope microcompositor on Linux + """ + + def __init__( + self, + node_id: int, + eis_socket_path: str, + use_win32_vk_code: bool = False, + ): + """创建 Gamescope 控制器 / Create Gamescope controller + + Args: + node_id: PipeWire node ID for the gamescope output stream + eis_socket_path: EIS socket path for libei input emulation + (path determined by gamescope environment, typically under $XDG_RUNTIME_DIR) + use_win32_vk_code: 为 True 时按键被视为 Win32 VK 键码并转换为 Linux evdev 码; + 默认 False,按原始 evdev 码处理 + / When True, key codes are interpreted as Win32 Virtual-Key codes and translated + to Linux evdev codes internally; default False passes raw evdev codes through + + Raises: + RuntimeError: 如果创建失败 + """ + super().__init__() + self._set_gamescope_api_properties() + + self._handle = Library.framework().MaaGamescopeControllerCreate( + ctypes.c_uint32(node_id), + eis_socket_path.encode(), + MaaBool(use_win32_vk_code), + ) + + if not self._handle: + raise RuntimeError("Failed to create Gamescope controller.") + + def _set_gamescope_api_properties(self): + Library.framework().MaaGamescopeControllerCreate.restype = MaaControllerHandle + Library.framework().MaaGamescopeControllerCreate.argtypes = [ + ctypes.c_uint32, + ctypes.c_char_p, + MaaBool, + ] + + class DbgController(Controller): """调试控制器,轮播图片截图,基本输入操作直接返回成功 / Debug controller that cycles through images from a directory""" From c8a05dd0ff594ab4eb8c5355314da131824b1aa5 Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 14:54:03 +0800 Subject: [PATCH 06/11] docs: add Gamescope controller documentation in Chinese and English --- docs/en_us/2.2-IntegratedInterfaceOverview.md | 12 ++++ docs/en_us/2.4-ControlMethods.md | 67 +++++++++++++++++++ ...45\345\217\243\344\270\200\350\247\210.md" | 12 ++++ ...71\345\274\217\350\257\264\346\230\216.md" | 67 +++++++++++++++++++ 4 files changed, 158 insertions(+) diff --git a/docs/en_us/2.2-IntegratedInterfaceOverview.md b/docs/en_us/2.2-IntegratedInterfaceOverview.md index ca691844c4..9081e19c62 100644 --- a/docs/en_us/2.2-IntegratedInterfaceOverview.md +++ b/docs/en_us/2.2-IntegratedInterfaceOverview.md @@ -306,6 +306,17 @@ Create PlayCover controller for controlling iOS applications running via [fork P Create WlRoots controller for controlling applications running in wlroots compositor on Linux. See [Control Methods](2.4-ControlMethods.md#wlroots-linux). +### MaaGamescopeControllerCreate + +- `node_id`: PipeWire node ID identifying the gamescope output stream +- `eis_socket_path`: EIS socket path for libei keyboard/mouse emulation (path determined by gamescope environment variables, typically under `$XDG_RUNTIME_DIR`) +- `use_win32_vk_code`: Whether to interpret key codes as Win32 Virtual-Key codes (VK_*). When `true`, key codes passed to `click_key` / `key_down` / `key_up` are interpreted as Win32 VK codes and translated to Linux evdev codes internally; when `false`, they are passed through as raw evdev codes. Default is `false` + +Create Gamescope controller for controlling applications running in Gamescope microcompositor on Linux. See [Control Methods](2.4-ControlMethods.md#gamescope-linux). + +> Linux only +> Requires `libpipewire >= 0.3.50` and `libei >= 1.0` + ### MaaGamepadControllerCreate - `hWnd`: Window handle for screencap. Pass nullptr/None/null if screencap is not needed @@ -596,6 +607,7 @@ Fields returned by each type: - `playcover`: `type`, `address` - `gamepad`: `type`, `hwnd`, `gamepad_type`, `screencap_method` - `wlroots`: `type`, `wlr_socket_path` +- `gamescope`: `type`, `node_id`, `eis_socket_path`, `use_win32_vk_code` - `custom`: `type` (if the custom controller implements the `get_info` callback, additional fields from it will also be included) - `dbg`: `type`, `path`, `image_count`, `image_index` - `replay`: `type`, `record_count`, `record_index` diff --git a/docs/en_us/2.4-ControlMethods.md b/docs/en_us/2.4-ControlMethods.md index 0d8c62fb35..f0b741f303 100644 --- a/docs/en_us/2.4-ControlMethods.md +++ b/docs/en_us/2.4-ControlMethods.md @@ -283,3 +283,70 @@ The wlroots compositor to be controlled must support the following protocols: Keycodes for MaaControllerPostKey{Down,Up} in the WlRoots controller are **by default** evdev key codes, defined in [linux/input-event-codes.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h). If you prefer using Win32 Virtual-Key codes (VK_*), pass `true` for the `use_win32_vk_code` parameter when calling `MaaWlRootsControllerCreate`. When enabled, key codes passed to `MaaControllerPostClickKey` / `MaaControllerPostKey{Down,Up}` are interpreted as Win32 VK codes and translated to evdev codes internally. This flag is decided at creation time and cannot be changed at runtime; it makes it easy to reuse key handling logic written for the Win32 controller. + +## Gamescope (Linux) + +The Gamescope controller is used to control applications running in a [Gamescope](https://github.com/ValveSoftware/gamescope) microcompositor on Linux. + +### Gamescope Prerequisites + +1. Linux only +2. `libpipewire >= 0.3.50` and `libei >= 1.0` must be installed +3. Must run within a Gamescope environment + +### Obtaining node_id + +`node_id` is the PipeWire node ID of the Gamescope output stream. It can be obtained as follows: + +```bash +# Find using pw-dump and jq +pw-dump | jq '.[] | select(.info.props["node.name"] | startswith("gamescope")) | .id' + +# Or use gamescopectl +gamescopectl info +``` + +### Obtaining eis_socket_path + +`eis_socket_path` is the EIS socket path for libei input emulation. It is typically located under `$XDG_RUNTIME_DIR`: + +```bash +# Check gamescope EIS sockets +ls $XDG_RUNTIME_DIR/gamescope-*-ei +# Path determined by gamescope environment variables, typically under $XDG_RUNTIME_DIR +``` + +### Keyboard input notice + +Keycodes for MaaControllerPostKey{Down,Up} in the Gamescope controller are **by default** evdev key codes, defined in [linux/input-event-codes.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h). + +If you prefer using Win32 Virtual-Key codes (VK_*), pass `true` for the `use_win32_vk_code` parameter when calling `MaaGamescopeControllerCreate`. When enabled, key codes passed to `MaaControllerPostClickKey` / `MaaControllerPostKey{Down,Up}` are interpreted as Win32 VK codes and translated to evdev codes internally. This flag is decided at creation time and cannot be changed at runtime. + +### Supported operations + +- screencap: Captures screenshots from the Gamescope output stream via PipeWire `pw_stream` +- click / swipe / touch_down / touch_move / touch_up: Emulates absolute pointer and button events via libei +- click_key / key_down / key_up: Emulates keyboard events via libei +- input_text: Supports ASCII printable characters (translated to evdev keyboard events internally) +- scroll: Emulates scroll wheel events via libei +- relative_move: Emulates relative pointer motion via libei + +### Unsupported operations + +- start_app: Not supported (returns false) +- stop_app: Not supported (returns false) + +### Code Example + +```python +from maa.controller import GamescopeController + +# Obtain node_id via pw-dump +# eis_socket_path is determined by gamescope environment variables +ctrl = GamescopeController( + node_id=42, + eis_socket_path=os.environ.get("GAMESCOPE_EIS_SOCKET"), + use_win32_vk_code=False, +) +ctrl.post_connection() +``` diff --git "a/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" "b/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" index a954fb314a..5a7efd0f0f 100644 --- "a/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" +++ "b/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" @@ -306,6 +306,17 @@ 创建 WlRoots 控制器,用于在 Linux 上控制在 wlroots 合成器中运行的应用程序。详见 [控制方式说明](2.4-控制方式说明.md#wlroots-linux)。 +### MaaGamescopeControllerCreate + +- `node_id`: PipeWire 节点 ID,标识 gamescope 的输出流 +- `eis_socket_path`: EIS 套接字路径,用于 libei 键盘鼠标模拟(路径由 gamescope 环境变量决定,通常位于 `$XDG_RUNTIME_DIR` 下) +- `use_win32_vk_code`: 是否将按键视为 Win32 Virtual-Key 键码(VK_*)。为 `true` 时,传入 `click_key` / `key_down` / `key_up` 的键码会被视为 Win32 VK 码并在内部转换为 Linux evdev 码;为 `false` 时按原始 evdev 码处理。默认 `false` + +创建 Gamescope 控制器,用于在 Linux 上控制 Gamescope 微合成器中运行的应用程序。详见 [控制方式说明](2.4-控制方式说明.md#gamescope-linux)。 + +> 仅支持 Linux 平台 +> 需要安装 `libpipewire >= 0.3.50` 和 `libei >= 1.0` + ### MaaGamepadControllerCreate - `hWnd`: 窗口句柄,用于截图。如果不需要截图可以传 nullptr/None/null @@ -595,6 +606,7 @@ - `playcover`: `type`, `address` - `gamepad`: `type`, `hwnd`, `gamepad_type`, `screencap_method` - `wlroots`: `type`, `wlr_socket_path` +- `gamescope`: `type`, `node_id`, `eis_socket_path`, `use_win32_vk_code` - `custom`: `type`(如果自定义控制器实现了 `get_info` 回调,还会包含其返回的额外字段) - `dbg`: `type`, `path`, `image_count`, `image_index` - `replay`: `type`, `record_count`, `record_index` diff --git "a/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" "b/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" index 64a27b2de8..2248131422 100644 --- "a/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" +++ "b/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" @@ -282,3 +282,70 @@ WlRoots 控制器用于在 Linux 上控制在专用 wlroots 合成器中运行 WlRoots 控制器下,使用 MaaControllerPostKey{Down,Up} 传入的按键**默认**为 evdev 扫描码,定义见 [linux/input-event-codes.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h)。 如需使用 Win32 Virtual-Key 键码(VK_*),在创建控制器时将 `MaaWlRootsControllerCreate` 的 `use_win32_vk_code` 参数置为 `true` 即可。开启后,`MaaControllerPostClickKey` / `MaaControllerPostKey{Down,Up}` 所接受的键码会被视为 Win32 VK 键码并在内部转换为 evdev 码。此开关在创建时一次性决定,不可运行时修改,便于从 Win32 控制器的代码中迁移按键逻辑到 WlRoots。 + +## Gamescope (Linux) + +Gamescope 控制器用于在 Linux 上控制 [Gamescope](https://github.com/ValveSoftware/gamescope) 微合成器中运行的应用程序。 + +### Gamescope 前置要求 + +1. 仅支持 Linux 平台 +2. 需要安装 `libpipewire >= 0.3.50` 和 `libei >= 1.0` +3. 需要在 Gamescope 环境中运行 + +### 获取 node_id + +`node_id` 是 PipeWire 中 Gamescope 输出流的节点 ID。可以通过以下方式获取: + +```bash +# 使用 pw-dump 和 jq 查找 +pw-dump | jq '.[] | select(.info.props["node.name"] | startswith("gamescope")) | .id' + +# 或使用 gamescopectl 查看 +gamescopectl info +``` + +### 获取 eis_socket_path + +`eis_socket_path` 是 libei 输入模拟的 EIS 套接字路径。通常位于 `$XDG_RUNTIME_DIR` 下: + +```bash +# 查看 gamescope 使用的 EIS 套接字 +ls $XDG_RUNTIME_DIR/gamescope-*-ei +# 路径由 gamescope 环境变量决定,通常位于 $XDG_RUNTIME_DIR 下 +``` + +### 键盘输入说明 + +Gamescope 控制器下,使用 MaaControllerPostKey{Down,Up} 传入的按键**默认**为 evdev 扫描码,定义见 [linux/input-event-codes.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h)。 + +如需使用 Win32 Virtual-Key 键码(VK_*),在创建控制器时将 `MaaGamescopeControllerCreate` 的 `use_win32_vk_code` 参数置为 `true` 即可。开启后,`MaaControllerPostClickKey` / `MaaControllerPostKey{Down,Up}` 所接受的键码会被视为 Win32 VK 键码并在内部转换为 evdev 码。此开关在创建时一次性决定,不可运行时修改。 + +### 支持的操作 + +- screencap: 通过 PipeWire `pw_stream` 从 Gamescope 输出流获取截图 +- click / swipe / touch_down / touch_move / touch_up: 通过 libei 模拟绝对指针和按键事件 +- click_key / key_down / key_up: 通过 libei 模拟键盘事件 +- input_text: 支持 ASCII 可打印字符(内部转换为 evdev 键盘事件) +- scroll: 通过 libei 模拟滚轮事件 +- relative_move: 通过 libei 模拟相对指针移动 + +### 不支持的操作 + +- start_app: 不支持(返回 false) +- stop_app: 不支持(返回 false) + +### 代码示例 + +```python +from maa.controller import GamescopeController + +# node_id 通过 pw-dump 获取 +# eis_socket_path 通过 gamescope 环境变量确定 +ctrl = GamescopeController( + node_id=42, + eis_socket_path=os.environ.get("GAMESCOPE_EIS_SOCKET"), + use_win32_vk_code=False, +) +ctrl.post_connection() +``` From fdccfe1e7e35e38dbcea05e7522ec39eda2d9769 Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 15:43:00 +0800 Subject: [PATCH 07/11] chore: update interface schema --- tools/interface.schema.json | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tools/interface.schema.json b/tools/interface.schema.json index 57538998f4..3efff5c3ad 100644 --- a/tools/interface.schema.json +++ b/tools/interface.schema.json @@ -406,7 +406,8 @@ "MacOS", "PlayCover", "Gamepad", - "WlRoots" + "WlRoots", + "Gamescope" ] }, "display_short_side": { @@ -623,6 +624,30 @@ } }, "additionalProperties": false + }, + "gamescope": { + "type": "object", + "title": "Gamescope 控制器配置", + "description": "Gamescope 控制器的具体配置(仅 Linux)", + "properties": { + "node_id": { + "type": "string", + "title": "PipeWire Node ID", + "description": "可选。PipeWire Node ID,用于指定 Gamescope 控制器的 PipeWire 节点" + }, + "eis_socket_path": { + "type": "string", + "title": "EIS Socket 路径", + "description": "可选。Gamescope EIS Socket 路径,用于连接 Gamescope 的 EIS 以实现输入控制" + }, + "use_win32_vk_code": { + "type": "boolean", + "title": "使用 Win32 VK 键码", + "description": "可选。为 true 时按键被视为 Win32 Virtual-Key 码,内部转换为 Linux evdev 码;否则按原始 evdev 码处理。默认 false", + "default": false + } + }, + "additionalProperties": false } }, "required": [ @@ -1096,4 +1121,4 @@ "resource" ], "additionalProperties": false -} +} \ No newline at end of file From 3bdec3042baf4c28ba3613ba01d798e5c64e3775 Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 18:44:10 +0800 Subject: [PATCH 08/11] feat: add gamescope to PiCli --- source/MaaPiCli/CLI/interactor.cpp | 99 ++++++++++++++++++++++++- source/MaaPiCli/CLI/interactor.h | 1 + source/MaaPiCli/Impl/Configurator.cpp | 10 +++ source/MaaPiCli/Impl/Runner.cpp | 12 +++ source/include/ProjectInterface/Types.h | 32 +++++++- 5 files changed, 148 insertions(+), 6 deletions(-) diff --git a/source/MaaPiCli/CLI/interactor.cpp b/source/MaaPiCli/CLI/interactor.cpp index f7867d5166..786fbc93fe 100644 --- a/source/MaaPiCli/CLI/interactor.cpp +++ b/source/MaaPiCli/CLI/interactor.cpp @@ -38,8 +38,10 @@ static constexpr bool kGamepadSupported = false; #if defined(__linux__) static constexpr bool kWlRootsSupported = true; +static constexpr bool kGamescopeSupported = true; #else static constexpr bool kWlRootsSupported = false; +static constexpr bool kGamescopeSupported = false; #endif // return [1, size] @@ -59,7 +61,7 @@ std::vector input_multi_impl(size_t size, std::string_view prompt) if (std::cin.eof()) { s_eof = true; - return { }; + return {}; } if (buffer.empty()) { @@ -100,7 +102,7 @@ int input(size_t size, std::string_view prompt = "Please input") while (true) { auto values = input_multi_impl(size, prompt); if (s_eof) { - return { }; + return {}; } if (values.size() != 1) { fail(); @@ -147,7 +149,7 @@ bool is_running_as_admin() BOOL is_member = FALSE; // CheckTokenMembership(nullptr, ...) checks the current effective token (UAC-aware). - BYTE sid_buffer[SECURITY_MAX_SID_SIZE] = { }; + BYTE sid_buffer[SECURITY_MAX_SID_SIZE] = {}; DWORD sid_size = sizeof(sid_buffer); PSID admin_sid = sid_buffer; @@ -375,6 +377,13 @@ void Interactor::print_config() const std::cout << "\t\t(WLRoots is only available on Linux)\n"; } } break; + case InterfaceData::Controller::Type::Gamescope: { + const auto& gc = config_.configuration().gamescope; + std::cout << MAA_NS::utf8_to_crt(std::format("\t\tNode ID: {}\n\t\tEIS socket: {}\n", gc.node_id, gc.eis_socket_path)); + if (!kGamescopeSupported) { + std::cout << "\t\t(Gamescope is only available on Linux)\n"; + } + } break; default: LogError << "Unknown controller type" << VAR(config_.configuration().controller.type); break; @@ -691,6 +700,26 @@ void Interactor::select_controller() config_.configuration().controller.type = InterfaceData::Controller::Type::Gamepad; select_gamepad(controller.gamepad); break; + case InterfaceData::Controller::Type::Gamescope: + if (!kGamescopeSupported) { + std::cout << "\nGamescope controller is only available on Linux.\n"; + bool has_other_controllers = std::ranges::any_of(all_controllers, [](const auto& ctrl) { + return ctrl.type != InterfaceData::Controller::Type::Gamescope; + }); + if (has_other_controllers) { + std::cout << "Please select another controller.\n\n"; + mpause(); + select_controller(); + } + else { + std::cout << "No other controllers available.\n\n"; + mpause(); + } + return; + } + config_.configuration().controller.type = InterfaceData::Controller::Type::Gamescope; + select_gamescope(); + break; default: LogError << "Unknown controller type" << VAR(controller.type); break; @@ -1150,6 +1179,41 @@ void Interactor::select_wlroots_manual_input() std::cout << "\n"; } +void Interactor::select_gamescope() +{ + std::cout << "### Configure Gamescope Controller ###\n\n"; + + auto& gc = config_.configuration().gamescope; + + // Ask for PipeWire node ID + std::cout << "Please input PipeWire node ID for gamescope output: "; + std::cin.sync(); + std::string buffer; + std::getline(std::cin, buffer); + + if (std::cin.eof()) { + s_eof = true; + return; + } + + gc.node_id = buffer.empty() ? 0 : static_cast(std::stoul(buffer)); + std::cout << "\n"; + + // Ask for EIS socket path + std::string default_eis = gc.eis_socket_path.empty() ? "/run/user/1000/wayland-0" : gc.eis_socket_path; + std::cout << "Please input EIS socket path [" << default_eis << "]: "; + std::cin.sync(); + std::getline(std::cin, buffer); + + if (std::cin.eof()) { + s_eof = true; + return; + } + + gc.eis_socket_path = buffer.empty() ? default_eis : buffer; + std::cout << "\n"; +} + void Interactor::select_resource() { using namespace MAA_PROJECT_INTERFACE_NS; @@ -1752,6 +1816,33 @@ bool Interactor::check_validity() } } + if (config_.configuration().controller.type == InterfaceData::Controller::Type::Gamescope) { + if (!kGamescopeSupported) { + LogError << "Gamescope controller is only available on Linux"; + return false; + } + + auto& gc = config_.configuration().gamescope; + if (gc.node_id == 0) { + auto& name = config_.configuration().controller.name; + auto controller_iter = + std::ranges::find(config_.interface_data().controller, name, std::mem_fn(&InterfaceData::Controller::name)); + + if (controller_iter != config_.interface_data().controller.end()) { + select_gamescope(); + } + } + + if (gc.node_id == 0) { + LogError << "Gamescope PipeWire node ID is not set"; + return false; + } + if (gc.eis_socket_path.empty()) { + LogError << "Gamescope EIS socket path is empty"; + return false; + } + } + return true; } @@ -1877,7 +1968,7 @@ std::string Interactor::get_display_name(const std::string& name, const std::str std::string Interactor::read_text_content(const std::string& text) const { if (text.empty()) { - return { }; + return {}; } // 先翻译文本(如果以 $ 开头) diff --git a/source/MaaPiCli/CLI/interactor.h b/source/MaaPiCli/CLI/interactor.h index bdba10b174..9d4fd6d415 100644 --- a/source/MaaPiCli/CLI/interactor.h +++ b/source/MaaPiCli/CLI/interactor.h @@ -30,6 +30,7 @@ class Interactor void select_wlroots(); void select_wlroots_auto_detect(); void select_wlroots_manual_input(); + void select_gamescope(); void select_resource(); void add_task(); diff --git a/source/MaaPiCli/Impl/Configurator.cpp b/source/MaaPiCli/Impl/Configurator.cpp index b94493dc2d..17b8a4bc5c 100644 --- a/source/MaaPiCli/Impl/Configurator.cpp +++ b/source/MaaPiCli/Impl/Configurator.cpp @@ -303,6 +303,16 @@ std::optional Configurator::generate_runtime() const runtime.controller_param = std::move(wlroots); } break; + case InterfaceData::Controller::Type::Gamescope: { + RuntimeParam::GamescopeParam gamescope; + + gamescope.node_id = config_.gamescope.node_id; + gamescope.eis_socket_path = config_.gamescope.eis_socket_path; + gamescope.use_win32_vk_code = controller.gamescope.use_win32_vk_code; + + runtime.controller_param = std::move(gamescope); + } break; + default: { LogError << "Unknown controller type" << VAR(controller.type); return std::nullopt; diff --git a/source/MaaPiCli/Impl/Runner.cpp b/source/MaaPiCli/Impl/Runner.cpp index bababd9944..8b65c010ff 100644 --- a/source/MaaPiCli/Impl/Runner.cpp +++ b/source/MaaPiCli/Impl/Runner.cpp @@ -123,6 +123,18 @@ bool Runner::run(const RuntimeParam& param) std::ignore = p_wlroots_param; LogError << "WlRoots controller is only supported on Linux"; return false; +#endif + } + else if (const auto* p_gamescope_param = std::get_if(¶m.controller_param)) { +#if defined(__linux__) + controller_handle = MaaGamescopeControllerCreate( + p_gamescope_param->node_id, + p_gamescope_param->eis_socket_path.c_str(), + p_gamescope_param->use_win32_vk_code); +#else + std::ignore = p_gamescope_param; + LogError << "Gamescope controller is only supported on Linux"; + return false; #endif } else { diff --git a/source/include/ProjectInterface/Types.h b/source/include/ProjectInterface/Types.h index fa19d64bd8..2b0a19095f 100644 --- a/source/include/ProjectInterface/Types.h +++ b/source/include/ProjectInterface/Types.h @@ -63,6 +63,13 @@ struct InterfaceData MEO_JSONIZATION(MEO_OPT use_win32_vk_code); }; + struct GamescopeConfig + { + bool use_win32_vk_code = false; + + MEO_JSONIZATION(MEO_OPT use_win32_vk_code); + }; + enum class Type { Invalid, @@ -72,6 +79,7 @@ struct InterfaceData PlayCover, Gamepad, WlRoots, + Gamescope, }; std::string name; @@ -98,6 +106,7 @@ struct InterfaceData PlayCoverConfig playcover; GamepadConfig gamepad; WlRootsConfig wlroots; + GamescopeConfig gamescope; MEO_JSONIZATION( name, @@ -114,7 +123,8 @@ struct InterfaceData MEO_OPT macos, MEO_OPT playcover, MEO_OPT gamepad, - MEO_OPT wlroots); + MEO_OPT wlroots, + MEO_OPT gamescope); }; struct Resource @@ -387,6 +397,14 @@ struct Configuration MEO_JSONIZATION(MEO_OPT wlr_socket_path); }; + struct GamescopeConfig + { + uint32_t node_id = 0; + std::string eis_socket_path; + + MEO_JSONIZATION(MEO_OPT node_id, MEO_OPT eis_socket_path); + }; + struct Option { std::string name; @@ -412,6 +430,7 @@ struct Configuration PlayCoverConfig playcover; GamepadConfig gamepad; WlRootsConfig wlroots; + GamescopeConfig gamescope; std::string resource; std::vector task; @@ -427,6 +446,7 @@ struct Configuration MEO_OPT playcover, MEO_OPT gamepad, MEO_OPT wlroots, + MEO_OPT gamescope, resource, task, MEO_OPT global_option, @@ -489,6 +509,13 @@ struct RuntimeParam bool use_win32_vk_code = false; }; + struct GamescopeParam + { + uint32_t node_id = 0; + std::string eis_socket_path; + bool use_win32_vk_code = false; + }; + struct Task { std::string name; @@ -505,7 +532,8 @@ struct RuntimeParam std::unordered_map env_vars; // v2.5.0: PI_* env vars }; - std::variant controller_param; + std::variant + controller_param; std::vector resource_path; std::vector task; From f20a8649ca7927badb6214e6fb61c1887fbb77ab Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 18:45:47 +0800 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20scroll=20y=E8=BD=B4=E5=8F=96?= =?UTF-8?q?=E5=8F=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/MaaGamescopeControlUnit/Client/EiInput.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/MaaGamescopeControlUnit/Client/EiInput.cpp b/source/MaaGamescopeControlUnit/Client/EiInput.cpp index e09cde2616..dad5fb45a9 100644 --- a/source/MaaGamescopeControlUnit/Client/EiInput.cpp +++ b/source/MaaGamescopeControlUnit/Client/EiInput.cpp @@ -232,7 +232,8 @@ bool EiInput::relative_move(int dx, int dy) bool EiInput::scroll(int dx, int dy) { - return send([dx, dy](struct ei_device* d) { ei_device_scroll_delta(d, static_cast(dx), static_cast(dy)); }); + // 对 dy 取反,在行为上和 Win32 保持一致 + return send([dx, dy](struct ei_device* d) { ei_device_scroll_delta(d, static_cast(dx), static_cast(-dy)); }); } MAA_CTRL_UNIT_NS_END From 7949a5c966d2a3fd78880f394285e148e0c6129d Mon Sep 17 00:00:00 2001 From: mx6436 Date: Tue, 19 May 2026 21:52:35 +0800 Subject: [PATCH 10/11] fix: handle EI_EVENT_SYNC --- source/MaaGamescopeControlUnit/Client/EiInput.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/MaaGamescopeControlUnit/Client/EiInput.cpp b/source/MaaGamescopeControlUnit/Client/EiInput.cpp index dad5fb45a9..20236a2e9c 100644 --- a/source/MaaGamescopeControlUnit/Client/EiInput.cpp +++ b/source/MaaGamescopeControlUnit/Client/EiInput.cpp @@ -175,6 +175,11 @@ void EiInput::handle_event(struct ei_event* event) } connected_ = false; break; + case EI_EVENT_SYNC: + if (device_) { + ei_device_frame(device_, ei_now(ei_)); + } + break; default: LogTrace << "Unhandled ei event" << VAR(ei_event_get_type(event)); break; From fb4d605679cb6ba665c3720635e9f4ef90f4151f Mon Sep 17 00:00:00 2001 From: mx6436 Date: Thu, 21 May 2026 22:50:23 +0800 Subject: [PATCH 11/11] docs: simplify Gamescope controller documentation Remove verbose prerequisite, setup, supported/unsupported operations, and code example sections from the Gamescope docs. Retain only the essential API parameter descriptions and keyboard input notice. --- docs/en_us/2.2-IntegratedInterfaceOverview.md | 5 +- docs/en_us/2.4-ControlMethods.md | 57 ------------------- ...45\345\217\243\344\270\200\350\247\210.md" | 5 +- ...71\345\274\217\350\257\264\346\230\216.md" | 57 ------------------- include/MaaFramework/Instance/MaaController.h | 2 +- 5 files changed, 3 insertions(+), 123 deletions(-) diff --git a/docs/en_us/2.2-IntegratedInterfaceOverview.md b/docs/en_us/2.2-IntegratedInterfaceOverview.md index 9081e19c62..d115e74008 100644 --- a/docs/en_us/2.2-IntegratedInterfaceOverview.md +++ b/docs/en_us/2.2-IntegratedInterfaceOverview.md @@ -309,14 +309,11 @@ Create WlRoots controller for controlling applications running in wlroots compos ### MaaGamescopeControllerCreate - `node_id`: PipeWire node ID identifying the gamescope output stream -- `eis_socket_path`: EIS socket path for libei keyboard/mouse emulation (path determined by gamescope environment variables, typically under `$XDG_RUNTIME_DIR`) +- `eis_socket_path`: EIS socket path for libei keyboard/mouse emulation, e.g. `/run/user/1000/gamescope-0-ei` - `use_win32_vk_code`: Whether to interpret key codes as Win32 Virtual-Key codes (VK_*). When `true`, key codes passed to `click_key` / `key_down` / `key_up` are interpreted as Win32 VK codes and translated to Linux evdev codes internally; when `false`, they are passed through as raw evdev codes. Default is `false` Create Gamescope controller for controlling applications running in Gamescope microcompositor on Linux. See [Control Methods](2.4-ControlMethods.md#gamescope-linux). -> Linux only -> Requires `libpipewire >= 0.3.50` and `libei >= 1.0` - ### MaaGamepadControllerCreate - `hWnd`: Window handle for screencap. Pass nullptr/None/null if screencap is not needed diff --git a/docs/en_us/2.4-ControlMethods.md b/docs/en_us/2.4-ControlMethods.md index f0b741f303..fb2a51906c 100644 --- a/docs/en_us/2.4-ControlMethods.md +++ b/docs/en_us/2.4-ControlMethods.md @@ -288,65 +288,8 @@ If you prefer using Win32 Virtual-Key codes (VK_*), pass `true` for the `use_win The Gamescope controller is used to control applications running in a [Gamescope](https://github.com/ValveSoftware/gamescope) microcompositor on Linux. -### Gamescope Prerequisites - -1. Linux only -2. `libpipewire >= 0.3.50` and `libei >= 1.0` must be installed -3. Must run within a Gamescope environment - -### Obtaining node_id - -`node_id` is the PipeWire node ID of the Gamescope output stream. It can be obtained as follows: - -```bash -# Find using pw-dump and jq -pw-dump | jq '.[] | select(.info.props["node.name"] | startswith("gamescope")) | .id' - -# Or use gamescopectl -gamescopectl info -``` - -### Obtaining eis_socket_path - -`eis_socket_path` is the EIS socket path for libei input emulation. It is typically located under `$XDG_RUNTIME_DIR`: - -```bash -# Check gamescope EIS sockets -ls $XDG_RUNTIME_DIR/gamescope-*-ei -# Path determined by gamescope environment variables, typically under $XDG_RUNTIME_DIR -``` - ### Keyboard input notice Keycodes for MaaControllerPostKey{Down,Up} in the Gamescope controller are **by default** evdev key codes, defined in [linux/input-event-codes.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h). If you prefer using Win32 Virtual-Key codes (VK_*), pass `true` for the `use_win32_vk_code` parameter when calling `MaaGamescopeControllerCreate`. When enabled, key codes passed to `MaaControllerPostClickKey` / `MaaControllerPostKey{Down,Up}` are interpreted as Win32 VK codes and translated to evdev codes internally. This flag is decided at creation time and cannot be changed at runtime. - -### Supported operations - -- screencap: Captures screenshots from the Gamescope output stream via PipeWire `pw_stream` -- click / swipe / touch_down / touch_move / touch_up: Emulates absolute pointer and button events via libei -- click_key / key_down / key_up: Emulates keyboard events via libei -- input_text: Supports ASCII printable characters (translated to evdev keyboard events internally) -- scroll: Emulates scroll wheel events via libei -- relative_move: Emulates relative pointer motion via libei - -### Unsupported operations - -- start_app: Not supported (returns false) -- stop_app: Not supported (returns false) - -### Code Example - -```python -from maa.controller import GamescopeController - -# Obtain node_id via pw-dump -# eis_socket_path is determined by gamescope environment variables -ctrl = GamescopeController( - node_id=42, - eis_socket_path=os.environ.get("GAMESCOPE_EIS_SOCKET"), - use_win32_vk_code=False, -) -ctrl.post_connection() -``` diff --git "a/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" "b/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" index 5a7efd0f0f..5ffc8994f1 100644 --- "a/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" +++ "b/docs/zh_cn/2.2-\351\233\206\346\210\220\346\216\245\345\217\243\344\270\200\350\247\210.md" @@ -309,14 +309,11 @@ ### MaaGamescopeControllerCreate - `node_id`: PipeWire 节点 ID,标识 gamescope 的输出流 -- `eis_socket_path`: EIS 套接字路径,用于 libei 键盘鼠标模拟(路径由 gamescope 环境变量决定,通常位于 `$XDG_RUNTIME_DIR` 下) +- `eis_socket_path`: EIS 套接字路径,用于 libei 键盘鼠标模拟,如 `/run/user/1000/gamescope-0-ei` - `use_win32_vk_code`: 是否将按键视为 Win32 Virtual-Key 键码(VK_*)。为 `true` 时,传入 `click_key` / `key_down` / `key_up` 的键码会被视为 Win32 VK 码并在内部转换为 Linux evdev 码;为 `false` 时按原始 evdev 码处理。默认 `false` 创建 Gamescope 控制器,用于在 Linux 上控制 Gamescope 微合成器中运行的应用程序。详见 [控制方式说明](2.4-控制方式说明.md#gamescope-linux)。 -> 仅支持 Linux 平台 -> 需要安装 `libpipewire >= 0.3.50` 和 `libei >= 1.0` - ### MaaGamepadControllerCreate - `hWnd`: 窗口句柄,用于截图。如果不需要截图可以传 nullptr/None/null diff --git "a/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" "b/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" index 2248131422..e82d952fb0 100644 --- "a/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" +++ "b/docs/zh_cn/2.4-\346\216\247\345\210\266\346\226\271\345\274\217\350\257\264\346\230\216.md" @@ -287,65 +287,8 @@ WlRoots 控制器下,使用 MaaControllerPostKey{Down,Up} 传入的按键**默 Gamescope 控制器用于在 Linux 上控制 [Gamescope](https://github.com/ValveSoftware/gamescope) 微合成器中运行的应用程序。 -### Gamescope 前置要求 - -1. 仅支持 Linux 平台 -2. 需要安装 `libpipewire >= 0.3.50` 和 `libei >= 1.0` -3. 需要在 Gamescope 环境中运行 - -### 获取 node_id - -`node_id` 是 PipeWire 中 Gamescope 输出流的节点 ID。可以通过以下方式获取: - -```bash -# 使用 pw-dump 和 jq 查找 -pw-dump | jq '.[] | select(.info.props["node.name"] | startswith("gamescope")) | .id' - -# 或使用 gamescopectl 查看 -gamescopectl info -``` - -### 获取 eis_socket_path - -`eis_socket_path` 是 libei 输入模拟的 EIS 套接字路径。通常位于 `$XDG_RUNTIME_DIR` 下: - -```bash -# 查看 gamescope 使用的 EIS 套接字 -ls $XDG_RUNTIME_DIR/gamescope-*-ei -# 路径由 gamescope 环境变量决定,通常位于 $XDG_RUNTIME_DIR 下 -``` - ### 键盘输入说明 Gamescope 控制器下,使用 MaaControllerPostKey{Down,Up} 传入的按键**默认**为 evdev 扫描码,定义见 [linux/input-event-codes.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input-event-codes.h)。 如需使用 Win32 Virtual-Key 键码(VK_*),在创建控制器时将 `MaaGamescopeControllerCreate` 的 `use_win32_vk_code` 参数置为 `true` 即可。开启后,`MaaControllerPostClickKey` / `MaaControllerPostKey{Down,Up}` 所接受的键码会被视为 Win32 VK 键码并在内部转换为 evdev 码。此开关在创建时一次性决定,不可运行时修改。 - -### 支持的操作 - -- screencap: 通过 PipeWire `pw_stream` 从 Gamescope 输出流获取截图 -- click / swipe / touch_down / touch_move / touch_up: 通过 libei 模拟绝对指针和按键事件 -- click_key / key_down / key_up: 通过 libei 模拟键盘事件 -- input_text: 支持 ASCII 可打印字符(内部转换为 evdev 键盘事件) -- scroll: 通过 libei 模拟滚轮事件 -- relative_move: 通过 libei 模拟相对指针移动 - -### 不支持的操作 - -- start_app: 不支持(返回 false) -- stop_app: 不支持(返回 false) - -### 代码示例 - -```python -from maa.controller import GamescopeController - -# node_id 通过 pw-dump 获取 -# eis_socket_path 通过 gamescope 环境变量确定 -ctrl = GamescopeController( - node_id=42, - eis_socket_path=os.environ.get("GAMESCOPE_EIS_SOCKET"), - use_win32_vk_code=False, -) -ctrl.post_connection() -``` diff --git a/include/MaaFramework/Instance/MaaController.h b/include/MaaFramework/Instance/MaaController.h index 018c5e1b5a..0269eb4018 100644 --- a/include/MaaFramework/Instance/MaaController.h +++ b/include/MaaFramework/Instance/MaaController.h @@ -137,7 +137,7 @@ extern "C" * @return The controller handle, or nullptr on failure. * * @note This controller is designed for Gamescope on Linux. - * @note Requires libpipewire >= 0.3.50 and libei >= 1.0. + * @note Requires libpipewire >= 0.3.50 and libei >= 1.6. */ MAA_FRAMEWORK_API MaaController* MaaGamescopeControllerCreate(uint32_t node_id, const char* eis_socket_path, MaaBool use_win32_vk_code);