mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-11-08 04:22:39 +01:00
## Summary Automatic controller assignment for Eden, aimed at couch co-op and handheld PCs (Steam Deck, Legion Go, ROG Ally, AYN Odin), plus a set of fixes for running Eden in Steam's Game Mode (gamescope) and under Steam Input. ## Controller Assignment Handler Settings > Controls gains a **Controllers** tab. The handler is a segmented choice: | Handler | Behaviour | |---|---| | **Modern Console** | Automatic: each controller that connects becomes the next player (up to 8). | | **Manual** (default) | Players are set in each player's tab, as before. Its Controller Applet choice: **Disabled**, **Custom Frontend** or **Real Hardware** (the console's own applet, LLE). | Under Modern Console, **Portable Handheld Gaming Device with Built-in Controller** (on/off) makes external controllers take the players first, with the handheld's own controls used only when none is connected. In the automatic modes: - Players follow their controllers across reconnects: a controller that leaves keeps its player for 45 seconds and takes it back when it returns. Each controller keeps its own mappings when it changes player, for the session. - At most eight players; a ninth controller waits until a player is free. - The user's saved controller setup is never rewritten; remaps made in the Controls tab last for the session, and input profiles are what persist. - Input profiles can be picked or removed per player, from the Controllers tab or each player tab. - A controller whose player has no controller mappings (a fresh setup) gets its default mapping. A DSU (cemuhook) motion binding from the manual setup is kept, for handhelds whose own gyro needs it. ## Gameplay options - **Pause When a Controller Disconnects** (on by default): the game pauses when a player's controller is turned off, or has been gone for 3 seconds, and resumes when a controller takes that player again. Pressing + on another controller goes on without it. - **Use Any Controller as Joy-Cons**: a game that only takes Joy-Cons gets them automatically (dual, or a single sideways one with the buttons arranged to fit). - **Button Layout**: By Position or By Label, for Xbox and PlayStation style controllers. - **Player Lights**: controllers that have them show their player number. ## Controllers tab and player tabs - Connected Players: each player with its controller and battery, profile picker, Swap, Config, Turn Off (Bluetooth), reorder arrows, drag to swap, and click to rumble. Controllers can be given a name for the session. - Assign Handheld Controller: auto-detect, or pick/Identify the handheld's own pad. - P1-P8 tabs show each player's battery and can be dragged or moved with arrows to change the player order. - In Modern Console the Controllers and player tabs use a modern card layout that follows the light and dark themes, including the system's dark mode (macOS). ## On-screen display - **Controller Overlay** (View menu, Ctrl+Shift+V): players, their controllers and batteries, and each player's recent buttons, including stick moves (LS/RS) and clicks (L3/R3). How many inputs are kept and for how long can be set. - **Controller Hints**: names a controller in the bottom-left corner when it joins or leaves a player, and says when the game pauses for a disconnected one. Works without the overlay. Both are drawn into the presented frame as crisp overlay panels (not scaled with the game, not in screenshots), so they show inside gamescope. ## Steam Input and gamescope fixes - **Native gyro under Steam Input.** Steam's virtual pads carry no motion sensors. Eden now opens the real motion-capable controller behind each virtual pad for its sensors only (Steam Deck, Steam Controller incl. the 2026 model, Sony, Nintendo), and turns on the Deck's IMU, which Steam keeps off unless the layout uses the gyro. No DSU server or Steam layout change needed. - **Bypass Steam Input** (Controllers tab, shown when launched through Steam): Eden opens the real controllers itself instead of Steam's virtual pads. - **Controller identity under Steam.** All virtual pads share one GUID and Steam moves physical controllers between them; controllers are tracked by their Steam handle and slot instead. - **Handheld detection under Steam.** The Deck's own controls are recognised behind Steam Input without mistaking standalone controllers (e.g. the Steam Controller) for them. - **Windows no longer rescale on hover in Game Mode.** gamescope scales a window to fit the screen together with any tooltip reaching past it, so the main window and Settings grew and shrank on hover. Under gamescope they now fill the screen. - **VSync modes in Game Mode.** gamescope offers only some present modes, so Immediate and FIFO Relaxed vanished silently. They are now listed greyed out with a tooltip pointing to Steam's Disable Frame Limit and Allow Tearing settings. - **Stick range testers on small screens.** The controller drawing scales down to fit on 800-pixel screens instead of cutting off the stick range circles. - **Midnight Blue themes.** A hovered menu-bar item is readable again, and hovered tabs no longer change size (which made the Settings tabs shake). ## Fixes - Crash when HID received a controller update before NPad's applet resource was set up. ## Testing Tested on a Steam Deck (Desktop and Game Mode, direct and Steam launches) with Xbox controllers, plus a stress test with up to 10 virtual controllers connecting, disconnecting and pressing at random in both automatic modes (no assignment errors, never more than 8 players, disconnect pauses resume as expected). ## AI disclosure Developed with Claude Opus 5.5 (implementation and debugging). I reviewed, tested and can explain every change. - [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md). - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. --- Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4583 Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
170 lines
5.8 KiB
C++
170 lines
5.8 KiB
C++
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <QKeyEvent>
|
|
#include <QList>
|
|
#include <QPointer>
|
|
#include <QProxyStyle>
|
|
#include <QToolButton>
|
|
#include <QWidget>
|
|
|
|
#include "common/controller_console_state.h"
|
|
|
|
namespace Core {
|
|
class System;
|
|
}
|
|
|
|
class QCheckBox;
|
|
class QString;
|
|
class QTimer;
|
|
class QTabBar;
|
|
class QTabWidget;
|
|
|
|
class ConfigureInputAdvanced;
|
|
class ConfigureInputPlayer;
|
|
class ConfigurePlayerAssignment;
|
|
|
|
class InputProfiles;
|
|
|
|
namespace InputCommon {
|
|
class InputSubsystem;
|
|
}
|
|
|
|
namespace Ui {
|
|
class ConfigureInput;
|
|
}
|
|
|
|
void OnDockedModeChanged(bool last_state, bool new_state, Core::System& system);
|
|
|
|
/// The settings tab bar in the automatic modes: player tabs drawn as bubbles in their colour
|
|
class PlayerTabStyle : public QProxyStyle {
|
|
public:
|
|
explicit PlayerTabStyle(const QString& base);
|
|
void drawControl(ControlElement element, const QStyleOption* option, QPainter* painter,
|
|
const QWidget* widget) const override;
|
|
QSize sizeFromContents(ContentsType type, const QStyleOption* option, const QSize& size,
|
|
const QWidget* widget) const override;
|
|
};
|
|
|
|
/// Under Eden's dark themes the style sheet draws the tabs: this layer paints the player tabs'
|
|
/// bubbles instead
|
|
class PlayerTabOverlay : public QWidget {
|
|
public:
|
|
PlayerTabOverlay(QTabBar* bar, QColor background);
|
|
/// A player tab being dragged (-1: none), drawn this far from its place
|
|
void SetDrag(int index, int offset);
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent* event) override;
|
|
|
|
private:
|
|
QTabBar* bar;
|
|
/// The theme's own background, which covers its tab under the bubble
|
|
QColor background;
|
|
int drag_index = -1;
|
|
int drag_offset = 0;
|
|
};
|
|
|
|
class ConfigureInput : public QWidget {
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit ConfigureInput(Core::System& system_, QWidget* parent = nullptr);
|
|
~ConfigureInput() override;
|
|
|
|
/// Initializes the input dialog with the given input subsystem.
|
|
void Initialize(InputCommon::InputSubsystem* input_subsystem_, std::size_t max_players = 8);
|
|
|
|
/// Save all button configurations to settings file.
|
|
void ApplyConfiguration();
|
|
|
|
QList<QWidget*> GetSubTabs() const;
|
|
|
|
/// A player's tab (0-based player)
|
|
QWidget* PlayerTab(int player) const;
|
|
|
|
private:
|
|
void changeEvent(QEvent* event) override;
|
|
bool eventFilter(QObject* object, QEvent* event) override;
|
|
|
|
/// Player tabs dropped in a new order: the controllers take that order, the tabs go back
|
|
void FinishTabDrag();
|
|
/// The players' new order, as their tabs were dropped: the controllers move to match
|
|
void ApplyDroppedOrder(const std::vector<int>& dropped, int dragged_player);
|
|
/// Eden's own drag of a player tab, under the dark themes (Qt's would be the theme's tab)
|
|
bool DragPlayerTab(QEvent* event);
|
|
/// The arrows on each player tab, swapping with the player before or after it
|
|
void UpdateTabArrows();
|
|
void SwapPlayers(int a, int b);
|
|
void RetranslateUI();
|
|
void ClearAll();
|
|
|
|
void UpdateDockedState(bool is_handheld);
|
|
void UpdateAllInputDevices();
|
|
void UpdateAllInputProfiles(std::size_t player_index);
|
|
// Enable preceding controllers or disable following ones
|
|
void PropagatePlayerNumberChanged(size_t player_index, bool checked,
|
|
bool reconnect_current = false);
|
|
|
|
/// Load configuration settings.
|
|
void LoadConfiguration();
|
|
void LoadPlayerControllerIndices();
|
|
|
|
/// Restore all buttons to their default values.
|
|
void RestoreDefaults();
|
|
|
|
std::unique_ptr<Ui::ConfigureInput> ui;
|
|
|
|
std::unique_ptr<InputProfiles> profiles;
|
|
|
|
std::array<ConfigureInputPlayer*, 8> player_controllers{};
|
|
ConfigurePlayerAssignment* assignment = nullptr;
|
|
/// Redraws the player tabs when the engine reloads the controllers
|
|
QTimer* follow_timer = nullptr;
|
|
unsigned long long followed_generation = 0;
|
|
std::function<void()> mirror_console_mode;
|
|
/// The settings dialog's tab widget whose tab clicks rumble the players' controllers
|
|
QTabWidget* rumble_tabs = nullptr;
|
|
std::array<QWidget*, 8> player_tabs{};
|
|
/// Dragging the player tabs: the tab bar, and the order when the drag began
|
|
QTabWidget* drag_tabs = nullptr;
|
|
/// The tab bar drawn with PlayerTabStyle (its player tabs as bubbles)
|
|
QPointer<QTabBar> pill_bar;
|
|
QPointer<PlayerTabOverlay> tab_overlay;
|
|
/// The players can be reordered from their tabs (the automatic modes, on this page)
|
|
bool tabs_editable = false;
|
|
/// Eden's own tab drag: the tab pressed, where, and whether it is being dragged
|
|
int own_drag_index = -1;
|
|
int own_drag_press_x = 0;
|
|
bool own_dragging = false;
|
|
QList<QWidget*> tab_order;
|
|
QWidget* dragged_tab = nullptr;
|
|
bool tab_dragging = false;
|
|
std::array<QString, 8> player_tab_titles{};
|
|
std::vector<Common::ControllerConsoleState::PlayerRow> last_rows;
|
|
/// How each player tab is lit (0 idle, 1 input held, 2 green blink, 3 red blink)
|
|
std::array<int, 8> tab_lit{};
|
|
/// The battery each player tab shows (percent * 2 + charging, -1 none)
|
|
std::array<int, 8> tab_battery{-1, -1, -1, -1, -1, -1, -1, -1};
|
|
std::array<QPointer<QToolButton>, 8> tab_lefts;
|
|
std::array<QPointer<QToolButton>, 8> tab_rights;
|
|
/// The tab to select once its player shows (the controller a drag or an arrow moved)
|
|
int pending_select = -1;
|
|
long long pending_select_until = 0;
|
|
// Checkboxes representing the "Connected Controllers".
|
|
std::array<QCheckBox*, 8> connected_controller_checkboxes{};
|
|
ConfigureInputAdvanced* advanced = nullptr;
|
|
|
|
Core::System& system;
|
|
};
|