Merge branch 'feat/multiplayer-room-overlay' into 'main'

feat(multiplayer): Add in-game multiplayer room overlay

See merge request citron/emulator!112
This commit is contained in:
Zephyron
2025-10-24 10:24:47 +10:00
8 changed files with 406 additions and 293 deletions

View File

@@ -223,6 +223,8 @@ add_executable(citron
util/overlay_dialog.ui util/overlay_dialog.ui
util/performance_overlay.cpp util/performance_overlay.cpp
util/performance_overlay.h util/performance_overlay.h
util/multiplayer_room_overlay.cpp
util/multiplayer_room_overlay.h
util/vram_overlay.cpp util/vram_overlay.cpp
util/vram_overlay.h util/vram_overlay.h
util/sequence_dialog/sequence_dialog.cpp util/sequence_dialog/sequence_dialog.cpp

View File

@@ -180,6 +180,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#endif #endif
#include "citron/util/clickable_label.h" #include "citron/util/clickable_label.h"
#include "citron/util/performance_overlay.h" #include "citron/util/performance_overlay.h"
#include "citron/util/multiplayer_room_overlay.h"
#include "citron/util/vram_overlay.h" #include "citron/util/vram_overlay.h"
#include "citron/vk_device_info.h" #include "citron/vk_device_info.h"
@@ -1095,6 +1096,12 @@ void GMainWindow::InitializeWidgets() {
performance_overlay = new PerformanceOverlay(this); performance_overlay = new PerformanceOverlay(this);
performance_overlay->hide(); performance_overlay->hide();
multiplayer_room_overlay = new MultiplayerRoomOverlay(this);
multiplayer_room_overlay->hide();
connect(this, &GMainWindow::EmulationStarting, multiplayer_room_overlay, &MultiplayerRoomOverlay::OnEmulationStarting);
connect(this, &GMainWindow::EmulationStopping, multiplayer_room_overlay, &MultiplayerRoomOverlay::OnEmulationStopping);
vram_overlay = new VramOverlay(this); vram_overlay = new VramOverlay(this);
vram_overlay->hide(); vram_overlay->hide();
@@ -1379,6 +1386,7 @@ void GMainWindow::InitializeHotkeys() {
LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); LinkActionShortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar"));
LinkActionShortcut(ui->action_Show_Performance_Overlay, QStringLiteral("Toggle Performance Overlay")); LinkActionShortcut(ui->action_Show_Performance_Overlay, QStringLiteral("Toggle Performance Overlay"));
LinkActionShortcut(ui->action_Show_Vram_Overlay, QStringLiteral("Toggle VRAM Overlay")); LinkActionShortcut(ui->action_Show_Vram_Overlay, QStringLiteral("Toggle VRAM Overlay"));
LinkActionShortcut(ui->action_Show_Multiplayer_Room_Overlay, QStringLiteral("Toggle Multiplayer Room Overlay"));
LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen")); LinkActionShortcut(ui->action_Fullscreen, QStringLiteral("Fullscreen"));
LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); LinkActionShortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot"));
LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true); LinkActionShortcut(ui->action_TAS_Start, QStringLiteral("TAS Start/Stop"), true);
@@ -1608,6 +1616,7 @@ void GMainWindow::ConnectMenuEvents() {
connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar);
connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar); connect_menu(ui->action_Show_Status_Bar, &GMainWindow::OnToggleStatusBar);
connect_menu(ui->action_Show_Performance_Overlay, &GMainWindow::OnTogglePerformanceOverlay); connect_menu(ui->action_Show_Performance_Overlay, &GMainWindow::OnTogglePerformanceOverlay);
connect_menu(ui->action_Show_Multiplayer_Room_Overlay, &GMainWindow::OnToggleMultiplayerRoomOverlay);
connect_menu(ui->action_Show_Vram_Overlay, &GMainWindow::OnToggleVramOverlay); connect_menu(ui->action_Show_Vram_Overlay, &GMainWindow::OnToggleVramOverlay);
connect_menu(ui->action_Toggle_Grid_View, &GMainWindow::OnToggleGridView); connect_menu(ui->action_Toggle_Grid_View, &GMainWindow::OnToggleGridView);
@@ -4930,6 +4939,16 @@ void GMainWindow::OnTogglePerformanceOverlay() {
} }
} }
void GMainWindow::OnToggleMultiplayerRoomOverlay() {
if (multiplayer_room_overlay) {
const bool is_checked = ui->action_Show_Multiplayer_Room_Overlay->isChecked();
multiplayer_room_overlay->SetVisible(is_checked);
// We will add a setting for this later if needed.
// UISettings::values.show_multiplayer_room_overlay = is_checked;
}
}
void GMainWindow::OnToggleVramOverlay() { void GMainWindow::OnToggleVramOverlay() {
if (vram_overlay) { if (vram_overlay) {
const bool is_checked = ui->action_Show_Vram_Overlay->isChecked(); const bool is_checked = ui->action_Show_Vram_Overlay->isChecked();

View File

@@ -6,14 +6,12 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <filesystem> #include <filesystem>
#include <QMainWindow> #include <QMainWindow>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QTimer> #include <QTimer>
#include <QTranslator> #include <QTranslator>
#include "common/announce_multiplayer_room.h" #include "common/announce_multiplayer_room.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "configuration/qt_config.h" #include "configuration/qt_config.h"
@@ -39,11 +37,9 @@ class LoadingScreen;
class MicroProfileDialog; class MicroProfileDialog;
class OverlayDialog; class OverlayDialog;
class PerformanceOverlay; class PerformanceOverlay;
class MultiplayerRoomOverlay;
class VramOverlay; class VramOverlay;
class ProfilerWidget; class ProfilerWidget;
// Forward declaration
class PerformanceOverlay;
class ControllerDialog; class ControllerDialog;
class QLabel; class QLabel;
class MultiplayerState; class MultiplayerState;
@@ -58,7 +54,6 @@ enum class GameListShortcutTarget;
enum class DumpRomFSTarget; enum class DumpRomFSTarget;
enum class InstalledEntryType; enum class InstalledEntryType;
class GameListPlaceholder; class GameListPlaceholder;
class QtAmiiboSettingsDialog; class QtAmiiboSettingsDialog;
class QtControllerSelectorDialog; class QtControllerSelectorDialog;
class QtProfileSelectionDialog; class QtProfileSelectionDialog;
@@ -66,75 +61,21 @@ class QtSoftwareKeyboardDialog;
class QtNXWebEngineView; class QtNXWebEngineView;
class UpdaterDialog; class UpdaterDialog;
enum class StartGameType { enum class StartGameType { Normal, Global };
Normal, // Can use custom configuration
Global, // Only uses global configuration
};
namespace Core { namespace Core { enum class SystemResultStatus : u32; class System; }
enum class SystemResultStatus : u32; namespace Core::Frontend { struct CabinetParameters; struct ControllerParameters; struct InlineAppearParameters; struct InlineTextParameters; struct KeyboardInitializeParameters; struct ProfileSelectParameters; }
class System; namespace DiscordRPC { class DiscordInterface; }
} // namespace Core namespace PlayTime { class PlayTimeManager; }
namespace FileSys { class ContentProvider; class ManualContentProvider; class VfsFilesystem; }
namespace Core::Frontend { namespace InputCommon { class InputSubsystem; }
struct CabinetParameters; namespace Service::AM { struct FrontendAppletParameters; enum class AppletId : u32; }
struct ControllerParameters; namespace Service::AM::Frontend { enum class SwkbdResult : u32; enum class SwkbdTextCheckResult : u32; enum class SwkbdReplyType : u32; enum class WebExitReason : u32; }
struct InlineAppearParameters; namespace Service::NFC { class NfcDevice; }
struct InlineTextParameters; namespace Service::NFP { enum class CabinetMode : u8; }
struct KeyboardInitializeParameters; namespace Ui { class MainWindow; }
struct ProfileSelectParameters; enum class EmulatedDirectoryTarget { NAND, SDMC };
} // namespace Core::Frontend namespace VkDeviceInfo { class Record; }
namespace DiscordRPC {
class DiscordInterface;
}
namespace PlayTime {
class PlayTimeManager;
}
namespace FileSys {
class ContentProvider;
class ManualContentProvider;
class VfsFilesystem;
} // namespace FileSys
namespace InputCommon {
class InputSubsystem;
}
namespace Service::AM {
struct FrontendAppletParameters;
enum class AppletId : u32;
} // namespace Service::AM
namespace Service::AM::Frontend {
enum class SwkbdResult : u32;
enum class SwkbdTextCheckResult : u32;
enum class SwkbdReplyType : u32;
enum class WebExitReason : u32;
} // namespace Service::AM::Frontend
namespace Service::NFC {
class NfcDevice;
} // namespace Service::NFC
namespace Service::NFP {
enum class CabinetMode : u8;
} // namespace Service::NFP
namespace Ui {
class MainWindow;
}
enum class EmulatedDirectoryTarget {
NAND,
SDMC,
};
namespace VkDeviceInfo {
class Record;
}
class VolumeButton : public QPushButton { class VolumeButton : public QPushButton {
Q_OBJECT Q_OBJECT
@@ -142,16 +83,12 @@ public:
explicit VolumeButton(QWidget* parent = nullptr) : QPushButton(parent), scroll_multiplier(1) { explicit VolumeButton(QWidget* parent = nullptr) : QPushButton(parent), scroll_multiplier(1) {
connect(&scroll_timer, &QTimer::timeout, this, &VolumeButton::ResetMultiplier); connect(&scroll_timer, &QTimer::timeout, this, &VolumeButton::ResetMultiplier);
} }
signals: signals:
void VolumeChanged(); void VolumeChanged();
protected: protected:
void wheelEvent(QWheelEvent* event) override; void wheelEvent(QWheelEvent* event) override;
private slots: private slots:
void ResetMultiplier(); void ResetMultiplier();
private: private:
int scroll_multiplier; int scroll_multiplier;
QTimer scroll_timer; QTimer scroll_timer;
@@ -160,94 +97,45 @@ private:
class GMainWindow : public QMainWindow { class GMainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
/// Max number of recently loaded items to keep track of
static const int max_recent_files_item = 10; static const int max_recent_files_item = 10;
friend class PerformanceOverlay; friend class PerformanceOverlay;
friend class VramOverlay; friend class VramOverlay;
enum { CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, CREATE_SHORTCUT_MSGBOX_SUCCESS, CREATE_SHORTCUT_MSGBOX_ERROR, CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING };
enum {
CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES,
CREATE_SHORTCUT_MSGBOX_SUCCESS,
CREATE_SHORTCUT_MSGBOX_ERROR,
CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING,
};
public: public:
void filterBarSetChecked(bool state); void filterBarSetChecked(bool state);
void UpdateUITheme(); void UpdateUITheme();
explicit GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan); explicit GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulkan);
~GMainWindow() override; ~GMainWindow() override;
bool DropAction(QDropEvent* event); bool DropAction(QDropEvent* event);
void AcceptDropEvent(QDropEvent* event); void AcceptDropEvent(QDropEvent* event);
MultiplayerState* GetMultiplayerState() { return multiplayer_state; }
/** bool IsEmulationRunning() const { return emulation_running; }
* This is the new function to provide access to the MultiplayerState.
*/
MultiplayerState* GetMultiplayerState() {
return multiplayer_state;
}
signals: signals:
/**
* Signal that is emitted when a new EmuThread has been created and an emulation session is
* about to start. At this time, the core system emulation has been initialized, and all
* emulation handles and memory should be valid.
*
* @param emu_thread Pointer to the newly created EmuThread (to be used by widgets that need to
* access/change emulation state).
*/
void EmulationStarting(EmuThread* emu_thread); void EmulationStarting(EmuThread* emu_thread);
/**
* Signal that is emitted when emulation is about to stop. At this time, the EmuThread and core
* system emulation handles and memory are still valid, but are about become invalid.
*/
void EmulationStopping(); void EmulationStopping();
// Signal that tells widgets to update icons to use the current theme
void UpdateThemedIcons(); void UpdateThemedIcons();
void UpdateInstallProgress(); void UpdateInstallProgress();
void AmiiboSettingsFinished(bool is_success, const std::string& name); void AmiiboSettingsFinished(bool is_success, const std::string& name);
void ControllerSelectorReconfigureFinished(bool is_success); void ControllerSelectorReconfigureFinished(bool is_success);
void ErrorDisplayFinished(); void ErrorDisplayFinished();
void ProfileSelectorFinishedSelection(std::optional<Common::UUID> uuid); void ProfileSelectorFinishedSelection(std::optional<Common::UUID> uuid);
void SoftwareKeyboardSubmitNormalText(Service::AM::Frontend::SwkbdResult result, std::u16string submitted_text, bool confirmed);
void SoftwareKeyboardSubmitNormalText(Service::AM::Frontend::SwkbdResult result, void SoftwareKeyboardSubmitInlineText(Service::AM::Frontend::SwkbdReplyType reply_type, std::u16string submitted_text, s32 cursor_position);
std::u16string submitted_text, bool confirmed);
void SoftwareKeyboardSubmitInlineText(Service::AM::Frontend::SwkbdReplyType reply_type,
std::u16string submitted_text, s32 cursor_position);
void WebBrowserExtractOfflineRomFS(); void WebBrowserExtractOfflineRomFS();
void WebBrowserClosed(Service::AM::Frontend::WebExitReason exit_reason, std::string last_url); void WebBrowserClosed(Service::AM::Frontend::WebExitReason exit_reason, std::string last_url);
void SigInterrupt(); void SigInterrupt();
public slots: public slots:
void OnLoadComplete(); void OnLoadComplete();
void OnExecuteProgram(std::size_t program_index); void OnExecuteProgram(std::size_t program_index);
void OnExit(); void OnExit();
void OnSaveConfig(); void OnSaveConfig();
void AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters, void AmiiboSettingsShowDialog(const Core::Frontend::CabinetParameters& parameters, std::shared_ptr<Service::NFC::NfcDevice> nfp_device);
std::shared_ptr<Service::NFC::NfcDevice> nfp_device);
void AmiiboSettingsRequestExit(); void AmiiboSettingsRequestExit();
void ControllerSelectorReconfigureControllers( void ControllerSelectorReconfigureControllers(const Core::Frontend::ControllerParameters& parameters);
const Core::Frontend::ControllerParameters& parameters);
void ControllerSelectorRequestExit(); void ControllerSelectorRequestExit();
void SoftwareKeyboardInitialize( void SoftwareKeyboardInitialize(bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters);
bool is_inline, Core::Frontend::KeyboardInitializeParameters initialize_parameters);
void SoftwareKeyboardShowNormal(); void SoftwareKeyboardShowNormal();
void SoftwareKeyboardShowTextCheck( void SoftwareKeyboardShowTextCheck(Service::AM::Frontend::SwkbdTextCheckResult text_check_result, std::u16string text_check_message);
Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message);
void SoftwareKeyboardShowInline(Core::Frontend::InlineAppearParameters appear_parameters); void SoftwareKeyboardShowInline(Core::Frontend::InlineAppearParameters appear_parameters);
void SoftwareKeyboardHideInline(); void SoftwareKeyboardHideInline();
void SoftwareKeyboardInlineTextChanged(Core::Frontend::InlineTextParameters text_parameters); void SoftwareKeyboardInlineTextChanged(Core::Frontend::InlineTextParameters text_parameters);
@@ -256,98 +144,51 @@ public slots:
void ErrorDisplayRequestExit(); void ErrorDisplayRequestExit();
void ProfileSelectorSelectProfile(const Core::Frontend::ProfileSelectParameters& parameters); void ProfileSelectorSelectProfile(const Core::Frontend::ProfileSelectParameters& parameters);
void ProfileSelectorRequestExit(); void ProfileSelectorRequestExit();
void WebBrowserOpenWebPage(const std::string& main_url, const std::string& additional_args, void WebBrowserOpenWebPage(const std::string& main_url, const std::string& additional_args, bool is_local);
bool is_local);
void WebBrowserRequestExit(); void WebBrowserRequestExit();
void OnAppFocusStateChanged(Qt::ApplicationState state); void OnAppFocusStateChanged(Qt::ApplicationState state);
void OnTasStateChanged(); void OnTasStateChanged();
private: private:
/// Updates an action's shortcut and text to reflect an updated hotkey from the hotkey registry. void LinkActionShortcut(QAction* action, const QString& action_name, const bool tas_allowed = false);
void LinkActionShortcut(QAction* action, const QString& action_name,
const bool tas_allowed = false);
void RegisterMetaTypes(); void RegisterMetaTypes();
void RegisterAutoloaderContents(); void RegisterAutoloaderContents();
void InitializeWidgets(); void InitializeWidgets();
void InitializeDebugWidgets(); void InitializeDebugWidgets();
void InitializeRecentFileMenuActions(); void InitializeRecentFileMenuActions();
void SetDefaultUIGeometry(); void SetDefaultUIGeometry();
void RestoreUIState(); void RestoreUIState();
void ConnectWidgetEvents(); void ConnectWidgetEvents();
void ConnectMenuEvents(); void ConnectMenuEvents();
void UpdateMenuState(); void UpdateMenuState();
void SetupPrepareForSleep(); void SetupPrepareForSleep();
void PreventOSSleep(); void PreventOSSleep();
void AllowOSSleep(); void AllowOSSleep();
bool LoadROM(const QString& filename, Service::AM::FrontendAppletParameters params); bool LoadROM(const QString& filename, Service::AM::FrontendAppletParameters params);
void BootGame(const QString& filename, Service::AM::FrontendAppletParameters params, void BootGame(const QString& filename, Service::AM::FrontendAppletParameters params, StartGameType with_config = StartGameType::Normal);
StartGameType with_config = StartGameType::Normal);
void BootGameFromList(const QString& filename, StartGameType with_config); void BootGameFromList(const QString& filename, StartGameType with_config);
void ShutdownGame(); void ShutdownGame();
void ShowTelemetryCallout(); void ShowTelemetryCallout();
void SetDiscordEnabled(bool state); void SetDiscordEnabled(bool state);
void LoadAmiibo(const QString& filename); void LoadAmiibo(const QString& filename);
bool SelectAndSetCurrentUser(const Core::Frontend::ProfileSelectParameters& parameters); bool SelectAndSetCurrentUser(const Core::Frontend::ProfileSelectParameters& parameters);
/**
* Stores the filename in the recently loaded files list.
* The new filename is stored at the beginning of the recently loaded files list.
* After inserting the new entry, duplicates are removed meaning that if
* this was inserted from \a OnMenuRecentFile(), the entry will be put on top
* and remove from its previous position.
*
* Finally, this function calls \a UpdateRecentFiles() to update the UI.
*
* @param filename the filename to store
*/
void StoreRecentFile(const QString& filename); void StoreRecentFile(const QString& filename);
/**
* Updates the recent files menu.
* Menu entries are rebuilt from the configuration file.
* If there is no entry in the menu, the menu is greyed out.
*/
void UpdateRecentFiles(); void UpdateRecentFiles();
/**
* If the emulation is running,
* asks the user if he really want to close the emulator
*
* @return true if the user confirmed
*/
bool ConfirmClose(); bool ConfirmClose();
bool ConfirmChangeGame(); bool ConfirmChangeGame();
bool ConfirmForceLockedExit(); bool ConfirmForceLockedExit();
void RequestGameExit(); void RequestGameExit();
void changeEvent(QEvent* event) override; void changeEvent(QEvent* event) override;
void closeEvent(QCloseEvent* event) override; void closeEvent(QCloseEvent* event) override;
std::string CreateTASFramesString(std::array<size_t, InputCommon::TasInput::PLAYER_NUMBER> frames) const;
std::string CreateTASFramesString( #ifdef __unix__
std::array<size_t, InputCommon::TasInput::PLAYER_NUMBER> frames) const; void SetupSigInterrupts();
static void HandleSigInterrupt(int);
#ifdef __unix__ void OnSigInterruptNotifierActivated();
void SetupSigInterrupts(); void SetGamemodeEnabled(bool state);
static void HandleSigInterrupt(int); #endif
void OnSigInterruptNotifierActivated(); Service::AM::FrontendAppletParameters ApplicationAppletParameters();
void SetGamemodeEnabled(bool state); Service::AM::FrontendAppletParameters LibraryAppletParameters(u64 program_id, Service::AM::AppletId applet_id);
#endif std::unique_ptr<FileSys::ManualContentProvider> autoloader_provider;
Service::AM::FrontendAppletParameters ApplicationAppletParameters();
Service::AM::FrontendAppletParameters LibraryAppletParameters(u64 program_id,
Service::AM::AppletId applet_id);
// This will hold and provide all discovered Autoloader content.
std::unique_ptr<FileSys::ManualContentProvider> autoloader_provider;
private slots: private slots:
void OnStartGame(); void OnStartGame();
void OnRestartGame(); void OnRestartGame();
@@ -357,22 +198,17 @@ private slots:
void OnPrepareForSleep(bool prepare_sleep); void OnPrepareForSleep(bool prepare_sleep);
void OnMenuReportCompatibility(); void OnMenuReportCompatibility();
void OnOpenSupport(); void OnOpenSupport();
/// Called whenever a user selects a game in the game list widget.
void OnGameListLoadFile(QString game_path, u64 program_id); void OnGameListLoadFile(QString game_path, u64 program_id);
void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, const std::string& game_path);
const std::string& game_path);
void OnTransferableShaderCacheOpenFile(u64 program_id); void OnTransferableShaderCacheOpenFile(u64 program_id);
void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type); void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type);
void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, const std::string& game_path);
const std::string& game_path);
void OnGameListRemovePlayTimeData(u64 program_id); void OnGameListRemovePlayTimeData(u64 program_id);
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void OnGameListVerifyIntegrity(const std::string& game_path); void OnGameListVerifyIntegrity(const std::string& game_path);
void OnGameListCopyTID(u64 program_id); void OnGameListCopyTID(u64 program_id);
void OnGameListNavigateToGamedbEntry(u64 program_id, void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list);
const CompatibilityList& compatibility_list); void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, GameListShortcutTarget target);
void OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
GameListShortcutTarget target);
void OnGameListOpenDirectory(const QString& directory); void OnGameListOpenDirectory(const QString& directory);
void OnGameListAddDirectory(); void OnGameListAddDirectory();
void OnGameListShowList(bool show); void OnGameListShowList(bool show);
@@ -411,16 +247,13 @@ private slots:
void OnToggleGridView(); void OnToggleGridView();
void OnToggleStatusBar(); void OnToggleStatusBar();
void OnTogglePerformanceOverlay(); void OnTogglePerformanceOverlay();
void OnToggleMultiplayerRoomOverlay();
void OnToggleVramOverlay(); void OnToggleVramOverlay();
void OnDisplayTitleBars(bool); void OnDisplayTitleBars(bool);
// Performance overlay access methods
double GetCurrentFPS() const; double GetCurrentFPS() const;
double GetCurrentFrameTime() const; double GetCurrentFrameTime() const;
u32 GetShadersBuilding() const; u32 GetShadersBuilding() const;
double GetEmulationSpeed() const; double GetEmulationSpeed() const;
// VRAM overlay access methods
u64 GetTotalVram() const; u64 GetTotalVram() const;
u64 GetUsedVram() const; u64 GetUsedVram() const;
u64 GetBufferMemoryUsage() const; u64 GetBufferMemoryUsage() const;
@@ -448,7 +281,6 @@ private slots:
void OnShutdownBeginDialog(); void OnShutdownBeginDialog();
void OnEmulationStopped(); void OnEmulationStopped();
void OnEmulationStopTimeExpired(); void OnEmulationStopTimeExpired();
private: private:
QString GetGameListErrorRemoving(InstalledEntryType type) const; QString GetGameListErrorRemoving(InstalledEntryType type) const;
void RemoveBaseContent(u64 program_id, InstalledEntryType type); void RemoveBaseContent(u64 program_id, InstalledEntryType type);
@@ -460,12 +292,10 @@ private:
void RemoveCustomConfiguration(u64 program_id, const std::string& game_path); void RemoveCustomConfiguration(u64 program_id, const std::string& game_path);
void RemovePlayTimeData(u64 program_id); void RemovePlayTimeData(u64 program_id);
void RemoveCacheStorage(u64 program_id); void RemoveCacheStorage(u64 program_id);
bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, u64* selected_title_id, u8* selected_content_record_type);
u64* selected_title_id, u8* selected_content_record_type);
ContentManager::InstallResult InstallNCA(const QString& filename); ContentManager::InstallResult InstallNCA(const QString& filename);
void MigrateConfigFiles(); void MigrateConfigFiles();
void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, std::string_view gpu_vendor = {});
std::string_view gpu_vendor = {});
void UpdateDockedButton(); void UpdateDockedButton();
void UpdateAPIText(); void UpdateAPIText();
void UpdateFilterText(); void UpdateFilterText();
@@ -485,54 +315,28 @@ private:
bool CheckFirmwarePresence(); bool CheckFirmwarePresence();
void SetFirmwareVersion(); void SetFirmwareVersion();
void ConfigureFilesystemProvider(const std::string& filepath); void ConfigureFilesystemProvider(const std::string& filepath);
/**
* Open (or not) the right confirm dialog based on current setting and game exit lock
* @returns true if the player confirmed or the settings do no require it
*/
bool ConfirmShutdownGame(); bool ConfirmShutdownGame();
QString GetTasStateDescription() const; QString GetTasStateDescription() const;
bool CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title); bool CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title);
bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, std::filesystem::path& out_icon_path);
std::filesystem::path& out_icon_path); bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, const std::filesystem::path& icon_path, const std::filesystem::path& command, const std::string& arguments, const std::string& categories, const std::string& keywords, const std::string& name);
bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, bool question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No), QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
const std::filesystem::path& icon_path,
const std::filesystem::path& command, const std::string& arguments,
const std::string& categories, const std::string& keywords,
const std::string& name);
/**
* Mimic the behavior of QMessageBox::question but link controller navigation to the dialog
* The only difference is that it returns a boolean.
*
* @returns true if buttons contains QMessageBox::Yes and the user clicks on the "Yes" button.
*/
bool question(QWidget* parent, const QString& title, const QString& text,
QMessageBox::StandardButtons buttons =
QMessageBox::StandardButtons(QMessageBox::Yes | QMessageBox::No),
QMessageBox::StandardButton defaultButton = QMessageBox::NoButton);
std::unique_ptr<Ui::MainWindow> ui; std::unique_ptr<Ui::MainWindow> ui;
std::unique_ptr<Core::System> system; std::unique_ptr<Core::System> system;
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager; std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
std::shared_ptr<InputCommon::InputSubsystem> input_subsystem; std::shared_ptr<InputCommon::InputSubsystem> input_subsystem;
MultiplayerState* multiplayer_state = nullptr; MultiplayerState* multiplayer_state = nullptr;
GRenderWindow* render_window; GRenderWindow* render_window;
GameList* game_list; GameList* game_list;
LoadingScreen* loading_screen; LoadingScreen* loading_screen;
QTimer shutdown_timer; QTimer shutdown_timer;
OverlayDialog* shutdown_dialog{}; OverlayDialog* shutdown_dialog{};
PerformanceOverlay* performance_overlay{}; PerformanceOverlay* performance_overlay{};
MultiplayerRoomOverlay* multiplayer_room_overlay{};
VramOverlay* vram_overlay{}; VramOverlay* vram_overlay{};
GameListPlaceholder* game_list_placeholder; GameListPlaceholder* game_list_placeholder;
std::vector<VkDeviceInfo::Record> vk_device_records; std::vector<VkDeviceInfo::Record> vk_device_records;
// Status bar elements
QLabel* message_label = nullptr; QLabel* message_label = nullptr;
QLabel* shader_building_label = nullptr; QLabel* shader_building_label = nullptr;
QLabel* res_scale_label = nullptr; QLabel* res_scale_label = nullptr;
@@ -550,72 +354,42 @@ private:
QWidget* volume_popup = nullptr; QWidget* volume_popup = nullptr;
QSlider* volume_slider = nullptr; QSlider* volume_slider = nullptr;
QTimer status_bar_update_timer; QTimer status_bar_update_timer;
std::unique_ptr<QtConfig> config; std::unique_ptr<QtConfig> config;
// Whether emulation is currently running in citron.
bool emulation_running = false; bool emulation_running = false;
std::unique_ptr<EmuThread> emu_thread; std::unique_ptr<EmuThread> emu_thread;
// The path to the game currently running
QString current_game_path; QString current_game_path;
// Whether a user was set on the command line (skips UserSelector if it's forced to show up)
bool user_flag_cmd_line = false; bool user_flag_cmd_line = false;
bool auto_paused = false; bool auto_paused = false;
bool auto_muted = false; bool auto_muted = false;
QTimer mouse_hide_timer; QTimer mouse_hide_timer;
QTimer update_input_timer; QTimer update_input_timer;
QString startup_icon_theme; QString startup_icon_theme;
bool os_dark_mode = false; bool os_dark_mode = false;
// FS
std::shared_ptr<FileSys::VfsFilesystem> vfs; std::shared_ptr<FileSys::VfsFilesystem> vfs;
std::unique_ptr<FileSys::ManualContentProvider> provider; std::unique_ptr<FileSys::ManualContentProvider> provider;
// Debugger panes
ProfilerWidget* profilerWidget; ProfilerWidget* profilerWidget;
MicroProfileDialog* microProfileDialog; MicroProfileDialog* microProfileDialog;
WaitTreeWidget* waitTreeWidget; WaitTreeWidget* waitTreeWidget;
ControllerDialog* controller_dialog; ControllerDialog* controller_dialog;
QAction* actions_recent_files[max_recent_files_item]; QAction* actions_recent_files[max_recent_files_item];
// stores default icon theme search paths for the platform
QStringList default_theme_paths; QStringList default_theme_paths;
HotkeyRegistry hotkey_registry; HotkeyRegistry hotkey_registry;
QTranslator translator; QTranslator translator;
// Install progress dialog
QProgressDialog* install_progress; QProgressDialog* install_progress;
// Last game booted, used for multi-process apps
QString last_filename_booted; QString last_filename_booted;
// Applets
QtAmiiboSettingsDialog* cabinet_applet = nullptr; QtAmiiboSettingsDialog* cabinet_applet = nullptr;
QtControllerSelectorDialog* controller_applet = nullptr; QtControllerSelectorDialog* controller_applet = nullptr;
QtProfileSelectionDialog* profile_select_applet = nullptr; QtProfileSelectionDialog* profile_select_applet = nullptr;
QDialog* error_applet = nullptr; QDialog* error_applet = nullptr;
QtSoftwareKeyboardDialog* software_keyboard = nullptr; QtSoftwareKeyboardDialog* software_keyboard = nullptr;
QtNXWebEngineView* web_applet = nullptr; QtNXWebEngineView* web_applet = nullptr;
// True if amiibo file select is visible
bool is_amiibo_file_select_active{}; bool is_amiibo_file_select_active{};
// True if load file select is visible
bool is_load_file_select_active{}; bool is_load_file_select_active{};
// True if TAS recording dialog is visible
bool is_tas_recording_dialog_active{}; bool is_tas_recording_dialog_active{};
#ifdef __unix__ #ifdef __unix__
QSocketNotifier* sig_interrupt_notifier; QSocketNotifier* sig_interrupt_notifier;
static std::array<int, 3> sig_interrupt_fds; static std::array<int, 3> sig_interrupt_fds;
#endif #endif
protected: protected:
void dropEvent(QDropEvent* event) override; void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override;

View File

@@ -128,6 +128,7 @@
<addaction name="action_Show_Status_Bar"/> <addaction name="action_Show_Status_Bar"/>
<addaction name="action_Show_Performance_Overlay"/> <addaction name="action_Show_Performance_Overlay"/>
<addaction name="action_Show_Vram_Overlay"/> <addaction name="action_Show_Vram_Overlay"/>
<addaction name="action_Show_Multiplayer_Room_Overlay"/>
<addaction name="action_Toggle_Grid_View"/> <addaction name="action_Toggle_Grid_View"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="menu_Reset_Window_Size"/> <addaction name="menu_Reset_Window_Size"/>
@@ -335,6 +336,17 @@
<string>Show VRAM Monitor</string> <string>Show VRAM Monitor</string>
</property> </property>
</action> </action>
<action name="action_Show_Multiplayer_Room_Overlay">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Show &amp;Multiplayer Room Overlay</string>
</property>
<property name="iconText">
<string>Show Multiplayer Room Overlay</string>
</property>
</action>
<action name="action_Toggle_Grid_View"> <action name="action_Toggle_Grid_View">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>

View File

@@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project // SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-FileCopyrightText: GPL-2.0-or-later
#include <QAction> #include <QAction>
#include <QApplication> #include <QApplication>
@@ -334,3 +335,8 @@ void MultiplayerState::UpdateGameList(QStandardItemModel* game_list) {
host_room->UpdateGameList(game_list); host_room->UpdateGameList(game_list);
} }
} }
// NEW: Definition for our check
bool MultiplayerState::IsClientRoomVisible() const {
return client_room && client_room->isVisible();
}

View File

@@ -34,20 +34,12 @@ public:
QAction* show_room, Core::System& system_); QAction* show_room, Core::System& system_);
~MultiplayerState(); ~MultiplayerState();
/**
* This is the new function to safely access the multiplayer session.
*/
std::shared_ptr<Core::AnnounceMultiplayerSession> GetSession() { std::shared_ptr<Core::AnnounceMultiplayerSession> GetSession() {
return announce_multiplayer_session; return announce_multiplayer_session;
} }
/**
* Close all open multiplayer related dialogs
*/
void Close(); void Close();
void SetNotificationStatus(NotificationStatus state); void SetNotificationStatus(NotificationStatus state);
void UpdateNotificationStatus(); void UpdateNotificationStatus();
ClickableLabel* GetStatusText() const { ClickableLabel* GetStatusText() const {
@@ -60,18 +52,14 @@ public:
void retranslateUi(); void retranslateUi();
/** Network::RoomNetwork& GetRoomNetwork() {
* Whether a public room is being hosted or not. return room_network;
* When this is true, Web Services configuration should be disabled. }
*/
bool IsClientRoomVisible() const; // NEW: Declaration for our check
bool IsHostingPublicRoom() const; bool IsHostingPublicRoom() const;
void UpdateCredentials(); void UpdateCredentials();
/**
* Updates the multiplayer dialogs with a new game list model.
* This model should be the original model of the game list.
*/
void UpdateGameList(QStandardItemModel* game_list); void UpdateGameList(QStandardItemModel* game_list);
public slots: public slots:

View File

@@ -0,0 +1,235 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QPainter>
#include <QPainterPath>
#include <QScreen>
#include <QTimer>
#include <QMouseEvent>
#include <QWindow>
#include <QSizeGrip>
#include <QGridLayout>
#include "citron/main.h"
#include "citron/bootmanager.h"
#include "citron/util/multiplayer_room_overlay.h"
#include "network/network.h"
#include "network/room.h"
MultiplayerRoomOverlay::MultiplayerRoomOverlay(GMainWindow* parent)
: QWidget(parent), main_window(parent) {
setAttribute(Qt::WA_TranslucentBackground, true);
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool | Qt::WindowStaysOnTopHint);
setFocusPolicy(Qt::ClickFocus);
background_color = QColor(20, 20, 20, 180);
border_color = QColor(60, 60, 60, 120);
main_layout = new QGridLayout(this);
main_layout->setContentsMargins(padding, padding, 0, 0); // No margins on bottom/right for grip
main_layout->setSpacing(6);
players_online_label = new QLabel(this);
chat_room_widget = new ChatRoom(this);
size_grip = new QSizeGrip(this);
players_online_label->setFont(QFont(QString::fromUtf8("Segoe UI"), 10, QFont::Bold));
players_online_label->setStyleSheet(QString::fromUtf8("color: #E0E0E0;"));
players_online_label->setText(QString::fromUtf8("Players Online: 0"));
players_online_label->setAttribute(Qt::WA_TransparentForMouseEvents, true);
size_grip->setFixedSize(16, 16);
if (main_window) {
GRenderWindow* render_window = main_window->findChild<GRenderWindow*>();
if (render_window) {
render_window->installEventFilter(this);
}
}
main_layout->addWidget(players_online_label, 0, 0, 1, 2);
main_layout->addWidget(chat_room_widget, 1, 0, 1, 2);
main_layout->addWidget(size_grip, 1, 1, 1, 1, Qt::AlignBottom | Qt::AlignRight);
main_layout->setRowStretch(1, 1);
main_layout->setColumnStretch(0, 1);
setLayout(main_layout);
update_timer.setSingleShot(false);
connect(&update_timer, &QTimer::timeout, this, &MultiplayerRoomOverlay::UpdateRoomData);
setMinimumSize(280, 220);
resize(320, 280);
UpdatePosition();
}
MultiplayerRoomOverlay::~MultiplayerRoomOverlay() {
DisconnectFromRoom();
}
void MultiplayerRoomOverlay::OnEmulationStarting() {
// When emulation starts, resume updates if we are visible.
if (is_visible) {
ConnectToRoom();
update_timer.start(500);
}
}
void MultiplayerRoomOverlay::OnEmulationStopping() {
// CRASH FIX: When emulation stops, immediately disconnect from network objects.
update_timer.stop();
DisconnectFromRoom();
}
void MultiplayerRoomOverlay::SetVisible(bool visible) {
if (is_visible == visible) return;
is_visible = visible;
if (visible) {
show();
// Only start connecting and updating if emulation is running.
if (main_window && main_window->IsEmulationRunning()) {
ConnectToRoom();
update_timer.start(500);
}
} else {
hide();
update_timer.stop();
DisconnectFromRoom();
}
}
void MultiplayerRoomOverlay::paintEvent(QPaintEvent* event) {
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
QPainterPath background_path;
background_path.addRoundedRect(rect(), corner_radius, corner_radius);
painter.fillPath(background_path, background_color);
painter.setPen(QPen(border_color, border_width));
painter.drawPath(background_path);
}
void MultiplayerRoomOverlay::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); if (!has_been_moved) UpdatePosition(); }
bool MultiplayerRoomOverlay::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::MouseButtonPress) { if (chat_room_widget->hasFocus()) { chat_room_widget->clearFocus(); } } return QObject::eventFilter(watched, event); }
#if defined(Q_OS_LINUX)
void MultiplayerRoomOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if (size_grip->geometry().contains(event->pos())) {
// Let the size grip handle the event
} else if (!childAt(event->pos()) || childAt(event->pos()) == this) {
if (windowHandle()) windowHandle()->startSystemMove();
}
}
QWidget::mousePressEvent(event);
}
void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) { QWidget::mouseMoveEvent(event); }
#else
void MultiplayerRoomOverlay::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if (size_grip->geometry().contains(event->pos())) {
// Let the size grip handle the event
} else if (!childAt(event->pos()) || childAt(event->pos()) == this) {
is_dragging = true;
drag_start_pos = event->globalPosition().toPoint();
widget_start_pos = this->pos();
setCursor(Qt::ClosedHandCursor);
}
}
QWidget::mousePressEvent(event);
}
void MultiplayerRoomOverlay::mouseMoveEvent(QMouseEvent* event) {
if (is_dragging) {
QPoint delta = event->globalPosition().toPoint() - drag_start_pos;
move(widget_start_pos + delta);
}
QWidget::mouseMoveEvent(event); // Corrected typo here
}
#endif
void MultiplayerRoomOverlay::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton && is_dragging) {
is_dragging = false;
has_been_moved = true;
setCursor(Qt::ArrowCursor);
}
QWidget::mouseReleaseEvent(event);
}
void MultiplayerRoomOverlay::ConnectToRoom() {
if (!main_window) return;
multiplayer_state = main_window->GetMultiplayerState();
if (!multiplayer_state) return;
if (multiplayer_state->IsClientRoomVisible()) {
chat_room_widget->setEnabled(false);
chat_room_widget->Clear();
chat_room_widget->AppendStatusMessage(tr("Chat available in main window."));
return;
}
chat_room_widget->setEnabled(true);
auto& room_network = multiplayer_state->GetRoomNetwork();
room_member = room_network.GetRoomMember().lock();
if (room_member) {
chat_room_widget->Initialize(&room_network);
} else {
chat_room_widget->Clear();
chat_room_widget->AppendStatusMessage(tr("Not connected to a room."));
}
}
void MultiplayerRoomOverlay::DisconnectFromRoom() {
chat_room_widget->Clear();
room_member.reset();
multiplayer_state = nullptr;
players_online_label->setText(QString::fromUtf8("Players Online: 0"));
}
void MultiplayerRoomOverlay::UpdateRoomData() {
if (!multiplayer_state) {
ConnectToRoom();
return;
}
if (multiplayer_state->IsClientRoomVisible()) {
if (chat_room_widget->isEnabled()) {
chat_room_widget->setEnabled(false);
chat_room_widget->Clear();
chat_room_widget->AppendStatusMessage(tr("Chat available in main window."));
}
} else {
if (!chat_room_widget->isEnabled()) {
ConnectToRoom();
}
}
if (room_member && room_member->GetState() >= Network::RoomMember::State::Joined) {
const auto& members = room_member->GetMemberInformation();
QString label_text = QString::fromStdString("Players Online: <span style='color: #4CAF50;'>%1</span>").arg(members.size());
players_online_label->setText(label_text);
if (chat_room_widget->isEnabled()) {
chat_room_widget->SetPlayerList(members);
}
} else {
players_online_label->setText(QString::fromUtf8("Players Online: 0"));
if (!room_member && !multiplayer_state->IsClientRoomVisible()) {
chat_room_widget->Clear();
chat_room_widget->AppendStatusMessage(tr("Not connected to a room."));
ConnectToRoom();
}
}
}
void MultiplayerRoomOverlay::UpdatePosition() {
if (!main_window) return;
if (!has_been_moved) {
QPoint main_window_pos = main_window->mapToGlobal(QPoint(0, 0));
move(main_window_pos.x() + main_window->width() - this->width() - 10, main_window_pos.y() + 10);
}
}

View File

@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWidget>
#include <QTimer>
#include <QPainter>
#include <QLabel>
#include <QGridLayout>
#include "citron/multiplayer/state.h"
#include "citron/multiplayer/chat_room.h"
class GMainWindow;
class QSizeGrip;
class MultiplayerRoomOverlay : public QWidget {
Q_OBJECT
public:
explicit MultiplayerRoomOverlay(GMainWindow* parent);
~MultiplayerRoomOverlay() override;
void SetVisible(bool visible);
bool IsVisible() const { return is_visible; }
public slots:
// These slots are connected to the main window to prevent crashes.
void OnEmulationStarting();
void OnEmulationStopping();
protected:
void paintEvent(QPaintEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
bool eventFilter(QObject* watched, QEvent* event) override;
private slots:
void UpdateRoomData();
private:
void UpdatePosition();
void ConnectToRoom();
void DisconnectFromRoom();
GMainWindow* main_window;
QTimer update_timer;
// UI Elements
QLabel* players_online_label;
ChatRoom* chat_room_widget;
QGridLayout* main_layout;
QSizeGrip* size_grip;
// Network and Data
MultiplayerState* multiplayer_state = nullptr;
std::shared_ptr<Network::RoomMember> room_member;
// Display settings
bool is_visible = false;
QColor background_color;
QColor border_color;
// Layout
int padding = 12;
int border_width = 1;
int corner_radius = 10;
// Drag functionality
bool is_dragging = false;
bool has_been_moved = false;
QPoint drag_start_pos;
QPoint widget_start_pos;
};