diff --git a/.history/.gitmodules_20250207123101 b/.history/.gitmodules_20250207123101 deleted file mode 100644 index c039794e..00000000 --- a/.history/.gitmodules_20250207123101 +++ /dev/null @@ -1,93 +0,0 @@ -[submodule "boost"] - path = externals/boost - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "nihstro"] - path = externals/nihstro - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "soundtouch"] - path = externals/soundtouch - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "catch2"] - path = externals/catch2 - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "dynarmic"] - path = externals/dynarmic - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "xbyak"] - path = externals/xbyak - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "fmt"] - path = externals/fmt - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "enet"] - path = externals/enet - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "inih"] - path = externals/inih/inih - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "libressl"] - path = externals/libressl - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "libusb"] - path = externals/libusb/libusb - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cubeb"] - path = externals/cubeb - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "discord-rpc"] - path = externals/discord-rpc - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cpp-jwt"] - path = externals/cpp-jwt - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "teakra"] - path = externals/teakra - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "lodepng"] - path = externals/lodepng/lodepng - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "zstd"] - path = externals/zstd - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "libyuv"] - path = externals/libyuv - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "sdl2"] - path = externals/sdl2/SDL - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cryptopp-cmake"] - path = externals/cryptopp-cmake - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cryptopp"] - path = externals/cryptopp - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "dds-ktx"] - path = externals/dds-ktx - url = https://github.com/septag/dds-ktx -[submodule "openal-soft"] - path = externals/openal-soft - url = https://github.com/kcat/openal-soft -[submodule "glslang"] - path = externals/glslang - url = https://github.com/KhronosGroup/glslang -[submodule "vma"] - path = externals/vma - url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator -[submodule "vulkan-headers"] - path = externals/vulkan-headers - url = https://github.com/KhronosGroup/Vulkan-Headers -[submodule "sirit"] - path = externals/sirit - url = https://github.com/PabloMK7/sirit -[submodule "faad2"] - path = externals/faad2/faad2 - url = https://github.com/knik0/faad2 -[submodule "library-headers"] - path = externals/library-headers - url = https://github.com/PabloMK7/ext-library-headers.git -[submodule "libadrenotools"] - path = externals/libadrenotools - url = https://github.com/bylaws/libadrenotools -[submodule "oaknut"] - path = externals/oaknut - url = https://github.com/merryhime/oaknut.git diff --git a/.history/.gitmodules_20250207123201 b/.history/.gitmodules_20250207123201 deleted file mode 100644 index 00cd86ec..00000000 --- a/.history/.gitmodules_20250207123201 +++ /dev/null @@ -1,93 +0,0 @@ -[submodule "boost"] - path = externals/boost - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "nihstro"] - path = externals/nihstro - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "soundtouch"] - path = externals/soundtouch - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "catch2"] - path = externals/catch2 - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "dynarmic"] - path = externals/dynarmic - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "xbyak"] - path = externals/xbyak - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "fmt"] - path = externals/fmt - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "enet"] - path = externals/enet - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "inih"] - path = externals/inih/inih - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "libressl"] - path = externals/libressl - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "libusb"] - path = externals/libusb/libusb - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cubeb"] - path = externals/cubeb - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "discord-rpc"] - path = externals/discord-rpc - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cpp-jwt"] - path = externals/cpp-jwt - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "teakra"] - path = externals/teakra - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "lodepng"] - path = externals/lodepng/lodepng - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "zstd"] - path = externals/zstd - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "libyuv"] - path = externals/libyuv - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "sdl2"] - path = externals/sdl2/SDL - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cryptopp-cmake"] - path = externals/cryptopp-cmake - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "cryptopp"] - path = externals/cryptopp - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "dds-ktx"] - path = externals/dds-ktx - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "openal-soft"] - path = externals/openal-soft - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "glslang"] - path = externals/glslang - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "vma"] - path = externals/vma - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "vulkan-headers"] - path = externals/vulkan-headers - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "sirit"] - path = externals/sirit - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "faad2"] - path = externals/faad2/faad2 - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "library-headers"] - path = externals/library-headers - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "libadrenotools"] - path = externals/libadrenotools - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS -[submodule "oaknut"] - path = externals/oaknut - url = http://rocky.hifuu.ink:3000/gh0s7/Lucina3DS diff --git a/.history/src/lucina3ds_qt/main_20250209230836.cpp b/.history/src/lucina3ds_qt/main_20250209230836.cpp new file mode 100644 index 00000000..ba244322 --- /dev/null +++ b/.history/src/lucina3ds_qt/main_20250209230836.cpp @@ -0,0 +1,3345 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include // for chdir +#endif +#ifdef _WIN32 +#include +#endif +#ifdef __unix__ +#include +#include +#include +#include "common/linux/gamemode.h" +#endif +#include "lucina3ds_qt/aboutdialog.h" +#include "lucina3ds_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/swkbd.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/compatdb.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/discord.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/loading_screen.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/qt_image_interface.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/graphics_device_info.h" +#include "common/arch.h" +#include "common/common_paths.h" +#include "common/detached_tasks.h" +#include "common/dynamic_library/dynamic_library.h" +#include "common/file_util.h" +#include "common/literals.h" +#include "common/logging/backend.h" +#include "common/logging/log.h" +#include "common/memory_detect.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#if LUCINA3DS_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/settings.h" +#include "core/core.h" +#include "core/dumping/backend.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_source_sd_savedata.h" +#include "core/frontend/applets/default_applets.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/nfc/nfc.h" +#include "core/loader/loader.h" +#include "core/movie.h" +#include "core/savestate.h" +#include "core/system_titles.h" +#include "input_common/main.h" +#include "network/network_settings.h" +#include "ui_main.h" +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" + +#ifdef __APPLE__ +#include "common/apple_authorization.h" +#endif + +#ifdef USE_DISCORD_PRESENCE +#include "lucina3ds_qt/discord_impl.h" +#endif + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); +#endif + +#ifdef _WIN32 +extern "C" { +// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +} +#endif + +#ifdef HAVE_SDL2 +#include +#endif + +constexpr int default_mouse_timeout = 2500; + +/** + * "Callouts" are one-time instructional messages shown to the user. In the config settings, there + * is a bitfield "callout_flags" options, used to track if a message has already been shown to the + * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. + */ + +const int GMainWindow::max_recent_files_item; + +static QString PrettyProductName() { +#ifdef _WIN32 + // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 + // With that notation change they changed the registry key used to denote the current version + QSettings windows_registry( + QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), + QSettings::NativeFormat); + const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); + if (release_id == QStringLiteral("2009")) { + const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); + const QString display_version = + windows_registry.value(QStringLiteral("DisplayVersion")).toString(); + const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); + const u32 version = current_build >= 22000 ? 11 : 10; + return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") + .arg(QString::number(version), display_version, QString::number(current_build), + QString::number(ubr)); + } +#endif + return QSysInfo::prettyProductName(); +} + +GMainWindow::GMainWindow(Core::System& system_) + : ui{std::make_unique()}, system{system_}, movie{system.Movie()}, + config{std::make_unique()}, emu_thread{nullptr} { + Common::Log::Initialize(); + Common::Log::Start(); + + Debugger::ToggleConsole(); + +#ifdef __unix__ + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); +#endif + + // register types to use in slots and signals + qRegisterMetaType("std::size_t"); + qRegisterMetaType("Service::AM::InstallStatus"); + + // Register CameraFactory + qt_cameras = std::make_shared(); + Camera::RegisterFactory("image", std::make_unique()); + Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + + LoadTranslation(); + + Pica::g_debug_context = Pica::DebugContext::Construct(); + setAcceptDrops(true); + ui->setupUi(this); + statusBar()->hide(); + + default_theme_paths = QIcon::themeSearchPaths(); + UpdateUITheme(); + + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(); + + Network::Init(); + + movie.SetPlaybackCompletionCallback([this] { + QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); + }); + + InitializeWidgets(); + InitializeDebugWidgets(); + InitializeRecentFileMenuActions(); + InitializeSaveStateMenuActions(); + InitializeHotkeys(); +#if ENABLE_QT_UPDATER + ShowUpdaterWidgets(); +#else + ui->action_Check_For_Updates->setVisible(false); + ui->action_Open_Maintenance_Tool->setVisible(false); +#endif + + SetDefaultUIGeometry(); + RestoreUIState(); + + ConnectAppEvents(); + ConnectMenuEvents(); + ConnectWidgetEvents(); + + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + Common::g_scm_desc); +#if LUCINA3DS_ARCH(x86_64) + const auto& caps = Common::GetCPUCaps(); + std::string cpu_string = caps.cpu_string; + if (caps.avx || caps.avx2 || caps.avx512) { + cpu_string += " | AVX"; + if (caps.avx512) { + cpu_string += "512"; + } else if (caps.avx2) { + cpu_string += '2'; + } + if (caps.fma || caps.fma4) { + cpu_string += " | FMA"; + } + } + LOG_INFO(Frontend, "Host CPU: {}", cpu_string); +#endif + LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); + const auto& mem_info = Common::GetMemInfo(); + using namespace Common::Literals; + LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB}); + LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB}); + UpdateWindowTitle(); + + show(); + + game_list->LoadCompatibilityList(); + game_list->PopulateAsync(UISettings::values.game_dirs); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); + +#ifdef ENABLE_OPENGL + gl_renderer = GetOpenGLRenderer(); +#if defined(_WIN32) + if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { + // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ + // We can override the version here to allow Citra to work. + // TODO: Remove this when OpenGL 4.0+ is advertised. + qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); + } +#endif +#endif + +#ifdef ENABLE_VULKAN + physical_devices = GetVulkanPhysicalDevices(); + if (physical_devices.empty()) { + QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), + tr("Vulkan initialization failed during boot.
" + "Your GPU may not support Vulkan 1.1, or you do not " + "have the latest graphics driver.")); + } +#endif + +#if ENABLE_QT_UPDATER + if (UISettings::values.check_for_update_on_start) { + CheckForUpdates(); + } +#endif + + QStringList args = QApplication::arguments(); + if (args.size() < 2) { + return; + } + + QString game_path; + for (int i = 1; i < args.size(); ++i) { + // Preserves drag/drop functionality + if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { + game_path = args[1]; + break; + } + + // Launch game in fullscreen mode + if (args[i] == QStringLiteral("-f")) { + ui->action_Fullscreen->setChecked(true); + continue; + } + + // Launch game in windowed mode + if (args[i] == QStringLiteral("-w")) { + ui->action_Fullscreen->setChecked(false); + continue; + } + + // Launch game at path + if (args[i] == QStringLiteral("-g")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + game_path = args[++i]; + } + } + + if (!game_path.isEmpty()) { + BootGame(game_path); + } +} + +GMainWindow::~GMainWindow() { + // Will get automatically deleted otherwise + if (!render_window->parent()) { + delete render_window; + } + + Pica::g_debug_context.reset(); + Network::Shutdown(); +} + +void GMainWindow::InitializeWidgets() { +#ifdef LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING + ui->action_Report_Compatibility->setVisible(true); +#endif + render_window = new GRenderWindow(this, emu_thread.get(), system, false); + secondary_window = new GRenderWindow(this, emu_thread.get(), system, true); + render_window->hide(); + secondary_window->hide(); + secondary_window->setParent(nullptr); + + game_list = new GameList(this); + ui->horizontalLayout->addWidget(game_list); + + game_list_placeholder = new GameListPlaceholder(this); + ui->horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui->horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, this, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + render_window->activateWindow(); + } + }); + + InputCommon::Init(); + multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(), + ui->action_Leave_Room, ui->action_Show_Room); + multiplayer_state->setVisible(false); + +#if ENABLE_QT_UPDATER + // Setup updater + updater = new Updater(this); + UISettings::values.updater_found = updater->HasUpdater(); +#endif + + UpdateBootHomeMenuState(); + + // Create status bar + message_label = new QLabel(); + // Configured separately for left alignment + message_label->setFrameStyle(QFrame::NoFrame); + message_label->setContentsMargins(4, 0, 4, 0); + message_label->setAlignment(Qt::AlignLeft); + statusBar()->addPermanentWidget(message_label, 1); + + progress_bar = new QProgressBar(); + progress_bar->hide(); + statusBar()->addPermanentWidget(progress_bar); + + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + + emu_speed_label = new QLabel(); + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label = new QLabel(); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label = new QLabel(); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { + label->setVisible(false); + label->setFrameStyle(QFrame::NoFrame); + label->setContentsMargins(4, 0, 4, 0); + statusBar()->addPermanentWidget(label); + } + + // Setup Graphics API button + graphics_api_button = new QPushButton(); + graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton")); + graphics_api_button->setFocusPolicy(Qt::NoFocus); + UpdateAPIIndicator(); + + connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); }); + + statusBar()->insertPermanentWidget(0, graphics_api_button); + + volume_popup = new QWidget(this); + volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); + volume_popup->setLayout(new QVBoxLayout()); + volume_popup->setMinimumWidth(200); + + volume_slider = new QSlider(Qt::Horizontal); + volume_slider->setObjectName(QStringLiteral("volume_slider")); + volume_slider->setMaximum(100); + volume_slider->setPageStep(5); + connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { + Settings::values.audio_muted = false; + const auto value = static_cast(percentage) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); + }); + volume_popup->layout()->addWidget(volume_slider); + + volume_button = new QPushButton(); + volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + volume_button->setFocusPolicy(Qt::NoFocus); + volume_button->setCheckable(true); + UpdateVolumeUI(); + connect(volume_button, &QPushButton::clicked, this, [&] { + UpdateVolumeUI(); + volume_popup->setVisible(!volume_popup->isVisible()); + QRect rect = volume_button->geometry(); + QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); + bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); + volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); + }); + statusBar()->insertPermanentWidget(1, volume_button); + + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + + statusBar()->setVisible(true); + + // Removes an ugly inner border from the status bar widgets under Linux + setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); + + QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows); +} + +void GMainWindow::InitializeDebugWidgets() { + connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this, + &GMainWindow::OnCreateGraphicsSurfaceViewer); + + QMenu* debug_menu = ui->menu_View_Debugging; + +#if MICROPROFILE_ENABLED + microProfileDialog = new MicroProfileDialog(this); + microProfileDialog->hide(); + debug_menu->addAction(microProfileDialog->toggleViewAction()); +#endif + + registersWidget = new RegistersWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, registersWidget); + registersWidget->hide(); + debug_menu->addAction(registersWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, registersWidget, + &RegistersWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, registersWidget, + &RegistersWidget::OnEmulationStopping); + + graphicsWidget = new GPUCommandStreamWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsWidget); + graphicsWidget->hide(); + debug_menu->addAction(graphicsWidget->toggleViewAction()); + + graphicsCommandsWidget = new GPUCommandListWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); + graphicsCommandsWidget->hide(); + debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); + + graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); + graphicsBreakpointsWidget->hide(); + debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); + + graphicsVertexShaderWidget = + new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget); + graphicsVertexShaderWidget->hide(); + debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction()); + + graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget); + graphicsTracingWidget->hide(); + debug_menu->addAction(graphicsTracingWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStopping); + + waitTreeWidget = new WaitTreeWidget(system, this); + addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget); + waitTreeWidget->hide(); + debug_menu->addAction(waitTreeWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, waitTreeWidget, + &WaitTreeWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + &WaitTreeWidget::OnEmulationStopping); + + lleServiceModulesWidget = new LLEServiceModulesWidget(this); + addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget); + lleServiceModulesWidget->hide(); + debug_menu->addAction(lleServiceModulesWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, + [this] { lleServiceModulesWidget->setDisabled(true); }); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + [this] { lleServiceModulesWidget->setDisabled(false); }); + + ipcRecorderWidget = new IPCRecorderWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); + ipcRecorderWidget->hide(); + debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, + &IPCRecorderWidget::OnEmulationStarting); +} + +void GMainWindow::InitializeRecentFileMenuActions() { + for (int i = 0; i < max_recent_files_item; ++i) { + actions_recent_files[i] = new QAction(this); + actions_recent_files[i]->setVisible(false); + connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); + + ui->menu_recent_files->addAction(actions_recent_files[i]); + } + ui->menu_recent_files->addSeparator(); + QAction* action_clear_recent_files = new QAction(this); + action_clear_recent_files->setText(tr("Clear Recent Files")); + connect(action_clear_recent_files, &QAction::triggered, this, [this] { + UISettings::values.recent_files.clear(); + UpdateRecentFiles(); + }); + ui->menu_recent_files->addAction(action_clear_recent_files); + + UpdateRecentFiles(); +} + +void GMainWindow::InitializeSaveStateMenuActions() { + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i] = new QAction(this); + actions_load_state[i]->setData(i + 1); + connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); + ui->menu_Load_State->addAction(actions_load_state[i]); + + actions_save_state[i] = new QAction(this); + actions_save_state[i]->setData(i + 1); + connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); + ui->menu_Save_State->addAction(actions_save_state[i]); + } + + connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + if (newest_slot != 0) { + actions_load_state[newest_slot - 1]->trigger(); + } + }); + connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + actions_save_state[oldest_slot - 1]->trigger(); + }); + + connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + + UpdateSaveStates(); +} + +void GMainWindow::InitializeHotkeys() { + hotkey_registry.LoadHotkeys(); + + const QString main_window = QStringLiteral("Main Window"); + const QString fullscreen = QStringLiteral("Fullscreen"); + const QString toggle_screen_layout = QStringLiteral("Toggle Screen Layout"); + const QString swap_screens = QStringLiteral("Swap Screens"); + const QString rotate_screens = QStringLiteral("Rotate Screens Upright"); + + const auto link_action_shortcut = [&](QAction* action, const QString& action_name) { + static const QString main_window = QStringLiteral("Main Window"); + action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); + action->setShortcutContext(hotkey_registry.GetShortcutContext(main_window, action_name)); + action->setAutoRepeat(false); + this->addAction(action); + }; + + link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); + link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); + link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); + link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lucina3DS")); + link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); + link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); + link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); + link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); + link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + link_action_shortcut(ui->action_Fullscreen, fullscreen); + link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, swap_screens); + link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, rotate_screens); + link_action_shortcut(ui->action_Enable_Frame_Advancing, + QStringLiteral("Toggle Frame Advancing")); + link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame")); + link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); + link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); + link_action_shortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); + link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); + link_action_shortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); + link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); + link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); + + const auto add_secondary_window_hotkey = [this](QKeySequence hotkey, const char* slot) { + // This action will fire specifically when secondary_window is in focus + QAction* secondary_window_action = new QAction(secondary_window); + secondary_window_action->setShortcut(hotkey); + + connect(secondary_window_action, SIGNAL(triggered()), this, slot); + secondary_window->addAction(secondary_window_action); + }; + + // Use the same fullscreen hotkey as the main window + const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen); + add_secondary_window_hotkey(fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen())); + + const auto toggle_screen_hotkey = + hotkey_registry.GetKeySequence(main_window, toggle_screen_layout); + add_secondary_window_hotkey(toggle_screen_hotkey, SLOT(ToggleScreenLayout())); + + const auto swap_screen_hotkey = hotkey_registry.GetKeySequence(main_window, swap_screens); + add_secondary_window_hotkey(swap_screen_hotkey, SLOT(TriggerSwapScreens())); + + const auto rotate_screen_hotkey = hotkey_registry.GetKeySequence(main_window, rotate_screens); + add_secondary_window_hotkey(rotate_screen_hotkey, SLOT(TriggerRotateScreens())); + + const auto connect_shortcut = [&](const QString& action_name, const auto& function) { + const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this); + connect(hotkey, &QShortcut::activated, this, function); + }; + + connect(hotkey_registry.GetHotkey(main_window, toggle_screen_layout, render_window), + &QShortcut::activated, this, &GMainWindow::ToggleScreenLayout); + + connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { + if (emulation_running) { + ui->action_Fullscreen->setChecked(false); + ToggleFullscreen(); + } + }); + connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] { + Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Toggle Texture Dumping"), + [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + connect_shortcut(QStringLiteral("Toggle Custom Textures"), + [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes + // the variable hold a garbage value after this function exits + static constexpr u16 SPEED_LIMIT_STEP = 5; + connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + return; + } + if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() + + SPEED_LIMIT_STEP); + } else { + Settings::values.frame_limit = 0; + } + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + Settings::values.frame_limit = 995; + } else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() - + SPEED_LIMIT_STEP); + UpdateStatusBar(); + } + UpdateStatusBar(); + }); + + connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); + connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); + connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); + + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the + // variable hold a garbage value after this function exits + static constexpr u16 FACTOR_3D_STEP = 5; + connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d > 0) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); + connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d < 100) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = + factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); +} + +void GMainWindow::SetDefaultUIGeometry() { + // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half + const QRect screenRect = screen()->geometry(); + + const int w = screenRect.width() * 2 / 3; + const int h = screenRect.height() / 2; + const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; + const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100; + + setGeometry(x, y, w, h); +} + +void GMainWindow::RestoreUIState() { + restoreGeometry(UISettings::values.geometry); + restoreState(UISettings::values.state); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); +#if MICROPROFILE_ENABLED + microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); + microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); +#endif + + game_list->LoadInterfaceLayout(); + + ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); + ToggleWindowMode(); + + ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); + SyncMenuUISettings(); + + ui->action_Display_Dock_Widget_Headers->setChecked( + UISettings::values.display_titlebar.GetValue()); + OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked()); + + ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + + ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); +} + +void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { + if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && + state != Qt::ApplicationActive) { + LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); + } + if (!emulation_running) { + return; + } + if (UISettings::values.pause_when_in_background) { + if (emu_thread->IsRunning() && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + auto_paused = true; + OnPauseGame(); + } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { + auto_paused = false; + OnStartGame(); + } + } + if (UISettings::values.mute_when_in_background) { + if (!Settings::values.audio_muted && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + Settings::values.audio_muted = true; + auto_muted = true; + } else if (auto_muted && state == Qt::ApplicationActive) { + Settings::values.audio_muted = false; + auto_muted = false; + } + UpdateVolumeUI(); + } +} + +bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::FileOpen) { + emit FileOpen(static_cast(event)); + return true; + } + return false; +} + +void GMainWindow::ConnectAppEvents() { + const auto filter = new GApplicationEventFilter(); + QGuiApplication::instance()->installEventFilter(filter); + + connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen); +} + +void GMainWindow::ConnectWidgetEvents() { + connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); + connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, + &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); + connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &GMainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, this, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); + + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, + &GMainWindow::OnGameListOpenPerGameProperties); + + connect(this, &GMainWindow::EmulationStarting, render_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, render_window, + &GRenderWindow::OnEmulationStopping); + connect(this, &GMainWindow::EmulationStarting, secondary_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, secondary_window, + &GRenderWindow::OnEmulationStopping); + + connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); + connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); + connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); +} + +void GMainWindow::ConnectMenuEvents() { + const auto connect_menu = [&](QAction* action, const auto& event_fn) { + connect(action, &QAction::triggered, this, event_fn); + // Add actions to this window so that hiding menus in fullscreen won't disable them + addAction(action); + // Add actions to the render window so that they work outside of single window mode + render_window->addAction(action); + }; + + // File + connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); + connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), + [this, region] { OnMenuBootHomeMenu(region); }); + } + connect_menu(ui->action_Exit, &QMainWindow::close); + connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); + connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo); + + // Emulation + connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); + connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); + connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); }); + connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); + connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); + connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); + + // View + connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); + connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); + connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); + connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); + + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + + connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); + connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens); + connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens); + + // Movie + connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie); + connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie); + connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie); + connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie); + connect_menu(ui->action_Movie_Read_Only_Mode, + [this](bool checked) { movie.SetReadOnly(checked); }); + connect_menu(ui->action_Enable_Frame_Advancing, [this] { + if (emulation_running) { + system.frame_limiter.SetFrameAdvancing(ui->action_Enable_Frame_Advancing->isChecked()); + ui->action_Advance_Frame->setEnabled(ui->action_Enable_Frame_Advancing->isChecked()); + } + }); + connect_menu(ui->action_Advance_Frame, [this] { + if (emulation_running && system.frame_limiter.IsFrameAdvancing()) { + ui->action_Enable_Frame_Advancing->setChecked(true); + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.AdvanceFrame(); + } + }); + connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); + connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); + + // Help + connect_menu(ui->action_Open_Lucina3DS_Folder, &GMainWindow::OnOpenLucina3DSFolder); + connect_menu(ui->action_Open_Log_Folder, []() { + QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + connect_menu(ui->action_FAQ, []() { + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); + }); + connect_menu(ui->action_About, &GMainWindow::OnMenuAboutLucina3DS); + +#if ENABLE_QT_UPDATER + connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); + connect_menu(ui->action_Open_Maintenance_Tool, &GMainWindow::OnOpenUpdater); +#endif +} + +void GMainWindow::UpdateMenuState() { + const bool is_paused = !emu_thread || !emu_thread->IsRunning(); + + const std::array running_actions{ + ui->action_Stop, + ui->action_Restart, + ui->action_Configure_Current_Game, + ui->action_Report_Compatibility, + ui->action_Load_Amiibo, + ui->action_Remove_Amiibo, + ui->action_Pause, + ui->action_Advance_Frame, + }; + + for (QAction* action : running_actions) { + action->setEnabled(emulation_running); + } + + ui->action_Capture_Screenshot->setEnabled(emulation_running); + + if (emulation_running && is_paused) { + ui->action_Pause->setText(tr("&Continue")); + } else { + ui->action_Pause->setText(tr("&Pause")); + } +} + +void GMainWindow::OnDisplayTitleBars(bool show) { + QList widgets = findChildren(); + + if (show) { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(nullptr); + if (old) { + delete old; + } + } + } else { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(new QWidget()); + if (old) { + delete old; + } + } + } +} + +#if ENABLE_QT_UPDATER +void GMainWindow::OnCheckForUpdates() { + explicit_update_check = true; + CheckForUpdates(); +} + +void GMainWindow::CheckForUpdates() { + if (updater->CheckForUpdates()) { + LOG_INFO(Frontend, "Update check started"); + } else { + LOG_WARNING(Frontend, "Unable to start check for updates"); + } +} + +void GMainWindow::OnUpdateFound(bool found, bool error) { + if (error) { + LOG_WARNING(Frontend, "Update check failed"); + return; + } + + if (!found) { + LOG_INFO(Frontend, "No updates found"); + + // If the user explicitly clicked the "Check for Updates" button, we are + // going to want to show them a prompt anyway. + if (explicit_update_check) { + explicit_update_check = false; + ShowNoUpdatePrompt(); + } + return; + } + + if (emulation_running && !explicit_update_check) { + LOG_INFO(Frontend, "Update found, deferring as game is running"); + defer_update_prompt = true; + return; + } + + LOG_INFO(Frontend, "Update found!"); + explicit_update_check = false; + + ShowUpdatePrompt(); +} + +void GMainWindow::ShowUpdatePrompt() { + defer_update_prompt = false; + + auto result = + QMessageBox::question(this, tr("Update Available"), + tr("An update is available. Would you like to install it now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + updater->LaunchUIOnExit(); + close(); + } +} + +void GMainWindow::ShowNoUpdatePrompt() { + QMessageBox::information(this, tr("No Update Found"), tr("No update is found."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void GMainWindow::OnOpenUpdater() { + updater->LaunchUI(); +} + +void GMainWindow::ShowUpdaterWidgets() { + ui->action_Check_For_Updates->setVisible(UISettings::values.updater_found); + ui->action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found); + + connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound); +} +#endif + +#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) +static std::optional HoldWakeLockLinux(u32 window_id = 0) { + if (!QDBusConnection::sessionBus().isConnected()) { + return {}; + } + // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit + QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Inhibit")); + if (!xdp.isValid()) { + LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); + return {}; + } + QVariantMap options = {}; + //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the + //: computer from sleeping + options.insert(QString::fromLatin1("reason"), + QCoreApplication::translate("GMainWindow", "Luinca3DS is running a game")); + // 0x4: Suspend lock; 0x8: Idle lock + QDBusReply reply = + xdp.call(QString::fromLatin1("Inhibit"), + QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); + + if (reply.isValid()) { + return reply.value(); + } + LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", + reply.error().message().toStdString()); + return {}; +} + +static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { + if (!QDBusConnection::sessionBus().isConnected()) { + return; + } + QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), + QString::fromLatin1("org.freedesktop.portal.Request")); + unlocker.call(QString::fromLatin1("Close")); +} +#endif // __unix__ + +void GMainWindow::PreventOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + auto reply = HoldWakeLockLinux(winId()); + if (reply) { + wake_lock = std::move(reply.value()); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +void GMainWindow::AllowOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(HAVE_SDL2) + SDL_EnableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + if (!wake_lock.path().isEmpty()) { + ReleaseWakeLockLinux(wake_lock); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +bool GMainWindow::LoadROM(const QString& filename) { + // Shutdown previous session if the emu thread is still active... + if (emu_thread) { + ShutdownGame(); + } + + if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) { + LOG_CRITICAL(Frontend, "Failed to initialize render targets!"); + return false; + } + + const auto scope = render_window->Acquire(); + + const Core::System::ResultStatus result{ + system.Load(*render_window, filename.toStdString(), secondary_window)}; + + if (result != Core::System::ResultStatus::Success) { + switch (result) { + case Core::System::ResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorSystemMode: + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + QMessageBox::critical( + this, tr("ROM Corrupted"), + tr("Your ROM is corrupted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { + QMessageBox::critical( + this, tr("ROM Encrypted"), + tr("Your ROM is encrypted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + } + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + QMessageBox::critical(this, tr("Unsupported ROM"), + tr("GBA Virtual Console ROMs are not supported by Lucina3DS.")); + break; + + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr(fmt::format( + "An error has occurred whilst communicating with the Artic Base Server.\n{}", + system.GetStatusDetails()) + .c_str())); + break; + default: + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + break; + } + return false; + } + + std::string title; + system.GetAppLoader().ReadTitle(title); + game_title = QString::fromStdString(title); + UpdateWindowTitle(); + + game_path = filename; + + return true; +} + +void GMainWindow::BootGame(const QString& filename) { + if (emu_thread) { + ShutdownGame(); + } + + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { + const auto answer = QMessageBox::question( + this, tr("CIA must be installed before usage"), + tr("Before using this CIA, you must install it. Do you want to install it now?"), + QMessageBox::Yes | QMessageBox::No); + + if (answer == QMessageBox::Yes) + InstallCIA(QStringList(filename)); + + return; + } + + show_artic_label = is_artic; + + LOG_INFO(Frontend, "Lucinca3DS starting..."); + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } + + if (movie_record_on_start) { + movie.PrepareForRecording(); + } + if (movie_playback_on_start) { + movie.PrepareForPlayback(movie_playback_path.toStdString()); + } + + const std::string path = filename.toStdString(); + auto loader = Loader::GetLoader(path); + + u64 title_id{0}; + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } + + system.ApplySettings(); + + Settings::LogSettings(); + + // Save configurations + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + config->Save(); + + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); + return; + } + + // Set everything up + if (movie_record_on_start) { + movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (movie_playback_on_start) { + movie.StartPlayback(movie_playback_path.toStdString()); + movie_playback_on_start = false; + movie_playback_path.clear(); + } + + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + + if (video_dumping_on_start) { + StartVideoDumping(video_dumping_path); + video_dumping_on_start = false; + video_dumping_path.clear(); + } + + // Register debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Register(); + } + + // Create and start the emulation thread + emu_thread = std::make_unique(system, *render_window); + emit EmulationStarting(emu_thread.get()); + emu_thread->start(); + + connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + + // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views + // before the CPU continues + connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget, + &RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget, + &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget, + &RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, + &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, + &LoadingScreen::OnLoadComplete); + + // Update the GUI + registersWidget->OnDebugModeEntered(); + if (ui->action_Single_Window_Mode->isChecked()) { + game_list->hide(); + game_list_placeholder->hide(); + } + status_bar_update_timer.start(1000); + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + } + + loading_screen->Prepare(system.GetAppLoader()); + loading_screen->show(); + + emulation_running = true; + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } + + OnStartGame(); +} + +void GMainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + if (ui->action_Fullscreen->isChecked()) { + HideFullscreen(); + } + + auto video_dumper = system.GetVideoDumper(); + if (video_dumper && video_dumper->IsDumping()) { + game_shutdown_delayed = true; + OnStopVideoDumping(); + return; + } + + AllowOSSleep(); + + discord_rpc->Pause(); + emu_thread->RequestStop(); + + // Release emu threads from any breakpoints + // This belongs after RequestStop() and before wait() because if emulation stops on a GPU + // breakpoint after (or before) RequestStop() is called, the emulation would never be able + // to continue out to the main loop and terminate. Thus wait() would hang forever. + // TODO(bunnei): This function is not thread safe, but it's being used as if it were + Pica::g_debug_context->ClearBreakpoints(); + + // Unregister debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Unregister(); + } + + // Frame advancing must be cancelled in order to release the emu thread from waiting + system.frame_limiter.SetFrameAdvancing(false); + + emit EmulationStopping(); + + // Wait for emulation thread to complete and delete it + emu_thread->wait(); + emu_thread = nullptr; + + OnCloseMovie(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif + + // The emulation is stopped, so closing the window or not does not matter anymore + disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + + render_window->hide(); + secondary_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); + + if (game_list->IsEmpty()) { + game_list_placeholder->show(); + } else { + game_list->show(); + } + game_list->SetFilterFocus(); + + setMouseTracking(false); + + // Disable status bar updates + status_bar_update_timer.stop(); + message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); + emu_speed_label->setVisible(false); + game_fps_label->setVisible(false); + emu_frametime_label->setVisible(false); + + UpdateSaveStates(); + + emulation_running = false; + +#if ENABLE_QT_UDPATER + if (defer_update_prompt) { + ShowUpdatePrompt(); + } +#endif + + game_title.clear(); + UpdateWindowTitle(); + + game_path.clear(); + + // Update the GUI + UpdateMenuState(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); +} + +void GMainWindow::StoreRecentFile(const QString& filename) { + UISettings::values.recent_files.prepend(filename); + UISettings::values.recent_files.removeDuplicates(); + while (UISettings::values.recent_files.size() > max_recent_files_item) { + UISettings::values.recent_files.removeLast(); + } + + UpdateRecentFiles(); +} + +void GMainWindow::UpdateRecentFiles() { + const int num_recent_files = + std::min(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); + + for (int i = 0; i < num_recent_files; i++) { + const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( + QFileInfo(UISettings::values.recent_files[i]).fileName()); + actions_recent_files[i]->setText(text); + actions_recent_files[i]->setData(UISettings::values.recent_files[i]); + actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); + actions_recent_files[i]->setVisible(true); + } + + for (int j = num_recent_files; j < max_recent_files_item; ++j) { + actions_recent_files[j]->setVisible(false); + } + + // Enable the recent files menu if the list isn't empty + ui->menu_recent_files->setEnabled(num_recent_files != 0); +} + +void GMainWindow::UpdateSaveStates() { + if (!system.IsPoweredOn()) { + ui->menu_Load_State->setEnabled(false); + ui->menu_Save_State->setEnabled(false); + return; + } + + ui->menu_Load_State->setEnabled(true); + ui->menu_Save_State->setEnabled(true); + ui->action_Load_from_Newest_Slot->setEnabled(false); + + oldest_slot = newest_slot = 0; + oldest_slot_time = std::numeric_limits::max(); + newest_slot_time = 0; + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return; + } + auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID()); + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i]->setEnabled(false); + actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); + actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); + } + for (const auto& savestate : savestates) { + const bool display_name = + savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch && + !savestate.build_name.empty(); + const auto text = + tr("Slot %1 - %2 %3") + .arg(savestate.slot) + .arg(QDateTime::fromSecsSinceEpoch(savestate.time) + .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))) + .arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String()) + .trimmed(); + + actions_load_state[savestate.slot - 1]->setEnabled(true); + actions_load_state[savestate.slot - 1]->setText(text); + actions_save_state[savestate.slot - 1]->setText(text); + + ui->action_Load_from_Newest_Slot->setEnabled(true); + + if (savestate.time > newest_slot_time) { + newest_slot = savestate.slot; + newest_slot_time = savestate.time; + } + if (savestate.time < oldest_slot_time) { + oldest_slot = savestate.slot; + oldest_slot_time = savestate.time; + } + } + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + if (!actions_load_state[i]->isEnabled()) { + // Prefer empty slot + oldest_slot = i + 1; + oldest_slot_time = 0; + break; + } + } +} + +void GMainWindow::OnGameListLoadFile(QString game_path) { + if (ConfirmChangeGame()) { + BootGame(game_path); + } +} + +void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { + std::string path; + std::string open_target; + + switch (target) { + case GameListOpenTarget::SAVE_DATA: { + open_target = "Save Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::EXT_DATA: { + open_target = "Extra Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::APPLICATION: { + open_target = "Application"; + auto media_type = Service::AM::GetTitleMediaType(data_id); + path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; + break; + } + case GameListOpenTarget::UPDATE_DATA: { + open_target = "Update Data"; + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + + "content/"; + break; + } + case GameListOpenTarget::TEXTURE_DUMP: { + open_target = "Dumped Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); + break; + } + case GameListOpenTarget::TEXTURE_LOAD: { + open_target = "Custom Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); + break; + } + case GameListOpenTarget::MODS: { + open_target = "Mods"; + path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + data_id); + break; + } + case GameListOpenTarget::DLC_DATA: { + open_target = "DLC Data"; + path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/0004008c/{:08x}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id); + break; + } + case GameListOpenTarget::SHADER_CACHE: { + open_target = "Shader Cache"; + path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir); + break; + } + default: + LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); + return; + } + + QString qpath = QString::fromStdString(path); + + QDir dir(qpath); + if (!dir.exists()) { + QMessageBox::critical( + this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)), + tr("Folder does not exist!")); + return; + } + + LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); + + QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); +} + +void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + QString directory; + if (it != compatibility_list.end()) + directory = it->second.second; + + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory)); +} + +void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { + auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setWindowFlags(dialog->windowFlags() & + ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); + dialog->setCancelButton(nullptr); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + const auto base_path = fmt::format( + "{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); + const auto update_path = + fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + program_id | 0x0004000e00000000); + using FutureWatcher = QFutureWatcher>; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, + [this, dialog, base_path, update_path, future_watcher] { + dialog->hide(); + const auto& [base, update] = future_watcher->result(); + if (base != Loader::ResultStatus::Success) { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not dump base RomFS.\nRefer to the log for details.")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path))); + if (update == Loader::ResultStatus::Success) { + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(update_path))); + } + }); + + auto future = QtConcurrent::run([game_path, base_path, update_path] { + std::unique_ptr loader = Loader::GetLoader(game_path.toStdString()); + return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path)); + }); + future_watcher->setFuture(future); +} + +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { + QString path; + if (directory == QStringLiteral("INSTALLED")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); + } else if (directory == QStringLiteral("SYSTEM")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000/title/00040010"); + } else { + path = directory; + } + if (!QFileInfo::exists(path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) + return; + UISettings::GameDir game_dir{dir_path, false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } +} + +void GMainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui->action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + +void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { + const auto loader = Loader::GetLoader(file.toStdString()); + + u64 title_id{}; + if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { + QMessageBox::information(this, tr("Properties"), + tr("The game properties could not be loaded.")); + return; + } + + OpenPerGameConfiguration(title_id, file); +} + +void GMainWindow::OnMenuLoadFile() { + const QString extensions = QStringLiteral("*.").append( + GameList::supported_file_extensions.join(QStringLiteral(" *."))); + const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)", + "%1 is an identifier for the 3DS executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), UISettings::values.roms_path, file_filter); + + if (filename.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + +void GMainWindow::OnMenuInstallCIA() { + QStringList filepaths = QFileDialog::getOpenFileNames( + this, tr("Load Files"), UISettings::values.roms_path, + tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)")); + + if (filepaths.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filepaths[0]).path(); + InstallCIA(filepaths); +} + +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + +void GMainWindow::OnMenuBootHomeMenu(u32 region) { + BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); +} + +void GMainWindow::InstallCIA(QStringList filepaths) { + ui->action_Install_CIA->setEnabled(false); + game_list->SetDirectoryWatcherEnabled(false); + progress_bar->show(); + progress_bar->setMaximum(INT_MAX); + + (void)QtConcurrent::run([&, filepaths] { + Service::AM::InstallStatus status; + const auto cia_progress = [&](std::size_t written, std::size_t total) { + emit UpdateProgress(written, total); + }; + for (const auto& current_path : filepaths) { + status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress); + emit CIAInstallReport(status, current_path); + } + emit CIAInstallFinished(); + }); +} + +void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) { + progress_bar->setValue( + static_cast(INT_MAX * (static_cast(written) / static_cast(total)))); +} + +void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) { + QString filename = QFileInfo(filepath).fileName(); + switch (status) { + case Service::AM::InstallStatus::Success: + this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFailedToOpenFile: + QMessageBox::critical(this, tr("Unable to open File"), + tr("Could not open %1").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorAborted: + QMessageBox::critical( + this, tr("Installation aborted"), + tr("The installation of %1 was aborted. Please see the log for more details") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorInvalid: + QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorEncrypted: + QMessageBox::critical(this, tr("Encrypted File"), + tr("%1 must be decrypted " + "before being used with Lucina3DS. A real 3DS is required.") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFileNotFound: + QMessageBox::critical(this, tr("Unable to find File"), + tr("Could not find %1").arg(filename)); + break; + } +} + +void GMainWindow::OnCIAInstallFinished() { + progress_bar->hide(); + progress_bar->setValue(0); + game_list->SetDirectoryWatcherEnabled(true); + ui->action_Install_CIA->setEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void GMainWindow::UninstallTitles( + const std::vector>& titles) { + if (titles.empty()) { + return; + } + + // Select the first title in the list as representative. + const auto first_name = std::get(titles[0]); + + QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, + static_cast(titles.size()), this); + progress.setWindowModality(Qt::WindowModal); + + QFutureWatcher future_watcher; + QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, + &QProgressDialog::reset); + QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, + &QProgressDialog::setValue); + + auto failed = false; + QString failed_name; + + const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { + const auto name = std::get(title); + const auto media_type = std::get(title); + const auto program_id = std::get(title); + + const auto result = Service::AM::UninstallProgram(media_type, program_id); + if (result.IsError()) { + LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), + result.raw); + failed = true; + failed_name = name; + future_watcher.cancel(); + } + }; + + future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); + progress.exec(); + future_watcher.waitForFinished(); + + if (failed) { + QMessageBox::critical(this, tr("Lucina3DS"), tr("Failed to uninstall '%1'.").arg(failed_name)); + } else if (!future_watcher.isCanceled()) { + QMessageBox::information(this, tr("Lucina3DS"), + tr("Successfully uninstalled '%1'.").arg(first_name)); + } +} + +void GMainWindow::OnMenuRecentFile() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + const QString filename = action->data().toString(); + if (QFileInfo::exists(filename)) { + BootGame(filename); + } else { + // Display an error message and remove the file from the list. + QMessageBox::information(this, tr("File not found"), + tr("File \"%1\" not found").arg(filename)); + + UISettings::values.recent_files.removeOne(filename); + UpdateRecentFiles(); + } +} + +void GMainWindow::OnStartGame() { + qt_cameras->ResumeCameras(); + + PreventOSSleep(); + + emu_thread->SetRunning(true); + graphics_api_button->setEnabled(false); + qRegisterMetaType("Core::System::ResultStatus"); + qRegisterMetaType("std::string"); + connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); + + UpdateMenuState(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StartGamemode(); +#endif + + UpdateSaveStates(); + UpdateStatusButtons(); +} + +void GMainWindow::OnRestartGame() { + if (!system.IsPoweredOn()) { + return; + } + // Make a copy since BootGame edits game_path + BootGame(QString(game_path)); +} + +void GMainWindow::OnPauseGame() { + emu_thread->SetRunning(false); + qt_cameras->PauseCameras(); + + UpdateMenuState(); + AllowOSSleep(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif +} + +void GMainWindow::OnPauseContinueGame() { + if (emulation_running) { + if (emu_thread->IsRunning()) { + OnPauseGame(); + } else { + OnStartGame(); + } + } +} + +void GMainWindow::OnStopGame() { + ShutdownGame(); + graphics_api_button->setEnabled(true); + Settings::RestoreGlobalState(false); + UpdateStatusButtons(); +} + +void GMainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnMenuReportCompatibility() { + if (!NetSettings::values.lucina3ds_token.empty() && !NetSettings::values.lucina3ds_username.empty()) { + CompatDB compatdb{this}; + compatdb.exec(); + } else { + QMessageBox::critical(this, tr("Missing Citra Account"), + tr("You must link your Citra account to submit test cases." + "
Go to Emulation > Configure... > Web to do so.")); + } +} + +void GMainWindow::ToggleFullscreen() { + if (!emulation_running) { + return; + } + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } else { + HideFullscreen(); + } +} + +void GMainWindow::ToggleSecondaryFullscreen() { + if (!emulation_running) { + return; + } + if (secondary_window->isFullScreen()) { + secondary_window->showNormal(); + } else { + secondary_window->showFullScreen(); + } +} + +void GMainWindow::ShowFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + UISettings::values.geometry = saveGeometry(); + ui->menubar->hide(); + statusBar()->hide(); + showFullScreen(); + } else { + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + render_window->showFullScreen(); + } +} + +void GMainWindow::HideFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->menubar->show(); + showNormal(); + restoreGeometry(UISettings::values.geometry); + } else { + render_window->showNormal(); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } +} + +void GMainWindow::ToggleWindowMode() { + if (ui->action_Single_Window_Mode->isChecked()) { + // Render in the main window... + render_window->BackupGeometry(); + ui->horizontalLayout->addWidget(render_window); + render_window->setFocusPolicy(Qt::StrongFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->setFocus(); + game_list->hide(); + } + + } else { + // Render in a separate window... + ui->horizontalLayout->removeWidget(render_window); + render_window->setParent(nullptr); + render_window->setFocusPolicy(Qt::NoFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->RestoreGeometry(); + game_list->show(); + } + } +} + +void GMainWindow::UpdateSecondaryWindowVisibility() { + if (!emulation_running) { + return; + } + if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) { + secondary_window->RestoreGeometry(); + secondary_window->show(); + } else { + secondary_window->BackupGeometry(); + secondary_window->hide(); + } +} + +void GMainWindow::ChangeScreenLayout() { + Settings::LayoutOption new_layout = Settings::LayoutOption::Default; + + if (ui->action_Screen_Layout_Default->isChecked()) { + new_layout = Settings::LayoutOption::Default; + } else if (ui->action_Screen_Layout_Single_Screen->isChecked()) { + new_layout = Settings::LayoutOption::SingleScreen; + } else if (ui->action_Screen_Layout_Large_Screen->isChecked()) { + new_layout = Settings::LayoutOption::LargeScreen; + } else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) { + new_layout = Settings::LayoutOption::HybridScreen; + } else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) { + new_layout = Settings::LayoutOption::SideScreen; + } else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) { + new_layout = Settings::LayoutOption::SeparateWindows; + } + + Settings::values.layout_option = new_layout; + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::ToggleScreenLayout() { + const Settings::LayoutOption new_layout = []() { + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::Default: + return Settings::LayoutOption::SingleScreen; + case Settings::LayoutOption::SingleScreen: + return Settings::LayoutOption::LargeScreen; + case Settings::LayoutOption::LargeScreen: + return Settings::LayoutOption::HybridScreen; + case Settings::LayoutOption::HybridScreen: + return Settings::LayoutOption::SideScreen; + case Settings::LayoutOption::SideScreen: + return Settings::LayoutOption::SeparateWindows; + case Settings::LayoutOption::SeparateWindows: + return Settings::LayoutOption::Default; + default: + LOG_ERROR(Frontend, "Unknown layout option {}", + Settings::values.layout_option.GetValue()); + return Settings::LayoutOption::Default; + } + }(); + + Settings::values.layout_option = new_layout; + SyncMenuUISettings(); + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnSwapScreens() { + Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::OnRotateScreens() { + Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::TriggerSwapScreens() { + ui->action_Screen_Layout_Swap_Screens->trigger(); +} + +void GMainWindow::TriggerRotateScreens() { + ui->action_Screen_Layout_Upright_Screens->trigger(); +} + +void GMainWindow::OnSaveState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + system.SendSignal(Core::System::Signal::Save, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); + newest_slot = action->data().toUInt(); +} + +void GMainWindow::OnLoadState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + if (UISettings::values.save_state_warning) { + QMessageBox::warning(this, tr("Savestates"), + tr("Warning: Savestates are NOT a replacement for in-game saves, " + "and are not meant to be reliable.\n\nUse at your own risk!")); + UISettings::values.save_state_warning = false; + config->Save(); + } + + system.SendSignal(Core::System::Signal::Load, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); +} + +void GMainWindow::OnConfigure() { + game_list->SetDirectoryWatcherEnabled(false); + Settings::SetConfiguringGlobal(true); + ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices, + !multiplayer_state->IsHostingPublicRoom()); + connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, + &GMainWindow::OnLanguageChanged); + auto old_theme = UISettings::values.theme; + const int old_input_profile_index = Settings::values.current_input_profile_index; + const auto old_input_profiles = Settings::values.input_profiles; + const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps; + const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); +#ifdef __unix__ + const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); +#endif + auto result = configureDialog.exec(); + game_list->SetDirectoryWatcherEnabled(true); + if (result == QDialog::Accepted) { + configureDialog.ApplyConfiguration(); + InitializeHotkeys(); + if (UISettings::values.theme != old_theme) { + UpdateUITheme(); + } + if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + } +#ifdef __unix__ + if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); + } +#endif + if (!multiplayer_state->IsHostingPublicRoom()) + multiplayer_state->UpdateCredentials(); + emit UpdateThemedIcons(); + SyncMenuUISettings(); + game_list->RefreshGameDirectory(); + config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + } + UpdateSecondaryWindowVisibility(); + UpdateBootHomeMenuState(); + UpdateStatusButtons(); + } else { + Settings::values.input_profiles = old_input_profiles; + Settings::values.touch_from_button_maps = old_touch_from_button_maps; + Settings::LoadProfile(old_input_profile_index); + } +} + +void GMainWindow::OnLoadAmiibo() { + if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { + return; + } + + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr) { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (nfc->IsTagActive()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("A tag is already in use.")); + return; + } + + if (!nfc->IsSearchingForAmiibos()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Game is not looking for amiibos.")); + return; + } + + const QString extensions{QStringLiteral("*.bin")}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); + + if (filename.isEmpty()) { + return; + } + + LoadAmiibo(filename); +} + +void GMainWindow::LoadAmiibo(const QString& filename) { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (!nfc->LoadAmiibo(filename.toStdString())) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Unable to open amiibo file \"%1\" for reading.").arg(filename)); + return; + } + + ui->action_Remove_Amiibo->setEnabled(true); +} + +void GMainWindow::OnRemoveAmiibo() { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + nfc->RemoveAmiibo(); + ui->action_Remove_Amiibo->setEnabled(false); +} + +void GMainWindow::OnOpenLucina3DSFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); +} + +void GMainWindow::OnToggleFilterBar() { + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + if (ui->action_Show_Filter_Bar->isChecked()) { + game_list->SetFilterFocus(); + } else { + game_list->ClearFilter(); + } +} + +void GMainWindow::OnCreateGraphicsSurfaceViewer() { + auto graphicsSurfaceViewerWidget = + new GraphicsSurfaceWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget); + // TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true); + graphicsSurfaceViewerWidget->show(); +} + +void GMainWindow::OnRecordMovie() { + MovieRecordDialog dialog(this, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_record_on_start = true; + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); + + if (emulation_running) { // Restart game + BootGame(QString(game_path)); + } + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(true); +} + +void GMainWindow::OnPlayMovie() { + MoviePlayDialog dialog(this, game_list, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_playback_on_start = true; + movie_playback_path = dialog.GetMoviePath(); + BootGame(dialog.GetGamePath()); + + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnCloseMovie() { + if (movie_record_on_start) { + QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } else { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording; + movie.Shutdown(); + if (was_recording) { + QMessageBox::information(this, tr("Movie Saved"), + tr("The movie is successfully saved.")); + } + + if (was_running) { + OnStartGame(); + } + } + + ui->action_Close_Movie->setEnabled(false); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnSaveMovie() { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) { + movie.SaveMovie(); + QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); + } else { + LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); + } + + if (was_running) { + OnStartGame(); + } +} + +void GMainWindow::OnCaptureScreenshot() { + if (!emu_thread) [[unlikely]] { + return; + } + + const bool was_running = emu_thread->IsRunning(); + + if (was_running || + (QMessageBox::question( + this, tr("Game will unpause"), + tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running) { + OnPauseGame(); + } + std::string path = UISettings::values.screenshot_path.GetValue(); + if (!FileUtil::IsDirectory(path)) { + if (!FileUtil::CreateFullPath(path)) { + QMessageBox::information( + this, tr("Invalid Screenshot Directory"), + tr("Cannot create specified screenshot directory. Screenshot " + "path is set back to its default value.")); + path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + path.append("screenshots/"); + UISettings::values.screenshot_path = path; + }; + } + + static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); + const std::string filename = game_title.remove(expr).toStdString(); + const std::string timestamp = QDateTime::currentDateTime() + .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) + .toStdString(); + path.append(fmt::format("/{}_{}.png", filename, timestamp)); + + auto* const screenshot_window = + secondary_window->HasFocus() ? secondary_window : render_window; + screenshot_window->CaptureScreenshot( + UISettings::values.screenshot_resolution_factor.GetValue(), + QString::fromStdString(path)); + OnStartGame(); + } +} + +void GMainWindow::OnDumpVideo() { + if (DynamicLibrary::FFmpeg::LoadFFmpeg()) { + if (ui->action_Dump_Video->isChecked()) { + OnStartVideoDumping(); + } else { + OnStopVideoDumping(); + } + } else { + ui->action_Dump_Video->setChecked(false); + + QMessageBox message_box; + message_box.setWindowTitle(tr("Could not load video dumper")); + message_box.setText( + tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." +#ifdef _WIN32 + "\n\nTo install FFmpeg to Lucina3DS, press Open and select your FFmpeg directory." +#endif + "\n\nTo view a guide on how to install FFmpeg, press Help.")); + message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help +#ifdef _WIN32 + | QMessageBox::Open +#endif + ); + auto result = message_box.exec(); + if (result == QMessageBox::Help) { + QDesktopServices::openUrl(QUrl(QStringLiteral( + "https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/"))); +#ifdef _WIN32 + } else if (result == QMessageBox::Open) { + OnOpenFFmpeg(); +#endif + } + } +} + +#ifdef _WIN32 +void GMainWindow::OnOpenFFmpeg() { + auto filename = + QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString(); + if (filename.empty()) { + return; + } + // Check for a bin directory if they chose the FFmpeg root directory. + auto bin_dir = filename + DIR_SEP + "bin"; + if (!FileUtil::Exists(bin_dir)) { + // Otherwise, assume the user directly selected the directory containing the DLLs. + bin_dir = filename; + } + + static const std::array library_names = { + Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR), + }; + + for (auto& library_name : library_names) { + if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("The provided FFmpeg directory is missing %1. Please make " + "sure the correct directory was selected.") + .arg(QString::fromStdString(library_name))); + return; + } + } + + std::atomic success(true); + auto process_file = [&success](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { + auto file_path = directory + DIR_SEP + virtual_name; + if (file_path.ends_with(".dll")) { + auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name; + if (!FileUtil::Copy(file_path, destination_path)) { + success.store(false); + return false; + } + } + return true; + }; + FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); + + if (success.load()) { + QMessageBox::information(this, tr("Lucina3DS"), tr("FFmpeg has been sucessfully installed.")); + } else { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("Installation of FFmpeg failed. Check the log file for details.")); + } +} +#endif + +void GMainWindow::OnStartVideoDumping() { + DumpingDialog dialog(this, system); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + ui->action_Dump_Video->setChecked(false); + return; + } + const auto path = dialog.GetFilePath(); + if (emulation_running) { + StartVideoDumping(path); + } else { + video_dumping_on_start = true; + video_dumping_path = path; + } +} + +void GMainWindow::StartVideoDumping(const QString& path) { + auto& renderer = system.GPU().Renderer(); + const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; + + auto dumper = std::make_shared(renderer); + if (dumper->StartDumping(path.toStdString(), layout)) { + system.RegisterVideoDumper(dumper); + } else { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui->action_Dump_Video->setChecked(false); + } +} + +void GMainWindow::OnStopVideoDumping() { + ui->action_Dump_Video->setChecked(false); + + if (video_dumping_on_start) { + video_dumping_on_start = false; + video_dumping_path.clear(); + } else { + auto dumper = system.GetVideoDumper(); + if (!dumper || !dumper->IsDumping()) { + return; + } + + game_paused_for_dumping = emu_thread->IsRunning(); + OnPauseGame(); + + auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, [this] { + if (game_shutdown_delayed) { + game_shutdown_delayed = false; + ShutdownGame(); + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; + OnStartGame(); + } + }); + future_watcher->setFuture(future); + } +} + +void GMainWindow::UpdateStatusBar() { + if (!emu_thread) [[unlikely]] { + status_bar_update_timer.stop(); + return; + } + + // Update movie status + const u64 current = movie.GetCurrentInputIndex(); + const u64 total = movie.GetTotalInputCount(); + const auto play_mode = movie.GetPlayMode(); + if (play_mode == Core::Movie::PlayMode::Recording) { + message_label->setText(tr("Recording %1").arg(current)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(true); + } else if (play_mode == Core::Movie::PlayMode::Playing) { + message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { + message_label->setText(tr("Movie Finished")); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (message_label_used_for_movie) { // Clear the label if movie was just closed + message_label->setText(QString{}); + message_label_used_for_movie = false; + ui->action_Save_Movie->setEnabled(false); + } + + auto results = system.GetAndResetPerfStats(); + + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 5> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + tr("(Accessing SystemSaveData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + + if (Settings::values.frame_limit.GetValue() == 0) { + emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); + } else { + emu_speed_label->setText(tr("Speed: %1% / %2%") + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.frame_limit.GetValue())); + } + game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); + emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } + emu_speed_label->setVisible(true); + game_fps_label->setVisible(true); + emu_frametime_label->setVisible(true); +} + +void GMainWindow::UpdateBootHomeMenuState() { + const auto current_region = Settings::values.region_value.GetValue(); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + const auto path = Core::GetHomeMenuNcchPath(region); + ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled( + (current_region == Settings::REGION_VALUE_AUTO_SELECT || + current_region == static_cast(region)) && + !path.empty() && FileUtil::Exists(path)); + } +} + +void GMainWindow::HideMouseCursor() { + if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + render_window->setCursor(QCursor(Qt::BlankCursor)); + secondary_window->setCursor(QCursor(Qt::BlankCursor)); + if (UISettings::values.single_window_mode.GetValue()) { + setCursor(QCursor(Qt::BlankCursor)); + } +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + render_window->unsetCursor(); + secondary_window->unsetCursor(); + if (emu_thread && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::OnMute() { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); +} + +void GMainWindow::OnDecreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume <= 30) { + step = 2; + } + if (current_volume <= 6) { + step = 1; + } + const auto value = + static_cast(std::max(current_volume - step, 0)) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::OnIncreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume < 30) { + step = 2; + } + if (current_volume < 6) { + step = 1; + } + const auto value = static_cast(current_volume + step) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::UpdateVolumeUI() { + const auto volume_value = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + volume_slider->setValue(volume_value); + if (Settings::values.audio_muted) { + volume_button->setChecked(false); + volume_button->setText(tr("VOLUME: MUTE")); + } else { + volume_button->setChecked(true); + volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); + } +} + +void GMainWindow::UpdateAPIIndicator(bool update) { + static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"), + QStringLiteral("VULKAN")}; + + static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"), + QStringLiteral("#91242a")}; + + u32 api_index = static_cast(Settings::values.graphics_api.GetValue()); + if (update) { + api_index = (api_index + 1) % graphics_apis.size(); + // Skip past any disabled renderers. +#ifndef ENABLE_SOFTWARE_RENDERER + if (api_index == static_cast(Settings::GraphicsAPI::Software)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_OPENGL + if (api_index == static_cast(Settings::GraphicsAPI::OpenGL)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_VULKAN + if (api_index == static_cast(Settings::GraphicsAPI::Vulkan)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif + Settings::values.graphics_api = static_cast(api_index); + } + + const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }") + .arg(graphics_api_colors[api_index]); + + graphics_api_button->setText(graphics_apis[api_index]); + graphics_api_button->setStyleSheet(style_sheet); +} + +void GMainWindow::UpdateStatusButtons() { + UpdateAPIIndicator(); + UpdateVolumeUI(); +} + +void GMainWindow::OnMouseActivity() { + ShowMouseCursor(); +} + +void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { + QString status_message; + + QString title, message; + QMessageBox::Icon error_severity_icon; + bool can_continue = true; + if (result == Core::System::ResultStatus::ErrorSystemFiles) { + const QString common_message = + tr("%1 is missing. Please dump your " + "system archives.
Continuing emulation may result in crashes and bugs."); + + if (!details.empty()) { + message = common_message.arg(QString::fromStdString(details)); + } else { + message = common_message.arg(tr("A system archive")); + } + + title = tr("System Archive Not Found"); + status_message = tr("System Archive Missing"); + error_severity_icon = QMessageBox::Icon::Critical; + } else if (result == Core::System::ResultStatus::ErrorSavestate) { + title = tr("Save/load Error"); + message = QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Warning; + } else if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + title = tr("Artic Base Server"); + message = + tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details) + .c_str()); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else { + title = tr("Fatal Error"); + message = + tr("A fatal error occurred. " + "Check " + "the log for details." + "
Continuing emulation may result in crashes and bugs."); + status_message = tr("Fatal Error encountered"); + error_severity_icon = QMessageBox::Icon::Critical; + } + + QMessageBox message_box; + message_box.setWindowTitle(title); + message_box.setText(message); + message_box.setIcon(error_severity_icon); + if (error_severity_icon == QMessageBox::Icon::Critical) { + if (can_continue) { + message_box.addButton(tr("Continue"), QMessageBox::RejectRole); + } + QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); + if (result != Core::System::ResultStatus::ShutdownRequested) + message_box.exec(); + + if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested || + message_box.clickedButton() == abort_button) { + if (emu_thread) { + ShutdownGame(); + return; + } + } + } else { + // This block should run when the error isn't too big of a deal + // e.g. when a save state can't be saved or loaded + message_box.addButton(tr("OK"), QMessageBox::RejectRole); + message_box.exec(); + } + + // Only show the message if the game is still running. + if (emu_thread) { + emu_thread->SetRunning(true); + message_label->setText(status_message); + message_label_used_for_movie = false; + } +} + +void GMainWindow::OnMenuAboutLucina3DS() { + AboutDialog about{this}; + about.exec(); +} + +bool GMainWindow::ConfirmClose() { + if (!emu_thread || !UISettings::values.confirm_before_closing) { + return true; + } + + QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Lucina3DS"), tr("Would you like to exit now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::closeEvent(QCloseEvent* event) { + if (!ConfirmClose()) { + event->ignore(); + return; + } + + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + hotkey_registry.SaveHotkeys(); + + // Shutdown session if the emu thread is active... + if (emu_thread) { + ShutdownGame(); + } + + render_window->close(); + secondary_window->close(); + multiplayer_state->Close(); + InputCommon::Shutdown(); + QWidget::closeEvent(event); +} + +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; +} + +static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin", + "3dsx", "app", "elf", "axf"}; + +static bool IsCorrectFileExtension(const QMimeData* mime) { + const QString& filename = mime->urls().at(0).toLocalFile(); + return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), + QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); +} + +static bool IsAcceptableDropEvent(QDropEvent* event) { + return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); +} + +void GMainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsAcceptableDropEvent(event)) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool GMainWindow::DropAction(QDropEvent* event) { + if (!IsAcceptableDropEvent(event)) { + return false; + } + + const QMimeData* mime_data = event->mimeData(); + const QString& filename = mime_data->urls().at(0).toLocalFile(); + + if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { + // Amiibo + LoadAmiibo(filename); + } else { + // Game + if (ConfirmChangeGame()) { + BootGame(filename); + } + } + return true; +} + +void GMainWindow::OnFileOpen(const QFileOpenEvent* event) { + BootGame(event->file()); +} + +void GMainWindow::dropEvent(QDropEvent* event) { + DropAction(event); +} + +void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { + AcceptDropEvent(event); +} + +void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { + AcceptDropEvent(event); +} + +bool GMainWindow::ConfirmChangeGame() { + if (!emu_thread) [[unlikely]] { + return true; + } + + auto answer = QMessageBox::question( + this, tr("Lucina3DS"), tr("The game is still running. Would you like to stop emulation?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::filterBarSetChecked(bool state) { + ui->action_Show_Filter_Bar->setChecked(state); + emit(OnToggleFilterBar()); +} + +void GMainWindow::UpdateUITheme() { + const QString icons_base_path = QStringLiteral(":/icons/"); + const QString default_theme = QStringLiteral("default"); + const QString default_theme_path = icons_base_path + default_theme; + + const QString& current_theme = UISettings::values.theme; + const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second); + QStringList theme_paths(default_theme_paths); + + if (is_default_theme || current_theme.isEmpty()) { + const QString theme_uri(QStringLiteral(":default/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, + "Unable to open default stylesheet, falling back to empty stylesheet"); + qApp->setStyleSheet({}); + setStyleSheet({}); + } + theme_paths.append(default_theme_path); + QIcon::setThemeName(default_theme); + } else { + const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found"); + } + + const QString current_theme_path = icons_base_path + current_theme; + theme_paths.append({default_theme_path, current_theme_path}); + QIcon::setThemeName(current_theme); + } + + QIcon::setThemeSearchPaths(theme_paths); +} + +void GMainWindow::LoadTranslation() { + // If the selected language is English, no need to install any translation + if (UISettings::values.language == QStringLiteral("en")) { + return; + } + + bool loaded; + + if (UISettings::values.language.isEmpty()) { + // Use the system's default locale + loaded = translator.load(QLocale::system(), {}, {}, QStringLiteral(":/languages/")); + } else { + // Otherwise load from the specified file + loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/")); + } + + if (loaded) { + qApp->installTranslator(&translator); + } else { + UISettings::values.language = QStringLiteral("en"); + } +} + +void GMainWindow::OnLanguageChanged(const QString& locale) { + if (UISettings::values.language != QStringLiteral("en")) { + qApp->removeTranslator(&translator); + } + + UISettings::values.language = locale; + LoadTranslation(); + ui->retranslateUi(this); + RetranslateStatusBar(); + UpdateWindowTitle(); +} + +void GMainWindow::OnConfigurePerGame() { + u64 title_id{}; + system.GetAppLoader().ReadProgramId(title_id); + OpenPerGameConfiguration(title_id, game_path); +} + +void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) { + Settings::SetConfiguringGlobal(false); + ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted) { + Settings::RestoreGlobalState(system.IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } + + // Do not cause the global config to write local settings into the config file + const bool is_powered_on = system.IsPoweredOn(); + Settings::RestoreGlobalState(system.IsPoweredOn()); + + if (!is_powered_on) { + config->Save(); + } + + UpdateStatusButtons(); +} + +void GMainWindow::OnMoviePlaybackCompleted() { + OnPauseGame(); + QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); +} + +void GMainWindow::UpdateWindowTitle() { + const QString full_name = QString::fromUtf8(Common::g_build_fullname); + + if (game_title.isEmpty()) { + setWindowTitle(QStringLiteral("Luinca3DS %1").arg(full_name)); + } else { + setWindowTitle(QStringLiteral("Luinca3DS %1 | %2").arg(full_name, game_title)); + render_window->setWindowTitle( + QStringLiteral("Lucina3DS %1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); + secondary_window->setWindowTitle(QStringLiteral("Lucina3DS %1 | %2 | %3") + .arg(full_name, game_title, tr("Secondary Window"))); + } +} + +void GMainWindow::UpdateUISettings() { + if (!ui->action_Fullscreen->isChecked()) { + UISettings::values.geometry = saveGeometry(); + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + } + UISettings::values.state = saveState(); +#if MICROPROFILE_ENABLED + UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); + UISettings::values.microprofile_visible = microProfileDialog->isVisible(); +#endif + UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); + UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); + UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); + UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); + UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.first_start = false; +} + +void GMainWindow::SyncMenuUISettings() { + ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::Default); + ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SingleScreen); + ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::LargeScreen); + ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::HybridScreen); + ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SideScreen); + ui->action_Screen_Layout_Separate_Windows->setChecked( + Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); + ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue()); + ui->action_Screen_Layout_Upright_Screens->setChecked( + Settings::values.upright_screen.GetValue()); +} + +void GMainWindow::RetranslateStatusBar() { + if (emu_thread) + UpdateStatusBar(); + + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + multiplayer_state->retranslateUi(); +} + +void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(system); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + +#ifdef __unix__ +void GMainWindow::SetGamemodeEnabled(bool state) { + if (emulation_running) { + Common::Linux::SetGamemodeState(state); + } +} +#endif + +#ifdef main +#undef main +#endif + +static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() { +#ifdef _WIN32 + // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. + // This is done by setting the optimal scaling policy for the primary screen. + + // Create a temporary QApplication. + int temp_argc = 0; + char** temp_argv = nullptr; + QApplication temp{temp_argc, temp_argv}; + + // Get the current screen geometry. + const QScreen* primary_screen = QGuiApplication::primaryScreen(); + if (!primary_screen) { + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; + } + + const QRect screen_rect = primary_screen->geometry(); + const qreal real_ratio = primary_screen->devicePixelRatio(); + const qreal real_width = std::trunc(screen_rect.width() * real_ratio); + const qreal real_height = std::trunc(screen_rect.height() * real_ratio); + + // Recommended minimum width and height for proper window fit. + // Any screen with a lower resolution than this will still have a scale of 1. + constexpr qreal minimum_width = 1350.0; + constexpr qreal minimum_height = 900.0; + + const qreal width_ratio = std::max(1.0, real_width / minimum_width); + const qreal height_ratio = std::max(1.0, real_height / minimum_height); + + // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. + const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio)); + + if (max_ratio > real_ratio) { + return Qt::HighDpiScaleFactorRoundingPolicy::Round; + } else { + return Qt::HighDpiScaleFactorRoundingPolicy::Floor; + } +#else + // Other OSes should be better than Windows at fractional scaling. + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; +#endif +} + +int main(int argc, char* argv[]) { + Common::DetachedTasks detached_tasks; + MicroProfileOnThreadCreate("Frontend"); + SCOPE_EXIT({ MicroProfileShutdown(); }); + + // Init settings params + QCoreApplication::setOrganizationName(QStringLiteral("Citra team | Lucina3DS")); + QCoreApplication::setApplicationName(QStringLiteral("Lucina3DS")); + + auto rounding_policy = GetHighDpiRoundingPolicy(); + QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); + +#ifdef __APPLE__ + auto bundle_dir = FileUtil::GetBundleDirectory(); + if (bundle_dir) { + FileUtil::SetCurrentDir(bundle_dir.value() + ".."); + } +#endif + +#ifdef ENABLE_OPENGL + QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); +#endif + + QApplication app(argc, argv); + + // Qt changes the locale and causes issues in float conversion using std::to_string() when + // generating shaders + setlocale(LC_ALL, "C"); + + auto& system{Core::System::GetInstance()}; + + // Register Qt image interface + system.RegisterImageInterface(std::make_shared()); + + GMainWindow main_window(system); + + // Register frontend applets + Frontend::RegisterDefaultApplets(system); + + system.RegisterMiiSelector(std::make_shared(main_window)); + system.RegisterSoftwareKeyboard(std::make_shared(main_window)); + +#ifdef __APPLE__ + // Register microphone permission check. + system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); +#endif + + main_window.show(); + + QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, + &GMainWindow::OnAppFocusStateChanged); + + int result = app.exec(); + detached_tasks.WaitForAllTasks(); + return result; +} diff --git a/.history/src/lucina3ds_qt/main_20250209233216.cpp b/.history/src/lucina3ds_qt/main_20250209233216.cpp new file mode 100644 index 00000000..2dd8e524 --- /dev/null +++ b/.history/src/lucina3ds_qt/main_20250209233216.cpp @@ -0,0 +1,3345 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include // for chdir +#endif +#ifdef _WIN32 +#include +#endif +#ifdef __unix__ +#include +#include +#include +#include "common/linux/gamemode.h" +#endif +#include "lucina3ds_qt/aboutdialog.h" +#include "lucina3ds_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/swkbd.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/compatdb.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/discord.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/loading_screen.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/qt_image_interface.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/graphics_device_info.h" +#include "common/arch.h" +#include "common/common_paths.h" +#include "common/detached_tasks.h" +#include "common/dynamic_library/dynamic_library.h" +#include "common/file_util.h" +#include "common/literals.h" +#include "common/logging/backend.h" +#include "common/logging/log.h" +#include "common/memory_detect.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#if LUCINA3DS_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/settings.h" +#include "core/core.h" +#include "core/dumping/backend.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_source_sd_savedata.h" +#include "core/frontend/applets/default_applets.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/nfc/nfc.h" +#include "core/loader/loader.h" +#include "core/movie.h" +#include "core/savestate.h" +#include "core/system_titles.h" +#include "input_common/main.h" +#include "network/network_settings.h" +#include "ui_main.h" +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" + +#ifdef __APPLE__ +#include "common/apple_authorization.h" +#endif + +#ifdef USE_DISCORD_PRESENCE +#include "lucina3ds_qt/discord_impl.h" +#endif + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); +#endif + +#ifdef _WIN32 +extern "C" { +// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +} +#endif + +#ifdef HAVE_SDL2 +#include +#endif + +constexpr int default_mouse_timeout = 2500; + +/** + * "Callouts" are one-time instructional messages shown to the user. In the config settings, there + * is a bitfield "callout_flags" options, used to track if a message has already been shown to the + * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. + */ + +const int GMainWindow::max_recent_files_item; + +static QString PrettyProductName() { +#ifdef _WIN32 + // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 + // With that notation change they changed the registry key used to denote the current version + QSettings windows_registry( + QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), + QSettings::NativeFormat); + const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); + if (release_id == QStringLiteral("2009")) { + const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); + const QString display_version = + windows_registry.value(QStringLiteral("DisplayVersion")).toString(); + const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); + const u32 version = current_build >= 22000 ? 11 : 10; + return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") + .arg(QString::number(version), display_version, QString::number(current_build), + QString::number(ubr)); + } +#endif + return QSysInfo::prettyProductName(); +} + +GMainWindow::GMainWindow(Core::System& system_) + : ui{std::make_unique()}, system{system_}, movie{system.Movie()}, + config{std::make_unique()}, emu_thread{nullptr} { + Common::Log::Initialize(); + Common::Log::Start(); + + Debugger::ToggleConsole(); + +#ifdef __unix__ + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); +#endif + + // register types to use in slots and signals + qRegisterMetaType("std::size_t"); + qRegisterMetaType("Service::AM::InstallStatus"); + + // Register CameraFactory + qt_cameras = std::make_shared(); + Camera::RegisterFactory("image", std::make_unique()); + Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + + LoadTranslation(); + + Pica::g_debug_context = Pica::DebugContext::Construct(); + setAcceptDrops(true); + ui->setupUi(this); + statusBar()->hide(); + + default_theme_paths = QIcon::themeSearchPaths(); + UpdateUITheme(); + + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(); + + Network::Init(); + + movie.SetPlaybackCompletionCallback([this] { + QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); + }); + + InitializeWidgets(); + InitializeDebugWidgets(); + InitializeRecentFileMenuActions(); + InitializeSaveStateMenuActions(); + InitializeHotkeys(); +#if ENABLE_QT_UPDATER + ShowUpdaterWidgets(); +#else + ui->action_Check_For_Updates->setVisible(false); + ui->action_Open_Maintenance_Tool->setVisible(false); +#endif + + SetDefaultUIGeometry(); + RestoreUIState(); + + ConnectAppEvents(); + ConnectMenuEvents(); + ConnectWidgetEvents(); + + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + Common::g_scm_desc); +#if LUCINA3DS_ARCH(x86_64) + const auto& caps = Common::GetCPUCaps(); + std::string cpu_string = caps.cpu_string; + if (caps.avx || caps.avx2 || caps.avx512) { + cpu_string += " | AVX"; + if (caps.avx512) { + cpu_string += "512"; + } else if (caps.avx2) { + cpu_string += '2'; + } + if (caps.fma || caps.fma4) { + cpu_string += " | FMA"; + } + } + LOG_INFO(Frontend, "Host CPU: {}", cpu_string); +#endif + LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); + const auto& mem_info = Common::GetMemInfo(); + using namespace Common::Literals; + LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB}); + LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB}); + UpdateWindowTitle(); + + show(); + + game_list->LoadCompatibilityList(); + game_list->PopulateAsync(UISettings::values.game_dirs); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); + +#ifdef ENABLE_OPENGL + gl_renderer = GetOpenGLRenderer(); +#if defined(_WIN32) + if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { + // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ + // We can override the version here to allow Citra to work. + // TODO: Remove this when OpenGL 4.0+ is advertised. + qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); + } +#endif +#endif + +#ifdef ENABLE_VULKAN + physical_devices = GetVulkanPhysicalDevices(); + if (physical_devices.empty()) { + QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), + tr("Vulkan initialization failed during boot.
" + "Your GPU may not support Vulkan 1.1, or you do not " + "have the latest graphics driver.")); + } +#endif + +#if ENABLE_QT_UPDATER + if (UISettings::values.check_for_update_on_start) { + CheckForUpdates(); + } +#endif + + QStringList args = QApplication::arguments(); + if (args.size() < 2) { + return; + } + + QString game_path; + for (int i = 1; i < args.size(); ++i) { + // Preserves drag/drop functionality + if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { + game_path = args[1]; + break; + } + + // Launch game in fullscreen mode + if (args[i] == QStringLiteral("-f")) { + ui->action_Fullscreen->setChecked(true); + continue; + } + + // Launch game in windowed mode + if (args[i] == QStringLiteral("-w")) { + ui->action_Fullscreen->setChecked(false); + continue; + } + + // Launch game at path + if (args[i] == QStringLiteral("-g")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + game_path = args[++i]; + } + } + + if (!game_path.isEmpty()) { + BootGame(game_path); + } +} + +GMainWindow::~GMainWindow() { + // Will get automatically deleted otherwise + if (!render_window->parent()) { + delete render_window; + } + + Pica::g_debug_context.reset(); + Network::Shutdown(); +} + +void GMainWindow::InitializeWidgets() { +#ifdef LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING + ui->action_Report_Compatibility->setVisible(true); +#endif + render_window = new GRenderWindow(this, emu_thread.get(), system, false); + secondary_window = new GRenderWindow(this, emu_thread.get(), system, true); + render_window->hide(); + secondary_window->hide(); + secondary_window->setParent(nullptr); + + game_list = new GameList(this); + ui->horizontalLayout->addWidget(game_list); + + game_list_placeholder = new GameListPlaceholder(this); + ui->horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui->horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, this, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + render_window->activateWindow(); + } + }); + + InputCommon::Init(); + multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(), + ui->action_Leave_Room, ui->action_Show_Room); + multiplayer_state->setVisible(false); + +#if ENABLE_QT_UPDATER + // Setup updater + updater = new Updater(this); + UISettings::values.updater_found = updater->HasUpdater(); +#endif + + UpdateBootHomeMenuState(); + + // Create status bar + message_label = new QLabel(); + // Configured separately for left alignment + message_label->setFrameStyle(QFrame::NoFrame); + message_label->setContentsMargins(4, 0, 4, 0); + message_label->setAlignment(Qt::AlignLeft); + statusBar()->addPermanentWidget(message_label, 1); + + progress_bar = new QProgressBar(); + progress_bar->hide(); + statusBar()->addPermanentWidget(progress_bar); + + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + + emu_speed_label = new QLabel(); + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label = new QLabel(); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label = new QLabel(); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { + label->setVisible(false); + label->setFrameStyle(QFrame::NoFrame); + label->setContentsMargins(4, 0, 4, 0); + statusBar()->addPermanentWidget(label); + } + + // Setup Graphics API button + graphics_api_button = new QPushButton(); + graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton")); + graphics_api_button->setFocusPolicy(Qt::NoFocus); + UpdateAPIIndicator(); + + connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); }); + + statusBar()->insertPermanentWidget(0, graphics_api_button); + + volume_popup = new QWidget(this); + volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); + volume_popup->setLayout(new QVBoxLayout()); + volume_popup->setMinimumWidth(200); + + volume_slider = new QSlider(Qt::Horizontal); + volume_slider->setObjectName(QStringLiteral("volume_slider")); + volume_slider->setMaximum(100); + volume_slider->setPageStep(5); + connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { + Settings::values.audio_muted = false; + const auto value = static_cast(percentage) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); + }); + volume_popup->layout()->addWidget(volume_slider); + + volume_button = new QPushButton(); + volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + volume_button->setFocusPolicy(Qt::NoFocus); + volume_button->setCheckable(true); + UpdateVolumeUI(); + connect(volume_button, &QPushButton::clicked, this, [&] { + UpdateVolumeUI(); + volume_popup->setVisible(!volume_popup->isVisible()); + QRect rect = volume_button->geometry(); + QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); + bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); + volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); + }); + statusBar()->insertPermanentWidget(1, volume_button); + + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + + statusBar()->setVisible(true); + + // Removes an ugly inner border from the status bar widgets under Linux + setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); + + QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows); +} + +void GMainWindow::InitializeDebugWidgets() { + connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this, + &GMainWindow::OnCreateGraphicsSurfaceViewer); + + QMenu* debug_menu = ui->menu_View_Debugging; + +#if MICROPROFILE_ENABLED + microProfileDialog = new MicroProfileDialog(this); + microProfileDialog->hide(); + debug_menu->addAction(microProfileDialog->toggleViewAction()); +#endif + + registersWidget = new RegistersWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, registersWidget); + registersWidget->hide(); + debug_menu->addAction(registersWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, registersWidget, + &RegistersWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, registersWidget, + &RegistersWidget::OnEmulationStopping); + + graphicsWidget = new GPUCommandStreamWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsWidget); + graphicsWidget->hide(); + debug_menu->addAction(graphicsWidget->toggleViewAction()); + + graphicsCommandsWidget = new GPUCommandListWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); + graphicsCommandsWidget->hide(); + debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); + + graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); + graphicsBreakpointsWidget->hide(); + debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); + + graphicsVertexShaderWidget = + new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget); + graphicsVertexShaderWidget->hide(); + debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction()); + + graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget); + graphicsTracingWidget->hide(); + debug_menu->addAction(graphicsTracingWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStopping); + + waitTreeWidget = new WaitTreeWidget(system, this); + addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget); + waitTreeWidget->hide(); + debug_menu->addAction(waitTreeWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, waitTreeWidget, + &WaitTreeWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + &WaitTreeWidget::OnEmulationStopping); + + lleServiceModulesWidget = new LLEServiceModulesWidget(this); + addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget); + lleServiceModulesWidget->hide(); + debug_menu->addAction(lleServiceModulesWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, + [this] { lleServiceModulesWidget->setDisabled(true); }); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + [this] { lleServiceModulesWidget->setDisabled(false); }); + + ipcRecorderWidget = new IPCRecorderWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); + ipcRecorderWidget->hide(); + debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, + &IPCRecorderWidget::OnEmulationStarting); +} + +void GMainWindow::InitializeRecentFileMenuActions() { + for (int i = 0; i < max_recent_files_item; ++i) { + actions_recent_files[i] = new QAction(this); + actions_recent_files[i]->setVisible(false); + connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); + + ui->menu_recent_files->addAction(actions_recent_files[i]); + } + ui->menu_recent_files->addSeparator(); + QAction* action_clear_recent_files = new QAction(this); + action_clear_recent_files->setText(tr("Clear Recent Files")); + connect(action_clear_recent_files, &QAction::triggered, this, [this] { + UISettings::values.recent_files.clear(); + UpdateRecentFiles(); + }); + ui->menu_recent_files->addAction(action_clear_recent_files); + + UpdateRecentFiles(); +} + +void GMainWindow::InitializeSaveStateMenuActions() { + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i] = new QAction(this); + actions_load_state[i]->setData(i + 1); + connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); + ui->menu_Load_State->addAction(actions_load_state[i]); + + actions_save_state[i] = new QAction(this); + actions_save_state[i]->setData(i + 1); + connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); + ui->menu_Save_State->addAction(actions_save_state[i]); + } + + connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + if (newest_slot != 0) { + actions_load_state[newest_slot - 1]->trigger(); + } + }); + connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + actions_save_state[oldest_slot - 1]->trigger(); + }); + + connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + + UpdateSaveStates(); +} + +void GMainWindow::InitializeHotkeys() { + hotkey_registry.LoadHotkeys(); + + const QString main_window = QStringLiteral("Main Window"); + const QString fullscreen = QStringLiteral("Fullscreen"); + const QString toggle_screen_layout = QStringLiteral("Toggle Screen Layout"); + const QString swap_screens = QStringLiteral("Swap Screens"); + const QString rotate_screens = QStringLiteral("Rotate Screens Upright"); + + const auto link_action_shortcut = [&](QAction* action, const QString& action_name) { + static const QString main_window = QStringLiteral("Main Window"); + action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); + action->setShortcutContext(hotkey_registry.GetShortcutContext(main_window, action_name)); + action->setAutoRepeat(false); + this->addAction(action); + }; + + link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); + link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); + link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); + link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lucina3DS")); + link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); + link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); + link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); + link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); + link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + link_action_shortcut(ui->action_Fullscreen, fullscreen); + link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, swap_screens); + link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, rotate_screens); + link_action_shortcut(ui->action_Enable_Frame_Advancing, + QStringLiteral("Toggle Frame Advancing")); + link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame")); + link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); + link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); + link_action_shortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); + link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); + link_action_shortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); + link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); + link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); + + const auto add_secondary_window_hotkey = [this](QKeySequence hotkey, const char* slot) { + // This action will fire specifically when secondary_window is in focus + QAction* secondary_window_action = new QAction(secondary_window); + secondary_window_action->setShortcut(hotkey); + + connect(secondary_window_action, SIGNAL(triggered()), this, slot); + secondary_window->addAction(secondary_window_action); + }; + + // Use the same fullscreen hotkey as the main window + const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen); + add_secondary_window_hotkey(fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen())); + + const auto toggle_screen_hotkey = + hotkey_registry.GetKeySequence(main_window, toggle_screen_layout); + add_secondary_window_hotkey(toggle_screen_hotkey, SLOT(ToggleScreenLayout())); + + const auto swap_screen_hotkey = hotkey_registry.GetKeySequence(main_window, swap_screens); + add_secondary_window_hotkey(swap_screen_hotkey, SLOT(TriggerSwapScreens())); + + const auto rotate_screen_hotkey = hotkey_registry.GetKeySequence(main_window, rotate_screens); + add_secondary_window_hotkey(rotate_screen_hotkey, SLOT(TriggerRotateScreens())); + + const auto connect_shortcut = [&](const QString& action_name, const auto& function) { + const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this); + connect(hotkey, &QShortcut::activated, this, function); + }; + + connect(hotkey_registry.GetHotkey(main_window, toggle_screen_layout, render_window), + &QShortcut::activated, this, &GMainWindow::ToggleScreenLayout); + + connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { + if (emulation_running) { + ui->action_Fullscreen->setChecked(false); + ToggleFullscreen(); + } + }); + connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] { + Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Toggle Texture Dumping"), + [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + connect_shortcut(QStringLiteral("Toggle Custom Textures"), + [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes + // the variable hold a garbage value after this function exits + static constexpr u16 SPEED_LIMIT_STEP = 5; + connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + return; + } + if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() + + SPEED_LIMIT_STEP); + } else { + Settings::values.frame_limit = 0; + } + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + Settings::values.frame_limit = 995; + } else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() - + SPEED_LIMIT_STEP); + UpdateStatusBar(); + } + UpdateStatusBar(); + }); + + connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); + connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); + connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); + + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the + // variable hold a garbage value after this function exits + static constexpr u16 FACTOR_3D_STEP = 5; + connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d > 0) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); + connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d < 100) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = + factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); +} + +void GMainWindow::SetDefaultUIGeometry() { + // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half + const QRect screenRect = screen()->geometry(); + + const int w = screenRect.width() * 2 / 3; + const int h = screenRect.height() / 2; + const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; + const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100; + + setGeometry(x, y, w, h); +} + +void GMainWindow::RestoreUIState() { + restoreGeometry(UISettings::values.geometry); + restoreState(UISettings::values.state); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); +#if MICROPROFILE_ENABLED + microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); + microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); +#endif + + game_list->LoadInterfaceLayout(); + + ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); + ToggleWindowMode(); + + ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); + SyncMenuUISettings(); + + ui->action_Display_Dock_Widget_Headers->setChecked( + UISettings::values.display_titlebar.GetValue()); + OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked()); + + ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + + ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); +} + +void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { + if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && + state != Qt::ApplicationActive) { + LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); + } + if (!emulation_running) { + return; + } + if (UISettings::values.pause_when_in_background) { + if (emu_thread->IsRunning() && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + auto_paused = true; + OnPauseGame(); + } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { + auto_paused = false; + OnStartGame(); + } + } + if (UISettings::values.mute_when_in_background) { + if (!Settings::values.audio_muted && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + Settings::values.audio_muted = true; + auto_muted = true; + } else if (auto_muted && state == Qt::ApplicationActive) { + Settings::values.audio_muted = false; + auto_muted = false; + } + UpdateVolumeUI(); + } +} + +bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::FileOpen) { + emit FileOpen(static_cast(event)); + return true; + } + return false; +} + +void GMainWindow::ConnectAppEvents() { + const auto filter = new GApplicationEventFilter(); + QGuiApplication::instance()->installEventFilter(filter); + + connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen); +} + +void GMainWindow::ConnectWidgetEvents() { + connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); + connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, + &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); + connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &GMainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, this, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); + + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, + &GMainWindow::OnGameListOpenPerGameProperties); + + connect(this, &GMainWindow::EmulationStarting, render_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, render_window, + &GRenderWindow::OnEmulationStopping); + connect(this, &GMainWindow::EmulationStarting, secondary_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, secondary_window, + &GRenderWindow::OnEmulationStopping); + + connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); + connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); + connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); +} + +void GMainWindow::ConnectMenuEvents() { + const auto connect_menu = [&](QAction* action, const auto& event_fn) { + connect(action, &QAction::triggered, this, event_fn); + // Add actions to this window so that hiding menus in fullscreen won't disable them + addAction(action); + // Add actions to the render window so that they work outside of single window mode + render_window->addAction(action); + }; + + // File + connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); + connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), + [this, region] { OnMenuBootHomeMenu(region); }); + } + connect_menu(ui->action_Exit, &QMainWindow::close); + connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); + connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo); + + // Emulation + connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); + connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); + connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); }); + connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); + connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); + connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); + + // View + connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); + connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); + connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); + connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); + + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + + connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); + connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens); + connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens); + + // Movie + connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie); + connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie); + connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie); + connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie); + connect_menu(ui->action_Movie_Read_Only_Mode, + [this](bool checked) { movie.SetReadOnly(checked); }); + connect_menu(ui->action_Enable_Frame_Advancing, [this] { + if (emulation_running) { + system.frame_limiter.SetFrameAdvancing(ui->action_Enable_Frame_Advancing->isChecked()); + ui->action_Advance_Frame->setEnabled(ui->action_Enable_Frame_Advancing->isChecked()); + } + }); + connect_menu(ui->action_Advance_Frame, [this] { + if (emulation_running && system.frame_limiter.IsFrameAdvancing()) { + ui->action_Enable_Frame_Advancing->setChecked(true); + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.AdvanceFrame(); + } + }); + connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); + connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); + + // Help + connect_menu(ui->action_Open_Lucina3DS_Folder, &GMainWindow::OnOpenLucina3DSFolder); + connect_menu(ui->action_Open_Log_Folder, []() { + QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + connect_menu(ui->action_FAQ, []() { + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); + }); + connect_menu(ui->action_About, &GMainWindow::OnMenuAboutLucina3DS); + +#if ENABLE_QT_UPDATER + connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); + connect_menu(ui->action_Open_Maintenance_Tool, &GMainWindow::OnOpenUpdater); +#endif +} + +void GMainWindow::UpdateMenuState() { + const bool is_paused = !emu_thread || !emu_thread->IsRunning(); + + const std::array running_actions{ + ui->action_Stop, + ui->action_Restart, + ui->action_Configure_Current_Game, + ui->action_Report_Compatibility, + ui->action_Load_Amiibo, + ui->action_Remove_Amiibo, + ui->action_Pause, + ui->action_Advance_Frame, + }; + + for (QAction* action : running_actions) { + action->setEnabled(emulation_running); + } + + ui->action_Capture_Screenshot->setEnabled(emulation_running); + + if (emulation_running && is_paused) { + ui->action_Pause->setText(tr("&Continue")); + } else { + ui->action_Pause->setText(tr("&Pause")); + } +} + +void GMainWindow::OnDisplayTitleBars(bool show) { + QList widgets = findChildren(); + + if (show) { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(nullptr); + if (old) { + delete old; + } + } + } else { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(new QWidget()); + if (old) { + delete old; + } + } + } +} + +#if ENABLE_QT_UPDATER +void GMainWindow::OnCheckForUpdates() { + explicit_update_check = true; + CheckForUpdates(); +} + +void GMainWindow::CheckForUpdates() { + if (updater->CheckForUpdates()) { + LOG_INFO(Frontend, "Update check started"); + } else { + LOG_WARNING(Frontend, "Unable to start check for updates"); + } +} + +void GMainWindow::OnUpdateFound(bool found, bool error) { + if (error) { + LOG_WARNING(Frontend, "Update check failed"); + return; + } + + if (!found) { + LOG_INFO(Frontend, "No updates found"); + + // If the user explicitly clicked the "Check for Updates" button, we are + // going to want to show them a prompt anyway. + if (explicit_update_check) { + explicit_update_check = false; + ShowNoUpdatePrompt(); + } + return; + } + + if (emulation_running && !explicit_update_check) { + LOG_INFO(Frontend, "Update found, deferring as game is running"); + defer_update_prompt = true; + return; + } + + LOG_INFO(Frontend, "Update found!"); + explicit_update_check = false; + + ShowUpdatePrompt(); +} + +void GMainWindow::ShowUpdatePrompt() { + defer_update_prompt = false; + + auto result = + QMessageBox::question(this, tr("Update Available"), + tr("An update is available. Would you like to install it now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + updater->LaunchUIOnExit(); + close(); + } +} + +void GMainWindow::ShowNoUpdatePrompt() { + QMessageBox::information(this, tr("No Update Found"), tr("No update is found."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void GMainWindow::OnOpenUpdater() { + updater->LaunchUI(); +} + +void GMainWindow::ShowUpdaterWidgets() { + ui->action_Check_For_Updates->setVisible(UISettings::values.updater_found); + ui->action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found); + + connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound); +} +#endif + +#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) +static std::optional HoldWakeLockLinux(u32 window_id = 0) { + if (!QDBusConnection::sessionBus().isConnected()) { + return {}; + } + // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit + QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Inhibit")); + if (!xdp.isValid()) { + LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); + return {}; + } + QVariantMap options = {}; + //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the + //: computer from sleeping + options.insert(QString::fromLatin1("reason"), + QCoreApplication::translate("GMainWindow", "Luinca3DS is running a game")); + // 0x4: Suspend lock; 0x8: Idle lock + QDBusReply reply = + xdp.call(QString::fromLatin1("Inhibit"), + QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); + + if (reply.isValid()) { + return reply.value(); + } + LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", + reply.error().message().toStdString()); + return {}; +} + +static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { + if (!QDBusConnection::sessionBus().isConnected()) { + return; + } + QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), + QString::fromLatin1("org.freedesktop.portal.Request")); + unlocker.call(QString::fromLatin1("Close")); +} +#endif // __unix__ + +void GMainWindow::PreventOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + auto reply = HoldWakeLockLinux(winId()); + if (reply) { + wake_lock = std::move(reply.value()); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +void GMainWindow::AllowOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(HAVE_SDL2) + SDL_EnableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + if (!wake_lock.path().isEmpty()) { + ReleaseWakeLockLinux(wake_lock); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +bool GMainWindow::LoadROM(const QString& filename) { + // Shutdown previous session if the emu thread is still active... + if (emu_thread) { + ShutdownGame(); + } + + if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) { + LOG_CRITICAL(Frontend, "Failed to initialize render targets!"); + return false; + } + + const auto scope = render_window->Acquire(); + + const Core::System::ResultStatus result{ + system.Load(*render_window, filename.toStdString(), secondary_window)}; + + if (result != Core::System::ResultStatus::Success) { + switch (result) { + case Core::System::ResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorSystemMode: + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + QMessageBox::critical( + this, tr("ROM Corrupted"), + tr("Your ROM is corrupted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { + QMessageBox::critical( + this, tr("ROM Encrypted"), + tr("Your ROM is encrypted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + } + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + QMessageBox::critical(this, tr("Unsupported ROM"), + tr("GBA Virtual Console ROMs are not supported by Lucina3DS.")); + break; + + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr(fmt::format( + "An error has occurred whilst communicating with the Artic Base Server.\n{}", + system.GetStatusDetails()) + .c_str())); + break; + default: + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + break; + } + return false; + } + + std::string title; + system.GetAppLoader().ReadTitle(title); + game_title = QString::fromStdString(title); + UpdateWindowTitle(); + + game_path = filename; + + return true; +} + +void GMainWindow::BootGame(const QString& filename) { + if (emu_thread) { + ShutdownGame(); + } + + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { + const auto answer = QMessageBox::question( + this, tr("CIA must be installed before usage"), + tr("Before using this CIA, you must install it. Do you want to install it now?"), + QMessageBox::Yes | QMessageBox::No); + + if (answer == QMessageBox::Yes) + InstallCIA(QStringList(filename)); + + return; + } + + show_artic_label = is_artic; + + LOG_INFO(Frontend, "Lucinca3DS starting..."); + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } + + if (movie_record_on_start) { + movie.PrepareForRecording(); + } + if (movie_playback_on_start) { + movie.PrepareForPlayback(movie_playback_path.toStdString()); + } + + const std::string path = filename.toStdString(); + auto loader = Loader::GetLoader(path); + + u64 title_id{0}; + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } + + system.ApplySettings(); + + Settings::LogSettings(); + + // Save configurations + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + config->Save(); + + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); + return; + } + + // Set everything up + if (movie_record_on_start) { + movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (movie_playback_on_start) { + movie.StartPlayback(movie_playback_path.toStdString()); + movie_playback_on_start = false; + movie_playback_path.clear(); + } + + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + + if (video_dumping_on_start) { + StartVideoDumping(video_dumping_path); + video_dumping_on_start = false; + video_dumping_path.clear(); + } + + // Register debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Register(); + } + + // Create and start the emulation thread + emu_thread = std::make_unique(system, *render_window); + emit EmulationStarting(emu_thread.get()); + emu_thread->start(); + + connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + + // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views + // before the CPU continues + connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget, + &RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget, + &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget, + &RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, + &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, + &LoadingScreen::OnLoadComplete); + + // Update the GUI + registersWidget->OnDebugModeEntered(); + if (ui->action_Single_Window_Mode->isChecked()) { + game_list->hide(); + game_list_placeholder->hide(); + } + status_bar_update_timer.start(1000); + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + } + + loading_screen->Prepare(system.GetAppLoader()); + loading_screen->show(); + + emulation_running = true; + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } + + OnStartGame(); +} + +void GMainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + if (ui->action_Fullscreen->isChecked()) { + HideFullscreen(); + } + + auto video_dumper = system.GetVideoDumper(); + if (video_dumper && video_dumper->IsDumping()) { + game_shutdown_delayed = true; + OnStopVideoDumping(); + return; + } + + AllowOSSleep(); + + discord_rpc->Pause(); + emu_thread->RequestStop(); + + // Release emu threads from any breakpoints + // This belongs after RequestStop() and before wait() because if emulation stops on a GPU + // breakpoint after (or before) RequestStop() is called, the emulation would never be able + // to continue out to the main loop and terminate. Thus wait() would hang forever. + // TODO(bunnei): This function is not thread safe, but it's being used as if it were + Pica::g_debug_context->ClearBreakpoints(); + + // Unregister debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Unregister(); + } + + // Frame advancing must be cancelled in order to release the emu thread from waiting + system.frame_limiter.SetFrameAdvancing(false); + + emit EmulationStopping(); + + // Wait for emulation thread to complete and delete it + emu_thread->wait(); + emu_thread = nullptr; + + OnCloseMovie(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif + + // The emulation is stopped, so closing the window or not does not matter anymore + disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + + render_window->hide(); + secondary_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); + + if (game_list->IsEmpty()) { + game_list_placeholder->show(); + } else { + game_list->show(); + } + game_list->SetFilterFocus(); + + setMouseTracking(false); + + // Disable status bar updates + status_bar_update_timer.stop(); + message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); + emu_speed_label->setVisible(false); + game_fps_label->setVisible(false); + emu_frametime_label->setVisible(false); + + UpdateSaveStates(); + + emulation_running = false; + +#if ENABLE_QT_UDPATER + if (defer_update_prompt) { + ShowUpdatePrompt(); + } +#endif + + game_title.clear(); + UpdateWindowTitle(); + + game_path.clear(); + + // Update the GUI + UpdateMenuState(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); +} + +void GMainWindow::StoreRecentFile(const QString& filename) { + UISettings::values.recent_files.prepend(filename); + UISettings::values.recent_files.removeDuplicates(); + while (UISettings::values.recent_files.size() > max_recent_files_item) { + UISettings::values.recent_files.removeLast(); + } + + UpdateRecentFiles(); +} + +void GMainWindow::UpdateRecentFiles() { + const int num_recent_files = + std::min(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); + + for (int i = 0; i < num_recent_files; i++) { + const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( + QFileInfo(UISettings::values.recent_files[i]).fileName()); + actions_recent_files[i]->setText(text); + actions_recent_files[i]->setData(UISettings::values.recent_files[i]); + actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); + actions_recent_files[i]->setVisible(true); + } + + for (int j = num_recent_files; j < max_recent_files_item; ++j) { + actions_recent_files[j]->setVisible(false); + } + + // Enable the recent files menu if the list isn't empty + ui->menu_recent_files->setEnabled(num_recent_files != 0); +} + +void GMainWindow::UpdateSaveStates() { + if (!system.IsPoweredOn()) { + ui->menu_Load_State->setEnabled(false); + ui->menu_Save_State->setEnabled(false); + return; + } + + ui->menu_Load_State->setEnabled(true); + ui->menu_Save_State->setEnabled(true); + ui->action_Load_from_Newest_Slot->setEnabled(false); + + oldest_slot = newest_slot = 0; + oldest_slot_time = std::numeric_limits::max(); + newest_slot_time = 0; + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return; + } + auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID()); + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i]->setEnabled(false); + actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); + actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); + } + for (const auto& savestate : savestates) { + const bool display_name = + savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch && + !savestate.build_name.empty(); + const auto text = + tr("Slot %1 - %2 %3") + .arg(savestate.slot) + .arg(QDateTime::fromSecsSinceEpoch(savestate.time) + .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))) + .arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String()) + .trimmed(); + + actions_load_state[savestate.slot - 1]->setEnabled(true); + actions_load_state[savestate.slot - 1]->setText(text); + actions_save_state[savestate.slot - 1]->setText(text); + + ui->action_Load_from_Newest_Slot->setEnabled(true); + + if (savestate.time > newest_slot_time) { + newest_slot = savestate.slot; + newest_slot_time = savestate.time; + } + if (savestate.time < oldest_slot_time) { + oldest_slot = savestate.slot; + oldest_slot_time = savestate.time; + } + } + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + if (!actions_load_state[i]->isEnabled()) { + // Prefer empty slot + oldest_slot = i + 1; + oldest_slot_time = 0; + break; + } + } +} + +void GMainWindow::OnGameListLoadFile(QString game_path) { + if (ConfirmChangeGame()) { + BootGame(game_path); + } +} + +void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { + std::string path; + std::string open_target; + + switch (target) { + case GameListOpenTarget::SAVE_DATA: { + open_target = "Save Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::EXT_DATA: { + open_target = "Extra Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::APPLICATION: { + open_target = "Application"; + auto media_type = Service::AM::GetTitleMediaType(data_id); + path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; + break; + } + case GameListOpenTarget::UPDATE_DATA: { + open_target = "Update Data"; + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + + "content/"; + break; + } + case GameListOpenTarget::TEXTURE_DUMP: { + open_target = "Dumped Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); + break; + } + case GameListOpenTarget::TEXTURE_LOAD: { + open_target = "Custom Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); + break; + } + case GameListOpenTarget::MODS: { + open_target = "Mods"; + path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + data_id); + break; + } + case GameListOpenTarget::DLC_DATA: { + open_target = "DLC Data"; + path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/0004008c/{:08x}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id); + break; + } + case GameListOpenTarget::SHADER_CACHE: { + open_target = "Shader Cache"; + path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir); + break; + } + default: + LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); + return; + } + + QString qpath = QString::fromStdString(path); + + QDir dir(qpath); + if (!dir.exists()) { + QMessageBox::critical( + this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)), + tr("Folder does not exist!")); + return; + } + + LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); + + QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); +} + +void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + QString directory; + if (it != compatibility_list.end()) + directory = it->second.second; + + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory)); +} + +void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { + auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setWindowFlags(dialog->windowFlags() & + ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); + dialog->setCancelButton(nullptr); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + const auto base_path = fmt::format( + "{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); + const auto update_path = + fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + program_id | 0x0004000e00000000); + using FutureWatcher = QFutureWatcher>; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, + [this, dialog, base_path, update_path, future_watcher] { + dialog->hide(); + const auto& [base, update] = future_watcher->result(); + if (base != Loader::ResultStatus::Success) { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not dump base RomFS.\nRefer to the log for details.")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path))); + if (update == Loader::ResultStatus::Success) { + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(update_path))); + } + }); + + auto future = QtConcurrent::run([game_path, base_path, update_path] { + std::unique_ptr loader = Loader::GetLoader(game_path.toStdString()); + return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path)); + }); + future_watcher->setFuture(future); +} + +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { + QString path; + if (directory == QStringLiteral("INSTALLED")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); + } else if (directory == QStringLiteral("SYSTEM")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000/title/00040010"); + } else { + path = directory; + } + if (!QFileInfo::exists(path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) + return; + UISettings::GameDir game_dir{dir_path, false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } +} + +void GMainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui->action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + +void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { + const auto loader = Loader::GetLoader(file.toStdString()); + + u64 title_id{}; + if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { + QMessageBox::information(this, tr("Properties"), + tr("The game properties could not be loaded.")); + return; + } + + OpenPerGameConfiguration(title_id, file); +} + +void GMainWindow::OnMenuLoadFile() { + const QString extensions = QStringLiteral("*.").append( + GameList::supported_file_extensions.join(QStringLiteral(" *."))); + const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)", + "%1 is an identifier for the 3DS executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), UISettings::values.roms_path, file_filter); + + if (filename.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + +void GMainWindow::OnMenuInstallCIA() { + QStringList filepaths = QFileDialog::getOpenFileNames( + this, tr("Load Files"), UISettings::values.roms_path, + tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)")); + + if (filepaths.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filepaths[0]).path(); + InstallCIA(filepaths); +} + +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + +void GMainWindow::OnMenuBootHomeMenu(u32 region) { + BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); +} + +void GMainWindow::InstallCIA(QStringList filepaths) { + ui->action_Install_CIA->setEnabled(false); + game_list->SetDirectoryWatcherEnabled(false); + progress_bar->show(); + progress_bar->setMaximum(INT_MAX); + + (void)QtConcurrent::run([&, filepaths] { + Service::AM::InstallStatus status; + const auto cia_progress = [&](std::size_t written, std::size_t total) { + emit UpdateProgress(written, total); + }; + for (const auto& current_path : filepaths) { + status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress); + emit CIAInstallReport(status, current_path); + } + emit CIAInstallFinished(); + }); +} + +void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) { + progress_bar->setValue( + static_cast(INT_MAX * (static_cast(written) / static_cast(total)))); +} + +void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) { + QString filename = QFileInfo(filepath).fileName(); + switch (status) { + case Service::AM::InstallStatus::Success: + this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFailedToOpenFile: + QMessageBox::critical(this, tr("Unable to open File"), + tr("Could not open %1").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorAborted: + QMessageBox::critical( + this, tr("Installation aborted"), + tr("The installation of %1 was aborted. Please see the log for more details") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorInvalid: + QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorEncrypted: + QMessageBox::critical(this, tr("Encrypted File"), + tr("%1 must be decrypted " + "before being used with Lucina3DS. A real 3DS is required.") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFileNotFound: + QMessageBox::critical(this, tr("Unable to find File"), + tr("Could not find %1").arg(filename)); + break; + } +} + +void GMainWindow::OnCIAInstallFinished() { + progress_bar->hide(); + progress_bar->setValue(0); + game_list->SetDirectoryWatcherEnabled(true); + ui->action_Install_CIA->setEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void GMainWindow::UninstallTitles( + const std::vector>& titles) { + if (titles.empty()) { + return; + } + + // Select the first title in the list as representative. + const auto first_name = std::get(titles[0]); + + QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, + static_cast(titles.size()), this); + progress.setWindowModality(Qt::WindowModal); + + QFutureWatcher future_watcher; + QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, + &QProgressDialog::reset); + QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, + &QProgressDialog::setValue); + + auto failed = false; + QString failed_name; + + const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { + const auto name = std::get(title); + const auto media_type = std::get(title); + const auto program_id = std::get(title); + + const auto result = Service::AM::UninstallProgram(media_type, program_id); + if (result.IsError()) { + LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), + result.raw); + failed = true; + failed_name = name; + future_watcher.cancel(); + } + }; + + future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); + progress.exec(); + future_watcher.waitForFinished(); + + if (failed) { + QMessageBox::critical(this, tr("Lucina3DS"), tr("Failed to uninstall '%1'.").arg(failed_name)); + } else if (!future_watcher.isCanceled()) { + QMessageBox::information(this, tr("Lucina3DS"), + tr("Successfully uninstalled '%1'.").arg(first_name)); + } +} + +void GMainWindow::OnMenuRecentFile() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + const QString filename = action->data().toString(); + if (QFileInfo::exists(filename)) { + BootGame(filename); + } else { + // Display an error message and remove the file from the list. + QMessageBox::information(this, tr("File not found"), + tr("File \"%1\" not found").arg(filename)); + + UISettings::values.recent_files.removeOne(filename); + UpdateRecentFiles(); + } +} + +void GMainWindow::OnStartGame() { + qt_cameras->ResumeCameras(); + + PreventOSSleep(); + + emu_thread->SetRunning(true); + graphics_api_button->setEnabled(false); + qRegisterMetaType("Core::System::ResultStatus"); + qRegisterMetaType("std::string"); + connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); + + UpdateMenuState(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StartGamemode(); +#endif + + UpdateSaveStates(); + UpdateStatusButtons(); +} + +void GMainWindow::OnRestartGame() { + if (!system.IsPoweredOn()) { + return; + } + // Make a copy since BootGame edits game_path + BootGame(QString(game_path)); +} + +void GMainWindow::OnPauseGame() { + emu_thread->SetRunning(false); + qt_cameras->PauseCameras(); + + UpdateMenuState(); + AllowOSSleep(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif +} + +void GMainWindow::OnPauseContinueGame() { + if (emulation_running) { + if (emu_thread->IsRunning()) { + OnPauseGame(); + } else { + OnStartGame(); + } + } +} + +void GMainWindow::OnStopGame() { + ShutdownGame(); + graphics_api_button->setEnabled(true); + Settings::RestoreGlobalState(false); + UpdateStatusButtons(); +} + +void GMainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnMenuReportCompatibility() { + if (!NetSettings::values.lucina3ds_token.empty() && !NetSettings::values.lucina3ds_username.empty()) { + CompatDB compatdb{this}; + compatdb.exec(); + } else { + QMessageBox::critical(this, tr("Missing Citra Account"), + tr("You must link your Citra account to submit test cases." + "
Go to Emulation > Configure... > Web to do so.")); + } +} + +void GMainWindow::ToggleFullscreen() { + if (!emulation_running) { + return; + } + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } else { + HideFullscreen(); + } +} + +void GMainWindow::ToggleSecondaryFullscreen() { + if (!emulation_running) { + return; + } + if (secondary_window->isFullScreen()) { + secondary_window->showNormal(); + } else { + secondary_window->showFullScreen(); + } +} + +void GMainWindow::ShowFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + UISettings::values.geometry = saveGeometry(); + ui->menubar->hide(); + statusBar()->hide(); + showFullScreen(); + } else { + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + render_window->showFullScreen(); + } +} + +void GMainWindow::HideFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->menubar->show(); + showNormal(); + restoreGeometry(UISettings::values.geometry); + } else { + render_window->showNormal(); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } +} + +void GMainWindow::ToggleWindowMode() { + if (ui->action_Single_Window_Mode->isChecked()) { + // Render in the main window... + render_window->BackupGeometry(); + ui->horizontalLayout->addWidget(render_window); + render_window->setFocusPolicy(Qt::StrongFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->setFocus(); + game_list->hide(); + } + + } else { + // Render in a separate window... + ui->horizontalLayout->removeWidget(render_window); + render_window->setParent(nullptr); + render_window->setFocusPolicy(Qt::NoFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->RestoreGeometry(); + game_list->show(); + } + } +} + +void GMainWindow::UpdateSecondaryWindowVisibility() { + if (!emulation_running) { + return; + } + if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) { + secondary_window->RestoreGeometry(); + secondary_window->show(); + } else { + secondary_window->BackupGeometry(); + secondary_window->hide(); + } +} + +void GMainWindow::ChangeScreenLayout() { + Settings::LayoutOption new_layout = Settings::LayoutOption::Default; + + if (ui->action_Screen_Layout_Default->isChecked()) { + new_layout = Settings::LayoutOption::Default; + } else if (ui->action_Screen_Layout_Single_Screen->isChecked()) { + new_layout = Settings::LayoutOption::SingleScreen; + } else if (ui->action_Screen_Layout_Large_Screen->isChecked()) { + new_layout = Settings::LayoutOption::LargeScreen; + } else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) { + new_layout = Settings::LayoutOption::HybridScreen; + } else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) { + new_layout = Settings::LayoutOption::SideScreen; + } else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) { + new_layout = Settings::LayoutOption::SeparateWindows; + } + + Settings::values.layout_option = new_layout; + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::ToggleScreenLayout() { + const Settings::LayoutOption new_layout = []() { + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::Default: + return Settings::LayoutOption::SingleScreen; + case Settings::LayoutOption::SingleScreen: + return Settings::LayoutOption::LargeScreen; + case Settings::LayoutOption::LargeScreen: + return Settings::LayoutOption::HybridScreen; + case Settings::LayoutOption::HybridScreen: + return Settings::LayoutOption::SideScreen; + case Settings::LayoutOption::SideScreen: + return Settings::LayoutOption::SeparateWindows; + case Settings::LayoutOption::SeparateWindows: + return Settings::LayoutOption::Default; + default: + LOG_ERROR(Frontend, "Unknown layout option {}", + Settings::values.layout_option.GetValue()); + return Settings::LayoutOption::Default; + } + }(); + + Settings::values.layout_option = new_layout; + SyncMenuUISettings(); + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnSwapScreens() { + Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::OnRotateScreens() { + Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::TriggerSwapScreens() { + ui->action_Screen_Layout_Swap_Screens->trigger(); +} + +void GMainWindow::TriggerRotateScreens() { + ui->action_Screen_Layout_Upright_Screens->trigger(); +} + +void GMainWindow::OnSaveState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + system.SendSignal(Core::System::Signal::Save, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); + newest_slot = action->data().toUInt(); +} + +void GMainWindow::OnLoadState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + if (UISettings::values.save_state_warning) { + QMessageBox::warning(this, tr("Savestates"), + tr("Warning: Savestates are NOT a replacement for in-game saves, " + "and are not meant to be reliable.\n\nUse at your own risk!")); + UISettings::values.save_state_warning = false; + config->Save(); + } + + system.SendSignal(Core::System::Signal::Load, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); +} + +void GMainWindow::OnConfigure() { + game_list->SetDirectoryWatcherEnabled(false); + Settings::SetConfiguringGlobal(true); + ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices, + !multiplayer_state->IsHostingPublicRoom()); + connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, + &GMainWindow::OnLanguageChanged); + auto old_theme = UISettings::values.theme; + const int old_input_profile_index = Settings::values.current_input_profile_index; + const auto old_input_profiles = Settings::values.input_profiles; + const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps; + const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); +#ifdef __unix__ + const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); +#endif + auto result = configureDialog.exec(); + game_list->SetDirectoryWatcherEnabled(true); + if (result == QDialog::Accepted) { + configureDialog.ApplyConfiguration(); + InitializeHotkeys(); + if (UISettings::values.theme != old_theme) { + UpdateUITheme(); + } + if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + } +#ifdef __unix__ + if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); + } +#endif + if (!multiplayer_state->IsHostingPublicRoom()) + multiplayer_state->UpdateCredentials(); + emit UpdateThemedIcons(); + SyncMenuUISettings(); + game_list->RefreshGameDirectory(); + config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + } + UpdateSecondaryWindowVisibility(); + UpdateBootHomeMenuState(); + UpdateStatusButtons(); + } else { + Settings::values.input_profiles = old_input_profiles; + Settings::values.touch_from_button_maps = old_touch_from_button_maps; + Settings::LoadProfile(old_input_profile_index); + } +} + +void GMainWindow::OnLoadAmiibo() { + if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { + return; + } + + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr) { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (nfc->IsTagActive()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("A tag is already in use.")); + return; + } + + if (!nfc->IsSearchingForAmiibos()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Game is not looking for amiibos.")); + return; + } + + const QString extensions{QStringLiteral("*.bin")}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); + + if (filename.isEmpty()) { + return; + } + + LoadAmiibo(filename); +} + +void GMainWindow::LoadAmiibo(const QString& filename) { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (!nfc->LoadAmiibo(filename.toStdString())) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Unable to open amiibo file \"%1\" for reading.").arg(filename)); + return; + } + + ui->action_Remove_Amiibo->setEnabled(true); +} + +void GMainWindow::OnRemoveAmiibo() { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + nfc->RemoveAmiibo(); + ui->action_Remove_Amiibo->setEnabled(false); +} + +void GMainWindow::OnOpenLucina3DSFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); +} + +void GMainWindow::OnToggleFilterBar() { + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + if (ui->action_Show_Filter_Bar->isChecked()) { + game_list->SetFilterFocus(); + } else { + game_list->ClearFilter(); + } +} + +void GMainWindow::OnCreateGraphicsSurfaceViewer() { + auto graphicsSurfaceViewerWidget = + new GraphicsSurfaceWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget); + // TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true); + graphicsSurfaceViewerWidget->show(); +} + +void GMainWindow::OnRecordMovie() { + MovieRecordDialog dialog(this, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_record_on_start = true; + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); + + if (emulation_running) { // Restart game + BootGame(QString(game_path)); + } + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(true); +} + +void GMainWindow::OnPlayMovie() { + MoviePlayDialog dialog(this, game_list, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_playback_on_start = true; + movie_playback_path = dialog.GetMoviePath(); + BootGame(dialog.GetGamePath()); + + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnCloseMovie() { + if (movie_record_on_start) { + QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } else { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording; + movie.Shutdown(); + if (was_recording) { + QMessageBox::information(this, tr("Movie Saved"), + tr("The movie is successfully saved.")); + } + + if (was_running) { + OnStartGame(); + } + } + + ui->action_Close_Movie->setEnabled(false); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnSaveMovie() { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) { + movie.SaveMovie(); + QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); + } else { + LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); + } + + if (was_running) { + OnStartGame(); + } +} + +void GMainWindow::OnCaptureScreenshot() { + if (!emu_thread) [[unlikely]] { + return; + } + + const bool was_running = emu_thread->IsRunning(); + + if (was_running || + (QMessageBox::question( + this, tr("Game will unpause"), + tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running) { + OnPauseGame(); + } + std::string path = UISettings::values.screenshot_path.GetValue(); + if (!FileUtil::IsDirectory(path)) { + if (!FileUtil::CreateFullPath(path)) { + QMessageBox::information( + this, tr("Invalid Screenshot Directory"), + tr("Cannot create specified screenshot directory. Screenshot " + "path is set back to its default value.")); + path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + path.append("screenshots/"); + UISettings::values.screenshot_path = path; + }; + } + + static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); + const std::string filename = game_title.remove(expr).toStdString(); + const std::string timestamp = QDateTime::currentDateTime() + .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) + .toStdString(); + path.append(fmt::format("/{}_{}.png", filename, timestamp)); + + auto* const screenshot_window = + secondary_window->HasFocus() ? secondary_window : render_window; + screenshot_window->CaptureScreenshot( + UISettings::values.screenshot_resolution_factor.GetValue(), + QString::fromStdString(path)); + OnStartGame(); + } +} + +void GMainWindow::OnDumpVideo() { + if (DynamicLibrary::FFmpeg::LoadFFmpeg()) { + if (ui->action_Dump_Video->isChecked()) { + OnStartVideoDumping(); + } else { + OnStopVideoDumping(); + } + } else { + ui->action_Dump_Video->setChecked(false); + + QMessageBox message_box; + message_box.setWindowTitle(tr("Could not load video dumper")); + message_box.setText( + tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." +#ifdef _WIN32 + "\n\nTo install FFmpeg to Lucina3DS, press Open and select your FFmpeg directory." +#endif + "\n\nTo view a guide on how to install FFmpeg, press Help.")); + message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help +#ifdef _WIN32 + | QMessageBox::Open +#endif + ); + auto result = message_box.exec(); + if (result == QMessageBox::Help) { + QDesktopServices::openUrl(QUrl(QStringLiteral( + "https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/"))); +#ifdef _WIN32 + } else if (result == QMessageBox::Open) { + OnOpenFFmpeg(); +#endif + } + } +} + +#ifdef _WIN32 +void GMainWindow::OnOpenFFmpeg() { + auto filename = + QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString(); + if (filename.empty()) { + return; + } + // Check for a bin directory if they chose the FFmpeg root directory. + auto bin_dir = filename + DIR_SEP + "bin"; + if (!FileUtil::Exists(bin_dir)) { + // Otherwise, assume the user directly selected the directory containing the DLLs. + bin_dir = filename; + } + + static const std::array library_names = { + Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR), + }; + + for (auto& library_name : library_names) { + if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("The provided FFmpeg directory is missing %1. Please make " + "sure the correct directory was selected.") + .arg(QString::fromStdString(library_name))); + return; + } + } + + std::atomic success(true); + auto process_file = [&success](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { + auto file_path = directory + DIR_SEP + virtual_name; + if (file_path.ends_with(".dll")) { + auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name; + if (!FileUtil::Copy(file_path, destination_path)) { + success.store(false); + return false; + } + } + return true; + }; + FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); + + if (success.load()) { + QMessageBox::information(this, tr("Lucina3DS"), tr("FFmpeg has been sucessfully installed.")); + } else { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("Installation of FFmpeg failed. Check the log file for details.")); + } +} +#endif + +void GMainWindow::OnStartVideoDumping() { + DumpingDialog dialog(this, system); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + ui->action_Dump_Video->setChecked(false); + return; + } + const auto path = dialog.GetFilePath(); + if (emulation_running) { + StartVideoDumping(path); + } else { + video_dumping_on_start = true; + video_dumping_path = path; + } +} + +void GMainWindow::StartVideoDumping(const QString& path) { + auto& renderer = system.GPU().Renderer(); + const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; + + auto dumper = std::make_shared(renderer); + if (dumper->StartDumping(path.toStdString(), layout)) { + system.RegisterVideoDumper(dumper); + } else { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui->action_Dump_Video->setChecked(false); + } +} + +void GMainWindow::OnStopVideoDumping() { + ui->action_Dump_Video->setChecked(false); + + if (video_dumping_on_start) { + video_dumping_on_start = false; + video_dumping_path.clear(); + } else { + auto dumper = system.GetVideoDumper(); + if (!dumper || !dumper->IsDumping()) { + return; + } + + game_paused_for_dumping = emu_thread->IsRunning(); + OnPauseGame(); + + auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, [this] { + if (game_shutdown_delayed) { + game_shutdown_delayed = false; + ShutdownGame(); + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; + OnStartGame(); + } + }); + future_watcher->setFuture(future); + } +} + +void GMainWindow::UpdateStatusBar() { + if (!emu_thread) [[unlikely]] { + status_bar_update_timer.stop(); + return; + } + + // Update movie status + const u64 current = movie.GetCurrentInputIndex(); + const u64 total = movie.GetTotalInputCount(); + const auto play_mode = movie.GetPlayMode(); + if (play_mode == Core::Movie::PlayMode::Recording) { + message_label->setText(tr("Recording %1").arg(current)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(true); + } else if (play_mode == Core::Movie::PlayMode::Playing) { + message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { + message_label->setText(tr("Movie Finished")); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (message_label_used_for_movie) { // Clear the label if movie was just closed + message_label->setText(QString{}); + message_label_used_for_movie = false; + ui->action_Save_Movie->setEnabled(false); + } + + auto results = system.GetAndResetPerfStats(); + + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 5> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + tr("(Accessing SystemSaveData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + + if (Settings::values.frame_limit.GetValue() == 0) { + emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); + } else { + emu_speed_label->setText(tr("Speed: %1% / %2%") + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.frame_limit.GetValue())); + } + game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); + emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } + emu_speed_label->setVisible(true); + game_fps_label->setVisible(true); + emu_frametime_label->setVisible(true); +} + +void GMainWindow::UpdateBootHomeMenuState() { + const auto current_region = Settings::values.region_value.GetValue(); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + const auto path = Core::GetHomeMenuNcchPath(region); + ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled( + (current_region == Settings::REGION_VALUE_AUTO_SELECT || + current_region == static_cast(region)) && + !path.empty() && FileUtil::Exists(path)); + } +} + +void GMainWindow::HideMouseCursor() { + if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + render_window->setCursor(QCursor(Qt::BlankCursor)); + secondary_window->setCursor(QCursor(Qt::BlankCursor)); + if (UISettings::values.single_window_mode.GetValue()) { + setCursor(QCursor(Qt::BlankCursor)); + } +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + render_window->unsetCursor(); + secondary_window->unsetCursor(); + if (emu_thread && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::OnMute() { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); +} + +void GMainWindow::OnDecreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume <= 30) { + step = 2; + } + if (current_volume <= 6) { + step = 1; + } + const auto value = + static_cast(std::max(current_volume - step, 0)) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::OnIncreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume < 30) { + step = 2; + } + if (current_volume < 6) { + step = 1; + } + const auto value = static_cast(current_volume + step) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::UpdateVolumeUI() { + const auto volume_value = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + volume_slider->setValue(volume_value); + if (Settings::values.audio_muted) { + volume_button->setChecked(false); + volume_button->setText(tr("VOLUME: MUTE")); + } else { + volume_button->setChecked(true); + volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); + } +} + +void GMainWindow::UpdateAPIIndicator(bool update) { + static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"), + QStringLiteral("VULKAN")}; + + static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"), + QStringLiteral("#91242a")}; + + u32 api_index = static_cast(Settings::values.graphics_api.GetValue()); + if (update) { + api_index = (api_index + 1) % graphics_apis.size(); + // Skip past any disabled renderers. +#ifndef ENABLE_SOFTWARE_RENDERER + if (api_index == static_cast(Settings::GraphicsAPI::Software)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_OPENGL + if (api_index == static_cast(Settings::GraphicsAPI::OpenGL)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_VULKAN + if (api_index == static_cast(Settings::GraphicsAPI::Vulkan)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif + Settings::values.graphics_api = static_cast(api_index); + } + + const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }") + .arg(graphics_api_colors[api_index]); + + graphics_api_button->setText(graphics_apis[api_index]); + graphics_api_button->setStyleSheet(style_sheet); +} + +void GMainWindow::UpdateStatusButtons() { + UpdateAPIIndicator(); + UpdateVolumeUI(); +} + +void GMainWindow::OnMouseActivity() { + ShowMouseCursor(); +} + +void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { + QString status_message; + + QString title, message; + QMessageBox::Icon error_severity_icon; + bool can_continue = true; + if (result == Core::System::ResultStatus::ErrorSystemFiles) { + const QString common_message = + tr("%1 is missing. Please dump your " + "system archives.
Continuing emulation may result in crashes and bugs."); + + if (!details.empty()) { + message = common_message.arg(QString::fromStdString(details)); + } else { + message = common_message.arg(tr("A system archive")); + } + + title = tr("System Archive Not Found"); + status_message = tr("System Archive Missing"); + error_severity_icon = QMessageBox::Icon::Critical; + } else if (result == Core::System::ResultStatus::ErrorSavestate) { + title = tr("Save/load Error"); + message = QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Warning; + } else if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + title = tr("Artic Base Server"); + message = + tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details) + .c_str()); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else { + title = tr("Fatal Error"); + message = + tr("A fatal error occurred. " + "Check " + "the log for details." + "
Continuing emulation may result in crashes and bugs."); + status_message = tr("Fatal Error encountered"); + error_severity_icon = QMessageBox::Icon::Critical; + } + + QMessageBox message_box; + message_box.setWindowTitle(title); + message_box.setText(message); + message_box.setIcon(error_severity_icon); + if (error_severity_icon == QMessageBox::Icon::Critical) { + if (can_continue) { + message_box.addButton(tr("Continue"), QMessageBox::RejectRole); + } + QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); + if (result != Core::System::ResultStatus::ShutdownRequested) + message_box.exec(); + + if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested || + message_box.clickedButton() == abort_button) { + if (emu_thread) { + ShutdownGame(); + return; + } + } + } else { + // This block should run when the error isn't too big of a deal + // e.g. when a save state can't be saved or loaded + message_box.addButton(tr("OK"), QMessageBox::RejectRole); + message_box.exec(); + } + + // Only show the message if the game is still running. + if (emu_thread) { + emu_thread->SetRunning(true); + message_label->setText(status_message); + message_label_used_for_movie = false; + } +} + +void GMainWindow::OnMenuAboutLucina3DS() { + AboutDialog about{this}; + about.exec(); +} + +bool GMainWindow::ConfirmClose() { + if (!emu_thread || !UISettings::values.confirm_before_closing) { + return true; + } + + QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Lucina3DS"), tr("Would you like to exit now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::closeEvent(QCloseEvent* event) { + if (!ConfirmClose()) { + event->ignore(); + return; + } + + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + hotkey_registry.SaveHotkeys(); + + // Shutdown session if the emu thread is active... + if (emu_thread) { + ShutdownGame(); + } + + render_window->close(); + secondary_window->close(); + multiplayer_state->Close(); + InputCommon::Shutdown(); + QWidget::closeEvent(event); +} + +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; +} + +static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin", + "3dsx", "app", "elf", "axf"}; + +static bool IsCorrectFileExtension(const QMimeData* mime) { + const QString& filename = mime->urls().at(0).toLocalFile(); + return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), + QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); +} + +static bool IsAcceptableDropEvent(QDropEvent* event) { + return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); +} + +void GMainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsAcceptableDropEvent(event)) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool GMainWindow::DropAction(QDropEvent* event) { + if (!IsAcceptableDropEvent(event)) { + return false; + } + + const QMimeData* mime_data = event->mimeData(); + const QString& filename = mime_data->urls().at(0).toLocalFile(); + + if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { + // Amiibo + LoadAmiibo(filename); + } else { + // Game + if (ConfirmChangeGame()) { + BootGame(filename); + } + } + return true; +} + +void GMainWindow::OnFileOpen(const QFileOpenEvent* event) { + BootGame(event->file()); +} + +void GMainWindow::dropEvent(QDropEvent* event) { + DropAction(event); +} + +void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { + AcceptDropEvent(event); +} + +void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { + AcceptDropEvent(event); +} + +bool GMainWindow::ConfirmChangeGame() { + if (!emu_thread) [[unlikely]] { + return true; + } + + auto answer = QMessageBox::question( + this, tr("Lucina3DS"), tr("The game is still running. Would you like to stop emulation?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::filterBarSetChecked(bool state) { + ui->action_Show_Filter_Bar->setChecked(state); + emit(OnToggleFilterBar()); +} + +void GMainWindow::UpdateUITheme() { + const QString icons_base_path = QStringLiteral(":/icons/"); + const QString default_theme = QStringLiteral("default"); + const QString default_theme_path = icons_base_path + default_theme; + + const QString& current_theme = UISettings::values.theme; + const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second); + QStringList theme_paths(default_theme_paths); + + if (is_default_theme || current_theme.isEmpty()) { + const QString theme_uri(QStringLiteral(":default/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, + "Unable to open default stylesheet, falling back to empty stylesheet"); + qApp->setStyleSheet({}); + setStyleSheet({}); + } + theme_paths.append(default_theme_path); + QIcon::setThemeName(default_theme); + } else { + const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found"); + } + + const QString current_theme_path = icons_base_path + current_theme; + theme_paths.append({default_theme_path, current_theme_path}); + QIcon::setThemeName(current_theme); + } + + QIcon::setThemeSearchPaths(theme_paths); +} + +void GMainWindow::LoadTranslation() { + // If the selected language is English, no need to install any translation + if (UISettings::values.language == QStringLiteral("en")) { + return; + } + + bool loaded; + + if (UISettings::values.language.isEmpty()) { + // Use the system's default locale + loaded = translator.load(QLocale::system(), {}, {}, QStringLiteral(":/languages/")); + } else { + // Otherwise load from the specified file + loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/")); + } + + if (loaded) { + qApp->installTranslator(&translator); + } else { + UISettings::values.language = QStringLiteral("en"); + } +} + +void GMainWindow::OnLanguageChanged(const QString& locale) { + if (UISettings::values.language != QStringLiteral("en")) { + qApp->removeTranslator(&translator); + } + + UISettings::values.language = locale; + LoadTranslation(); + ui->retranslateUi(this); + RetranslateStatusBar(); + UpdateWindowTitle(); +} + +void GMainWindow::OnConfigurePerGame() { + u64 title_id{}; + system.GetAppLoader().ReadProgramId(title_id); + OpenPerGameConfiguration(title_id, game_path); +} + +void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) { + Settings::SetConfiguringGlobal(false); + ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted) { + Settings::RestoreGlobalState(system.IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } + + // Do not cause the global config to write local settings into the config file + const bool is_powered_on = system.IsPoweredOn(); + Settings::RestoreGlobalState(system.IsPoweredOn()); + + if (!is_powered_on) { + config->Save(); + } + + UpdateStatusButtons(); +} + +void GMainWindow::OnMoviePlaybackCompleted() { + OnPauseGame(); + QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); +} + +void GMainWindow::UpdateWindowTitle() { + const QString full_name = QString::fromUtf8(Common::g_build_fullname); + + if (game_title.isEmpty()) { + setWindowTitle(QStringLiteral("%1").arg(full_name)); + } else { + setWindowTitle(QStringLiteral("%1 | %2").arg(full_name, game_title)); + render_window->setWindowTitle( + QStringLiteral("%1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); + secondary_window->setWindowTitle(QStringLiteral("Lucina3DS %1 | %2 | %3") + .arg(full_name, game_title, tr("Secondary Window"))); + } +} + +void GMainWindow::UpdateUISettings() { + if (!ui->action_Fullscreen->isChecked()) { + UISettings::values.geometry = saveGeometry(); + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + } + UISettings::values.state = saveState(); +#if MICROPROFILE_ENABLED + UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); + UISettings::values.microprofile_visible = microProfileDialog->isVisible(); +#endif + UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); + UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); + UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); + UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); + UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.first_start = false; +} + +void GMainWindow::SyncMenuUISettings() { + ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::Default); + ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SingleScreen); + ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::LargeScreen); + ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::HybridScreen); + ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SideScreen); + ui->action_Screen_Layout_Separate_Windows->setChecked( + Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); + ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue()); + ui->action_Screen_Layout_Upright_Screens->setChecked( + Settings::values.upright_screen.GetValue()); +} + +void GMainWindow::RetranslateStatusBar() { + if (emu_thread) + UpdateStatusBar(); + + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + multiplayer_state->retranslateUi(); +} + +void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(system); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + +#ifdef __unix__ +void GMainWindow::SetGamemodeEnabled(bool state) { + if (emulation_running) { + Common::Linux::SetGamemodeState(state); + } +} +#endif + +#ifdef main +#undef main +#endif + +static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() { +#ifdef _WIN32 + // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. + // This is done by setting the optimal scaling policy for the primary screen. + + // Create a temporary QApplication. + int temp_argc = 0; + char** temp_argv = nullptr; + QApplication temp{temp_argc, temp_argv}; + + // Get the current screen geometry. + const QScreen* primary_screen = QGuiApplication::primaryScreen(); + if (!primary_screen) { + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; + } + + const QRect screen_rect = primary_screen->geometry(); + const qreal real_ratio = primary_screen->devicePixelRatio(); + const qreal real_width = std::trunc(screen_rect.width() * real_ratio); + const qreal real_height = std::trunc(screen_rect.height() * real_ratio); + + // Recommended minimum width and height for proper window fit. + // Any screen with a lower resolution than this will still have a scale of 1. + constexpr qreal minimum_width = 1350.0; + constexpr qreal minimum_height = 900.0; + + const qreal width_ratio = std::max(1.0, real_width / minimum_width); + const qreal height_ratio = std::max(1.0, real_height / minimum_height); + + // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. + const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio)); + + if (max_ratio > real_ratio) { + return Qt::HighDpiScaleFactorRoundingPolicy::Round; + } else { + return Qt::HighDpiScaleFactorRoundingPolicy::Floor; + } +#else + // Other OSes should be better than Windows at fractional scaling. + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; +#endif +} + +int main(int argc, char* argv[]) { + Common::DetachedTasks detached_tasks; + MicroProfileOnThreadCreate("Frontend"); + SCOPE_EXIT({ MicroProfileShutdown(); }); + + // Init settings params + QCoreApplication::setOrganizationName(QStringLiteral("Citra team | Lucina3DS")); + QCoreApplication::setApplicationName(QStringLiteral("Lucina3DS")); + + auto rounding_policy = GetHighDpiRoundingPolicy(); + QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); + +#ifdef __APPLE__ + auto bundle_dir = FileUtil::GetBundleDirectory(); + if (bundle_dir) { + FileUtil::SetCurrentDir(bundle_dir.value() + ".."); + } +#endif + +#ifdef ENABLE_OPENGL + QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); +#endif + + QApplication app(argc, argv); + + // Qt changes the locale and causes issues in float conversion using std::to_string() when + // generating shaders + setlocale(LC_ALL, "C"); + + auto& system{Core::System::GetInstance()}; + + // Register Qt image interface + system.RegisterImageInterface(std::make_shared()); + + GMainWindow main_window(system); + + // Register frontend applets + Frontend::RegisterDefaultApplets(system); + + system.RegisterMiiSelector(std::make_shared(main_window)); + system.RegisterSoftwareKeyboard(std::make_shared(main_window)); + +#ifdef __APPLE__ + // Register microphone permission check. + system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); +#endif + + main_window.show(); + + QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, + &GMainWindow::OnAppFocusStateChanged); + + int result = app.exec(); + detached_tasks.WaitForAllTasks(); + return result; +} diff --git a/.history/src/lucina3ds_qt/main_20250209233407.cpp b/.history/src/lucina3ds_qt/main_20250209233407.cpp new file mode 100644 index 00000000..68f1b22e --- /dev/null +++ b/.history/src/lucina3ds_qt/main_20250209233407.cpp @@ -0,0 +1,3345 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include // for chdir +#endif +#ifdef _WIN32 +#include +#endif +#ifdef __unix__ +#include +#include +#include +#include "common/linux/gamemode.h" +#endif +#include "lucina3ds_qt/aboutdialog.h" +#include "lucina3ds_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/swkbd.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/compatdb.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/discord.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/loading_screen.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/qt_image_interface.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/graphics_device_info.h" +#include "common/arch.h" +#include "common/common_paths.h" +#include "common/detached_tasks.h" +#include "common/dynamic_library/dynamic_library.h" +#include "common/file_util.h" +#include "common/literals.h" +#include "common/logging/backend.h" +#include "common/logging/log.h" +#include "common/memory_detect.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#if LUCINA3DS_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/settings.h" +#include "core/core.h" +#include "core/dumping/backend.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_source_sd_savedata.h" +#include "core/frontend/applets/default_applets.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/nfc/nfc.h" +#include "core/loader/loader.h" +#include "core/movie.h" +#include "core/savestate.h" +#include "core/system_titles.h" +#include "input_common/main.h" +#include "network/network_settings.h" +#include "ui_main.h" +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" + +#ifdef __APPLE__ +#include "common/apple_authorization.h" +#endif + +#ifdef USE_DISCORD_PRESENCE +#include "lucina3ds_qt/discord_impl.h" +#endif + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); +#endif + +#ifdef _WIN32 +extern "C" { +// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +} +#endif + +#ifdef HAVE_SDL2 +#include +#endif + +constexpr int default_mouse_timeout = 2500; + +/** + * "Callouts" are one-time instructional messages shown to the user. In the config settings, there + * is a bitfield "callout_flags" options, used to track if a message has already been shown to the + * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. + */ + +const int GMainWindow::max_recent_files_item; + +static QString PrettyProductName() { +#ifdef _WIN32 + // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 + // With that notation change they changed the registry key used to denote the current version + QSettings windows_registry( + QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), + QSettings::NativeFormat); + const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); + if (release_id == QStringLiteral("2009")) { + const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); + const QString display_version = + windows_registry.value(QStringLiteral("DisplayVersion")).toString(); + const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); + const u32 version = current_build >= 22000 ? 11 : 10; + return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") + .arg(QString::number(version), display_version, QString::number(current_build), + QString::number(ubr)); + } +#endif + return QSysInfo::prettyProductName(); +} + +GMainWindow::GMainWindow(Core::System& system_) + : ui{std::make_unique()}, system{system_}, movie{system.Movie()}, + config{std::make_unique()}, emu_thread{nullptr} { + Common::Log::Initialize(); + Common::Log::Start(); + + Debugger::ToggleConsole(); + +#ifdef __unix__ + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); +#endif + + // register types to use in slots and signals + qRegisterMetaType("std::size_t"); + qRegisterMetaType("Service::AM::InstallStatus"); + + // Register CameraFactory + qt_cameras = std::make_shared(); + Camera::RegisterFactory("image", std::make_unique()); + Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + + LoadTranslation(); + + Pica::g_debug_context = Pica::DebugContext::Construct(); + setAcceptDrops(true); + ui->setupUi(this); + statusBar()->hide(); + + default_theme_paths = QIcon::themeSearchPaths(); + UpdateUITheme(); + + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(); + + Network::Init(); + + movie.SetPlaybackCompletionCallback([this] { + QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); + }); + + InitializeWidgets(); + InitializeDebugWidgets(); + InitializeRecentFileMenuActions(); + InitializeSaveStateMenuActions(); + InitializeHotkeys(); +#if ENABLE_QT_UPDATER + ShowUpdaterWidgets(); +#else + ui->action_Check_For_Updates->setVisible(false); + ui->action_Open_Maintenance_Tool->setVisible(false); +#endif + + SetDefaultUIGeometry(); + RestoreUIState(); + + ConnectAppEvents(); + ConnectMenuEvents(); + ConnectWidgetEvents(); + + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + Common::g_scm_desc); +#if LUCINA3DS_ARCH(x86_64) + const auto& caps = Common::GetCPUCaps(); + std::string cpu_string = caps.cpu_string; + if (caps.avx || caps.avx2 || caps.avx512) { + cpu_string += " | AVX"; + if (caps.avx512) { + cpu_string += "512"; + } else if (caps.avx2) { + cpu_string += '2'; + } + if (caps.fma || caps.fma4) { + cpu_string += " | FMA"; + } + } + LOG_INFO(Frontend, "Host CPU: {}", cpu_string); +#endif + LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); + const auto& mem_info = Common::GetMemInfo(); + using namespace Common::Literals; + LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB}); + LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB}); + UpdateWindowTitle(); + + show(); + + game_list->LoadCompatibilityList(); + game_list->PopulateAsync(UISettings::values.game_dirs); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); + +#ifdef ENABLE_OPENGL + gl_renderer = GetOpenGLRenderer(); +#if defined(_WIN32) + if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { + // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ + // We can override the version here to allow Citra to work. + // TODO: Remove this when OpenGL 4.0+ is advertised. + qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); + } +#endif +#endif + +#ifdef ENABLE_VULKAN + physical_devices = GetVulkanPhysicalDevices(); + if (physical_devices.empty()) { + QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), + tr("Vulkan initialization failed during boot.
" + "Your GPU may not support Vulkan 1.1, or you do not " + "have the latest graphics driver.")); + } +#endif + +#if ENABLE_QT_UPDATER + if (UISettings::values.check_for_update_on_start) { + CheckForUpdates(); + } +#endif + + QStringList args = QApplication::arguments(); + if (args.size() < 2) { + return; + } + + QString game_path; + for (int i = 1; i < args.size(); ++i) { + // Preserves drag/drop functionality + if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { + game_path = args[1]; + break; + } + + // Launch game in fullscreen mode + if (args[i] == QStringLiteral("-f")) { + ui->action_Fullscreen->setChecked(true); + continue; + } + + // Launch game in windowed mode + if (args[i] == QStringLiteral("-w")) { + ui->action_Fullscreen->setChecked(false); + continue; + } + + // Launch game at path + if (args[i] == QStringLiteral("-g")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + game_path = args[++i]; + } + } + + if (!game_path.isEmpty()) { + BootGame(game_path); + } +} + +GMainWindow::~GMainWindow() { + // Will get automatically deleted otherwise + if (!render_window->parent()) { + delete render_window; + } + + Pica::g_debug_context.reset(); + Network::Shutdown(); +} + +void GMainWindow::InitializeWidgets() { +#ifdef LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING + ui->action_Report_Compatibility->setVisible(true); +#endif + render_window = new GRenderWindow(this, emu_thread.get(), system, false); + secondary_window = new GRenderWindow(this, emu_thread.get(), system, true); + render_window->hide(); + secondary_window->hide(); + secondary_window->setParent(nullptr); + + game_list = new GameList(this); + ui->horizontalLayout->addWidget(game_list); + + game_list_placeholder = new GameListPlaceholder(this); + ui->horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui->horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, this, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + render_window->activateWindow(); + } + }); + + InputCommon::Init(); + multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(), + ui->action_Leave_Room, ui->action_Show_Room); + multiplayer_state->setVisible(false); + +#if ENABLE_QT_UPDATER + // Setup updater + updater = new Updater(this); + UISettings::values.updater_found = updater->HasUpdater(); +#endif + + UpdateBootHomeMenuState(); + + // Create status bar + message_label = new QLabel(); + // Configured separately for left alignment + message_label->setFrameStyle(QFrame::NoFrame); + message_label->setContentsMargins(4, 0, 4, 0); + message_label->setAlignment(Qt::AlignLeft); + statusBar()->addPermanentWidget(message_label, 1); + + progress_bar = new QProgressBar(); + progress_bar->hide(); + statusBar()->addPermanentWidget(progress_bar); + + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + + emu_speed_label = new QLabel(); + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label = new QLabel(); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label = new QLabel(); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { + label->setVisible(false); + label->setFrameStyle(QFrame::NoFrame); + label->setContentsMargins(4, 0, 4, 0); + statusBar()->addPermanentWidget(label); + } + + // Setup Graphics API button + graphics_api_button = new QPushButton(); + graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton")); + graphics_api_button->setFocusPolicy(Qt::NoFocus); + UpdateAPIIndicator(); + + connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); }); + + statusBar()->insertPermanentWidget(0, graphics_api_button); + + volume_popup = new QWidget(this); + volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); + volume_popup->setLayout(new QVBoxLayout()); + volume_popup->setMinimumWidth(200); + + volume_slider = new QSlider(Qt::Horizontal); + volume_slider->setObjectName(QStringLiteral("volume_slider")); + volume_slider->setMaximum(100); + volume_slider->setPageStep(5); + connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { + Settings::values.audio_muted = false; + const auto value = static_cast(percentage) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); + }); + volume_popup->layout()->addWidget(volume_slider); + + volume_button = new QPushButton(); + volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + volume_button->setFocusPolicy(Qt::NoFocus); + volume_button->setCheckable(true); + UpdateVolumeUI(); + connect(volume_button, &QPushButton::clicked, this, [&] { + UpdateVolumeUI(); + volume_popup->setVisible(!volume_popup->isVisible()); + QRect rect = volume_button->geometry(); + QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); + bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); + volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); + }); + statusBar()->insertPermanentWidget(1, volume_button); + + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + + statusBar()->setVisible(true); + + // Removes an ugly inner border from the status bar widgets under Linux + setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); + + QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows); +} + +void GMainWindow::InitializeDebugWidgets() { + connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this, + &GMainWindow::OnCreateGraphicsSurfaceViewer); + + QMenu* debug_menu = ui->menu_View_Debugging; + +#if MICROPROFILE_ENABLED + microProfileDialog = new MicroProfileDialog(this); + microProfileDialog->hide(); + debug_menu->addAction(microProfileDialog->toggleViewAction()); +#endif + + registersWidget = new RegistersWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, registersWidget); + registersWidget->hide(); + debug_menu->addAction(registersWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, registersWidget, + &RegistersWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, registersWidget, + &RegistersWidget::OnEmulationStopping); + + graphicsWidget = new GPUCommandStreamWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsWidget); + graphicsWidget->hide(); + debug_menu->addAction(graphicsWidget->toggleViewAction()); + + graphicsCommandsWidget = new GPUCommandListWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); + graphicsCommandsWidget->hide(); + debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); + + graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); + graphicsBreakpointsWidget->hide(); + debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); + + graphicsVertexShaderWidget = + new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget); + graphicsVertexShaderWidget->hide(); + debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction()); + + graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget); + graphicsTracingWidget->hide(); + debug_menu->addAction(graphicsTracingWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStopping); + + waitTreeWidget = new WaitTreeWidget(system, this); + addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget); + waitTreeWidget->hide(); + debug_menu->addAction(waitTreeWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, waitTreeWidget, + &WaitTreeWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + &WaitTreeWidget::OnEmulationStopping); + + lleServiceModulesWidget = new LLEServiceModulesWidget(this); + addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget); + lleServiceModulesWidget->hide(); + debug_menu->addAction(lleServiceModulesWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, + [this] { lleServiceModulesWidget->setDisabled(true); }); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + [this] { lleServiceModulesWidget->setDisabled(false); }); + + ipcRecorderWidget = new IPCRecorderWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); + ipcRecorderWidget->hide(); + debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, + &IPCRecorderWidget::OnEmulationStarting); +} + +void GMainWindow::InitializeRecentFileMenuActions() { + for (int i = 0; i < max_recent_files_item; ++i) { + actions_recent_files[i] = new QAction(this); + actions_recent_files[i]->setVisible(false); + connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); + + ui->menu_recent_files->addAction(actions_recent_files[i]); + } + ui->menu_recent_files->addSeparator(); + QAction* action_clear_recent_files = new QAction(this); + action_clear_recent_files->setText(tr("Clear Recent Files")); + connect(action_clear_recent_files, &QAction::triggered, this, [this] { + UISettings::values.recent_files.clear(); + UpdateRecentFiles(); + }); + ui->menu_recent_files->addAction(action_clear_recent_files); + + UpdateRecentFiles(); +} + +void GMainWindow::InitializeSaveStateMenuActions() { + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i] = new QAction(this); + actions_load_state[i]->setData(i + 1); + connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); + ui->menu_Load_State->addAction(actions_load_state[i]); + + actions_save_state[i] = new QAction(this); + actions_save_state[i]->setData(i + 1); + connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); + ui->menu_Save_State->addAction(actions_save_state[i]); + } + + connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + if (newest_slot != 0) { + actions_load_state[newest_slot - 1]->trigger(); + } + }); + connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + actions_save_state[oldest_slot - 1]->trigger(); + }); + + connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + + UpdateSaveStates(); +} + +void GMainWindow::InitializeHotkeys() { + hotkey_registry.LoadHotkeys(); + + const QString main_window = QStringLiteral("Main Window"); + const QString fullscreen = QStringLiteral("Fullscreen"); + const QString toggle_screen_layout = QStringLiteral("Toggle Screen Layout"); + const QString swap_screens = QStringLiteral("Swap Screens"); + const QString rotate_screens = QStringLiteral("Rotate Screens Upright"); + + const auto link_action_shortcut = [&](QAction* action, const QString& action_name) { + static const QString main_window = QStringLiteral("Main Window"); + action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); + action->setShortcutContext(hotkey_registry.GetShortcutContext(main_window, action_name)); + action->setAutoRepeat(false); + this->addAction(action); + }; + + link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); + link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); + link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); + link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lucina3DS")); + link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); + link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); + link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); + link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); + link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + link_action_shortcut(ui->action_Fullscreen, fullscreen); + link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, swap_screens); + link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, rotate_screens); + link_action_shortcut(ui->action_Enable_Frame_Advancing, + QStringLiteral("Toggle Frame Advancing")); + link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame")); + link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); + link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); + link_action_shortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); + link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); + link_action_shortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); + link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); + link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); + + const auto add_secondary_window_hotkey = [this](QKeySequence hotkey, const char* slot) { + // This action will fire specifically when secondary_window is in focus + QAction* secondary_window_action = new QAction(secondary_window); + secondary_window_action->setShortcut(hotkey); + + connect(secondary_window_action, SIGNAL(triggered()), this, slot); + secondary_window->addAction(secondary_window_action); + }; + + // Use the same fullscreen hotkey as the main window + const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen); + add_secondary_window_hotkey(fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen())); + + const auto toggle_screen_hotkey = + hotkey_registry.GetKeySequence(main_window, toggle_screen_layout); + add_secondary_window_hotkey(toggle_screen_hotkey, SLOT(ToggleScreenLayout())); + + const auto swap_screen_hotkey = hotkey_registry.GetKeySequence(main_window, swap_screens); + add_secondary_window_hotkey(swap_screen_hotkey, SLOT(TriggerSwapScreens())); + + const auto rotate_screen_hotkey = hotkey_registry.GetKeySequence(main_window, rotate_screens); + add_secondary_window_hotkey(rotate_screen_hotkey, SLOT(TriggerRotateScreens())); + + const auto connect_shortcut = [&](const QString& action_name, const auto& function) { + const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this); + connect(hotkey, &QShortcut::activated, this, function); + }; + + connect(hotkey_registry.GetHotkey(main_window, toggle_screen_layout, render_window), + &QShortcut::activated, this, &GMainWindow::ToggleScreenLayout); + + connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { + if (emulation_running) { + ui->action_Fullscreen->setChecked(false); + ToggleFullscreen(); + } + }); + connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] { + Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Toggle Texture Dumping"), + [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + connect_shortcut(QStringLiteral("Toggle Custom Textures"), + [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes + // the variable hold a garbage value after this function exits + static constexpr u16 SPEED_LIMIT_STEP = 5; + connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + return; + } + if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() + + SPEED_LIMIT_STEP); + } else { + Settings::values.frame_limit = 0; + } + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + Settings::values.frame_limit = 995; + } else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() - + SPEED_LIMIT_STEP); + UpdateStatusBar(); + } + UpdateStatusBar(); + }); + + connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); + connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); + connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); + + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the + // variable hold a garbage value after this function exits + static constexpr u16 FACTOR_3D_STEP = 5; + connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d > 0) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); + connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d < 100) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = + factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); +} + +void GMainWindow::SetDefaultUIGeometry() { + // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half + const QRect screenRect = screen()->geometry(); + + const int w = screenRect.width() * 2 / 3; + const int h = screenRect.height() / 2; + const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; + const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100; + + setGeometry(x, y, w, h); +} + +void GMainWindow::RestoreUIState() { + restoreGeometry(UISettings::values.geometry); + restoreState(UISettings::values.state); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); +#if MICROPROFILE_ENABLED + microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); + microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); +#endif + + game_list->LoadInterfaceLayout(); + + ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); + ToggleWindowMode(); + + ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); + SyncMenuUISettings(); + + ui->action_Display_Dock_Widget_Headers->setChecked( + UISettings::values.display_titlebar.GetValue()); + OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked()); + + ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + + ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); +} + +void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { + if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && + state != Qt::ApplicationActive) { + LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); + } + if (!emulation_running) { + return; + } + if (UISettings::values.pause_when_in_background) { + if (emu_thread->IsRunning() && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + auto_paused = true; + OnPauseGame(); + } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { + auto_paused = false; + OnStartGame(); + } + } + if (UISettings::values.mute_when_in_background) { + if (!Settings::values.audio_muted && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + Settings::values.audio_muted = true; + auto_muted = true; + } else if (auto_muted && state == Qt::ApplicationActive) { + Settings::values.audio_muted = false; + auto_muted = false; + } + UpdateVolumeUI(); + } +} + +bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::FileOpen) { + emit FileOpen(static_cast(event)); + return true; + } + return false; +} + +void GMainWindow::ConnectAppEvents() { + const auto filter = new GApplicationEventFilter(); + QGuiApplication::instance()->installEventFilter(filter); + + connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen); +} + +void GMainWindow::ConnectWidgetEvents() { + connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); + connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, + &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); + connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &GMainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, this, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); + + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, + &GMainWindow::OnGameListOpenPerGameProperties); + + connect(this, &GMainWindow::EmulationStarting, render_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, render_window, + &GRenderWindow::OnEmulationStopping); + connect(this, &GMainWindow::EmulationStarting, secondary_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, secondary_window, + &GRenderWindow::OnEmulationStopping); + + connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); + connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); + connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); +} + +void GMainWindow::ConnectMenuEvents() { + const auto connect_menu = [&](QAction* action, const auto& event_fn) { + connect(action, &QAction::triggered, this, event_fn); + // Add actions to this window so that hiding menus in fullscreen won't disable them + addAction(action); + // Add actions to the render window so that they work outside of single window mode + render_window->addAction(action); + }; + + // File + connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); + connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), + [this, region] { OnMenuBootHomeMenu(region); }); + } + connect_menu(ui->action_Exit, &QMainWindow::close); + connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); + connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo); + + // Emulation + connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); + connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); + connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); }); + connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); + connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); + connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); + + // View + connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); + connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); + connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); + connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); + + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + + connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); + connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens); + connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens); + + // Movie + connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie); + connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie); + connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie); + connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie); + connect_menu(ui->action_Movie_Read_Only_Mode, + [this](bool checked) { movie.SetReadOnly(checked); }); + connect_menu(ui->action_Enable_Frame_Advancing, [this] { + if (emulation_running) { + system.frame_limiter.SetFrameAdvancing(ui->action_Enable_Frame_Advancing->isChecked()); + ui->action_Advance_Frame->setEnabled(ui->action_Enable_Frame_Advancing->isChecked()); + } + }); + connect_menu(ui->action_Advance_Frame, [this] { + if (emulation_running && system.frame_limiter.IsFrameAdvancing()) { + ui->action_Enable_Frame_Advancing->setChecked(true); + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.AdvanceFrame(); + } + }); + connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); + connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); + + // Help + connect_menu(ui->action_Open_Lucina3DS_Folder, &GMainWindow::OnOpenLucina3DSFolder); + connect_menu(ui->action_Open_Log_Folder, []() { + QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + connect_menu(ui->action_FAQ, []() { + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); + }); + connect_menu(ui->action_About, &GMainWindow::OnMenuAboutLucina3DS); + +#if ENABLE_QT_UPDATER + connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); + connect_menu(ui->action_Open_Maintenance_Tool, &GMainWindow::OnOpenUpdater); +#endif +} + +void GMainWindow::UpdateMenuState() { + const bool is_paused = !emu_thread || !emu_thread->IsRunning(); + + const std::array running_actions{ + ui->action_Stop, + ui->action_Restart, + ui->action_Configure_Current_Game, + ui->action_Report_Compatibility, + ui->action_Load_Amiibo, + ui->action_Remove_Amiibo, + ui->action_Pause, + ui->action_Advance_Frame, + }; + + for (QAction* action : running_actions) { + action->setEnabled(emulation_running); + } + + ui->action_Capture_Screenshot->setEnabled(emulation_running); + + if (emulation_running && is_paused) { + ui->action_Pause->setText(tr("&Continue")); + } else { + ui->action_Pause->setText(tr("&Pause")); + } +} + +void GMainWindow::OnDisplayTitleBars(bool show) { + QList widgets = findChildren(); + + if (show) { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(nullptr); + if (old) { + delete old; + } + } + } else { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(new QWidget()); + if (old) { + delete old; + } + } + } +} + +#if ENABLE_QT_UPDATER +void GMainWindow::OnCheckForUpdates() { + explicit_update_check = true; + CheckForUpdates(); +} + +void GMainWindow::CheckForUpdates() { + if (updater->CheckForUpdates()) { + LOG_INFO(Frontend, "Update check started"); + } else { + LOG_WARNING(Frontend, "Unable to start check for updates"); + } +} + +void GMainWindow::OnUpdateFound(bool found, bool error) { + if (error) { + LOG_WARNING(Frontend, "Update check failed"); + return; + } + + if (!found) { + LOG_INFO(Frontend, "No updates found"); + + // If the user explicitly clicked the "Check for Updates" button, we are + // going to want to show them a prompt anyway. + if (explicit_update_check) { + explicit_update_check = false; + ShowNoUpdatePrompt(); + } + return; + } + + if (emulation_running && !explicit_update_check) { + LOG_INFO(Frontend, "Update found, deferring as game is running"); + defer_update_prompt = true; + return; + } + + LOG_INFO(Frontend, "Update found!"); + explicit_update_check = false; + + ShowUpdatePrompt(); +} + +void GMainWindow::ShowUpdatePrompt() { + defer_update_prompt = false; + + auto result = + QMessageBox::question(this, tr("Update Available"), + tr("An update is available. Would you like to install it now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + updater->LaunchUIOnExit(); + close(); + } +} + +void GMainWindow::ShowNoUpdatePrompt() { + QMessageBox::information(this, tr("No Update Found"), tr("No update is found."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void GMainWindow::OnOpenUpdater() { + updater->LaunchUI(); +} + +void GMainWindow::ShowUpdaterWidgets() { + ui->action_Check_For_Updates->setVisible(UISettings::values.updater_found); + ui->action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found); + + connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound); +} +#endif + +#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) +static std::optional HoldWakeLockLinux(u32 window_id = 0) { + if (!QDBusConnection::sessionBus().isConnected()) { + return {}; + } + // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit + QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Inhibit")); + if (!xdp.isValid()) { + LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); + return {}; + } + QVariantMap options = {}; + //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the + //: computer from sleeping + options.insert(QString::fromLatin1("reason"), + QCoreApplication::translate("GMainWindow", "Luinca3DS is running a game")); + // 0x4: Suspend lock; 0x8: Idle lock + QDBusReply reply = + xdp.call(QString::fromLatin1("Inhibit"), + QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); + + if (reply.isValid()) { + return reply.value(); + } + LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", + reply.error().message().toStdString()); + return {}; +} + +static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { + if (!QDBusConnection::sessionBus().isConnected()) { + return; + } + QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), + QString::fromLatin1("org.freedesktop.portal.Request")); + unlocker.call(QString::fromLatin1("Close")); +} +#endif // __unix__ + +void GMainWindow::PreventOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + auto reply = HoldWakeLockLinux(winId()); + if (reply) { + wake_lock = std::move(reply.value()); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +void GMainWindow::AllowOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(HAVE_SDL2) + SDL_EnableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + if (!wake_lock.path().isEmpty()) { + ReleaseWakeLockLinux(wake_lock); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +bool GMainWindow::LoadROM(const QString& filename) { + // Shutdown previous session if the emu thread is still active... + if (emu_thread) { + ShutdownGame(); + } + + if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) { + LOG_CRITICAL(Frontend, "Failed to initialize render targets!"); + return false; + } + + const auto scope = render_window->Acquire(); + + const Core::System::ResultStatus result{ + system.Load(*render_window, filename.toStdString(), secondary_window)}; + + if (result != Core::System::ResultStatus::Success) { + switch (result) { + case Core::System::ResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorSystemMode: + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + QMessageBox::critical( + this, tr("ROM Corrupted"), + tr("Your ROM is corrupted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { + QMessageBox::critical( + this, tr("ROM Encrypted"), + tr("Your ROM is encrypted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + } + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + QMessageBox::critical(this, tr("Unsupported ROM"), + tr("GBA Virtual Console ROMs are not supported by Lucina3DS.")); + break; + + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr(fmt::format( + "An error has occurred whilst communicating with the Artic Base Server.\n{}", + system.GetStatusDetails()) + .c_str())); + break; + default: + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + break; + } + return false; + } + + std::string title; + system.GetAppLoader().ReadTitle(title); + game_title = QString::fromStdString(title); + UpdateWindowTitle(); + + game_path = filename; + + return true; +} + +void GMainWindow::BootGame(const QString& filename) { + if (emu_thread) { + ShutdownGame(); + } + + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { + const auto answer = QMessageBox::question( + this, tr("CIA must be installed before usage"), + tr("Before using this CIA, you must install it. Do you want to install it now?"), + QMessageBox::Yes | QMessageBox::No); + + if (answer == QMessageBox::Yes) + InstallCIA(QStringList(filename)); + + return; + } + + show_artic_label = is_artic; + + LOG_INFO(Frontend, "Lucinca3DS starting..."); + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } + + if (movie_record_on_start) { + movie.PrepareForRecording(); + } + if (movie_playback_on_start) { + movie.PrepareForPlayback(movie_playback_path.toStdString()); + } + + const std::string path = filename.toStdString(); + auto loader = Loader::GetLoader(path); + + u64 title_id{0}; + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } + + system.ApplySettings(); + + Settings::LogSettings(); + + // Save configurations + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + config->Save(); + + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); + return; + } + + // Set everything up + if (movie_record_on_start) { + movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (movie_playback_on_start) { + movie.StartPlayback(movie_playback_path.toStdString()); + movie_playback_on_start = false; + movie_playback_path.clear(); + } + + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + + if (video_dumping_on_start) { + StartVideoDumping(video_dumping_path); + video_dumping_on_start = false; + video_dumping_path.clear(); + } + + // Register debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Register(); + } + + // Create and start the emulation thread + emu_thread = std::make_unique(system, *render_window); + emit EmulationStarting(emu_thread.get()); + emu_thread->start(); + + connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + + // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views + // before the CPU continues + connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget, + &RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget, + &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget, + &RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, + &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, + &LoadingScreen::OnLoadComplete); + + // Update the GUI + registersWidget->OnDebugModeEntered(); + if (ui->action_Single_Window_Mode->isChecked()) { + game_list->hide(); + game_list_placeholder->hide(); + } + status_bar_update_timer.start(1000); + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + } + + loading_screen->Prepare(system.GetAppLoader()); + loading_screen->show(); + + emulation_running = true; + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } + + OnStartGame(); +} + +void GMainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + if (ui->action_Fullscreen->isChecked()) { + HideFullscreen(); + } + + auto video_dumper = system.GetVideoDumper(); + if (video_dumper && video_dumper->IsDumping()) { + game_shutdown_delayed = true; + OnStopVideoDumping(); + return; + } + + AllowOSSleep(); + + discord_rpc->Pause(); + emu_thread->RequestStop(); + + // Release emu threads from any breakpoints + // This belongs after RequestStop() and before wait() because if emulation stops on a GPU + // breakpoint after (or before) RequestStop() is called, the emulation would never be able + // to continue out to the main loop and terminate. Thus wait() would hang forever. + // TODO(bunnei): This function is not thread safe, but it's being used as if it were + Pica::g_debug_context->ClearBreakpoints(); + + // Unregister debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Unregister(); + } + + // Frame advancing must be cancelled in order to release the emu thread from waiting + system.frame_limiter.SetFrameAdvancing(false); + + emit EmulationStopping(); + + // Wait for emulation thread to complete and delete it + emu_thread->wait(); + emu_thread = nullptr; + + OnCloseMovie(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif + + // The emulation is stopped, so closing the window or not does not matter anymore + disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + + render_window->hide(); + secondary_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); + + if (game_list->IsEmpty()) { + game_list_placeholder->show(); + } else { + game_list->show(); + } + game_list->SetFilterFocus(); + + setMouseTracking(false); + + // Disable status bar updates + status_bar_update_timer.stop(); + message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); + emu_speed_label->setVisible(false); + game_fps_label->setVisible(false); + emu_frametime_label->setVisible(false); + + UpdateSaveStates(); + + emulation_running = false; + +#if ENABLE_QT_UDPATER + if (defer_update_prompt) { + ShowUpdatePrompt(); + } +#endif + + game_title.clear(); + UpdateWindowTitle(); + + game_path.clear(); + + // Update the GUI + UpdateMenuState(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); +} + +void GMainWindow::StoreRecentFile(const QString& filename) { + UISettings::values.recent_files.prepend(filename); + UISettings::values.recent_files.removeDuplicates(); + while (UISettings::values.recent_files.size() > max_recent_files_item) { + UISettings::values.recent_files.removeLast(); + } + + UpdateRecentFiles(); +} + +void GMainWindow::UpdateRecentFiles() { + const int num_recent_files = + std::min(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); + + for (int i = 0; i < num_recent_files; i++) { + const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( + QFileInfo(UISettings::values.recent_files[i]).fileName()); + actions_recent_files[i]->setText(text); + actions_recent_files[i]->setData(UISettings::values.recent_files[i]); + actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); + actions_recent_files[i]->setVisible(true); + } + + for (int j = num_recent_files; j < max_recent_files_item; ++j) { + actions_recent_files[j]->setVisible(false); + } + + // Enable the recent files menu if the list isn't empty + ui->menu_recent_files->setEnabled(num_recent_files != 0); +} + +void GMainWindow::UpdateSaveStates() { + if (!system.IsPoweredOn()) { + ui->menu_Load_State->setEnabled(false); + ui->menu_Save_State->setEnabled(false); + return; + } + + ui->menu_Load_State->setEnabled(true); + ui->menu_Save_State->setEnabled(true); + ui->action_Load_from_Newest_Slot->setEnabled(false); + + oldest_slot = newest_slot = 0; + oldest_slot_time = std::numeric_limits::max(); + newest_slot_time = 0; + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return; + } + auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID()); + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i]->setEnabled(false); + actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); + actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); + } + for (const auto& savestate : savestates) { + const bool display_name = + savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch && + !savestate.build_name.empty(); + const auto text = + tr("Slot %1 - %2 %3") + .arg(savestate.slot) + .arg(QDateTime::fromSecsSinceEpoch(savestate.time) + .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))) + .arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String()) + .trimmed(); + + actions_load_state[savestate.slot - 1]->setEnabled(true); + actions_load_state[savestate.slot - 1]->setText(text); + actions_save_state[savestate.slot - 1]->setText(text); + + ui->action_Load_from_Newest_Slot->setEnabled(true); + + if (savestate.time > newest_slot_time) { + newest_slot = savestate.slot; + newest_slot_time = savestate.time; + } + if (savestate.time < oldest_slot_time) { + oldest_slot = savestate.slot; + oldest_slot_time = savestate.time; + } + } + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + if (!actions_load_state[i]->isEnabled()) { + // Prefer empty slot + oldest_slot = i + 1; + oldest_slot_time = 0; + break; + } + } +} + +void GMainWindow::OnGameListLoadFile(QString game_path) { + if (ConfirmChangeGame()) { + BootGame(game_path); + } +} + +void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { + std::string path; + std::string open_target; + + switch (target) { + case GameListOpenTarget::SAVE_DATA: { + open_target = "Save Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::EXT_DATA: { + open_target = "Extra Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::APPLICATION: { + open_target = "Application"; + auto media_type = Service::AM::GetTitleMediaType(data_id); + path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; + break; + } + case GameListOpenTarget::UPDATE_DATA: { + open_target = "Update Data"; + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + + "content/"; + break; + } + case GameListOpenTarget::TEXTURE_DUMP: { + open_target = "Dumped Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); + break; + } + case GameListOpenTarget::TEXTURE_LOAD: { + open_target = "Custom Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); + break; + } + case GameListOpenTarget::MODS: { + open_target = "Mods"; + path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + data_id); + break; + } + case GameListOpenTarget::DLC_DATA: { + open_target = "DLC Data"; + path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/0004008c/{:08x}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id); + break; + } + case GameListOpenTarget::SHADER_CACHE: { + open_target = "Shader Cache"; + path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir); + break; + } + default: + LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); + return; + } + + QString qpath = QString::fromStdString(path); + + QDir dir(qpath); + if (!dir.exists()) { + QMessageBox::critical( + this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)), + tr("Folder does not exist!")); + return; + } + + LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); + + QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); +} + +void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + QString directory; + if (it != compatibility_list.end()) + directory = it->second.second; + + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory)); +} + +void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { + auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setWindowFlags(dialog->windowFlags() & + ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); + dialog->setCancelButton(nullptr); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + const auto base_path = fmt::format( + "{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); + const auto update_path = + fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + program_id | 0x0004000e00000000); + using FutureWatcher = QFutureWatcher>; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, + [this, dialog, base_path, update_path, future_watcher] { + dialog->hide(); + const auto& [base, update] = future_watcher->result(); + if (base != Loader::ResultStatus::Success) { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not dump base RomFS.\nRefer to the log for details.")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path))); + if (update == Loader::ResultStatus::Success) { + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(update_path))); + } + }); + + auto future = QtConcurrent::run([game_path, base_path, update_path] { + std::unique_ptr loader = Loader::GetLoader(game_path.toStdString()); + return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path)); + }); + future_watcher->setFuture(future); +} + +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { + QString path; + if (directory == QStringLiteral("INSTALLED")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); + } else if (directory == QStringLiteral("SYSTEM")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000/title/00040010"); + } else { + path = directory; + } + if (!QFileInfo::exists(path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) + return; + UISettings::GameDir game_dir{dir_path, false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } +} + +void GMainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui->action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + +void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { + const auto loader = Loader::GetLoader(file.toStdString()); + + u64 title_id{}; + if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { + QMessageBox::information(this, tr("Properties"), + tr("The game properties could not be loaded.")); + return; + } + + OpenPerGameConfiguration(title_id, file); +} + +void GMainWindow::OnMenuLoadFile() { + const QString extensions = QStringLiteral("*.").append( + GameList::supported_file_extensions.join(QStringLiteral(" *."))); + const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)", + "%1 is an identifier for the 3DS executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), UISettings::values.roms_path, file_filter); + + if (filename.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + +void GMainWindow::OnMenuInstallCIA() { + QStringList filepaths = QFileDialog::getOpenFileNames( + this, tr("Load Files"), UISettings::values.roms_path, + tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)")); + + if (filepaths.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filepaths[0]).path(); + InstallCIA(filepaths); +} + +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + +void GMainWindow::OnMenuBootHomeMenu(u32 region) { + BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); +} + +void GMainWindow::InstallCIA(QStringList filepaths) { + ui->action_Install_CIA->setEnabled(false); + game_list->SetDirectoryWatcherEnabled(false); + progress_bar->show(); + progress_bar->setMaximum(INT_MAX); + + (void)QtConcurrent::run([&, filepaths] { + Service::AM::InstallStatus status; + const auto cia_progress = [&](std::size_t written, std::size_t total) { + emit UpdateProgress(written, total); + }; + for (const auto& current_path : filepaths) { + status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress); + emit CIAInstallReport(status, current_path); + } + emit CIAInstallFinished(); + }); +} + +void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) { + progress_bar->setValue( + static_cast(INT_MAX * (static_cast(written) / static_cast(total)))); +} + +void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) { + QString filename = QFileInfo(filepath).fileName(); + switch (status) { + case Service::AM::InstallStatus::Success: + this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFailedToOpenFile: + QMessageBox::critical(this, tr("Unable to open File"), + tr("Could not open %1").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorAborted: + QMessageBox::critical( + this, tr("Installation aborted"), + tr("The installation of %1 was aborted. Please see the log for more details") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorInvalid: + QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorEncrypted: + QMessageBox::critical(this, tr("Encrypted File"), + tr("%1 must be decrypted " + "before being used with Lucina3DS. A real 3DS is required.") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFileNotFound: + QMessageBox::critical(this, tr("Unable to find File"), + tr("Could not find %1").arg(filename)); + break; + } +} + +void GMainWindow::OnCIAInstallFinished() { + progress_bar->hide(); + progress_bar->setValue(0); + game_list->SetDirectoryWatcherEnabled(true); + ui->action_Install_CIA->setEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void GMainWindow::UninstallTitles( + const std::vector>& titles) { + if (titles.empty()) { + return; + } + + // Select the first title in the list as representative. + const auto first_name = std::get(titles[0]); + + QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, + static_cast(titles.size()), this); + progress.setWindowModality(Qt::WindowModal); + + QFutureWatcher future_watcher; + QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, + &QProgressDialog::reset); + QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, + &QProgressDialog::setValue); + + auto failed = false; + QString failed_name; + + const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { + const auto name = std::get(title); + const auto media_type = std::get(title); + const auto program_id = std::get(title); + + const auto result = Service::AM::UninstallProgram(media_type, program_id); + if (result.IsError()) { + LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), + result.raw); + failed = true; + failed_name = name; + future_watcher.cancel(); + } + }; + + future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); + progress.exec(); + future_watcher.waitForFinished(); + + if (failed) { + QMessageBox::critical(this, tr("Lucina3DS"), tr("Failed to uninstall '%1'.").arg(failed_name)); + } else if (!future_watcher.isCanceled()) { + QMessageBox::information(this, tr("Lucina3DS"), + tr("Successfully uninstalled '%1'.").arg(first_name)); + } +} + +void GMainWindow::OnMenuRecentFile() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + const QString filename = action->data().toString(); + if (QFileInfo::exists(filename)) { + BootGame(filename); + } else { + // Display an error message and remove the file from the list. + QMessageBox::information(this, tr("File not found"), + tr("File \"%1\" not found").arg(filename)); + + UISettings::values.recent_files.removeOne(filename); + UpdateRecentFiles(); + } +} + +void GMainWindow::OnStartGame() { + qt_cameras->ResumeCameras(); + + PreventOSSleep(); + + emu_thread->SetRunning(true); + graphics_api_button->setEnabled(false); + qRegisterMetaType("Core::System::ResultStatus"); + qRegisterMetaType("std::string"); + connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); + + UpdateMenuState(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StartGamemode(); +#endif + + UpdateSaveStates(); + UpdateStatusButtons(); +} + +void GMainWindow::OnRestartGame() { + if (!system.IsPoweredOn()) { + return; + } + // Make a copy since BootGame edits game_path + BootGame(QString(game_path)); +} + +void GMainWindow::OnPauseGame() { + emu_thread->SetRunning(false); + qt_cameras->PauseCameras(); + + UpdateMenuState(); + AllowOSSleep(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif +} + +void GMainWindow::OnPauseContinueGame() { + if (emulation_running) { + if (emu_thread->IsRunning()) { + OnPauseGame(); + } else { + OnStartGame(); + } + } +} + +void GMainWindow::OnStopGame() { + ShutdownGame(); + graphics_api_button->setEnabled(true); + Settings::RestoreGlobalState(false); + UpdateStatusButtons(); +} + +void GMainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnMenuReportCompatibility() { + if (!NetSettings::values.lucina3ds_token.empty() && !NetSettings::values.lucina3ds_username.empty()) { + CompatDB compatdb{this}; + compatdb.exec(); + } else { + QMessageBox::critical(this, tr("Missing Citra Account"), + tr("You must link your Citra account to submit test cases." + "
Go to Emulation > Configure... > Web to do so.")); + } +} + +void GMainWindow::ToggleFullscreen() { + if (!emulation_running) { + return; + } + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } else { + HideFullscreen(); + } +} + +void GMainWindow::ToggleSecondaryFullscreen() { + if (!emulation_running) { + return; + } + if (secondary_window->isFullScreen()) { + secondary_window->showNormal(); + } else { + secondary_window->showFullScreen(); + } +} + +void GMainWindow::ShowFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + UISettings::values.geometry = saveGeometry(); + ui->menubar->hide(); + statusBar()->hide(); + showFullScreen(); + } else { + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + render_window->showFullScreen(); + } +} + +void GMainWindow::HideFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->menubar->show(); + showNormal(); + restoreGeometry(UISettings::values.geometry); + } else { + render_window->showNormal(); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } +} + +void GMainWindow::ToggleWindowMode() { + if (ui->action_Single_Window_Mode->isChecked()) { + // Render in the main window... + render_window->BackupGeometry(); + ui->horizontalLayout->addWidget(render_window); + render_window->setFocusPolicy(Qt::StrongFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->setFocus(); + game_list->hide(); + } + + } else { + // Render in a separate window... + ui->horizontalLayout->removeWidget(render_window); + render_window->setParent(nullptr); + render_window->setFocusPolicy(Qt::NoFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->RestoreGeometry(); + game_list->show(); + } + } +} + +void GMainWindow::UpdateSecondaryWindowVisibility() { + if (!emulation_running) { + return; + } + if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) { + secondary_window->RestoreGeometry(); + secondary_window->show(); + } else { + secondary_window->BackupGeometry(); + secondary_window->hide(); + } +} + +void GMainWindow::ChangeScreenLayout() { + Settings::LayoutOption new_layout = Settings::LayoutOption::Default; + + if (ui->action_Screen_Layout_Default->isChecked()) { + new_layout = Settings::LayoutOption::Default; + } else if (ui->action_Screen_Layout_Single_Screen->isChecked()) { + new_layout = Settings::LayoutOption::SingleScreen; + } else if (ui->action_Screen_Layout_Large_Screen->isChecked()) { + new_layout = Settings::LayoutOption::LargeScreen; + } else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) { + new_layout = Settings::LayoutOption::HybridScreen; + } else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) { + new_layout = Settings::LayoutOption::SideScreen; + } else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) { + new_layout = Settings::LayoutOption::SeparateWindows; + } + + Settings::values.layout_option = new_layout; + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::ToggleScreenLayout() { + const Settings::LayoutOption new_layout = []() { + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::Default: + return Settings::LayoutOption::SingleScreen; + case Settings::LayoutOption::SingleScreen: + return Settings::LayoutOption::LargeScreen; + case Settings::LayoutOption::LargeScreen: + return Settings::LayoutOption::HybridScreen; + case Settings::LayoutOption::HybridScreen: + return Settings::LayoutOption::SideScreen; + case Settings::LayoutOption::SideScreen: + return Settings::LayoutOption::SeparateWindows; + case Settings::LayoutOption::SeparateWindows: + return Settings::LayoutOption::Default; + default: + LOG_ERROR(Frontend, "Unknown layout option {}", + Settings::values.layout_option.GetValue()); + return Settings::LayoutOption::Default; + } + }(); + + Settings::values.layout_option = new_layout; + SyncMenuUISettings(); + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnSwapScreens() { + Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::OnRotateScreens() { + Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::TriggerSwapScreens() { + ui->action_Screen_Layout_Swap_Screens->trigger(); +} + +void GMainWindow::TriggerRotateScreens() { + ui->action_Screen_Layout_Upright_Screens->trigger(); +} + +void GMainWindow::OnSaveState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + system.SendSignal(Core::System::Signal::Save, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); + newest_slot = action->data().toUInt(); +} + +void GMainWindow::OnLoadState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + if (UISettings::values.save_state_warning) { + QMessageBox::warning(this, tr("Savestates"), + tr("Warning: Savestates are NOT a replacement for in-game saves, " + "and are not meant to be reliable.\n\nUse at your own risk!")); + UISettings::values.save_state_warning = false; + config->Save(); + } + + system.SendSignal(Core::System::Signal::Load, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); +} + +void GMainWindow::OnConfigure() { + game_list->SetDirectoryWatcherEnabled(false); + Settings::SetConfiguringGlobal(true); + ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices, + !multiplayer_state->IsHostingPublicRoom()); + connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, + &GMainWindow::OnLanguageChanged); + auto old_theme = UISettings::values.theme; + const int old_input_profile_index = Settings::values.current_input_profile_index; + const auto old_input_profiles = Settings::values.input_profiles; + const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps; + const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); +#ifdef __unix__ + const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); +#endif + auto result = configureDialog.exec(); + game_list->SetDirectoryWatcherEnabled(true); + if (result == QDialog::Accepted) { + configureDialog.ApplyConfiguration(); + InitializeHotkeys(); + if (UISettings::values.theme != old_theme) { + UpdateUITheme(); + } + if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + } +#ifdef __unix__ + if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); + } +#endif + if (!multiplayer_state->IsHostingPublicRoom()) + multiplayer_state->UpdateCredentials(); + emit UpdateThemedIcons(); + SyncMenuUISettings(); + game_list->RefreshGameDirectory(); + config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + } + UpdateSecondaryWindowVisibility(); + UpdateBootHomeMenuState(); + UpdateStatusButtons(); + } else { + Settings::values.input_profiles = old_input_profiles; + Settings::values.touch_from_button_maps = old_touch_from_button_maps; + Settings::LoadProfile(old_input_profile_index); + } +} + +void GMainWindow::OnLoadAmiibo() { + if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { + return; + } + + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr) { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (nfc->IsTagActive()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("A tag is already in use.")); + return; + } + + if (!nfc->IsSearchingForAmiibos()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Game is not looking for amiibos.")); + return; + } + + const QString extensions{QStringLiteral("*.bin")}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); + + if (filename.isEmpty()) { + return; + } + + LoadAmiibo(filename); +} + +void GMainWindow::LoadAmiibo(const QString& filename) { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (!nfc->LoadAmiibo(filename.toStdString())) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Unable to open amiibo file \"%1\" for reading.").arg(filename)); + return; + } + + ui->action_Remove_Amiibo->setEnabled(true); +} + +void GMainWindow::OnRemoveAmiibo() { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + nfc->RemoveAmiibo(); + ui->action_Remove_Amiibo->setEnabled(false); +} + +void GMainWindow::OnOpenLucina3DSFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); +} + +void GMainWindow::OnToggleFilterBar() { + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + if (ui->action_Show_Filter_Bar->isChecked()) { + game_list->SetFilterFocus(); + } else { + game_list->ClearFilter(); + } +} + +void GMainWindow::OnCreateGraphicsSurfaceViewer() { + auto graphicsSurfaceViewerWidget = + new GraphicsSurfaceWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget); + // TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true); + graphicsSurfaceViewerWidget->show(); +} + +void GMainWindow::OnRecordMovie() { + MovieRecordDialog dialog(this, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_record_on_start = true; + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); + + if (emulation_running) { // Restart game + BootGame(QString(game_path)); + } + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(true); +} + +void GMainWindow::OnPlayMovie() { + MoviePlayDialog dialog(this, game_list, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_playback_on_start = true; + movie_playback_path = dialog.GetMoviePath(); + BootGame(dialog.GetGamePath()); + + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnCloseMovie() { + if (movie_record_on_start) { + QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } else { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording; + movie.Shutdown(); + if (was_recording) { + QMessageBox::information(this, tr("Movie Saved"), + tr("The movie is successfully saved.")); + } + + if (was_running) { + OnStartGame(); + } + } + + ui->action_Close_Movie->setEnabled(false); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnSaveMovie() { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) { + movie.SaveMovie(); + QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); + } else { + LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); + } + + if (was_running) { + OnStartGame(); + } +} + +void GMainWindow::OnCaptureScreenshot() { + if (!emu_thread) [[unlikely]] { + return; + } + + const bool was_running = emu_thread->IsRunning(); + + if (was_running || + (QMessageBox::question( + this, tr("Game will unpause"), + tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running) { + OnPauseGame(); + } + std::string path = UISettings::values.screenshot_path.GetValue(); + if (!FileUtil::IsDirectory(path)) { + if (!FileUtil::CreateFullPath(path)) { + QMessageBox::information( + this, tr("Invalid Screenshot Directory"), + tr("Cannot create specified screenshot directory. Screenshot " + "path is set back to its default value.")); + path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + path.append("screenshots/"); + UISettings::values.screenshot_path = path; + }; + } + + static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); + const std::string filename = game_title.remove(expr).toStdString(); + const std::string timestamp = QDateTime::currentDateTime() + .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) + .toStdString(); + path.append(fmt::format("/{}_{}.png", filename, timestamp)); + + auto* const screenshot_window = + secondary_window->HasFocus() ? secondary_window : render_window; + screenshot_window->CaptureScreenshot( + UISettings::values.screenshot_resolution_factor.GetValue(), + QString::fromStdString(path)); + OnStartGame(); + } +} + +void GMainWindow::OnDumpVideo() { + if (DynamicLibrary::FFmpeg::LoadFFmpeg()) { + if (ui->action_Dump_Video->isChecked()) { + OnStartVideoDumping(); + } else { + OnStopVideoDumping(); + } + } else { + ui->action_Dump_Video->setChecked(false); + + QMessageBox message_box; + message_box.setWindowTitle(tr("Could not load video dumper")); + message_box.setText( + tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." +#ifdef _WIN32 + "\n\nTo install FFmpeg to Lucina3DS, press Open and select your FFmpeg directory." +#endif + "\n\nTo view a guide on how to install FFmpeg, press Help.")); + message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help +#ifdef _WIN32 + | QMessageBox::Open +#endif + ); + auto result = message_box.exec(); + if (result == QMessageBox::Help) { + QDesktopServices::openUrl(QUrl(QStringLiteral( + "https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/"))); +#ifdef _WIN32 + } else if (result == QMessageBox::Open) { + OnOpenFFmpeg(); +#endif + } + } +} + +#ifdef _WIN32 +void GMainWindow::OnOpenFFmpeg() { + auto filename = + QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString(); + if (filename.empty()) { + return; + } + // Check for a bin directory if they chose the FFmpeg root directory. + auto bin_dir = filename + DIR_SEP + "bin"; + if (!FileUtil::Exists(bin_dir)) { + // Otherwise, assume the user directly selected the directory containing the DLLs. + bin_dir = filename; + } + + static const std::array library_names = { + Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR), + }; + + for (auto& library_name : library_names) { + if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("The provided FFmpeg directory is missing %1. Please make " + "sure the correct directory was selected.") + .arg(QString::fromStdString(library_name))); + return; + } + } + + std::atomic success(true); + auto process_file = [&success](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { + auto file_path = directory + DIR_SEP + virtual_name; + if (file_path.ends_with(".dll")) { + auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name; + if (!FileUtil::Copy(file_path, destination_path)) { + success.store(false); + return false; + } + } + return true; + }; + FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); + + if (success.load()) { + QMessageBox::information(this, tr("Lucina3DS"), tr("FFmpeg has been sucessfully installed.")); + } else { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("Installation of FFmpeg failed. Check the log file for details.")); + } +} +#endif + +void GMainWindow::OnStartVideoDumping() { + DumpingDialog dialog(this, system); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + ui->action_Dump_Video->setChecked(false); + return; + } + const auto path = dialog.GetFilePath(); + if (emulation_running) { + StartVideoDumping(path); + } else { + video_dumping_on_start = true; + video_dumping_path = path; + } +} + +void GMainWindow::StartVideoDumping(const QString& path) { + auto& renderer = system.GPU().Renderer(); + const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; + + auto dumper = std::make_shared(renderer); + if (dumper->StartDumping(path.toStdString(), layout)) { + system.RegisterVideoDumper(dumper); + } else { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui->action_Dump_Video->setChecked(false); + } +} + +void GMainWindow::OnStopVideoDumping() { + ui->action_Dump_Video->setChecked(false); + + if (video_dumping_on_start) { + video_dumping_on_start = false; + video_dumping_path.clear(); + } else { + auto dumper = system.GetVideoDumper(); + if (!dumper || !dumper->IsDumping()) { + return; + } + + game_paused_for_dumping = emu_thread->IsRunning(); + OnPauseGame(); + + auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, [this] { + if (game_shutdown_delayed) { + game_shutdown_delayed = false; + ShutdownGame(); + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; + OnStartGame(); + } + }); + future_watcher->setFuture(future); + } +} + +void GMainWindow::UpdateStatusBar() { + if (!emu_thread) [[unlikely]] { + status_bar_update_timer.stop(); + return; + } + + // Update movie status + const u64 current = movie.GetCurrentInputIndex(); + const u64 total = movie.GetTotalInputCount(); + const auto play_mode = movie.GetPlayMode(); + if (play_mode == Core::Movie::PlayMode::Recording) { + message_label->setText(tr("Recording %1").arg(current)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(true); + } else if (play_mode == Core::Movie::PlayMode::Playing) { + message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { + message_label->setText(tr("Movie Finished")); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (message_label_used_for_movie) { // Clear the label if movie was just closed + message_label->setText(QString{}); + message_label_used_for_movie = false; + ui->action_Save_Movie->setEnabled(false); + } + + auto results = system.GetAndResetPerfStats(); + + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 5> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + tr("(Accessing SystemSaveData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + + if (Settings::values.frame_limit.GetValue() == 0) { + emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); + } else { + emu_speed_label->setText(tr("Speed: %1% / %2%") + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.frame_limit.GetValue())); + } + game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); + emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } + emu_speed_label->setVisible(true); + game_fps_label->setVisible(true); + emu_frametime_label->setVisible(true); +} + +void GMainWindow::UpdateBootHomeMenuState() { + const auto current_region = Settings::values.region_value.GetValue(); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + const auto path = Core::GetHomeMenuNcchPath(region); + ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled( + (current_region == Settings::REGION_VALUE_AUTO_SELECT || + current_region == static_cast(region)) && + !path.empty() && FileUtil::Exists(path)); + } +} + +void GMainWindow::HideMouseCursor() { + if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + render_window->setCursor(QCursor(Qt::BlankCursor)); + secondary_window->setCursor(QCursor(Qt::BlankCursor)); + if (UISettings::values.single_window_mode.GetValue()) { + setCursor(QCursor(Qt::BlankCursor)); + } +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + render_window->unsetCursor(); + secondary_window->unsetCursor(); + if (emu_thread && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::OnMute() { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); +} + +void GMainWindow::OnDecreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume <= 30) { + step = 2; + } + if (current_volume <= 6) { + step = 1; + } + const auto value = + static_cast(std::max(current_volume - step, 0)) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::OnIncreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume < 30) { + step = 2; + } + if (current_volume < 6) { + step = 1; + } + const auto value = static_cast(current_volume + step) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::UpdateVolumeUI() { + const auto volume_value = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + volume_slider->setValue(volume_value); + if (Settings::values.audio_muted) { + volume_button->setChecked(false); + volume_button->setText(tr("VOLUME: MUTE")); + } else { + volume_button->setChecked(true); + volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); + } +} + +void GMainWindow::UpdateAPIIndicator(bool update) { + static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"), + QStringLiteral("VULKAN")}; + + static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"), + QStringLiteral("#91242a")}; + + u32 api_index = static_cast(Settings::values.graphics_api.GetValue()); + if (update) { + api_index = (api_index + 1) % graphics_apis.size(); + // Skip past any disabled renderers. +#ifndef ENABLE_SOFTWARE_RENDERER + if (api_index == static_cast(Settings::GraphicsAPI::Software)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_OPENGL + if (api_index == static_cast(Settings::GraphicsAPI::OpenGL)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_VULKAN + if (api_index == static_cast(Settings::GraphicsAPI::Vulkan)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif + Settings::values.graphics_api = static_cast(api_index); + } + + const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }") + .arg(graphics_api_colors[api_index]); + + graphics_api_button->setText(graphics_apis[api_index]); + graphics_api_button->setStyleSheet(style_sheet); +} + +void GMainWindow::UpdateStatusButtons() { + UpdateAPIIndicator(); + UpdateVolumeUI(); +} + +void GMainWindow::OnMouseActivity() { + ShowMouseCursor(); +} + +void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { + QString status_message; + + QString title, message; + QMessageBox::Icon error_severity_icon; + bool can_continue = true; + if (result == Core::System::ResultStatus::ErrorSystemFiles) { + const QString common_message = + tr("%1 is missing. Please dump your " + "system archives.
Continuing emulation may result in crashes and bugs."); + + if (!details.empty()) { + message = common_message.arg(QString::fromStdString(details)); + } else { + message = common_message.arg(tr("A system archive")); + } + + title = tr("System Archive Not Found"); + status_message = tr("System Archive Missing"); + error_severity_icon = QMessageBox::Icon::Critical; + } else if (result == Core::System::ResultStatus::ErrorSavestate) { + title = tr("Save/load Error"); + message = QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Warning; + } else if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + title = tr("Artic Base Server"); + message = + tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details) + .c_str()); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else { + title = tr("Fatal Error"); + message = + tr("A fatal error occurred. " + "Check " + "the log for details." + "
Continuing emulation may result in crashes and bugs."); + status_message = tr("Fatal Error encountered"); + error_severity_icon = QMessageBox::Icon::Critical; + } + + QMessageBox message_box; + message_box.setWindowTitle(title); + message_box.setText(message); + message_box.setIcon(error_severity_icon); + if (error_severity_icon == QMessageBox::Icon::Critical) { + if (can_continue) { + message_box.addButton(tr("Continue"), QMessageBox::RejectRole); + } + QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); + if (result != Core::System::ResultStatus::ShutdownRequested) + message_box.exec(); + + if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested || + message_box.clickedButton() == abort_button) { + if (emu_thread) { + ShutdownGame(); + return; + } + } + } else { + // This block should run when the error isn't too big of a deal + // e.g. when a save state can't be saved or loaded + message_box.addButton(tr("OK"), QMessageBox::RejectRole); + message_box.exec(); + } + + // Only show the message if the game is still running. + if (emu_thread) { + emu_thread->SetRunning(true); + message_label->setText(status_message); + message_label_used_for_movie = false; + } +} + +void GMainWindow::OnMenuAboutLucina3DS() { + AboutDialog about{this}; + about.exec(); +} + +bool GMainWindow::ConfirmClose() { + if (!emu_thread || !UISettings::values.confirm_before_closing) { + return true; + } + + QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Lucina3DS"), tr("Would you like to exit now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::closeEvent(QCloseEvent* event) { + if (!ConfirmClose()) { + event->ignore(); + return; + } + + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + hotkey_registry.SaveHotkeys(); + + // Shutdown session if the emu thread is active... + if (emu_thread) { + ShutdownGame(); + } + + render_window->close(); + secondary_window->close(); + multiplayer_state->Close(); + InputCommon::Shutdown(); + QWidget::closeEvent(event); +} + +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; +} + +static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin", + "3dsx", "app", "elf", "axf"}; + +static bool IsCorrectFileExtension(const QMimeData* mime) { + const QString& filename = mime->urls().at(0).toLocalFile(); + return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), + QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); +} + +static bool IsAcceptableDropEvent(QDropEvent* event) { + return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); +} + +void GMainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsAcceptableDropEvent(event)) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool GMainWindow::DropAction(QDropEvent* event) { + if (!IsAcceptableDropEvent(event)) { + return false; + } + + const QMimeData* mime_data = event->mimeData(); + const QString& filename = mime_data->urls().at(0).toLocalFile(); + + if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { + // Amiibo + LoadAmiibo(filename); + } else { + // Game + if (ConfirmChangeGame()) { + BootGame(filename); + } + } + return true; +} + +void GMainWindow::OnFileOpen(const QFileOpenEvent* event) { + BootGame(event->file()); +} + +void GMainWindow::dropEvent(QDropEvent* event) { + DropAction(event); +} + +void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { + AcceptDropEvent(event); +} + +void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { + AcceptDropEvent(event); +} + +bool GMainWindow::ConfirmChangeGame() { + if (!emu_thread) [[unlikely]] { + return true; + } + + auto answer = QMessageBox::question( + this, tr("Lucina3DS"), tr("The game is still running. Would you like to stop emulation?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::filterBarSetChecked(bool state) { + ui->action_Show_Filter_Bar->setChecked(state); + emit(OnToggleFilterBar()); +} + +void GMainWindow::UpdateUITheme() { + const QString icons_base_path = QStringLiteral(":/icons/"); + const QString default_theme = QStringLiteral("default"); + const QString default_theme_path = icons_base_path + default_theme; + + const QString& current_theme = UISettings::values.theme; + const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second); + QStringList theme_paths(default_theme_paths); + + if (is_default_theme || current_theme.isEmpty()) { + const QString theme_uri(QStringLiteral(":default/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, + "Unable to open default stylesheet, falling back to empty stylesheet"); + qApp->setStyleSheet({}); + setStyleSheet({}); + } + theme_paths.append(default_theme_path); + QIcon::setThemeName(default_theme); + } else { + const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found"); + } + + const QString current_theme_path = icons_base_path + current_theme; + theme_paths.append({default_theme_path, current_theme_path}); + QIcon::setThemeName(current_theme); + } + + QIcon::setThemeSearchPaths(theme_paths); +} + +void GMainWindow::LoadTranslation() { + // If the selected language is English, no need to install any translation + if (UISettings::values.language == QStringLiteral("en")) { + return; + } + + bool loaded; + + if (UISettings::values.language.isEmpty()) { + // Use the system's default locale + loaded = translator.load(QLocale::system(), {}, {}, QStringLiteral(":/languages/")); + } else { + // Otherwise load from the specified file + loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/")); + } + + if (loaded) { + qApp->installTranslator(&translator); + } else { + UISettings::values.language = QStringLiteral("en"); + } +} + +void GMainWindow::OnLanguageChanged(const QString& locale) { + if (UISettings::values.language != QStringLiteral("en")) { + qApp->removeTranslator(&translator); + } + + UISettings::values.language = locale; + LoadTranslation(); + ui->retranslateUi(this); + RetranslateStatusBar(); + UpdateWindowTitle(); +} + +void GMainWindow::OnConfigurePerGame() { + u64 title_id{}; + system.GetAppLoader().ReadProgramId(title_id); + OpenPerGameConfiguration(title_id, game_path); +} + +void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) { + Settings::SetConfiguringGlobal(false); + ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted) { + Settings::RestoreGlobalState(system.IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } + + // Do not cause the global config to write local settings into the config file + const bool is_powered_on = system.IsPoweredOn(); + Settings::RestoreGlobalState(system.IsPoweredOn()); + + if (!is_powered_on) { + config->Save(); + } + + UpdateStatusButtons(); +} + +void GMainWindow::OnMoviePlaybackCompleted() { + OnPauseGame(); + QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); +} + +void GMainWindow::UpdateWindowTitle() { + const QString full_name = QString::fromUtf8(Common::g_build_fullname); + + if (game_title.isEmpty()) { + setWindowTitle(QStringLiteral("%1").arg(full_name)); + } else { + setWindowTitle(QStringLiteral("%1 | %2").arg(full_name, game_title)); + render_window->setWindowTitle( + QStringLiteral("%1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); + secondary_window->setWindowTitle(QStringLiteral("%1 | %2 | %3") + .arg(full_name, game_title, tr("Secondary Window"))); + } +} + +void GMainWindow::UpdateUISettings() { + if (!ui->action_Fullscreen->isChecked()) { + UISettings::values.geometry = saveGeometry(); + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + } + UISettings::values.state = saveState(); +#if MICROPROFILE_ENABLED + UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); + UISettings::values.microprofile_visible = microProfileDialog->isVisible(); +#endif + UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); + UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); + UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); + UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); + UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.first_start = false; +} + +void GMainWindow::SyncMenuUISettings() { + ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::Default); + ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SingleScreen); + ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::LargeScreen); + ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::HybridScreen); + ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SideScreen); + ui->action_Screen_Layout_Separate_Windows->setChecked( + Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); + ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue()); + ui->action_Screen_Layout_Upright_Screens->setChecked( + Settings::values.upright_screen.GetValue()); +} + +void GMainWindow::RetranslateStatusBar() { + if (emu_thread) + UpdateStatusBar(); + + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + multiplayer_state->retranslateUi(); +} + +void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(system); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + +#ifdef __unix__ +void GMainWindow::SetGamemodeEnabled(bool state) { + if (emulation_running) { + Common::Linux::SetGamemodeState(state); + } +} +#endif + +#ifdef main +#undef main +#endif + +static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() { +#ifdef _WIN32 + // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. + // This is done by setting the optimal scaling policy for the primary screen. + + // Create a temporary QApplication. + int temp_argc = 0; + char** temp_argv = nullptr; + QApplication temp{temp_argc, temp_argv}; + + // Get the current screen geometry. + const QScreen* primary_screen = QGuiApplication::primaryScreen(); + if (!primary_screen) { + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; + } + + const QRect screen_rect = primary_screen->geometry(); + const qreal real_ratio = primary_screen->devicePixelRatio(); + const qreal real_width = std::trunc(screen_rect.width() * real_ratio); + const qreal real_height = std::trunc(screen_rect.height() * real_ratio); + + // Recommended minimum width and height for proper window fit. + // Any screen with a lower resolution than this will still have a scale of 1. + constexpr qreal minimum_width = 1350.0; + constexpr qreal minimum_height = 900.0; + + const qreal width_ratio = std::max(1.0, real_width / minimum_width); + const qreal height_ratio = std::max(1.0, real_height / minimum_height); + + // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. + const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio)); + + if (max_ratio > real_ratio) { + return Qt::HighDpiScaleFactorRoundingPolicy::Round; + } else { + return Qt::HighDpiScaleFactorRoundingPolicy::Floor; + } +#else + // Other OSes should be better than Windows at fractional scaling. + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; +#endif +} + +int main(int argc, char* argv[]) { + Common::DetachedTasks detached_tasks; + MicroProfileOnThreadCreate("Frontend"); + SCOPE_EXIT({ MicroProfileShutdown(); }); + + // Init settings params + QCoreApplication::setOrganizationName(QStringLiteral("Citra team | Lucina3DS")); + QCoreApplication::setApplicationName(QStringLiteral("Lucina3DS")); + + auto rounding_policy = GetHighDpiRoundingPolicy(); + QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); + +#ifdef __APPLE__ + auto bundle_dir = FileUtil::GetBundleDirectory(); + if (bundle_dir) { + FileUtil::SetCurrentDir(bundle_dir.value() + ".."); + } +#endif + +#ifdef ENABLE_OPENGL + QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); +#endif + + QApplication app(argc, argv); + + // Qt changes the locale and causes issues in float conversion using std::to_string() when + // generating shaders + setlocale(LC_ALL, "C"); + + auto& system{Core::System::GetInstance()}; + + // Register Qt image interface + system.RegisterImageInterface(std::make_shared()); + + GMainWindow main_window(system); + + // Register frontend applets + Frontend::RegisterDefaultApplets(system); + + system.RegisterMiiSelector(std::make_shared(main_window)); + system.RegisterSoftwareKeyboard(std::make_shared(main_window)); + +#ifdef __APPLE__ + // Register microphone permission check. + system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); +#endif + + main_window.show(); + + QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, + &GMainWindow::OnAppFocusStateChanged); + + int result = app.exec(); + detached_tasks.WaitForAllTasks(); + return result; +} diff --git a/.history/src/lucina3ds_qt/main_20250209234822.cpp b/.history/src/lucina3ds_qt/main_20250209234822.cpp new file mode 100644 index 00000000..043412a8 --- /dev/null +++ b/.history/src/lucina3ds_qt/main_20250209234822.cpp @@ -0,0 +1,3346 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include // for chdir +#endif +#ifdef _WIN32 +#include +#endif +#ifdef __unix__ +#include +#include +#include +#include "common/linux/gamemode.h" +#endif +#include "lucina3ds_qt/aboutdialog.h" +#include "lucina3ds_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/swkbd.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/compatdb.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/discord.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/loading_screen.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/qt_image_interface.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/graphics_device_info.h" +#include "common/arch.h" +#include "common/common_paths.h" +#include "common/detached_tasks.h" +#include "common/dynamic_library/dynamic_library.h" +#include "common/file_util.h" +#include "common/literals.h" +#include "common/logging/backend.h" +#include "common/logging/log.h" +#include "common/memory_detect.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#if LUCINA3DS_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/settings.h" +#include "core/core.h" +#include "core/dumping/backend.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_source_sd_savedata.h" +#include "core/frontend/applets/default_applets.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/nfc/nfc.h" +#include "core/loader/loader.h" +#include "core/movie.h" +#include "core/savestate.h" +#include "core/system_titles.h" +#include "input_common/main.h" +#include "network/network_settings.h" +#include "ui_main.h" +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" + +#ifdef __APPLE__ +#include "common/apple_authorization.h" +#endif + +#ifdef USE_DISCORD_PRESENCE +#include "lucina3ds_qt/discord_impl.h" +#endif + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); +#endif + +#ifdef _WIN32 +extern "C" { +// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +} +#endif + +#ifdef HAVE_SDL2 +#include +#endif + +constexpr int default_mouse_timeout = 2500; + +/** + * "Callouts" are one-time instructional messages shown to the user. In the config settings, there + * is a bitfield "callout_flags" options, used to track if a message has already been shown to the + * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. + */ + +const int GMainWindow::max_recent_files_item; + +static QString PrettyProductName() { +#ifdef _WIN32 + // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 + // With that notation change they changed the registry key used to denote the current version + QSettings windows_registry( + QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), + QSettings::NativeFormat); + const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); + if (release_id == QStringLiteral("2009")) { + const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); + const QString display_version = + windows_registry.value(QStringLiteral("DisplayVersion")).toString(); + const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); + const u32 version = current_build >= 22000 ? 11 : 10; + return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") + .arg(QString::number(version), display_version, QString::number(current_build), + QString::number(ubr)); + } +#endif + return QSysInfo::prettyProductName(); +} + +GMainWindow::GMainWindow(Core::System& system_) + : ui{std::make_unique()}, system{system_}, movie{system.Movie()}, + config{std::make_unique()}, emu_thread{nullptr} { + Common::Log::Initialize(); + Common::Log::Start(); + + Debugger::ToggleConsole(); + +#ifdef __unix__ + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); +#endif + + // register types to use in slots and signals + qRegisterMetaType("std::size_t"); + qRegisterMetaType("Service::AM::InstallStatus"); + + // Register CameraFactory + qt_cameras = std::make_shared(); + Camera::RegisterFactory("image", std::make_unique()); + Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + + LoadTranslation(); + + Pica::g_debug_context = Pica::DebugContext::Construct(); + setAcceptDrops(true); + ui->setupUi(this); + statusBar()->hide(); + + default_theme_paths = QIcon::themeSearchPaths(); + UpdateUITheme(); + + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(); + + Network::Init(); + + movie.SetPlaybackCompletionCallback([this] { + QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); + }); + + InitializeWidgets(); + InitializeDebugWidgets(); + InitializeRecentFileMenuActions(); + InitializeSaveStateMenuActions(); + InitializeHotkeys(); +#if ENABLE_QT_UPDATER + ShowUpdaterWidgets(); +#else + ui->action_Check_For_Updates->setVisible(false); + ui->action_Open_Maintenance_Tool->setVisible(false); +#endif + + SetDefaultUIGeometry(); + RestoreUIState(); + + ConnectAppEvents(); + ConnectMenuEvents(); + ConnectWidgetEvents(); + + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + Common::g_scm_desc); +#if LUCINA3DS_ARCH(x86_64) + const auto& caps = Common::GetCPUCaps(); + std::string cpu_string = caps.cpu_string; + if (caps.avx || caps.avx2 || caps.avx512) { + cpu_string += " | AVX"; + if (caps.avx512) { + cpu_string += "512"; + } else if (caps.avx2) { + cpu_string += '2'; + } + if (caps.fma || caps.fma4) { + cpu_string += " | FMA"; + } + } + LOG_INFO(Frontend, "Host CPU: {}", cpu_string); +#endif + LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); + const auto& mem_info = Common::GetMemInfo(); + using namespace Common::Literals; + LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB}); + LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB}); + UpdateWindowTitle(); + + show(); + + game_list->LoadCompatibilityList(); + game_list->PopulateAsync(UISettings::values.game_dirs); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); + +#ifdef ENABLE_OPENGL + gl_renderer = GetOpenGLRenderer(); +#if defined(_WIN32) + if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { + // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ + // We can override the version here to allow Citra to work. + // TODO: Remove this when OpenGL 4.0+ is advertised. + qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); + } +#endif +#endif + +#ifdef ENABLE_VULKAN + physical_devices = GetVulkanPhysicalDevices(); + if (physical_devices.empty()) { + QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), + tr("Vulkan initialization failed during boot.
" + "Your GPU may not support Vulkan 1.1, or you do not " + "have the latest graphics driver.")); + } +#endif + +#if ENABLE_QT_UPDATER + if (UISettings::values.check_for_update_on_start) { + CheckForUpdates(); + } +#endif + + QStringList args = QApplication::arguments(); + if (args.size() < 2) { + return; + } + + QString game_path; + for (int i = 1; i < args.size(); ++i) { + // Preserves drag/drop functionality + if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { + game_path = args[1]; + break; + } + + // Launch game in fullscreen mode + if (args[i] == QStringLiteral("-f")) { + ui->action_Fullscreen->setChecked(true); + continue; + } + + // Launch game in windowed mode + if (args[i] == QStringLiteral("-w")) { + ui->action_Fullscreen->setChecked(false); + continue; + } + + // Launch game at path + if (args[i] == QStringLiteral("-g")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + game_path = args[++i]; + } + } + + if (!game_path.isEmpty()) { + BootGame(game_path); + } +} + +GMainWindow::~GMainWindow() { + // Will get automatically deleted otherwise + if (!render_window->parent()) { + delete render_window; + } + + Pica::g_debug_context.reset(); + Network::Shutdown(); +} + +void GMainWindow::InitializeWidgets() { +#ifdef LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING + ui->action_Report_Compatibility->setVisible(true); +#endif + render_window = new GRenderWindow(this, emu_thread.get(), system, false); + secondary_window = new GRenderWindow(this, emu_thread.get(), system, true); + render_window->hide(); + secondary_window->hide(); + secondary_window->setParent(nullptr); + + game_list = new GameList(this); + ui->horizontalLayout->addWidget(game_list); + + game_list_placeholder = new GameListPlaceholder(this); + ui->horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui->horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, this, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + render_window->activateWindow(); + } + }); + + InputCommon::Init(); + multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(), + ui->action_Leave_Room, ui->action_Show_Room); + multiplayer_state->setVisible(false); + +#if ENABLE_QT_UPDATER + // Setup updater + updater = new Updater(this); + UISettings::values.updater_found = updater->HasUpdater(); +#endif + + UpdateBootHomeMenuState(); + + // Create status bar + message_label = new QLabel(); + // Configured separately for left alignment + message_label->setFrameStyle(QFrame::NoFrame); + message_label->setContentsMargins(4, 0, 4, 0); + message_label->setAlignment(Qt::AlignLeft); + statusBar()->addPermanentWidget(message_label, 1); + + progress_bar = new QProgressBar(); + progress_bar->hide(); + statusBar()->addPermanentWidget(progress_bar); + + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + + emu_speed_label = new QLabel(); + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label = new QLabel(); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label = new QLabel(); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { + label->setVisible(false); + label->setFrameStyle(QFrame::NoFrame); + label->setContentsMargins(4, 0, 4, 0); + statusBar()->addPermanentWidget(label); + } + + // Setup Graphics API button + graphics_api_button = new QPushButton(); + graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton")); + graphics_api_button->setFocusPolicy(Qt::NoFocus); + UpdateAPIIndicator(); + + connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); }); + + statusBar()->insertPermanentWidget(0, graphics_api_button); + + volume_popup = new QWidget(this); + volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); + volume_popup->setLayout(new QVBoxLayout()); + volume_popup->setMinimumWidth(200); + + volume_slider = new QSlider(Qt::Horizontal); + volume_slider->setObjectName(QStringLiteral("volume_slider")); + volume_slider->setMaximum(100); + volume_slider->setPageStep(5); + connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { + Settings::values.audio_muted = false; + const auto value = static_cast(percentage) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); + }); + volume_popup->layout()->addWidget(volume_slider); + + volume_button = new QPushButton(); + volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + volume_button->setFocusPolicy(Qt::NoFocus); + volume_button->setCheckable(true); + UpdateVolumeUI(); + connect(volume_button, &QPushButton::clicked, this, [&] { + UpdateVolumeUI(); + volume_popup->setVisible(!volume_popup->isVisible()); + QRect rect = volume_button->geometry(); + QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); + bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); + volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); + }); + statusBar()->insertPermanentWidget(1, volume_button); + + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + + statusBar()->setVisible(true); + + // Removes an ugly inner border from the status bar widgets under Linux + setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); + + QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows); +} + +void GMainWindow::InitializeDebugWidgets() { + connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this, + &GMainWindow::OnCreateGraphicsSurfaceViewer); + + QMenu* debug_menu = ui->menu_View_Debugging; + +#if MICROPROFILE_ENABLED + microProfileDialog = new MicroProfileDialog(this); + microProfileDialog->hide(); + debug_menu->addAction(microProfileDialog->toggleViewAction()); +#endif + + registersWidget = new RegistersWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, registersWidget); + registersWidget->hide(); + debug_menu->addAction(registersWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, registersWidget, + &RegistersWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, registersWidget, + &RegistersWidget::OnEmulationStopping); + + graphicsWidget = new GPUCommandStreamWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsWidget); + graphicsWidget->hide(); + debug_menu->addAction(graphicsWidget->toggleViewAction()); + + graphicsCommandsWidget = new GPUCommandListWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); + graphicsCommandsWidget->hide(); + debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); + + graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); + graphicsBreakpointsWidget->hide(); + debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); + + graphicsVertexShaderWidget = + new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget); + graphicsVertexShaderWidget->hide(); + debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction()); + + graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget); + graphicsTracingWidget->hide(); + debug_menu->addAction(graphicsTracingWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStopping); + + waitTreeWidget = new WaitTreeWidget(system, this); + addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget); + waitTreeWidget->hide(); + debug_menu->addAction(waitTreeWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, waitTreeWidget, + &WaitTreeWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + &WaitTreeWidget::OnEmulationStopping); + + lleServiceModulesWidget = new LLEServiceModulesWidget(this); + addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget); + lleServiceModulesWidget->hide(); + debug_menu->addAction(lleServiceModulesWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, + [this] { lleServiceModulesWidget->setDisabled(true); }); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + [this] { lleServiceModulesWidget->setDisabled(false); }); + + ipcRecorderWidget = new IPCRecorderWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); + ipcRecorderWidget->hide(); + debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, + &IPCRecorderWidget::OnEmulationStarting); +} + +void GMainWindow::InitializeRecentFileMenuActions() { + for (int i = 0; i < max_recent_files_item; ++i) { + actions_recent_files[i] = new QAction(this); + actions_recent_files[i]->setVisible(false); + connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); + + ui->menu_recent_files->addAction(actions_recent_files[i]); + } + ui->menu_recent_files->addSeparator(); + QAction* action_clear_recent_files = new QAction(this); + action_clear_recent_files->setText(tr("Clear Recent Files")); + connect(action_clear_recent_files, &QAction::triggered, this, [this] { + UISettings::values.recent_files.clear(); + UpdateRecentFiles(); + }); + ui->menu_recent_files->addAction(action_clear_recent_files); + + UpdateRecentFiles(); +} + +void GMainWindow::InitializeSaveStateMenuActions() { + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i] = new QAction(this); + actions_load_state[i]->setData(i + 1); + connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); + ui->menu_Load_State->addAction(actions_load_state[i]); + + actions_save_state[i] = new QAction(this); + actions_save_state[i]->setData(i + 1); + connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); + ui->menu_Save_State->addAction(actions_save_state[i]); + } + + connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + if (newest_slot != 0) { + actions_load_state[newest_slot - 1]->trigger(); + } + }); + connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + actions_save_state[oldest_slot - 1]->trigger(); + }); + + connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + + UpdateSaveStates(); +} + +void GMainWindow::InitializeHotkeys() { + hotkey_registry.LoadHotkeys(); + + const QString main_window = QStringLiteral("Main Window"); + const QString fullscreen = QStringLiteral("Fullscreen"); + const QString toggle_screen_layout = QStringLiteral("Toggle Screen Layout"); + const QString swap_screens = QStringLiteral("Swap Screens"); + const QString rotate_screens = QStringLiteral("Rotate Screens Upright"); + + const auto link_action_shortcut = [&](QAction* action, const QString& action_name) { + static const QString main_window = QStringLiteral("Main Window"); + action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); + action->setShortcutContext(hotkey_registry.GetShortcutContext(main_window, action_name)); + action->setAutoRepeat(false); + this->addAction(action); + }; + + link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); + link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); + link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); + link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lucina3DS")); + link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); + link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); + link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); + link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); + link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + link_action_shortcut(ui->action_Fullscreen, fullscreen); + link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, swap_screens); + link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, rotate_screens); + link_action_shortcut(ui->action_Enable_Frame_Advancing, + QStringLiteral("Toggle Frame Advancing")); + link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame")); + link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); + link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); + link_action_shortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); + link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); + link_action_shortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); + link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); + link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); + + const auto add_secondary_window_hotkey = [this](QKeySequence hotkey, const char* slot) { + // This action will fire specifically when secondary_window is in focus + QAction* secondary_window_action = new QAction(secondary_window); + secondary_window_action->setShortcut(hotkey); + + connect(secondary_window_action, SIGNAL(triggered()), this, slot); + secondary_window->addAction(secondary_window_action); + }; + + // Use the same fullscreen hotkey as the main window + const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen); + add_secondary_window_hotkey(fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen())); + + const auto toggle_screen_hotkey = + hotkey_registry.GetKeySequence(main_window, toggle_screen_layout); + add_secondary_window_hotkey(toggle_screen_hotkey, SLOT(ToggleScreenLayout())); + + const auto swap_screen_hotkey = hotkey_registry.GetKeySequence(main_window, swap_screens); + add_secondary_window_hotkey(swap_screen_hotkey, SLOT(TriggerSwapScreens())); + + const auto rotate_screen_hotkey = hotkey_registry.GetKeySequence(main_window, rotate_screens); + add_secondary_window_hotkey(rotate_screen_hotkey, SLOT(TriggerRotateScreens())); + + const auto connect_shortcut = [&](const QString& action_name, const auto& function) { + const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this); + connect(hotkey, &QShortcut::activated, this, function); + }; + + connect(hotkey_registry.GetHotkey(main_window, toggle_screen_layout, render_window), + &QShortcut::activated, this, &GMainWindow::ToggleScreenLayout); + + connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { + if (emulation_running) { + ui->action_Fullscreen->setChecked(false); + ToggleFullscreen(); + } + }); + connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] { + Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Toggle Texture Dumping"), + [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + connect_shortcut(QStringLiteral("Toggle Custom Textures"), + [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes + // the variable hold a garbage value after this function exits + static constexpr u16 SPEED_LIMIT_STEP = 5; + connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + return; + } + if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() + + SPEED_LIMIT_STEP); + } else { + Settings::values.frame_limit = 0; + } + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + Settings::values.frame_limit = 995; + } else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() - + SPEED_LIMIT_STEP); + UpdateStatusBar(); + } + UpdateStatusBar(); + }); + + connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); + connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); + connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); + + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the + // variable hold a garbage value after this function exits + static constexpr u16 FACTOR_3D_STEP = 5; + connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d > 0) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); + connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d < 100) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = + factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); +} + +void GMainWindow::SetDefaultUIGeometry() { + // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half + const QRect screenRect = screen()->geometry(); + + const int w = screenRect.width() * 2 / 3; + const int h = screenRect.height() / 2; + const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; + const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100; + + setGeometry(x, y, w, h); +} + +void GMainWindow::RestoreUIState() { + restoreGeometry(UISettings::values.geometry); + restoreState(UISettings::values.state); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); +#if MICROPROFILE_ENABLED + microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); + microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); +#endif + + game_list->LoadInterfaceLayout(); + + ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); + ToggleWindowMode(); + + ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); + SyncMenuUISettings(); + + ui->action_Display_Dock_Widget_Headers->setChecked( + UISettings::values.display_titlebar.GetValue()); + OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked()); + + ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + + ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); +} + +void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { + if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && + state != Qt::ApplicationActive) { + LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); + } + if (!emulation_running) { + return; + } + if (UISettings::values.pause_when_in_background) { + if (emu_thread->IsRunning() && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + auto_paused = true; + OnPauseGame(); + } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { + auto_paused = false; + OnStartGame(); + } + } + if (UISettings::values.mute_when_in_background) { + if (!Settings::values.audio_muted && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + Settings::values.audio_muted = true; + auto_muted = true; + } else if (auto_muted && state == Qt::ApplicationActive) { + Settings::values.audio_muted = false; + auto_muted = false; + } + UpdateVolumeUI(); + } +} + +bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::FileOpen) { + emit FileOpen(static_cast(event)); + return true; + } + return false; +} + +void GMainWindow::ConnectAppEvents() { + const auto filter = new GApplicationEventFilter(); + QGuiApplication::instance()->installEventFilter(filter); + + connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen); +} + +void GMainWindow::ConnectWidgetEvents() { + connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); + connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, + &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); + connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &GMainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, this, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); + + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, + &GMainWindow::OnGameListOpenPerGameProperties); + + connect(this, &GMainWindow::EmulationStarting, render_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, render_window, + &GRenderWindow::OnEmulationStopping); + connect(this, &GMainWindow::EmulationStarting, secondary_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, secondary_window, + &GRenderWindow::OnEmulationStopping); + + connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); + connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); + connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); +} + +void GMainWindow::ConnectMenuEvents() { + const auto connect_menu = [&](QAction* action, const auto& event_fn) { + connect(action, &QAction::triggered, this, event_fn); + // Add actions to this window so that hiding menus in fullscreen won't disable them + addAction(action); + // Add actions to the render window so that they work outside of single window mode + render_window->addAction(action); + }; + + // File + connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); + connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), + [this, region] { OnMenuBootHomeMenu(region); }); + } + connect_menu(ui->action_Exit, &QMainWindow::close); + connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); + connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo); + + // Emulation + connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); + connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); + connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); }); + connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); + connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); + connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); + + // View + connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); + connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); + connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); + connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); + + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + + connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); + connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens); + connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens); + + // Movie + connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie); + connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie); + connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie); + connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie); + connect_menu(ui->action_Movie_Read_Only_Mode, + [this](bool checked) { movie.SetReadOnly(checked); }); + connect_menu(ui->action_Enable_Frame_Advancing, [this] { + if (emulation_running) { + system.frame_limiter.SetFrameAdvancing(ui->action_Enable_Frame_Advancing->isChecked()); + ui->action_Advance_Frame->setEnabled(ui->action_Enable_Frame_Advancing->isChecked()); + } + }); + connect_menu(ui->action_Advance_Frame, [this] { + if (emulation_running && system.frame_limiter.IsFrameAdvancing()) { + ui->action_Enable_Frame_Advancing->setChecked(true); + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.AdvanceFrame(); + } + }); + connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); + connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); + + // Help + connect_menu(ui->action_Open_Lucina3DS_Folder, &GMainWindow::OnOpenLucina3DSFolder); + connect_menu(ui->action_Open_Log_Folder, []() { + QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + connect_menu(ui->action_FAQ, []() { + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); + }); + connect_menu(ui->action_About, &GMainWindow::OnMenuAboutLucina3DS); + +#if ENABLE_QT_UPDATER + connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); + connect_menu(ui->action_Open_Maintenance_Tool, &GMainWindow::OnOpenUpdater); +#endif +} + +void GMainWindow::UpdateMenuState() { + const bool is_paused = !emu_thread || !emu_thread->IsRunning(); + + const std::array running_actions{ + ui->action_Stop, + ui->action_Restart, + ui->action_Configure_Current_Game, + ui->action_Report_Compatibility, + ui->action_Load_Amiibo, + ui->action_Remove_Amiibo, + ui->action_Pause, + ui->action_Advance_Frame, + }; + + for (QAction* action : running_actions) { + action->setEnabled(emulation_running); + } + + ui->action_Capture_Screenshot->setEnabled(emulation_running); + + if (emulation_running && is_paused) { + ui->action_Pause->setText(tr("&Continue")); + } else { + ui->action_Pause->setText(tr("&Pause")); + } +} + +void GMainWindow::OnDisplayTitleBars(bool show) { + QList widgets = findChildren(); + + if (show) { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(nullptr); + if (old) { + delete old; + } + } + } else { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(new QWidget()); + if (old) { + delete old; + } + } + } +} + +#if ENABLE_QT_UPDATER +void GMainWindow::OnCheckForUpdates() { + explicit_update_check = true; + CheckForUpdates(); +} + +void GMainWindow::CheckForUpdates() { + if (updater->CheckForUpdates()) { + LOG_INFO(Frontend, "Update check started"); + } else { + LOG_WARNING(Frontend, "Unable to start check for updates"); + } +} + +void GMainWindow::OnUpdateFound(bool found, bool error) { + if (error) { + LOG_WARNING(Frontend, "Update check failed"); + return; + } + + if (!found) { + LOG_INFO(Frontend, "No updates found"); + + // If the user explicitly clicked the "Check for Updates" button, we are + // going to want to show them a prompt anyway. + if (explicit_update_check) { + explicit_update_check = false; + ShowNoUpdatePrompt(); + } + return; + } + + if (emulation_running && !explicit_update_check) { + LOG_INFO(Frontend, "Update found, deferring as game is running"); + defer_update_prompt = true; + return; + } + + LOG_INFO(Frontend, "Update found!"); + explicit_update_check = false; + + ShowUpdatePrompt(); +} + +void GMainWindow::ShowUpdatePrompt() { + defer_update_prompt = false; + + auto result = + QMessageBox::question(this, tr("Update Available"), + tr("An update is available. Would you like to install it now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + updater->LaunchUIOnExit(); + close(); + } +} + +void GMainWindow::ShowNoUpdatePrompt() { + QMessageBox::information(this, tr("No Update Found"), tr("No update is found."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void GMainWindow::OnOpenUpdater() { + updater->LaunchUI(); +} + +void GMainWindow::ShowUpdaterWidgets() { + ui->action_Check_For_Updates->setVisible(UISettings::values.updater_found); + ui->action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found); + + connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound); +} +#endif + +#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) +static std::optional HoldWakeLockLinux(u32 window_id = 0) { + if (!QDBusConnection::sessionBus().isConnected()) { + return {}; + } + // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit + QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Inhibit")); + if (!xdp.isValid()) { + LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); + return {}; + } + QVariantMap options = {}; + //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the + //: computer from sleeping + options.insert(QString::fromLatin1("reason"), + QCoreApplication::translate("GMainWindow", "Luinca3DS is running a game")); + // 0x4: Suspend lock; 0x8: Idle lock + QDBusReply reply = + xdp.call(QString::fromLatin1("Inhibit"), + QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); + + if (reply.isValid()) { + return reply.value(); + } + LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", + reply.error().message().toStdString()); + return {}; +} + +static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { + if (!QDBusConnection::sessionBus().isConnected()) { + return; + } + QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), + QString::fromLatin1("org.freedesktop.portal.Request")); + unlocker.call(QString::fromLatin1("Close")); +} +#endif // __unix__ + +void GMainWindow::PreventOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + auto reply = HoldWakeLockLinux(winId()); + if (reply) { + wake_lock = std::move(reply.value()); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +void GMainWindow::AllowOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(HAVE_SDL2) + SDL_EnableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + if (!wake_lock.path().isEmpty()) { + ReleaseWakeLockLinux(wake_lock); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +bool GMainWindow::LoadROM(const QString& filename) { + // Shutdown previous session if the emu thread is still active... + if (emu_thread) { + ShutdownGame(); + } + + if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) { + LOG_CRITICAL(Frontend, "Failed to initialize render targets!"); + return false; + } + + const auto scope = render_window->Acquire(); + + const Core::System::ResultStatus result{ + system.Load(*render_window, filename.toStdString(), secondary_window)}; + + if (result != Core::System::ResultStatus::Success) { + switch (result) { + case Core::System::ResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorSystemMode: + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + QMessageBox::critical( + this, tr("ROM Corrupted"), + tr("Your ROM is corrupted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { + QMessageBox::critical( + this, tr("ROM Encrypted"), + tr("Your ROM is encrypted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + } + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + QMessageBox::critical(this, tr("Unsupported ROM"), + tr("GBA Virtual Console ROMs are not supported by Lucina3DS.")); + break; + + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr(fmt::format( + "An error has occurred whilst communicating with the Artic Base Server.\n{}", + system.GetStatusDetails()) + .c_str())); + break; + default: + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + break; + } + return false; + } + + std::string title; + system.GetAppLoader().ReadTitle(title); + game_title = QString::fromStdString(title); + UpdateWindowTitle(); + + game_path = filename; + + return true; +} + +void GMainWindow::BootGame(const QString& filename) { + if (emu_thread) { + ShutdownGame(); + } + + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { + const auto answer = QMessageBox::question( + this, tr("CIA must be installed before usage"), + tr("Before using this CIA, you must install it. Do you want to install it now?"), + QMessageBox::Yes | QMessageBox::No); + + if (answer == QMessageBox::Yes) + InstallCIA(QStringList(filename)); + + return; + } + + show_artic_label = is_artic; + + LOG_INFO(Frontend, "Lucinca3DS starting..."); + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } + + if (movie_record_on_start) { + movie.PrepareForRecording(); + } + if (movie_playback_on_start) { + movie.PrepareForPlayback(movie_playback_path.toStdString()); + } + + const std::string path = filename.toStdString(); + auto loader = Loader::GetLoader(path); + + u64 title_id{0}; + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } + + system.ApplySettings(); + + Settings::LogSettings(); + + // Save configurations + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + config->Save(); + + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); + return; + } + + // Set everything up + if (movie_record_on_start) { + movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (movie_playback_on_start) { + movie.StartPlayback(movie_playback_path.toStdString()); + movie_playback_on_start = false; + movie_playback_path.clear(); + } + + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + + if (video_dumping_on_start) { + StartVideoDumping(video_dumping_path); + video_dumping_on_start = false; + video_dumping_path.clear(); + } + + // Register debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Register(); + } + + // Create and start the emulation thread + emu_thread = std::make_unique(system, *render_window); + emit EmulationStarting(emu_thread.get()); + emu_thread->start(); + + connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + + // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views + // before the CPU continues + connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget, + &RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget, + &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget, + &RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, + &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, + &LoadingScreen::OnLoadComplete); + + // Update the GUI + registersWidget->OnDebugModeEntered(); + if (ui->action_Single_Window_Mode->isChecked()) { + game_list->hide(); + game_list_placeholder->hide(); + } + status_bar_update_timer.start(1000); + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + } + + loading_screen->Prepare(system.GetAppLoader()); + loading_screen->show(); + + emulation_running = true; + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } + + OnStartGame(); +} + +void GMainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + if (ui->action_Fullscreen->isChecked()) { + HideFullscreen(); + } + + auto video_dumper = system.GetVideoDumper(); + if (video_dumper && video_dumper->IsDumping()) { + game_shutdown_delayed = true; + OnStopVideoDumping(); + return; + } + + AllowOSSleep(); + + discord_rpc->Pause(); + emu_thread->RequestStop(); + + // Release emu threads from any breakpoints + // This belongs after RequestStop() and before wait() because if emulation stops on a GPU + // breakpoint after (or before) RequestStop() is called, the emulation would never be able + // to continue out to the main loop and terminate. Thus wait() would hang forever. + // TODO(bunnei): This function is not thread safe, but it's being used as if it were + Pica::g_debug_context->ClearBreakpoints(); + + // Unregister debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Unregister(); + } + + // Frame advancing must be cancelled in order to release the emu thread from waiting + system.frame_limiter.SetFrameAdvancing(false); + + emit EmulationStopping(); + + // Wait for emulation thread to complete and delete it + emu_thread->wait(); + emu_thread = nullptr; + + OnCloseMovie(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif + + // The emulation is stopped, so closing the window or not does not matter anymore + disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + + render_window->hide(); + secondary_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); + + if (game_list->IsEmpty()) { + game_list_placeholder->show(); + } else { + game_list->show(); + } + game_list->SetFilterFocus(); + + setMouseTracking(false); + + // Disable status bar updates + status_bar_update_timer.stop(); + message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); + emu_speed_label->setVisible(false); + game_fps_label->setVisible(false); + emu_frametime_label->setVisible(false); + + UpdateSaveStates(); + + emulation_running = false; + +#if ENABLE_QT_UDPATER + if (defer_update_prompt) { + ShowUpdatePrompt(); + } +#endif + + game_title.clear(); + UpdateWindowTitle(); + + game_path.clear(); + + // Update the GUI + UpdateMenuState(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); +} + +void GMainWindow::StoreRecentFile(const QString& filename) { + UISettings::values.recent_files.prepend(filename); + UISettings::values.recent_files.removeDuplicates(); + while (UISettings::values.recent_files.size() > max_recent_files_item) { + UISettings::values.recent_files.removeLast(); + } + + UpdateRecentFiles(); +} + +void GMainWindow::UpdateRecentFiles() { + const int num_recent_files = + std::min(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); + + for (int i = 0; i < num_recent_files; i++) { + const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( + QFileInfo(UISettings::values.recent_files[i]).fileName()); + actions_recent_files[i]->setText(text); + actions_recent_files[i]->setData(UISettings::values.recent_files[i]); + actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); + actions_recent_files[i]->setVisible(true); + } + + for (int j = num_recent_files; j < max_recent_files_item; ++j) { + actions_recent_files[j]->setVisible(false); + } + + // Enable the recent files menu if the list isn't empty + ui->menu_recent_files->setEnabled(num_recent_files != 0); +} + +void GMainWindow::UpdateSaveStates() { + if (!system.IsPoweredOn()) { + ui->menu_Load_State->setEnabled(false); + ui->menu_Save_State->setEnabled(false); + return; + } + + ui->menu_Load_State->setEnabled(true); + ui->menu_Save_State->setEnabled(true); + ui->action_Load_from_Newest_Slot->setEnabled(false); + + oldest_slot = newest_slot = 0; + oldest_slot_time = std::numeric_limits::max(); + newest_slot_time = 0; + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return; + } + auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID()); + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i]->setEnabled(false); + actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); + actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); + } + for (const auto& savestate : savestates) { + const bool display_name = + savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch && + !savestate.build_name.empty(); + const auto text = + tr("Slot %1 - %2 %3") + .arg(savestate.slot) + .arg(QDateTime::fromSecsSinceEpoch(savestate.time) + .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))) + .arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String()) + .trimmed(); + + actions_load_state[savestate.slot - 1]->setEnabled(true); + actions_load_state[savestate.slot - 1]->setText(text); + actions_save_state[savestate.slot - 1]->setText(text); + + ui->action_Load_from_Newest_Slot->setEnabled(true); + + if (savestate.time > newest_slot_time) { + newest_slot = savestate.slot; + newest_slot_time = savestate.time; + } + if (savestate.time < oldest_slot_time) { + oldest_slot = savestate.slot; + oldest_slot_time = savestate.time; + } + } + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + if (!actions_load_state[i]->isEnabled()) { + // Prefer empty slot + oldest_slot = i + 1; + oldest_slot_time = 0; + break; + } + } +} + +void GMainWindow::OnGameListLoadFile(QString game_path) { + if (ConfirmChangeGame()) { + BootGame(game_path); + } +} + +void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { + std::string path; + std::string open_target; + + switch (target) { + case GameListOpenTarget::SAVE_DATA: { + open_target = "Save Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::EXT_DATA: { + open_target = "Extra Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::APPLICATION: { + open_target = "Application"; + auto media_type = Service::AM::GetTitleMediaType(data_id); + path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; + break; + } + case GameListOpenTarget::UPDATE_DATA: { + open_target = "Update Data"; + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + + "content/"; + break; + } + case GameListOpenTarget::TEXTURE_DUMP: { + open_target = "Dumped Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); + break; + } + case GameListOpenTarget::TEXTURE_LOAD: { + open_target = "Custom Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); + break; + } + case GameListOpenTarget::MODS: { + open_target = "Mods"; + path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + data_id); + break; + } + case GameListOpenTarget::DLC_DATA: { + open_target = "DLC Data"; + path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/0004008c/{:08x}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id); + break; + } + case GameListOpenTarget::SHADER_CACHE: { + open_target = "Shader Cache"; + path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir); + break; + } + default: + LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); + return; + } + + QString qpath = QString::fromStdString(path); + + QDir dir(qpath); + if (!dir.exists()) { + QMessageBox::critical( + this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)), + tr("Folder does not exist!")); + return; + } + + LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); + + QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); +} + +void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + QString directory; + if (it != compatibility_list.end()) + directory = it->second.second; + + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory)); +} + +void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { + auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setWindowFlags(dialog->windowFlags() & + ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); + dialog->setCancelButton(nullptr); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + const auto base_path = fmt::format( + "{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); + const auto update_path = + fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + program_id | 0x0004000e00000000); + using FutureWatcher = QFutureWatcher>; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, + [this, dialog, base_path, update_path, future_watcher] { + dialog->hide(); + const auto& [base, update] = future_watcher->result(); + if (base != Loader::ResultStatus::Success) { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not dump base RomFS.\nRefer to the log for details.")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path))); + if (update == Loader::ResultStatus::Success) { + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(update_path))); + } + }); + + auto future = QtConcurrent::run([game_path, base_path, update_path] { + std::unique_ptr loader = Loader::GetLoader(game_path.toStdString()); + return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path)); + }); + future_watcher->setFuture(future); +} + +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { + QString path; + if (directory == QStringLiteral("INSTALLED")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); + } else if (directory == QStringLiteral("SYSTEM")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000/title/00040010"); + } else { + path = directory; + } + if (!QFileInfo::exists(path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) + return; + UISettings::GameDir game_dir{dir_path, false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } +} + +void GMainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui->action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + +void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { + const auto loader = Loader::GetLoader(file.toStdString()); + + u64 title_id{}; + if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { + QMessageBox::information(this, tr("Properties"), + tr("The game properties could not be loaded.")); + return; + } + + OpenPerGameConfiguration(title_id, file); +} + +void GMainWindow::OnMenuLoadFile() { + const QString extensions = QStringLiteral("*.").append( + GameList::supported_file_extensions.join(QStringLiteral(" *."))); + const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)", + "%1 is an identifier for the 3DS executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), UISettings::values.roms_path, file_filter); + + if (filename.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + +void GMainWindow::OnMenuInstallCIA() { + QStringList filepaths = QFileDialog::getOpenFileNames( + this, tr("Load Files"), UISettings::values.roms_path, + tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)")); + + if (filepaths.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filepaths[0]).path(); + InstallCIA(filepaths); +} + +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + +void GMainWindow::OnMenuBootHomeMenu(u32 region) { + BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); +} + +void GMainWindow::InstallCIA(QStringList filepaths) { + ui->action_Install_CIA->setEnabled(false); + game_list->SetDirectoryWatcherEnabled(false); + progress_bar->show(); + progress_bar->setMaximum(INT_MAX); + + (void)QtConcurrent::run([&, filepaths] { + Service::AM::InstallStatus status; + const auto cia_progress = [&](std::size_t written, std::size_t total) { + emit UpdateProgress(written, total); + }; + for (const auto& current_path : filepaths) { + status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress); + emit CIAInstallReport(status, current_path); + } + emit CIAInstallFinished(); + }); +} + +void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) { + progress_bar->setValue( + static_cast(INT_MAX * (static_cast(written) / static_cast(total)))); +} + +void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) { + QString filename = QFileInfo(filepath).fileName(); + switch (status) { + case Service::AM::InstallStatus::Success: + this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFailedToOpenFile: + QMessageBox::critical(this, tr("Unable to open File"), + tr("Could not open %1").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorAborted: + QMessageBox::critical( + this, tr("Installation aborted"), + tr("The installation of %1 was aborted. Please see the log for more details") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorInvalid: + QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorEncrypted: + QMessageBox::critical(this, tr("Encrypted File"), + tr("%1 must be decrypted " + "before being used with Lucina3DS. A real 3DS is required.") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFileNotFound: + QMessageBox::critical(this, tr("Unable to find File"), + tr("Could not find %1").arg(filename)); + break; + } +} + +void GMainWindow::OnCIAInstallFinished() { + progress_bar->hide(); + progress_bar->setValue(0); + game_list->SetDirectoryWatcherEnabled(true); + ui->action_Install_CIA->setEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void GMainWindow::UninstallTitles( + const std::vector>& titles) { + if (titles.empty()) { + return; + } + + // Select the first title in the list as representative. + const auto first_name = std::get(titles[0]); + + QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, + static_cast(titles.size()), this); + progress.setWindowModality(Qt::WindowModal); + + QFutureWatcher future_watcher; + QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, + &QProgressDialog::reset); + QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, + &QProgressDialog::setValue); + + auto failed = false; + QString failed_name; + + const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { + const auto name = std::get(title); + const auto media_type = std::get(title); + const auto program_id = std::get(title); + + const auto result = Service::AM::UninstallProgram(media_type, program_id); + if (result.IsError()) { + LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), + result.raw); + failed = true; + failed_name = name; + future_watcher.cancel(); + } + }; + + future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); + progress.exec(); + future_watcher.waitForFinished(); + + if (failed) { + QMessageBox::critical(this, tr("Lucina3DS"), tr("Failed to uninstall '%1'.").arg(failed_name)); + } else if (!future_watcher.isCanceled()) { + QMessageBox::information(this, tr("Lucina3DS"), + tr("Successfully uninstalled '%1'.").arg(first_name)); + } +} + +void GMainWindow::OnMenuRecentFile() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + const QString filename = action->data().toString(); + if (QFileInfo::exists(filename)) { + BootGame(filename); + } else { + // Display an error message and remove the file from the list. + QMessageBox::information(this, tr("File not found"), + tr("File \"%1\" not found").arg(filename)); + + UISettings::values.recent_files.removeOne(filename); + UpdateRecentFiles(); + } +} + +void GMainWindow::OnStartGame() { + qt_cameras->ResumeCameras(); + + PreventOSSleep(); + + emu_thread->SetRunning(true); + graphics_api_button->setEnabled(false); + qRegisterMetaType("Core::System::ResultStatus"); + qRegisterMetaType("std::string"); + connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); + + UpdateMenuState(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StartGamemode(); +#endif + + UpdateSaveStates(); + UpdateStatusButtons(); +} + +void GMainWindow::OnRestartGame() { + if (!system.IsPoweredOn()) { + return; + } + // Make a copy since BootGame edits game_path + BootGame(QString(game_path)); +} + +void GMainWindow::OnPauseGame() { + emu_thread->SetRunning(false); + qt_cameras->PauseCameras(); + + UpdateMenuState(); + AllowOSSleep(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif +} + +void GMainWindow::OnPauseContinueGame() { + if (emulation_running) { + if (emu_thread->IsRunning()) { + OnPauseGame(); + } else { + OnStartGame(); + } + } +} + +void GMainWindow::OnStopGame() { + ShutdownGame(); + graphics_api_button->setEnabled(true); + Settings::RestoreGlobalState(false); + UpdateStatusButtons(); +} + +void GMainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnMenuReportCompatibility() { + if (!NetSettings::values.lucina3ds_token.empty() && !NetSettings::values.lucina3ds_username.empty()) { + CompatDB compatdb{this}; + compatdb.exec(); + } else { + QMessageBox::critical(this, tr("Missing Citra Account"), + tr("You must link your Citra account to submit test cases." + "
Go to Emulation > Configure... > Web to do so.")); + } +} + +void GMainWindow::ToggleFullscreen() { + if (!emulation_running) { + return; + } + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } else { + HideFullscreen(); + } +} + +void GMainWindow::ToggleSecondaryFullscreen() { + if (!emulation_running) { + return; + } + if (secondary_window->isFullScreen()) { + secondary_window->showNormal(); + } else { + secondary_window->showFullScreen(); + } +} + +void GMainWindow::ShowFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + UISettings::values.geometry = saveGeometry(); + ui->menubar->hide(); + statusBar()->hide(); + showFullScreen(); + } else { + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + render_window->showFullScreen(); + } +} + +void GMainWindow::HideFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->menubar->show(); + showNormal(); + restoreGeometry(UISettings::values.geometry); + } else { + render_window->showNormal(); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } +} + +void GMainWindow::ToggleWindowMode() { + if (ui->action_Single_Window_Mode->isChecked()) { + // Render in the main window... + render_window->BackupGeometry(); + ui->horizontalLayout->addWidget(render_window); + render_window->setFocusPolicy(Qt::StrongFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->setFocus(); + game_list->hide(); + } + + } else { + // Render in a separate window... + ui->horizontalLayout->removeWidget(render_window); + render_window->setParent(nullptr); + render_window->setFocusPolicy(Qt::NoFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->RestoreGeometry(); + game_list->show(); + } + } +} + +void GMainWindow::UpdateSecondaryWindowVisibility() { + if (!emulation_running) { + return; + } + if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) { + secondary_window->RestoreGeometry(); + secondary_window->show(); + } else { + secondary_window->BackupGeometry(); + secondary_window->hide(); + } +} + +void GMainWindow::ChangeScreenLayout() { + Settings::LayoutOption new_layout = Settings::LayoutOption::Default; + + if (ui->action_Screen_Layout_Default->isChecked()) { + new_layout = Settings::LayoutOption::Default; + } else if (ui->action_Screen_Layout_Single_Screen->isChecked()) { + new_layout = Settings::LayoutOption::SingleScreen; + } else if (ui->action_Screen_Layout_Large_Screen->isChecked()) { + new_layout = Settings::LayoutOption::LargeScreen; + } else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) { + new_layout = Settings::LayoutOption::HybridScreen; + } else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) { + new_layout = Settings::LayoutOption::SideScreen; + } else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) { + new_layout = Settings::LayoutOption::SeparateWindows; + } + + Settings::values.layout_option = new_layout; + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::ToggleScreenLayout() { + const Settings::LayoutOption new_layout = []() { + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::Default: + return Settings::LayoutOption::SingleScreen; + case Settings::LayoutOption::SingleScreen: + return Settings::LayoutOption::LargeScreen; + case Settings::LayoutOption::LargeScreen: + return Settings::LayoutOption::HybridScreen; + case Settings::LayoutOption::HybridScreen: + return Settings::LayoutOption::SideScreen; + case Settings::LayoutOption::SideScreen: + return Settings::LayoutOption::SeparateWindows; + case Settings::LayoutOption::SeparateWindows: + return Settings::LayoutOption::Default; + default: + LOG_ERROR(Frontend, "Unknown layout option {}", + Settings::values.layout_option.GetValue()); + return Settings::LayoutOption::Default; + } + }(); + + Settings::values.layout_option = new_layout; + SyncMenuUISettings(); + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnSwapScreens() { + Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::OnRotateScreens() { + Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::TriggerSwapScreens() { + ui->action_Screen_Layout_Swap_Screens->trigger(); +} + +void GMainWindow::TriggerRotateScreens() { + ui->action_Screen_Layout_Upright_Screens->trigger(); +} + +void GMainWindow::OnSaveState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + system.SendSignal(Core::System::Signal::Save, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); + newest_slot = action->data().toUInt(); +} + +void GMainWindow::OnLoadState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + if (UISettings::values.save_state_warning) { + QMessageBox::warning(this, tr("Savestates"), + tr("Warning: Savestates are NOT a replacement for in-game saves, " + "and are not meant to be reliable.\n\nUse at your own risk!")); + UISettings::values.save_state_warning = false; + config->Save(); + } + + system.SendSignal(Core::System::Signal::Load, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); +} + +void GMainWindow::OnConfigure() { + game_list->SetDirectoryWatcherEnabled(false); + Settings::SetConfiguringGlobal(true); + ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices, + !multiplayer_state->IsHostingPublicRoom()); + connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, + &GMainWindow::OnLanguageChanged); + auto old_theme = UISettings::values.theme; + const int old_input_profile_index = Settings::values.current_input_profile_index; + const auto old_input_profiles = Settings::values.input_profiles; + const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps; + const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); +#ifdef __unix__ + const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); +#endif + auto result = configureDialog.exec(); + game_list->SetDirectoryWatcherEnabled(true); + if (result == QDialog::Accepted) { + configureDialog.ApplyConfiguration(); + InitializeHotkeys(); + if (UISettings::values.theme != old_theme) { + UpdateUITheme(); + } + if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + } +#ifdef __unix__ + if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); + } +#endif + if (!multiplayer_state->IsHostingPublicRoom()) + multiplayer_state->UpdateCredentials(); + emit UpdateThemedIcons(); + SyncMenuUISettings(); + game_list->RefreshGameDirectory(); + config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + } + UpdateSecondaryWindowVisibility(); + UpdateBootHomeMenuState(); + UpdateStatusButtons(); + } else { + Settings::values.input_profiles = old_input_profiles; + Settings::values.touch_from_button_maps = old_touch_from_button_maps; + Settings::LoadProfile(old_input_profile_index); + } +} + +void GMainWindow::OnLoadAmiibo() { + if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { + return; + } + + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr) { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (nfc->IsTagActive()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("A tag is already in use.")); + return; + } + + if (!nfc->IsSearchingForAmiibos()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Game is not looking for amiibos.")); + return; + } + + const QString extensions{QStringLiteral("*.bin")}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); + + if (filename.isEmpty()) { + return; + } + + LoadAmiibo(filename); +} + +void GMainWindow::LoadAmiibo(const QString& filename) { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (!nfc->LoadAmiibo(filename.toStdString())) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Unable to open amiibo file \"%1\" for reading.").arg(filename)); + return; + } + + ui->action_Remove_Amiibo->setEnabled(true); +} + +void GMainWindow::OnRemoveAmiibo() { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + nfc->RemoveAmiibo(); + ui->action_Remove_Amiibo->setEnabled(false); +} + +void GMainWindow::OnOpenLucina3DSFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); +} + +void GMainWindow::OnToggleFilterBar() { + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + if (ui->action_Show_Filter_Bar->isChecked()) { + game_list->SetFilterFocus(); + } else { + game_list->ClearFilter(); + } +} + +void GMainWindow::OnCreateGraphicsSurfaceViewer() { + auto graphicsSurfaceViewerWidget = + new GraphicsSurfaceWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget); + // TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true); + graphicsSurfaceViewerWidget->show(); +} + +void GMainWindow::OnRecordMovie() { + MovieRecordDialog dialog(this, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_record_on_start = true; + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); + + if (emulation_running) { // Restart game + BootGame(QString(game_path)); + } + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(true); +} + +void GMainWindow::OnPlayMovie() { + MoviePlayDialog dialog(this, game_list, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_playback_on_start = true; + movie_playback_path = dialog.GetMoviePath(); + BootGame(dialog.GetGamePath()); + + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnCloseMovie() { + if (movie_record_on_start) { + QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } else { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording; + movie.Shutdown(); + if (was_recording) { + QMessageBox::information(this, tr("Movie Saved"), + tr("The movie is successfully saved.")); + } + + if (was_running) { + OnStartGame(); + } + } + + ui->action_Close_Movie->setEnabled(false); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnSaveMovie() { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) { + movie.SaveMovie(); + QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); + } else { + LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); + } + + if (was_running) { + OnStartGame(); + } +} + +void GMainWindow::OnCaptureScreenshot() { + if (!emu_thread) [[unlikely]] { + return; + } + + const bool was_running = emu_thread->IsRunning(); + + if (was_running || + (QMessageBox::question( + this, tr("Game will unpause"), + tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running) { + OnPauseGame(); + } + std::string path = UISettings::values.screenshot_path.GetValue(); + if (!FileUtil::IsDirectory(path)) { + if (!FileUtil::CreateFullPath(path)) { + QMessageBox::information( + this, tr("Invalid Screenshot Directory"), + tr("Cannot create specified screenshot directory. Screenshot " + "path is set back to its default value.")); + path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + path.append("screenshots/"); + UISettings::values.screenshot_path = path; + }; + } + + static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); + const std::string filename = game_title.remove(expr).toStdString(); + const std::string timestamp = QDateTime::currentDateTime() + .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) + .toStdString(); + path.append(fmt::format("/{}_{}.png", filename, timestamp)); + + auto* const screenshot_window = + secondary_window->HasFocus() ? secondary_window : render_window; + screenshot_window->CaptureScreenshot( + UISettings::values.screenshot_resolution_factor.GetValue(), + QString::fromStdString(path)); + OnStartGame(); + } +} + +void GMainWindow::OnDumpVideo() { + if (DynamicLibrary::FFmpeg::LoadFFmpeg()) { + if (ui->action_Dump_Video->isChecked()) { + OnStartVideoDumping(); + } else { + OnStopVideoDumping(); + } + } else { + ui->action_Dump_Video->setChecked(false); + + QMessageBox message_box; + message_box.setWindowTitle(tr("Could not load video dumper")); + message_box.setText( + tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." +#ifdef _WIN32 + "\n\nTo install FFmpeg to Lucina3DS, press Open and select your FFmpeg directory." +#endif + "\n\nTo view a guide on how to install FFmpeg, press Help.")); + message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help +#ifdef _WIN32 + | QMessageBox::Open +#endif + ); + auto result = message_box.exec(); + if (result == QMessageBox::Help) { + QDesktopServices::openUrl(QUrl(QStringLiteral( + "https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/"))); +#ifdef _WIN32 + } else if (result == QMessageBox::Open) { + OnOpenFFmpeg(); +#endif + } + } +} + +#ifdef _WIN32 +void GMainWindow::OnOpenFFmpeg() { + auto filename = + QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString(); + if (filename.empty()) { + return; + } + // Check for a bin directory if they chose the FFmpeg root directory. + auto bin_dir = filename + DIR_SEP + "bin"; + if (!FileUtil::Exists(bin_dir)) { + // Otherwise, assume the user directly selected the directory containing the DLLs. + bin_dir = filename; + } + + static const std::array library_names = { + Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR), + }; + + for (auto& library_name : library_names) { + if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("The provided FFmpeg directory is missing %1. Please make " + "sure the correct directory was selected.") + .arg(QString::fromStdString(library_name))); + return; + } + } + + std::atomic success(true); + auto process_file = [&success](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { + auto file_path = directory + DIR_SEP + virtual_name; + if (file_path.ends_with(".dll")) { + auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name; + if (!FileUtil::Copy(file_path, destination_path)) { + success.store(false); + return false; + } + } + return true; + }; + FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); + + if (success.load()) { + QMessageBox::information(this, tr("Lucina3DS"), tr("FFmpeg has been sucessfully installed.")); + } else { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("Installation of FFmpeg failed. Check the log file for details.")); + } +} +#endif + +void GMainWindow::OnStartVideoDumping() { + DumpingDialog dialog(this, system); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + ui->action_Dump_Video->setChecked(false); + return; + } + const auto path = dialog.GetFilePath(); + if (emulation_running) { + StartVideoDumping(path); + } else { + video_dumping_on_start = true; + video_dumping_path = path; + } +} + +void GMainWindow::StartVideoDumping(const QString& path) { + auto& renderer = system.GPU().Renderer(); + const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; + + auto dumper = std::make_shared(renderer); + if (dumper->StartDumping(path.toStdString(), layout)) { + system.RegisterVideoDumper(dumper); + } else { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui->action_Dump_Video->setChecked(false); + } +} + +void GMainWindow::OnStopVideoDumping() { + ui->action_Dump_Video->setChecked(false); + + if (video_dumping_on_start) { + video_dumping_on_start = false; + video_dumping_path.clear(); + } else { + auto dumper = system.GetVideoDumper(); + if (!dumper || !dumper->IsDumping()) { + return; + } + + game_paused_for_dumping = emu_thread->IsRunning(); + OnPauseGame(); + + auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, [this] { + if (game_shutdown_delayed) { + game_shutdown_delayed = false; + ShutdownGame(); + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; + OnStartGame(); + } + }); + future_watcher->setFuture(future); + } +} + +void GMainWindow::UpdateStatusBar() { + if (!emu_thread) [[unlikely]] { + status_bar_update_timer.stop(); + return; + } + + // Update movie status + const u64 current = movie.GetCurrentInputIndex(); + const u64 total = movie.GetTotalInputCount(); + const auto play_mode = movie.GetPlayMode(); + if (play_mode == Core::Movie::PlayMode::Recording) { + message_label->setText(tr("Recording %1").arg(current)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(true); + } else if (play_mode == Core::Movie::PlayMode::Playing) { + message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { + message_label->setText(tr("Movie Finished")); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (message_label_used_for_movie) { // Clear the label if movie was just closed + message_label->setText(QString{}); + message_label_used_for_movie = false; + ui->action_Save_Movie->setEnabled(false); + } + + auto results = system.GetAndResetPerfStats(); + + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 5> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + tr("(Accessing SystemSaveData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + + if (Settings::values.frame_limit.GetValue() == 0) { + emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); + } else { + emu_speed_label->setText(tr("Speed: %1% / %2%") + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.frame_limit.GetValue())); + } + game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); + emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } + emu_speed_label->setVisible(true); + game_fps_label->setVisible(true); + emu_frametime_label->setVisible(true); +} + +void GMainWindow::UpdateBootHomeMenuState() { + const auto current_region = Settings::values.region_value.GetValue(); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + const auto path = Core::GetHomeMenuNcchPath(region); + ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled( + (current_region == Settings::REGION_VALUE_AUTO_SELECT || + current_region == static_cast(region)) && + !path.empty() && FileUtil::Exists(path)); + } +} + +void GMainWindow::HideMouseCursor() { + if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + render_window->setCursor(QCursor(Qt::BlankCursor)); + secondary_window->setCursor(QCursor(Qt::BlankCursor)); + if (UISettings::values.single_window_mode.GetValue()) { + setCursor(QCursor(Qt::BlankCursor)); + } +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + render_window->unsetCursor(); + secondary_window->unsetCursor(); + if (emu_thread && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::OnMute() { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); +} + +void GMainWindow::OnDecreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume <= 30) { + step = 2; + } + if (current_volume <= 6) { + step = 1; + } + const auto value = + static_cast(std::max(current_volume - step, 0)) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::OnIncreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume < 30) { + step = 2; + } + if (current_volume < 6) { + step = 1; + } + const auto value = static_cast(current_volume + step) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::UpdateVolumeUI() { + const auto volume_value = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + volume_slider->setValue(volume_value); + if (Settings::values.audio_muted) { + volume_button->setChecked(false); + volume_button->setText(tr("VOLUME: MUTE")); + } else { + volume_button->setChecked(true); + volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); + } +} + +void GMainWindow::UpdateAPIIndicator(bool update) { + static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"), + QStringLiteral("VULKAN")}; + + static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"), + QStringLiteral("#91242a")}; + + u32 api_index = static_cast(Settings::values.graphics_api.GetValue()); + if (update) { + api_index = (api_index + 1) % graphics_apis.size(); + // Skip past any disabled renderers. +#ifndef ENABLE_SOFTWARE_RENDERER + if (api_index == static_cast(Settings::GraphicsAPI::Software)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_OPENGL + if (api_index == static_cast(Settings::GraphicsAPI::OpenGL)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_VULKAN + if (api_index == static_cast(Settings::GraphicsAPI::Vulkan)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif + Settings::values.graphics_api = static_cast(api_index); + } + + const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }") + .arg(graphics_api_colors[api_index]); + + graphics_api_button->setText(graphics_apis[api_index]); + graphics_api_button->setStyleSheet(style_sheet); +} + +void GMainWindow::UpdateStatusButtons() { + UpdateAPIIndicator(); + UpdateVolumeUI(); +} + +void GMainWindow::OnMouseActivity() { + ShowMouseCursor(); +} + +void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { + QString status_message; + + QString title, message; + QMessageBox::Icon error_severity_icon; + bool can_continue = true; + if (result == Core::System::ResultStatus::ErrorSystemFiles) { + const QString common_message = + tr("%1 is missing. Please dump your " + "system archives.
Continuing emulation may result in crashes and bugs."); + + if (!details.empty()) { + message = common_message.arg(QString::fromStdString(details)); + } else { + message = common_message.arg(tr("A system archive")); + } + + title = tr("System Archive Not Found"); + status_message = tr("System Archive Missing"); + error_severity_icon = QMessageBox::Icon::Critical; + } else if (result == Core::System::ResultStatus::ErrorSavestate) { + title = tr("Save/load Error"); + message = QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Warning; + } else if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + title = tr("Artic Base Server"); + message = + tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details) + .c_str()); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else { + title = tr("Fatal Error"); + message = + tr("A fatal error occurred. " + "Check " + "the log for details." + "
Continuing emulation may result in crashes and bugs."); + status_message = tr("Fatal Error encountered"); + error_severity_icon = QMessageBox::Icon::Critical; + } + + QMessageBox message_box; + message_box.setWindowTitle(title); + message_box.setText(message); + message_box.setIcon(error_severity_icon); + if (error_severity_icon == QMessageBox::Icon::Critical) { + if (can_continue) { + message_box.addButton(tr("Continue"), QMessageBox::RejectRole); + } + QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); + if (result != Core::System::ResultStatus::ShutdownRequested) + message_box.exec(); + + if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested || + message_box.clickedButton() == abort_button) { + if (emu_thread) { + ShutdownGame(); + return; + } + } + } else { + // This block should run when the error isn't too big of a deal + // e.g. when a save state can't be saved or loaded + message_box.addButton(tr("OK"), QMessageBox::RejectRole); + message_box.exec(); + } + + // Only show the message if the game is still running. + if (emu_thread) { + emu_thread->SetRunning(true); + message_label->setText(status_message); + message_label_used_for_movie = false; + } +} + +void GMainWindow::OnMenuAboutLucina3DS() { + AboutDialog about{this}; + about.exec(); +} + +bool GMainWindow::ConfirmClose() { + if (!emu_thread || !UISettings::values.confirm_before_closing) { + return true; + } + + QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Lucina3DS"), tr("Would you like to exit now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::closeEvent(QCloseEvent* event) { + if (!ConfirmClose()) { + event->ignore(); + return; + } + + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + hotkey_registry.SaveHotkeys(); + + // Shutdown session if the emu thread is active... + if (emu_thread) { + ShutdownGame(); + } + + render_window->close(); + secondary_window->close(); + multiplayer_state->Close(); + InputCommon::Shutdown(); + QWidget::closeEvent(event); +} + +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; +} + +static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin", + "3dsx", "app", "elf", "axf"}; + +static bool IsCorrectFileExtension(const QMimeData* mime) { + const QString& filename = mime->urls().at(0).toLocalFile(); + return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), + QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); +} + +static bool IsAcceptableDropEvent(QDropEvent* event) { + return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); +} + +void GMainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsAcceptableDropEvent(event)) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool GMainWindow::DropAction(QDropEvent* event) { + if (!IsAcceptableDropEvent(event)) { + return false; + } + + const QMimeData* mime_data = event->mimeData(); + const QString& filename = mime_data->urls().at(0).toLocalFile(); + + if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { + // Amiibo + LoadAmiibo(filename); + } else { + // Game + if (ConfirmChangeGame()) { + BootGame(filename); + } + } + return true; +} + +void GMainWindow::OnFileOpen(const QFileOpenEvent* event) { + BootGame(event->file()); +} + +void GMainWindow::dropEvent(QDropEvent* event) { + DropAction(event); +} + +void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { + AcceptDropEvent(event); +} + +void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { + AcceptDropEvent(event); +} + +bool GMainWindow::ConfirmChangeGame() { + if (!emu_thread) [[unlikely]] { + return true; + } + + auto answer = QMessageBox::question( + this, tr("Lucina3DS"), tr("The game is still running. Would you like to stop emulation?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::filterBarSetChecked(bool state) { + ui->action_Show_Filter_Bar->setChecked(state); + emit(OnToggleFilterBar()); +} + +void GMainWindow::UpdateUITheme() { + const QString icons_base_path = QStringLiteral(":/icons/"); + const QString default_theme = QStringLiteral("default"); + const QString default_theme_path = icons_base_path + default_theme; + + const QString& current_theme = UISettings::values.theme; + const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second); + QStringList theme_paths(default_theme_paths); + + if (is_default_theme || current_theme.isEmpty()) { + const QString theme_uri(QStringLiteral(":default/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, + "Unable to open default stylesheet, falling back to empty stylesheet"); + qApp->setStyleSheet({}); + setStyleSheet({}); + } + theme_paths.append(default_theme_path); + QIcon::setThemeName(default_theme); + } else { + const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found"); + } + + const QString current_theme_path = icons_base_path + current_theme; + theme_paths.append({default_theme_path, current_theme_path}); + QIcon::setThemeName(current_theme); + } + + QIcon::setThemeSearchPaths(theme_paths); +} + +void GMainWindow::LoadTranslation() { + // If the selected language is English, no need to install any translation + if (UISettings::values.language == QStringLiteral("en")) { + return; + } + + bool loaded; + + if (UISettings::values.language.isEmpty()) { + // Use the system's default locale + loaded = translator.load(QLocale::system(), {}, {}, QStringLiteral(":/languages/")); + } else { + // Otherwise load from the specified file + loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/")); + } + + if (loaded) { + qApp->installTranslator(&translator); + } else { + UISettings::values.language = QStringLiteral("en"); + } +} + +void GMainWindow::OnLanguageChanged(const QString& locale) { + if (UISettings::values.language != QStringLiteral("en")) { + qApp->removeTranslator(&translator); + } + + UISettings::values.language = locale; + LoadTranslation(); + ui->retranslateUi(this); + RetranslateStatusBar(); + UpdateWindowTitle(); +} + +void GMainWindow::OnConfigurePerGame() { + u64 title_id{}; + system.GetAppLoader().ReadProgramId(title_id); + OpenPerGameConfiguration(title_id, game_path); +} + +void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) { + Settings::SetConfiguringGlobal(false); + ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted) { + Settings::RestoreGlobalState(system.IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } + + // Do not cause the global config to write local settings into the config file + const bool is_powered_on = system.IsPoweredOn(); + Settings::RestoreGlobalState(system.IsPoweredOn()); + + if (!is_powered_on) { + config->Save(); + } + + UpdateStatusButtons(); +} + +void GMainWindow::OnMoviePlaybackCompleted() { + OnPauseGame(); + QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); +} + +void GMainWindow::UpdateWindowTitle() { + const QString full_name = QString::fromUtf8(Common::g_build_fullname); + + if (game_title.isEmpty()) { + setWindowTitle(QStringLiteral("%1").arg(full_name)); + } else { + setWindowTitle(QStringLiteral("%1 | %2").arg(full_name, game_title)); + render_window->setWindowTitle( + QStringLiteral("%1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); + secondary_window->setWindowTitle(QStringLiteral("%1 | %2 | %3") + .arg(full_name, game_title, tr("Secondary Window"))); + } +} + +void GMainWindow::UpdateUISettings() { + if (!ui->action_Fullscreen->isChecked()) { + UISettings::values.geometry = saveGeometry(); + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + } + UISettings::values.state = saveState(); +#if MICROPROFILE_ENABLED + UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); + UISettings::values.microprofile_visible = microProfileDialog->isVisible(); +#endif + UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); + UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); + UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); + UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); + UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.first_start = false; +} + +void GMainWindow::SyncMenuUISettings() { + ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::Default); + ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SingleScreen); + ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::LargeScreen); + ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::HybridScreen); + ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SideScreen); + ui->action_Screen_Layout_Separate_Windows->setChecked( + Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); + ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue()); + ui->action_Screen_Layout_Upright_Screens->setChecked( + Settings::values.upright_screen.GetValue()); +} + +void GMainWindow::RetranslateStatusBar() { + if (emu_thread) + UpdateStatusBar(); + + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + multiplayer_state->retranslateUi(); +} + +void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(system); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + +#ifdef __unix__ +void GMainWindow::SetGamemodeEnabled(bool state) { + if (emulation_running) { + Common::Linux::SetGamemodeState(state); + } +} +#endif + +#ifdef main +#undef main +#endif + +static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() { +#ifdef _WIN32 + // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. + // This is done by setting the optimal scaling policy for the primary screen. + + // Create a temporary QApplication. + int temp_argc = 0; + char** temp_argv = nullptr; + QApplication temp{temp_argc, temp_argv}; + + // Get the current screen geometry. + const QScreen* primary_screen = QGuiApplication::primaryScreen(); + if (!primary_screen) { + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; + } + + const QRect screen_rect = primary_screen->geometry(); + const qreal real_ratio = primary_screen->devicePixelRatio(); + const qreal real_width = std::trunc(screen_rect.width() * real_ratio); + const qreal real_height = std::trunc(screen_rect.height() * real_ratio); + + // Recommended minimum width and height for proper window fit. + // Any screen with a lower resolution than this will still have a scale of 1. + constexpr qreal minimum_width = 1350.0; + constexpr qreal minimum_height = 900.0; + + const qreal width_ratio = std::max(1.0, real_width / minimum_width); + const qreal height_ratio = std::max(1.0, real_height / minimum_height); + + // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. + const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio)); + + if (max_ratio > real_ratio) { + return Qt::HighDpiScaleFactorRoundingPolicy::Round; + } else { + return Qt::HighDpiScaleFactorRoundingPolicy::Floor; + } +#else + // Other OSes should be better than Windows at fractional scaling. + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; +#endif +} + +int main(int argc, char* argv[]) { + Common::DetachedTasks detached_tasks; + MicroProfileOnThreadCreate("Frontend"); + SCOPE_EXIT({ MicroProfileShutdown(); }); + + // Init settings params + QCoreApplication::setOrganizationName(QStringLiteral("Citra team | Lucina3DS")); + QCoreApplication::setApplicationName(QStringLiteral("Lucina3DS")); + + auto rounding_policy = GetHighDpiRoundingPolicy(); + QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); + +#ifdef __APPLE__ + auto bundle_dir = FileUtil::GetBundleDirectory(); + if (bundle_dir) { + FileUtil::SetCurrentDir(bundle_dir.value() + ".."); + } +#endif + +#ifdef ENABLE_OPENGL + QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); +#endif + + QApplication app(argc, argv); + + // Qt changes the locale and causes issues in float conversion using std::to_string() when + // generating shaders + setlocale(LC_ALL, "C"); + + auto& system{Core::System::GetInstance()}; + + // Register Qt image interface + system.RegisterImageInterface(std::make_shared()); + + GMainWindow main_window(system); + + // Register frontend applets + Frontend::RegisterDefaultApplets(system); + + system.RegisterMiiSelector(std::make_shared(main_window)); + system.RegisterSoftwareKeyboard(std::make_shared(main_window)); + +#ifdef __APPLE__ + // Register microphone permission check. + system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); +#endif + + main_window.setWindowIcon(QIcon("/usr/share/icons/hicolor/512x512/apps/lucina3ds.png")); + main_window.show(); + + QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, + &GMainWindow::OnAppFocusStateChanged); + + int result = app.exec(); + detached_tasks.WaitForAllTasks(); + return result; +} diff --git a/.history/src/lucina3ds_qt/main_20250209234858.cpp b/.history/src/lucina3ds_qt/main_20250209234858.cpp new file mode 100644 index 00000000..6cab297e --- /dev/null +++ b/.history/src/lucina3ds_qt/main_20250209234858.cpp @@ -0,0 +1,3348 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include // for chdir +#endif +#ifdef _WIN32 +#include +#endif +#ifdef __unix__ +#include +#include +#include +#include "common/linux/gamemode.h" +#endif +#include "lucina3ds_qt/aboutdialog.h" +#include "lucina3ds_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/swkbd.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/compatdb.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/discord.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/loading_screen.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/qt_image_interface.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/graphics_device_info.h" +#include "common/arch.h" +#include "common/common_paths.h" +#include "common/detached_tasks.h" +#include "common/dynamic_library/dynamic_library.h" +#include "common/file_util.h" +#include "common/literals.h" +#include "common/logging/backend.h" +#include "common/logging/log.h" +#include "common/memory_detect.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#if LUCINA3DS_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/settings.h" +#include "core/core.h" +#include "core/dumping/backend.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_source_sd_savedata.h" +#include "core/frontend/applets/default_applets.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/nfc/nfc.h" +#include "core/loader/loader.h" +#include "core/movie.h" +#include "core/savestate.h" +#include "core/system_titles.h" +#include "input_common/main.h" +#include "network/network_settings.h" +#include "ui_main.h" +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" + +#ifdef __APPLE__ +#include "common/apple_authorization.h" +#endif + +#ifdef USE_DISCORD_PRESENCE +#include "lucina3ds_qt/discord_impl.h" +#endif + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); +#endif + +#ifdef _WIN32 +extern "C" { +// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +} +#endif + +#ifdef HAVE_SDL2 +#include +#endif + +constexpr int default_mouse_timeout = 2500; + +/** + * "Callouts" are one-time instructional messages shown to the user. In the config settings, there + * is a bitfield "callout_flags" options, used to track if a message has already been shown to the + * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. + */ + +const int GMainWindow::max_recent_files_item; + +static QString PrettyProductName() { +#ifdef _WIN32 + // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 + // With that notation change they changed the registry key used to denote the current version + QSettings windows_registry( + QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), + QSettings::NativeFormat); + const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); + if (release_id == QStringLiteral("2009")) { + const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); + const QString display_version = + windows_registry.value(QStringLiteral("DisplayVersion")).toString(); + const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); + const u32 version = current_build >= 22000 ? 11 : 10; + return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") + .arg(QString::number(version), display_version, QString::number(current_build), + QString::number(ubr)); + } +#endif + return QSysInfo::prettyProductName(); +} + +GMainWindow::GMainWindow(Core::System& system_) + : ui{std::make_unique()}, system{system_}, movie{system.Movie()}, + config{std::make_unique()}, emu_thread{nullptr} { + Common::Log::Initialize(); + Common::Log::Start(); + + Debugger::ToggleConsole(); + +#ifdef __unix__ + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); +#endif + + // register types to use in slots and signals + qRegisterMetaType("std::size_t"); + qRegisterMetaType("Service::AM::InstallStatus"); + + // Register CameraFactory + qt_cameras = std::make_shared(); + Camera::RegisterFactory("image", std::make_unique()); + Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + + LoadTranslation(); + + Pica::g_debug_context = Pica::DebugContext::Construct(); + setAcceptDrops(true); + ui->setupUi(this); + statusBar()->hide(); + + default_theme_paths = QIcon::themeSearchPaths(); + UpdateUITheme(); + + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(); + + Network::Init(); + + movie.SetPlaybackCompletionCallback([this] { + QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); + }); + + InitializeWidgets(); + InitializeDebugWidgets(); + InitializeRecentFileMenuActions(); + InitializeSaveStateMenuActions(); + InitializeHotkeys(); +#if ENABLE_QT_UPDATER + ShowUpdaterWidgets(); +#else + ui->action_Check_For_Updates->setVisible(false); + ui->action_Open_Maintenance_Tool->setVisible(false); +#endif + + SetDefaultUIGeometry(); + RestoreUIState(); + + ConnectAppEvents(); + ConnectMenuEvents(); + ConnectWidgetEvents(); + + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + Common::g_scm_desc); +#if LUCINA3DS_ARCH(x86_64) + const auto& caps = Common::GetCPUCaps(); + std::string cpu_string = caps.cpu_string; + if (caps.avx || caps.avx2 || caps.avx512) { + cpu_string += " | AVX"; + if (caps.avx512) { + cpu_string += "512"; + } else if (caps.avx2) { + cpu_string += '2'; + } + if (caps.fma || caps.fma4) { + cpu_string += " | FMA"; + } + } + LOG_INFO(Frontend, "Host CPU: {}", cpu_string); +#endif + LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); + const auto& mem_info = Common::GetMemInfo(); + using namespace Common::Literals; + LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB}); + LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB}); + UpdateWindowTitle(); + + show(); + + game_list->LoadCompatibilityList(); + game_list->PopulateAsync(UISettings::values.game_dirs); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); + +#ifdef ENABLE_OPENGL + gl_renderer = GetOpenGLRenderer(); +#if defined(_WIN32) + if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { + // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ + // We can override the version here to allow Citra to work. + // TODO: Remove this when OpenGL 4.0+ is advertised. + qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); + } +#endif +#endif + +#ifdef ENABLE_VULKAN + physical_devices = GetVulkanPhysicalDevices(); + if (physical_devices.empty()) { + QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), + tr("Vulkan initialization failed during boot.
" + "Your GPU may not support Vulkan 1.1, or you do not " + "have the latest graphics driver.")); + } +#endif + +#if ENABLE_QT_UPDATER + if (UISettings::values.check_for_update_on_start) { + CheckForUpdates(); + } +#endif + + QStringList args = QApplication::arguments(); + if (args.size() < 2) { + return; + } + + QString game_path; + for (int i = 1; i < args.size(); ++i) { + // Preserves drag/drop functionality + if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { + game_path = args[1]; + break; + } + + // Launch game in fullscreen mode + if (args[i] == QStringLiteral("-f")) { + ui->action_Fullscreen->setChecked(true); + continue; + } + + // Launch game in windowed mode + if (args[i] == QStringLiteral("-w")) { + ui->action_Fullscreen->setChecked(false); + continue; + } + + // Launch game at path + if (args[i] == QStringLiteral("-g")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + game_path = args[++i]; + } + } + + if (!game_path.isEmpty()) { + BootGame(game_path); + } +} + +GMainWindow::~GMainWindow() { + // Will get automatically deleted otherwise + if (!render_window->parent()) { + delete render_window; + } + + Pica::g_debug_context.reset(); + Network::Shutdown(); +} + +void GMainWindow::InitializeWidgets() { +#ifdef LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING + ui->action_Report_Compatibility->setVisible(true); +#endif + render_window = new GRenderWindow(this, emu_thread.get(), system, false); + secondary_window = new GRenderWindow(this, emu_thread.get(), system, true); + render_window->hide(); + secondary_window->hide(); + secondary_window->setParent(nullptr); + + game_list = new GameList(this); + ui->horizontalLayout->addWidget(game_list); + + game_list_placeholder = new GameListPlaceholder(this); + ui->horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui->horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, this, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + render_window->activateWindow(); + } + }); + + InputCommon::Init(); + multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(), + ui->action_Leave_Room, ui->action_Show_Room); + multiplayer_state->setVisible(false); + +#if ENABLE_QT_UPDATER + // Setup updater + updater = new Updater(this); + UISettings::values.updater_found = updater->HasUpdater(); +#endif + + UpdateBootHomeMenuState(); + + // Create status bar + message_label = new QLabel(); + // Configured separately for left alignment + message_label->setFrameStyle(QFrame::NoFrame); + message_label->setContentsMargins(4, 0, 4, 0); + message_label->setAlignment(Qt::AlignLeft); + statusBar()->addPermanentWidget(message_label, 1); + + progress_bar = new QProgressBar(); + progress_bar->hide(); + statusBar()->addPermanentWidget(progress_bar); + + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + + emu_speed_label = new QLabel(); + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label = new QLabel(); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label = new QLabel(); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { + label->setVisible(false); + label->setFrameStyle(QFrame::NoFrame); + label->setContentsMargins(4, 0, 4, 0); + statusBar()->addPermanentWidget(label); + } + + // Setup Graphics API button + graphics_api_button = new QPushButton(); + graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton")); + graphics_api_button->setFocusPolicy(Qt::NoFocus); + UpdateAPIIndicator(); + + connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); }); + + statusBar()->insertPermanentWidget(0, graphics_api_button); + + volume_popup = new QWidget(this); + volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); + volume_popup->setLayout(new QVBoxLayout()); + volume_popup->setMinimumWidth(200); + + volume_slider = new QSlider(Qt::Horizontal); + volume_slider->setObjectName(QStringLiteral("volume_slider")); + volume_slider->setMaximum(100); + volume_slider->setPageStep(5); + connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { + Settings::values.audio_muted = false; + const auto value = static_cast(percentage) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); + }); + volume_popup->layout()->addWidget(volume_slider); + + volume_button = new QPushButton(); + volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + volume_button->setFocusPolicy(Qt::NoFocus); + volume_button->setCheckable(true); + UpdateVolumeUI(); + connect(volume_button, &QPushButton::clicked, this, [&] { + UpdateVolumeUI(); + volume_popup->setVisible(!volume_popup->isVisible()); + QRect rect = volume_button->geometry(); + QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); + bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); + volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); + }); + statusBar()->insertPermanentWidget(1, volume_button); + + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + + statusBar()->setVisible(true); + + // Removes an ugly inner border from the status bar widgets under Linux + setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); + + QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows); +} + +void GMainWindow::InitializeDebugWidgets() { + connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this, + &GMainWindow::OnCreateGraphicsSurfaceViewer); + + QMenu* debug_menu = ui->menu_View_Debugging; + +#if MICROPROFILE_ENABLED + microProfileDialog = new MicroProfileDialog(this); + microProfileDialog->hide(); + debug_menu->addAction(microProfileDialog->toggleViewAction()); +#endif + + registersWidget = new RegistersWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, registersWidget); + registersWidget->hide(); + debug_menu->addAction(registersWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, registersWidget, + &RegistersWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, registersWidget, + &RegistersWidget::OnEmulationStopping); + + graphicsWidget = new GPUCommandStreamWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsWidget); + graphicsWidget->hide(); + debug_menu->addAction(graphicsWidget->toggleViewAction()); + + graphicsCommandsWidget = new GPUCommandListWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); + graphicsCommandsWidget->hide(); + debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); + + graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); + graphicsBreakpointsWidget->hide(); + debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); + + graphicsVertexShaderWidget = + new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget); + graphicsVertexShaderWidget->hide(); + debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction()); + + graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget); + graphicsTracingWidget->hide(); + debug_menu->addAction(graphicsTracingWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStopping); + + waitTreeWidget = new WaitTreeWidget(system, this); + addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget); + waitTreeWidget->hide(); + debug_menu->addAction(waitTreeWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, waitTreeWidget, + &WaitTreeWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + &WaitTreeWidget::OnEmulationStopping); + + lleServiceModulesWidget = new LLEServiceModulesWidget(this); + addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget); + lleServiceModulesWidget->hide(); + debug_menu->addAction(lleServiceModulesWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, + [this] { lleServiceModulesWidget->setDisabled(true); }); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + [this] { lleServiceModulesWidget->setDisabled(false); }); + + ipcRecorderWidget = new IPCRecorderWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); + ipcRecorderWidget->hide(); + debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, + &IPCRecorderWidget::OnEmulationStarting); +} + +void GMainWindow::InitializeRecentFileMenuActions() { + for (int i = 0; i < max_recent_files_item; ++i) { + actions_recent_files[i] = new QAction(this); + actions_recent_files[i]->setVisible(false); + connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); + + ui->menu_recent_files->addAction(actions_recent_files[i]); + } + ui->menu_recent_files->addSeparator(); + QAction* action_clear_recent_files = new QAction(this); + action_clear_recent_files->setText(tr("Clear Recent Files")); + connect(action_clear_recent_files, &QAction::triggered, this, [this] { + UISettings::values.recent_files.clear(); + UpdateRecentFiles(); + }); + ui->menu_recent_files->addAction(action_clear_recent_files); + + UpdateRecentFiles(); +} + +void GMainWindow::InitializeSaveStateMenuActions() { + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i] = new QAction(this); + actions_load_state[i]->setData(i + 1); + connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); + ui->menu_Load_State->addAction(actions_load_state[i]); + + actions_save_state[i] = new QAction(this); + actions_save_state[i]->setData(i + 1); + connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); + ui->menu_Save_State->addAction(actions_save_state[i]); + } + + connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + if (newest_slot != 0) { + actions_load_state[newest_slot - 1]->trigger(); + } + }); + connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + actions_save_state[oldest_slot - 1]->trigger(); + }); + + connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + + UpdateSaveStates(); +} + +void GMainWindow::InitializeHotkeys() { + hotkey_registry.LoadHotkeys(); + + const QString main_window = QStringLiteral("Main Window"); + const QString fullscreen = QStringLiteral("Fullscreen"); + const QString toggle_screen_layout = QStringLiteral("Toggle Screen Layout"); + const QString swap_screens = QStringLiteral("Swap Screens"); + const QString rotate_screens = QStringLiteral("Rotate Screens Upright"); + + const auto link_action_shortcut = [&](QAction* action, const QString& action_name) { + static const QString main_window = QStringLiteral("Main Window"); + action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); + action->setShortcutContext(hotkey_registry.GetShortcutContext(main_window, action_name)); + action->setAutoRepeat(false); + this->addAction(action); + }; + + link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); + link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); + link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); + link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lucina3DS")); + link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); + link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); + link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); + link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); + link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + link_action_shortcut(ui->action_Fullscreen, fullscreen); + link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, swap_screens); + link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, rotate_screens); + link_action_shortcut(ui->action_Enable_Frame_Advancing, + QStringLiteral("Toggle Frame Advancing")); + link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame")); + link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); + link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); + link_action_shortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); + link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); + link_action_shortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); + link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); + link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); + + const auto add_secondary_window_hotkey = [this](QKeySequence hotkey, const char* slot) { + // This action will fire specifically when secondary_window is in focus + QAction* secondary_window_action = new QAction(secondary_window); + secondary_window_action->setShortcut(hotkey); + + connect(secondary_window_action, SIGNAL(triggered()), this, slot); + secondary_window->addAction(secondary_window_action); + }; + + // Use the same fullscreen hotkey as the main window + const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen); + add_secondary_window_hotkey(fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen())); + + const auto toggle_screen_hotkey = + hotkey_registry.GetKeySequence(main_window, toggle_screen_layout); + add_secondary_window_hotkey(toggle_screen_hotkey, SLOT(ToggleScreenLayout())); + + const auto swap_screen_hotkey = hotkey_registry.GetKeySequence(main_window, swap_screens); + add_secondary_window_hotkey(swap_screen_hotkey, SLOT(TriggerSwapScreens())); + + const auto rotate_screen_hotkey = hotkey_registry.GetKeySequence(main_window, rotate_screens); + add_secondary_window_hotkey(rotate_screen_hotkey, SLOT(TriggerRotateScreens())); + + const auto connect_shortcut = [&](const QString& action_name, const auto& function) { + const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this); + connect(hotkey, &QShortcut::activated, this, function); + }; + + connect(hotkey_registry.GetHotkey(main_window, toggle_screen_layout, render_window), + &QShortcut::activated, this, &GMainWindow::ToggleScreenLayout); + + connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { + if (emulation_running) { + ui->action_Fullscreen->setChecked(false); + ToggleFullscreen(); + } + }); + connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] { + Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Toggle Texture Dumping"), + [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + connect_shortcut(QStringLiteral("Toggle Custom Textures"), + [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes + // the variable hold a garbage value after this function exits + static constexpr u16 SPEED_LIMIT_STEP = 5; + connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + return; + } + if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() + + SPEED_LIMIT_STEP); + } else { + Settings::values.frame_limit = 0; + } + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + Settings::values.frame_limit = 995; + } else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() - + SPEED_LIMIT_STEP); + UpdateStatusBar(); + } + UpdateStatusBar(); + }); + + connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); + connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); + connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); + + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the + // variable hold a garbage value after this function exits + static constexpr u16 FACTOR_3D_STEP = 5; + connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d > 0) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); + connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d < 100) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = + factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); +} + +void GMainWindow::SetDefaultUIGeometry() { + // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half + const QRect screenRect = screen()->geometry(); + + const int w = screenRect.width() * 2 / 3; + const int h = screenRect.height() / 2; + const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; + const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100; + + setGeometry(x, y, w, h); +} + +void GMainWindow::RestoreUIState() { + restoreGeometry(UISettings::values.geometry); + restoreState(UISettings::values.state); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); +#if MICROPROFILE_ENABLED + microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); + microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); +#endif + + game_list->LoadInterfaceLayout(); + + ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); + ToggleWindowMode(); + + ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); + SyncMenuUISettings(); + + ui->action_Display_Dock_Widget_Headers->setChecked( + UISettings::values.display_titlebar.GetValue()); + OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked()); + + ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + + ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); +} + +void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { + if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && + state != Qt::ApplicationActive) { + LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); + } + if (!emulation_running) { + return; + } + if (UISettings::values.pause_when_in_background) { + if (emu_thread->IsRunning() && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + auto_paused = true; + OnPauseGame(); + } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { + auto_paused = false; + OnStartGame(); + } + } + if (UISettings::values.mute_when_in_background) { + if (!Settings::values.audio_muted && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + Settings::values.audio_muted = true; + auto_muted = true; + } else if (auto_muted && state == Qt::ApplicationActive) { + Settings::values.audio_muted = false; + auto_muted = false; + } + UpdateVolumeUI(); + } +} + +bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::FileOpen) { + emit FileOpen(static_cast(event)); + return true; + } + return false; +} + +void GMainWindow::ConnectAppEvents() { + const auto filter = new GApplicationEventFilter(); + QGuiApplication::instance()->installEventFilter(filter); + + connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen); +} + +void GMainWindow::ConnectWidgetEvents() { + connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); + connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, + &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); + connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &GMainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, this, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); + + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, + &GMainWindow::OnGameListOpenPerGameProperties); + + connect(this, &GMainWindow::EmulationStarting, render_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, render_window, + &GRenderWindow::OnEmulationStopping); + connect(this, &GMainWindow::EmulationStarting, secondary_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, secondary_window, + &GRenderWindow::OnEmulationStopping); + + connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); + connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); + connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); +} + +void GMainWindow::ConnectMenuEvents() { + const auto connect_menu = [&](QAction* action, const auto& event_fn) { + connect(action, &QAction::triggered, this, event_fn); + // Add actions to this window so that hiding menus in fullscreen won't disable them + addAction(action); + // Add actions to the render window so that they work outside of single window mode + render_window->addAction(action); + }; + + // File + connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); + connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), + [this, region] { OnMenuBootHomeMenu(region); }); + } + connect_menu(ui->action_Exit, &QMainWindow::close); + connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); + connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo); + + // Emulation + connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); + connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); + connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); }); + connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); + connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); + connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); + + // View + connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); + connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); + connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); + connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); + + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + + connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); + connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens); + connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens); + + // Movie + connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie); + connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie); + connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie); + connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie); + connect_menu(ui->action_Movie_Read_Only_Mode, + [this](bool checked) { movie.SetReadOnly(checked); }); + connect_menu(ui->action_Enable_Frame_Advancing, [this] { + if (emulation_running) { + system.frame_limiter.SetFrameAdvancing(ui->action_Enable_Frame_Advancing->isChecked()); + ui->action_Advance_Frame->setEnabled(ui->action_Enable_Frame_Advancing->isChecked()); + } + }); + connect_menu(ui->action_Advance_Frame, [this] { + if (emulation_running && system.frame_limiter.IsFrameAdvancing()) { + ui->action_Enable_Frame_Advancing->setChecked(true); + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.AdvanceFrame(); + } + }); + connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); + connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); + + // Help + connect_menu(ui->action_Open_Lucina3DS_Folder, &GMainWindow::OnOpenLucina3DSFolder); + connect_menu(ui->action_Open_Log_Folder, []() { + QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + connect_menu(ui->action_FAQ, []() { + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); + }); + connect_menu(ui->action_About, &GMainWindow::OnMenuAboutLucina3DS); + +#if ENABLE_QT_UPDATER + connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); + connect_menu(ui->action_Open_Maintenance_Tool, &GMainWindow::OnOpenUpdater); +#endif +} + +void GMainWindow::UpdateMenuState() { + const bool is_paused = !emu_thread || !emu_thread->IsRunning(); + + const std::array running_actions{ + ui->action_Stop, + ui->action_Restart, + ui->action_Configure_Current_Game, + ui->action_Report_Compatibility, + ui->action_Load_Amiibo, + ui->action_Remove_Amiibo, + ui->action_Pause, + ui->action_Advance_Frame, + }; + + for (QAction* action : running_actions) { + action->setEnabled(emulation_running); + } + + ui->action_Capture_Screenshot->setEnabled(emulation_running); + + if (emulation_running && is_paused) { + ui->action_Pause->setText(tr("&Continue")); + } else { + ui->action_Pause->setText(tr("&Pause")); + } +} + +void GMainWindow::OnDisplayTitleBars(bool show) { + QList widgets = findChildren(); + + if (show) { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(nullptr); + if (old) { + delete old; + } + } + } else { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(new QWidget()); + if (old) { + delete old; + } + } + } +} + +#if ENABLE_QT_UPDATER +void GMainWindow::OnCheckForUpdates() { + explicit_update_check = true; + CheckForUpdates(); +} + +void GMainWindow::CheckForUpdates() { + if (updater->CheckForUpdates()) { + LOG_INFO(Frontend, "Update check started"); + } else { + LOG_WARNING(Frontend, "Unable to start check for updates"); + } +} + +void GMainWindow::OnUpdateFound(bool found, bool error) { + if (error) { + LOG_WARNING(Frontend, "Update check failed"); + return; + } + + if (!found) { + LOG_INFO(Frontend, "No updates found"); + + // If the user explicitly clicked the "Check for Updates" button, we are + // going to want to show them a prompt anyway. + if (explicit_update_check) { + explicit_update_check = false; + ShowNoUpdatePrompt(); + } + return; + } + + if (emulation_running && !explicit_update_check) { + LOG_INFO(Frontend, "Update found, deferring as game is running"); + defer_update_prompt = true; + return; + } + + LOG_INFO(Frontend, "Update found!"); + explicit_update_check = false; + + ShowUpdatePrompt(); +} + +void GMainWindow::ShowUpdatePrompt() { + defer_update_prompt = false; + + auto result = + QMessageBox::question(this, tr("Update Available"), + tr("An update is available. Would you like to install it now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + updater->LaunchUIOnExit(); + close(); + } +} + +void GMainWindow::ShowNoUpdatePrompt() { + QMessageBox::information(this, tr("No Update Found"), tr("No update is found."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void GMainWindow::OnOpenUpdater() { + updater->LaunchUI(); +} + +void GMainWindow::ShowUpdaterWidgets() { + ui->action_Check_For_Updates->setVisible(UISettings::values.updater_found); + ui->action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found); + + connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound); +} +#endif + +#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) +static std::optional HoldWakeLockLinux(u32 window_id = 0) { + if (!QDBusConnection::sessionBus().isConnected()) { + return {}; + } + // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit + QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Inhibit")); + if (!xdp.isValid()) { + LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); + return {}; + } + QVariantMap options = {}; + //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the + //: computer from sleeping + options.insert(QString::fromLatin1("reason"), + QCoreApplication::translate("GMainWindow", "Luinca3DS is running a game")); + // 0x4: Suspend lock; 0x8: Idle lock + QDBusReply reply = + xdp.call(QString::fromLatin1("Inhibit"), + QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); + + if (reply.isValid()) { + return reply.value(); + } + LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", + reply.error().message().toStdString()); + return {}; +} + +static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { + if (!QDBusConnection::sessionBus().isConnected()) { + return; + } + QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), + QString::fromLatin1("org.freedesktop.portal.Request")); + unlocker.call(QString::fromLatin1("Close")); +} +#endif // __unix__ + +void GMainWindow::PreventOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + auto reply = HoldWakeLockLinux(winId()); + if (reply) { + wake_lock = std::move(reply.value()); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +void GMainWindow::AllowOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(HAVE_SDL2) + SDL_EnableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + if (!wake_lock.path().isEmpty()) { + ReleaseWakeLockLinux(wake_lock); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +bool GMainWindow::LoadROM(const QString& filename) { + // Shutdown previous session if the emu thread is still active... + if (emu_thread) { + ShutdownGame(); + } + + if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) { + LOG_CRITICAL(Frontend, "Failed to initialize render targets!"); + return false; + } + + const auto scope = render_window->Acquire(); + + const Core::System::ResultStatus result{ + system.Load(*render_window, filename.toStdString(), secondary_window)}; + + if (result != Core::System::ResultStatus::Success) { + switch (result) { + case Core::System::ResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorSystemMode: + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + QMessageBox::critical( + this, tr("ROM Corrupted"), + tr("Your ROM is corrupted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { + QMessageBox::critical( + this, tr("ROM Encrypted"), + tr("Your ROM is encrypted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + } + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + QMessageBox::critical(this, tr("Unsupported ROM"), + tr("GBA Virtual Console ROMs are not supported by Lucina3DS.")); + break; + + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr(fmt::format( + "An error has occurred whilst communicating with the Artic Base Server.\n{}", + system.GetStatusDetails()) + .c_str())); + break; + default: + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + break; + } + return false; + } + + std::string title; + system.GetAppLoader().ReadTitle(title); + game_title = QString::fromStdString(title); + UpdateWindowTitle(); + + game_path = filename; + + return true; +} + +void GMainWindow::BootGame(const QString& filename) { + if (emu_thread) { + ShutdownGame(); + } + + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { + const auto answer = QMessageBox::question( + this, tr("CIA must be installed before usage"), + tr("Before using this CIA, you must install it. Do you want to install it now?"), + QMessageBox::Yes | QMessageBox::No); + + if (answer == QMessageBox::Yes) + InstallCIA(QStringList(filename)); + + return; + } + + show_artic_label = is_artic; + + LOG_INFO(Frontend, "Lucinca3DS starting..."); + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } + + if (movie_record_on_start) { + movie.PrepareForRecording(); + } + if (movie_playback_on_start) { + movie.PrepareForPlayback(movie_playback_path.toStdString()); + } + + const std::string path = filename.toStdString(); + auto loader = Loader::GetLoader(path); + + u64 title_id{0}; + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } + + system.ApplySettings(); + + Settings::LogSettings(); + + // Save configurations + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + config->Save(); + + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); + return; + } + + // Set everything up + if (movie_record_on_start) { + movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (movie_playback_on_start) { + movie.StartPlayback(movie_playback_path.toStdString()); + movie_playback_on_start = false; + movie_playback_path.clear(); + } + + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + + if (video_dumping_on_start) { + StartVideoDumping(video_dumping_path); + video_dumping_on_start = false; + video_dumping_path.clear(); + } + + // Register debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Register(); + } + + // Create and start the emulation thread + emu_thread = std::make_unique(system, *render_window); + emit EmulationStarting(emu_thread.get()); + emu_thread->start(); + + connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + + // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views + // before the CPU continues + connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget, + &RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget, + &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget, + &RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, + &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, + &LoadingScreen::OnLoadComplete); + + // Update the GUI + registersWidget->OnDebugModeEntered(); + if (ui->action_Single_Window_Mode->isChecked()) { + game_list->hide(); + game_list_placeholder->hide(); + } + status_bar_update_timer.start(1000); + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + } + + loading_screen->Prepare(system.GetAppLoader()); + loading_screen->show(); + + emulation_running = true; + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } + + OnStartGame(); +} + +void GMainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + if (ui->action_Fullscreen->isChecked()) { + HideFullscreen(); + } + + auto video_dumper = system.GetVideoDumper(); + if (video_dumper && video_dumper->IsDumping()) { + game_shutdown_delayed = true; + OnStopVideoDumping(); + return; + } + + AllowOSSleep(); + + discord_rpc->Pause(); + emu_thread->RequestStop(); + + // Release emu threads from any breakpoints + // This belongs after RequestStop() and before wait() because if emulation stops on a GPU + // breakpoint after (or before) RequestStop() is called, the emulation would never be able + // to continue out to the main loop and terminate. Thus wait() would hang forever. + // TODO(bunnei): This function is not thread safe, but it's being used as if it were + Pica::g_debug_context->ClearBreakpoints(); + + // Unregister debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Unregister(); + } + + // Frame advancing must be cancelled in order to release the emu thread from waiting + system.frame_limiter.SetFrameAdvancing(false); + + emit EmulationStopping(); + + // Wait for emulation thread to complete and delete it + emu_thread->wait(); + emu_thread = nullptr; + + OnCloseMovie(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif + + // The emulation is stopped, so closing the window or not does not matter anymore + disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + + render_window->hide(); + secondary_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); + + if (game_list->IsEmpty()) { + game_list_placeholder->show(); + } else { + game_list->show(); + } + game_list->SetFilterFocus(); + + setMouseTracking(false); + + // Disable status bar updates + status_bar_update_timer.stop(); + message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); + emu_speed_label->setVisible(false); + game_fps_label->setVisible(false); + emu_frametime_label->setVisible(false); + + UpdateSaveStates(); + + emulation_running = false; + +#if ENABLE_QT_UDPATER + if (defer_update_prompt) { + ShowUpdatePrompt(); + } +#endif + + game_title.clear(); + UpdateWindowTitle(); + + game_path.clear(); + + // Update the GUI + UpdateMenuState(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); +} + +void GMainWindow::StoreRecentFile(const QString& filename) { + UISettings::values.recent_files.prepend(filename); + UISettings::values.recent_files.removeDuplicates(); + while (UISettings::values.recent_files.size() > max_recent_files_item) { + UISettings::values.recent_files.removeLast(); + } + + UpdateRecentFiles(); +} + +void GMainWindow::UpdateRecentFiles() { + const int num_recent_files = + std::min(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); + + for (int i = 0; i < num_recent_files; i++) { + const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( + QFileInfo(UISettings::values.recent_files[i]).fileName()); + actions_recent_files[i]->setText(text); + actions_recent_files[i]->setData(UISettings::values.recent_files[i]); + actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); + actions_recent_files[i]->setVisible(true); + } + + for (int j = num_recent_files; j < max_recent_files_item; ++j) { + actions_recent_files[j]->setVisible(false); + } + + // Enable the recent files menu if the list isn't empty + ui->menu_recent_files->setEnabled(num_recent_files != 0); +} + +void GMainWindow::UpdateSaveStates() { + if (!system.IsPoweredOn()) { + ui->menu_Load_State->setEnabled(false); + ui->menu_Save_State->setEnabled(false); + return; + } + + ui->menu_Load_State->setEnabled(true); + ui->menu_Save_State->setEnabled(true); + ui->action_Load_from_Newest_Slot->setEnabled(false); + + oldest_slot = newest_slot = 0; + oldest_slot_time = std::numeric_limits::max(); + newest_slot_time = 0; + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return; + } + auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID()); + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i]->setEnabled(false); + actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); + actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); + } + for (const auto& savestate : savestates) { + const bool display_name = + savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch && + !savestate.build_name.empty(); + const auto text = + tr("Slot %1 - %2 %3") + .arg(savestate.slot) + .arg(QDateTime::fromSecsSinceEpoch(savestate.time) + .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))) + .arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String()) + .trimmed(); + + actions_load_state[savestate.slot - 1]->setEnabled(true); + actions_load_state[savestate.slot - 1]->setText(text); + actions_save_state[savestate.slot - 1]->setText(text); + + ui->action_Load_from_Newest_Slot->setEnabled(true); + + if (savestate.time > newest_slot_time) { + newest_slot = savestate.slot; + newest_slot_time = savestate.time; + } + if (savestate.time < oldest_slot_time) { + oldest_slot = savestate.slot; + oldest_slot_time = savestate.time; + } + } + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + if (!actions_load_state[i]->isEnabled()) { + // Prefer empty slot + oldest_slot = i + 1; + oldest_slot_time = 0; + break; + } + } +} + +void GMainWindow::OnGameListLoadFile(QString game_path) { + if (ConfirmChangeGame()) { + BootGame(game_path); + } +} + +void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { + std::string path; + std::string open_target; + + switch (target) { + case GameListOpenTarget::SAVE_DATA: { + open_target = "Save Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::EXT_DATA: { + open_target = "Extra Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::APPLICATION: { + open_target = "Application"; + auto media_type = Service::AM::GetTitleMediaType(data_id); + path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; + break; + } + case GameListOpenTarget::UPDATE_DATA: { + open_target = "Update Data"; + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + + "content/"; + break; + } + case GameListOpenTarget::TEXTURE_DUMP: { + open_target = "Dumped Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); + break; + } + case GameListOpenTarget::TEXTURE_LOAD: { + open_target = "Custom Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); + break; + } + case GameListOpenTarget::MODS: { + open_target = "Mods"; + path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + data_id); + break; + } + case GameListOpenTarget::DLC_DATA: { + open_target = "DLC Data"; + path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/0004008c/{:08x}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id); + break; + } + case GameListOpenTarget::SHADER_CACHE: { + open_target = "Shader Cache"; + path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir); + break; + } + default: + LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); + return; + } + + QString qpath = QString::fromStdString(path); + + QDir dir(qpath); + if (!dir.exists()) { + QMessageBox::critical( + this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)), + tr("Folder does not exist!")); + return; + } + + LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); + + QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); +} + +void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + QString directory; + if (it != compatibility_list.end()) + directory = it->second.second; + + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory)); +} + +void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { + auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setWindowFlags(dialog->windowFlags() & + ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); + dialog->setCancelButton(nullptr); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + const auto base_path = fmt::format( + "{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); + const auto update_path = + fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + program_id | 0x0004000e00000000); + using FutureWatcher = QFutureWatcher>; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, + [this, dialog, base_path, update_path, future_watcher] { + dialog->hide(); + const auto& [base, update] = future_watcher->result(); + if (base != Loader::ResultStatus::Success) { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not dump base RomFS.\nRefer to the log for details.")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path))); + if (update == Loader::ResultStatus::Success) { + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(update_path))); + } + }); + + auto future = QtConcurrent::run([game_path, base_path, update_path] { + std::unique_ptr loader = Loader::GetLoader(game_path.toStdString()); + return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path)); + }); + future_watcher->setFuture(future); +} + +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { + QString path; + if (directory == QStringLiteral("INSTALLED")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); + } else if (directory == QStringLiteral("SYSTEM")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000/title/00040010"); + } else { + path = directory; + } + if (!QFileInfo::exists(path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) + return; + UISettings::GameDir game_dir{dir_path, false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } +} + +void GMainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui->action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + +void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { + const auto loader = Loader::GetLoader(file.toStdString()); + + u64 title_id{}; + if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { + QMessageBox::information(this, tr("Properties"), + tr("The game properties could not be loaded.")); + return; + } + + OpenPerGameConfiguration(title_id, file); +} + +void GMainWindow::OnMenuLoadFile() { + const QString extensions = QStringLiteral("*.").append( + GameList::supported_file_extensions.join(QStringLiteral(" *."))); + const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)", + "%1 is an identifier for the 3DS executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), UISettings::values.roms_path, file_filter); + + if (filename.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + +void GMainWindow::OnMenuInstallCIA() { + QStringList filepaths = QFileDialog::getOpenFileNames( + this, tr("Load Files"), UISettings::values.roms_path, + tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)")); + + if (filepaths.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filepaths[0]).path(); + InstallCIA(filepaths); +} + +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + +void GMainWindow::OnMenuBootHomeMenu(u32 region) { + BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); +} + +void GMainWindow::InstallCIA(QStringList filepaths) { + ui->action_Install_CIA->setEnabled(false); + game_list->SetDirectoryWatcherEnabled(false); + progress_bar->show(); + progress_bar->setMaximum(INT_MAX); + + (void)QtConcurrent::run([&, filepaths] { + Service::AM::InstallStatus status; + const auto cia_progress = [&](std::size_t written, std::size_t total) { + emit UpdateProgress(written, total); + }; + for (const auto& current_path : filepaths) { + status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress); + emit CIAInstallReport(status, current_path); + } + emit CIAInstallFinished(); + }); +} + +void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) { + progress_bar->setValue( + static_cast(INT_MAX * (static_cast(written) / static_cast(total)))); +} + +void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) { + QString filename = QFileInfo(filepath).fileName(); + switch (status) { + case Service::AM::InstallStatus::Success: + this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFailedToOpenFile: + QMessageBox::critical(this, tr("Unable to open File"), + tr("Could not open %1").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorAborted: + QMessageBox::critical( + this, tr("Installation aborted"), + tr("The installation of %1 was aborted. Please see the log for more details") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorInvalid: + QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorEncrypted: + QMessageBox::critical(this, tr("Encrypted File"), + tr("%1 must be decrypted " + "before being used with Lucina3DS. A real 3DS is required.") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFileNotFound: + QMessageBox::critical(this, tr("Unable to find File"), + tr("Could not find %1").arg(filename)); + break; + } +} + +void GMainWindow::OnCIAInstallFinished() { + progress_bar->hide(); + progress_bar->setValue(0); + game_list->SetDirectoryWatcherEnabled(true); + ui->action_Install_CIA->setEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void GMainWindow::UninstallTitles( + const std::vector>& titles) { + if (titles.empty()) { + return; + } + + // Select the first title in the list as representative. + const auto first_name = std::get(titles[0]); + + QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, + static_cast(titles.size()), this); + progress.setWindowModality(Qt::WindowModal); + + QFutureWatcher future_watcher; + QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, + &QProgressDialog::reset); + QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, + &QProgressDialog::setValue); + + auto failed = false; + QString failed_name; + + const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { + const auto name = std::get(title); + const auto media_type = std::get(title); + const auto program_id = std::get(title); + + const auto result = Service::AM::UninstallProgram(media_type, program_id); + if (result.IsError()) { + LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), + result.raw); + failed = true; + failed_name = name; + future_watcher.cancel(); + } + }; + + future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); + progress.exec(); + future_watcher.waitForFinished(); + + if (failed) { + QMessageBox::critical(this, tr("Lucina3DS"), tr("Failed to uninstall '%1'.").arg(failed_name)); + } else if (!future_watcher.isCanceled()) { + QMessageBox::information(this, tr("Lucina3DS"), + tr("Successfully uninstalled '%1'.").arg(first_name)); + } +} + +void GMainWindow::OnMenuRecentFile() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + const QString filename = action->data().toString(); + if (QFileInfo::exists(filename)) { + BootGame(filename); + } else { + // Display an error message and remove the file from the list. + QMessageBox::information(this, tr("File not found"), + tr("File \"%1\" not found").arg(filename)); + + UISettings::values.recent_files.removeOne(filename); + UpdateRecentFiles(); + } +} + +void GMainWindow::OnStartGame() { + qt_cameras->ResumeCameras(); + + PreventOSSleep(); + + emu_thread->SetRunning(true); + graphics_api_button->setEnabled(false); + qRegisterMetaType("Core::System::ResultStatus"); + qRegisterMetaType("std::string"); + connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); + + UpdateMenuState(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StartGamemode(); +#endif + + UpdateSaveStates(); + UpdateStatusButtons(); +} + +void GMainWindow::OnRestartGame() { + if (!system.IsPoweredOn()) { + return; + } + // Make a copy since BootGame edits game_path + BootGame(QString(game_path)); +} + +void GMainWindow::OnPauseGame() { + emu_thread->SetRunning(false); + qt_cameras->PauseCameras(); + + UpdateMenuState(); + AllowOSSleep(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif +} + +void GMainWindow::OnPauseContinueGame() { + if (emulation_running) { + if (emu_thread->IsRunning()) { + OnPauseGame(); + } else { + OnStartGame(); + } + } +} + +void GMainWindow::OnStopGame() { + ShutdownGame(); + graphics_api_button->setEnabled(true); + Settings::RestoreGlobalState(false); + UpdateStatusButtons(); +} + +void GMainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnMenuReportCompatibility() { + if (!NetSettings::values.lucina3ds_token.empty() && !NetSettings::values.lucina3ds_username.empty()) { + CompatDB compatdb{this}; + compatdb.exec(); + } else { + QMessageBox::critical(this, tr("Missing Citra Account"), + tr("You must link your Citra account to submit test cases." + "
Go to Emulation > Configure... > Web to do so.")); + } +} + +void GMainWindow::ToggleFullscreen() { + if (!emulation_running) { + return; + } + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } else { + HideFullscreen(); + } +} + +void GMainWindow::ToggleSecondaryFullscreen() { + if (!emulation_running) { + return; + } + if (secondary_window->isFullScreen()) { + secondary_window->showNormal(); + } else { + secondary_window->showFullScreen(); + } +} + +void GMainWindow::ShowFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + UISettings::values.geometry = saveGeometry(); + ui->menubar->hide(); + statusBar()->hide(); + showFullScreen(); + } else { + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + render_window->showFullScreen(); + } +} + +void GMainWindow::HideFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->menubar->show(); + showNormal(); + restoreGeometry(UISettings::values.geometry); + } else { + render_window->showNormal(); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } +} + +void GMainWindow::ToggleWindowMode() { + if (ui->action_Single_Window_Mode->isChecked()) { + // Render in the main window... + render_window->BackupGeometry(); + ui->horizontalLayout->addWidget(render_window); + render_window->setFocusPolicy(Qt::StrongFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->setFocus(); + game_list->hide(); + } + + } else { + // Render in a separate window... + ui->horizontalLayout->removeWidget(render_window); + render_window->setParent(nullptr); + render_window->setFocusPolicy(Qt::NoFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->RestoreGeometry(); + game_list->show(); + } + } +} + +void GMainWindow::UpdateSecondaryWindowVisibility() { + if (!emulation_running) { + return; + } + if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) { + secondary_window->RestoreGeometry(); + secondary_window->show(); + } else { + secondary_window->BackupGeometry(); + secondary_window->hide(); + } +} + +void GMainWindow::ChangeScreenLayout() { + Settings::LayoutOption new_layout = Settings::LayoutOption::Default; + + if (ui->action_Screen_Layout_Default->isChecked()) { + new_layout = Settings::LayoutOption::Default; + } else if (ui->action_Screen_Layout_Single_Screen->isChecked()) { + new_layout = Settings::LayoutOption::SingleScreen; + } else if (ui->action_Screen_Layout_Large_Screen->isChecked()) { + new_layout = Settings::LayoutOption::LargeScreen; + } else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) { + new_layout = Settings::LayoutOption::HybridScreen; + } else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) { + new_layout = Settings::LayoutOption::SideScreen; + } else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) { + new_layout = Settings::LayoutOption::SeparateWindows; + } + + Settings::values.layout_option = new_layout; + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::ToggleScreenLayout() { + const Settings::LayoutOption new_layout = []() { + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::Default: + return Settings::LayoutOption::SingleScreen; + case Settings::LayoutOption::SingleScreen: + return Settings::LayoutOption::LargeScreen; + case Settings::LayoutOption::LargeScreen: + return Settings::LayoutOption::HybridScreen; + case Settings::LayoutOption::HybridScreen: + return Settings::LayoutOption::SideScreen; + case Settings::LayoutOption::SideScreen: + return Settings::LayoutOption::SeparateWindows; + case Settings::LayoutOption::SeparateWindows: + return Settings::LayoutOption::Default; + default: + LOG_ERROR(Frontend, "Unknown layout option {}", + Settings::values.layout_option.GetValue()); + return Settings::LayoutOption::Default; + } + }(); + + Settings::values.layout_option = new_layout; + SyncMenuUISettings(); + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnSwapScreens() { + Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::OnRotateScreens() { + Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::TriggerSwapScreens() { + ui->action_Screen_Layout_Swap_Screens->trigger(); +} + +void GMainWindow::TriggerRotateScreens() { + ui->action_Screen_Layout_Upright_Screens->trigger(); +} + +void GMainWindow::OnSaveState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + system.SendSignal(Core::System::Signal::Save, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); + newest_slot = action->data().toUInt(); +} + +void GMainWindow::OnLoadState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + if (UISettings::values.save_state_warning) { + QMessageBox::warning(this, tr("Savestates"), + tr("Warning: Savestates are NOT a replacement for in-game saves, " + "and are not meant to be reliable.\n\nUse at your own risk!")); + UISettings::values.save_state_warning = false; + config->Save(); + } + + system.SendSignal(Core::System::Signal::Load, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); +} + +void GMainWindow::OnConfigure() { + game_list->SetDirectoryWatcherEnabled(false); + Settings::SetConfiguringGlobal(true); + ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices, + !multiplayer_state->IsHostingPublicRoom()); + connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, + &GMainWindow::OnLanguageChanged); + auto old_theme = UISettings::values.theme; + const int old_input_profile_index = Settings::values.current_input_profile_index; + const auto old_input_profiles = Settings::values.input_profiles; + const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps; + const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); +#ifdef __unix__ + const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); +#endif + auto result = configureDialog.exec(); + game_list->SetDirectoryWatcherEnabled(true); + if (result == QDialog::Accepted) { + configureDialog.ApplyConfiguration(); + InitializeHotkeys(); + if (UISettings::values.theme != old_theme) { + UpdateUITheme(); + } + if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + } +#ifdef __unix__ + if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); + } +#endif + if (!multiplayer_state->IsHostingPublicRoom()) + multiplayer_state->UpdateCredentials(); + emit UpdateThemedIcons(); + SyncMenuUISettings(); + game_list->RefreshGameDirectory(); + config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + } + UpdateSecondaryWindowVisibility(); + UpdateBootHomeMenuState(); + UpdateStatusButtons(); + } else { + Settings::values.input_profiles = old_input_profiles; + Settings::values.touch_from_button_maps = old_touch_from_button_maps; + Settings::LoadProfile(old_input_profile_index); + } +} + +void GMainWindow::OnLoadAmiibo() { + if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { + return; + } + + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr) { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (nfc->IsTagActive()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("A tag is already in use.")); + return; + } + + if (!nfc->IsSearchingForAmiibos()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Game is not looking for amiibos.")); + return; + } + + const QString extensions{QStringLiteral("*.bin")}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); + + if (filename.isEmpty()) { + return; + } + + LoadAmiibo(filename); +} + +void GMainWindow::LoadAmiibo(const QString& filename) { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (!nfc->LoadAmiibo(filename.toStdString())) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Unable to open amiibo file \"%1\" for reading.").arg(filename)); + return; + } + + ui->action_Remove_Amiibo->setEnabled(true); +} + +void GMainWindow::OnRemoveAmiibo() { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + nfc->RemoveAmiibo(); + ui->action_Remove_Amiibo->setEnabled(false); +} + +void GMainWindow::OnOpenLucina3DSFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); +} + +void GMainWindow::OnToggleFilterBar() { + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + if (ui->action_Show_Filter_Bar->isChecked()) { + game_list->SetFilterFocus(); + } else { + game_list->ClearFilter(); + } +} + +void GMainWindow::OnCreateGraphicsSurfaceViewer() { + auto graphicsSurfaceViewerWidget = + new GraphicsSurfaceWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget); + // TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true); + graphicsSurfaceViewerWidget->show(); +} + +void GMainWindow::OnRecordMovie() { + MovieRecordDialog dialog(this, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_record_on_start = true; + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); + + if (emulation_running) { // Restart game + BootGame(QString(game_path)); + } + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(true); +} + +void GMainWindow::OnPlayMovie() { + MoviePlayDialog dialog(this, game_list, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_playback_on_start = true; + movie_playback_path = dialog.GetMoviePath(); + BootGame(dialog.GetGamePath()); + + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnCloseMovie() { + if (movie_record_on_start) { + QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } else { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording; + movie.Shutdown(); + if (was_recording) { + QMessageBox::information(this, tr("Movie Saved"), + tr("The movie is successfully saved.")); + } + + if (was_running) { + OnStartGame(); + } + } + + ui->action_Close_Movie->setEnabled(false); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnSaveMovie() { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) { + movie.SaveMovie(); + QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); + } else { + LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); + } + + if (was_running) { + OnStartGame(); + } +} + +void GMainWindow::OnCaptureScreenshot() { + if (!emu_thread) [[unlikely]] { + return; + } + + const bool was_running = emu_thread->IsRunning(); + + if (was_running || + (QMessageBox::question( + this, tr("Game will unpause"), + tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running) { + OnPauseGame(); + } + std::string path = UISettings::values.screenshot_path.GetValue(); + if (!FileUtil::IsDirectory(path)) { + if (!FileUtil::CreateFullPath(path)) { + QMessageBox::information( + this, tr("Invalid Screenshot Directory"), + tr("Cannot create specified screenshot directory. Screenshot " + "path is set back to its default value.")); + path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + path.append("screenshots/"); + UISettings::values.screenshot_path = path; + }; + } + + static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); + const std::string filename = game_title.remove(expr).toStdString(); + const std::string timestamp = QDateTime::currentDateTime() + .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) + .toStdString(); + path.append(fmt::format("/{}_{}.png", filename, timestamp)); + + auto* const screenshot_window = + secondary_window->HasFocus() ? secondary_window : render_window; + screenshot_window->CaptureScreenshot( + UISettings::values.screenshot_resolution_factor.GetValue(), + QString::fromStdString(path)); + OnStartGame(); + } +} + +void GMainWindow::OnDumpVideo() { + if (DynamicLibrary::FFmpeg::LoadFFmpeg()) { + if (ui->action_Dump_Video->isChecked()) { + OnStartVideoDumping(); + } else { + OnStopVideoDumping(); + } + } else { + ui->action_Dump_Video->setChecked(false); + + QMessageBox message_box; + message_box.setWindowTitle(tr("Could not load video dumper")); + message_box.setText( + tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." +#ifdef _WIN32 + "\n\nTo install FFmpeg to Lucina3DS, press Open and select your FFmpeg directory." +#endif + "\n\nTo view a guide on how to install FFmpeg, press Help.")); + message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help +#ifdef _WIN32 + | QMessageBox::Open +#endif + ); + auto result = message_box.exec(); + if (result == QMessageBox::Help) { + QDesktopServices::openUrl(QUrl(QStringLiteral( + "https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/"))); +#ifdef _WIN32 + } else if (result == QMessageBox::Open) { + OnOpenFFmpeg(); +#endif + } + } +} + +#ifdef _WIN32 +void GMainWindow::OnOpenFFmpeg() { + auto filename = + QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString(); + if (filename.empty()) { + return; + } + // Check for a bin directory if they chose the FFmpeg root directory. + auto bin_dir = filename + DIR_SEP + "bin"; + if (!FileUtil::Exists(bin_dir)) { + // Otherwise, assume the user directly selected the directory containing the DLLs. + bin_dir = filename; + } + + static const std::array library_names = { + Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR), + }; + + for (auto& library_name : library_names) { + if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("The provided FFmpeg directory is missing %1. Please make " + "sure the correct directory was selected.") + .arg(QString::fromStdString(library_name))); + return; + } + } + + std::atomic success(true); + auto process_file = [&success](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { + auto file_path = directory + DIR_SEP + virtual_name; + if (file_path.ends_with(".dll")) { + auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name; + if (!FileUtil::Copy(file_path, destination_path)) { + success.store(false); + return false; + } + } + return true; + }; + FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); + + if (success.load()) { + QMessageBox::information(this, tr("Lucina3DS"), tr("FFmpeg has been sucessfully installed.")); + } else { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("Installation of FFmpeg failed. Check the log file for details.")); + } +} +#endif + +void GMainWindow::OnStartVideoDumping() { + DumpingDialog dialog(this, system); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + ui->action_Dump_Video->setChecked(false); + return; + } + const auto path = dialog.GetFilePath(); + if (emulation_running) { + StartVideoDumping(path); + } else { + video_dumping_on_start = true; + video_dumping_path = path; + } +} + +void GMainWindow::StartVideoDumping(const QString& path) { + auto& renderer = system.GPU().Renderer(); + const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; + + auto dumper = std::make_shared(renderer); + if (dumper->StartDumping(path.toStdString(), layout)) { + system.RegisterVideoDumper(dumper); + } else { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui->action_Dump_Video->setChecked(false); + } +} + +void GMainWindow::OnStopVideoDumping() { + ui->action_Dump_Video->setChecked(false); + + if (video_dumping_on_start) { + video_dumping_on_start = false; + video_dumping_path.clear(); + } else { + auto dumper = system.GetVideoDumper(); + if (!dumper || !dumper->IsDumping()) { + return; + } + + game_paused_for_dumping = emu_thread->IsRunning(); + OnPauseGame(); + + auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, [this] { + if (game_shutdown_delayed) { + game_shutdown_delayed = false; + ShutdownGame(); + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; + OnStartGame(); + } + }); + future_watcher->setFuture(future); + } +} + +void GMainWindow::UpdateStatusBar() { + if (!emu_thread) [[unlikely]] { + status_bar_update_timer.stop(); + return; + } + + // Update movie status + const u64 current = movie.GetCurrentInputIndex(); + const u64 total = movie.GetTotalInputCount(); + const auto play_mode = movie.GetPlayMode(); + if (play_mode == Core::Movie::PlayMode::Recording) { + message_label->setText(tr("Recording %1").arg(current)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(true); + } else if (play_mode == Core::Movie::PlayMode::Playing) { + message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { + message_label->setText(tr("Movie Finished")); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (message_label_used_for_movie) { // Clear the label if movie was just closed + message_label->setText(QString{}); + message_label_used_for_movie = false; + ui->action_Save_Movie->setEnabled(false); + } + + auto results = system.GetAndResetPerfStats(); + + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 5> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + tr("(Accessing SystemSaveData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + + if (Settings::values.frame_limit.GetValue() == 0) { + emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); + } else { + emu_speed_label->setText(tr("Speed: %1% / %2%") + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.frame_limit.GetValue())); + } + game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); + emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } + emu_speed_label->setVisible(true); + game_fps_label->setVisible(true); + emu_frametime_label->setVisible(true); +} + +void GMainWindow::UpdateBootHomeMenuState() { + const auto current_region = Settings::values.region_value.GetValue(); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + const auto path = Core::GetHomeMenuNcchPath(region); + ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled( + (current_region == Settings::REGION_VALUE_AUTO_SELECT || + current_region == static_cast(region)) && + !path.empty() && FileUtil::Exists(path)); + } +} + +void GMainWindow::HideMouseCursor() { + if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + render_window->setCursor(QCursor(Qt::BlankCursor)); + secondary_window->setCursor(QCursor(Qt::BlankCursor)); + if (UISettings::values.single_window_mode.GetValue()) { + setCursor(QCursor(Qt::BlankCursor)); + } +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + render_window->unsetCursor(); + secondary_window->unsetCursor(); + if (emu_thread && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::OnMute() { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); +} + +void GMainWindow::OnDecreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume <= 30) { + step = 2; + } + if (current_volume <= 6) { + step = 1; + } + const auto value = + static_cast(std::max(current_volume - step, 0)) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::OnIncreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume < 30) { + step = 2; + } + if (current_volume < 6) { + step = 1; + } + const auto value = static_cast(current_volume + step) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::UpdateVolumeUI() { + const auto volume_value = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + volume_slider->setValue(volume_value); + if (Settings::values.audio_muted) { + volume_button->setChecked(false); + volume_button->setText(tr("VOLUME: MUTE")); + } else { + volume_button->setChecked(true); + volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); + } +} + +void GMainWindow::UpdateAPIIndicator(bool update) { + static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"), + QStringLiteral("VULKAN")}; + + static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"), + QStringLiteral("#91242a")}; + + u32 api_index = static_cast(Settings::values.graphics_api.GetValue()); + if (update) { + api_index = (api_index + 1) % graphics_apis.size(); + // Skip past any disabled renderers. +#ifndef ENABLE_SOFTWARE_RENDERER + if (api_index == static_cast(Settings::GraphicsAPI::Software)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_OPENGL + if (api_index == static_cast(Settings::GraphicsAPI::OpenGL)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_VULKAN + if (api_index == static_cast(Settings::GraphicsAPI::Vulkan)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif + Settings::values.graphics_api = static_cast(api_index); + } + + const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }") + .arg(graphics_api_colors[api_index]); + + graphics_api_button->setText(graphics_apis[api_index]); + graphics_api_button->setStyleSheet(style_sheet); +} + +void GMainWindow::UpdateStatusButtons() { + UpdateAPIIndicator(); + UpdateVolumeUI(); +} + +void GMainWindow::OnMouseActivity() { + ShowMouseCursor(); +} + +void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { + QString status_message; + + QString title, message; + QMessageBox::Icon error_severity_icon; + bool can_continue = true; + if (result == Core::System::ResultStatus::ErrorSystemFiles) { + const QString common_message = + tr("%1 is missing. Please dump your " + "system archives.
Continuing emulation may result in crashes and bugs."); + + if (!details.empty()) { + message = common_message.arg(QString::fromStdString(details)); + } else { + message = common_message.arg(tr("A system archive")); + } + + title = tr("System Archive Not Found"); + status_message = tr("System Archive Missing"); + error_severity_icon = QMessageBox::Icon::Critical; + } else if (result == Core::System::ResultStatus::ErrorSavestate) { + title = tr("Save/load Error"); + message = QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Warning; + } else if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + title = tr("Artic Base Server"); + message = + tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details) + .c_str()); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else { + title = tr("Fatal Error"); + message = + tr("A fatal error occurred. " + "Check " + "the log for details." + "
Continuing emulation may result in crashes and bugs."); + status_message = tr("Fatal Error encountered"); + error_severity_icon = QMessageBox::Icon::Critical; + } + + QMessageBox message_box; + message_box.setWindowTitle(title); + message_box.setText(message); + message_box.setIcon(error_severity_icon); + if (error_severity_icon == QMessageBox::Icon::Critical) { + if (can_continue) { + message_box.addButton(tr("Continue"), QMessageBox::RejectRole); + } + QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); + if (result != Core::System::ResultStatus::ShutdownRequested) + message_box.exec(); + + if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested || + message_box.clickedButton() == abort_button) { + if (emu_thread) { + ShutdownGame(); + return; + } + } + } else { + // This block should run when the error isn't too big of a deal + // e.g. when a save state can't be saved or loaded + message_box.addButton(tr("OK"), QMessageBox::RejectRole); + message_box.exec(); + } + + // Only show the message if the game is still running. + if (emu_thread) { + emu_thread->SetRunning(true); + message_label->setText(status_message); + message_label_used_for_movie = false; + } +} + +void GMainWindow::OnMenuAboutLucina3DS() { + AboutDialog about{this}; + about.exec(); +} + +bool GMainWindow::ConfirmClose() { + if (!emu_thread || !UISettings::values.confirm_before_closing) { + return true; + } + + QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Lucina3DS"), tr("Would you like to exit now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::closeEvent(QCloseEvent* event) { + if (!ConfirmClose()) { + event->ignore(); + return; + } + + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + hotkey_registry.SaveHotkeys(); + + // Shutdown session if the emu thread is active... + if (emu_thread) { + ShutdownGame(); + } + + render_window->close(); + secondary_window->close(); + multiplayer_state->Close(); + InputCommon::Shutdown(); + QWidget::closeEvent(event); +} + +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; +} + +static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin", + "3dsx", "app", "elf", "axf"}; + +static bool IsCorrectFileExtension(const QMimeData* mime) { + const QString& filename = mime->urls().at(0).toLocalFile(); + return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), + QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); +} + +static bool IsAcceptableDropEvent(QDropEvent* event) { + return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); +} + +void GMainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsAcceptableDropEvent(event)) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool GMainWindow::DropAction(QDropEvent* event) { + if (!IsAcceptableDropEvent(event)) { + return false; + } + + const QMimeData* mime_data = event->mimeData(); + const QString& filename = mime_data->urls().at(0).toLocalFile(); + + if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { + // Amiibo + LoadAmiibo(filename); + } else { + // Game + if (ConfirmChangeGame()) { + BootGame(filename); + } + } + return true; +} + +void GMainWindow::OnFileOpen(const QFileOpenEvent* event) { + BootGame(event->file()); +} + +void GMainWindow::dropEvent(QDropEvent* event) { + DropAction(event); +} + +void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { + AcceptDropEvent(event); +} + +void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { + AcceptDropEvent(event); +} + +bool GMainWindow::ConfirmChangeGame() { + if (!emu_thread) [[unlikely]] { + return true; + } + + auto answer = QMessageBox::question( + this, tr("Lucina3DS"), tr("The game is still running. Would you like to stop emulation?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::filterBarSetChecked(bool state) { + ui->action_Show_Filter_Bar->setChecked(state); + emit(OnToggleFilterBar()); +} + +void GMainWindow::UpdateUITheme() { + const QString icons_base_path = QStringLiteral(":/icons/"); + const QString default_theme = QStringLiteral("default"); + const QString default_theme_path = icons_base_path + default_theme; + + const QString& current_theme = UISettings::values.theme; + const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second); + QStringList theme_paths(default_theme_paths); + + if (is_default_theme || current_theme.isEmpty()) { + const QString theme_uri(QStringLiteral(":default/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, + "Unable to open default stylesheet, falling back to empty stylesheet"); + qApp->setStyleSheet({}); + setStyleSheet({}); + } + theme_paths.append(default_theme_path); + QIcon::setThemeName(default_theme); + } else { + const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found"); + } + + const QString current_theme_path = icons_base_path + current_theme; + theme_paths.append({default_theme_path, current_theme_path}); + QIcon::setThemeName(current_theme); + } + + QIcon::setThemeSearchPaths(theme_paths); +} + +void GMainWindow::LoadTranslation() { + // If the selected language is English, no need to install any translation + if (UISettings::values.language == QStringLiteral("en")) { + return; + } + + bool loaded; + + if (UISettings::values.language.isEmpty()) { + // Use the system's default locale + loaded = translator.load(QLocale::system(), {}, {}, QStringLiteral(":/languages/")); + } else { + // Otherwise load from the specified file + loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/")); + } + + if (loaded) { + qApp->installTranslator(&translator); + } else { + UISettings::values.language = QStringLiteral("en"); + } +} + +void GMainWindow::OnLanguageChanged(const QString& locale) { + if (UISettings::values.language != QStringLiteral("en")) { + qApp->removeTranslator(&translator); + } + + UISettings::values.language = locale; + LoadTranslation(); + ui->retranslateUi(this); + RetranslateStatusBar(); + UpdateWindowTitle(); +} + +void GMainWindow::OnConfigurePerGame() { + u64 title_id{}; + system.GetAppLoader().ReadProgramId(title_id); + OpenPerGameConfiguration(title_id, game_path); +} + +void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) { + Settings::SetConfiguringGlobal(false); + ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted) { + Settings::RestoreGlobalState(system.IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } + + // Do not cause the global config to write local settings into the config file + const bool is_powered_on = system.IsPoweredOn(); + Settings::RestoreGlobalState(system.IsPoweredOn()); + + if (!is_powered_on) { + config->Save(); + } + + UpdateStatusButtons(); +} + +void GMainWindow::OnMoviePlaybackCompleted() { + OnPauseGame(); + QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); +} + +void GMainWindow::UpdateWindowTitle() { + const QString full_name = QString::fromUtf8(Common::g_build_fullname); + + if (game_title.isEmpty()) { + setWindowTitle(QStringLiteral("%1").arg(full_name)); + } else { + setWindowTitle(QStringLiteral("%1 | %2").arg(full_name, game_title)); + render_window->setWindowTitle( + QStringLiteral("%1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); + secondary_window->setWindowTitle(QStringLiteral("%1 | %2 | %3") + .arg(full_name, game_title, tr("Secondary Window"))); + } +} + +void GMainWindow::UpdateUISettings() { + if (!ui->action_Fullscreen->isChecked()) { + UISettings::values.geometry = saveGeometry(); + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + } + UISettings::values.state = saveState(); +#if MICROPROFILE_ENABLED + UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); + UISettings::values.microprofile_visible = microProfileDialog->isVisible(); +#endif + UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); + UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); + UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); + UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); + UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.first_start = false; +} + +void GMainWindow::SyncMenuUISettings() { + ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::Default); + ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SingleScreen); + ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::LargeScreen); + ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::HybridScreen); + ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SideScreen); + ui->action_Screen_Layout_Separate_Windows->setChecked( + Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); + ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue()); + ui->action_Screen_Layout_Upright_Screens->setChecked( + Settings::values.upright_screen.GetValue()); +} + +void GMainWindow::RetranslateStatusBar() { + if (emu_thread) + UpdateStatusBar(); + + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + multiplayer_state->retranslateUi(); +} + +void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(system); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + +#ifdef __unix__ +void GMainWindow::SetGamemodeEnabled(bool state) { + if (emulation_running) { + Common::Linux::SetGamemodeState(state); + } +} +#endif + +#ifdef main +#undef main +#endif + +static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() { +#ifdef _WIN32 + // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. + // This is done by setting the optimal scaling policy for the primary screen. + + // Create a temporary QApplication. + int temp_argc = 0; + char** temp_argv = nullptr; + QApplication temp{temp_argc, temp_argv}; + + // Get the current screen geometry. + const QScreen* primary_screen = QGuiApplication::primaryScreen(); + if (!primary_screen) { + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; + } + + const QRect screen_rect = primary_screen->geometry(); + const qreal real_ratio = primary_screen->devicePixelRatio(); + const qreal real_width = std::trunc(screen_rect.width() * real_ratio); + const qreal real_height = std::trunc(screen_rect.height() * real_ratio); + + // Recommended minimum width and height for proper window fit. + // Any screen with a lower resolution than this will still have a scale of 1. + constexpr qreal minimum_width = 1350.0; + constexpr qreal minimum_height = 900.0; + + const qreal width_ratio = std::max(1.0, real_width / minimum_width); + const qreal height_ratio = std::max(1.0, real_height / minimum_height); + + // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. + const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio)); + + if (max_ratio > real_ratio) { + return Qt::HighDpiScaleFactorRoundingPolicy::Round; + } else { + return Qt::HighDpiScaleFactorRoundingPolicy::Floor; + } +#else + // Other OSes should be better than Windows at fractional scaling. + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; +#endif +} + +int main(int argc, char* argv[]) { + Common::DetachedTasks detached_tasks; + MicroProfileOnThreadCreate("Frontend"); + SCOPE_EXIT({ MicroProfileShutdown(); }); + + // Init settings params + QCoreApplication::setOrganizationName(QStringLiteral("Citra team | Lucina3DS")); + QCoreApplication::setApplicationName(QStringLiteral("Lucina3DS")); + + auto rounding_policy = GetHighDpiRoundingPolicy(); + QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); + +#ifdef __APPLE__ + auto bundle_dir = FileUtil::GetBundleDirectory(); + if (bundle_dir) { + FileUtil::SetCurrentDir(bundle_dir.value() + ".."); + } +#endif + +#ifdef ENABLE_OPENGL + QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); +#endif + + QApplication app(argc, argv); + app.setWindowIcon(QIcon("/usr/share/icons/hicolor/512x512/apps/lucina3ds.png")); + + + // Qt changes the locale and causes issues in float conversion using std::to_string() when + // generating shaders + setlocale(LC_ALL, "C"); + + auto& system{Core::System::GetInstance()}; + + // Register Qt image interface + system.RegisterImageInterface(std::make_shared()); + + GMainWindow main_window(system); + + // Register frontend applets + Frontend::RegisterDefaultApplets(system); + + system.RegisterMiiSelector(std::make_shared(main_window)); + system.RegisterSoftwareKeyboard(std::make_shared(main_window)); + +#ifdef __APPLE__ + // Register microphone permission check. + system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); +#endif + + main_window.setWindowIcon(QIcon("/usr/share/icons/hicolor/512x512/apps/lucina3ds.png")); + main_window.show(); + + QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, + &GMainWindow::OnAppFocusStateChanged); + + int result = app.exec(); + detached_tasks.WaitForAllTasks(); + return result; +} diff --git a/.history/src/lucina3ds_qt/main_20250209235145.cpp b/.history/src/lucina3ds_qt/main_20250209235145.cpp new file mode 100644 index 00000000..c5c1247b --- /dev/null +++ b/.history/src/lucina3ds_qt/main_20250209235145.cpp @@ -0,0 +1,3348 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __APPLE__ +#include // for chdir +#endif +#ifdef _WIN32 +#include +#endif +#ifdef __unix__ +#include +#include +#include +#include "common/linux/gamemode.h" +#endif +#include "lucina3ds_qt/aboutdialog.h" +#include "lucina3ds_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/swkbd.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/compatdb.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/discord.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/loading_screen.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/qt_image_interface.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/graphics_device_info.h" +#include "common/arch.h" +#include "common/common_paths.h" +#include "common/detached_tasks.h" +#include "common/dynamic_library/dynamic_library.h" +#include "common/file_util.h" +#include "common/literals.h" +#include "common/logging/backend.h" +#include "common/logging/log.h" +#include "common/memory_detect.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#if LUCINA3DS_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/settings.h" +#include "core/core.h" +#include "core/dumping/backend.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_source_sd_savedata.h" +#include "core/frontend/applets/default_applets.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/hle/service/nfc/nfc.h" +#include "core/loader/loader.h" +#include "core/movie.h" +#include "core/savestate.h" +#include "core/system_titles.h" +#include "input_common/main.h" +#include "network/network_settings.h" +#include "ui_main.h" +#include "video_core/gpu.h" +#include "video_core/renderer_base.h" + +#ifdef __APPLE__ +#include "common/apple_authorization.h" +#endif + +#ifdef USE_DISCORD_PRESENCE +#include "lucina3ds_qt/discord_impl.h" +#endif + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); +#endif + +#ifdef _WIN32 +extern "C" { +// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +} +#endif + +#ifdef HAVE_SDL2 +#include +#endif + +constexpr int default_mouse_timeout = 2500; + +/** + * "Callouts" are one-time instructional messages shown to the user. In the config settings, there + * is a bitfield "callout_flags" options, used to track if a message has already been shown to the + * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. + */ + +const int GMainWindow::max_recent_files_item; + +static QString PrettyProductName() { +#ifdef _WIN32 + // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 + // With that notation change they changed the registry key used to denote the current version + QSettings windows_registry( + QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), + QSettings::NativeFormat); + const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); + if (release_id == QStringLiteral("2009")) { + const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); + const QString display_version = + windows_registry.value(QStringLiteral("DisplayVersion")).toString(); + const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); + const u32 version = current_build >= 22000 ? 11 : 10; + return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") + .arg(QString::number(version), display_version, QString::number(current_build), + QString::number(ubr)); + } +#endif + return QSysInfo::prettyProductName(); +} + +GMainWindow::GMainWindow(Core::System& system_) + : ui{std::make_unique()}, system{system_}, movie{system.Movie()}, + config{std::make_unique()}, emu_thread{nullptr} { + Common::Log::Initialize(); + Common::Log::Start(); + + Debugger::ToggleConsole(); + +#ifdef __unix__ + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); +#endif + + // register types to use in slots and signals + qRegisterMetaType("std::size_t"); + qRegisterMetaType("Service::AM::InstallStatus"); + + // Register CameraFactory + qt_cameras = std::make_shared(); + Camera::RegisterFactory("image", std::make_unique()); + Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + + LoadTranslation(); + + Pica::g_debug_context = Pica::DebugContext::Construct(); + setAcceptDrops(true); + ui->setupUi(this); + statusBar()->hide(); + + default_theme_paths = QIcon::themeSearchPaths(); + UpdateUITheme(); + + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + discord_rpc->Update(); + + Network::Init(); + + movie.SetPlaybackCompletionCallback([this] { + QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); + }); + + InitializeWidgets(); + InitializeDebugWidgets(); + InitializeRecentFileMenuActions(); + InitializeSaveStateMenuActions(); + InitializeHotkeys(); +#if ENABLE_QT_UPDATER + ShowUpdaterWidgets(); +#else + ui->action_Check_For_Updates->setVisible(false); + ui->action_Open_Maintenance_Tool->setVisible(false); +#endif + + SetDefaultUIGeometry(); + RestoreUIState(); + + ConnectAppEvents(); + ConnectMenuEvents(); + ConnectWidgetEvents(); + + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + Common::g_scm_desc); +#if LUCINA3DS_ARCH(x86_64) + const auto& caps = Common::GetCPUCaps(); + std::string cpu_string = caps.cpu_string; + if (caps.avx || caps.avx2 || caps.avx512) { + cpu_string += " | AVX"; + if (caps.avx512) { + cpu_string += "512"; + } else if (caps.avx2) { + cpu_string += '2'; + } + if (caps.fma || caps.fma4) { + cpu_string += " | FMA"; + } + } + LOG_INFO(Frontend, "Host CPU: {}", cpu_string); +#endif + LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); + const auto& mem_info = Common::GetMemInfo(); + using namespace Common::Literals; + LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB}); + LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB}); + UpdateWindowTitle(); + + show(); + + game_list->LoadCompatibilityList(); + game_list->PopulateAsync(UISettings::values.game_dirs); + + mouse_hide_timer.setInterval(default_mouse_timeout); + connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); + connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); + +#ifdef ENABLE_OPENGL + gl_renderer = GetOpenGLRenderer(); +#if defined(_WIN32) + if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { + // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ + // We can override the version here to allow Citra to work. + // TODO: Remove this when OpenGL 4.0+ is advertised. + qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); + } +#endif +#endif + +#ifdef ENABLE_VULKAN + physical_devices = GetVulkanPhysicalDevices(); + if (physical_devices.empty()) { + QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), + tr("Vulkan initialization failed during boot.
" + "Your GPU may not support Vulkan 1.1, or you do not " + "have the latest graphics driver.")); + } +#endif + +#if ENABLE_QT_UPDATER + if (UISettings::values.check_for_update_on_start) { + CheckForUpdates(); + } +#endif + + QStringList args = QApplication::arguments(); + if (args.size() < 2) { + return; + } + + QString game_path; + for (int i = 1; i < args.size(); ++i) { + // Preserves drag/drop functionality + if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { + game_path = args[1]; + break; + } + + // Launch game in fullscreen mode + if (args[i] == QStringLiteral("-f")) { + ui->action_Fullscreen->setChecked(true); + continue; + } + + // Launch game in windowed mode + if (args[i] == QStringLiteral("-w")) { + ui->action_Fullscreen->setChecked(false); + continue; + } + + // Launch game at path + if (args[i] == QStringLiteral("-g")) { + if (i >= args.size() - 1) { + continue; + } + + if (args[i + 1].startsWith(QChar::fromLatin1('-'))) { + continue; + } + + game_path = args[++i]; + } + } + + if (!game_path.isEmpty()) { + BootGame(game_path); + } +} + +GMainWindow::~GMainWindow() { + // Will get automatically deleted otherwise + if (!render_window->parent()) { + delete render_window; + } + + Pica::g_debug_context.reset(); + Network::Shutdown(); +} + +void GMainWindow::InitializeWidgets() { +#ifdef LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING + ui->action_Report_Compatibility->setVisible(true); +#endif + render_window = new GRenderWindow(this, emu_thread.get(), system, false); + secondary_window = new GRenderWindow(this, emu_thread.get(), system, true); + render_window->hide(); + secondary_window->hide(); + secondary_window->setParent(nullptr); + + game_list = new GameList(this); + ui->horizontalLayout->addWidget(game_list); + + game_list_placeholder = new GameListPlaceholder(this); + ui->horizontalLayout->addWidget(game_list_placeholder); + game_list_placeholder->setVisible(false); + + loading_screen = new LoadingScreen(this); + loading_screen->hide(); + ui->horizontalLayout->addWidget(loading_screen); + connect(loading_screen, &LoadingScreen::Hidden, this, [&] { + loading_screen->Clear(); + if (emulation_running) { + render_window->show(); + render_window->setFocus(); + render_window->activateWindow(); + } + }); + + InputCommon::Init(); + multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(), + ui->action_Leave_Room, ui->action_Show_Room); + multiplayer_state->setVisible(false); + +#if ENABLE_QT_UPDATER + // Setup updater + updater = new Updater(this); + UISettings::values.updater_found = updater->HasUpdater(); +#endif + + UpdateBootHomeMenuState(); + + // Create status bar + message_label = new QLabel(); + // Configured separately for left alignment + message_label->setFrameStyle(QFrame::NoFrame); + message_label->setContentsMargins(4, 0, 4, 0); + message_label->setAlignment(Qt::AlignLeft); + statusBar()->addPermanentWidget(message_label, 1); + + progress_bar = new QProgressBar(); + progress_bar->hide(); + statusBar()->addPermanentWidget(progress_bar); + + artic_traffic_label = new QLabel(); + artic_traffic_label->setToolTip( + tr("Current Artic Base traffic speed. Higher values indicate bigger transfer loads.")); + + emu_speed_label = new QLabel(); + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label = new QLabel(); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label = new QLabel(); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + for (auto& label : + {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { + label->setVisible(false); + label->setFrameStyle(QFrame::NoFrame); + label->setContentsMargins(4, 0, 4, 0); + statusBar()->addPermanentWidget(label); + } + + // Setup Graphics API button + graphics_api_button = new QPushButton(); + graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton")); + graphics_api_button->setFocusPolicy(Qt::NoFocus); + UpdateAPIIndicator(); + + connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); }); + + statusBar()->insertPermanentWidget(0, graphics_api_button); + + volume_popup = new QWidget(this); + volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); + volume_popup->setLayout(new QVBoxLayout()); + volume_popup->setMinimumWidth(200); + + volume_slider = new QSlider(Qt::Horizontal); + volume_slider->setObjectName(QStringLiteral("volume_slider")); + volume_slider->setMaximum(100); + volume_slider->setPageStep(5); + connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { + Settings::values.audio_muted = false; + const auto value = static_cast(percentage) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); + }); + volume_popup->layout()->addWidget(volume_slider); + + volume_button = new QPushButton(); + volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); + volume_button->setFocusPolicy(Qt::NoFocus); + volume_button->setCheckable(true); + UpdateVolumeUI(); + connect(volume_button, &QPushButton::clicked, this, [&] { + UpdateVolumeUI(); + volume_popup->setVisible(!volume_popup->isVisible()); + QRect rect = volume_button->geometry(); + QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); + bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); + volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); + }); + statusBar()->insertPermanentWidget(1, volume_button); + + statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); + statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + + statusBar()->setVisible(true); + + // Removes an ugly inner border from the status bar widgets under Linux + setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); + + QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side); + actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows); +} + +void GMainWindow::InitializeDebugWidgets() { + connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this, + &GMainWindow::OnCreateGraphicsSurfaceViewer); + + QMenu* debug_menu = ui->menu_View_Debugging; + +#if MICROPROFILE_ENABLED + microProfileDialog = new MicroProfileDialog(this); + microProfileDialog->hide(); + debug_menu->addAction(microProfileDialog->toggleViewAction()); +#endif + + registersWidget = new RegistersWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, registersWidget); + registersWidget->hide(); + debug_menu->addAction(registersWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, registersWidget, + &RegistersWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, registersWidget, + &RegistersWidget::OnEmulationStopping); + + graphicsWidget = new GPUCommandStreamWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsWidget); + graphicsWidget->hide(); + debug_menu->addAction(graphicsWidget->toggleViewAction()); + + graphicsCommandsWidget = new GPUCommandListWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); + graphicsCommandsWidget->hide(); + debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); + + graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); + graphicsBreakpointsWidget->hide(); + debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); + + graphicsVertexShaderWidget = + new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget); + graphicsVertexShaderWidget->hide(); + debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction()); + + graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget); + graphicsTracingWidget->hide(); + debug_menu->addAction(graphicsTracingWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget, + &GraphicsTracingWidget::OnEmulationStopping); + + waitTreeWidget = new WaitTreeWidget(system, this); + addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget); + waitTreeWidget->hide(); + debug_menu->addAction(waitTreeWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, waitTreeWidget, + &WaitTreeWidget::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + &WaitTreeWidget::OnEmulationStopping); + + lleServiceModulesWidget = new LLEServiceModulesWidget(this); + addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget); + lleServiceModulesWidget->hide(); + debug_menu->addAction(lleServiceModulesWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, + [this] { lleServiceModulesWidget->setDisabled(true); }); + connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, + [this] { lleServiceModulesWidget->setDisabled(false); }); + + ipcRecorderWidget = new IPCRecorderWidget(system, this); + addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); + ipcRecorderWidget->hide(); + debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); + connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, + &IPCRecorderWidget::OnEmulationStarting); +} + +void GMainWindow::InitializeRecentFileMenuActions() { + for (int i = 0; i < max_recent_files_item; ++i) { + actions_recent_files[i] = new QAction(this); + actions_recent_files[i]->setVisible(false); + connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); + + ui->menu_recent_files->addAction(actions_recent_files[i]); + } + ui->menu_recent_files->addSeparator(); + QAction* action_clear_recent_files = new QAction(this); + action_clear_recent_files->setText(tr("Clear Recent Files")); + connect(action_clear_recent_files, &QAction::triggered, this, [this] { + UISettings::values.recent_files.clear(); + UpdateRecentFiles(); + }); + ui->menu_recent_files->addAction(action_clear_recent_files); + + UpdateRecentFiles(); +} + +void GMainWindow::InitializeSaveStateMenuActions() { + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i] = new QAction(this); + actions_load_state[i]->setData(i + 1); + connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); + ui->menu_Load_State->addAction(actions_load_state[i]); + + actions_save_state[i] = new QAction(this); + actions_save_state[i]->setData(i + 1); + connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); + ui->menu_Save_State->addAction(actions_save_state[i]); + } + + connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + if (newest_slot != 0) { + actions_load_state[newest_slot - 1]->trigger(); + } + }); + connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] { + UpdateSaveStates(); + actions_save_state[oldest_slot - 1]->trigger(); + }); + + connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + + UpdateSaveStates(); +} + +void GMainWindow::InitializeHotkeys() { + hotkey_registry.LoadHotkeys(); + + const QString main_window = QStringLiteral("Main Window"); + const QString fullscreen = QStringLiteral("Fullscreen"); + const QString toggle_screen_layout = QStringLiteral("Toggle Screen Layout"); + const QString swap_screens = QStringLiteral("Swap Screens"); + const QString rotate_screens = QStringLiteral("Rotate Screens Upright"); + + const auto link_action_shortcut = [&](QAction* action, const QString& action_name) { + static const QString main_window = QStringLiteral("Main Window"); + action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); + action->setShortcutContext(hotkey_registry.GetShortcutContext(main_window, action_name)); + action->setAutoRepeat(false); + this->addAction(action); + }; + + link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); + link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); + link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); + link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lucina3DS")); + link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); + link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); + link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); + link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); + link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); + link_action_shortcut(ui->action_Fullscreen, fullscreen); + link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); + link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, swap_screens); + link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, rotate_screens); + link_action_shortcut(ui->action_Enable_Frame_Advancing, + QStringLiteral("Toggle Frame Advancing")); + link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame")); + link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); + link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); + link_action_shortcut(ui->action_View_Lobby, + QStringLiteral("Multiplayer Browse Public Game Lobby")); + link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); + link_action_shortcut(ui->action_Connect_To_Room, + QStringLiteral("Multiplayer Direct Connect to Room")); + link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); + link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); + + const auto add_secondary_window_hotkey = [this](QKeySequence hotkey, const char* slot) { + // This action will fire specifically when secondary_window is in focus + QAction* secondary_window_action = new QAction(secondary_window); + secondary_window_action->setShortcut(hotkey); + + connect(secondary_window_action, SIGNAL(triggered()), this, slot); + secondary_window->addAction(secondary_window_action); + }; + + // Use the same fullscreen hotkey as the main window + const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen); + add_secondary_window_hotkey(fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen())); + + const auto toggle_screen_hotkey = + hotkey_registry.GetKeySequence(main_window, toggle_screen_layout); + add_secondary_window_hotkey(toggle_screen_hotkey, SLOT(ToggleScreenLayout())); + + const auto swap_screen_hotkey = hotkey_registry.GetKeySequence(main_window, swap_screens); + add_secondary_window_hotkey(swap_screen_hotkey, SLOT(TriggerSwapScreens())); + + const auto rotate_screen_hotkey = hotkey_registry.GetKeySequence(main_window, rotate_screens); + add_secondary_window_hotkey(rotate_screen_hotkey, SLOT(TriggerRotateScreens())); + + const auto connect_shortcut = [&](const QString& action_name, const auto& function) { + const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this); + connect(hotkey, &QShortcut::activated, this, function); + }; + + connect(hotkey_registry.GetHotkey(main_window, toggle_screen_layout, render_window), + &QShortcut::activated, this, &GMainWindow::ToggleScreenLayout); + + connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { + if (emulation_running) { + ui->action_Fullscreen->setChecked(false); + ToggleFullscreen(); + } + }); + connect_shortcut(QStringLiteral("Toggle Per-Game Speed"), [&] { + Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Toggle Texture Dumping"), + [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); + connect_shortcut(QStringLiteral("Toggle Custom Textures"), + [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes + // the variable hold a garbage value after this function exits + static constexpr u16 SPEED_LIMIT_STEP = 5; + connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + return; + } + if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() + + SPEED_LIMIT_STEP); + } else { + Settings::values.frame_limit = 0; + } + UpdateStatusBar(); + }); + connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] { + if (Settings::values.frame_limit.GetValue() == 0) { + Settings::values.frame_limit = 995; + } else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) { + Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() - + SPEED_LIMIT_STEP); + UpdateStatusBar(); + } + UpdateStatusBar(); + }); + + connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); + connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); + connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); + + // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the + // variable hold a garbage value after this function exits + static constexpr u16 FACTOR_3D_STEP = 5; + connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d > 0) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); + connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] { + const auto factor_3d = Settings::values.factor_3d.GetValue(); + if (factor_3d < 100) { + if (factor_3d % FACTOR_3D_STEP != 0) { + Settings::values.factor_3d = + factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP); + } else { + Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP; + } + UpdateStatusBar(); + } + }); +} + +void GMainWindow::SetDefaultUIGeometry() { + // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half + const QRect screenRect = screen()->geometry(); + + const int w = screenRect.width() * 2 / 3; + const int h = screenRect.height() / 2; + const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; + const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100; + + setGeometry(x, y, w, h); +} + +void GMainWindow::RestoreUIState() { + restoreGeometry(UISettings::values.geometry); + restoreState(UISettings::values.state); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); +#if MICROPROFILE_ENABLED + microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); + microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); +#endif + + game_list->LoadInterfaceLayout(); + + ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); + ToggleWindowMode(); + + ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); + SyncMenuUISettings(); + + ui->action_Display_Dock_Widget_Headers->setChecked( + UISettings::values.display_titlebar.GetValue()); + OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked()); + + ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + + ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); +} + +void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { + if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && + state != Qt::ApplicationActive) { + LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); + } + if (!emulation_running) { + return; + } + if (UISettings::values.pause_when_in_background) { + if (emu_thread->IsRunning() && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + auto_paused = true; + OnPauseGame(); + } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { + auto_paused = false; + OnStartGame(); + } + } + if (UISettings::values.mute_when_in_background) { + if (!Settings::values.audio_muted && + (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { + Settings::values.audio_muted = true; + auto_muted = true; + } else if (auto_muted && state == Qt::ApplicationActive) { + Settings::values.audio_muted = false; + auto_muted = false; + } + UpdateVolumeUI(); + } +} + +bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::FileOpen) { + emit FileOpen(static_cast(event)); + return true; + } + return false; +} + +void GMainWindow::ConnectAppEvents() { + const auto filter = new GApplicationEventFilter(); + QGuiApplication::instance()->installEventFilter(filter); + + connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen); +} + +void GMainWindow::ConnectWidgetEvents() { + connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); + connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); + connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); + connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, + &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); + connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); + connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, + &GMainWindow::OnGameListAddDirectory); + connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, this, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); + + connect(game_list, &GameList::OpenPerGameGeneralRequested, this, + &GMainWindow::OnGameListOpenPerGameProperties); + + connect(this, &GMainWindow::EmulationStarting, render_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, render_window, + &GRenderWindow::OnEmulationStopping); + connect(this, &GMainWindow::EmulationStarting, secondary_window, + &GRenderWindow::OnEmulationStarting); + connect(this, &GMainWindow::EmulationStopping, secondary_window, + &GRenderWindow::OnEmulationStopping); + + connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); + connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); + connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); +} + +void GMainWindow::ConnectMenuEvents() { + const auto connect_menu = [&](QAction* action, const auto& event_fn) { + connect(action, &QAction::triggered, this, event_fn); + // Add actions to this window so that hiding menus in fullscreen won't disable them + addAction(action); + // Add actions to the render window so that they work outside of single window mode + render_window->addAction(action); + }; + + // File + connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); + connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); + connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), + [this, region] { OnMenuBootHomeMenu(region); }); + } + connect_menu(ui->action_Exit, &QMainWindow::close); + connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); + connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo); + + // Emulation + connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); + connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); + connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); }); + connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); + connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); + connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); + + // View + connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); + connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); + connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); + connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); + + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + + connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); + connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout); + connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens); + connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens); + + // Movie + connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie); + connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie); + connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie); + connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie); + connect_menu(ui->action_Movie_Read_Only_Mode, + [this](bool checked) { movie.SetReadOnly(checked); }); + connect_menu(ui->action_Enable_Frame_Advancing, [this] { + if (emulation_running) { + system.frame_limiter.SetFrameAdvancing(ui->action_Enable_Frame_Advancing->isChecked()); + ui->action_Advance_Frame->setEnabled(ui->action_Enable_Frame_Advancing->isChecked()); + } + }); + connect_menu(ui->action_Advance_Frame, [this] { + if (emulation_running && system.frame_limiter.IsFrameAdvancing()) { + ui->action_Enable_Frame_Advancing->setChecked(true); + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.AdvanceFrame(); + } + }); + connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); + connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); + + // Help + connect_menu(ui->action_Open_Lucina3DS_Folder, &GMainWindow::OnOpenLucina3DSFolder); + connect_menu(ui->action_Open_Log_Folder, []() { + QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + connect_menu(ui->action_FAQ, []() { + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); + }); + connect_menu(ui->action_About, &GMainWindow::OnMenuAboutLucina3DS); + +#if ENABLE_QT_UPDATER + connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); + connect_menu(ui->action_Open_Maintenance_Tool, &GMainWindow::OnOpenUpdater); +#endif +} + +void GMainWindow::UpdateMenuState() { + const bool is_paused = !emu_thread || !emu_thread->IsRunning(); + + const std::array running_actions{ + ui->action_Stop, + ui->action_Restart, + ui->action_Configure_Current_Game, + ui->action_Report_Compatibility, + ui->action_Load_Amiibo, + ui->action_Remove_Amiibo, + ui->action_Pause, + ui->action_Advance_Frame, + }; + + for (QAction* action : running_actions) { + action->setEnabled(emulation_running); + } + + ui->action_Capture_Screenshot->setEnabled(emulation_running); + + if (emulation_running && is_paused) { + ui->action_Pause->setText(tr("&Continue")); + } else { + ui->action_Pause->setText(tr("&Pause")); + } +} + +void GMainWindow::OnDisplayTitleBars(bool show) { + QList widgets = findChildren(); + + if (show) { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(nullptr); + if (old) { + delete old; + } + } + } else { + for (QDockWidget* widget : widgets) { + QWidget* old = widget->titleBarWidget(); + widget->setTitleBarWidget(new QWidget()); + if (old) { + delete old; + } + } + } +} + +#if ENABLE_QT_UPDATER +void GMainWindow::OnCheckForUpdates() { + explicit_update_check = true; + CheckForUpdates(); +} + +void GMainWindow::CheckForUpdates() { + if (updater->CheckForUpdates()) { + LOG_INFO(Frontend, "Update check started"); + } else { + LOG_WARNING(Frontend, "Unable to start check for updates"); + } +} + +void GMainWindow::OnUpdateFound(bool found, bool error) { + if (error) { + LOG_WARNING(Frontend, "Update check failed"); + return; + } + + if (!found) { + LOG_INFO(Frontend, "No updates found"); + + // If the user explicitly clicked the "Check for Updates" button, we are + // going to want to show them a prompt anyway. + if (explicit_update_check) { + explicit_update_check = false; + ShowNoUpdatePrompt(); + } + return; + } + + if (emulation_running && !explicit_update_check) { + LOG_INFO(Frontend, "Update found, deferring as game is running"); + defer_update_prompt = true; + return; + } + + LOG_INFO(Frontend, "Update found!"); + explicit_update_check = false; + + ShowUpdatePrompt(); +} + +void GMainWindow::ShowUpdatePrompt() { + defer_update_prompt = false; + + auto result = + QMessageBox::question(this, tr("Update Available"), + tr("An update is available. Would you like to install it now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + updater->LaunchUIOnExit(); + close(); + } +} + +void GMainWindow::ShowNoUpdatePrompt() { + QMessageBox::information(this, tr("No Update Found"), tr("No update is found."), + QMessageBox::Ok, QMessageBox::Ok); +} + +void GMainWindow::OnOpenUpdater() { + updater->LaunchUI(); +} + +void GMainWindow::ShowUpdaterWidgets() { + ui->action_Check_For_Updates->setVisible(UISettings::values.updater_found); + ui->action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found); + + connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound); +} +#endif + +#if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) +static std::optional HoldWakeLockLinux(u32 window_id = 0) { + if (!QDBusConnection::sessionBus().isConnected()) { + return {}; + } + // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit + QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), + QStringLiteral("/org/freedesktop/portal/desktop"), + QStringLiteral("org.freedesktop.portal.Inhibit")); + if (!xdp.isValid()) { + LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); + return {}; + } + QVariantMap options = {}; + //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the + //: computer from sleeping + options.insert(QString::fromLatin1("reason"), + QCoreApplication::translate("GMainWindow", "Luinca3DS is running a game")); + // 0x4: Suspend lock; 0x8: Idle lock + QDBusReply reply = + xdp.call(QString::fromLatin1("Inhibit"), + QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); + + if (reply.isValid()) { + return reply.value(); + } + LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", + reply.error().message().toStdString()); + return {}; +} + +static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { + if (!QDBusConnection::sessionBus().isConnected()) { + return; + } + QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), + QString::fromLatin1("org.freedesktop.portal.Request")); + unlocker.call(QString::fromLatin1("Close")); +} +#endif // __unix__ + +void GMainWindow::PreventOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); +#elif defined(HAVE_SDL2) + SDL_DisableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + auto reply = HoldWakeLockLinux(winId()); + if (reply) { + wake_lock = std::move(reply.value()); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +void GMainWindow::AllowOSSleep() { +#ifdef _WIN32 + SetThreadExecutionState(ES_CONTINUOUS); +#elif defined(HAVE_SDL2) + SDL_EnableScreenSaver(); +#if defined(__unix__) && !defined(__APPLE__) + if (!wake_lock.path().isEmpty()) { + ReleaseWakeLockLinux(wake_lock); + } +#endif // defined(__unix__) && !defined(__APPLE__) +#endif // _WIN32 +} + +bool GMainWindow::LoadROM(const QString& filename) { + // Shutdown previous session if the emu thread is still active... + if (emu_thread) { + ShutdownGame(); + } + + if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) { + LOG_CRITICAL(Frontend, "Failed to initialize render targets!"); + return false; + } + + const auto scope = render_window->Acquire(); + + const Core::System::ResultStatus result{ + system.Load(*render_window, filename.toStdString(), secondary_window)}; + + if (result != Core::System::ResultStatus::Success) { + switch (result) { + case Core::System::ResultStatus::ErrorGetLoader: + LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorSystemMode: + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + QMessageBox::critical( + this, tr("ROM Corrupted"), + tr("Your ROM is corrupted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { + QMessageBox::critical( + this, tr("ROM Encrypted"), + tr("Your ROM is encrypted.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + } + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + QMessageBox::critical( + this, tr("Invalid ROM Format"), + tr("Your ROM format is not supported.
Please follow the guides to redump your " + "game " + "cartridges or " + "installed " + "titles.")); + break; + + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + QMessageBox::critical(this, tr("Unsupported ROM"), + tr("GBA Virtual Console ROMs are not supported by Lucina3DS.")); + break; + + case Core::System::ResultStatus::ErrorArticDisconnected: + QMessageBox::critical( + this, tr("Artic Base Server"), + tr(fmt::format( + "An error has occurred whilst communicating with the Artic Base Server.\n{}", + system.GetStatusDetails()) + .c_str())); + break; + default: + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + break; + } + return false; + } + + std::string title; + system.GetAppLoader().ReadTitle(title); + game_title = QString::fromStdString(title); + UpdateWindowTitle(); + + game_path = filename; + + return true; +} + +void GMainWindow::BootGame(const QString& filename) { + if (emu_thread) { + ShutdownGame(); + } + + const bool is_artic = filename.startsWith(QString::fromStdString("articbase://")); + + if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { + const auto answer = QMessageBox::question( + this, tr("CIA must be installed before usage"), + tr("Before using this CIA, you must install it. Do you want to install it now?"), + QMessageBox::Yes | QMessageBox::No); + + if (answer == QMessageBox::Yes) + InstallCIA(QStringList(filename)); + + return; + } + + show_artic_label = is_artic; + + LOG_INFO(Frontend, "Lucinca3DS starting..."); + if (!is_artic) { + StoreRecentFile(filename); // Put the filename on top of the list + } + + if (movie_record_on_start) { + movie.PrepareForRecording(); + } + if (movie_playback_on_start) { + movie.PrepareForPlayback(movie_playback_path.toStdString()); + } + + const std::string path = filename.toStdString(); + auto loader = Loader::GetLoader(path); + + u64 title_id{0}; + Loader::ResultStatus res = loader->ReadProgramId(title_id); + + if (Loader::ResultStatus::Success == res) { + // Load per game settings + const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; + const std::string config_file_name = + title_id == 0 ? name : fmt::format("{:016X}", title_id); + LOG_INFO(Frontend, "Loading per game config file for title {}", config_file_name); + Config per_game_config(config_file_name, Config::ConfigType::PerGameConfig); + } + + // Artic Base Server cannot accept a client multiple times, so multiple loaders are not + // possible. Instead register the app loader early and do not create it again on system load. + if (!loader->SupportsMultipleInstancesForSameFile()) { + system.RegisterAppLoaderEarly(loader); + } + + system.ApplySettings(); + + Settings::LogSettings(); + + // Save configurations + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + config->Save(); + + if (!LoadROM(filename)) { + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); + return; + } + + // Set everything up + if (movie_record_on_start) { + movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString()); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } + if (movie_playback_on_start) { + movie.StartPlayback(movie_playback_path.toStdString()); + movie_playback_on_start = false; + movie_playback_path.clear(); + } + + if (ui->action_Enable_Frame_Advancing->isChecked()) { + ui->action_Advance_Frame->setEnabled(true); + system.frame_limiter.SetFrameAdvancing(true); + } else { + ui->action_Advance_Frame->setEnabled(false); + } + + if (video_dumping_on_start) { + StartVideoDumping(video_dumping_path); + video_dumping_on_start = false; + video_dumping_path.clear(); + } + + // Register debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Register(); + } + + // Create and start the emulation thread + emu_thread = std::make_unique(system, *render_window); + emit EmulationStarting(emu_thread.get()); + emu_thread->start(); + + connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); + + // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views + // before the CPU continues + connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget, + &RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget, + &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget, + &RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, + &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); + + connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, + &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); + connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, + &LoadingScreen::OnLoadComplete); + + // Update the GUI + registersWidget->OnDebugModeEntered(); + if (ui->action_Single_Window_Mode->isChecked()) { + game_list->hide(); + game_list_placeholder->hide(); + } + status_bar_update_timer.start(1000); + + if (UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + setMouseTracking(true); + } + + loading_screen->Prepare(system.GetAppLoader()); + loading_screen->show(); + + emulation_running = true; + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } + + OnStartGame(); +} + +void GMainWindow::ShutdownGame() { + if (!emulation_running) { + return; + } + + if (ui->action_Fullscreen->isChecked()) { + HideFullscreen(); + } + + auto video_dumper = system.GetVideoDumper(); + if (video_dumper && video_dumper->IsDumping()) { + game_shutdown_delayed = true; + OnStopVideoDumping(); + return; + } + + AllowOSSleep(); + + discord_rpc->Pause(); + emu_thread->RequestStop(); + + // Release emu threads from any breakpoints + // This belongs after RequestStop() and before wait() because if emulation stops on a GPU + // breakpoint after (or before) RequestStop() is called, the emulation would never be able + // to continue out to the main loop and terminate. Thus wait() would hang forever. + // TODO(bunnei): This function is not thread safe, but it's being used as if it were + Pica::g_debug_context->ClearBreakpoints(); + + // Unregister debug widgets + if (graphicsWidget->isVisible()) { + graphicsWidget->Unregister(); + } + + // Frame advancing must be cancelled in order to release the emu thread from waiting + system.frame_limiter.SetFrameAdvancing(false); + + emit EmulationStopping(); + + // Wait for emulation thread to complete and delete it + emu_thread->wait(); + emu_thread = nullptr; + + OnCloseMovie(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif + + // The emulation is stopped, so closing the window or not does not matter anymore + disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); + + render_window->hide(); + secondary_window->hide(); + loading_screen->hide(); + loading_screen->Clear(); + + if (game_list->IsEmpty()) { + game_list_placeholder->show(); + } else { + game_list->show(); + } + game_list->SetFilterFocus(); + + setMouseTracking(false); + + // Disable status bar updates + status_bar_update_timer.stop(); + message_label_used_for_movie = false; + show_artic_label = false; + artic_traffic_label->setVisible(false); + emu_speed_label->setVisible(false); + game_fps_label->setVisible(false); + emu_frametime_label->setVisible(false); + + UpdateSaveStates(); + + emulation_running = false; + +#if ENABLE_QT_UDPATER + if (defer_update_prompt) { + ShowUpdatePrompt(); + } +#endif + + game_title.clear(); + UpdateWindowTitle(); + + game_path.clear(); + + // Update the GUI + UpdateMenuState(); + + // When closing the game, destroy the GLWindow to clear the context after the game is closed + render_window->ReleaseRenderTarget(); + secondary_window->ReleaseRenderTarget(); +} + +void GMainWindow::StoreRecentFile(const QString& filename) { + UISettings::values.recent_files.prepend(filename); + UISettings::values.recent_files.removeDuplicates(); + while (UISettings::values.recent_files.size() > max_recent_files_item) { + UISettings::values.recent_files.removeLast(); + } + + UpdateRecentFiles(); +} + +void GMainWindow::UpdateRecentFiles() { + const int num_recent_files = + std::min(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); + + for (int i = 0; i < num_recent_files; i++) { + const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( + QFileInfo(UISettings::values.recent_files[i]).fileName()); + actions_recent_files[i]->setText(text); + actions_recent_files[i]->setData(UISettings::values.recent_files[i]); + actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); + actions_recent_files[i]->setVisible(true); + } + + for (int j = num_recent_files; j < max_recent_files_item; ++j) { + actions_recent_files[j]->setVisible(false); + } + + // Enable the recent files menu if the list isn't empty + ui->menu_recent_files->setEnabled(num_recent_files != 0); +} + +void GMainWindow::UpdateSaveStates() { + if (!system.IsPoweredOn()) { + ui->menu_Load_State->setEnabled(false); + ui->menu_Save_State->setEnabled(false); + return; + } + + ui->menu_Load_State->setEnabled(true); + ui->menu_Save_State->setEnabled(true); + ui->action_Load_from_Newest_Slot->setEnabled(false); + + oldest_slot = newest_slot = 0; + oldest_slot_time = std::numeric_limits::max(); + newest_slot_time = 0; + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return; + } + auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID()); + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i]->setEnabled(false); + actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); + actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); + } + for (const auto& savestate : savestates) { + const bool display_name = + savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch && + !savestate.build_name.empty(); + const auto text = + tr("Slot %1 - %2 %3") + .arg(savestate.slot) + .arg(QDateTime::fromSecsSinceEpoch(savestate.time) + .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))) + .arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String()) + .trimmed(); + + actions_load_state[savestate.slot - 1]->setEnabled(true); + actions_load_state[savestate.slot - 1]->setText(text); + actions_save_state[savestate.slot - 1]->setText(text); + + ui->action_Load_from_Newest_Slot->setEnabled(true); + + if (savestate.time > newest_slot_time) { + newest_slot = savestate.slot; + newest_slot_time = savestate.time; + } + if (savestate.time < oldest_slot_time) { + oldest_slot = savestate.slot; + oldest_slot_time = savestate.time; + } + } + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + if (!actions_load_state[i]->isEnabled()) { + // Prefer empty slot + oldest_slot = i + 1; + oldest_slot_time = 0; + break; + } + } +} + +void GMainWindow::OnGameListLoadFile(QString game_path) { + if (ConfirmChangeGame()) { + BootGame(game_path); + } +} + +void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { + std::string path; + std::string open_target; + + switch (target) { + case GameListOpenTarget::SAVE_DATA: { + open_target = "Save Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::EXT_DATA: { + open_target = "Extra Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); + break; + } + case GameListOpenTarget::APPLICATION: { + open_target = "Application"; + auto media_type = Service::AM::GetTitleMediaType(data_id); + path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; + break; + } + case GameListOpenTarget::UPDATE_DATA: { + open_target = "Update Data"; + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + + "content/"; + break; + } + case GameListOpenTarget::TEXTURE_DUMP: { + open_target = "Dumped Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); + break; + } + case GameListOpenTarget::TEXTURE_LOAD: { + open_target = "Custom Textures"; + path = fmt::format("{}textures/{:016X}/", + FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); + break; + } + case GameListOpenTarget::MODS: { + open_target = "Mods"; + path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), + data_id); + break; + } + case GameListOpenTarget::DLC_DATA: { + open_target = "DLC Data"; + path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/0004008c/{:08x}/content/", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id); + break; + } + case GameListOpenTarget::SHADER_CACHE: { + open_target = "Shader Cache"; + path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir); + break; + } + default: + LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); + return; + } + + QString qpath = QString::fromStdString(path); + + QDir dir(qpath); + if (!dir.exists()) { + QMessageBox::critical( + this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)), + tr("Folder does not exist!")); + return; + } + + LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); + + QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); +} + +void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, + const CompatibilityList& compatibility_list) { + auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); + + QString directory; + if (it != compatibility_list.end()) + directory = it->second.second; + + QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory)); +} + +void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { + auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); + dialog->setWindowModality(Qt::WindowModal); + dialog->setWindowFlags(dialog->windowFlags() & + ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); + dialog->setCancelButton(nullptr); + dialog->setMinimumDuration(0); + dialog->setValue(0); + + const auto base_path = fmt::format( + "{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); + const auto update_path = + fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), + program_id | 0x0004000e00000000); + using FutureWatcher = QFutureWatcher>; + auto* future_watcher = new FutureWatcher(this); + connect(future_watcher, &FutureWatcher::finished, this, + [this, dialog, base_path, update_path, future_watcher] { + dialog->hide(); + const auto& [base, update] = future_watcher->result(); + if (base != Loader::ResultStatus::Success) { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not dump base RomFS.\nRefer to the log for details.")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path))); + if (update == Loader::ResultStatus::Success) { + QDesktopServices::openUrl( + QUrl::fromLocalFile(QString::fromStdString(update_path))); + } + }); + + auto future = QtConcurrent::run([game_path, base_path, update_path] { + std::unique_ptr loader = Loader::GetLoader(game_path.toStdString()); + return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path)); + }); + future_watcher->setFuture(future); +} + +void GMainWindow::OnGameListOpenDirectory(const QString& directory) { + QString path; + if (directory == QStringLiteral("INSTALLED")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); + } else if (directory == QStringLiteral("SYSTEM")) { + path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000/title/00040010"); + } else { + path = directory; + } + if (!QFileInfo::exists(path)) { + QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); +} + +void GMainWindow::OnGameListAddDirectory() { + const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); + if (dir_path.isEmpty()) + return; + UISettings::GameDir game_dir{dir_path, false, true}; + if (!UISettings::values.game_dirs.contains(game_dir)) { + UISettings::values.game_dirs.append(game_dir); + game_list->PopulateAsync(UISettings::values.game_dirs); + } else { + LOG_WARNING(Frontend, "Selected directory is already in the game list"); + } +} + +void GMainWindow::OnGameListShowList(bool show) { + if (emulation_running && ui->action_Single_Window_Mode->isChecked()) + return; + game_list->setVisible(show); + game_list_placeholder->setVisible(!show); +}; + +void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { + const auto loader = Loader::GetLoader(file.toStdString()); + + u64 title_id{}; + if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { + QMessageBox::information(this, tr("Properties"), + tr("The game properties could not be loaded.")); + return; + } + + OpenPerGameConfiguration(title_id, file); +} + +void GMainWindow::OnMenuLoadFile() { + const QString extensions = QStringLiteral("*.").append( + GameList::supported_file_extensions.join(QStringLiteral(" *."))); + const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)", + "%1 is an identifier for the 3DS executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), UISettings::values.roms_path, file_filter); + + if (filename.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); +} + +void GMainWindow::OnMenuInstallCIA() { + QStringList filepaths = QFileDialog::getOpenFileNames( + this, tr("Load Files"), UISettings::values.roms_path, + tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)")); + + if (filepaths.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filepaths[0]).path(); + InstallCIA(filepaths); +} + +void GMainWindow::OnMenuConnectArticBase() { + bool ok = false; + auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), + tr("Enter Artic Base server address:"), QLineEdit::Normal, + UISettings::values.last_artic_base_addr, &ok); + if (ok) { + UISettings::values.last_artic_base_addr = res; + BootGame(QString::fromStdString("articbase://").append(res)); + } +} + +void GMainWindow::OnMenuBootHomeMenu(u32 region) { + BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); +} + +void GMainWindow::InstallCIA(QStringList filepaths) { + ui->action_Install_CIA->setEnabled(false); + game_list->SetDirectoryWatcherEnabled(false); + progress_bar->show(); + progress_bar->setMaximum(INT_MAX); + + (void)QtConcurrent::run([&, filepaths] { + Service::AM::InstallStatus status; + const auto cia_progress = [&](std::size_t written, std::size_t total) { + emit UpdateProgress(written, total); + }; + for (const auto& current_path : filepaths) { + status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress); + emit CIAInstallReport(status, current_path); + } + emit CIAInstallFinished(); + }); +} + +void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) { + progress_bar->setValue( + static_cast(INT_MAX * (static_cast(written) / static_cast(total)))); +} + +void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) { + QString filename = QFileInfo(filepath).fileName(); + switch (status) { + case Service::AM::InstallStatus::Success: + this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFailedToOpenFile: + QMessageBox::critical(this, tr("Unable to open File"), + tr("Could not open %1").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorAborted: + QMessageBox::critical( + this, tr("Installation aborted"), + tr("The installation of %1 was aborted. Please see the log for more details") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorInvalid: + QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); + break; + case Service::AM::InstallStatus::ErrorEncrypted: + QMessageBox::critical(this, tr("Encrypted File"), + tr("%1 must be decrypted " + "before being used with Lucina3DS. A real 3DS is required.") + .arg(filename)); + break; + case Service::AM::InstallStatus::ErrorFileNotFound: + QMessageBox::critical(this, tr("Unable to find File"), + tr("Could not find %1").arg(filename)); + break; + } +} + +void GMainWindow::OnCIAInstallFinished() { + progress_bar->hide(); + progress_bar->setValue(0); + game_list->SetDirectoryWatcherEnabled(true); + ui->action_Install_CIA->setEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + +void GMainWindow::UninstallTitles( + const std::vector>& titles) { + if (titles.empty()) { + return; + } + + // Select the first title in the list as representative. + const auto first_name = std::get(titles[0]); + + QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, + static_cast(titles.size()), this); + progress.setWindowModality(Qt::WindowModal); + + QFutureWatcher future_watcher; + QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, + &QProgressDialog::reset); + QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, + &QFutureWatcher::cancel); + QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, + &QProgressDialog::setValue); + + auto failed = false; + QString failed_name; + + const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { + const auto name = std::get(title); + const auto media_type = std::get(title); + const auto program_id = std::get(title); + + const auto result = Service::AM::UninstallProgram(media_type, program_id); + if (result.IsError()) { + LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), + result.raw); + failed = true; + failed_name = name; + future_watcher.cancel(); + } + }; + + future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); + progress.exec(); + future_watcher.waitForFinished(); + + if (failed) { + QMessageBox::critical(this, tr("Lucina3DS"), tr("Failed to uninstall '%1'.").arg(failed_name)); + } else if (!future_watcher.isCanceled()) { + QMessageBox::information(this, tr("Lucina3DS"), + tr("Successfully uninstalled '%1'.").arg(first_name)); + } +} + +void GMainWindow::OnMenuRecentFile() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + const QString filename = action->data().toString(); + if (QFileInfo::exists(filename)) { + BootGame(filename); + } else { + // Display an error message and remove the file from the list. + QMessageBox::information(this, tr("File not found"), + tr("File \"%1\" not found").arg(filename)); + + UISettings::values.recent_files.removeOne(filename); + UpdateRecentFiles(); + } +} + +void GMainWindow::OnStartGame() { + qt_cameras->ResumeCameras(); + + PreventOSSleep(); + + emu_thread->SetRunning(true); + graphics_api_button->setEnabled(false); + qRegisterMetaType("Core::System::ResultStatus"); + qRegisterMetaType("std::string"); + connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); + + UpdateMenuState(); + + discord_rpc->Update(); + +#ifdef __unix__ + Common::Linux::StartGamemode(); +#endif + + UpdateSaveStates(); + UpdateStatusButtons(); +} + +void GMainWindow::OnRestartGame() { + if (!system.IsPoweredOn()) { + return; + } + // Make a copy since BootGame edits game_path + BootGame(QString(game_path)); +} + +void GMainWindow::OnPauseGame() { + emu_thread->SetRunning(false); + qt_cameras->PauseCameras(); + + UpdateMenuState(); + AllowOSSleep(); + +#ifdef __unix__ + Common::Linux::StopGamemode(); +#endif +} + +void GMainWindow::OnPauseContinueGame() { + if (emulation_running) { + if (emu_thread->IsRunning()) { + OnPauseGame(); + } else { + OnStartGame(); + } + } +} + +void GMainWindow::OnStopGame() { + ShutdownGame(); + graphics_api_button->setEnabled(true); + Settings::RestoreGlobalState(false); + UpdateStatusButtons(); +} + +void GMainWindow::OnLoadComplete() { + loading_screen->OnLoadComplete(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnMenuReportCompatibility() { + if (!NetSettings::values.lucina3ds_token.empty() && !NetSettings::values.lucina3ds_username.empty()) { + CompatDB compatdb{this}; + compatdb.exec(); + } else { + QMessageBox::critical(this, tr("Missing Citra Account"), + tr("You must link your Citra account to submit test cases." + "
Go to Emulation > Configure... > Web to do so.")); + } +} + +void GMainWindow::ToggleFullscreen() { + if (!emulation_running) { + return; + } + if (ui->action_Fullscreen->isChecked()) { + ShowFullscreen(); + } else { + HideFullscreen(); + } +} + +void GMainWindow::ToggleSecondaryFullscreen() { + if (!emulation_running) { + return; + } + if (secondary_window->isFullScreen()) { + secondary_window->showNormal(); + } else { + secondary_window->showFullScreen(); + } +} + +void GMainWindow::ShowFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + UISettings::values.geometry = saveGeometry(); + ui->menubar->hide(); + statusBar()->hide(); + showFullScreen(); + } else { + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + render_window->showFullScreen(); + } +} + +void GMainWindow::HideFullscreen() { + if (ui->action_Single_Window_Mode->isChecked()) { + statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); + ui->menubar->show(); + showNormal(); + restoreGeometry(UISettings::values.geometry); + } else { + render_window->showNormal(); + render_window->restoreGeometry(UISettings::values.renderwindow_geometry); + } +} + +void GMainWindow::ToggleWindowMode() { + if (ui->action_Single_Window_Mode->isChecked()) { + // Render in the main window... + render_window->BackupGeometry(); + ui->horizontalLayout->addWidget(render_window); + render_window->setFocusPolicy(Qt::StrongFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->setFocus(); + game_list->hide(); + } + + } else { + // Render in a separate window... + ui->horizontalLayout->removeWidget(render_window); + render_window->setParent(nullptr); + render_window->setFocusPolicy(Qt::NoFocus); + if (emulation_running) { + render_window->setVisible(true); + render_window->RestoreGeometry(); + game_list->show(); + } + } +} + +void GMainWindow::UpdateSecondaryWindowVisibility() { + if (!emulation_running) { + return; + } + if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) { + secondary_window->RestoreGeometry(); + secondary_window->show(); + } else { + secondary_window->BackupGeometry(); + secondary_window->hide(); + } +} + +void GMainWindow::ChangeScreenLayout() { + Settings::LayoutOption new_layout = Settings::LayoutOption::Default; + + if (ui->action_Screen_Layout_Default->isChecked()) { + new_layout = Settings::LayoutOption::Default; + } else if (ui->action_Screen_Layout_Single_Screen->isChecked()) { + new_layout = Settings::LayoutOption::SingleScreen; + } else if (ui->action_Screen_Layout_Large_Screen->isChecked()) { + new_layout = Settings::LayoutOption::LargeScreen; + } else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) { + new_layout = Settings::LayoutOption::HybridScreen; + } else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) { + new_layout = Settings::LayoutOption::SideScreen; + } else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) { + new_layout = Settings::LayoutOption::SeparateWindows; + } + + Settings::values.layout_option = new_layout; + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::ToggleScreenLayout() { + const Settings::LayoutOption new_layout = []() { + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::Default: + return Settings::LayoutOption::SingleScreen; + case Settings::LayoutOption::SingleScreen: + return Settings::LayoutOption::LargeScreen; + case Settings::LayoutOption::LargeScreen: + return Settings::LayoutOption::HybridScreen; + case Settings::LayoutOption::HybridScreen: + return Settings::LayoutOption::SideScreen; + case Settings::LayoutOption::SideScreen: + return Settings::LayoutOption::SeparateWindows; + case Settings::LayoutOption::SeparateWindows: + return Settings::LayoutOption::Default; + default: + LOG_ERROR(Frontend, "Unknown layout option {}", + Settings::values.layout_option.GetValue()); + return Settings::LayoutOption::Default; + } + }(); + + Settings::values.layout_option = new_layout; + SyncMenuUISettings(); + system.ApplySettings(); + UpdateSecondaryWindowVisibility(); +} + +void GMainWindow::OnSwapScreens() { + Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::OnRotateScreens() { + Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked(); + system.ApplySettings(); +} + +void GMainWindow::TriggerSwapScreens() { + ui->action_Screen_Layout_Swap_Screens->trigger(); +} + +void GMainWindow::TriggerRotateScreens() { + ui->action_Screen_Layout_Upright_Screens->trigger(); +} + +void GMainWindow::OnSaveState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + system.SendSignal(Core::System::Signal::Save, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); + newest_slot = action->data().toUInt(); +} + +void GMainWindow::OnLoadState() { + QAction* action = qobject_cast(sender()); + ASSERT(action); + + if (UISettings::values.save_state_warning) { + QMessageBox::warning(this, tr("Savestates"), + tr("Warning: Savestates are NOT a replacement for in-game saves, " + "and are not meant to be reliable.\n\nUse at your own risk!")); + UISettings::values.save_state_warning = false; + config->Save(); + } + + system.SendSignal(Core::System::Signal::Load, action->data().toUInt()); + system.frame_limiter.AdvanceFrame(); +} + +void GMainWindow::OnConfigure() { + game_list->SetDirectoryWatcherEnabled(false); + Settings::SetConfiguringGlobal(true); + ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices, + !multiplayer_state->IsHostingPublicRoom()); + connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, + &GMainWindow::OnLanguageChanged); + auto old_theme = UISettings::values.theme; + const int old_input_profile_index = Settings::values.current_input_profile_index; + const auto old_input_profiles = Settings::values.input_profiles; + const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps; + const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); +#ifdef __unix__ + const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); +#endif + auto result = configureDialog.exec(); + game_list->SetDirectoryWatcherEnabled(true); + if (result == QDialog::Accepted) { + configureDialog.ApplyConfiguration(); + InitializeHotkeys(); + if (UISettings::values.theme != old_theme) { + UpdateUITheme(); + } + if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { + SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); + } +#ifdef __unix__ + if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { + SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); + } +#endif + if (!multiplayer_state->IsHostingPublicRoom()) + multiplayer_state->UpdateCredentials(); + emit UpdateThemedIcons(); + SyncMenuUISettings(); + game_list->RefreshGameDirectory(); + config->Save(); + if (UISettings::values.hide_mouse && emulation_running) { + setMouseTracking(true); + mouse_hide_timer.start(); + } else { + setMouseTracking(false); + } + UpdateSecondaryWindowVisibility(); + UpdateBootHomeMenuState(); + UpdateStatusButtons(); + } else { + Settings::values.input_profiles = old_input_profiles; + Settings::values.touch_from_button_maps = old_touch_from_button_maps; + Settings::LoadProfile(old_input_profile_index); + } +} + +void GMainWindow::OnLoadAmiibo() { + if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { + return; + } + + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (nfc == nullptr) { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (nfc->IsTagActive()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("A tag is already in use.")); + return; + } + + if (!nfc->IsSearchingForAmiibos()) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Game is not looking for amiibos.")); + return; + } + + const QString extensions{QStringLiteral("*.bin")}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); + + if (filename.isEmpty()) { + return; + } + + LoadAmiibo(filename); +} + +void GMainWindow::LoadAmiibo(const QString& filename) { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + if (!nfc->LoadAmiibo(filename.toStdString())) { + QMessageBox::warning(this, tr("Error opening amiibo data file"), + tr("Unable to open amiibo file \"%1\" for reading.").arg(filename)); + return; + } + + ui->action_Remove_Amiibo->setEnabled(true); +} + +void GMainWindow::OnRemoveAmiibo() { + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService("nfc:u"); + if (!nfc) [[unlikely]] { + return; + } + + std::scoped_lock lock{system.Kernel().GetHLELock()}; + nfc->RemoveAmiibo(); + ui->action_Remove_Amiibo->setEnabled(false); +} + +void GMainWindow::OnOpenLucina3DSFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); +} + +void GMainWindow::OnToggleFilterBar() { + game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); + if (ui->action_Show_Filter_Bar->isChecked()) { + game_list->SetFilterFocus(); + } else { + game_list->ClearFilter(); + } +} + +void GMainWindow::OnCreateGraphicsSurfaceViewer() { + auto graphicsSurfaceViewerWidget = + new GraphicsSurfaceWidget(system, Pica::g_debug_context, this); + addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget); + // TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true); + graphicsSurfaceViewerWidget->show(); +} + +void GMainWindow::OnRecordMovie() { + MovieRecordDialog dialog(this, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_record_on_start = true; + movie_record_path = dialog.GetPath(); + movie_record_author = dialog.GetAuthor(); + + if (emulation_running) { // Restart game + BootGame(QString(game_path)); + } + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(true); +} + +void GMainWindow::OnPlayMovie() { + MoviePlayDialog dialog(this, game_list, system); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + movie_playback_on_start = true; + movie_playback_path = dialog.GetMoviePath(); + BootGame(dialog.GetGamePath()); + + ui->action_Close_Movie->setEnabled(true); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnCloseMovie() { + if (movie_record_on_start) { + QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); + movie_record_on_start = false; + movie_record_path.clear(); + movie_record_author.clear(); + } else { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording; + movie.Shutdown(); + if (was_recording) { + QMessageBox::information(this, tr("Movie Saved"), + tr("The movie is successfully saved.")); + } + + if (was_running) { + OnStartGame(); + } + } + + ui->action_Close_Movie->setEnabled(false); + ui->action_Save_Movie->setEnabled(false); +} + +void GMainWindow::OnSaveMovie() { + const bool was_running = emu_thread && emu_thread->IsRunning(); + if (was_running) { + OnPauseGame(); + } + + if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) { + movie.SaveMovie(); + QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); + } else { + LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); + } + + if (was_running) { + OnStartGame(); + } +} + +void GMainWindow::OnCaptureScreenshot() { + if (!emu_thread) [[unlikely]] { + return; + } + + const bool was_running = emu_thread->IsRunning(); + + if (was_running || + (QMessageBox::question( + this, tr("Game will unpause"), + tr("The game will be unpaused, and the next frame will be captured. Is this okay?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { + if (was_running) { + OnPauseGame(); + } + std::string path = UISettings::values.screenshot_path.GetValue(); + if (!FileUtil::IsDirectory(path)) { + if (!FileUtil::CreateFullPath(path)) { + QMessageBox::information( + this, tr("Invalid Screenshot Directory"), + tr("Cannot create specified screenshot directory. Screenshot " + "path is set back to its default value.")); + path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + path.append("screenshots/"); + UISettings::values.screenshot_path = path; + }; + } + + static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); + const std::string filename = game_title.remove(expr).toStdString(); + const std::string timestamp = QDateTime::currentDateTime() + .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) + .toStdString(); + path.append(fmt::format("/{}_{}.png", filename, timestamp)); + + auto* const screenshot_window = + secondary_window->HasFocus() ? secondary_window : render_window; + screenshot_window->CaptureScreenshot( + UISettings::values.screenshot_resolution_factor.GetValue(), + QString::fromStdString(path)); + OnStartGame(); + } +} + +void GMainWindow::OnDumpVideo() { + if (DynamicLibrary::FFmpeg::LoadFFmpeg()) { + if (ui->action_Dump_Video->isChecked()) { + OnStartVideoDumping(); + } else { + OnStopVideoDumping(); + } + } else { + ui->action_Dump_Video->setChecked(false); + + QMessageBox message_box; + message_box.setWindowTitle(tr("Could not load video dumper")); + message_box.setText( + tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." +#ifdef _WIN32 + "\n\nTo install FFmpeg to Lucina3DS, press Open and select your FFmpeg directory." +#endif + "\n\nTo view a guide on how to install FFmpeg, press Help.")); + message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help +#ifdef _WIN32 + | QMessageBox::Open +#endif + ); + auto result = message_box.exec(); + if (result == QMessageBox::Help) { + QDesktopServices::openUrl(QUrl(QStringLiteral( + "https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/"))); +#ifdef _WIN32 + } else if (result == QMessageBox::Open) { + OnOpenFFmpeg(); +#endif + } + } +} + +#ifdef _WIN32 +void GMainWindow::OnOpenFFmpeg() { + auto filename = + QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString(); + if (filename.empty()) { + return; + } + // Check for a bin directory if they chose the FFmpeg root directory. + auto bin_dir = filename + DIR_SEP + "bin"; + if (!FileUtil::Exists(bin_dir)) { + // Otherwise, assume the user directly selected the directory containing the DLLs. + bin_dir = filename; + } + + static const std::array library_names = { + Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR), + Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR), + }; + + for (auto& library_name : library_names) { + if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("The provided FFmpeg directory is missing %1. Please make " + "sure the correct directory was selected.") + .arg(QString::fromStdString(library_name))); + return; + } + } + + std::atomic success(true); + auto process_file = [&success](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { + auto file_path = directory + DIR_SEP + virtual_name; + if (file_path.ends_with(".dll")) { + auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name; + if (!FileUtil::Copy(file_path, destination_path)) { + success.store(false); + return false; + } + } + return true; + }; + FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); + + if (success.load()) { + QMessageBox::information(this, tr("Lucina3DS"), tr("FFmpeg has been sucessfully installed.")); + } else { + QMessageBox::critical(this, tr("Lucina3DS"), + tr("Installation of FFmpeg failed. Check the log file for details.")); + } +} +#endif + +void GMainWindow::OnStartVideoDumping() { + DumpingDialog dialog(this, system); + if (dialog.exec() != QDialog::DialogCode::Accepted) { + ui->action_Dump_Video->setChecked(false); + return; + } + const auto path = dialog.GetFilePath(); + if (emulation_running) { + StartVideoDumping(path); + } else { + video_dumping_on_start = true; + video_dumping_path = path; + } +} + +void GMainWindow::StartVideoDumping(const QString& path) { + auto& renderer = system.GPU().Renderer(); + const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; + + auto dumper = std::make_shared(renderer); + if (dumper->StartDumping(path.toStdString(), layout)) { + system.RegisterVideoDumper(dumper); + } else { + QMessageBox::critical( + this, tr("Lucina3DS"), + tr("Could not start video dumping.
Refer to the log for details.")); + ui->action_Dump_Video->setChecked(false); + } +} + +void GMainWindow::OnStopVideoDumping() { + ui->action_Dump_Video->setChecked(false); + + if (video_dumping_on_start) { + video_dumping_on_start = false; + video_dumping_path.clear(); + } else { + auto dumper = system.GetVideoDumper(); + if (!dumper || !dumper->IsDumping()) { + return; + } + + game_paused_for_dumping = emu_thread->IsRunning(); + OnPauseGame(); + + auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); }); + auto* future_watcher = new QFutureWatcher(this); + connect(future_watcher, &QFutureWatcher::finished, this, [this] { + if (game_shutdown_delayed) { + game_shutdown_delayed = false; + ShutdownGame(); + } else if (game_paused_for_dumping) { + game_paused_for_dumping = false; + OnStartGame(); + } + }); + future_watcher->setFuture(future); + } +} + +void GMainWindow::UpdateStatusBar() { + if (!emu_thread) [[unlikely]] { + status_bar_update_timer.stop(); + return; + } + + // Update movie status + const u64 current = movie.GetCurrentInputIndex(); + const u64 total = movie.GetTotalInputCount(); + const auto play_mode = movie.GetPlayMode(); + if (play_mode == Core::Movie::PlayMode::Recording) { + message_label->setText(tr("Recording %1").arg(current)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(true); + } else if (play_mode == Core::Movie::PlayMode::Playing) { + message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { + message_label->setText(tr("Movie Finished")); + message_label_used_for_movie = true; + ui->action_Save_Movie->setEnabled(false); + } else if (message_label_used_for_movie) { // Clear the label if movie was just closed + message_label->setText(QString{}); + message_label_used_for_movie = false; + ui->action_Save_Movie->setEnabled(false); + } + + auto results = system.GetAndResetPerfStats(); + + if (show_artic_label) { + const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); + const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) + : (results.artic_transmitted / 1000.0); + static const std::array, 5> + perf_events = { + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, + tr("(Accessing SharedExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, + tr("(Accessing SystemSaveData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, + tr("(Accessing BossExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, + tr("(Accessing ExtData)")), + std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, + tr("(Accessing SaveData)")), + }; + + const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); + QString event{}; + for (auto p : perf_events) { + if (results.artic_events.Get(p.first)) { + event = QString::fromStdString(" ") + p.second; + break; + } + } + + static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), + QStringLiteral("#ff3333")}; + + int style_index; + + if (value > 200.0) { + style_index = 2; + } else if (value > 125.0) { + style_index = 1; + } else { + style_index = 0; + } + const QString style_sheet = + QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); + + artic_traffic_label->setText( + tr("Artic Base Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); + artic_traffic_label->setStyleSheet(style_sheet); + } + + if (Settings::values.frame_limit.GetValue() == 0) { + emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); + } else { + emu_speed_label->setText(tr("Speed: %1% / %2%") + .arg(results.emulation_speed * 100.0, 0, 'f', 0) + .arg(Settings::values.frame_limit.GetValue())); + } + game_fps_label->setText(tr("Game: %1 FPS").arg(results.game_fps, 0, 'f', 0)); + emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); + + if (show_artic_label) { + artic_traffic_label->setVisible(true); + } + emu_speed_label->setVisible(true); + game_fps_label->setVisible(true); + emu_frametime_label->setVisible(true); +} + +void GMainWindow::UpdateBootHomeMenuState() { + const auto current_region = Settings::values.region_value.GetValue(); + for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { + const auto path = Core::GetHomeMenuNcchPath(region); + ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled( + (current_region == Settings::REGION_VALUE_AUTO_SELECT || + current_region == static_cast(region)) && + !path.empty() && FileUtil::Exists(path)); + } +} + +void GMainWindow::HideMouseCursor() { + if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) { + mouse_hide_timer.stop(); + ShowMouseCursor(); + return; + } + render_window->setCursor(QCursor(Qt::BlankCursor)); + secondary_window->setCursor(QCursor(Qt::BlankCursor)); + if (UISettings::values.single_window_mode.GetValue()) { + setCursor(QCursor(Qt::BlankCursor)); + } +} + +void GMainWindow::ShowMouseCursor() { + unsetCursor(); + render_window->unsetCursor(); + secondary_window->unsetCursor(); + if (emu_thread && UISettings::values.hide_mouse) { + mouse_hide_timer.start(); + } +} + +void GMainWindow::OnMute() { + Settings::values.audio_muted = !Settings::values.audio_muted; + UpdateVolumeUI(); +} + +void GMainWindow::OnDecreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume <= 30) { + step = 2; + } + if (current_volume <= 6) { + step = 1; + } + const auto value = + static_cast(std::max(current_volume - step, 0)) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::OnIncreaseVolume() { + Settings::values.audio_muted = false; + const auto current_volume = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + int step = 5; + if (current_volume < 30) { + step = 2; + } + if (current_volume < 6) { + step = 1; + } + const auto value = static_cast(current_volume + step) / volume_slider->maximum(); + Settings::values.volume.SetValue(value); + UpdateVolumeUI(); +} + +void GMainWindow::UpdateVolumeUI() { + const auto volume_value = + static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); + volume_slider->setValue(volume_value); + if (Settings::values.audio_muted) { + volume_button->setChecked(false); + volume_button->setText(tr("VOLUME: MUTE")); + } else { + volume_button->setChecked(true); + volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); + } +} + +void GMainWindow::UpdateAPIIndicator(bool update) { + static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"), + QStringLiteral("VULKAN")}; + + static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"), + QStringLiteral("#91242a")}; + + u32 api_index = static_cast(Settings::values.graphics_api.GetValue()); + if (update) { + api_index = (api_index + 1) % graphics_apis.size(); + // Skip past any disabled renderers. +#ifndef ENABLE_SOFTWARE_RENDERER + if (api_index == static_cast(Settings::GraphicsAPI::Software)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_OPENGL + if (api_index == static_cast(Settings::GraphicsAPI::OpenGL)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif +#ifndef ENABLE_VULKAN + if (api_index == static_cast(Settings::GraphicsAPI::Vulkan)) { + api_index = (api_index + 1) % graphics_apis.size(); + } +#endif + Settings::values.graphics_api = static_cast(api_index); + } + + const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }") + .arg(graphics_api_colors[api_index]); + + graphics_api_button->setText(graphics_apis[api_index]); + graphics_api_button->setStyleSheet(style_sheet); +} + +void GMainWindow::UpdateStatusButtons() { + UpdateAPIIndicator(); + UpdateVolumeUI(); +} + +void GMainWindow::OnMouseActivity() { + ShowMouseCursor(); +} + +void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + OnMouseActivity(); +} + +void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { + QString status_message; + + QString title, message; + QMessageBox::Icon error_severity_icon; + bool can_continue = true; + if (result == Core::System::ResultStatus::ErrorSystemFiles) { + const QString common_message = + tr("%1 is missing. Please dump your " + "system archives.
Continuing emulation may result in crashes and bugs."); + + if (!details.empty()) { + message = common_message.arg(QString::fromStdString(details)); + } else { + message = common_message.arg(tr("A system archive")); + } + + title = tr("System Archive Not Found"); + status_message = tr("System Archive Missing"); + error_severity_icon = QMessageBox::Icon::Critical; + } else if (result == Core::System::ResultStatus::ErrorSavestate) { + title = tr("Save/load Error"); + message = QString::fromStdString(details); + error_severity_icon = QMessageBox::Icon::Warning; + } else if (result == Core::System::ResultStatus::ErrorArticDisconnected) { + title = tr("Artic Base Server"); + message = + tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details) + .c_str()); + error_severity_icon = QMessageBox::Icon::Critical; + can_continue = false; + } else { + title = tr("Fatal Error"); + message = + tr("A fatal error occurred. " + "Check " + "the log for details." + "
Continuing emulation may result in crashes and bugs."); + status_message = tr("Fatal Error encountered"); + error_severity_icon = QMessageBox::Icon::Critical; + } + + QMessageBox message_box; + message_box.setWindowTitle(title); + message_box.setText(message); + message_box.setIcon(error_severity_icon); + if (error_severity_icon == QMessageBox::Icon::Critical) { + if (can_continue) { + message_box.addButton(tr("Continue"), QMessageBox::RejectRole); + } + QPushButton* abort_button = message_box.addButton(tr("Quit Game"), QMessageBox::AcceptRole); + if (result != Core::System::ResultStatus::ShutdownRequested) + message_box.exec(); + + if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested || + message_box.clickedButton() == abort_button) { + if (emu_thread) { + ShutdownGame(); + return; + } + } + } else { + // This block should run when the error isn't too big of a deal + // e.g. when a save state can't be saved or loaded + message_box.addButton(tr("OK"), QMessageBox::RejectRole); + message_box.exec(); + } + + // Only show the message if the game is still running. + if (emu_thread) { + emu_thread->SetRunning(true); + message_label->setText(status_message); + message_label_used_for_movie = false; + } +} + +void GMainWindow::OnMenuAboutLucina3DS() { + AboutDialog about{this}; + about.exec(); +} + +bool GMainWindow::ConfirmClose() { + if (!emu_thread || !UISettings::values.confirm_before_closing) { + return true; + } + + QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Lucina3DS"), tr("Would you like to exit now?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::closeEvent(QCloseEvent* event) { + if (!ConfirmClose()) { + event->ignore(); + return; + } + + UpdateUISettings(); + game_list->SaveInterfaceLayout(); + hotkey_registry.SaveHotkeys(); + + // Shutdown session if the emu thread is active... + if (emu_thread) { + ShutdownGame(); + } + + render_window->close(); + secondary_window->close(); + multiplayer_state->Close(); + InputCommon::Shutdown(); + QWidget::closeEvent(event); +} + +static bool IsSingleFileDropEvent(const QMimeData* mime) { + return mime->hasUrls() && mime->urls().length() == 1; +} + +static const std::array AcceptedExtensions = {"cci", "3ds", "cxi", "bin", + "3dsx", "app", "elf", "axf"}; + +static bool IsCorrectFileExtension(const QMimeData* mime) { + const QString& filename = mime->urls().at(0).toLocalFile(); + return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), + QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); +} + +static bool IsAcceptableDropEvent(QDropEvent* event) { + return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); +} + +void GMainWindow::AcceptDropEvent(QDropEvent* event) { + if (IsAcceptableDropEvent(event)) { + event->setDropAction(Qt::DropAction::LinkAction); + event->accept(); + } +} + +bool GMainWindow::DropAction(QDropEvent* event) { + if (!IsAcceptableDropEvent(event)) { + return false; + } + + const QMimeData* mime_data = event->mimeData(); + const QString& filename = mime_data->urls().at(0).toLocalFile(); + + if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { + // Amiibo + LoadAmiibo(filename); + } else { + // Game + if (ConfirmChangeGame()) { + BootGame(filename); + } + } + return true; +} + +void GMainWindow::OnFileOpen(const QFileOpenEvent* event) { + BootGame(event->file()); +} + +void GMainWindow::dropEvent(QDropEvent* event) { + DropAction(event); +} + +void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { + AcceptDropEvent(event); +} + +void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { + AcceptDropEvent(event); +} + +bool GMainWindow::ConfirmChangeGame() { + if (!emu_thread) [[unlikely]] { + return true; + } + + auto answer = QMessageBox::question( + this, tr("Lucina3DS"), tr("The game is still running. Would you like to stop emulation?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + return answer != QMessageBox::No; +} + +void GMainWindow::filterBarSetChecked(bool state) { + ui->action_Show_Filter_Bar->setChecked(state); + emit(OnToggleFilterBar()); +} + +void GMainWindow::UpdateUITheme() { + const QString icons_base_path = QStringLiteral(":/icons/"); + const QString default_theme = QStringLiteral("default"); + const QString default_theme_path = icons_base_path + default_theme; + + const QString& current_theme = UISettings::values.theme; + const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second); + QStringList theme_paths(default_theme_paths); + + if (is_default_theme || current_theme.isEmpty()) { + const QString theme_uri(QStringLiteral(":default/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, + "Unable to open default stylesheet, falling back to empty stylesheet"); + qApp->setStyleSheet({}); + setStyleSheet({}); + } + theme_paths.append(default_theme_path); + QIcon::setThemeName(default_theme); + } else { + const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss")); + QFile f(theme_uri); + if (f.open(QFile::ReadOnly | QFile::Text)) { + QTextStream ts(&f); + qApp->setStyleSheet(ts.readAll()); + setStyleSheet(ts.readAll()); + } else { + LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found"); + } + + const QString current_theme_path = icons_base_path + current_theme; + theme_paths.append({default_theme_path, current_theme_path}); + QIcon::setThemeName(current_theme); + } + + QIcon::setThemeSearchPaths(theme_paths); +} + +void GMainWindow::LoadTranslation() { + // If the selected language is English, no need to install any translation + if (UISettings::values.language == QStringLiteral("en")) { + return; + } + + bool loaded; + + if (UISettings::values.language.isEmpty()) { + // Use the system's default locale + loaded = translator.load(QLocale::system(), {}, {}, QStringLiteral(":/languages/")); + } else { + // Otherwise load from the specified file + loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/")); + } + + if (loaded) { + qApp->installTranslator(&translator); + } else { + UISettings::values.language = QStringLiteral("en"); + } +} + +void GMainWindow::OnLanguageChanged(const QString& locale) { + if (UISettings::values.language != QStringLiteral("en")) { + qApp->removeTranslator(&translator); + } + + UISettings::values.language = locale; + LoadTranslation(); + ui->retranslateUi(this); + RetranslateStatusBar(); + UpdateWindowTitle(); +} + +void GMainWindow::OnConfigurePerGame() { + u64 title_id{}; + system.GetAppLoader().ReadProgramId(title_id); + OpenPerGameConfiguration(title_id, game_path); +} + +void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) { + Settings::SetConfiguringGlobal(false); + ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system); + const auto result = dialog.exec(); + + if (result != QDialog::Accepted) { + Settings::RestoreGlobalState(system.IsPoweredOn()); + return; + } else if (result == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } + + // Do not cause the global config to write local settings into the config file + const bool is_powered_on = system.IsPoweredOn(); + Settings::RestoreGlobalState(system.IsPoweredOn()); + + if (!is_powered_on) { + config->Save(); + } + + UpdateStatusButtons(); +} + +void GMainWindow::OnMoviePlaybackCompleted() { + OnPauseGame(); + QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); +} + +void GMainWindow::UpdateWindowTitle() { + const QString full_name = QString::fromUtf8(Common::g_build_fullname); + + if (game_title.isEmpty()) { + setWindowTitle(QStringLiteral("%1").arg(full_name)); + } else { + setWindowTitle(QStringLiteral("%1 | %2").arg(full_name, game_title)); + render_window->setWindowTitle( + QStringLiteral("%1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); + secondary_window->setWindowTitle(QStringLiteral("%1 | %2 | %3") + .arg(full_name, game_title, tr("Secondary Window"))); + } +} + +void GMainWindow::UpdateUISettings() { + if (!ui->action_Fullscreen->isChecked()) { + UISettings::values.geometry = saveGeometry(); + UISettings::values.renderwindow_geometry = render_window->saveGeometry(); + } + UISettings::values.state = saveState(); +#if MICROPROFILE_ENABLED + UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); + UISettings::values.microprofile_visible = microProfileDialog->isVisible(); +#endif + UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); + UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); + UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); + UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); + UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); + UISettings::values.first_start = false; +} + +void GMainWindow::SyncMenuUISettings() { + ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::Default); + ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SingleScreen); + ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::LargeScreen); + ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::HybridScreen); + ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() == + Settings::LayoutOption::SideScreen); + ui->action_Screen_Layout_Separate_Windows->setChecked( + Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); + ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue()); + ui->action_Screen_Layout_Upright_Screens->setChecked( + Settings::values.upright_screen.GetValue()); +} + +void GMainWindow::RetranslateStatusBar() { + if (emu_thread) + UpdateStatusBar(); + + emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " + "indicate emulation is running faster or slower than a 3DS.")); + game_fps_label->setToolTip(tr("How many frames per second the game is currently displaying. " + "This will vary from game to game and scene to scene.")); + emu_frametime_label->setToolTip( + tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " + "full-speed emulation this should be at most 16.67 ms.")); + + multiplayer_state->retranslateUi(); +} + +void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { +#ifdef USE_DISCORD_PRESENCE + if (state) { + discord_rpc = std::make_unique(system); + } else { + discord_rpc = std::make_unique(); + } +#else + discord_rpc = std::make_unique(); +#endif + discord_rpc->Update(); +} + +#ifdef __unix__ +void GMainWindow::SetGamemodeEnabled(bool state) { + if (emulation_running) { + Common::Linux::SetGamemodeState(state); + } +} +#endif + +#ifdef main +#undef main +#endif + +static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() { +#ifdef _WIN32 + // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. + // This is done by setting the optimal scaling policy for the primary screen. + + // Create a temporary QApplication. + int temp_argc = 0; + char** temp_argv = nullptr; + QApplication temp{temp_argc, temp_argv}; + + // Get the current screen geometry. + const QScreen* primary_screen = QGuiApplication::primaryScreen(); + if (!primary_screen) { + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; + } + + const QRect screen_rect = primary_screen->geometry(); + const qreal real_ratio = primary_screen->devicePixelRatio(); + const qreal real_width = std::trunc(screen_rect.width() * real_ratio); + const qreal real_height = std::trunc(screen_rect.height() * real_ratio); + + // Recommended minimum width and height for proper window fit. + // Any screen with a lower resolution than this will still have a scale of 1. + constexpr qreal minimum_width = 1350.0; + constexpr qreal minimum_height = 900.0; + + const qreal width_ratio = std::max(1.0, real_width / minimum_width); + const qreal height_ratio = std::max(1.0, real_height / minimum_height); + + // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. + const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio)); + + if (max_ratio > real_ratio) { + return Qt::HighDpiScaleFactorRoundingPolicy::Round; + } else { + return Qt::HighDpiScaleFactorRoundingPolicy::Floor; + } +#else + // Other OSes should be better than Windows at fractional scaling. + return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; +#endif +} + +int main(int argc, char* argv[]) { + Common::DetachedTasks detached_tasks; + MicroProfileOnThreadCreate("Frontend"); + SCOPE_EXIT({ MicroProfileShutdown(); }); + + // Init settings params + QCoreApplication::setOrganizationName(QStringLiteral("Citra team | Lucina3DS")); + QCoreApplication::setApplicationName(QStringLiteral("Lucina3DS")); + + auto rounding_policy = GetHighDpiRoundingPolicy(); + QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); + +#ifdef __APPLE__ + auto bundle_dir = FileUtil::GetBundleDirectory(); + if (bundle_dir) { + FileUtil::SetCurrentDir(bundle_dir.value() + ".."); + } +#endif + +#ifdef ENABLE_OPENGL + QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); +#endif + + QApplication app(argc, argv); + app.setWindowIcon(QIcon(QString::fromUtf8("/usr/share/icons/hicolor/512x512/apps/lucina3ds.png"))); + + + // Qt changes the locale and causes issues in float conversion using std::to_string() when + // generating shaders + setlocale(LC_ALL, "C"); + + auto& system{Core::System::GetInstance()}; + + // Register Qt image interface + system.RegisterImageInterface(std::make_shared()); + + GMainWindow main_window(system); + + // Register frontend applets + Frontend::RegisterDefaultApplets(system); + + system.RegisterMiiSelector(std::make_shared(main_window)); + system.RegisterSoftwareKeyboard(std::make_shared(main_window)); + +#ifdef __APPLE__ + // Register microphone permission check. + system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); +#endif + + main_window.setWindowIcon(QIcon(QString::fromUtf8("/usr/share/icons/hicolor/512x512/apps/lucina3ds.png"))); + main_window.show(); + + QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, + &GMainWindow::OnAppFocusStateChanged); + + int result = app.exec(); + detached_tasks.WaitForAllTasks(); + return result; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c227f06..53d13987 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modul include(DownloadExternals) include(CMakeDependentOption) -project(citra LANGUAGES C CXX ASM) +project(Lucina3DS LANGUAGES C CXX ASM) # Some submodules like to pick their own default build type if not specified. # Make sure we default to Release build type always, unless the generator has custom types. @@ -88,12 +88,12 @@ option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF) # Compile options CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF) option(ENABLE_LTO "Enable link time optimization" ${DEFAULT_ENABLE_LTO}) -option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) -option(CITRA_WARNINGS_AS_ERRORS "Enable warnings as errors" ON) +option(LUCINA3DS_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) +option(LUCINA3DS_WARNINGS_AS_ERRORS "Enable warnings as errors" ON) include(CitraHandleSystemLibs) -if (CITRA_USE_PRECOMPILED_HEADERS) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) message(STATUS "Using Precompiled Headers.") set(CMAKE_PCH_INSTANTIATE_TEMPLATES ON) @@ -434,11 +434,11 @@ add_subdirectory(src) add_subdirectory(dist/installer) -# Set citra-qt project or citra project as default StartUp Project in Visual Studio depending on whether QT is enabled or not +# Set lucina3ds-qt project or lucina3ds project as default StartUp Project in Visual Studio depending on whether QT is enabled or not if(ENABLE_QT) - set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT citra-qt) + set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT lucina3ds-qt) else() - set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT citra) + set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT lucina3ds) endif() # Create target for outputting distributable bundles. @@ -446,13 +446,13 @@ endif() if (NOT ANDROID AND NOT IOS) include(BundleTarget) if (ENABLE_SDL2_FRONTEND) - bundle_target(citra) + bundle_target(lucina3ds) endif() if (ENABLE_QT) - bundle_target(citra-qt) + bundle_target(lucina3ds-qt) endif() if (ENABLE_DEDICATED_ROOM) - bundle_target(citra-room) + bundle_target(lucina3ds-room) endif() endif() @@ -464,22 +464,22 @@ endif() # http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html # http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html if(ENABLE_QT AND UNIX AND NOT APPLE) - install(FILES "${PROJECT_SOURCE_DIR}/dist/citra-qt.desktop" + install(FILES "${PROJECT_SOURCE_DIR}/dist/lucina3ds-qt.desktop" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/applications") - install(FILES "${PROJECT_SOURCE_DIR}/dist/citra.svg" + install(FILES "${PROJECT_SOURCE_DIR}/dist/lucina3ds.svg" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/apps") - install(FILES "${PROJECT_SOURCE_DIR}/dist/citra.xml" + install(FILES "${PROJECT_SOURCE_DIR}/dist/lucina3ds.xml" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/mime/packages") endif() if(UNIX) if(ENABLE_SDL2) - install(FILES "${PROJECT_SOURCE_DIR}/dist/citra.6" + install(FILES "${PROJECT_SOURCE_DIR}/dist/lucina3ds.6" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/man/man6") endif() if (ENABLE_QT) - install(FILES "${PROJECT_SOURCE_DIR}/dist/citra-qt.6" + install(FILES "${PROJECT_SOURCE_DIR}/dist/lucina3ds-qt.6" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/man/man6") endif() endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 3e98984c..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -**The Contributor's Guide has moved to [the wiki](https://github.com/citra-emu/citra/wiki/Contributing).** \ No newline at end of file diff --git a/Doxyfile b/Doxyfile index 6ddf2cf2..860984ef 100644 --- a/Doxyfile +++ b/Doxyfile @@ -32,7 +32,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = Citra +PROJECT_NAME = Lucina3DS # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version diff --git a/dist/Lucina3DS.png b/dist/Lucina3DS.png new file mode 100644 index 00000000..cc93e355 Binary files /dev/null and b/dist/Lucina3DS.png differ diff --git a/dist/citra-room.desktop b/dist/citra-room.desktop deleted file mode 100644 index 89df53bb..00000000 --- a/dist/citra-room.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Name=Citra Room -Comment=Multiplayer room host for Citra -Icon=citra -TryExec=citra-room -Exec=citra-room %f -Categories=Game;Emulator; -Keywords=3DS;Nintendo diff --git a/dist/citra.ico b/dist/citra.ico deleted file mode 100644 index 2c408b93..00000000 Binary files a/dist/citra.ico and /dev/null differ diff --git a/dist/citra.svg b/dist/citra.svg deleted file mode 100644 index b6abc1cc..00000000 --- a/dist/citra.svg +++ /dev/null @@ -1,2 +0,0 @@ - -image/svg+xml diff --git a/dist/compatibility_list/compatibility_list.json b/dist/compatibility_list/compatibility_list.json index e69de29b..ca7f3ff9 100644 --- a/dist/compatibility_list/compatibility_list.json +++ b/dist/compatibility_list/compatibility_list.json @@ -0,0 +1,9638 @@ +[ + { + "compatibility": 0, + "directory": "10-in-1-arcade-collection", + "releases": [ + {"id": "00040000000C1400"} + ], + "title": "10-in-1: Arcade Collection" + }, + { + "compatibility": 99, + "directory": "1001-spikes", + "releases": [ + {"id": "000400000008FE00"} + ], + "title": "1001 Spikes" + }, + { + "compatibility": 99, + "directory": "101-dinopets-3d", + "releases": [ + {"id": "00040000000B8500"} + ], + "title": "101 DinoPets 3D" + }, + { + "compatibility": 99, + "directory": "101-penguin-pets-3d", + "releases": [ + {"id": "00040000000EBE00"} + ], + "title": "101 Penguin Pets 3D" + }, + { + "compatibility": 99, + "directory": "101-pony-pets-3d", + "releases": [ + {"id": "0004000000124F00"} + ], + "title": "101 Pony Pets 3D" + }, + { + "compatibility": 99, + "directory": "2-fast-4-gnomz", + "releases": [ + {"id": "0004000000097200"} + ], + "title": "2 Fast 4 Gnomz" + }, + { + "compatibility": 0, + "directory": "2048", + "releases": [ + {"id": "0004000000139000"} + ], + "title": "2048" + }, + { + "compatibility": 99, + "directory": "36-fragments-of-midnight", + "releases": [ + {"id": "000400000F70E300"} + ], + "title": "36 Fragments of Midnight" + }, + { + "compatibility": 4, + "directory": "3d-after-burner-ii", + "releases": [ + {"id": "0004000000158900"} + ], + "title": "3D After Burner II" + }, + { + "compatibility": 99, + "directory": "3d-altered-beast", + "releases": [ + {"id": "00040000000D9D00"} + ], + "title": "3D Altered Beast" + }, + { + "compatibility": 0, + "directory": "3d-classics-excitebike", + "releases": [ + {"id": "0004000000054300"} + ], + "title": "3D Classics: Excitebike" + }, + { + "compatibility": 1, + "directory": "3d-classics-kid-icarus", + "releases": [ + {"id": "0004000000054200"} + ], + "title": "3D Classics: Kid Icarus" + }, + { + "compatibility": 0, + "directory": "3d-classics-kirbys-adventure", + "releases": [ + {"id": "0004000000054600"} + ], + "title": "3D Classics: Kirby's Adventure" + }, + { + "compatibility": 1, + "directory": "3d-classics-twinbee", + "releases": [ + {"id": "0004000000054400"} + ], + "title": "3D Classics: TwinBee" + }, + { + "compatibility": 0, + "directory": "3d-classics-urban-champion", + "releases": [ + {"id": "0004000000054500"} + ], + "title": "3D Classics: Urban Champion" + }, + { + "compatibility": 2, + "directory": "3d-classics-xevious", + "releases": [ + {"id": "0004000000054700"} + ], + "title": "3D Classics: Xevious" + }, + { + "compatibility": 0, + "directory": "3d-ecco-the-dolphin", + "releases": [ + {"id": "00040000000D9E00"} + ], + "title": "3D Ecco the Dolphin" + }, + { + "compatibility": 4, + "directory": "3d-fantasy-zone", + "releases": [ + {"id": "0004000000152300"} + ], + "title": "3D Fantasy Zone" + }, + { + "compatibility": 2, + "directory": "3d-fantasy-zone-ii-w", + "releases": [ + {"id": "0004000000158B00"} + ], + "title": "3D Fantasy Zone II W" + }, + { + "compatibility": 4, + "directory": "3d-galaxy-force-ii", + "releases": [ + {"id": "00040000000C0B00"} + ], + "title": "3D Galaxy Force II" + }, + { + "compatibility": 99, + "directory": "3d-game-collection", + "releases": [ + {"id": "00040000000C0500"} + ], + "title": "3D Game Collection" + }, + { + "compatibility": 99, + "directory": "3d-gunstar-heroes", + "releases": [ + {"id": "0004000000158400"} + ], + "title": "3D Gunstar Heroes" + }, + { + "compatibility": 99, + "directory": "3d-mahjongg", + "releases": [ + {"id": "00040000000C1000"} + ], + "title": "3D MahJongg" + }, + { + "compatibility": 4, + "directory": "3d-out-run", + "releases": [ + {"id": "0004000000158A00"} + ], + "title": "3D Out Run" + }, + { + "compatibility": 99, + "directory": "3d-retro-dungeon-puzzle-challenge", + "releases": [ + {"id": "000400000F710A00"} + ], + "title": "3D Retro Dungeon Puzzle Challenge" + }, + { + "compatibility": 99, + "directory": "3d-shinobi-iii-return-of-the-ninja-master", + "releases": [ + {"id": "00040000000D9F00"} + ], + "title": "3D Shinobi III: Return of the Ninja Master" + }, + { + "compatibility": 99, + "directory": "3d-solitaire", + "releases": [ + {"id": "000400000008C100"} + ], + "title": "3D Solitaire" + }, + { + "compatibility": 1, + "directory": "3d-sonic-the-hedgehog", + "releases": [ + {"id": "00040000000DA000"} + ], + "title": "3D Sonic The Hedgehog" + }, + { + "compatibility": 3, + "directory": "3d-sonic-the-hedgehog-2", + "releases": [ + {"id": "0004000000158D00"} + ], + "title": "3D Sonic The Hedgehog 2" + }, + { + "compatibility": 0, + "directory": "3d-space-harrier", + "releases": [ + {"id": "00040000000C0C00"} + ], + "title": "3D Space Harrier" + }, + { + "compatibility": 1, + "directory": "3d-streets-of-rage", + "releases": [ + {"id": "00040000000DA100"} + ], + "title": "3D Streets of Rage" + }, + { + "compatibility": 2, + "directory": "3d-streets-of-rage-2", + "releases": [ + {"id": "0004000000158300"} + ], + "title": "3D Streets of Rage 2" + }, + { + "compatibility": 0, + "directory": "3d-super-hang-on", + "releases": [ + {"id": "00040000000C0D00"} + ], + "title": "3D Super Hang-On" + }, + { + "compatibility": 4, + "directory": "3d-thunder-blade", + "releases": [ + {"id": "0004000000158C00"} + ], + "title": "3D Thunder Blade" + }, + { + "compatibility": 99, + "directory": "50-classic-games-3d", + "releases": [ + {"id": "00040000000BF300"} + ], + "title": "50 Classic Games 3D" + }, + { + "compatibility": 99, + "directory": "6180-the-moon", + "releases": [ + {"id": "000400000F70A500"} + ], + "title": "6180 the moon" + }, + { + "compatibility": 0, + "directory": "7th-dragon-iii-code-vfd", + "releases": [ + {"id": "000400000018F800"} + ], + "title": "7th Dragon III Code: VFD" + }, + { + "compatibility": 0, + "directory": "80s-overdrive", + "releases": [ + {"id": "000400000018F600"} + ], + "title": "80'S OVERDRIVE" + }, + { + "compatibility": 1, + "directory": "a-train-3d-city-simulator", + "releases": [ + {"id": "0004000000155700"} + ], + "title": "A-Train 3D: City Simulator" + }, + { + "compatibility": 2, + "directory": "ace-combat-assault-horizon-legacy", + "releases": [ + {"id": "0004000000064900"} + ], + "title": "ACE COMBAT Assault Horizon Legacy" + }, + { + "compatibility": 1, + "directory": "adventure-bar-story", + "releases": [ + {"id": "0004000000160F00"} + ], + "title": "Adventure Bar Story" + }, + { + "compatibility": 99, + "directory": "adventure-island", + "releases": [ + {"id": "000400000004FE00"} + ], + "title": "Adventure Island" + }, + { + "compatibility": 99, + "directory": "adventure-island-ii", + "releases": [ + {"id": "00040000000CA700"} + ], + "title": "Adventure Island II" + }, + { + "compatibility": 99, + "directory": "adventure-labyrinth-story", + "releases": [ + {"id": "0004000000194300"} + ], + "title": "Adventure Labyrinth Story" + }, + { + "compatibility": 99, + "directory": "adventures-of-lolo", + "releases": [ + {"id": "000400000010FA00"} + ], + "title": "Adventures of Lolo" + }, + { + "compatibility": 99, + "directory": "aero-porter", + "releases": [ + {"id": "00040000000BC000"} + ], + "title": "AERO PORTER" + }, + { + "compatibility": 1, + "directory": "aeternoblade", + "releases": [ + {"id": "00040000000CD300"} + ], + "title": "AeternoBlade" + }, + { + "compatibility": 0, + "directory": "airace-speed", + "releases": [ + {"id": "00040000000A2F00"} + ], + "title": "AiRace Speed" + }, + { + "compatibility": 99, + "directory": "airace-xeno", + "releases": [ + {"id": "000400000012BF00"} + ], + "title": "AiRace Xeno" + }, + { + "compatibility": 99, + "directory": "akari-by-nikoli", + "releases": [ + {"id": "000400000008CE00"} + ], + "title": "Akari by Nikoli" + }, + { + "compatibility": 99, + "directory": "alchemic-dungeons", + "releases": [ + {"id": "00040000001C6B00"} + ], + "title": "Alchemic Dungeons" + }, + { + "compatibility": 99, + "directory": "alien-on-the-run", + "releases": [ + {"id": "0004000000127600"} + ], + "title": "Alien On The Run" + }, + { + "compatibility": 99, + "directory": "alleyway", + "releases": [ + {"id": "0004000000040E00"} + ], + "title": "Alleyway" + }, + { + "compatibility": 2, + "directory": "alphadia", + "releases": [ + {"id": "0004000000188100"} + ], + "title": "Alphadia" + }, + { + "compatibility": 99, + "directory": "alter-world", + "releases": [ + {"id": "000400000F70F300"} + ], + "title": "Alter World" + }, + { + "compatibility": 99, + "directory": "ambition-of-the-slimes", + "releases": [ + {"id": "00040000001A1700"} + ], + "title": "Ambition of the Slimes" + }, + { + "compatibility": 99, + "directory": "anglers-club-ultimate-bass-fishing-3d", + "releases": [ + {"id": "0004000000048200"} + ], + "title": "Angler's Club: Ultimate Bass Fishing 3D" + }, + { + "compatibility": 99, + "directory": "angry-bunnies", + "releases": [ + {"id": "00040000000C5800"} + ], + "title": "Angry Bunnies" + }, + { + "compatibility": 2, + "directory": "angry-video-game-nerd-adventures", + "releases": [ + {"id": "0004000000161000"} + ], + "title": "Angry Video Game Nerd Adventures" + }, + { + "compatibility": 1, + "directory": "animal-crossing-happy-home-designer", + "releases": [ + {"id": "000400000014F100"} + ], + "title": "Animal Crossing: Happy Home Designer" + }, + { + "compatibility": 0, + "directory": "animal-crossing-new-leaf", + "releases": [ + {"id": "0004000000086300"}, + {"id": "0004000000086400"} + ], + "title": "Animal Crossing: New Leaf" + }, + { + "compatibility": 0, + "directory": "animal-crossing-new-leaf-welcome-amiibo", + "releases": [ + {"id": "0004000000198E00"} + ], + "title": "Animal Crossing: New Leaf Welcome amiibo" + }, + { + "compatibility": 99, + "directory": "anime-workshop", + "releases": [ + {"id": "00040000001BEF00"} + ], + "title": "Anime Workshop" + }, + { + "compatibility": 99, + "directory": "another-world-20th-anniversary-edition", + "releases": [ + {"id": "0004000000127200"} + ], + "title": "Another World - 20th Anniversary Edition" + }, + { + "compatibility": 2, + "directory": "apollo-justice-ace-attorney", + "releases": [ + {"id": "00040000001BD200"} + ], + "title": "Apollo Justice: Ace Attorney" + }, + { + "compatibility": 0, + "directory": "aqua-moto-racing-3d", + "releases": [ + {"id": "0004000000095200"} + ], + "title": "Aqua Moto Racing 3D" + }, + { + "compatibility": 99, + "directory": "arc-style-baseball-3d", + "releases": [ + {"id": "000400000010C600"} + ], + "title": "ARC STYLE: Baseball 3D" + }, + { + "compatibility": 99, + "directory": "arc-style-soccer-3d", + "releases": [ + {"id": "0004000000086A00"} + ], + "title": "ARC STYLE: Soccer 3D" + }, + { + "compatibility": 99, + "directory": "arc-style-solitaire", + "releases": [ + {"id": "00040000000AE900"} + ], + "title": "ARC STYLE: Solitaire" + }, + { + "compatibility": 99, + "directory": "arcade-classics-3d", + "releases": [ + {"id": "0004000000107600"} + ], + "title": "Arcade Classics 3D" + }, + { + "compatibility": 99, + "directory": "are-you-smarter-than-a-5th-grader", + "releases": [ + {"id": "000400000016F100"} + ], + "title": "Are You Smarter Than a 5th Grader?" + }, + { + "compatibility": 4, + "directory": "art-academy-lessons-for-everyone", + "releases": [ + {"id": "0004000000095800"} + ], + "title": "Art Academy: Lessons for Everyone!" + }, + { + "compatibility": 1, + "directory": "art-of-balance-touch", + "releases": [ + {"id": "000400000008FF00"} + ], + "title": "Art of Balance TOUCH!" + }, + { + "compatibility": 99, + "directory": "ascent-of-kings", + "releases": [ + {"id": "000400000F70BC00"} + ], + "title": "Ascent of Kings" + }, + { + "compatibility": 0, + "directory": "asdivine-cross", + "releases": [ + {"id": "00040000001C5000"} + ], + "title": "Asdivine Cross" + }, + { + "compatibility": 99, + "directory": "ash", + "releases": [ + {"id": "000400000016EE00"} + ], + "title": "ASH" + }, + { + "compatibility": 0, + "directory": "asphalt-3d", + "releases": [ + {"id": "0004000000034C00"} + ], + "title": "Asphalt 3D" + }, + { + "compatibility": 99, + "directory": "asterix-the-mansions-of-the-gods", + "releases": [ + {"id": "0004000000150C00"} + ], + "title": "Asterix The Mansions of the Gods" + }, + { + "compatibility": 99, + "directory": "atlantic-quest", + "releases": [ + {"id": "0004000000114000"} + ], + "title": "Atlantic Quest" + }, + { + "compatibility": 2, + "directory": "attack-of-the-friday-monsters-a-tokyo-tale", + "releases": [ + {"id": "00040000000E7600"} + ], + "title": "ATTACK OF THE FRIDAY MONSTERS! A TOKYO TALE" + }, + { + "compatibility": 1, + "directory": "atv-wild-ride-3d", + "releases": [ + {"id": "000400000009CA00"} + ], + "title": "ATV Wild Ride 3D" + }, + { + "compatibility": 99, + "directory": "avenging-spirit", + "releases": [ + {"id": "0004000000049800"} + ], + "title": "Avenging Spirit" + }, + { + "compatibility": 99, + "directory": "azada", + "releases": [ + {"id": "0004000000130A00"} + ], + "title": "Azada" + }, + { + "compatibility": 99, + "directory": "azure-snake", + "releases": [ + {"id": "00040000001D8400"} + ], + "title": "Azure Snake" + }, + { + "compatibility": 0, + "directory": "azure-striker-gunovlt-striker-pack", + "releases": [ + {"id": "00040000001A5600"} + ], + "title": "Azure Striker Gunovlt Striker Pack" + }, + { + "compatibility": 1, + "directory": "azure-striker-gunvolt", + "releases": [ + {"id": "000400000014C800"} + ], + "title": "Azure Striker GUNVOLT" + }, + { + "compatibility": 0, + "directory": "azure-striker-gunvolt-2", + "releases": [ + {"id": "0004000000196A00"} + ], + "title": "Azure Striker GUNVOLT 2" + }, + { + "compatibility": 99, + "directory": "azure-striker-gunvolt-the-anime", + "releases": [ + {"id": "00040000001B7600"} + ], + "title": "Azure Striker Gunvolt: The Anime" + }, + { + "compatibility": 0, + "directory": "balloon-fight", + "releases": [ + {"id": "0004000000070300"} + ], + "title": "Balloon Fight" + }, + { + "compatibility": 99, + "directory": "balloon-kid", + "releases": [ + {"id": "0004000000067200"} + ], + "title": "Balloon Kid" + }, + { + "compatibility": 99, + "directory": "balloon-pop-2", + "releases": [ + {"id": "0004000000035C00"} + ], + "title": "Balloon Pop 2" + }, + { + "compatibility": 99, + "directory": "balloon-pop-remix", + "releases": [ + {"id": "00040000000A8300"} + ], + "title": "Balloon Pop Remix" + }, + { + "compatibility": 99, + "directory": "banana-bliss-jungle-puzzles", + "releases": [ + {"id": "0004000000116B00"} + ], + "title": "Banana Bliss: Jungle Puzzles" + }, + { + "compatibility": 99, + "directory": "baseball", + "releases": [ + {"id": "0004000000040C00"} + ], + "title": "Baseball" + }, + { + "compatibility": 99, + "directory": "bases-loaded", + "releases": [ + {"id": "00040000000CE500"} + ], + "title": "Bases Loaded" + }, + { + "compatibility": 3, + "directory": "batman-arkham-origins-blackgate", + "releases": [ + {"id": "00040000000D6B00"} + ], + "title": "Batman: Arkham Origins Blackgate" + }, + { + "compatibility": 99, + "directory": "battleminer", + "releases": [ + {"id": "000400000014C900"} + ], + "title": "Battleminer" + }, + { + "compatibility": 1, + "directory": "battleminerz", + "releases": [ + {"id": "00040000001B4200"} + ], + "title": "Battleminerz" + }, + { + "compatibility": 2, + "directory": "battleship", + "releases": [ + {"id": "0004000000071700"} + ], + "title": "BATTLESHIP" + }, + { + "compatibility": 3, + "directory": "ben-10-galactic-racing", + "releases": [ + {"id": "0004000000040700"} + ], + "title": "Ben 10 Galactic Racing" + }, + { + "compatibility": 2, + "directory": "ben-10-omniverse", + "releases": [ + {"id": "000400000009D000"} + ], + "title": "Ben 10 Omniverse" + }, + { + "compatibility": 99, + "directory": "best-of-arcade-games-air-hockey", + "releases": [ + {"id": "0004000000101600"} + ], + "title": "Best of Arcade Games - Air Hockey" + }, + { + "compatibility": 99, + "directory": "best-of-arcade-games-brick-breaker", + "releases": [ + {"id": "0004000000101E00"} + ], + "title": "Best of Arcade Games - Brick Breaker" + }, + { + "compatibility": 99, + "directory": "best-of-arcade-games-bubble-buster", + "releases": [ + {"id": "0004000000101700"} + ], + "title": "Best of Arcade Games - Bubble Buster" + }, + { + "compatibility": 99, + "directory": "best-of-arcade-games-tetraminos", + "releases": [ + {"id": "0004000000101800"} + ], + "title": "Best of Arcade Games - Tetraminos" + }, + { + "compatibility": 99, + "directory": "best-of-board-games-chess", + "releases": [ + {"id": "0004000000101900"} + ], + "title": "Best of Board Games - Chess" + }, + { + "compatibility": 99, + "directory": "best-of-board-games-mahjong", + "releases": [ + {"id": "0004000000101A00"} + ], + "title": "Best of Board Games - Mahjong" + }, + { + "compatibility": 99, + "directory": "best-of-board-games-solitaire", + "releases": [ + {"id": "0004000000101B00"} + ], + "title": "Best of Board Games - Solitaire" + }, + { + "compatibility": 99, + "directory": "best-of-casual-games", + "releases": [ + {"id": "000400000011DC00"} + ], + "title": "Best of Casual Games" + }, + { + "compatibility": 99, + "directory": "best-of-mahjong", + "releases": [ + {"id": "0004000000123500"} + ], + "title": "Best of Mahjong" + }, + { + "compatibility": 99, + "directory": "best-of-solitaire", + "releases": [ + {"id": "0004000000123600"} + ], + "title": "Best of Solitaire" + }, + { + "compatibility": 1, + "directory": "beyblade-evolution", + "releases": [ + {"id": "00040000000CBF00"} + ], + "title": "Beyblade Evolution" + }, + { + "compatibility": 99, + "directory": "big-bass-arcade-no-limit", + "releases": [ + {"id": "00040000000D6600"} + ], + "title": "Big Bass Arcade: No Limit" + }, + { + "compatibility": 99, + "directory": "bike-rider-dx", + "releases": [ + {"id": "00040000000FC000"} + ], + "title": "Bike Rider DX" + }, + { + "compatibility": 0, + "directory": "bike-rider-dx2-galaxy", + "releases": [ + {"id": "0004000000128600"} + ], + "title": "BIKE RIDER DX2: GALAXY" + }, + { + "compatibility": 99, + "directory": "bingo-collection", + "releases": [ + {"id": "00040000001BD100"} + ], + "title": "Bingo Collection" + }, + { + "compatibility": 99, + "directory": "bionic-commando", + "releases": [ + {"id": "0004000000058400"} + ], + "title": "BIONIC COMMANDO" + }, + { + "compatibility": 99, + "directory": "bionic-commando-elite-forces", + "releases": [ + {"id": "000400000011A800"} + ], + "title": "Bionic Commando: Elite Forces" + }, + { + "compatibility": 0, + "directory": "bird-mania-3d", + "releases": [ + {"id": "000400000008C000"} + ], + "title": "Bird Mania 3D" + }, + { + "compatibility": 2, + "directory": "bird-mania-christmas-3d", + "releases": [ + {"id": "0004000000113D00"} + ], + "title": "Bird Mania Christmas 3D" + }, + { + "compatibility": 99, + "directory": "bit-boy-arcade", + "releases": [ + {"id": "000400000008D100"} + ], + "title": "Bit Boy!! ARCADE" + }, + { + "compatibility": 0, + "directory": "bit-dungeon-plus", + "releases": [ + {"id": "00040000001B9200"} + ], + "title": "Bit Dungeon Plus" + }, + { + "compatibility": 1, + "directory": "bittrip-saga", + "releases": [ + {"id": "0004000000045F00"} + ], + "title": "BIT.TRIP SAGA" + }, + { + "compatibility": 1, + "directory": "blast-em-bunnies", + "releases": [ + {"id": "0004000000107700"} + ], + "title": "Blast 'Em Bunnies" + }, + { + "compatibility": 99, + "directory": "blaster-master", + "releases": [ + {"id": "0004000000099200"} + ], + "title": "Blaster Master" + }, + { + "compatibility": 99, + "directory": "blaster-master-enemy-below", + "releases": [ + {"id": "0004000000061C00"} + ], + "title": "Blaster Master Enemy Below" + }, + { + "compatibility": 1, + "directory": "blaster-master-zero", + "releases": [ + {"id": "00040000001BD000"} + ], + "title": "Blaster Master Zero" + }, + { + "compatibility": 99, + "directory": "blasting-agent-ultimate-edition", + "releases": [ + {"id": "000400000019F200"} + ], + "title": "Blasting Agent: Ultimate Edition" + }, + { + "compatibility": 0, + "directory": "blazblue-clonephantasma-", + "releases": [ + {"id": "000400000010F800"} + ], + "title": "BLAZBLUE -CLONEPHANTASMA-" + }, + { + "compatibility": 1, + "directory": "blazblue-continuum-shift-ii", + "releases": [ + {"id": "000400000004AC00"} + ], + "title": "BlazBlue: Continuum Shift II" + }, + { + "compatibility": 99, + "directory": "block-a-pix-color", + "releases": [ + {"id": "00040000001D3000"} + ], + "title": "Block-a-Pix Color" + }, + { + "compatibility": 99, + "directory": "blockform", + "releases": [ + {"id": "000400000F707000"} + ], + "title": "BlockForm" + }, + { + "compatibility": 99, + "directory": "blok-drop-chaos", + "releases": [ + {"id": "000400000F70FB00"} + ], + "title": "BLOK DROP CHAOS" + }, + { + "compatibility": 99, + "directory": "bloo-kid-2", + "releases": [ + {"id": "0004000000150D00"} + ], + "title": "Bloo Kid 2" + }, + { + "compatibility": 99, + "directory": "bloodstained-curse-of-the-moon", + "releases": [ + {"id": "00040000001D4000"} + ], + "title": "Bloodstained: Curse of the Moon" + }, + { + "compatibility": 99, + "directory": "bomb-monkey", + "releases": [ + {"id": "000400000009B200"} + ], + "title": "Bomb Monkey" + }, + { + "compatibility": 0, + "directory": "bonds-of-the-skies", + "releases": [ + {"id": "00040000001CC700"} + ], + "title": "Bonds of the Skies" + }, + { + "compatibility": 99, + "directory": "bowling-bonanza-3d", + "releases": [ + {"id": "00040000000C7E00"} + ], + "title": "Bowling Bonanza 3D" + }, + { + "compatibility": 99, + "directory": "box-up", + "releases": [ + {"id": "000400000F708900"} + ], + "title": "BOX UP" + }, + { + "compatibility": 0, + "directory": "boxboxboy", + "releases": [ + {"id": "000400000018EE00"} + ], + "title": "BOXBOXBOY!" + }, + { + "compatibility": 0, + "directory": "boxboy", + "releases": [ + {"id": "0004000000154500"} + ], + "title": "BOXBOY!" + }, + { + "compatibility": 99, + "directory": "boxzle", + "releases": [ + {"id": "00040000000FE400"} + ], + "title": "Boxzle" + }, + { + "compatibility": 0, + "directory": "brain-age-concentration-training", + "releases": [ + {"id": "00040000000B3C00"} + ], + "title": "Brain Age: Concentration Training" + }, + { + "compatibility": 99, + "directory": "bratz-fashion-boutique", + "releases": [ + {"id": "00040000000A7300"} + ], + "title": "Bratz: Fashion Boutique" + }, + { + "compatibility": 0, + "directory": "brave-dungeon", + "releases": [ + {"id": "00040000001C0100"} + ], + "title": "Brave Dungeon" + }, + { + "compatibility": 99, + "directory": "brave-tank-hero", + "releases": [ + {"id": "0004000000175A00"} + ], + "title": "Brave Tank Hero" + }, + { + "compatibility": 0, + "directory": "bravely-default", + "releases": [ + {"id": "00040000000FC500"} + ], + "title": "Bravely Default" + }, + { + "compatibility": 1, + "directory": "bravely-second-end-layer", + "releases": [ + {"id": "000400000017BA00"} + ], + "title": "BRAVELY SECOND: END LAYER" + }, + { + "compatibility": 0, + "directory": "bravely-second-end-layer-demo", + "releases": [ + {"id": "0004000000183700"} + ], + "title": "BRAVELY SECOND: END LAYER DEMO" + }, + { + "compatibility": 99, + "directory": "breakout-defense", + "releases": [ + {"id": "00040000001D6600"} + ], + "title": "Breakout Defense" + }, + { + "compatibility": 99, + "directory": "breakout-defense", + "releases": [ + {"id": "000400000F70B300"} + ], + "title": "Breakout Defense" + }, + { + "compatibility": 99, + "directory": "breakout-defense-2", + "releases": [ + {"id": "000400000F70FA00"} + ], + "title": "Breakout Defense 2" + }, + { + "compatibility": 99, + "directory": "breakout-defense-2", + "releases": [ + {"id": "00040000001D7D00"} + ], + "title": "Breakout Defense 2" + }, + { + "compatibility": 99, + "directory": "breath-of-fire", + "releases": [ + {"id": "000400000F705400"} + ], + "title": "BREATH OF FIRE" + }, + { + "compatibility": 99, + "directory": "breath-of-fire-ii", + "releases": [ + {"id": "000400000F705600"} + ], + "title": "Breath of Fire II" + }, + { + "compatibility": 99, + "directory": "brick-race", + "releases": [ + {"id": "000400000F707200"} + ], + "title": "BRICK RACE" + }, + { + "compatibility": 99, + "directory": "brick-thru", + "releases": [ + {"id": "000400000F710600"} + ], + "title": "BRICK THRU" + }, + { + "compatibility": 99, + "directory": "brilliant-hamsters", + "releases": [ + {"id": "00040000000C1B00"} + ], + "title": "Brilliant Hamsters!" + }, + { + "compatibility": 99, + "directory": "brunch-panic", + "releases": [ + {"id": "00040000000F2C00"} + ], + "title": "Brunch Panic" + }, + { + "compatibility": 99, + "directory": "bubble-pop-world", + "releases": [ + {"id": "00040000000BF200"} + ], + "title": "Bubble Pop World" + }, + { + "compatibility": 99, + "directory": "bugs-vs-tanks", + "releases": [ + {"id": "00040000000D9800"} + ], + "title": "BUGS vs. TANKS!" + }, + { + "compatibility": 99, + "directory": "burger-time-deluxe", + "releases": [ + {"id": "0004000000050A00"} + ], + "title": "Burger Time Deluxe" + }, + { + "compatibility": 5, + "directory": "bust-a-move-universe", + "releases": [ + {"id": "0004000000035E00"} + ], + "title": "BUST-A-MOVE UNIVERSE" + }, + { + "compatibility": 99, + "directory": "butterfly-inchworm-animation-ii", + "releases": [ + {"id": "0004000000110E00"} + ], + "title": "Butterfly Inchworm Animation II" + }, + { + "compatibility": 0, + "directory": "bye-bye-boxboy", + "releases": [ + {"id": "00040000001B5300"} + ], + "title": "BYE-BYE BOXBOY!" + }, + { + "compatibility": 99, + "directory": "candy-match-3", + "releases": [ + {"id": "000400000010A900"} + ], + "title": "Candy Match 3" + }, + { + "compatibility": 99, + "directory": "candy-please", + "releases": [ + {"id": "00040000001B5E00"} + ], + "title": "Candy, Please!" + }, + { + "compatibility": 3, + "directory": "captain-america-super-soldier", + "releases": [ + {"id": "0004000000040600"} + ], + "title": "Captain America: Super Soldier" + }, + { + "compatibility": 1, + "directory": "captain-toad-treasure-tracker", + "releases": [ + {"id": "00040000001CB100"} + ], + "title": "Captain Toad: Treasure Tracker" + }, + { + "compatibility": 99, + "directory": "carnival-games-wild-west-3d", + "releases": [ + {"id": "0004000000061400"} + ], + "title": "Carnival Games Wild West 3D" + }, + { + "compatibility": 99, + "directory": "carps-and-dragons", + "releases": [ + {"id": "0004000000086D00"} + ], + "title": "Carps & Dragons" + }, + { + "compatibility": 1, + "directory": "cartoon-network-battle-crashers", + "releases": [ + {"id": "0004000000192000"} + ], + "title": "Cartoon Network: Battle Crashers" + }, + { + "compatibility": 1, + "directory": "cartoon-network-punch-time-explosion", + "releases": [ + {"id": "0004000000043900"} + ], + "title": "Cartoon Network Punch Time Explosion" + }, + { + "compatibility": 99, + "directory": "castle-clout-3d", + "releases": [ + {"id": "0004000000097300"} + ], + "title": "Castle Clout 3D" + }, + { + "compatibility": 99, + "directory": "castle-conqueror-defender", + "releases": [ + {"id": "00040000000A8400"} + ], + "title": "Castle Conqueror Defender" + }, + { + "compatibility": 99, + "directory": "castle-conqueror-ex", + "releases": [ + {"id": "0004000000146A00"} + ], + "title": "Castle Conqueror EX" + }, + { + "compatibility": 99, + "directory": "castlevania", + "releases": [ + {"id": "000400000009DB00"} + ], + "title": "Castlevania" + }, + { + "compatibility": 99, + "directory": "castlevania-dracula-x", + "releases": [ + {"id": "000400000F707D00"} + ], + "title": "Castlevania Dracula X" + }, + { + "compatibility": 99, + "directory": "castlevania-ii-simons-quest", + "releases": [ + {"id": "00040000000BDB00"} + ], + "title": "Castlevania II: Simon's Quest" + }, + { + "compatibility": 99, + "directory": "castlevania-iii-draculas-curse", + "releases": [ + {"id": "00040000000B2A00"} + ], + "title": "Castlevania III: Dracula's Curse" + }, + { + "compatibility": 3, + "directory": "castlevania-lords-of-shadow-mirror-of-fate", + "releases": [ + {"id": "0004000000096600"} + ], + "title": "Castlevania: Lords of Shadow - Mirror of Fate" + }, + { + "compatibility": 99, + "directory": "castlevania-the-adventure", + "releases": [ + {"id": "000400000007DE00"} + ], + "title": "Castlevania: The Adventure" + }, + { + "compatibility": 99, + "directory": "catrap", + "releases": [ + {"id": "000400000004F800"} + ], + "title": "CATRAP" + }, + { + "compatibility": 1, + "directory": "cave-story", + "releases": [ + {"id": "000400000009B300"} + ], + "title": "Cave Story" + }, + { + "compatibility": 0, + "directory": "cave-story-3d", + "releases": [ + {"id": "000400000004A100"} + ], + "title": "Cave Story 3D" + }, + { + "compatibility": 2, + "directory": "centipede-infestation", + "releases": [ + {"id": "000400000004AD00"} + ], + "title": "Centipede: Infestation" + }, + { + "compatibility": 0, + "directory": "chain-blaster", + "releases": [ + {"id": "00040000000DCC00"} + ], + "title": "Chain Blaster" + }, + { + "compatibility": 0, + "directory": "chase-cold-case-investigations-~distant-memories~", + "releases": [ + {"id": "00040000001A6600"} + ], + "title": "Chase Cold Case Investigations ~Distant Memories~" + }, + { + "compatibility": 99, + "directory": "chat-a-lot", + "releases": [ + {"id": "0004000000123700"} + ], + "title": "Chat-A-Lot" + }, + { + "compatibility": 99, + "directory": "chibi-robo-photo-finder", + "releases": [ + {"id": "0004000000107C00"} + ], + "title": "Chibi-Robo!: Photo Finder" + }, + { + "compatibility": 3, + "directory": "chibi-robo-zip-lash", + "releases": [ + {"id": "0004000000163000"} + ], + "title": "Chibi-Robo! Zip Lash" + }, + { + "compatibility": 1, + "directory": "chicken-wiggle", + "releases": [ + {"id": "00040000001A7F00"} + ], + "title": "Chicken Wiggle" + }, + { + "compatibility": 99, + "directory": "christmas-night-archery", + "releases": [ + {"id": "000400000F70FE00"} + ], + "title": "Christmas Night Archery" + }, + { + "compatibility": 99, + "directory": "chronus-arc", + "releases": [ + {"id": "0004000000179000"} + ], + "title": "Chronus Arc" + }, + { + "compatibility": 1, + "directory": "citizens-of-earth", + "releases": [ + {"id": "000400000012C100"} + ], + "title": "Citizens of Earth" + }, + { + "compatibility": 99, + "directory": "city-connection", + "releases": [ + {"id": "00040000000B2E00"} + ], + "title": "City Connection" + }, + { + "compatibility": 99, + "directory": "city-mysteries", + "releases": [ + {"id": "000400000013E800"} + ], + "title": "City Mysteries" + }, + { + "compatibility": 99, + "directory": "classic-games-overload-card-and-puzzle-edition", + "releases": [ + {"id": "0004000000091700"} + ], + "title": "Classic Games Overload: Card & Puzzle Edition" + }, + { + "compatibility": 1, + "directory": "cloudy-with-a-chance-of-meatballs-2", + "releases": [ + {"id": "00040000000E6A00"} + ], + "title": "Cloudy With a Chance of Meatballs 2" + }, + { + "compatibility": 99, + "directory": "clu-clu-land", + "releases": [ + {"id": "00040000000CDB00"} + ], + "title": "Clu Clu Land" + }, + { + "compatibility": 99, + "directory": "coaster-creator-3d", + "releases": [ + {"id": "000400000008B200"} + ], + "title": "Coaster Creator 3D" + }, + { + "compatibility": 99, + "directory": "cocoro-line-defender", + "releases": [ + {"id": "0004000000109900"} + ], + "title": "Cocoro - Line Defender" + }, + { + "compatibility": 99, + "directory": "cocoto-alien-brick-breaker", + "releases": [ + {"id": "00040000000BF400"} + ], + "title": "Cocoto Alien Brick Breaker" + }, + { + "compatibility": 1, + "directory": "code-name-steam", + "releases": [ + {"id": "0004000000132500"} + ], + "title": "Code Name: S.T.E.A.M." + }, + { + "compatibility": 1, + "directory": "code-of-princess", + "releases": [ + {"id": "00040000000A2C00"} + ], + "title": "Code of Princess" + }, + { + "compatibility": 99, + "directory": "collide-a-ball", + "releases": [ + {"id": "00040000001A1800"} + ], + "title": "Collide-a-Ball" + }, + { + "compatibility": 99, + "directory": "color-cubes", + "releases": [ + {"id": "000400000F709A00"} + ], + "title": "COLOR CUBES" + }, + { + "compatibility": 99, + "directory": "color-zen", + "releases": [ + {"id": "0004000000116D00"} + ], + "title": "Color Zen" + }, + { + "compatibility": 99, + "directory": "color-zen-kids", + "releases": [ + {"id": "0004000000124E00"} + ], + "title": "Color Zen Kids" + }, + { + "compatibility": 2, + "directory": "colors-3d", + "releases": [ + {"id": "0004000000081100"} + ], + "title": "Colors! 3D" + }, + { + "compatibility": 99, + "directory": "columns", + "releases": [ + {"id": "000400000008BE00"} + ], + "title": "Columns" + }, + { + "compatibility": 1, + "directory": "combat-of-giants-dinosaurs-3d", + "releases": [ + {"id": "0004000000035100"} + ], + "title": "Combat of Giants Dinosaurs 3D" + }, + { + "compatibility": 99, + "directory": "comic-workshop", + "releases": [ + {"id": "000400000013EA00"} + ], + "title": "Comic Workshop" + }, + { + "compatibility": 99, + "directory": "comic-workshop-2", + "releases": [ + {"id": "0004000000167F00"} + ], + "title": "Comic Workshop 2" + }, + { + "compatibility": 2, + "directory": "conception-ii-children-of-the-seven-stars", + "releases": [ + {"id": "0004000000112C00"} + ], + "title": "Conception II: Children of the Seven Stars" + }, + { + "compatibility": 99, + "directory": "contra-iii-the-alien-wars", + "releases": [ + {"id": "000400000F702C00"} + ], + "title": "Contra III: The Alien Wars" + }, + { + "compatibility": 99, + "directory": "conveni-dream", + "releases": [ + {"id": "0004000000194400"} + ], + "title": "Conveni Dream" + }, + { + "compatibility": 1, + "directory": "cooking-mama-4-kitchen-magic", + "releases": [ + {"id": "000400000004E400"} + ], + "title": "Cooking Mama 4: Kitchen Magic" + }, + { + "compatibility": 1, + "directory": "cooking-mama-5-bon-appetit", + "releases": [ + {"id": "000400000012D600"} + ], + "title": "Cooking Mama 5: Bon Appétit!" + }, + { + "compatibility": 0, + "directory": "cooking-mama-sweet-shop", + "releases": [ + {"id": "00040000001B9C00"} + ], + "title": "Cooking Mama: Sweet Shop" + }, + { + "compatibility": 2, + "directory": "corpse-party", + "releases": [ + {"id": "0004000000194200"} + ], + "title": "Corpse Party" + }, + { + "compatibility": 2, + "directory": "crash-city-mayhem", + "releases": [ + {"id": "000400000004CD00"} + ], + "title": "Crash City Mayhem" + }, + { + "compatibility": 99, + "directory": "crash-n-the-boys-street-challenge", + "releases": [ + {"id": "00040000000B7100"} + ], + "title": "Crash 'n the Boys Street Challenge" + }, + { + "compatibility": 0, + "directory": "crashmo", + "releases": [ + {"id": "00040000000B6E00"} + ], + "title": "Crashmo" + }, + { + "compatibility": 99, + "directory": "crazy-chicken-directors-cut-3d", + "releases": [ + {"id": "0004000000087400"} + ], + "title": "Crazy Chicken: Director's Cut 3D" + }, + { + "compatibility": 99, + "directory": "crazy-chicken-pirates-3d", + "releases": [ + {"id": "0004000000087500"} + ], + "title": "Crazy Chicken Pirates 3D" + }, + { + "compatibility": 99, + "directory": "crazy-construction", + "releases": [ + {"id": "00040000000CEE00"} + ], + "title": "CRAZY CONSTRUCTION" + }, + { + "compatibility": 99, + "directory": "crazy-kangaroo", + "releases": [ + {"id": "0004000000092400"} + ], + "title": "Crazy Kangaroo" + }, + { + "compatibility": 0, + "directory": "creeping-terror", + "releases": [ + {"id": "00040000001CB600"} + ], + "title": "Creeping Terror" + }, + { + "compatibility": 1, + "directory": "crimson-shroud", + "releases": [ + {"id": "00040000000BBF00"} + ], + "title": "CRIMSON SHROUD" + }, + { + "compatibility": 99, + "directory": "crollors-game-pack", + "releases": [ + {"id": "00040000001C4A00"} + ], + "title": "Crollors Game Pack" + }, + { + "compatibility": 1, + "directory": "crosswords-plus", + "releases": [ + {"id": "000400000005C800"} + ], + "title": "Crosswords Plus" + }, + { + "compatibility": 1, + "directory": "crush3d", + "releases": [ + {"id": "0004000000037100"} + ], + "title": "Crush3D" + }, + { + "compatibility": 99, + "directory": "crystal-warriors", + "releases": [ + {"id": "000400000009C100"} + ], + "title": "Crystal Warriors" + }, + { + "compatibility": 0, + "directory": "crystareino", + "releases": [ + {"id": "00040000001C6C00"} + ], + "title": "Crystareino" + }, + { + "compatibility": 1, + "directory": "cube-creator-3d", + "releases": [ + {"id": "0004000000151100"} + ], + "title": "Cube Creator 3D" + }, + { + "compatibility": 3, + "directory": "cube-creator-dx", + "releases": [ + {"id": "00040000001C7F00"} + ], + "title": "Cube Creator DX" + }, + { + "compatibility": 99, + "directory": "cube-tactics", + "releases": [ + {"id": "0004000000101C00"} + ], + "title": "Cube Tactics" + }, + { + "compatibility": 99, + "directory": "cubic-ninja", + "releases": [ + {"id": "0004000000046500"} + ], + "title": "Cubic Ninja" + }, + { + "compatibility": 0, + "directory": "cubit-the-hardcore-platformer-robot", + "releases": [ + {"id": "0004000000112300"} + ], + "title": "Cubit The Hardcore Platformer Robot" + }, + { + "compatibility": 1, + "directory": "culdcept-revolt", + "releases": [ + {"id": "00040000001BEC00"} + ], + "title": "Culdcept Revolt" + }, + { + "compatibility": 99, + "directory": "cup-critters", + "releases": [ + {"id": "000400000F709000"} + ], + "title": "CUP CRITTERS" + }, + { + "compatibility": 1, + "directory": "cursed-castilla", + "releases": [ + {"id": "00040000001BF000"} + ], + "title": "Cursed Castilla" + }, + { + "compatibility": 99, + "directory": "cut-the-rope", + "releases": [ + {"id": "00040000000EB900"} + ], + "title": "Cut the Rope" + }, + { + "compatibility": 1, + "directory": "cut-the-rope-triple-treat", + "releases": [ + {"id": "0004000000112600"} + ], + "title": "Cut the Rope: Triple Treat" + }, + { + "compatibility": 99, + "directory": "cycle-of-eternity", + "releases": [ + {"id": "000400000F710000"} + ], + "title": "Cycle of Eternity" + }, + { + "compatibility": 99, + "directory": "dan-mcfox-head-hunter", + "releases": [ + {"id": "000400000018FC00"} + ], + "title": "Dan McFox: Head Hunter" + }, + { + "compatibility": 99, + "directory": "dangerous-road", + "releases": [ + {"id": "00040000001AFE00"} + ], + "title": "Dangerous Road" + }, + { + "compatibility": 99, + "directory": "darts-up-3d", + "releases": [ + {"id": "00040000000EB200"} + ], + "title": "Darts Up 3D" + }, + { + "compatibility": 1, + "directory": "dead-or-alive-dimensions", + "releases": [ + {"id": "0004000000034F00"} + ], + "title": "DEAD OR ALIVE Dimensions" + }, + { + "compatibility": 99, + "directory": "deca-sports-extreme", + "releases": [ + {"id": "0004000000044600"} + ], + "title": "Deca Sports Extreme" + }, + { + "compatibility": 0, + "directory": "dededes-drum-dash-deluxe", + "releases": [ + {"id": "0004000000147A00"} + ], + "title": "Dedede’s Drum Dash Deluxe" + }, + { + "compatibility": 0, + "directory": "deer-drive-legends", + "releases": [ + {"id": "0004000000061F00"} + ], + "title": "Deer Drive Legends" + }, + { + "compatibility": 99, + "directory": "deer-hunting-king", + "releases": [ + {"id": "0004000000092600"} + ], + "title": "Deer Hunting King" + }, + { + "compatibility": 99, + "directory": "defend-your-crypt", + "releases": [ + {"id": "0004000000198200"} + ], + "title": "Defend your Crypt" + }, + { + "compatibility": 99, + "directory": "defenders-of-oasis", + "releases": [ + {"id": "000400000009C200"} + ], + "title": "Defenders of Oasis" + }, + { + "compatibility": 2, + "directory": "dementium-remastered", + "releases": [ + {"id": "0004000000161B00"} + ], + "title": "Dementium Remastered" + }, + { + "compatibility": 99, + "directory": "demon-king-box", + "releases": [ + {"id": "0004000000134900"} + ], + "title": "Demon King Box" + }, + { + "compatibility": 99, + "directory": "demons-crest", + "releases": [ + {"id": "000400000F706800"} + ], + "title": "DEMON'S CREST" + }, + { + "compatibility": 2, + "directory": "detective-pikachu", + "releases": [ + {"id": "00040000001C1E00"} + ], + "title": "Detective Pikachu" + }, + { + "compatibility": 99, + "directory": "detective-pikachu-special-demo", + "releases": [ + {"id": "00040000001D2900"} + ], + "title": "Detective Pikachu Special Demo" + }, + { + "compatibility": 1, + "directory": "dig-dug", + "releases": [ + {"id": "000400000009DE00"} + ], + "title": "DIG DUG" + }, + { + "compatibility": 99, + "directory": "digger-dan-dx", + "releases": [ + {"id": "000400000018F300"} + ], + "title": "Digger Dan DX" + }, + { + "compatibility": 1, + "directory": "dillons-dead-heat-breakers", + "releases": [ + {"id": "00040000001CF300"} + ], + "title": "Dillon’s Dead-Heat Breakers" + }, + { + "compatibility": 0, + "directory": "dillons-rolling-western", + "releases": [ + {"id": "000400000007C400"} + ], + "title": "Dillon's Rolling Western" + }, + { + "compatibility": 2, + "directory": "dillons-rolling-western-the-last-ranger", + "releases": [ + {"id": "00040000000CC500"} + ], + "title": "Dillon's Rolling Western The Last Ranger" + }, + { + "compatibility": 0, + "directory": "disney-art-academy", + "releases": [ + {"id": "0004000000161700"} + ], + "title": "Disney Art Academy" + }, + { + "compatibility": 1, + "directory": "disney-epic-mickey-power-of-illusion", + "releases": [ + {"id": "00040000000A2D00"} + ], + "title": "Disney Epic Mickey: Power of Illusion" + }, + { + "compatibility": 1, + "directory": "disney-infinity", + "releases": [ + {"id": "00040000000B9C00"} + ], + "title": "Disney Infinity" + }, + { + "compatibility": 3, + "directory": "disney-magical-world", + "releases": [ + {"id": "00040000000EA900"} + ], + "title": "Disney Magical World" + }, + { + "compatibility": 1, + "directory": "disney-magical-world-2", + "releases": [ + {"id": "000400000018AE00"} + ], + "title": "Disney Magical World 2" + }, + { + "compatibility": 99, + "directory": "disney-planes", + "releases": [ + {"id": "00040000000B0800"} + ], + "title": "Disney Planes" + }, + { + "compatibility": 4, + "directory": "disney-princess-my-fairytale-adventure", + "releases": [ + {"id": "0004000000083900"} + ], + "title": "Disney Princess: My Fairytale Adventure" + }, + { + "compatibility": 99, + "directory": "disneypixar-finding-nemo-escape-to-the-big-blue-edition", + "releases": [ + {"id": "00040000000A2100"} + ], + "title": "Disney/Pixar: Finding Nemo Escape to the Big Blue Edition" + }, + { + "compatibility": 99, + "directory": "doctor-lautrec-and-the-forgotten-knights", + "releases": [ + {"id": "0004000000036800"} + ], + "title": "Doctor Lautrec and the Forgotten Knights" + }, + { + "compatibility": 0, + "directory": "dodge-club-pocket", + "releases": [ + {"id": "00040000001A0200"} + ], + "title": "Dodge Club Pocket" + }, + { + "compatibility": 99, + "directory": "dodgebox", + "releases": [ + {"id": "000400000F70BB00"} + ], + "title": "DodgeBox" + }, + { + "compatibility": 99, + "directory": "doll-fashion-atelier", + "releases": [ + {"id": "000400000018AA00"} + ], + "title": "Doll Fashion Atelier" + }, + { + "compatibility": 99, + "directory": "donkey-kong", + "releases": [ + {"id": "00040000000A4600"} + ], + "title": "Donkey Kong" + }, + { + "compatibility": 99, + "directory": "donkey-kong", + "releases": [ + {"id": "0004000000041D00"} + ], + "title": "Donkey Kong" + }, + { + "compatibility": 99, + "directory": "donkey-kong-3", + "releases": [ + {"id": "00040000000B5F00"} + ], + "title": "Donkey Kong 3" + }, + { + "compatibility": 3, + "directory": "donkey-kong-country", + "releases": [ + {"id": "000400000F701B00"} + ], + "title": "Donkey Kong Country" + }, + { + "compatibility": 3, + "directory": "donkey-kong-country-2-diddys-kong-quest", + "releases": [ + {"id": "000400000F702000"} + ], + "title": "Donkey Kong Country 2: Diddy's Kong Quest" + }, + { + "compatibility": 99, + "directory": "donkey-kong-country-3-dixie-kongs-double-trouble", + "releases": [ + {"id": "000400000F702F00"} + ], + "title": "Donkey Kong Country 3: Dixie Kong's Double Trouble" + }, + { + "compatibility": 0, + "directory": "donkey-kong-country-returns-3d", + "releases": [ + {"id": "00040000000CCE00"} + ], + "title": "Donkey Kong Country Returns 3D" + }, + { + "compatibility": 99, + "directory": "donkey-kong-country-returns-3d-nintendo-direct-2142013", + "releases": [ + {"id": "00040000000D9B00"} + ], + "title": "Donkey Kong Country Returns 3D Nintendo Direct 2.14.2013" + }, + { + "compatibility": 2, + "directory": "donkey-kong-jr", + "releases": [ + {"id": "0004000000070000"} + ], + "title": "Donkey Kong Jr." + }, + { + "compatibility": 99, + "directory": "donkey-kong-land", + "releases": [ + {"id": "0004000000050100"} + ], + "title": "Donkey Kong Land" + }, + { + "compatibility": 99, + "directory": "donkey-kong-land-2", + "releases": [ + {"id": "000400000011A300"} + ], + "title": "Donkey Kong Land 2" + }, + { + "compatibility": 99, + "directory": "donkey-kong-land-iii", + "releases": [ + {"id": "000400000011A600"} + ], + "title": "Donkey Kong Land III" + }, + { + "compatibility": 99, + "directory": "dont-crash-go", + "releases": [ + {"id": "000400000F70A900"} + ], + "title": "DON'T CRASH GO" + }, + { + "compatibility": 99, + "directory": "dot-runner-complete-edition", + "releases": [ + {"id": "0004000000095300"} + ], + "title": "Dot Runner: Complete Edition" + }, + { + "compatibility": 99, + "directory": "double-breakout", + "releases": [ + {"id": "00040000001D6500"} + ], + "title": "Double Breakout" + }, + { + "compatibility": 99, + "directory": "double-breakout", + "releases": [ + {"id": "000400000F70AF00"} + ], + "title": "Double Breakout" + }, + { + "compatibility": 99, + "directory": "double-dragon", + "releases": [ + {"id": "0004000000049C00"} + ], + "title": "Double Dragon" + }, + { + "compatibility": 99, + "directory": "double-dragon", + "releases": [ + {"id": "00040000000B5700"} + ], + "title": "Double Dragon" + }, + { + "compatibility": 99, + "directory": "double-dragon-ii-the-revenge", + "releases": [ + {"id": "00040000000D3600"} + ], + "title": "Double Dragon II: The Revenge" + }, + { + "compatibility": 99, + "directory": "dr-mario", + "releases": [ + {"id": "0004000000050700"} + ], + "title": "Dr. Mario" + }, + { + "compatibility": 0, + "directory": "dr-mario-miracle-cure", + "releases": [ + {"id": "000400000013BB00"} + ], + "title": "Dr. Mario: Miracle Cure" + }, + { + "compatibility": 99, + "directory": "dr-robotniks-mean-bean-machine", + "releases": [ + {"id": "000400000009C300"} + ], + "title": "Dr. Robotnik's Mean Bean Machine" + }, + { + "compatibility": 1, + "directory": "dragon-ball-fusions", + "releases": [ + {"id": "00040000001AA900"} + ], + "title": "Dragon Ball Fusions" + }, + { + "compatibility": 99, + "directory": "dragon-crystal", + "releases": [ + {"id": "000400000008BB00"} + ], + "title": "Dragon Crystal" + }, + { + "compatibility": 99, + "directory": "dragon-fantasy-the-black-tome-of-ice", + "releases": [ + {"id": "0004000000163E00"} + ], + "title": "Dragon Fantasy: The Black Tome of Ice" + }, + { + "compatibility": 0, + "directory": "dragon-fantasy-the-volumes-of-westeria", + "releases": [ + {"id": "0004000000163F00"} + ], + "title": "Dragon Fantasy: The Volumes of Westeria" + }, + { + "compatibility": 99, + "directory": "dragon-lapis", + "releases": [ + {"id": "00040000001D3500"} + ], + "title": "Dragon Lapis" + }, + { + "compatibility": 1, + "directory": "dragon-quest-vii-fragments-of-the-forgotten-past", + "releases": [ + {"id": "000400000018EF00"} + ], + "title": "Dragon Quest VII: Fragments of the Forgotten Past" + }, + { + "compatibility": 2, + "directory": "dragon-quest-viii-journey-of-the-cursed-king", + "releases": [ + {"id": "000400000018F100"} + ], + "title": "Dragon Quest VIII: Journey of the Cursed King" + }, + { + "compatibility": 0, + "directory": "dragon-sinker", + "releases": [ + {"id": "00040000001B8800"} + ], + "title": "Dragon Sinker" + }, + { + "compatibility": 99, + "directory": "dragons-wrath", + "releases": [ + {"id": "00040000001D4C00"} + ], + "title": "Dragon's Wrath" + }, + { + "compatibility": 0, + "directory": "drancia-saga", + "releases": [ + {"id": "00040000001AFC00"} + ], + "title": "Drancia Saga" + }, + { + "compatibility": 1, + "directory": "dream-trigger-3d", + "releases": [ + {"id": "0004000000043600"} + ], + "title": "Dream Trigger 3D" + }, + { + "compatibility": 99, + "directory": "dress-to-play-cute-witches", + "releases": [ + {"id": "00040000000B7700"} + ], + "title": "Dress To Play: Cute Witches!" + }, + { + "compatibility": 99, + "directory": "dress-to-play-magic-bubbles", + "releases": [ + {"id": "00040000000D2B00"} + ], + "title": "Dress To Play: Magic Bubbles!" + }, + { + "compatibility": 99, + "directory": "driver-renegade", + "releases": [ + {"id": "0004000000036000"} + ], + "title": "DRIVER RENEGADE" + }, + { + "compatibility": 99, + "directory": "drone-fight", + "releases": [ + {"id": "00040000001C0000"} + ], + "title": "Drone Fight" + }, + { + "compatibility": 99, + "directory": "drop-zone-under-fire", + "releases": [ + {"id": "000400000015F100"} + ], + "title": "Drop Zone - Under Fire" + }, + { + "compatibility": 99, + "directory": "dualpensports", + "releases": [ + {"id": "0004000000036600"} + ], + "title": "DualPenSports" + }, + { + "compatibility": 1, + "directory": "earthbound", + "releases": [ + {"id": "000400000F701600"} + ], + "title": "EarthBound" + }, + { + "compatibility": 0, + "directory": "edge", + "releases": [ + {"id": "0004000000108B00"} + ], + "title": "EDGE" + }, + { + "compatibility": 99, + "directory": "elliot-quest", + "releases": [ + {"id": "00040000001AD800"} + ], + "title": "Elliot Quest" + }, + { + "compatibility": 1, + "directory": "elminage-original", + "releases": [ + {"id": "00040000001BFF00"} + ], + "title": "Elminage Original" + }, + { + "compatibility": 99, + "directory": "epic-word-search-collection", + "releases": [ + {"id": "000400000016AA00"} + ], + "title": "Epic Word Search Collection" + }, + { + "compatibility": 99, + "directory": "epic-word-search-collection-2", + "releases": [ + {"id": "000400000019B500"} + ], + "title": "Epic Word Search Collection 2" + }, + { + "compatibility": 99, + "directory": "epic-word-search-holiday-special", + "releases": [ + {"id": "00040000001AD100"} + ], + "title": "Epic Word Search Holiday Special" + }, + { + "compatibility": 99, + "directory": "escape-from-zombie-city", + "releases": [ + {"id": "0004000000107B00"} + ], + "title": "Escape From Zombie City" + }, + { + "compatibility": 0, + "directory": "escapevektor", + "releases": [ + {"id": "0004000000086C00"} + ], + "title": "escapeVektor" + }, + { + "compatibility": 1, + "directory": "etrian-mystery-dungeon", + "releases": [ + {"id": "000400000015B200"} + ], + "title": "Etrian Mystery Dungeon" + }, + { + "compatibility": 0, + "directory": "etrian-odyssey-2-untold-the-fafnir-knight", + "releases": [ + {"id": "000400000015F200"} + ], + "title": "Etrian Odyssey 2 Untold: The Fafnir Knight" + }, + { + "compatibility": 0, + "directory": "etrian-odyssey-iv-legends-of-the-titan", + "releases": [ + {"id": "00040000000BD300"} + ], + "title": "Etrian Odyssey IV: Legends of the Titan" + }, + { + "compatibility": 0, + "directory": "etrian-odyssey-nexus", + "releases": [ + {"id": "00040000001D4E00"} + ], + "title": "Etrian Odyssey Nexus" + }, + { + "compatibility": 0, + "directory": "etrian-odyssey-untold-the-millennium-girl", + "releases": [ + {"id": "00040000000EC700"} + ], + "title": "Etrian Odyssey Untold: The Millennium Girl" + }, + { + "compatibility": 0, + "directory": "etrian-odyssey-v-beyond-the-myth", + "releases": [ + {"id": "00040000001C5100"} + ], + "title": "Etrian Odyssey V: Beyond the Myth" + }, + { + "compatibility": 99, + "directory": "european-conqueror-3d", + "releases": [ + {"id": "0004000000119500"} + ], + "title": "European Conqueror 3D" + }, + { + "compatibility": 1, + "directory": "ever-oasis", + "releases": [ + {"id": "00040000001A4800"} + ], + "title": "Ever Oasis" + }, + { + "compatibility": 1, + "directory": "excave", + "releases": [ + {"id": "0004000000158500"} + ], + "title": "Excave" + }, + { + "compatibility": 99, + "directory": "excave-ii-wizard-of-the-underworld", + "releases": [ + {"id": "000400000015C500"} + ], + "title": "Excave II : Wizard of the Underworld" + }, + { + "compatibility": 99, + "directory": "excave-iii-tower-of-destiny", + "releases": [ + {"id": "000400000016E000"} + ], + "title": "Excave III : Tower of Destiny" + }, + { + "compatibility": 99, + "directory": "f-zero", + "releases": [ + {"id": "000400000F701900"} + ], + "title": "F-Zero" + }, + { + "compatibility": 99, + "directory": "face-racers-photo-finish", + "releases": [ + {"id": "0004000000037200"} + ], + "title": "Face Racers: Photo Finish" + }, + { + "compatibility": 1, + "directory": "fairune", + "releases": [ + {"id": "0004000000158700"} + ], + "title": "Fairune" + }, + { + "compatibility": 1, + "directory": "fairune2", + "releases": [ + {"id": "000400000016D500"} + ], + "title": "Fairune2" + }, + { + "compatibility": 99, + "directory": "family-bowling-3d", + "releases": [ + {"id": "00040000000AE800"} + ], + "title": "Family Bowling 3D" + }, + { + "compatibility": 99, + "directory": "family-fishing", + "releases": [ + {"id": "0004000000178F00"} + ], + "title": "Family Fishing" + }, + { + "compatibility": 2, + "directory": "family-kart-3d", + "releases": [ + {"id": "00040000000AE700"} + ], + "title": "Family Kart 3D" + }, + { + "compatibility": 99, + "directory": "family-table-tennis-3d", + "releases": [ + {"id": "00040000000E6E00"} + ], + "title": "Family Table Tennis 3D" + }, + { + "compatibility": 99, + "directory": "family-tennis-3d", + "releases": [ + {"id": "000400000008BF00"} + ], + "title": "Family Tennis 3D" + }, + { + "compatibility": 1, + "directory": "fantasy-life", + "releases": [ + {"id": "0004000000113200"} + ], + "title": "Fantasy Life" + }, + { + "compatibility": 99, + "directory": "fantasy-pirates", + "releases": [ + {"id": "0004000000165D00"} + ], + "title": "Fantasy Pirates" + }, + { + "compatibility": 99, + "directory": "farming-simulator-18", + "releases": [ + {"id": "00040000001B7900"} + ], + "title": "Farming Simulator 18" + }, + { + "compatibility": 99, + "directory": "fast-and-furious-showdown", + "releases": [ + {"id": "00040000000B6200"} + ], + "title": "Fast & Furious: Showdown" + }, + { + "compatibility": 99, + "directory": "fat-dragons", + "releases": [ + {"id": "00040000001CFA00"} + ], + "title": "Fat Dragons" + }, + { + "compatibility": 4, + "directory": "fifa-15", + "releases": [ + {"id": "000400000013C700"} + ], + "title": "FIFA 15" + }, + { + "compatibility": 99, + "directory": "fifteen", + "releases": [ + {"id": "000400000F70B900"} + ], + "title": "FIFTEEN" + }, + { + "compatibility": 2, + "directory": "final-fantasy-explorers", + "releases": [ + {"id": "000400000016ED00"} + ], + "title": "FINAL FANTASY EXPLORERS" + }, + { + "compatibility": 99, + "directory": "final-fight", + "releases": [ + {"id": "000400000F703F00"} + ], + "title": "Final Fight" + }, + { + "compatibility": 99, + "directory": "final-fight-2", + "releases": [ + {"id": "000400000F704C00"} + ], + "title": "Final Fight 2" + }, + { + "compatibility": 99, + "directory": "final-fight-3", + "releases": [ + {"id": "000400000F704E00"} + ], + "title": "Final Fight 3" + }, + { + "compatibility": 1, + "directory": "fire-emblem-awakening", + "releases": [ + {"id": "00040000000A0500"} + ], + "title": "Fire Emblem Awakening" + }, + { + "compatibility": 1, + "directory": "fire-emblem-echoes-shadows-of-valentia", + "releases": [ + {"id": "00040000001B4000"} + ], + "title": "Fire Emblem Echoes: Shadows of Valentia" + }, + { + "compatibility": 3, + "directory": "fire-emblem-fates-birthright", + "releases": [ + {"id": "0004000000179400"} + ], + "title": "Fire Emblem Fates: Birthright" + }, + { + "compatibility": 2, + "directory": "fire-emblem-fates-conquest", + "releases": [ + {"id": "0004000000179600"} + ], + "title": "Fire Emblem Fates: Conquest" + }, + { + "compatibility": 2, + "directory": "fire-emblem-warriors", + "releases": [ + {"id": "000400000F70CC00"} + ], + "title": "Fire Emblem Warriors" + }, + { + "compatibility": 99, + "directory": "fishdom-h2o-hidden-odyssey", + "releases": [ + {"id": "00040000000C5900"} + ], + "title": "Fishdom H2O: Hidden Odyssey" + }, + { + "compatibility": 99, + "directory": "flap-flap", + "releases": [ + {"id": "0004000000154A00"} + ], + "title": "Flap Flap" + }, + { + "compatibility": 99, + "directory": "flick-golf-3d", + "releases": [ + {"id": "000400000015C600"} + ], + "title": "Flick Golf 3D" + }, + { + "compatibility": 3, + "directory": "fluidity-spin-cycle", + "releases": [ + {"id": "00040000000AE000"} + ], + "title": "Fluidity: Spin Cycle" + }, + { + "compatibility": 99, + "directory": "fortified-zone", + "releases": [ + {"id": "0004000000046B00"} + ], + "title": "Fortified Zone" + }, + { + "compatibility": 1, + "directory": "fossil-fighters-frontier", + "releases": [ + {"id": "000400000012DB00"} + ], + "title": "Fossil Fighters: Frontier" + }, + { + "compatibility": 99, + "directory": "four-bombs", + "releases": [ + {"id": "000400000F70AA00"} + ], + "title": "FOUR BOMBS" + }, + { + "compatibility": 99, + "directory": "fractured-soul", + "releases": [ + {"id": "00040000000AA800"} + ], + "title": "Fractured Soul" + }, + { + "compatibility": 2, + "directory": "freakyforms-deluxe-your-creations-alive", + "releases": [ + {"id": "000400000009FA00"} + ], + "title": "Freakyforms Deluxe: Your Creations Alive!" + }, + { + "compatibility": 0, + "directory": "frogger-3d", + "releases": [ + {"id": "0004000000036900"} + ], + "title": "Frogger 3D" + }, + { + "compatibility": 99, + "directory": "frontier-days-founding-pioneers", + "releases": [ + {"id": "00040000001B7200"} + ], + "title": "Frontier Days Founding Pioneers" + }, + { + "compatibility": 99, + "directory": "frutakia-2", + "releases": [ + {"id": "000400000F70C800"} + ], + "title": "Frutakia 2" + }, + { + "compatibility": 1, + "directory": "fun-fun-minigolf-touch", + "releases": [ + {"id": "0004000000086E00"} + ], + "title": "Fun! Fun! Minigolf TOUCH!" + }, + { + "compatibility": 99, + "directory": "funfair-party-games", + "releases": [ + {"id": "00040000000C7600"} + ], + "title": "Funfair Party Games" + }, + { + "compatibility": 99, + "directory": "g-loc-air-battle", + "releases": [ + {"id": "000400000009C400"} + ], + "title": "G-LOC Air Battle" + }, + { + "compatibility": 1, + "directory": "gabrielles-ghostly-groove-3d", + "releases": [ + {"id": "0004000000046400"} + ], + "title": "Gabrielle's Ghostly Groove 3D" + }, + { + "compatibility": 99, + "directory": "gabrielles-ghostly-groove-mini", + "releases": [ + {"id": "00040000000E7700"} + ], + "title": "Gabrielle's Ghostly Groove Mini" + }, + { + "compatibility": 99, + "directory": "galaga", + "releases": [ + {"id": "00040000000CE000"} + ], + "title": "Galaga" + }, + { + "compatibility": 99, + "directory": "galaxy-blaster", + "releases": [ + {"id": "000400000F709400"} + ], + "title": "GALAXY BLASTER" + }, + { + "compatibility": 99, + "directory": "galaxy-blaster-code-red", + "releases": [ + {"id": "000400000F70D500"} + ], + "title": "GALAXY BLASTER CODE RED" + }, + { + "compatibility": 2, + "directory": "game-and-watch-gallery", + "releases": [ + {"id": "0004000000042300"} + ], + "title": "Game & Watch Gallery" + }, + { + "compatibility": 99, + "directory": "game-and-watch-gallery-2", + "releases": [ + {"id": "0004000000082B00"} + ], + "title": "Game & Watch Gallery 2" + }, + { + "compatibility": 99, + "directory": "game-and-watch-gallery-3", + "releases": [ + {"id": "000400000010FE00"} + ], + "title": "Game & Watch Gallery 3" + }, + { + "compatibility": 99, + "directory": "games-for-toddlers-2", + "releases": [ + {"id": "000400000F711300"} + ], + "title": "Games for Toddlers 2" + }, + { + "compatibility": 0, + "directory": "games-to-play-on-halloween", + "releases": [ + {"id": "00040000001D7E00"} + ], + "title": "Games to Play on Halloween!" + }, + { + "compatibility": 99, + "directory": "gardening-mama-2-forest-friends", + "releases": [ + {"id": "000400000012D700"} + ], + "title": "Gardening Mama 2: Forest Friends" + }, + { + "compatibility": 99, + "directory": "gardenscapes", + "releases": [ + {"id": "00040000000C0E00"} + ], + "title": "Gardenscapes" + }, + { + "compatibility": 0, + "directory": "garfield-kart", + "releases": [ + {"id": "0004000000169800"} + ], + "title": "Garfield Kart" + }, + { + "compatibility": 99, + "directory": "gargoyles-quest", + "releases": [ + {"id": "000400000004F400"} + ], + "title": "GARGOYLE'S QUEST" + }, + { + "compatibility": 99, + "directory": "gargoyles-quest-ii-the-demon-darkness", + "releases": [ + {"id": "00040000000F8900"} + ], + "title": "Gargoyle's Quest II: The Demon Darkness" + }, + { + "compatibility": 99, + "directory": "geki-yaba-runner-deluxe", + "releases": [ + {"id": "00040000001A3100"} + ], + "title": "Geki Yaba Runner Deluxe" + }, + { + "compatibility": 99, + "directory": "generator-rex-agent-of-providence", + "releases": [ + {"id": "0004000000036C00"} + ], + "title": "Generator Rex: Agent of Providence" + }, + { + "compatibility": 99, + "directory": "ghostsn-goblins", + "releases": [ + {"id": "0004000000099500"} + ], + "title": "Ghosts'n Goblins" + }, + { + "compatibility": 99, + "directory": "girls-fashion-shoot", + "releases": [ + {"id": "00040000000B6300"} + ], + "title": "Girls' Fashion Shoot" + }, + { + "compatibility": 99, + "directory": "glory-of-generals", + "releases": [ + {"id": "000400000013F900"} + ], + "title": "Glory of Generals" + }, + { + "compatibility": 99, + "directory": "glory-of-generals-the-pacific", + "releases": [ + {"id": "000400000013FA00"} + ], + "title": "GLORY OF GENERALS: THE PACIFIC" + }, + { + "compatibility": 99, + "directory": "go-go-kokopolo-3d", + "releases": [ + {"id": "00040000000B7800"} + ], + "title": "Go! Go! Kokopolo 3D" + }, + { + "compatibility": 99, + "directory": "golf", + "releases": [ + {"id": "0004000000042B00"} + ], + "title": "Golf" + }, + { + "compatibility": 0, + "directory": "goosebumps-the-game", + "releases": [ + {"id": "0004000000160200"} + ], + "title": "Goosebumps: The Game" + }, + { + "compatibility": 99, + "directory": "gotcha-racing", + "releases": [ + {"id": "000400000016F000"} + ], + "title": "Gotcha Racing" + }, + { + "compatibility": 99, + "directory": "gothic-masquerade", + "releases": [ + {"id": "000400000013E900"} + ], + "title": "Gothic Masquerade" + }, + { + "compatibility": 0, + "directory": "gotta-protectors", + "releases": [ + {"id": "0004000000196E00"} + ], + "title": "Gotta Protectors" + }, + { + "compatibility": 99, + "directory": "gourmet-dream", + "releases": [ + {"id": "0004000000194500"} + ], + "title": "Gourmet Dream" + }, + { + "compatibility": 99, + "directory": "governor-of-poker", + "releases": [ + {"id": "00040000000ED500"} + ], + "title": "Governor of Poker" + }, + { + "compatibility": 99, + "directory": "gradius", + "releases": [ + {"id": "0004000000094A00"} + ], + "title": "Gradius" + }, + { + "compatibility": 1, + "directory": "gravity-falls-legend-of-the-gnome-gemulets", + "releases": [ + {"id": "0004000000149100"} + ], + "title": "Gravity Falls - Legend of the Gnome Gemulets" + }, + { + "compatibility": 0, + "directory": "green-lantern-rise-of-the-manhunters", + "releases": [ + {"id": "0004000000035300"} + ], + "title": "Green Lantern: Rise of the Manhunters" + }, + { + "compatibility": 4, + "directory": "grinsia", + "releases": [ + {"id": "0004000000111100"} + ], + "title": "Grinsia" + }, + { + "compatibility": 99, + "directory": "groove-heaven", + "releases": [ + {"id": "00040000000E6600"} + ], + "title": "Groove Heaven" + }, + { + "compatibility": 99, + "directory": "guide-the-ghost", + "releases": [ + {"id": "000400000F70D400"} + ], + "title": "GUIDE THE GHOST" + }, + { + "compatibility": 99, + "directory": "gummy-bears-magical-medallion", + "releases": [ + {"id": "00040000000CFC00"} + ], + "title": "Gummy Bears Magical Medallion" + }, + { + "compatibility": 99, + "directory": "gummy-bears-mini-golf", + "releases": [ + {"id": "00040000000CEC00"} + ], + "title": "Gummy Bears Mini Golf" + }, + { + "compatibility": 0, + "directory": "gunman-clive", + "releases": [ + {"id": "00040000000B8F00"} + ], + "title": "Gunman Clive" + }, + { + "compatibility": 99, + "directory": "gunman-clive-2", + "releases": [ + {"id": "000400000012D300"} + ], + "title": "Gunman Clive 2" + }, + { + "compatibility": 99, + "directory": "gunslugs", + "releases": [ + {"id": "000400000019B400"} + ], + "title": "Gunslugs" + }, + { + "compatibility": 99, + "directory": "gunslugs-2", + "releases": [ + {"id": "0004000000180200"} + ], + "title": "Gunslugs 2" + }, + { + "compatibility": 3, + "directory": "gurumin-3d-a-monstrous-adventure", + "releases": [ + {"id": "000400000018C200"} + ], + "title": "Gurumin 3D: A Monstrous Adventure" + }, + { + "compatibility": 1, + "directory": "hakuoki-memories-of-the-shinsengumi", + "releases": [ + {"id": "00040000000DD900"} + ], + "title": "Hakuoki: Memories of the Shinsengumi" + }, + { + "compatibility": 99, + "directory": "halloween-night-archery", + "releases": [ + {"id": "000400000F70F400"} + ], + "title": "Halloween Night Archery" + }, + { + "compatibility": 99, + "directory": "happy-circus", + "releases": [ + {"id": "00040000000EBC00"} + ], + "title": "Happy Circus" + }, + { + "compatibility": 2, + "directory": "harmoknight", + "releases": [ + {"id": "00040000000C2D00"} + ], + "title": "HarmoKnight" + }, + { + "compatibility": 99, + "directory": "harvest-moon", + "releases": [ + {"id": "0004000000099B00"} + ], + "title": "Harvest Moon" + }, + { + "compatibility": 99, + "directory": "harvest-moon-2-gbc", + "releases": [ + {"id": "0004000000110000"} + ], + "title": "Harvest Moon 2 GBC" + }, + { + "compatibility": 99, + "directory": "harvest-moon-3-gbc", + "releases": [ + {"id": "0004000000110200"} + ], + "title": "Harvest Moon 3 GBC" + }, + { + "compatibility": 2, + "directory": "harvest-moon-3d-a-new-beginning", + "releases": [ + {"id": "00040000000A5900"} + ], + "title": "Harvest Moon 3D: A New Beginning" + }, + { + "compatibility": 3, + "directory": "harvest-moon-3d-the-tale-of-two-towns", + "releases": [ + {"id": "0004000000047900"} + ], + "title": "Harvest Moon 3D: The Tale of Two Towns" + }, + { + "compatibility": 1, + "directory": "harvest-moon-skytree-village", + "releases": [ + {"id": "00040000001A3500"} + ], + "title": "Harvest Moon: Skytree Village" + }, + { + "compatibility": 1, + "directory": "harvest-moon-the-lost-valley", + "releases": [ + {"id": "000400000010F600"} + ], + "title": "Harvest Moon: The Lost Valley" + }, + { + "compatibility": 1, + "directory": "hatsune-miku-project-mirai-dx", + "releases": [ + {"id": "0004000000148C00"} + ], + "title": "Hatsune Miku: Project Mirai DX" + }, + { + "compatibility": 0, + "directory": "hazumi", + "releases": [ + {"id": "000400000013C100"} + ], + "title": "Hazumi" + }, + { + "compatibility": 99, + "directory": "heart-beaten", + "releases": [ + {"id": "0004000000161C00"} + ], + "title": "Heart Beaten" + }, + { + "compatibility": 99, + "directory": "heavy-fire-black-arms-3d", + "releases": [ + {"id": "00040000000C4000"} + ], + "title": "Heavy Fire: Black Arms 3D" + }, + { + "compatibility": 99, + "directory": "heavy-fire-special-operations-3d", + "releases": [ + {"id": "00040000000AAF00"} + ], + "title": "Heavy Fire: Special Operations 3D" + }, + { + "compatibility": 99, + "directory": "hello-kitty-and-sanrio-friends-3d-racing", + "releases": [ + {"id": "000400000014EF00"} + ], + "title": "Hello Kitty and Sanrio Friends 3D Racing" + }, + { + "compatibility": 99, + "directory": "hello-kitty-picnic-with-sanrio-friends", + "releases": [ + {"id": "00040000000A8100"} + ], + "title": "Hello Kitty Picnic with Sanrio Friends" + }, + { + "compatibility": 99, + "directory": "hello-kittys-magic-apron", + "releases": [ + {"id": "0004000000175100"} + ], + "title": "Hello Kitty's Magic Apron" + }, + { + "compatibility": 3, + "directory": "heroes-of-ruin", + "releases": [ + {"id": "0004000000075100"} + ], + "title": "Heroes of Ruin" + }, + { + "compatibility": 0, + "directory": "hey-pikmin", + "releases": [ + {"id": "00040000001AFA00"} + ], + "title": "Hey! Pikmin" + }, + { + "compatibility": 99, + "directory": "heyawake-by-nikoli", + "releases": [ + {"id": "0004000000092800"} + ], + "title": "Heyawake by Nikoli" + }, + { + "compatibility": 99, + "directory": "hidden-expedition-titanic", + "releases": [ + {"id": "00040000000DDA00"} + ], + "title": "Hidden Expedition Titanic" + }, + { + "compatibility": 99, + "directory": "hideaways-foggy-valley", + "releases": [ + {"id": "000400000013EB00"} + ], + "title": "Hideaways: Foggy Valley" + }, + { + "compatibility": 99, + "directory": "hiding-out", + "releases": [ + {"id": "00040000001CC900"} + ], + "title": "Hiding Out" + }, + { + "compatibility": 99, + "directory": "hit-ninja", + "releases": [ + {"id": "000400000F709500"} + ], + "title": "Hit Ninja" + }, + { + "compatibility": 99, + "directory": "hitori-by-nikoli", + "releases": [ + {"id": "0004000000092900"} + ], + "title": "Hitori by Nikoli" + }, + { + "compatibility": 99, + "directory": "hollywood-fame-hidden-object-adventure", + "releases": [ + {"id": "000400000011D300"} + ], + "title": "Hollywood Fame: Hidden Object Adventure" + }, + { + "compatibility": 3, + "directory": "hometown-story", + "releases": [ + {"id": "00040000000F9900"} + ], + "title": "Hometown Story" + }, + { + "compatibility": 99, + "directory": "horse-vet-3d", + "releases": [ + {"id": "000400000014CB00"} + ], + "title": "Horse Vet 3D" + }, + { + "compatibility": 99, + "directory": "hotel-transylvania", + "releases": [ + {"id": "00040000000A2600"} + ], + "title": "Hotel Transylvania" + }, + { + "compatibility": 99, + "directory": "hyperlight-ex", + "releases": [ + {"id": "000400000F707F00"} + ], + "title": "Hyperlight EX" + }, + { + "compatibility": 2, + "directory": "hyrule-warriors-legends", + "releases": [ + {"id": "000400000017EA00"} + ], + "title": "Hyrule Warriors Legends" + }, + { + "compatibility": 1, + "directory": "i-am-an-air-traffic-controller-airport-hero-hawaii", + "releases": [ + {"id": "00040000000FC100"} + ], + "title": "I am an Air Traffic Controller Airport Hero Hawaii" + }, + { + "compatibility": 99, + "directory": "i-am-an-air-traffic-controller-airport-hero-narita", + "releases": [ + {"id": "0004000000173E00"} + ], + "title": "I am an Air Traffic Controller Airport Hero Narita" + }, + { + "compatibility": 0, + "directory": "i-am-an-air-traffic-controller-airport-hero-osaka-kix", + "releases": [ + {"id": "00040000001C3F00"} + ], + "title": "I am an air traffic controller AIRPORT HERO OSAKA-KIX" + }, + { + "compatibility": 99, + "directory": "i-love-my-cats", + "releases": [ + {"id": "0004000000167300"} + ], + "title": "I Love My Cats" + }, + { + "compatibility": 99, + "directory": "i-love-my-dogs", + "releases": [ + {"id": "0004000000167200"} + ], + "title": "I Love My Dogs" + }, + { + "compatibility": 99, + "directory": "i-love-my-horse", + "releases": [ + {"id": "0004000000141600"} + ], + "title": "I Love My Horse" + }, + { + "compatibility": 99, + "directory": "i-love-my-little-boy", + "releases": [ + {"id": "0004000000141700"} + ], + "title": "I Love My Little Boy" + }, + { + "compatibility": 99, + "directory": "i-love-my-little-girl", + "releases": [ + {"id": "0004000000141900"} + ], + "title": "I Love My Little Girl" + }, + { + "compatibility": 99, + "directory": "i-love-my-pony", + "releases": [ + {"id": "0004000000178C00"} + ], + "title": "I Love My Pony" + }, + { + "compatibility": 99, + "directory": "ice-age-continental-drift-arctic-games", + "releases": [ + {"id": "0004000000086900"} + ], + "title": "Ice Age Continental Drift: Arctic Games" + }, + { + "compatibility": 99, + "directory": "ice-climber", + "releases": [ + {"id": "0004000000070600"} + ], + "title": "Ice Climber" + }, + { + "compatibility": 1, + "directory": "ice-station-z", + "releases": [ + {"id": "0004000000190300"} + ], + "title": "Ice Station Z" + }, + { + "compatibility": 99, + "directory": "ifo", + "releases": [ + {"id": "000400000F710C00"} + ], + "title": "I.F.O" + }, + { + "compatibility": 0, + "directory": "ikachan", + "releases": [ + {"id": "00040000000C4300"} + ], + "title": "Ikachan" + }, + { + "compatibility": 99, + "directory": "imagine-babyz", + "releases": [ + {"id": "000400000004C300"} + ], + "title": "Imagine Babyz" + }, + { + "compatibility": 99, + "directory": "imagine-fashion-life", + "releases": [ + {"id": "0004000000047800"} + ], + "title": "Imagine Fashion Life" + }, + { + "compatibility": 3, + "directory": "inazuma-eleven", + "releases": [ + {"id": "0004000000112B00"} + ], + "title": "INAZUMA ELEVEN" + }, + { + "compatibility": 1, + "directory": "infinite-dunamis", + "releases": [ + {"id": "0004000000194600"} + ], + "title": "Infinite Dunamis" + }, + { + "compatibility": 99, + "directory": "infinite-golf", + "releases": [ + {"id": "000400000F70B500"} + ], + "title": "Infinite Golf" + }, + { + "compatibility": 99, + "directory": "insect-planet-td", + "releases": [ + {"id": "00040000001D5600"} + ], + "title": "Insect Planet TD" + }, + { + "compatibility": 0, + "directory": "ironfall-invasion", + "releases": [ + {"id": "000400000015D800"} + ], + "title": "IRONFALL Invasion" + }, + { + "compatibility": 99, + "directory": "ive-got-to-run-complete-edition", + "releases": [ + {"id": "0004000000148600"} + ], + "title": "I've Got to Run: Complete Edition!" + }, + { + "compatibility": 0, + "directory": "jake-hunter-detective-story-ghost-of-the-dusk", + "releases": [ + {"id": "00040000001CBC00"} + ], + "title": "Jake Hunter Detective Story: Ghost of the Dusk" + }, + { + "compatibility": 0, + "directory": "james-noirs-hollywood-crimes", + "releases": [ + {"id": "0004000000036100"} + ], + "title": "James Noir's Hollywood Crimes" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-5-types-of-trains", + "releases": [ + {"id": "00040000001D3900"} + ], + "title": "Japanese Rail Sim 3D 5 types of trains" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-journey-in-suburbs-1", + "releases": [ + {"id": "0004000000161900"} + ], + "title": "Japanese Rail Sim 3D Journey in Suburbs #1" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-journey-in-suburbs-1-vol-2", + "releases": [ + {"id": "0004000000193D00"} + ], + "title": "Japanese Rail Sim 3D Journey in suburbs #1 Vol. 2" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-journey-in-suburbs-1-vol-3", + "releases": [ + {"id": "0004000000193E00"} + ], + "title": "Japanese Rail Sim 3D Journey in suburbs #1 Vol. 3" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-journey-in-suburbs-1-vol-4", + "releases": [ + {"id": "0004000000193F00"} + ], + "title": "Japanese Rail Sim 3D Journey in suburbs #1 Vol. 4" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-journey-in-suburbs-2", + "releases": [ + {"id": "00040000001BED00"} + ], + "title": "Japanese Rail Sim 3D Journey in suburbs #2" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-journey-to-kyoto", + "releases": [ + {"id": "0004000000173F00"} + ], + "title": "Japanese Rail Sim 3D Journey to Kyoto" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-monorail-trip-to-okinawa", + "releases": [ + {"id": "00040000001A5D00"} + ], + "title": "Japanese Rail Sim 3D Monorail Trip to Okinawa" + }, + { + "compatibility": 99, + "directory": "japanese-rail-sim-3d-travel-of-steam", + "releases": [ + {"id": "00040000001CC500"} + ], + "title": "Japanese Rail Sim 3D Travel of Steam" + }, + { + "compatibility": 99, + "directory": "jaws-ultimate-predator", + "releases": [ + {"id": "0004000000048600"} + ], + "title": "Jaws: Ultimate Predator" + }, + { + "compatibility": 0, + "directory": "jett-rocket-ii-the-wrath-of-taikai", + "releases": [ + {"id": "0004000000087100"} + ], + "title": "Jett Rocket II: The Wrath of Taikai" + }, + { + "compatibility": 99, + "directory": "jewel-master-cradle-of-egypt-2-3d", + "releases": [ + {"id": "00040000000B4400"} + ], + "title": "Jewel Master: Cradle Of Egypt 2 3D" + }, + { + "compatibility": 99, + "directory": "jewel-master-cradle-of-rome-2", + "releases": [ + {"id": "000400000009D100"} + ], + "title": "Jewel Master: Cradle of Rome 2" + }, + { + "compatibility": 99, + "directory": "jewel-match-3", + "releases": [ + {"id": "00040000000CFE00"} + ], + "title": "Jewel Match 3" + }, + { + "compatibility": 99, + "directory": "jewel-quest-4-heritage", + "releases": [ + {"id": "00040000000DDC00"} + ], + "title": "Jewel Quest 4 Heritage" + }, + { + "compatibility": 99, + "directory": "jewel-quest-6-the-sapphire-dragon", + "releases": [ + {"id": "00040000000DDD00"} + ], + "title": "Jewel Quest 6 - The Sapphire Dragon" + }, + { + "compatibility": 99, + "directory": "johnny-dynamite", + "releases": [ + {"id": "000400000011BB00"} + ], + "title": "Johnny Dynamite" + }, + { + "compatibility": 99, + "directory": "johnny-hotshot", + "releases": [ + {"id": "00040000000B7A00"} + ], + "title": "Johnny Hotshot" + }, + { + "compatibility": 99, + "directory": "johnny-impossible", + "releases": [ + {"id": "000400000009A500"} + ], + "title": "Johnny Impossible" + }, + { + "compatibility": 99, + "directory": "johnny-kung-fu", + "releases": [ + {"id": "0004000000090000"} + ], + "title": "Johnny Kung Fu" + }, + { + "compatibility": 99, + "directory": "johnnys-payday-panic", + "releases": [ + {"id": "0004000000183000"} + ], + "title": "JOHNNY'S PAYDAY PANIC" + }, + { + "compatibility": 99, + "directory": "journey-to-kreisia", + "releases": [ + {"id": "000400000019E100"} + ], + "title": "Journey to Kreisia" + }, + { + "compatibility": 0, + "directory": "jump-trials-supreme", + "releases": [ + {"id": "00040000000F1500"} + ], + "title": "Jump Trials Supreme" + }, + { + "compatibility": 1, + "directory": "justice-chronicles", + "releases": [ + {"id": "0004000000188200"} + ], + "title": "Justice Chronicles" + }, + { + "compatibility": 99, + "directory": "kakuro-by-nikoli", + "releases": [ + {"id": "0004000000087200"} + ], + "title": "Kakuro by Nikoli" + }, + { + "compatibility": 99, + "directory": "kami", + "releases": [ + {"id": "0004000000160300"} + ], + "title": "KAMI" + }, + { + "compatibility": 99, + "directory": "karous-the-beast-of-reeden-", + "releases": [ + {"id": "0004000000150600"} + ], + "title": "KAROUS - THE BEAST OF RE:EDEN -" + }, + { + "compatibility": 1, + "directory": "kersploosh", + "releases": [ + {"id": "00040000000C8700"} + ], + "title": "Kersploosh!" + }, + { + "compatibility": 99, + "directory": "ketzals-corridors", + "releases": [ + {"id": "0004000000083800"} + ], + "title": "Ketzal's Corridors" + }, + { + "compatibility": 99, + "directory": "kid-icarus-of-myths-and-monsters", + "releases": [ + {"id": "0004000000069400"} + ], + "title": "Kid Icarus Of Myths and Monsters" + }, + { + "compatibility": 1, + "directory": "kid-icarus-uprising", + "releases": [ + {"id": "0004000000030100"} + ], + "title": "Kid Icarus: Uprising" + }, + { + "compatibility": 99, + "directory": "kid-icarus-uprising-montage", + "releases": [ + {"id": "00040000000BE300"} + ], + "title": "Kid Icarus: Uprising Montage" + }, + { + "compatibility": 0, + "directory": "kid-tripp", + "releases": [ + {"id": "00040000001C7B00"} + ], + "title": "Kid Tripp" + }, + { + "compatibility": 1, + "directory": "kingdom-hearts-3d-dream-drop-distance", + "releases": [ + {"id": "000400000008D300"}, + {"id": "0004000000095500"}, + {"id": "000400000004EE00"} + ], + "title": "KINGDOM HEARTS 3D [Dream Drop Distance]" + }, + { + "compatibility": 0, + "directory": "kingdoms-item-shop", + "releases": [ + {"id": "000400000019E300"} + ], + "title": "Kingdom's Item Shop" + }, + { + "compatibility": 1, + "directory": "kirby-battle-royale", + "releases": [ + {"id": "00040000001C2000"} + ], + "title": "Kirby Battle Royale" + }, + { + "compatibility": 0, + "directory": "kirby-battle-royale-demo", + "releases": [ + {"id": "00040000001CAB00"} + ], + "title": "Kirby Battle Royale Demo" + }, + { + "compatibility": 1, + "directory": "kirby-fighters-deluxe", + "releases": [ + {"id": "0004000000147E00"} + ], + "title": "Kirby Fighters Deluxe" + }, + { + "compatibility": 2, + "directory": "kirby-planet-robobot", + "releases": [ + {"id": "0004000000183600"} + ], + "title": "Kirby Planet Robobot" + }, + { + "compatibility": 1, + "directory": "kirby-triple-deluxe", + "releases": [ + {"id": "000400000010BF00"} + ], + "title": "Kirby: Triple Deluxe" + }, + { + "compatibility": 99, + "directory": "kirbys-block-ball", + "releases": [ + {"id": "0004000000069100"} + ], + "title": "Kirby's Block Ball" + }, + { + "compatibility": 1, + "directory": "kirbys-blowout-blast", + "releases": [ + {"id": "0004000000196F00"} + ], + "title": "Kirby’s Blowout Blast" + }, + { + "compatibility": 99, + "directory": "kirbys-dream-course", + "releases": [ + {"id": "000400000F703B00"} + ], + "title": "Kirby's Dream Course" + }, + { + "compatibility": 2, + "directory": "kirbys-dream-land", + "releases": [ + {"id": "0004000000041700"} + ], + "title": "Kirby's Dream Land" + }, + { + "compatibility": 99, + "directory": "kirbys-dream-land-2", + "releases": [ + {"id": "000400000007DB00"} + ], + "title": "Kirby's Dream Land 2" + }, + { + "compatibility": 99, + "directory": "kirbys-pinball-land", + "releases": [ + {"id": "000400000008B000"} + ], + "title": "Kirby's Pinball Land" + }, + { + "compatibility": 99, + "directory": "kirbys-star-stacker", + "releases": [ + {"id": "000400000008AC00"} + ], + "title": "Kirby's Star Stacker" + }, + { + "compatibility": 0, + "directory": "kokuga", + "releases": [ + {"id": "00040000000EAD00"} + ], + "title": "Kokuga" + }, + { + "compatibility": 0, + "directory": "korg-dsn-12", + "releases": [ + {"id": "0004000000149E00"} + ], + "title": "KORG DSN-12" + }, + { + "compatibility": 3, + "directory": "korg-m01d", + "releases": [ + {"id": "00040000000F1600"} + ], + "title": "KORG M01D" + }, + { + "compatibility": 99, + "directory": "kung-fu-fight", + "releases": [ + {"id": "00040000001BDC00"} + ], + "title": "Kung Fu FIGHT!" + }, + { + "compatibility": 99, + "directory": "kung-fu-rabbit", + "releases": [ + {"id": "000400000010F700"} + ], + "title": "Kung Fu Rabbit" + }, + { + "compatibility": 99, + "directory": "kutar-apple", + "releases": [ + {"id": "00040000001ABD00"} + ], + "title": "Kutar Apple" + }, + { + "compatibility": 99, + "directory": "kutar-burger-factory", + "releases": [ + {"id": "00040000001ABE00"} + ], + "title": "Kutar Burger Factory" + }, + { + "compatibility": 99, + "directory": "kutar-concert-staff", + "releases": [ + {"id": "00040000001ABF00"} + ], + "title": "Kutar Concert Staff" + }, + { + "compatibility": 0, + "directory": "kutar-end-credits", + "releases": [ + {"id": "00040000001AC000"} + ], + "title": "Kutar End Credits" + }, + { + "compatibility": 99, + "directory": "kutar-jump-rope", + "releases": [ + {"id": "00040000001AC100"} + ], + "title": "Kutar Jump Rope" + }, + { + "compatibility": 99, + "directory": "kutar-magic-ball", + "releases": [ + {"id": "00040000001AC200"} + ], + "title": "Kutar Magic Ball" + }, + { + "compatibility": 99, + "directory": "kutar-powder-factory", + "releases": [ + {"id": "00040000001AC300"} + ], + "title": "Kutar Powder Factory" + }, + { + "compatibility": 99, + "directory": "kutar-quiz", + "releases": [ + {"id": "00040000001AC400"} + ], + "title": "Kutar Quiz" + }, + { + "compatibility": 99, + "directory": "kutar-ski-lift", + "releases": [ + {"id": "00040000001AC500"} + ], + "title": "Kutar Ski Lift" + }, + { + "compatibility": 0, + "directory": "kutar-tube-rider", + "releases": [ + {"id": "00040000001AC600"} + ], + "title": "Kutar Tube Rider" + }, + { + "compatibility": 99, + "directory": "lalaloopsy-carnival-of-friends", + "releases": [ + {"id": "00040000000AE300"} + ], + "title": "Lalaloopsy: Carnival of Friends" + }, + { + "compatibility": 2, + "directory": "langrisser-reincarnation-tensei-", + "releases": [ + {"id": "0004000000187F00"} + ], + "title": "Langrisser Re:Incarnation -TENSEI-" + }, + { + "compatibility": 0, + "directory": "laytons-mystery-journey", + "releases": [ + {"id": "00040000001CB400"} + ], + "title": "LAYTON'S MYSTERY JOURNEY" + }, + { + "compatibility": 99, + "directory": "league-of-heroes", + "releases": [ + {"id": "00040000000D6800"} + ], + "title": "League of Heroes" + }, + { + "compatibility": 99, + "directory": "legend-of-the-river-king", + "releases": [ + {"id": "0004000000099D00"} + ], + "title": "Legend of the River King" + }, + { + "compatibility": 99, + "directory": "legend-of-the-river-king-2", + "releases": [ + {"id": "0004000000110500"} + ], + "title": "Legend of the River King 2" + }, + { + "compatibility": 0, + "directory": "legna-tactica", + "releases": [ + {"id": "00040000001A9500"} + ], + "title": "Legna Tactica" + }, + { + "compatibility": 2, + "directory": "lego-batman-2-dc-super-heroes", + "releases": [ + {"id": "000400000005A200"} + ], + "title": "LEGO Batman 2: DC Super Heroes" + }, + { + "compatibility": 2, + "directory": "lego-batman-3-beyond-gotham", + "releases": [ + {"id": "000400000011DD00"} + ], + "title": "LEGO Batman 3: Beyond Gotham" + }, + { + "compatibility": 3, + "directory": "lego-city-undercover-the-chase-begins", + "releases": [ + {"id": "00040000000AD500"} + ], + "title": "LEGO City Undercover: The Chase Begins" + }, + { + "compatibility": 2, + "directory": "lego-harry-potter-years-5-7", + "releases": [ + {"id": "000400000004A400"} + ], + "title": "LEGO Harry Potter: Years 5-7" + }, + { + "compatibility": 1, + "directory": "lego-jurassic-world", + "releases": [ + {"id": "000400000015B000"} + ], + "title": "LEGO Jurassic World" + }, + { + "compatibility": 1, + "directory": "lego-legends-of-chima-lavals-journey", + "releases": [ + {"id": "00040000000AF800"} + ], + "title": "LEGO Legends of Chima: Laval's Journey" + }, + { + "compatibility": 2, + "directory": "lego-marvel-super-heroes-universe-in-peril", + "releases": [ + {"id": "00040000000D2000"} + ], + "title": "LEGO Marvel Super Heroes: Universe in Peril" + }, + { + "compatibility": 1, + "directory": "lego-marvels-avengers", + "releases": [ + {"id": "0004000000168500"} + ], + "title": "LEGO Marvel's Avengers" + }, + { + "compatibility": 2, + "directory": "lego-ninjago-nindroids", + "releases": [ + {"id": "0004000000118C00"} + ], + "title": "LEGO Ninjago: Nindroids" + }, + { + "compatibility": 0, + "directory": "lego-ninjago-shadow-of-ronin", + "releases": [ + {"id": "000400000014EB00"} + ], + "title": "LEGO Ninjago: Shadow of Ronin" + }, + { + "compatibility": 2, + "directory": "lego-pirates-of-the-caribbean-the-video-game", + "releases": [ + {"id": "0004000000037400"} + ], + "title": "LEGO Pirates of the Caribbean: The Video Game" + }, + { + "compatibility": 1, + "directory": "lego-star-wars-iii-the-clone-wars", + "releases": [ + {"id": "0004000000035400"} + ], + "title": "LEGO Star Wars III: The Clone Wars" + }, + { + "compatibility": 1, + "directory": "lego-star-wars-the-force-awakens", + "releases": [ + {"id": "000400000017F900"} + ], + "title": "LEGO Star Wars: The Force Awakens" + }, + { + "compatibility": 0, + "directory": "lets-golf-3d", + "releases": [ + {"id": "000400000005CA00"} + ], + "title": "Let's Golf! 3D" + }, + { + "compatibility": 99, + "directory": "lets-ride-best-in-breed-3d", + "releases": [ + {"id": "0004000000062000"} + ], + "title": "Let's Ride: Best in Breed 3D" + }, + { + "compatibility": 2, + "directory": "liberation-maiden", + "releases": [ + {"id": "00040000000BBE00"} + ], + "title": "LIBERATION MAIDEN" + }, + { + "compatibility": 99, + "directory": "life-force", + "releases": [ + {"id": "00040000000B5A00"} + ], + "title": "Life Force" + }, + { + "compatibility": 99, + "directory": "life-with-horses-3d", + "releases": [ + {"id": "0004000000116E00"} + ], + "title": "Life with Horses 3D" + }, + { + "compatibility": 99, + "directory": "link-a-pix-color", + "releases": [ + {"id": "00040000001D1300"} + ], + "title": "Link-a-Pix Color" + }, + { + "compatibility": 99, + "directory": "lionel-city-builder-3d-rise-of-the-rails", + "releases": [ + {"id": "0004000000151300"} + ], + "title": "Lionel City Builder 3D: Rise of the Rails" + }, + { + "compatibility": 99, + "directory": "little-adventure-on-the-prairie", + "releases": [ + {"id": "00040000001C9200"} + ], + "title": "Little Adventure on the Prairie" + }, + { + "compatibility": 0, + "directory": "little-battlers-experience", + "releases": [ + {"id": "0004000000147100"} + ], + "title": "Little Battlers eXperience" + }, + { + "compatibility": 99, + "directory": "lockn-chase", + "releases": [ + {"id": "0004000000059A00"} + ], + "title": "Lock'N Chase" + }, + { + "compatibility": 3, + "directory": "lord-of-magna-maiden-heaven", + "releases": [ + {"id": "0004000000164300"} + ], + "title": "Lord of Magna: Maiden Heaven" + }, + { + "compatibility": 99, + "directory": "love-hero", + "releases": [ + {"id": "000400000F712500"} + ], + "title": "Love Hero" + }, + { + "compatibility": 2, + "directory": "lufia-the-legend-returns", + "releases": [ + {"id": "000400000011AA00"} + ], + "title": "Lufia: The Legend Returns" + }, + { + "compatibility": 0, + "directory": "luigis-mansion", + "releases": [ + {"id": "00040000001D1900"} + ], + "title": "Luigi’s Mansion" + }, + { + "compatibility": 3, + "directory": "luigis-mansion-dark-moon", + "releases": [ + {"id": "0004000000055F00"} + ], + "title": "Luigi's Mansion: Dark Moon" + }, + { + "compatibility": 99, + "directory": "luv-me-buddies-wonderland", + "releases": [ + {"id": "000400000014CC00"} + ], + "title": "Luv Me Buddies Wonderland" + }, + { + "compatibility": 99, + "directory": "luxor", + "releases": [ + {"id": "00040000000F1100"} + ], + "title": "Luxor" + }, + { + "compatibility": 99, + "directory": "mach-rider", + "releases": [ + {"id": "00040000000DEC00"} + ], + "title": "Mach Rider" + }, + { + "compatibility": 99, + "directory": "machine-knight", + "releases": [ + {"id": "00040000001CF800"} + ], + "title": "Machine Knight" + }, + { + "compatibility": 99, + "directory": "mad-dog-mccree", + "releases": [ + {"id": "0004000000087300"} + ], + "title": "Mad Dog McCree" + }, + { + "compatibility": 99, + "directory": "madagascar-3-the-video-game", + "releases": [ + {"id": "0004000000087D00"} + ], + "title": "Madagascar 3: The Video Game" + }, + { + "compatibility": 2, + "directory": "madden-nfl-football", + "releases": [ + {"id": "0004000000035500"} + ], + "title": "Madden NFL Football" + }, + { + "compatibility": 99, + "directory": "mahjong-3d-essentials", + "releases": [ + {"id": "00040000000F2B00"} + ], + "title": "Mahjong 3D - Essentials" + }, + { + "compatibility": 99, + "directory": "mahjong-3d-warriors-of-the-emperor", + "releases": [ + {"id": "00040000000C2700"} + ], + "title": "Mahjong 3D - Warriors of the Emperor" + }, + { + "compatibility": 2, + "directory": "mahjong-cub3d", + "releases": [ + {"id": "000400000004EB00"} + ], + "title": "MAHJONG CUB3D" + }, + { + "compatibility": 99, + "directory": "mahjong-mysteries-ancient-athena", + "releases": [ + {"id": "00040000000C0F00"} + ], + "title": "Mahjong Mysteries - Ancient Athena" + }, + { + "compatibility": 0, + "directory": "mario-and-donkey-kong-minis-on-the-move", + "releases": [ + {"id": "00040000000C9D00"} + ], + "title": "Mario and Donkey Kong: Minis on the Move" + }, + { + "compatibility": 1, + "directory": "mario-and-luigi-dream-team", + "releases": [ + {"id": "00040000000D5A00"} + ], + "title": "Mario & Luigi: Dream Team" + }, + { + "compatibility": 2, + "directory": "mario-and-luigi-paper-jam", + "releases": [ + {"id": "0004000000132700"} + ], + "title": "Mario & Luigi: Paper Jam" + }, + { + "compatibility": 0, + "directory": "mario-and-luigi-superstar-saga-bowsers-minions", + "releases": [ + {"id": "00040000001B8F00"} + ], + "title": "Mario and Luigi: Superstar Saga + Bowser’s Minions" + }, + { + "compatibility": 1, + "directory": "mario-and-luigibowsers-inside-story-bowser-jrs-journey", + "releases": [ + {"id": "00040000001D1400"} + ], + "title": "Mario & Luigi:Bowser’s Inside Story + Bowser Jr.’s Journey" + }, + { + "compatibility": 99, + "directory": "mario-bros", + "releases": [ + {"id": "00040000000BD000"} + ], + "title": "Mario Bros" + }, + { + "compatibility": 99, + "directory": "mario-golf", + "releases": [ + {"id": "0004000000093C00"} + ], + "title": "Mario Golf" + }, + { + "compatibility": 2, + "directory": "mario-golf-world-tour", + "releases": [ + {"id": "00040000000DCD00"} + ], + "title": "Mario Golf: World Tour" + }, + { + "compatibility": 1, + "directory": "mario-kart-7", + "releases": [ + {"id": "0004000000030800"} + ], + "title": "Mario Kart 7" + }, + { + "compatibility": 1, + "directory": "mario-party-island-tour", + "releases": [ + {"id": "00040000000F8100"} + ], + "title": "Mario Party Island Tour" + }, + { + "compatibility": 0, + "directory": "mario-party-star-rush", + "releases": [ + {"id": "000400000019BD00"} + ], + "title": "Mario Party Star Rush" + }, + { + "compatibility": 99, + "directory": "mario-party-star-rush-party-guest-edition", + "releases": [ + {"id": "00040000001A5F00"} + ], + "title": "Mario Party Star Rush Party Guest Edition" + }, + { + "compatibility": 0, + "directory": "mario-party-the-top-100", + "releases": [ + {"id": "00040000001C4E00"} + ], + "title": "Mario Party: The Top 100" + }, + { + "compatibility": 1, + "directory": "mario-sports-superstars", + "releases": [ + {"id": "0004000000188C00"} + ], + "title": "Mario Sports Superstars" + }, + { + "compatibility": 99, + "directory": "mario-tennis", + "releases": [ + {"id": "00040000000D3D00"} + ], + "title": "Mario Tennis" + }, + { + "compatibility": 3, + "directory": "mario-tennis-open", + "releases": [ + {"id": "000400000007C700"} + ], + "title": "Mario Tennis Open" + }, + { + "compatibility": 2, + "directory": "mario-vs-donkey-kong-tipping-stars", + "releases": [ + {"id": "000400000012C800"} + ], + "title": "Mario vs. Donkey Kong: Tipping Stars" + }, + { + "compatibility": 99, + "directory": "mario-vs-donkey-kong-tipping-stars-ver-101", + "releases": [ + {"id": "0004000E0012C800"} + ], + "title": "Mario vs. Donkey Kong: Tipping Stars Ver. 1.0.1" + }, + { + "compatibility": 2, + "directory": "marios-picross", + "releases": [ + {"id": "0004000000042000"} + ], + "title": "Mario's Picross" + }, + { + "compatibility": 99, + "directory": "marus-mission", + "releases": [ + {"id": "0004000000058800"} + ], + "title": "Maru's Mission" + }, + { + "compatibility": 99, + "directory": "marvel-pinball-3d", + "releases": [ + {"id": "000400000008E300"} + ], + "title": "Marvel Pinball 3D" + }, + { + "compatibility": 99, + "directory": "masyu-by-nikoli", + "releases": [ + {"id": "000400000008CF00"} + ], + "title": "Masyu by Nikoli" + }, + { + "compatibility": 99, + "directory": "me-and-my-furry-patients-3d", + "releases": [ + {"id": "0004000000126500"} + ], + "title": "Me & My furry patients 3D" + }, + { + "compatibility": 99, + "directory": "me-and-my-pets-3d", + "releases": [ + {"id": "0004000000139200"} + ], + "title": "Me & My Pets 3D" + }, + { + "compatibility": 99, + "directory": "mega-man", + "releases": [ + {"id": "0004000000094400"} + ], + "title": "Mega Man" + }, + { + "compatibility": 99, + "directory": "mega-man-2", + "releases": [ + {"id": "0004000000099900"} + ], + "title": "Mega Man 2" + }, + { + "compatibility": 99, + "directory": "mega-man-3", + "releases": [ + {"id": "00040000000A0D00"} + ], + "title": "Mega Man 3" + }, + { + "compatibility": 99, + "directory": "mega-man-4", + "releases": [ + {"id": "00040000000A1000"} + ], + "title": "Mega Man 4" + }, + { + "compatibility": 99, + "directory": "mega-man-5", + "releases": [ + {"id": "00040000000A1300"} + ], + "title": "Mega Man 5" + }, + { + "compatibility": 4, + "directory": "mega-man-6", + "releases": [ + {"id": "00040000000A1600"} + ], + "title": "Mega Man 6" + }, + { + "compatibility": 99, + "directory": "mega-man-7", + "releases": [ + {"id": "000400000F702800"} + ], + "title": "MEGA MAN 7" + }, + { + "compatibility": 99, + "directory": "mega-man-dr-wilys-revenge", + "releases": [ + {"id": "000400000004F100"} + ], + "title": "Mega Man: Dr. Wily's Revenge" + }, + { + "compatibility": 99, + "directory": "mega-man-ii", + "releases": [ + {"id": "00040000000EF200"} + ], + "title": "Mega Man II" + }, + { + "compatibility": 99, + "directory": "mega-man-iii", + "releases": [ + {"id": "00040000000EF500"} + ], + "title": "Mega Man III" + }, + { + "compatibility": 99, + "directory": "mega-man-iv", + "releases": [ + {"id": "00040000000EF800"} + ], + "title": "Mega Man IV" + }, + { + "compatibility": 2, + "directory": "mega-man-legacy-collection", + "releases": [ + {"id": "0004000000174100"} + ], + "title": "Mega Man Legacy Collection" + }, + { + "compatibility": 99, + "directory": "mega-man-v", + "releases": [ + {"id": "00040000000EFB00"} + ], + "title": "Mega Man V" + }, + { + "compatibility": 99, + "directory": "mega-man-x", + "releases": [ + {"id": "000400000F702600"} + ], + "title": "Mega Man X" + }, + { + "compatibility": 99, + "directory": "mega-man-x2", + "releases": [ + {"id": "000400000F703300"} + ], + "title": "Mega Man X2" + }, + { + "compatibility": 99, + "directory": "mega-man-x3", + "releases": [ + {"id": "000400000F704A00"} + ], + "title": "MEGA MAN X3" + }, + { + "compatibility": 99, + "directory": "mega-man-xtreme", + "releases": [ + {"id": "00040000000EFE00"} + ], + "title": "Mega Man Xtreme" + }, + { + "compatibility": 2, + "directory": "mega-man-xtreme-2", + "releases": [ + {"id": "00040000000F0000"} + ], + "title": "Mega Man Xtreme 2" + }, + { + "compatibility": 99, + "directory": "mercenaries-saga-2", + "releases": [ + {"id": "000400000016CC00"} + ], + "title": "Mercenaries Saga 2" + }, + { + "compatibility": 0, + "directory": "mercenaries-saga-3", + "releases": [ + {"id": "00040000001B1C00"} + ], + "title": "Mercenaries Saga 3" + }, + { + "compatibility": 99, + "directory": "mes-comptines", + "releases": [ + {"id": "0004000000143500"} + ], + "title": "Mes Comptines" + }, + { + "compatibility": 3, + "directory": "metal-gear-solid-snake-eater-3d", + "releases": [ + {"id": "0004000000081E00"} + ], + "title": "METAL GEAR SOLID SNAKE EATER 3D" + }, + { + "compatibility": 2, + "directory": "metroid", + "releases": [ + {"id": "000400000006EE00"} + ], + "title": "Metroid" + }, + { + "compatibility": 2, + "directory": "metroid-ii-return-of-samus", + "releases": [ + {"id": "0004000000050400"} + ], + "title": "Metroid II - Return of Samus" + }, + { + "compatibility": 2, + "directory": "metroid-prime-federation-force", + "releases": [ + {"id": "000400000016E300"} + ], + "title": "Metroid Prime: Federation Force" + }, + { + "compatibility": 2, + "directory": "metroid-prime-federation-force-blast-ball-demo", + "releases": [ + {"id": "000400000016F600"} + ], + "title": "Metroid Prime: Federation Force Blast Ball Demo" + }, + { + "compatibility": 1, + "directory": "metroid-samus-returns", + "releases": [ + {"id": "00040000001BB200"} + ], + "title": "Metroid: Samus Returns" + }, + { + "compatibility": 99, + "directory": "metroid-samus-returns-announcement-trailer", + "releases": [ + {"id": "00040000001C7400"} + ], + "title": "Metroid: Samus Returns Announcement Trailer" + }, + { + "compatibility": 99, + "directory": "mh3u-data-transfer-program", + "releases": [ + {"id": "00040000000C6F00"} + ], + "title": "MH3U Data Transfer Program" + }, + { + "compatibility": 0, + "directory": "michael-jackson-the-experience", + "releases": [ + {"id": "0004000000048500"} + ], + "title": "Michael Jackson The Experience" + }, + { + "compatibility": 99, + "directory": "mighty-bomb-jack", + "releases": [ + {"id": "0004000000094000"} + ], + "title": "Mighty Bomb Jack" + }, + { + "compatibility": 2, + "directory": "mighty-final-fight", + "releases": [ + {"id": "00040000000F8600"} + ], + "title": "Mighty Final Fight" + }, + { + "compatibility": 0, + "directory": "mighty-gunvolt", + "releases": [ + {"id": "000400000014CD00"} + ], + "title": "MIGHTY GUNVOLT" + }, + { + "compatibility": 5, + "directory": "mighty-gunvolt-burst", + "releases": [ + {"id": "00040000001C7700"} + ], + "title": "MIGHTY GUNVOLT BURST" + }, + { + "compatibility": 0, + "directory": "mighty-switch-force", + "releases": [ + {"id": "0004000000066300"} + ], + "title": "Mighty Switch Force!" + }, + { + "compatibility": 1, + "directory": "mighty-switch-force-2", + "releases": [ + {"id": "00040000000C2800"} + ], + "title": "Mighty Switch Force! 2" + }, + { + "compatibility": 4, + "directory": "miitopia", + "releases": [ + {"id": "00040000001B4E00"} + ], + "title": "Miitopia" + }, + { + "compatibility": 1, + "directory": "miitopia-casting-call", + "releases": [ + {"id": "00040000001B8C00"} + ], + "title": "Miitopia: Casting Call" + }, + { + "compatibility": 99, + "directory": "milons-secret-castle", + "releases": [ + {"id": "00040000000B3200"} + ], + "title": "Milon's Secret Castle" + }, + { + "compatibility": 2, + "directory": "minecraft-new-nintendo-3ds-edition", + "releases": [ + {"id": "00040000001B8700"} + ], + "title": "Minecraft: New Nintendo 3DS Edition" + }, + { + "compatibility": 99, + "directory": "mini-golf-resort", + "releases": [ + {"id": "000400000017FA00"} + ], + "title": "Mini Golf Resort" + }, + { + "compatibility": 1, + "directory": "mini-mario-and-friends-amiibo-challenge", + "releases": [ + {"id": "000400000016C200"} + ], + "title": "Mini Mario & Friends amiibo Challenge" + }, + { + "compatibility": 99, + "directory": "mini-sports-collection", + "releases": [ + {"id": "00040000001B6E00"} + ], + "title": "Mini Sports Collection" + }, + { + "compatibility": 3, + "directory": "moco-moco-friends", + "releases": [ + {"id": "000400000017F200"} + ], + "title": "Moco Moco Friends" + }, + { + "compatibility": 99, + "directory": "mole-mania", + "releases": [ + {"id": "0004000000093800"} + ], + "title": "Mole Mania" + }, + { + "compatibility": 3, + "directory": "mom-hid-my-game", + "releases": [ + {"id": "000400000F70E000"} + ], + "title": "Mom Hid My Game!" + }, + { + "compatibility": 99, + "directory": "mononoke-forest", + "releases": [ + {"id": "00040000001BD300"} + ], + "title": "Mononoke Forest" + }, + { + "compatibility": 1, + "directory": "monster-4x4-3d", + "releases": [ + {"id": "0004000000048700"} + ], + "title": "Monster 4x4 3D" + }, + { + "compatibility": 99, + "directory": "monster-combine-td", + "releases": [ + {"id": "0004000000150500"} + ], + "title": "Monster Combine TD" + }, + { + "compatibility": 2, + "directory": "monster-hunter-3-ultimate", + "releases": [ + {"id": "00040000000AE400"} + ], + "title": "Monster Hunter 3 Ultimate" + }, + { + "compatibility": 2, + "directory": "monster-hunter-4-ultimate", + "releases": [ + {"id": "0004000000126300"} + ], + "title": "Monster Hunter 4 Ultimate" + }, + { + "compatibility": 99, + "directory": "monster-hunter-4-ultimate-special-demo", + "releases": [ + {"id": "000400000015FA00"} + ], + "title": "Monster Hunter 4 Ultimate Special Demo" + }, + { + "compatibility": 1, + "directory": "monster-hunter-generations", + "releases": [ + {"id": "0004000000187000"} + ], + "title": "Monster Hunter Generations" + }, + { + "compatibility": 99, + "directory": "monster-hunter-generations-special-demo", + "releases": [ + {"id": "000400000019F000"} + ], + "title": "Monster Hunter Generations Special Demo" + }, + { + "compatibility": 99, + "directory": "monster-hunter-generations-ultimate-save-transfer-app", + "releases": [ + {"id": "00040000001D2500"} + ], + "title": "Monster Hunter Generations Ultimate Save Transfer App" + }, + { + "compatibility": 3, + "directory": "monster-hunter-stories", + "releases": [ + {"id": "00040000001BC500"} + ], + "title": "Monster Hunter Stories" + }, + { + "compatibility": 99, + "directory": "monster-hunter-stories-demo", + "releases": [ + {"id": "00040000001CB800"} + ], + "title": "MONSTER HUNTER STORIES Demo" + }, + { + "compatibility": 99, + "directory": "monster-shooter", + "releases": [ + {"id": "0004000000092500"} + ], + "title": "Monster Shooter" + }, + { + "compatibility": 1, + "directory": "moon-chronicles", + "releases": [ + {"id": "000400000012E700"} + ], + "title": "Moon Chronicles" + }, + { + "compatibility": 99, + "directory": "murder-on-the-titanic", + "releases": [ + {"id": "00040000000C1100"} + ], + "title": "Murder on the Titanic" + }, + { + "compatibility": 99, + "directory": "music-on-electric-guitar", + "releases": [ + {"id": "0004000000164500"} + ], + "title": "Music On: Electric Guitar" + }, + { + "compatibility": 99, + "directory": "musicverse-electronic-keyboard", + "releases": [ + {"id": "000400000016A800"} + ], + "title": "Musicverse: Electronic Keyboard" + }, + { + "compatibility": 1, + "directory": "mutant-mudds", + "releases": [ + {"id": "0004000000086600"} + ], + "title": "Mutant Mudds" + }, + { + "compatibility": 0, + "directory": "mutant-mudds-super-challenge", + "releases": [ + {"id": "0004000000165A00"} + ], + "title": "Mutant Mudds Super Challenge" + }, + { + "compatibility": 99, + "directory": "my-baby-pet-hotel-3d", + "releases": [ + {"id": "00040000000F6E00"} + ], + "title": "My Baby Pet Hotel 3D" + }, + { + "compatibility": 99, + "directory": "my-exotic-farm", + "releases": [ + {"id": "00040000000E7000"} + ], + "title": "My Exotic Farm" + }, + { + "compatibility": 99, + "directory": "my-farm-3d", + "releases": [ + {"id": "00040000000E6200"} + ], + "title": "My Farm 3D" + }, + { + "compatibility": 99, + "directory": "my-first-songs", + "releases": [ + {"id": "0004000000142000"} + ], + "title": "My First Songs" + }, + { + "compatibility": 99, + "directory": "my-first-songs-2", + "releases": [ + {"id": "000400000012D500"} + ], + "title": "My First Songs 2" + }, + { + "compatibility": 99, + "directory": "my-horse-3d-best-friends", + "releases": [ + {"id": "000400000015C100"} + ], + "title": "My Horse 3D - Best Friends" + }, + { + "compatibility": 99, + "directory": "my-life-on-a-farm-3d", + "releases": [ + {"id": "000400000014CA00"} + ], + "title": "My Life on a Farm 3D" + }, + { + "compatibility": 99, + "directory": "my-little-baby-3d", + "releases": [ + {"id": "00040000000FCF00"} + ], + "title": "My Little Baby 3D" + }, + { + "compatibility": 99, + "directory": "my-pet-school-3d", + "releases": [ + {"id": "000400000014CF00"} + ], + "title": "My Pet School 3D" + }, + { + "compatibility": 99, + "directory": "my-pets", + "releases": [ + {"id": "0004000000187200"} + ], + "title": "MY PETS" + }, + { + "compatibility": 99, + "directory": "my-riding-stables-3d-jumping-for-the-team", + "releases": [ + {"id": "00040000000D6900"} + ], + "title": "My Riding Stables 3D - Jumping for the Team" + }, + { + "compatibility": 99, + "directory": "my-style-studio-hair-salon", + "releases": [ + {"id": "0004000000109A00"} + ], + "title": "My Style Studio: Hair Salon" + }, + { + "compatibility": 99, + "directory": "my-vet-practice-3d-in-the-country", + "releases": [ + {"id": "00040000000F7000"} + ], + "title": "My Vet Practice 3D - In the Country" + }, + { + "compatibility": 99, + "directory": "my-western-horse-3d", + "releases": [ + {"id": "00040000000F1300"} + ], + "title": "My Western Horse 3D" + }, + { + "compatibility": 99, + "directory": "my-zoo-vet-practice-3d", + "releases": [ + {"id": "000400000016AB00"} + ], + "title": "My Zoo Vet Practice 3D" + }, + { + "compatibility": 0, + "directory": "myst", + "releases": [ + {"id": "000400000007C200"} + ], + "title": "Myst" + }, + { + "compatibility": 99, + "directory": "mysterious-stars-3d-a-fairy-tale", + "releases": [ + {"id": "00040000001C0800"} + ], + "title": "Mysterious Stars 3D: A Fairy Tale" + }, + { + "compatibility": 99, + "directory": "mysterious-stars-3d-road-to-idol", + "releases": [ + {"id": "00040000001C0900"} + ], + "title": "Mysterious Stars 3D: Road To Idol" + }, + { + "compatibility": 99, + "directory": "mystery-case-files-dire-grove", + "releases": [ + {"id": "00040000000DDE00"} + ], + "title": "Mystery Case Files Dire Grove" + }, + { + "compatibility": 99, + "directory": "mystery-case-files-ravenhearst", + "releases": [ + {"id": "00040000000DDF00"} + ], + "title": "Mystery Case Files Ravenhearst" + }, + { + "compatibility": 99, + "directory": "mystery-case-files-return-to-ravenhearst", + "releases": [ + {"id": "00040000000DE000"} + ], + "title": "Mystery Case Files Return to Ravenhearst" + }, + { + "compatibility": 99, + "directory": "mystical-ninja-starring-goemon", + "releases": [ + {"id": "000400000007E400"} + ], + "title": "MYSTICAL NINJA starring GOEMON" + }, + { + "compatibility": 0, + "directory": "nano-assault", + "releases": [ + {"id": "0004000000047B00"} + ], + "title": "Nano Assault" + }, + { + "compatibility": 0, + "directory": "nano-assault-ex", + "releases": [ + {"id": "00040000000B9E00"} + ], + "title": "Nano Assault EX" + }, + { + "compatibility": 99, + "directory": "navy-commander", + "releases": [ + {"id": "0004000000160400"} + ], + "title": "Navy Commander" + }, + { + "compatibility": 99, + "directory": "ncis-3d-based-on-the-tv-series", + "releases": [ + {"id": "0004000000047D00"} + ], + "title": "NCIS 3D BASED ON THE TV SERIES" + }, + { + "compatibility": 0, + "directory": "need-for-speed-the-run", + "releases": [ + {"id": "0004000000051400"} + ], + "title": "Need For Speed: The Run" + }, + { + "compatibility": 99, + "directory": "nes-open-tournament-golf", + "releases": [ + {"id": "000400000006FA00"} + ], + "title": "NES Open Tournament Golf" + }, + { + "compatibility": 4, + "directory": "netflix", + "releases": [ + {"id": "0004000000057700"} + ], + "title": "Netflix" + }, + { + "compatibility": 1, + "directory": "new-super-mario-bros-2", + "releases": [ + {"id": "000400000007AE00"} + ], + "title": "New Super Mario Bros. 2" + }, + { + "compatibility": 99, + "directory": "nicktoons-mlb-3d", + "releases": [ + {"id": "0004000000061200"} + ], + "title": "Nicktoons MLB 3D" + }, + { + "compatibility": 5, + "directory": "nightsky", + "releases": [ + {"id": "0004000000087700"} + ], + "title": "NightSky" + }, + { + "compatibility": 99, + "directory": "ninja-battle-heroes", + "releases": [ + {"id": "000400000013D200"} + ], + "title": "Ninja Battle Heroes" + }, + { + "compatibility": 99, + "directory": "ninja-gaiden", + "releases": [ + {"id": "0004000000094700"} + ], + "title": "Ninja Gaiden" + }, + { + "compatibility": 99, + "directory": "ninja-gaiden-ii-the-dark-sword-of-chaos", + "releases": [ + {"id": "00040000000A4800"} + ], + "title": "Ninja Gaiden II: The Dark Sword of Chaos" + }, + { + "compatibility": 99, + "directory": "ninja-gaiden-iii-the-ancient-ship-of-doom", + "releases": [ + {"id": "00040000000A6B00"} + ], + "title": "Ninja Gaiden III: The Ancient Ship of Doom" + }, + { + "compatibility": 0, + "directory": "ninja-usagimaru-the-gem-of-blessings-", + "releases": [ + {"id": "000400000015CA00"} + ], + "title": "Ninja Usagimaru - The Gem of Blessings -" + }, + { + "compatibility": 1, + "directory": "ninja-usagimaru-the-mysterious-karakuri-castle", + "releases": [ + {"id": "00040000001A0800"} + ], + "title": "Ninja Usagimaru - The Mysterious Karakuri Castle" + }, + { + "compatibility": 3, + "directory": "nintendo-3ds-guide-louvre-english-version", + "releases": [ + {"id": "00040000000CB900"} + ], + "title": "Nintendo 3DS Guide: Louvre (English Version)" + }, + { + "compatibility": 99, + "directory": "nintendo-3ds-guide-louvre-french-version", + "releases": [ + {"id": "00040000000D5600"} + ], + "title": "Nintendo 3DS Guide: Louvre (French Version)" + }, + { + "compatibility": 99, + "directory": "nintendo-3ds-guide-louvre-spanish-version", + "releases": [ + {"id": "00040000000D5900"} + ], + "title": "Nintendo 3DS Guide: Louvre (Spanish Version)" + }, + { + "compatibility": 4, + "directory": "nintendo-badge-arcade", + "releases": [ + {"id": "0004000000153500"} + ], + "title": "Nintendo Badge Arcade" + }, + { + "compatibility": 1, + "directory": "nintendo-video", + "releases": [ + {"id": "000400000004AA00"} + ], + "title": "Nintendo Video" + }, + { + "compatibility": 1, + "directory": "nintendogs-cats-french-bulldog", + "releases": [ + {"id": "0004000000031200"} + ], + "title": "nintendogs + cats: French Bulldog" + }, + { + "compatibility": 1, + "directory": "nintendogs-cats-golden-retriever", + "releases": [ + {"id": "0004000000030D00"} + ], + "title": "nintendogs + cats: Golden Retriever" + }, + { + "compatibility": 1, + "directory": "nintendogs-cats-toy-poodle", + "releases": [ + {"id": "0004000000031700"} + ], + "title": "nintendogs + cats: Toy Poodle" + }, + { + "compatibility": 99, + "directory": "noahs-cradle", + "releases": [ + {"id": "00040000001A4300"} + ], + "title": "Noah's Cradle" + }, + { + "compatibility": 0, + "directory": "noitu-love-devolution", + "releases": [ + {"id": "000400000018F700"} + ], + "title": "Noitu Love: Devolution" + }, + { + "compatibility": 99, + "directory": "now-i-know-my-abcs-2", + "releases": [ + {"id": "000400000F710100"} + ], + "title": "Now I know my ABCs 2" + }, + { + "compatibility": 99, + "directory": "nurikabe-by-nikoli", + "releases": [ + {"id": "0004000000092A00"} + ], + "title": "Nurikabe by Nikoli" + }, + { + "compatibility": 99, + "directory": "ocean-runner", + "releases": [ + {"id": "00040000000F6B00"} + ], + "title": "Ocean Runner" + }, + { + "compatibility": 0, + "directory": "of-mice-and-sand", + "releases": [ + {"id": "00040000001C1A00"} + ], + "title": "Of Mice And Sand" + }, + { + "compatibility": 99, + "directory": "ohno-odyssey", + "releases": [ + {"id": "0004000000107500"} + ], + "title": "Ohno Odyssey" + }, + { + "compatibility": 99, + "directory": "olliolli", + "releases": [ + {"id": "000400000015A400"} + ], + "title": "OlliOlli" + }, + { + "compatibility": 0, + "directory": "operation-cobra", + "releases": [ + {"id": "000400000F70C600"} + ], + "title": "Operation COBRA" + }, + { + "compatibility": 99, + "directory": "order-up", + "releases": [ + {"id": "000400000009D200"} + ], + "title": "Order Up!!" + }, + { + "compatibility": 99, + "directory": "outback-pet-rescue-3d", + "releases": [ + {"id": "0004000000143A00"} + ], + "title": "Outback Pet Rescue 3D" + }, + { + "compatibility": 99, + "directory": "outdoors-unleashed-africa-3d", + "releases": [ + {"id": "00040000000AE500"} + ], + "title": "Outdoors Unleashed: Africa 3D" + }, + { + "compatibility": 1, + "directory": "pac-man", + "releases": [ + {"id": "000400000009A000"} + ], + "title": "PAC-MAN" + }, + { + "compatibility": 99, + "directory": "pac-man", + "releases": [ + {"id": "000400000004FB00"} + ], + "title": "PAC-MAN" + }, + { + "compatibility": 0, + "directory": "pac-man-and-galaga-dimensions", + "releases": [ + {"id": "0004000000036300"} + ], + "title": "PAC-MAN & Galaga Dimensions" + }, + { + "compatibility": 1, + "directory": "pac-man-and-the-ghostly-adventures", + "releases": [ + {"id": "00040000000F1200"} + ], + "title": "PAC-MAN and the Ghostly Adventures" + }, + { + "compatibility": 3, + "directory": "pac-man-and-the-ghostly-adventures-2", + "releases": [ + {"id": "0004000000135F00"} + ], + "title": "PAC-MAN and the Ghostly Adventures 2" + }, + { + "compatibility": 99, + "directory": "paddington-adventures-in-london", + "releases": [ + {"id": "000400000016B900"} + ], + "title": "Paddington: Adventures in London" + }, + { + "compatibility": 99, + "directory": "painting-workshop", + "releases": [ + {"id": "0004000000143C00"} + ], + "title": "Painting Workshop" + }, + { + "compatibility": 1, + "directory": "paper-mario-sticker-star", + "releases": [ + {"id": "00040000000A5E00"} + ], + "title": "Paper Mario: Sticker Star" + }, + { + "compatibility": 0, + "directory": "parascientific-escape-crossing-at-the-farthest-horizon", + "releases": [ + {"id": "00040000001C7D00"} + ], + "title": "Parascientific Escape - Crossing at the Farthest Horizon" + }, + { + "compatibility": 0, + "directory": "parascientific-escape-cruise-in-the-distant-seas", + "releases": [ + {"id": "0004000000186500"} + ], + "title": "Parascientific Escape Cruise in the Distant Seas" + }, + { + "compatibility": 0, + "directory": "parascientific-escape-gear-detective", + "releases": [ + {"id": "00040000001AE700"} + ], + "title": "Parascientific Escape - Gear Detective" + }, + { + "compatibility": 99, + "directory": "parking-star-3d", + "releases": [ + {"id": "0004000000131500"} + ], + "title": "Parking Star 3D" + }, + { + "compatibility": 99, + "directory": "paws-and-claws-pampered-pets-resort-3d", + "releases": [ + {"id": "0004000000062100"} + ], + "title": "Paws & Claws: Pampered Pets Resort 3D" + }, + { + "compatibility": 99, + "directory": "pazuru", + "releases": [ + {"id": "0004000000154C00"} + ], + "title": "Pazuru" + }, + { + "compatibility": 99, + "directory": "penguin-hop", + "releases": [ + {"id": "000400000F711900"} + ], + "title": "PENGUIN HOP" + }, + { + "compatibility": 99, + "directory": "percys-predicament-deluxe", + "releases": [ + {"id": "000400000F704600"} + ], + "title": "Percy's Predicament Deluxe" + }, + { + "compatibility": 2, + "directory": "persona-q-shadow-of-the-labyrinth", + "releases": [ + {"id": "0004000000123400"} + ], + "title": "Persona Q: Shadow of the Labyrinth" + }, + { + "compatibility": 99, + "directory": "pet-hospital", + "releases": [ + {"id": "0004000000186F00"} + ], + "title": "PET HOSPITAL" + }, + { + "compatibility": 99, + "directory": "pet-inn-3d", + "releases": [ + {"id": "0004000000186A00"} + ], + "title": "PET INN 3D" + }, + { + "compatibility": 99, + "directory": "pet-zombies", + "releases": [ + {"id": "0004000000036200"} + ], + "title": "Pet Zombies" + }, + { + "compatibility": 0, + "directory": "petit-novel-series-harvest-december", + "releases": [ + {"id": "0004000000182800"} + ], + "title": "Petit Novel series - Harvest December" + }, + { + "compatibility": 99, + "directory": "petite-zombies", + "releases": [ + {"id": "00040000001D5B00"} + ], + "title": "Petite Zombies" + }, + { + "compatibility": 99, + "directory": "petz-beach", + "releases": [ + {"id": "0004000000077C00"} + ], + "title": "Petz Beach" + }, + { + "compatibility": 99, + "directory": "petz-countryside", + "releases": [ + {"id": "0004000000077B00"} + ], + "title": "Petz Countryside" + }, + { + "compatibility": 3, + "directory": "petz-fantasy-3d", + "releases": [ + {"id": "0004000000044B00"} + ], + "title": "Petz Fantasy 3D" + }, + { + "compatibility": 3, + "directory": "phasmophobia-hall-of-specters-3d", + "releases": [ + {"id": "000400000F711D00"} + ], + "title": "Phasmophobia: Hall of Specters 3D" + }, + { + "compatibility": 99, + "directory": "phils-epic-fill-a-pix-adventure", + "releases": [ + {"id": "00040000001CCF00"} + ], + "title": "Phil's Epic Fill-a-Pix Adventure" + }, + { + "compatibility": 2, + "directory": "phoenix-wright-ace-attorney-dual-destinies", + "releases": [ + {"id": "00040000000F1400"} + ], + "title": "Phoenix Wright: Ace Attorney - Dual Destinies" + }, + { + "compatibility": 1, + "directory": "phoenix-wright-ace-attorney-spirit-of-justice", + "releases": [ + {"id": "000400000018F400"} + ], + "title": "Phoenix Wright: Ace Attorney – Spirit of Justice" + }, + { + "compatibility": 0, + "directory": "phoenix-wright-ace-attorney-trilogy", + "releases": [ + {"id": "0004000000138F00"} + ], + "title": "Phoenix Wright: Ace Attorney Trilogy" + }, + { + "compatibility": 0, + "directory": "photos-with-mario", + "releases": [ + {"id": "0004000000130500"} + ], + "title": "Photos with Mario" + }, + { + "compatibility": 99, + "directory": "physical-contact-2048", + "releases": [ + {"id": "000400000F70EA00"} + ], + "title": "Physical contact: 2048" + }, + { + "compatibility": 99, + "directory": "physical-contact-picture-place", + "releases": [ + {"id": "000400000F70EB00"} + ], + "title": "Physical Contact: Picture Place" + }, + { + "compatibility": 99, + "directory": "physical-contact-speed", + "releases": [ + {"id": "000400000F70E900"} + ], + "title": "Physical Contact: SPEED" + }, + { + "compatibility": 99, + "directory": "pic-a-pix-color", + "releases": [ + {"id": "00040000001B7800"} + ], + "title": "Pic-a-Pix Color" + }, + { + "compatibility": 0, + "directory": "picdun-2-witchs-curse", + "releases": [ + {"id": "00040000000C5100"} + ], + "title": "Picdun 2: Witch's Curse" + }, + { + "compatibility": 99, + "directory": "pick-a-gem", + "releases": [ + {"id": "00040000000D6C00"} + ], + "title": "Pick-A-Gem" + }, + { + "compatibility": 2, + "directory": "picross-3d-round-2", + "releases": [ + {"id": "0004000000187D00"} + ], + "title": "Picross 3D Round 2" + }, + { + "compatibility": 1, + "directory": "picross-e", + "releases": [ + {"id": "00040000000E5D00"} + ], + "title": "PICROSS e" + }, + { + "compatibility": 99, + "directory": "picross-e2", + "releases": [ + {"id": "00040000000CD400"} + ], + "title": "PICROSS e2" + }, + { + "compatibility": 99, + "directory": "picross-e3", + "releases": [ + {"id": "0004000000101D00"} + ], + "title": "PICROSS e3" + }, + { + "compatibility": 0, + "directory": "picross-e4", + "releases": [ + {"id": "0004000000127300"} + ], + "title": "PICROSS e4" + }, + { + "compatibility": 0, + "directory": "picross-e5", + "releases": [ + {"id": "0004000000149800"} + ], + "title": "PICROSS e5" + }, + { + "compatibility": 99, + "directory": "picross-e6", + "releases": [ + {"id": "000400000016EF00"} + ], + "title": "PICROSS e6" + }, + { + "compatibility": 0, + "directory": "picross-e7", + "releases": [ + {"id": "00040000001ADB00"} + ], + "title": "PICROSS e7" + }, + { + "compatibility": 0, + "directory": "picross-e8", + "releases": [ + {"id": "00040000001CF700"} + ], + "title": "PICROSS e8" + }, + { + "compatibility": 1, + "directory": "pikmin-short-movies-3d", + "releases": [ + {"id": "000400000011D500"} + ], + "title": "Pikmin Short Movies 3D" + }, + { + "compatibility": 99, + "directory": "pilotwings", + "releases": [ + {"id": "000400000F702200"} + ], + "title": "Pilotwings" + }, + { + "compatibility": 2, + "directory": "pilotwings-resort", + "releases": [ + {"id": "0004000000031C00"} + ], + "title": "Pilotwings Resort" + }, + { + "compatibility": 99, + "directory": "pinball-breakout", + "releases": [ + {"id": "000400000F70B400"} + ], + "title": "Pinball Breakout" + }, + { + "compatibility": 99, + "directory": "pinball-breakout", + "releases": [ + {"id": "00040000001D6F00"} + ], + "title": "Pinball Breakout" + }, + { + "compatibility": 99, + "directory": "pinball-breakout-2", + "releases": [ + {"id": "00040000001D6400"} + ], + "title": "Pinball Breakout 2" + }, + { + "compatibility": 0, + "directory": "pinball-hall-of-fame-the-williams-collection", + "releases": [ + {"id": "000400000004C600"} + ], + "title": "Pinball Hall of Fame: The Williams Collection" + }, + { + "compatibility": 99, + "directory": "pinball-revenge-of-the-gator", + "releases": [ + {"id": "00040000000B2700"} + ], + "title": "Pinball: Revenge of the Gator" + }, + { + "compatibility": 0, + "directory": "ping-pong-trick-shot", + "releases": [ + {"id": "0004000000167500"} + ], + "title": "Ping Pong Trick Shot" + }, + { + "compatibility": 99, + "directory": "ping-pong-trick-shot-2", + "releases": [ + {"id": "00040000001BFE00"} + ], + "title": "Ping Pong Trick Shot 2" + }, + { + "compatibility": 99, + "directory": "pink-dot-blue-dot", + "releases": [ + {"id": "000400000F709E00"} + ], + "title": "PINK DOT BLUE DOT" + }, + { + "compatibility": 4, + "directory": "pirate-pop-plus", + "releases": [ + {"id": "000400000F708600"} + ], + "title": "Pirate Pop Plus" + }, + { + "compatibility": 0, + "directory": "pix3d", + "releases": [ + {"id": "00040000000AFA00"} + ], + "title": "PIX3D" + }, + { + "compatibility": 99, + "directory": "pixel-hunter", + "releases": [ + {"id": "000400000F707800"} + ], + "title": "Pixel Hunter" + }, + { + "compatibility": 99, + "directory": "pixel-paint", + "releases": [ + {"id": "000400000019E400"} + ], + "title": "Pixel Paint" + }, + { + "compatibility": 99, + "directory": "pixelmaker", + "releases": [ + {"id": "00040000001A6700"} + ], + "title": "PixelMaker" + }, + { + "compatibility": 99, + "directory": "pixelmaker-studio", + "releases": [ + {"id": "00040000001C6D00"} + ], + "title": "PixelMaker Studio" + }, + { + "compatibility": 99, + "directory": "plain-video-poker", + "releases": [ + {"id": "0004000000139800"} + ], + "title": "Plain Video Poker" + }, + { + "compatibility": 99, + "directory": "planet-crashers", + "releases": [ + {"id": "000400000009B500"} + ], + "title": "Planet Crashers" + }, + { + "compatibility": 99, + "directory": "plantera", + "releases": [ + {"id": "00040000001A8F00"} + ], + "title": "Plantera" + }, + { + "compatibility": 1, + "directory": "pocket-card-jockey", + "releases": [ + {"id": "0004000000194800"} + ], + "title": "Pocket Card Jockey" + }, + { + "compatibility": 1, + "directory": "pokedex-3d-pro", + "releases": [ + {"id": "0004000000051E00"} + ], + "title": "Pokédex 3D Pro" + }, + { + "compatibility": 2, + "directory": "pokemon-alpha-sapphire", + "releases": [ + {"id": "000400000011C500"} + ], + "title": "Pokémon Alpha Sapphire" + }, + { + "compatibility": 2, + "directory": "pokemon-art-academy", + "releases": [ + {"id": "00040000000D0A00"} + ], + "title": "Pokémon Art Academy" + }, + { + "compatibility": 4, + "directory": "pokemon-bank", + "releases": [ + {"id": "00040000000C9B00"} + ], + "title": "Pokémon Bank" + }, + { + "compatibility": 99, + "directory": "pokemon-battle-trozei", + "releases": [ + {"id": "000400000011C600"} + ], + "title": "Pokémon Battle Trozei" + }, + { + "compatibility": 2, + "directory": "pokemon-blue-version-english-version", + "releases": [ + {"id": "0004000000171100"} + ], + "title": "Pokémon Blue Version (English Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-blue-version-french-version", + "releases": [ + {"id": "0004000000171700"} + ], + "title": "Pokémon Blue Version (French Version)" + }, + { + "compatibility": 1, + "directory": "pokemon-blue-version-spanish-version", + "releases": [ + {"id": "0004000000171A00"} + ], + "title": "Pokémon Blue Version (Spanish Version)" + }, + { + "compatibility": 2, + "directory": "pokemon-crystal-version-english-version", + "releases": [ + {"id": "0004000000172800"} + ], + "title": "Pokémon Crystal Version (English Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-crystal-version-french-version", + "releases": [ + {"id": "0004000000172E00"} + ], + "title": "Pokémon Crystal Version (French Version)" + }, + { + "compatibility": 2, + "directory": "pokemon-crystal-version-spanish-version", + "releases": [ + {"id": "0004000000173100"} + ], + "title": "Pokémon Crystal Version (Spanish Version)" + }, + { + "compatibility": 2, + "directory": "pokemon-dream-radar", + "releases": [ + {"id": "00040000000AE100"} + ], + "title": "Pokémon Dream Radar" + }, + { + "compatibility": 1, + "directory": "pokemon-gold-version-english-version", + "releases": [ + {"id": "0004000000172600"} + ], + "title": "Pokémon Gold Version (English Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-gold-version-french-version", + "releases": [ + {"id": "0004000000172C00"} + ], + "title": "Pokémon Gold Version (French Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-gold-version-spanish-version", + "releases": [ + {"id": "0004000000172F00"} + ], + "title": "Pokémon Gold Version (Spanish Version)" + }, + { + "compatibility": 3, + "directory": "pokemon-moon", + "releases": [ + {"id": "0004000000175E00"} + ], + "title": "Pokémon Moon" + }, + { + "compatibility": 1, + "directory": "pokemon-mystery-dungeon-gates-to-infinity", + "releases": [ + {"id": "00040000000BA800"} + ], + "title": "Pokémon Mystery Dungeon: Gates to Infinity" + }, + { + "compatibility": 3, + "directory": "pokemon-omega-ruby", + "releases": [ + {"id": "000400000011C400"} + ], + "title": "Pokémon Omega Ruby" + }, + { + "compatibility": 1, + "directory": "pokemon-omega-ruby-and-alpha-sapphire-special-demo", + "releases": [ + {"id": "000400000014A300"} + ], + "title": "Pokémon Omega Ruby and Alpha Sapphire Special Demo" + }, + { + "compatibility": 0, + "directory": "pokemon-picross", + "releases": [ + {"id": "000400000017C100"} + ], + "title": "Pokémon Picross" + }, + { + "compatibility": 99, + "directory": "pokemon-puzzle-challenge", + "releases": [ + {"id": "0004000000119D00"} + ], + "title": "Pokémon Puzzle Challenge" + }, + { + "compatibility": 1, + "directory": "pokemon-red-version-english-version", + "releases": [ + {"id": "0004000000171000"} + ], + "title": "Pokémon Red Version (English Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-red-version-french-version", + "releases": [ + {"id": "0004000000171600"} + ], + "title": "Pokémon Red Version (French Version)" + }, + { + "compatibility": 0, + "directory": "pokemon-red-version-spanish-version", + "releases": [ + {"id": "0004000000171900"} + ], + "title": "Pokémon Red Version (Spanish Version)" + }, + { + "compatibility": 2, + "directory": "pokemon-rumble-blast", + "releases": [ + {"id": "0004000000066C00"} + ], + "title": "Pokémon Rumble Blast" + }, + { + "compatibility": 1, + "directory": "pokemon-rumble-world", + "releases": [ + {"id": "0004000000164600"} + ], + "title": "Pokémon Rumble World" + }, + { + "compatibility": 2, + "directory": "pokemon-shuffle", + "releases": [ + {"id": "0004000000141000"} + ], + "title": "Pokémon Shuffle" + }, + { + "compatibility": 2, + "directory": "pokemon-silver-version-english-version", + "releases": [ + {"id": "0004000000172700"} + ], + "title": "Pokémon Silver Version (English Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-silver-version-french-version", + "releases": [ + {"id": "0004000000172D00"} + ], + "title": "Pokémon Silver Version (French Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-silver-version-spanish-version", + "releases": [ + {"id": "0004000000173000"} + ], + "title": "Pokémon Silver Version (Spanish Version)" + }, + { + "compatibility": 3, + "directory": "pokemon-sun", + "releases": [ + {"id": "0004000000164800"} + ], + "title": "Pokémon Sun" + }, + { + "compatibility": 2, + "directory": "pokemon-sun-and-pokemon-moon-special-demo", + "releases": [ + {"id": "00040000001A7100"} + ], + "title": "Pokémon Sun and Pokémon Moon Special Demo" + }, + { + "compatibility": 2, + "directory": "pokemon-super-mystery-dungeon", + "releases": [ + {"id": "0004000000174600"} + ], + "title": "Pokémon Super Mystery Dungeon" + }, + { + "compatibility": 2, + "directory": "pokemon-trading-card-game", + "releases": [ + {"id": "000400000011A000"} + ], + "title": "Pokémon Trading Card Game" + }, + { + "compatibility": 3, + "directory": "pokemon-ultra-moon", + "releases": [ + {"id": "00040000001B5100"} + ], + "title": "Pokémon Ultra Moon" + }, + { + "compatibility": 2, + "directory": "pokemon-ultra-sun", + "releases": [ + {"id": "00040000001B5000"} + ], + "title": "Pokémon Ultra Sun" + }, + { + "compatibility": 2, + "directory": "pokemon-x", + "releases": [ + {"id": "0004000000055D00"} + ], + "title": "Pokémon X" + }, + { + "compatibility": 2, + "directory": "pokemon-y", + "releases": [ + {"id": "0004000000055E00"} + ], + "title": "Pokémon Y" + }, + { + "compatibility": 2, + "directory": "pokemon-yellow-version-english-version", + "releases": [ + {"id": "0004000000171200"} + ], + "title": "Pokémon Yellow Version (English Version)" + }, + { + "compatibility": 99, + "directory": "pokemon-yellow-version-french-version", + "releases": [ + {"id": "0004000000171800"} + ], + "title": "Pokémon Yellow Version (French Version)" + }, + { + "compatibility": 0, + "directory": "pokemon-yellow-version-spanish-version", + "releases": [ + {"id": "0004000000171B00"} + ], + "title": "Pokémon Yellow Version (Spanish Version)" + }, + { + "compatibility": 99, + "directory": "polara", + "releases": [ + {"id": "000400000019F400"} + ], + "title": "Polara" + }, + { + "compatibility": 1, + "directory": "poochy-and-yoshis-woolly-world", + "releases": [ + {"id": "00040000001A4100"} + ], + "title": "Poochy & Yoshi’s Woolly World" + }, + { + "compatibility": 99, + "directory": "power-disc-slam", + "releases": [ + {"id": "0004000000158E00"} + ], + "title": "Power Disc Slam" + }, + { + "compatibility": 99, + "directory": "prince-of-persia", + "releases": [ + {"id": "0004000000069A00"} + ], + "title": "Prince of Persia" + }, + { + "compatibility": 2, + "directory": "pro-evolution-soccer-2012-3d", + "releases": [ + {"id": "0004000000067500"} + ], + "title": "Pro Evolution Soccer 2012 3D" + }, + { + "compatibility": 2, + "directory": "professor-layton-and-the-azran-legacy", + "releases": [ + {"id": "00040000000F2F00"} + ], + "title": "Professor Layton and the Azran Legacy" + }, + { + "compatibility": 1, + "directory": "professor-layton-and-the-miracle-mask", + "releases": [ + {"id": "00040000000A8500"} + ], + "title": "Professor Layton and the Miracle Mask" + }, + { + "compatibility": 1, + "directory": "professor-layton-vs-phoenix-wright-ace-attorney", + "releases": [ + {"id": "0004000000100700"} + ], + "title": "Professor Layton vs. Phoenix Wright: Ace Attorney" + }, + { + "compatibility": 99, + "directory": "proun", + "releases": [ + {"id": "0004000000148F00"} + ], + "title": "Proun+" + }, + { + "compatibility": 99, + "directory": "psycho-pigs", + "releases": [ + {"id": "0004000000191F00"} + ], + "title": "Psycho Pigs" + }, + { + "compatibility": 5, + "directory": "punch-club", + "releases": [ + {"id": "00040000001A8200"} + ], + "title": "Punch Club" + }, + { + "compatibility": 99, + "directory": "punch-out-featuring-mr-dream", + "releases": [ + {"id": "000400000006EB00"} + ], + "title": "Punch-Out!! Featuring Mr. Dream" + }, + { + "compatibility": 99, + "directory": "puppies-3d", + "releases": [ + {"id": "000400000004E000"} + ], + "title": "Puppies 3D" + }, + { + "compatibility": 99, + "directory": "pure-chess", + "releases": [ + {"id": "00040000000F1700"} + ], + "title": "Pure Chess" + }, + { + "compatibility": 99, + "directory": "purr-pals-purrfection", + "releases": [ + {"id": "0004000000084900"} + ], + "title": "Purr Pals Purrfection" + }, + { + "compatibility": 0, + "directory": "pushmo", + "releases": [ + {"id": "0004000000068E00"} + ], + "title": "Pushmo" + }, + { + "compatibility": 1, + "directory": "puzzle-and-dragons-z-super-mario-bros-edition", + "releases": [ + {"id": "0004000000137700"} + ], + "title": "Puzzle & Dragons Z + Super Mario Bros. Edition" + }, + { + "compatibility": 99, + "directory": "puzzle-and-dragons-z-super-mario-bros-edition-20", + "releases": [ + {"id": "0004000E00137700"} + ], + "title": "Puzzle & Dragons Z + Super Mario Bros. Edition 2.0" + }, + { + "compatibility": 99, + "directory": "puzzle-and-dragons-z-super-mario-bros-edition-demo", + "releases": [ + {"id": "000400000016C500"} + ], + "title": "Puzzle & Dragons Z + Super Mario Bros. Edition Demo" + }, + { + "compatibility": 99, + "directory": "puzzle-labyrinth", + "releases": [ + {"id": "0004000000190F00"} + ], + "title": "Puzzle Labyrinth" + }, + { + "compatibility": 99, + "directory": "puzzlebox-setup", + "releases": [ + {"id": "000400000015AA00"} + ], + "title": "PUZZLEBOX setup" + }, + { + "compatibility": 99, + "directory": "puzzler-brain-games", + "releases": [ + {"id": "00040000000D4D00"} + ], + "title": "Puzzler Brain Games" + }, + { + "compatibility": 99, + "directory": "puzzler-mind-gym-3", + "releases": [ + {"id": "000400000004EC00"} + ], + "title": "Puzzler Mind Gym 3" + }, + { + "compatibility": 0, + "directory": "pyramids", + "releases": [ + {"id": "0004000000068200"} + ], + "title": "Pyramids" + }, + { + "compatibility": 99, + "directory": "pyramids-2", + "releases": [ + {"id": "0004000000126E00"} + ], + "title": "Pyramids 2" + }, + { + "compatibility": 99, + "directory": "qix", + "releases": [ + {"id": "0004000000041400"} + ], + "title": "Qix" + }, + { + "compatibility": 99, + "directory": "quarth", + "releases": [ + {"id": "000400000007E100"} + ], + "title": "Quarth" + }, + { + "compatibility": 1, + "directory": "quell-memento", + "releases": [ + {"id": "0004000000119600"} + ], + "title": "Quell Memento" + }, + { + "compatibility": 99, + "directory": "quell-reflect", + "releases": [ + {"id": "0004000000119700"} + ], + "title": "Quell Reflect" + }, + { + "compatibility": 99, + "directory": "quest-of-dungeons", + "releases": [ + {"id": "0004000000186D00"} + ], + "title": "Quest of Dungeons" + }, + { + "compatibility": 99, + "directory": "quiet-please", + "releases": [ + {"id": "00040000001B3500"} + ], + "title": "Quiet, Please!" + }, + { + "compatibility": 0, + "directory": "rabbids-rumble", + "releases": [ + {"id": "0004000000055400"} + ], + "title": "Rabbids Rumble" + }, + { + "compatibility": 2, + "directory": "rabbids-travel-in-time-3d", + "releases": [ + {"id": "0004000000035700"} + ], + "title": "Rabbids Travel in Time 3D" + }, + { + "compatibility": 1, + "directory": "race-to-the-line", + "releases": [ + {"id": "0004000000107800"} + ], + "title": "Race To The Line" + }, + { + "compatibility": 99, + "directory": "radar-mission", + "releases": [ + {"id": "0004000000042E00"} + ], + "title": "Radar Mission" + }, + { + "compatibility": 1, + "directory": "radiant-historia-perfect-chronology", + "releases": [ + {"id": "00040000001C8F00"} + ], + "title": "Radiant Historia: Perfect Chronology" + }, + { + "compatibility": 99, + "directory": "radiohammer", + "releases": [ + {"id": "000400000017A100"} + ], + "title": "Radiohammer" + }, + { + "compatibility": 99, + "directory": "rage-of-the-gladiator", + "releases": [ + {"id": "00040000000AAC00"} + ], + "title": "Rage of the Gladiator" + }, + { + "compatibility": 1, + "directory": "rainbow-snake", + "releases": [ + {"id": "00040000001D3F00"} + ], + "title": "Rainbow Snake" + }, + { + "compatibility": 99, + "directory": "raining-coins", + "releases": [ + {"id": "000400000F70CB00"} + ], + "title": "Raining Coins" + }, + { + "compatibility": 99, + "directory": "rayman", + "releases": [ + {"id": "0004000000083600"} + ], + "title": "Rayman" + }, + { + "compatibility": 2, + "directory": "rayman-3d", + "releases": [ + {"id": "0004000000036400"} + ], + "title": "RAYMAN 3D" + }, + { + "compatibility": 0, + "directory": "rayman-origins", + "releases": [ + {"id": "0004000000057600"} + ], + "title": "RAYMAN ORIGINS" + }, + { + "compatibility": 99, + "directory": "real-heroes-firefighter-3d", + "releases": [ + {"id": "00040000000C5700"} + ], + "title": "Real Heroes: Firefighter 3D" + }, + { + "compatibility": 99, + "directory": "reel-fishing-3d-paradise-mini", + "releases": [ + {"id": "00040000000C2000"} + ], + "title": "Reel Fishing 3D Paradise Mini" + }, + { + "compatibility": 1, + "directory": "reel-fishing-paradise-3d", + "releases": [ + {"id": "0004000000048D00"} + ], + "title": "Reel Fishing Paradise 3D" + }, + { + "compatibility": 99, + "directory": "renegade", + "releases": [ + {"id": "00040000000CA900"} + ], + "title": "Renegade" + }, + { + "compatibility": 3, + "directory": "resident-evil-revelations", + "releases": [ + {"id": "0004000000060200"} + ], + "title": "Resident Evil Revelations" + }, + { + "compatibility": 2, + "directory": "resident-evil-the-mercenaries-3d", + "releases": [ + {"id": "0004000000035900"} + ], + "title": "Resident Evil: The Mercenaries 3D" + }, + { + "compatibility": 0, + "directory": "retro-city-rampage-dx", + "releases": [ + {"id": "000400000010D200"} + ], + "title": "Retro City Rampage: DX" + }, + { + "compatibility": 1, + "directory": "return-to-popolocrois-a-story-of-seasons-fairytale", + "releases": [ + {"id": "000400000018CC00"} + ], + "title": "Return to PopoloCrois: A STORY OF SEASONS Fairytale" + }, + { + "compatibility": 1, + "directory": "rhythm-heaven-megamix", + "releases": [ + {"id": "000400000018A400"} + ], + "title": "Rhythm Heaven Megamix" + }, + { + "compatibility": 1, + "directory": "rhythm-thief-and-the-emperors-treasure", + "releases": [ + {"id": "0004000000067600"} + ], + "title": "Rhythm Thief & the Emperor's Treasure" + }, + { + "compatibility": 2, + "directory": "ridge-racer-3d", + "releases": [ + {"id": "0004000000035800"} + ], + "title": "RIDGE RACER 3D" + }, + { + "compatibility": 99, + "directory": "riding-stables-3d", + "releases": [ + {"id": "00040000000BE700"} + ], + "title": "Riding Stables 3D" + }, + { + "compatibility": 99, + "directory": "riding-star-3d", + "releases": [ + {"id": "0004000000149000"} + ], + "title": "Riding Star 3D" + }, + { + "compatibility": 99, + "directory": "rise-of-the-guardians-the-video-game", + "releases": [ + {"id": "00040000000B0400"} + ], + "title": "Rise of the Guardians: The Video Game" + }, + { + "compatibility": 0, + "directory": "river-city-knights-of-justice", + "releases": [ + {"id": "00040000001C0D00"} + ], + "title": "River City: Knights of Justice" + }, + { + "compatibility": 99, + "directory": "river-city-ransom", + "releases": [ + {"id": "00040000000A4300"} + ], + "title": "River City Ransom" + }, + { + "compatibility": 1, + "directory": "river-city-rival-showdown", + "releases": [ + {"id": "00040000001CC600"} + ], + "title": "River City: Rival Showdown" + }, + { + "compatibility": 3, + "directory": "river-city-tokyo-rumble", + "releases": [ + {"id": "0004000000197700"} + ], + "title": "River City: Tokyo Rumble" + }, + { + "compatibility": 99, + "directory": "robot-rescue-3d", + "releases": [ + {"id": "00040000000D0500"} + ], + "title": "Robot Rescue 3D" + }, + { + "compatibility": 1, + "directory": "rodea-the-sky-soldier", + "releases": [ + {"id": "0004000000168800"} + ], + "title": "Rodea the Sky Soldier" + }, + { + "compatibility": 0, + "directory": "rpg-maker-fes", + "releases": [ + {"id": "00040000001BD500"} + ], + "title": "RPG Maker Fes" + }, + { + "compatibility": 0, + "directory": "rpg-maker-player", + "releases": [ + {"id": "00040000001BD400"} + ], + "title": "RPG Maker Player" + }, + { + "compatibility": 99, + "directory": "rto", + "releases": [ + {"id": "000400000F70D600"} + ], + "title": "RTO" + }, + { + "compatibility": 99, + "directory": "rto-2", + "releases": [ + {"id": "000400000F70FD00"} + ], + "title": "RTO 2" + }, + { + "compatibility": 99, + "directory": "rto-2", + "releases": [ + {"id": "00040000001D7300"} + ], + "title": "RTO 2" + }, + { + "compatibility": 99, + "directory": "rto-3", + "releases": [ + {"id": "000400000F712000"} + ], + "title": "RTO 3" + }, + { + "compatibility": 99, + "directory": "rto-3", + "releases": [ + {"id": "00040000001D8100"} + ], + "title": "RTO 3" + }, + { + "compatibility": 99, + "directory": "rubiks-cube", + "releases": [ + {"id": "000400000010BD00"} + ], + "title": "Rubik's Cube" + }, + { + "compatibility": 1, + "directory": "runbow-pocket", + "releases": [ + {"id": "000400000F708700"} + ], + "title": "Runbow Pocket" + }, + { + "compatibility": 1, + "directory": "rune-factory-4", + "releases": [ + {"id": "00040000000D2800"} + ], + "title": "Rune Factory 4" + }, + { + "compatibility": 99, + "directory": "runny-egg", + "releases": [ + {"id": "0004000000166200"} + ], + "title": "Runny Egg" + }, + { + "compatibility": 2, + "directory": "rustys-real-deal-baseball", + "releases": [ + {"id": "0004000000106400"} + ], + "title": "Rusty's Real Deal Baseball" + }, + { + "compatibility": 99, + "directory": "rv-7-my-drone", + "releases": [ + {"id": "0004000000188000"} + ], + "title": "RV-7 My Drone" + }, + { + "compatibility": 2, + "directory": "rytmik-ultimate", + "releases": [ + {"id": "000400000014DB00"} + ], + "title": "Rytmik Ultimate" + }, + { + "compatibility": 1, + "directory": "sabans-power-rangers-megaforce", + "releases": [ + {"id": "00040000000E7100"} + ], + "title": "Saban's Power Rangers Megaforce" + }, + { + "compatibility": 0, + "directory": "sabans-power-rangers-super-megaforce", + "releases": [ + {"id": "000400000012CF00"} + ], + "title": "Saban's Power Rangers Super Megaforce" + }, + { + "compatibility": 0, + "directory": "sadame", + "releases": [ + {"id": "0004000000186C00"} + ], + "title": "Sadame" + }, + { + "compatibility": 99, + "directory": "safari-quest", + "releases": [ + {"id": "000400000014D500"} + ], + "title": "Safari Quest" + }, + { + "compatibility": 99, + "directory": "sakura-samurai-art-of-the-sword", + "releases": [ + {"id": "000400000007CD00"} + ], + "title": "Sakura Samurai: Art of the Sword" + }, + { + "compatibility": 99, + "directory": "samurai-defender", + "releases": [ + {"id": "000400000016AC00"} + ], + "title": "Samurai Defender" + }, + { + "compatibility": 99, + "directory": "samurai-g", + "releases": [ + {"id": "00040000000AF300"} + ], + "title": "Samurai G" + }, + { + "compatibility": 0, + "directory": "samurai-sword-destiny", + "releases": [ + {"id": "000400000007FB00"} + ], + "title": "Samurai Sword Destiny" + }, + { + "compatibility": 1, + "directory": "samurai-warriors-chronicles", + "releases": [ + {"id": "0004000000034D00"} + ], + "title": "SAMURAI WARRIORS: Chronicles" + }, + { + "compatibility": 99, + "directory": "samurai-warriors-chronicles-3", + "releases": [ + {"id": "000400000016F200"} + ], + "title": "SAMURAI WARRIORS: Chronicles 3" + }, + { + "compatibility": 99, + "directory": "sanrio-characters-picross", + "releases": [ + {"id": "00040000001D3400"} + ], + "title": "Sanrio characters Picross" + }, + { + "compatibility": 4, + "directory": "save-data-transfer-tool", + "releases": [ + {"id": "00040000000C7300"} + ], + "title": "Save Data Transfer Tool" + }, + { + "compatibility": 99, + "directory": "scarygirl-illustration-kit", + "releases": [ + {"id": "000400000013D800"} + ], + "title": "Scarygirl Illustration Kit" + }, + { + "compatibility": 99, + "directory": "scat", + "releases": [ + {"id": "000400000011AF00"} + ], + "title": "S.C.A.T." + }, + { + "compatibility": 99, + "directory": "scooby-doo-and-looney-tunes-cartoon-universe-adventure", + "releases": [ + {"id": "000400000012EE00"} + ], + "title": "Scooby Doo & Looney Tunes Cartoon Universe: Adventure" + }, + { + "compatibility": 99, + "directory": "scoopn-birds", + "releases": [ + {"id": "000400000F708400"} + ], + "title": "Scoop'n Birds" + }, + { + "compatibility": 1, + "directory": "scribblenauts-unlimited", + "releases": [ + {"id": "0004000000038600"} + ], + "title": "Scribblenauts Unlimited" + }, + { + "compatibility": 0, + "directory": "scribblenauts-unmasked-a-dc-comics-adventure", + "releases": [ + {"id": "00040000000D2300"} + ], + "title": "Scribblenauts Unmasked: A DC Comics Adventure" + }, + { + "compatibility": 99, + "directory": "secret-agent-files-miami", + "releases": [ + {"id": "00040000000C6E00"} + ], + "title": "Secret Agent Files: Miami" + }, + { + "compatibility": 99, + "directory": "secret-empires-of-the-ancient-world", + "releases": [ + {"id": "000400000013EC00"} + ], + "title": "Secret Empires of the Ancient World" + }, + { + "compatibility": 99, + "directory": "secret-journeys-cities-of-the-world", + "releases": [ + {"id": "000400000013ED00"} + ], + "title": "Secret Journeys: Cities of the World" + }, + { + "compatibility": 99, + "directory": "secret-mysteries-in-new-york", + "releases": [ + {"id": "00040000000F4A00"} + ], + "title": "Secret Mysteries in New York" + }, + { + "compatibility": 3, + "directory": "sega-3d-classics-collection", + "releases": [ + {"id": "0004000000185E00"} + ], + "title": "SEGA 3D Classics Collection" + }, + { + "compatibility": 2, + "directory": "senran-kagura-2-deep-crimson", + "releases": [ + {"id": "0004000000165000"} + ], + "title": "SENRAN KAGURA 2: Deep Crimson" + }, + { + "compatibility": 2, + "directory": "senran-kagura-burst", + "releases": [ + {"id": "00040000000ED100"} + ], + "title": "SENRAN KAGURA Burst" + }, + { + "compatibility": 99, + "directory": "severed", + "releases": [ + {"id": "0004000000166300"} + ], + "title": "Severed" + }, + { + "compatibility": 99, + "directory": "shadow-of-the-ninja", + "releases": [ + {"id": "000400000011AD00"} + ], + "title": "Shadow of the Ninja" + }, + { + "compatibility": 99, + "directory": "shanghai-mahjong", + "releases": [ + {"id": "000400000010AA00"} + ], + "title": "Shanghai Mahjong" + }, + { + "compatibility": 99, + "directory": "shantae", + "releases": [ + {"id": "00040000000BE500"} + ], + "title": "Shantae" + }, + { + "compatibility": 1, + "directory": "shantae-and-the-pirates-curse", + "releases": [ + {"id": "00040000000C2900"} + ], + "title": "Shantae and the Pirate's Curse" + }, + { + "compatibility": 0, + "directory": "shift-dx", + "releases": [ + {"id": "0004000000190000"} + ], + "title": "Shift DX" + }, + { + "compatibility": 1, + "directory": "shifting-world", + "releases": [ + {"id": "0004000000057100"} + ], + "title": "Shifting World" + }, + { + "compatibility": 1, + "directory": "shin-megami-tensei-devil-summoner-soul-hackers", + "releases": [ + {"id": "00040000000C5600"} + ], + "title": "Shin Megami Tensei: Devil Summoner: Soul Hackers" + }, + { + "compatibility": 1, + "directory": "shin-megami-tensei-devil-survivor-2-record-breaker", + "releases": [ + {"id": "0004000000159500"} + ], + "title": "Shin Megami Tensei: Devil Survivor 2: Record Breaker" + }, + { + "compatibility": 1, + "directory": "shin-megami-tensei-devil-survivor-overclocked", + "releases": [ + {"id": "0004000000038800"} + ], + "title": "Shin Megami Tensei: Devil Survivor Overclocked" + }, + { + "compatibility": 2, + "directory": "shin-megami-tensei-iv", + "releases": [ + {"id": "00040000000E5C00"} + ], + "title": "Shin Megami Tensei IV" + }, + { + "compatibility": 1, + "directory": "shin-megami-tensei-iv-apocalypse", + "releases": [ + {"id": "000400000019A200"} + ], + "title": "Shin Megami Tensei IV: Apocalypse" + }, + { + "compatibility": 0, + "directory": "shin-megami-tensei-strange-journey-redux", + "releases": [ + {"id": "00040000001CD200"} + ], + "title": "Shin Megami Tensei: Strange Journey Redux" + }, + { + "compatibility": 99, + "directory": "shining-force-sword-of-hajya", + "releases": [ + {"id": "000400000009C500"} + ], + "title": "Shining Force: Sword of Hajya" + }, + { + "compatibility": 2, + "directory": "shinobi", + "releases": [ + {"id": "0004000000055B00"} + ], + "title": "Shinobi" + }, + { + "compatibility": 99, + "directory": "shinobi", + "releases": [ + {"id": "000400000008BC00"} + ], + "title": "Shinobi" + }, + { + "compatibility": 99, + "directory": "shoot-the-ball", + "releases": [ + {"id": "000400000F708800"} + ], + "title": "SHOOT THE BALL" + }, + { + "compatibility": 1, + "directory": "shovel-knight-treasure-trove", + "releases": [ + {"id": "0004000000119A00"} + ], + "title": "Shovel Knight: Treasure Trove" + }, + { + "compatibility": 99, + "directory": "side-pocket", + "releases": [ + {"id": "0004000000050D00"} + ], + "title": "Side Pocket" + }, + { + "compatibility": 99, + "directory": "siesta-fiesta", + "releases": [ + {"id": "0004000000139300"} + ], + "title": "Siesta Fiesta" + }, + { + "compatibility": 99, + "directory": "skater-cat", + "releases": [ + {"id": "00040000000E6700"} + ], + "title": "Skater Cat" + }, + { + "compatibility": 99, + "directory": "sky-kid", + "releases": [ + {"id": "00040000000CAC00"} + ], + "title": "Sky Kid" + }, + { + "compatibility": 99, + "directory": "skylanders-giants", + "releases": [ + {"id": "0004000000091D00"} + ], + "title": "Skylanders Giants" + }, + { + "compatibility": 3, + "directory": "skylanders-spyros-adventure", + "releases": [ + {"id": "0004000000036E00"} + ], + "title": "Skylanders Spyro's Adventure" + }, + { + "compatibility": 5, + "directory": "skylanders-superchargers-racing", + "releases": [ + {"id": "0004000000165E00"} + ], + "title": "Skylanders SuperChargers Racing" + }, + { + "compatibility": 1, + "directory": "skylanders-swap-force", + "releases": [ + {"id": "00040000000E6500"} + ], + "title": "Skylanders SWAP Force" + }, + { + "compatibility": 99, + "directory": "skylanders-trap-team", + "releases": [ + {"id": "0004000000131200"} + ], + "title": "Skylanders Trap Team" + }, + { + "compatibility": 0, + "directory": "skypeace", + "releases": [ + {"id": "000400000013CC00"} + ], + "title": "SKYPEACE" + }, + { + "compatibility": 0, + "directory": "slice-it", + "releases": [ + {"id": "0004000000131400"} + ], + "title": "Slice It!" + }, + { + "compatibility": 99, + "directory": "slitherlink-by-nikoli", + "releases": [ + {"id": "000400000008D000"} + ], + "title": "Slitherlink by Nikoli" + }, + { + "compatibility": 99, + "directory": "smash-bowling-3d", + "releases": [ + {"id": "00040000000EC800"} + ], + "title": "Smash Bowling 3D" + }, + { + "compatibility": 99, + "directory": "smash-cat-heroes", + "releases": [ + {"id": "0004000000128500"} + ], + "title": "Smash Cat Heroes" + }, + { + "compatibility": 0, + "directory": "smash-controller", + "releases": [ + {"id": "000400000014C500"} + ], + "title": "Smash Controller" + }, + { + "compatibility": 1, + "directory": "smilebasic", + "releases": [ + {"id": "000400000016DE00"} + ], + "title": "SmileBASIC" + }, + { + "compatibility": 99, + "directory": "snow-moto-racing-3d", + "releases": [ + {"id": "0004000000097400"} + ], + "title": "Snow Moto Racing 3D" + }, + { + "compatibility": 99, + "directory": "soccer-up-3d", + "releases": [ + {"id": "0004000000087E00"} + ], + "title": "Soccer Up 3D" + }, + { + "compatibility": 99, + "directory": "soccer-up-online", + "releases": [ + {"id": "0004000000126F00"} + ], + "title": "Soccer Up Online" + }, + { + "compatibility": 99, + "directory": "solomons-key", + "releases": [ + {"id": "00040000000A6800"} + ], + "title": "Solomon's Key" + }, + { + "compatibility": 3, + "directory": "sonic-and-all-stars-racing-transformed", + "releases": [ + {"id": "00040000000B3500"} + ], + "title": "Sonic & All-Stars Racing Transformed" + }, + { + "compatibility": 99, + "directory": "sonic-blast", + "releases": [ + {"id": "0004000000096A00"} + ], + "title": "Sonic Blast" + }, + { + "compatibility": 2, + "directory": "sonic-boom-fire-and-ice", + "releases": [ + {"id": "0004000000161300"} + ], + "title": "Sonic Boom: Fire & Ice" + }, + { + "compatibility": 2, + "directory": "sonic-boom-shattered-crystal", + "releases": [ + {"id": "0004000000127500"} + ], + "title": "Sonic Boom: Shattered Crystal" + }, + { + "compatibility": 99, + "directory": "sonic-drift-2", + "releases": [ + {"id": "000400000008BD00"} + ], + "title": "Sonic Drift 2" + }, + { + "compatibility": 0, + "directory": "sonic-generations", + "releases": [ + {"id": "0004000000062300"} + ], + "title": "Sonic Generations" + }, + { + "compatibility": 99, + "directory": "sonic-labyrinth", + "releases": [ + {"id": "0004000000096800"} + ], + "title": "Sonic Labyrinth" + }, + { + "compatibility": 0, + "directory": "sonic-lost-world", + "releases": [ + {"id": "00040000000C8C00"} + ], + "title": "Sonic Lost World" + }, + { + "compatibility": 0, + "directory": "sonic-the-hedgehog", + "releases": [ + {"id": "00040000000C7B00"} + ], + "title": "Sonic the Hedgehog" + }, + { + "compatibility": 99, + "directory": "sonic-the-hedgehog-2", + "releases": [ + {"id": "00040000000C7A00"} + ], + "title": "Sonic The Hedgehog 2" + }, + { + "compatibility": 1, + "directory": "sonic-the-hedgehog-triple-trouble", + "releases": [ + {"id": "000400000008BA00"} + ], + "title": "Sonic the Hedgehog: Triple Trouble" + }, + { + "compatibility": 99, + "directory": "space-defender", + "releases": [ + {"id": "00040000001D2700"} + ], + "title": "SPACE DEFENDER" + }, + { + "compatibility": 99, + "directory": "space-lift-danger-panic", + "releases": [ + {"id": "0004000000154300"} + ], + "title": "Space Lift Danger Panic!" + }, + { + "compatibility": 99, + "directory": "sparkle-snapshots-3d", + "releases": [ + {"id": "000400000008CD00"} + ], + "title": "Sparkle Snapshots 3D" + }, + { + "compatibility": 99, + "directory": "speedx-3d", + "releases": [ + {"id": "0004000000087A00"} + ], + "title": "SpeedX 3D" + }, + { + "compatibility": 1, + "directory": "speedx-3d-hyper-edition", + "releases": [ + {"id": "00040000000E6D00"} + ], + "title": "SpeedX 3D Hyper Edition" + }, + { + "compatibility": 99, + "directory": "spelunker", + "releases": [ + {"id": "00040000000A6500"} + ], + "title": "Spelunker" + }, + { + "compatibility": 1, + "directory": "spider-man-edge-of-time", + "releases": [ + {"id": "0004000000061A00"} + ], + "title": "Spider-Man: Edge of Time" + }, + { + "compatibility": 2, + "directory": "spirit-camera-the-cursed-memoir", + "releases": [ + {"id": "000400000007B000"} + ], + "title": "Spirit Camera: The Cursed Memoir" + }, + { + "compatibility": 99, + "directory": "splat-the-difference", + "releases": [ + {"id": "000400000019BF00"} + ], + "title": "Splat The Difference" + }, + { + "compatibility": 0, + "directory": "spongebob-squigglepants", + "releases": [ + {"id": "0004000000044500"} + ], + "title": "Spongebob Squigglepants" + }, + { + "compatibility": 99, + "directory": "spot-the-differences", + "releases": [ + {"id": "000400000010AB00"} + ], + "title": "Spot The Differences!" + }, + { + "compatibility": 99, + "directory": "spy-hunter", + "releases": [ + {"id": "000400000009C900"} + ], + "title": "Spy Hunter" + }, + { + "compatibility": 99, + "directory": "squareboy-vs-bullies-arena-edition", + "releases": [ + {"id": "00040000001C5B00"} + ], + "title": "Squareboy vs Bullies: Arena Edition" + }, + { + "compatibility": 99, + "directory": "squids-odyssey", + "releases": [ + {"id": "000400000010E900"} + ], + "title": "Squids Odyssey" + }, + { + "compatibility": 99, + "directory": "sssnakes", + "releases": [ + {"id": "0004000000183500"} + ], + "title": "Sssnakes" + }, + { + "compatibility": 99, + "directory": "stack-em-high", + "releases": [ + {"id": "000400000F70C000"} + ], + "title": "Stack 'em High" + }, + { + "compatibility": 3, + "directory": "star-fox-64-3d", + "releases": [ + {"id": "0004000000049000"} + ], + "title": "Star Fox 64 3D" + }, + { + "compatibility": 99, + "directory": "star-soldier", + "releases": [ + {"id": "00040000000A4B00"} + ], + "title": "Star Soldier" + }, + { + "compatibility": 99, + "directory": "star-wars-pinball", + "releases": [ + {"id": "00040000000E6800"} + ], + "title": "Star Wars Pinball" + }, + { + "compatibility": 0, + "directory": "steamworld-dig", + "releases": [ + {"id": "00040000000B7C00"} + ], + "title": "SteamWorld Dig" + }, + { + "compatibility": 0, + "directory": "steamworld-dig-2", + "releases": [ + {"id": "00040000001B2D00"} + ], + "title": "SteamWorld Dig 2" + }, + { + "compatibility": 0, + "directory": "steamworld-heist", + "releases": [ + {"id": "000400000012D900"} + ], + "title": "SteamWorld Heist" + }, + { + "compatibility": 1, + "directory": "steel-diver", + "releases": [ + {"id": "0004000000032400"} + ], + "title": "Steel Diver" + }, + { + "compatibility": 0, + "directory": "steel-diver-sub-wars", + "releases": [ + {"id": "00040000000D7D00"} + ], + "title": "Steel Diver: Sub Wars" + }, + { + "compatibility": 99, + "directory": "steel-empire", + "releases": [ + {"id": "000400000013CD00"} + ], + "title": "Steel Empire" + }, + { + "compatibility": 1, + "directory": "stella-glow", + "releases": [ + {"id": "0004000000173700"} + ], + "title": "Stella Glow" + }, + { + "compatibility": 99, + "directory": "stickman-super-athletics", + "releases": [ + {"id": "000400000019A400"} + ], + "title": "Stickman Super Athletics" + }, + { + "compatibility": 99, + "directory": "storm-chaser-tornado-alley", + "releases": [ + {"id": "000400000F711400"} + ], + "title": "Storm Chaser - Tornado Alley" + }, + { + "compatibility": 2, + "directory": "story-of-seasons", + "releases": [ + {"id": "0004000000142100"} + ], + "title": "STORY OF SEASONS" + }, + { + "compatibility": 1, + "directory": "story-of-seasons-trio-of-towns", + "releases": [ + {"id": "000400000019F500"} + ], + "title": "STORY OF SEASONS: Trio of Towns" + }, + { + "compatibility": 99, + "directory": "street-fighter-2010-the-final-fight", + "releases": [ + {"id": "00040000000FF300"} + ], + "title": "Street Fighter 2010: The Final Fight" + }, + { + "compatibility": 99, + "directory": "street-fighter-alpha-2", + "releases": [ + {"id": "000400000F704300"} + ], + "title": "Street Fighter Alpha 2" + }, + { + "compatibility": 99, + "directory": "street-fighter-ii-turbo-hyper-fighting", + "releases": [ + {"id": "000400000F704100"} + ], + "title": "Street Fighter II Turbo: Hyper Fighting" + }, + { + "compatibility": 3, + "directory": "streetpass-mii-plaza-ver-50", + "releases": [ + {"id": "0004000E00021800"} + ], + "title": "StreetPass Mii Plaza Ver. 5.0" + }, + { + "compatibility": 0, + "directory": "stretchmo", + "releases": [ + {"id": "0004000000163100"} + ], + "title": "Stretchmo" + }, + { + "compatibility": 99, + "directory": "strike-force-foxx", + "releases": [ + {"id": "0004000000123E00"} + ], + "title": "Strike Force Foxx" + }, + { + "compatibility": 1, + "directory": "style-savvy-fashion-forward", + "releases": [ + {"id": "0004000000196500"} + ], + "title": "Style Savvy: Fashion Forward" + }, + { + "compatibility": 2, + "directory": "style-savvy-styling-star", + "releases": [ + {"id": "00040000001C2500"} + ], + "title": "Style Savvy: Styling Star" + }, + { + "compatibility": 2, + "directory": "style-savvy-trendsetters", + "releases": [ + {"id": "00040000000A9100"} + ], + "title": "Style Savvy: Trendsetters" + }, + { + "compatibility": 0, + "directory": "subaracity", + "releases": [ + {"id": "00040000001A8100"} + ], + "title": "SubaraCity" + }, + { + "compatibility": 1, + "directory": "sudoku-by-nikoli", + "releases": [ + {"id": "000400000008E000"} + ], + "title": "Sudoku by Nikoli" + }, + { + "compatibility": 99, + "directory": "sudoku-party", + "releases": [ + {"id": "00040000001B2000"} + ], + "title": "Sudoku Party" + }, + { + "compatibility": 99, + "directory": "sumico", + "releases": [ + {"id": "000400000015C200"} + ], + "title": "Sumico" + }, + { + "compatibility": 99, + "directory": "summer-carnival-92-recca", + "releases": [ + {"id": "00040000000D0100"} + ], + "title": "Summer Carnival '92 RECCA" + }, + { + "compatibility": 99, + "directory": "super-black-bass-3d", + "releases": [ + {"id": "000400000009E800"} + ], + "title": "Super Black Bass 3D" + }, + { + "compatibility": 99, + "directory": "super-c", + "releases": [ + {"id": "000400000009E100"} + ], + "title": "Super C" + }, + { + "compatibility": 99, + "directory": "super-castlevania-iv", + "releases": [ + {"id": "000400000F703500"} + ], + "title": "Super Castlevania IV" + }, + { + "compatibility": 0, + "directory": "super-destronaut-3d", + "releases": [ + {"id": "000400000F70A600"} + ], + "title": "Super Destronaut 3D" + }, + { + "compatibility": 99, + "directory": "super-dodge-ball", + "releases": [ + {"id": "00040000000BD800"} + ], + "title": "Super Dodge Ball" + }, + { + "compatibility": 99, + "directory": "super-ghoulsn-ghosts", + "releases": [ + {"id": "000400000F702A00"} + ], + "title": "Super Ghouls'n Ghosts" + }, + { + "compatibility": 99, + "directory": "super-little-acorns-3d-turbo", + "releases": [ + {"id": "00040000000DAA00"} + ], + "title": "Super Little Acorns 3D Turbo" + }, + { + "compatibility": 2, + "directory": "super-mario-3d-land", + "releases": [ + {"id": "0004000000054000"} + ], + "title": "Super Mario 3D Land" + }, + { + "compatibility": 1, + "directory": "super-mario-bros", + "releases": [ + {"id": "000400000006E500"} + ], + "title": "Super Mario Bros." + }, + { + "compatibility": 99, + "directory": "super-mario-bros-2", + "releases": [ + {"id": "00040000000A6200"} + ], + "title": "Super Mario Bros. 2" + }, + { + "compatibility": 1, + "directory": "super-mario-bros-3", + "releases": [ + {"id": "000400000006E800"} + ], + "title": "Super Mario Bros. 3" + }, + { + "compatibility": 99, + "directory": "super-mario-bros-deluxe", + "releases": [ + {"id": "00040000000FFA00"} + ], + "title": "Super Mario Bros. Deluxe" + }, + { + "compatibility": 1, + "directory": "super-mario-bros-the-lost-levels", + "releases": [ + {"id": "0004000000094E00"} + ], + "title": "Super Mario Bros.: The Lost Levels" + }, + { + "compatibility": 99, + "directory": "super-mario-kart", + "releases": [ + {"id": "000400000F701400"} + ], + "title": "Super Mario Kart" + }, + { + "compatibility": 1, + "directory": "super-mario-land", + "releases": [ + {"id": "0004000000040800"} + ], + "title": "Super Mario Land" + }, + { + "compatibility": 99, + "directory": "super-mario-land-2-6-golden-coins", + "releases": [ + {"id": "0004000000041A00"} + ], + "title": "Super Mario Land 2: 6 Golden Coins" + }, + { + "compatibility": 3, + "directory": "super-mario-maker-for-nintendo-3ds", + "releases": [ + {"id": "00040000001A0400"} + ], + "title": "Super Mario Maker for Nintendo 3DS" + }, + { + "compatibility": 2, + "directory": "super-mario-world", + "releases": [ + {"id": "000400000F700E00"} + ], + "title": "Super Mario World" + }, + { + "compatibility": 2, + "directory": "super-metroid", + "releases": [ + {"id": "000400000F701200"} + ], + "title": "Super Metroid" + }, + { + "compatibility": 1, + "directory": "super-monkey-ball-3d", + "releases": [ + {"id": "0004000000037000"} + ], + "title": "Super Monkey Ball 3D" + }, + { + "compatibility": 99, + "directory": "super-punch-out", + "releases": [ + {"id": "000400000F702400"} + ], + "title": "Super Punch-Out!!" + }, + { + "compatibility": 2, + "directory": "super-smash-bros-for-nintendo-3ds", + "releases": [ + {"id": "00040000000EDF00"} + ], + "title": "Super Smash Bros. for Nintendo 3DS" + }, + { + "compatibility": 99, + "directory": "super-street-fighter-ii-the-new-challengers", + "releases": [ + {"id": "000400000F703100"} + ], + "title": "Super Street Fighter II: The New Challengers" + }, + { + "compatibility": 2, + "directory": "super-street-fighter-iv-3d-edition", + "releases": [ + {"id": "0004000000032D00"} + ], + "title": "SUPER STREET FIGHTER IV 3D EDITION" + }, + { + "compatibility": 99, + "directory": "super-strike-beach-volleyball", + "releases": [ + {"id": "0004000000190400"} + ], + "title": "Super Strike Beach Volleyball" + }, + { + "compatibility": 1, + "directory": "sushi-striker-the-way-of-sushido", + "releases": [ + {"id": "00040000001C1C00"} + ], + "title": "Sushi Striker: The Way of Sushido" + }, + { + "compatibility": 0, + "directory": "swapdoodle", + "releases": [ + {"id": "00040000001A2D00"} + ], + "title": "Swapdoodle" + }, + { + "compatibility": 99, + "directory": "swapnote", + "releases": [ + {"id": "0004000000051700"} + ], + "title": "Swapnote" + }, + { + "compatibility": 99, + "directory": "sweet-memories-blackjack", + "releases": [ + {"id": "0004000000092700"} + ], + "title": "Sweet Memories - Blackjack" + }, + { + "compatibility": 99, + "directory": "swipe", + "releases": [ + {"id": "000400000F70C200"} + ], + "title": "SWIPE" + }, + { + "compatibility": 0, + "directory": "swords-and-darkness", + "releases": [ + {"id": "0004000000146200"} + ], + "title": "SWORDS & DARKNESS" + }, + { + "compatibility": 99, + "directory": "swords-and-soldiers-3d", + "releases": [ + {"id": "00040000000D0600"} + ], + "title": "Swords & Soldiers 3D" + }, + { + "compatibility": 0, + "directory": "symphony-of-eternity", + "releases": [ + {"id": "00040000001B8900"} + ], + "title": "Symphony of Eternity" + }, + { + "compatibility": 99, + "directory": "table-tennis-infinity", + "releases": [ + {"id": "000400000F70C400"} + ], + "title": "TABLE TENNIS INFINITY" + }, + { + "compatibility": 99, + "directory": "tails-adventure", + "releases": [ + {"id": "00040000000DE900"} + ], + "title": "Tails Adventure" + }, + { + "compatibility": 4, + "directory": "tales-of-the-abyss-3d", + "releases": [ + {"id": "0004000000061300"} + ], + "title": "Tales of the Abyss 3D" + }, + { + "compatibility": 99, + "directory": "talking-phrasebook-7-languages", + "releases": [ + {"id": "00040000000FE200"} + ], + "title": "Talking Phrasebook - 7 Languages" + }, + { + "compatibility": 99, + "directory": "tangram-attack", + "releases": [ + {"id": "0004000000146800"} + ], + "title": "Tangram Attack" + }, + { + "compatibility": 99, + "directory": "tangram-style", + "releases": [ + {"id": "00040000000EBB00"} + ], + "title": "Tangram Style" + }, + { + "compatibility": 99, + "directory": "tank-troopers", + "releases": [ + {"id": "000400000017CC00"} + ], + "title": "Tank Troopers" + }, + { + "compatibility": 0, + "directory": "tappingo", + "releases": [ + {"id": "0004000000119800"} + ], + "title": "Tappingo" + }, + { + "compatibility": 1, + "directory": "team-kirby-clash-deluxe", + "releases": [ + {"id": "00040000001AB800"} + ], + "title": "Team Kirby Clash Deluxe" + }, + { + "compatibility": 99, + "directory": "tecmo-bowl", + "releases": [ + {"id": "000400000009E400"} + ], + "title": "Tecmo Bowl" + }, + { + "compatibility": 4, + "directory": "teenage-mutant-ninja-turtles-master-splinters-training-pack", + "releases": [ + {"id": "0004000000170B00"} + ], + "title": "Teenage Mutant Ninja Turtles: Master Splinter's Training Pack" + }, + { + "compatibility": 0, + "directory": "tekken-3d-prime-edition", + "releases": [ + {"id": "0004000000080300"} + ], + "title": "TEKKEN 3D Prime Edition" + }, + { + "compatibility": 99, + "directory": "tennis", + "releases": [ + {"id": "0004000000041100"} + ], + "title": "Tennis" + }, + { + "compatibility": 2, + "directory": "terraria", + "releases": [ + {"id": "000400000016A900"} + ], + "title": "Terraria" + }, + { + "compatibility": 2, + "directory": "the-alliance-alive", + "releases": [ + {"id": "00040000001CCD00"} + ], + "title": "The Alliance Alive" + }, + { + "compatibility": 2, + "directory": "the-battle-cats-pop", + "releases": [ + {"id": "000400000018BC00"} + ], + "title": "The Battle Cats POP!" + }, + { + "compatibility": 2, + "directory": "the-binding-of-isaac-rebirth", + "releases": [ + {"id": "000400000F700800"} + ], + "title": "The Binding of Isaac: Rebirth" + }, + { + "compatibility": 99, + "directory": "the-delusions-of-von-sottendorff", + "releases": [ + {"id": "00040000000E5900"} + ], + "title": "The delusions of Von Sottendorff" + }, + { + "compatibility": 3, + "directory": "the-denpa-men-2-beyond-the-waves", + "releases": [ + {"id": "00040000000C3300"} + ], + "title": "THE \"DENPA\" MEN 2: Beyond the Waves" + }, + { + "compatibility": 2, + "directory": "the-denpa-men-3-the-rise-of-digitoll", + "releases": [ + {"id": "000400000011DE00"} + ], + "title": "THE \"DENPA\" MEN 3 The Rise of Digitoll" + }, + { + "compatibility": 2, + "directory": "the-denpa-men-they-came-by-wave", + "releases": [ + {"id": "00040000000A3100"} + ], + "title": "THE \"DENPA\" MEN They Came By Wave" + }, + { + "compatibility": 99, + "directory": "the-hand-of-panda", + "releases": [ + {"id": "0004000000150700"} + ], + "title": "The Hand of Panda" + }, + { + "compatibility": 99, + "directory": "the-hidden", + "releases": [ + {"id": "0004000000037300"} + ], + "title": "The Hidden" + }, + { + "compatibility": 1, + "directory": "the-keep", + "releases": [ + {"id": "0004000000124000"} + ], + "title": "The Keep" + }, + { + "compatibility": 0, + "directory": "the-legend-of-dark-witch", + "releases": [ + {"id": "000400000014EE00"} + ], + "title": "The Legend of Dark Witch" + }, + { + "compatibility": 1, + "directory": "the-legend-of-dark-witch-2", + "releases": [ + {"id": "0004000000186600"} + ], + "title": "The Legend of Dark Witch 2" + }, + { + "compatibility": 1, + "directory": "the-legend-of-dark-witch-3-wisdom-and-lunacy", + "releases": [ + {"id": "00040000001CD600"} + ], + "title": "The Legend of Dark Witch 3 Wisdom and Lunacy" + }, + { + "compatibility": 0, + "directory": "the-legend-of-kusakari", + "releases": [ + {"id": "000400000018B800"} + ], + "title": "The Legend of Kusakari" + }, + { + "compatibility": 2, + "directory": "the-legend-of-legacy", + "releases": [ + {"id": "000400000016DB00"} + ], + "title": "The Legend of Legacy" + }, + { + "compatibility": 99, + "directory": "the-legend-of-the-mystical-ninja", + "releases": [ + {"id": "000400000F703700"} + ], + "title": "The Legend of The Mystical Ninja" + }, + { + "compatibility": 1, + "directory": "the-legend-of-zelda", + "releases": [ + {"id": "000400000006F100"} + ], + "title": "The Legend of Zelda" + }, + { + "compatibility": 1, + "directory": "the-legend-of-zelda-a-link-between-worlds", + "releases": [ + {"id": "00040000000EC300"} + ], + "title": "The Legend of Zelda: A Link Between Worlds" + }, + { + "compatibility": 99, + "directory": "the-legend-of-zelda-a-link-between-worlds-trailer", + "releases": [ + {"id": "00040000000EC600"} + ], + "title": "The Legend of Zelda: A Link Between Worlds Trailer" + }, + { + "compatibility": 1, + "directory": "the-legend-of-zelda-a-link-to-the-past", + "releases": [ + {"id": "000400000F701000"} + ], + "title": "The Legend of Zelda: A Link to the Past" + }, + { + "compatibility": 99, + "directory": "the-legend-of-zelda-ballad-of-the-goddess", + "releases": [ + {"id": "00040000001B5800"} + ], + "title": "The Legend of Zelda Ballad of The Goddess" + }, + { + "compatibility": 99, + "directory": "the-legend-of-zelda-great-fairys-fountain-theme", + "releases": [ + {"id": "00040000001B5A00"} + ], + "title": "The Legend of Zelda Great Fairy's Fountain Theme" + }, + { + "compatibility": 2, + "directory": "the-legend-of-zelda-links-awakening-dx", + "releases": [ + {"id": "0004000000042700"} + ], + "title": "The Legend of Zelda: Link's Awakening DX" + }, + { + "compatibility": 0, + "directory": "the-legend-of-zelda-main-theme-medley", + "releases": [ + {"id": "00040000001B5900"} + ], + "title": "The Legend of Zelda Main Theme Medley" + }, + { + "compatibility": 2, + "directory": "the-legend-of-zelda-majoras-mask-3d", + "releases": [ + {"id": "0004000000125500"} + ], + "title": "The Legend of Zelda: Majora’s Mask 3D" + }, + { + "compatibility": 2, + "directory": "the-legend-of-zelda-ocarina-of-time-3d", + "releases": [ + {"id": "0004000000033500"} + ], + "title": "The Legend of Zelda: Ocarina of Time 3D" + }, + { + "compatibility": 1, + "directory": "the-legend-of-zelda-oracle-of-ages", + "releases": [ + {"id": "0004000000058F00"} + ], + "title": "The Legend of Zelda: Oracle of Ages" + }, + { + "compatibility": 1, + "directory": "the-legend-of-zelda-oracle-of-seasons", + "releases": [ + {"id": "0004000000058C00"} + ], + "title": "The Legend of Zelda: Oracle of Seasons" + }, + { + "compatibility": 2, + "directory": "the-legend-of-zelda-tri-force-heroes", + "releases": [ + {"id": "0004000000176F00"} + ], + "title": "The Legend of Zelda: Tri Force Heroes" + }, + { + "compatibility": 99, + "directory": "the-legend-of-zelda-tri-force-heroes-demo", + "releases": [ + {"id": "0004000000182200"} + ], + "title": "The Legend of Zelda: Tri Force Heroes Demo" + }, + { + "compatibility": 2, + "directory": "the-lego-movie-videogame", + "releases": [ + {"id": "0004000000105A00"} + ], + "title": "The LEGO Movie Videogame" + }, + { + "compatibility": 99, + "directory": "the-magic-hammer", + "releases": [ + {"id": "000400000016F300"} + ], + "title": "The Magic Hammer" + }, + { + "compatibility": 99, + "directory": "the-mysterious-murasame-castle", + "releases": [ + {"id": "00040000000D3200"} + ], + "title": "The Mysterious Murasame Castle" + }, + { + "compatibility": 2, + "directory": "the-sims-3", + "releases": [ + {"id": "0004000000036500"} + ], + "title": "The Sims 3" + }, + { + "compatibility": 99, + "directory": "the-starship-damrey", + "releases": [ + {"id": "00040000000D9900"} + ], + "title": "THE STARSHIP DAMREY" + }, + { + "compatibility": 3, + "directory": "the-sword-of-hope-ii", + "releases": [ + {"id": "0004000000083100"} + ], + "title": "The Sword of Hope II" + }, + { + "compatibility": 99, + "directory": "the-trash-pack", + "releases": [ + {"id": "00040000000B0600"} + ], + "title": "The Trash Pack" + }, + { + "compatibility": 0, + "directory": "theatrhythm-final-fantasy", + "releases": [ + {"id": "0004000000095100"} + ], + "title": "THEATRHYTHM FINAL FANTASY" + }, + { + "compatibility": 1, + "directory": "theatrhythm-final-fantasy-curtain-call", + "releases": [ + {"id": "00040000000FD500"} + ], + "title": "THEATRHYTHM FINAL FANTASY CURTAIN CALL" + }, + { + "compatibility": 1, + "directory": "thor-god-of-thunder", + "releases": [ + {"id": "0004000000040500"} + ], + "title": "Thor: God of Thunder" + }, + { + "compatibility": 2, + "directory": "thorium-wars-attack-of-the-skyfighter", + "releases": [ + {"id": "0004000000123F00"} + ], + "title": "Thorium Wars: Attack of the Skyfighter" + }, + { + "compatibility": 99, + "directory": "tiny-games-knights-and-dragons", + "releases": [ + {"id": "0004000000125300"} + ], + "title": "Tiny Games - Knights & Dragons" + }, + { + "compatibility": 0, + "directory": "titan-attacks", + "releases": [ + {"id": "0004000000158600"} + ], + "title": "Titan Attacks!" + }, + { + "compatibility": 99, + "directory": "toki-tori", + "releases": [ + {"id": "0004000000094C00"} + ], + "title": "Toki Tori" + }, + { + "compatibility": 99, + "directory": "toki-tori-3d", + "releases": [ + {"id": "0004000000182000"} + ], + "title": "Toki Tori 3D" + }, + { + "compatibility": 1, + "directory": "tokyo-crash-mobs", + "releases": [ + {"id": "00040000000BCB00"} + ], + "title": "Tokyo Crash Mobs" + }, + { + "compatibility": 1, + "directory": "tom-clancys-ghost-recon-shadow-wars", + "releases": [ + {"id": "0004000000035A00"} + ], + "title": "Tom Clancy’s Ghost Recon Shadow Wars" + }, + { + "compatibility": 4, + "directory": "tom-clancys-splinter-cell-3d", + "releases": [ + {"id": "0004000000040100"} + ], + "title": "Tom Clancy's Splinter Cell 3D" + }, + { + "compatibility": 2, + "directory": "tomodachi-life", + "releases": [ + {"id": "000400000008C300"}, + {"id": "000400000008C400"} + ], + "title": "Tomodachi Life" + }, + { + "compatibility": 99, + "directory": "top-model-3d", + "releases": [ + {"id": "0004000000148700"} + ], + "title": "Top Model 3D" + }, + { + "compatibility": 99, + "directory": "touch-battle-tank-tag-combat-", + "releases": [ + {"id": "00040000001ADC00"} + ], + "title": "Touch Battle Tank - Tag Combat -" + }, + { + "compatibility": 99, + "directory": "toy-defense", + "releases": [ + {"id": "00040000000FE600"} + ], + "title": "Toy Defense" + }, + { + "compatibility": 99, + "directory": "toy-stunt-bike", + "releases": [ + {"id": "0004000000125400"} + ], + "title": "Toy Stunt Bike" + }, + { + "compatibility": 99, + "directory": "toys-vs-monsters", + "releases": [ + {"id": "0004000000150800"} + ], + "title": "TOYS VS MONSTERS" + }, + { + "compatibility": 99, + "directory": "travel-adventures-with-hello-kitty", + "releases": [ + {"id": "00040000000B0500"} + ], + "title": "Travel Adventures with Hello Kitty" + }, + { + "compatibility": 99, + "directory": "triple-breakout", + "releases": [ + {"id": "000400000F70BE00"} + ], + "title": "Triple Breakout" + }, + { + "compatibility": 99, + "directory": "triple-breakout", + "releases": [ + {"id": "00040000001D7400"} + ], + "title": "Triple Breakout" + }, + { + "compatibility": 99, + "directory": "tumble-pop", + "releases": [ + {"id": "0004000000082E00"} + ], + "title": "Tumble Pop" + }, + { + "compatibility": 2, + "directory": "ultimate-nes-remix", + "releases": [ + {"id": "0004000000132000"} + ], + "title": "Ultimate NES Remix" + }, + { + "compatibility": 1, + "directory": "unchained-blades", + "releases": [ + {"id": "000400000009EB00"} + ], + "title": "Unchained Blades" + }, + { + "compatibility": 99, + "directory": "undead-bowling", + "releases": [ + {"id": "00040000000EBA00"} + ], + "title": "Undead Bowling" + }, + { + "compatibility": 99, + "directory": "undead-storm-nightmare", + "releases": [ + {"id": "0004000000124800"} + ], + "title": "Undead Storm Nightmare" + }, + { + "compatibility": 99, + "directory": "unlucky-mage", + "releases": [ + {"id": "00040000001A9700"} + ], + "title": "Unlucky Mage" + }, + { + "compatibility": 99, + "directory": "up-up-bot", + "releases": [ + {"id": "000400000F711000"} + ], + "title": "UP UP BOT" + }, + { + "compatibility": 99, + "directory": "urban-trial-freestyle", + "releases": [ + {"id": "00040000000C7400"} + ], + "title": "URBAN TRIAL FREESTYLE" + }, + { + "compatibility": 3, + "directory": "urban-trial-freestyle-2", + "releases": [ + {"id": "00040000001A4500"} + ], + "title": "URBAN TRIAL FREESTYLE 2" + }, + { + "compatibility": 99, + "directory": "vampire-master-of-darkness", + "releases": [ + {"id": "000400000009C600"} + ], + "title": "Vampire Master of Darkness" + }, + { + "compatibility": 99, + "directory": "van-helsing-sniper-zx100", + "releases": [ + {"id": "000400000010C500"} + ], + "title": "Van Helsing sniper Zx100" + }, + { + "compatibility": 99, + "directory": "vectorracing", + "releases": [ + {"id": "000400000008B800"} + ], + "title": "VectorRacing" + }, + { + "compatibility": 99, + "directory": "viking-invasion-2-tower-defense", + "releases": [ + {"id": "0004000000090200"} + ], + "title": "Viking Invasion 2 - Tower Defense" + }, + { + "compatibility": 99, + "directory": "voxelmaker", + "releases": [ + {"id": "00040000001C6E00"} + ], + "title": "VoxelMaker" + }, + { + "compatibility": 0, + "directory": "vvvvvv", + "releases": [ + {"id": "000400000007FD00"} + ], + "title": "VVVVVV" + }, + { + "compatibility": 99, + "directory": "wakedas", + "releases": [ + {"id": "00040000000E6900"} + ], + "title": "WAKEDAS" + }, + { + "compatibility": 99, + "directory": "waku-waku-sweets-happy-sweets-making", + "releases": [ + {"id": "00040000001D0100"} + ], + "title": "WAKU WAKU SWEETS: Happy Sweets Making" + }, + { + "compatibility": 99, + "directory": "wario-land-3", + "releases": [ + {"id": "000400000008AE00"} + ], + "title": "Wario Land 3" + }, + { + "compatibility": 2, + "directory": "wario-land-ii", + "releases": [ + {"id": "000400000007D800"} + ], + "title": "Wario Land II" + }, + { + "compatibility": 2, + "directory": "wario-land-super-mario-land-3", + "releases": [ + {"id": "0004000000057C00"} + ], + "title": "Wario Land: Super Mario Land 3" + }, + { + "compatibility": 0, + "directory": "warios-woods", + "releases": [ + {"id": "0004000000071500"} + ], + "title": "Wario's Woods" + }, + { + "compatibility": 1, + "directory": "warioware-gold", + "releases": [ + {"id": "00040000001D1C00"} + ], + "title": "WarioWare Gold" + }, + { + "compatibility": 99, + "directory": "warioware-gold-special-demo", + "releases": [ + {"id": "00040000001D4500"} + ], + "title": "WarioWare Gold Special Demo" + }, + { + "compatibility": 1, + "directory": "weapon-shop-de-omasse", + "releases": [ + {"id": "0004000000115100"} + ], + "title": "WEAPON SHOP de OMASSE" + }, + { + "compatibility": 99, + "directory": "wild-adventures-ultimate-deer-hunt-3d", + "releases": [ + {"id": "00040000000C1500"} + ], + "title": "Wild Adventures: Ultimate Deer Hunt 3D" + }, + { + "compatibility": 99, + "directory": "winter-sports-feel-the-spirit", + "releases": [ + {"id": "0004000000117B00"} + ], + "title": "Winter Sports - Feel the Spirit" + }, + { + "compatibility": 0, + "directory": "witch-and-hero", + "releases": [ + {"id": "00040000000D0700"} + ], + "title": "Witch & Hero" + }, + { + "compatibility": 99, + "directory": "witch-and-hero-2", + "releases": [ + {"id": "000400000016D600"} + ], + "title": "Witch & Hero 2" + }, + { + "compatibility": 99, + "directory": "witch-and-hero-3", + "releases": [ + {"id": "00040000001D1100"} + ], + "title": "Witch & Hero 3" + }, + { + "compatibility": 99, + "directory": "wizdom", + "releases": [ + {"id": "000400000012E800"} + ], + "title": "Wizdom" + }, + { + "compatibility": 1, + "directory": "woah-dave", + "releases": [ + {"id": "000400000013EE00"} + ], + "title": "Woah Dave!" + }, + { + "compatibility": 99, + "directory": "worcle-worlds", + "releases": [ + {"id": "0004000000187400"} + ], + "title": "Worcle Worlds" + }, + { + "compatibility": 99, + "directory": "word-logic-by-powgi", + "releases": [ + {"id": "00040000001A0900"} + ], + "title": "Word Logic by POWGI" + }, + { + "compatibility": 99, + "directory": "word-puzzles-by-powgi", + "releases": [ + {"id": "0004000000175B00"} + ], + "title": "Word Puzzles by POWGI" + }, + { + "compatibility": 99, + "directory": "word-search-10k", + "releases": [ + {"id": "00040000001B2200"} + ], + "title": "Word Search 10K" + }, + { + "compatibility": 99, + "directory": "word-search-by-powgi", + "releases": [ + {"id": "0004000000164200"} + ], + "title": "Word Search by POWGI" + }, + { + "compatibility": 99, + "directory": "word-wizard-3d", + "releases": [ + {"id": "0004000000107900"} + ], + "title": "Word Wizard 3D" + }, + { + "compatibility": 99, + "directory": "wordsup-academy", + "releases": [ + {"id": "0004000000185F00"} + ], + "title": "WordsUp! Academy" + }, + { + "compatibility": 99, + "directory": "world-conqueror-3d", + "releases": [ + {"id": "00040000000FE700"} + ], + "title": "World Conqueror 3D" + }, + { + "compatibility": 99, + "directory": "wrc-official-game-of-the-fia-world-rally-championship", + "releases": [ + {"id": "0004000000149600"} + ], + "title": "WRC Official Game of the FIA World Rally Championship" + }, + { + "compatibility": 1, + "directory": "wreck-it-ralph", + "releases": [ + {"id": "0004000000097100"} + ], + "title": "Wreck-it Ralph" + }, + { + "compatibility": 99, + "directory": "wrecking-crew", + "releases": [ + {"id": "000400000006F700"} + ], + "title": "Wrecking Crew" + }, + { + "compatibility": 1, + "directory": "xenoblade-chronicles-3d", + "releases": [ + {"id": "000400000F700100"} + ], + "title": "Xenoblade Chronicles 3D" + }, + { + "compatibility": 1, + "directory": "xeodrifter", + "releases": [ + {"id": "000400000014D600"} + ], + "title": "Xeodrifter" + }, + { + "compatibility": 99, + "directory": "xtreme-sports", + "releases": [ + {"id": "00040000000F0200"} + ], + "title": "Xtreme Sports" + }, + { + "compatibility": 2, + "directory": "yo-kai-watch", + "releases": [ + {"id": "0004000000167700"} + ], + "title": "YO-KAI WATCH" + }, + { + "compatibility": 1, + "directory": "yo-kai-watch-2-bony-spirits", + "releases": [ + {"id": "000400000019A900"} + ], + "title": "YO-KAI WATCH 2: Bony Spirits" + }, + { + "compatibility": 0, + "directory": "yo-kai-watch-2-fleshy-souls", + "releases": [ + {"id": "000400000019AA00"} + ], + "title": "YO-KAI WATCH 2: Fleshy Souls" + }, + { + "compatibility": 1, + "directory": "yo-kai-watch-2-psychic-specters", + "releases": [ + {"id": "00040000001B2700"} + ], + "title": "YO-KAI WATCH 2: Psychic Specters" + }, + { + "compatibility": 2, + "directory": "yo-kai-watch-3", + "releases": [ + {"id": "00040000001D6700"} + ], + "title": "YO-KAI WATCH 3" + }, + { + "compatibility": 0, + "directory": "yo-kai-watch-blasters-red-cat-corps", + "releases": [ + {"id": "00040000001CEB00"} + ], + "title": "YO-KAI WATCH BLASTERS: Red Cat Corps" + }, + { + "compatibility": 0, + "directory": "yo-kai-watch-blasters-white-dog-squad", + "releases": [ + {"id": "00040000001CEF00"} + ], + "title": "YO-KAI WATCH BLASTERS: White Dog Squad" + }, + { + "compatibility": 99, + "directory": "yoshi", + "releases": [ + {"id": "0004000000071200"} + ], + "title": "Yoshi" + }, + { + "compatibility": 2, + "directory": "yoshis-new-island", + "releases": [ + {"id": "0004000000111B00"} + ], + "title": "Yoshi's New Island" + }, + { + "compatibility": 4, + "directory": "youtube", + "releases": [ + {"id": "00040000000B0F00"} + ], + "title": "YouTube" + }, + { + "compatibility": 99, + "directory": "yu-gi-oh-zexal-world-duel-carnival", + "releases": [ + {"id": "0004000000136100"} + ], + "title": "Yu-Gi-Oh! ZEXAL World Duel Carnival" + }, + { + "compatibility": 0, + "directory": "yumis-odd-odyssey", + "releases": [ + {"id": "0004000000112D00"} + ], + "title": "Yumi's Odd Odyssey" + }, + { + "compatibility": 2, + "directory": "zelda-ii-the-adventure-of-link", + "releases": [ + {"id": "0004000000070900"} + ], + "title": "Zelda II - The Adventure of Link" + }, + { + "compatibility": 0, + "directory": "zen-pinball-3d", + "releases": [ + {"id": "000400000007FC00"} + ], + "title": "Zen Pinball 3D" + }, + { + "compatibility": 2, + "directory": "zero-escape-virtues-last-reward", + "releases": [ + {"id": "0004000000096700"} + ], + "title": "Zero Escape: Virtue's Last Reward" + }, + { + "compatibility": 1, + "directory": "zero-escape-zero-time-dilemma", + "releases": [ + {"id": "000400000017B200"} + ], + "title": "Zero Escape: Zero Time Dilemma" + }, + { + "compatibility": 99, + "directory": "zeus-quest-remastered", + "releases": [ + {"id": "000400000F70CA00"} + ], + "title": "Zeus Quest Remastered" + }, + { + "compatibility": 99, + "directory": "zig-zag-go", + "releases": [ + {"id": "000400000F710500"} + ], + "title": "ZIG ZAG GO" + }, + { + "compatibility": 99, + "directory": "zombie-incident", + "releases": [ + {"id": "000400000012E900"} + ], + "title": "Zombie Incident" + }, + { + "compatibility": 99, + "directory": "zombie-panic-in-wonderland-dx", + "releases": [ + {"id": "00040000000F5600"} + ], + "title": "Zombie Panic in Wonderland DX" + }, + { + "compatibility": 99, + "directory": "zombie-slayer-diox", + "releases": [ + {"id": "000400000007FF00"} + ], + "title": "Zombie Slayer Diox" + }, + { + "compatibility": 99, + "directory": "zumas-revenge", + "releases": [ + {"id": "000480044B5A5445"} + ], + "title": "Zuma's Revenge" + } +] \ No newline at end of file diff --git a/dist/doc-icon.png b/dist/doc-icon.png index 9b577321..48113c0d 100644 Binary files a/dist/doc-icon.png and b/dist/doc-icon.png differ diff --git a/dist/icon.png b/dist/icon.png index fd102724..cc93e355 100644 Binary files a/dist/icon.png and b/dist/icon.png differ diff --git a/dist/citra-qt.6 b/dist/lucina3ds-qt.6 similarity index 78% rename from dist/citra-qt.6 rename to dist/lucina3ds-qt.6 index d09e96b5..bc6ac204 100644 --- a/dist/citra-qt.6 +++ b/dist/lucina3ds-qt.6 @@ -1,22 +1,22 @@ .Dd November 22 2016 -.Dt citra-qt 6 +.Dt lucina3ds-qt 6 .Os .Sh NAME -.Nm Citra-Qt +.Nm Lucina3DS-Qt .Nd Nintendo 3DS Emulator/Debugger (Qt) .Sh SYNOPSIS -.Nm citra-qt +.Nm Lucina3DS-qt .Op Ar file .Sh DESCRIPTION -Citra is an experimental open-source Nintendo 3DS emulator/debugger. +Lucina3DS is an experimental open-source Nintendo 3DS emulator/debugger. .Pp -.Nm citra-qt +.Nm lucina3ds-qt is the Qt implementation. .Sh FILES .Bl -tag -width Ds -.It Pa $XDG_DATA_HOME/citra-emu +.It Pa $XDG_DATA_HOME/lucina3ds Emulator storage. -.It Pa $XDG_CONFIG_HOME/citra-emu +.It Pa $XDG_CONFIG_HOME/lucina3ds Configuration files. .El .Sh AUTHORS diff --git a/dist/citra-qt.desktop b/dist/lucina3ds-qt.desktop similarity index 84% rename from dist/citra-qt.desktop rename to dist/lucina3ds-qt.desktop index cc4ec41a..b10c0ea0 100644 --- a/dist/citra-qt.desktop +++ b/dist/lucina3ds-qt.desktop @@ -1,14 +1,14 @@ [Desktop Entry] Version=1.0 Type=Application -Name=Citra +Name=Lucina3DS GenericName=3DS Emulator GenericName[fr]=Émulateur 3DS Comment=Nintendo 3DS video game console emulator Comment[fr]=Émulateur de console de jeu Nintendo 3DS -Icon=citra -TryExec=citra-qt -Exec=citra-qt %f +Icon=lucina3ds +TryExec=lucina3ds-qt +Exec=lucina3ds-qt %f Categories=Game;Emulator;Qt; MimeType=application/x-ctr-3dsx;application/x-ctr-cci;application/x-ctr-cia;application/x-ctr-cxi; Keywords=3DS;Nintendo; diff --git a/dist/lucina3ds-room.desktop b/dist/lucina3ds-room.desktop new file mode 100644 index 00000000..81dbcc02 --- /dev/null +++ b/dist/lucina3ds-room.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Lucina3DS Room +Comment=Multiplayer room host for Lucina3DS +Icon=lucina3ds +TryExec=lucina3ds-room +Exec=lucina3ds-room %f +Categories=Game;Emulator; +Keywords=3DS;Nintendo diff --git a/dist/citra.6 b/dist/lucina3ds.6 similarity index 92% rename from dist/citra.6 rename to dist/lucina3ds.6 index 4807b087..b30e63b7 100644 --- a/dist/citra.6 +++ b/dist/lucina3ds.6 @@ -1,11 +1,11 @@ .Dd September 13 2024 -.Dt citra 6 +.Dt lucina3ds 6 .Os .Sh NAME -.Nm Citra +.Nm Lucina3DS .Nd Nintendo 3DS Emulator/Debugger (SDL) .Sh SYNOPSIS -.Nm citra +.Nm lucina3ds .Op Ar options .Op Ar file .Sh OPTIONS @@ -31,9 +31,9 @@ Shows syntax help and exits .It Fl v , Fl Fl version Describes the installed version and exits .Sh DESCRIPTION -Citra is an experimental open-source Nintendo 3DS emulator/debugger. +Lucina3DS is an experimental open-source Nintendo 3DS emulator/debugger. .Pp -.Nm citra +.Nm lucina3ds is the Simple DirectMedia Layer (SDL) implementation. .Sh FILES .Bl -tag -width Ds diff --git a/dist/citra.desktop b/dist/lucina3ds.desktop similarity index 84% rename from dist/citra.desktop rename to dist/lucina3ds.desktop index 24468cd1..bffd2c9a 100644 --- a/dist/citra.desktop +++ b/dist/lucina3ds.desktop @@ -1,14 +1,14 @@ [Desktop Entry] Version=1.0 Type=Application -Name=Citra +Name=Lucina3DS GenericName=3DS Emulator GenericName[fr]=Émulateur 3DS Comment=Nintendo 3DS video game console emulator Comment[fr]=Émulateur de console de jeu Nintendo 3DS -Icon=citra -TryExec=citra -Exec=citra %f +Icon=lucina3ds +TryExec=lucina3ds-qt +Exec=lucina3ds-qt %f Categories=Game;Emulator; MimeType=application/x-ctr-3dsx;application/x-ctr-cci;application/x-ctr-cia;application/x-ctr-cxi; Keywords=3DS;Nintendo; diff --git a/dist/lucina3ds.ico b/dist/lucina3ds.ico new file mode 100644 index 00000000..80c570f3 Binary files /dev/null and b/dist/lucina3ds.ico differ diff --git a/dist/citra.manifest b/dist/lucina3ds.manifest similarity index 100% rename from dist/citra.manifest rename to dist/lucina3ds.manifest diff --git a/dist/lucina3ds.svg b/dist/lucina3ds.svg new file mode 100644 index 00000000..2428fe59 --- /dev/null +++ b/dist/lucina3ds.svg @@ -0,0 +1,131 @@ + + + + diff --git a/dist/citra.xml b/dist/lucina3ds.xml similarity index 94% rename from dist/citra.xml rename to dist/lucina3ds.xml index 6d47c876..39bce9b8 100644 --- a/dist/citra.xml +++ b/dist/lucina3ds.xml @@ -4,7 +4,7 @@ Nintendo 3DS homebrew executable Exécutable non-officiel pour Nintendo 3DS  3DSX - + @@ -14,7 +14,7 @@ Image de cartouche Nintendo 3DS CCI CTR Cart Image - + @@ -25,7 +25,7 @@ Exécutable Nintendo 3DS CXI CTR eXecutable Image - + @@ -35,7 +35,7 @@ Archive installable Nintendo 3DS CIA CTR Importable Archive - + diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index 6da47531..a3263a49 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -13,7 +13,7 @@ icons/48x48/no_avatar.png icons/48x48/plus.png icons/48x48/sd_card.png - icons/256x256/citra.png + icons/256x256/lucina3ds.png icons/256x256/plus_folder.png diff --git a/dist/qt_themes/default/icons/256x256/citra.png b/dist/qt_themes/default/icons/256x256/citra.png deleted file mode 100644 index dbbfa747..00000000 Binary files a/dist/qt_themes/default/icons/256x256/citra.png and /dev/null differ diff --git a/dist/qt_themes/default/icons/256x256/lucina3ds.png b/dist/qt_themes/default/icons/256x256/lucina3ds.png new file mode 100644 index 00000000..bf19e4cb Binary files /dev/null and b/dist/qt_themes/default/icons/256x256/lucina3ds.png differ diff --git a/dist/scripting/citra.py b/dist/scripting/citra.py index 50766203..1b8e4b7a 100644 --- a/dist/scripting/citra.py +++ b/dist/scripting/citra.py @@ -11,10 +11,10 @@ class RequestType(enum.IntEnum): ReadMemory = 1, WriteMemory = 2 -CITRA_PORT = 45987 +LUCINA3DS_PORT = 45987 -class Citra: - def __init__(self, address="127.0.0.1", port=CITRA_PORT): +class Lucina3DS: + def __init__(self, address="127.0.0.1", port=LUCINA3DS_PORT): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.address = address @@ -45,7 +45,7 @@ class Citra: request_data = struct.pack("II", read_address, temp_read_size) request, request_id = self._generate_header(RequestType.ReadMemory, len(request_data)) request += request_data - self.socket.sendto(request, (self.address, CITRA_PORT)) + self.socket.sendto(request, (self.address, LUCINA3DS_PORT)) raw_reply = self.socket.recv(MAX_PACKET_SIZE) reply_data = self._read_and_validate_header(raw_reply, request_id, RequestType.ReadMemory) @@ -77,7 +77,7 @@ class Citra: request_data += write_contents[:temp_write_size] request, request_id = self._generate_header(RequestType.WriteMemory, len(request_data)) request += request_data - self.socket.sendto(request, (self.address, CITRA_PORT)) + self.socket.sendto(request, (self.address, LUCINA3DS_PORT)) raw_reply = self.socket.recv(MAX_PACKET_SIZE) reply_data = self._read_and_validate_header(raw_reply, request_id, RequestType.WriteMemory) @@ -92,4 +92,4 @@ class Citra: if "__main__" == __name__: import doctest - doctest.testmod(extraglobs={'c': Citra()}) + doctest.testmod(extraglobs={'c': LUCINA3DS()}) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index fc45473b..a70345ef 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -116,7 +116,7 @@ if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE) else() set(DYNARMIC_TESTS OFF CACHE BOOL "") set(DYNARMIC_FRONTENDS "A32" CACHE STRING "") - set(DYNARMIC_USE_PRECOMPILED_HEADERS ${CITRA_USE_PRECOMPILED_HEADERS} CACHE BOOL "") + set(DYNARMIC_USE_PRECOMPILED_HEADERS ${LUCINA3DS_USE_PRECOMPILED_HEADERS} CACHE BOOL "") add_subdirectory(dynarmic EXCLUDE_FROM_ALL) endif() endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dfceb401..5cfaad08 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -85,14 +85,14 @@ if (MSVC) /wd4324 # 'struct_name': structure was padded due to __declspec(align()) ) - if (CITRA_WARNINGS_AS_ERRORS) + if (LUCINA3DS_WARNINGS_AS_ERRORS) add_compile_options(/WX) endif() # Since MSVC's debugging information is not very deterministic, so we have to disable it # when using ccache or other caching tools if (CMAKE_C_COMPILER_LAUNCHER STREQUAL "ccache" OR CMAKE_CXX_COMPILER_LAUNCHER STREQUAL "ccache" - OR CITRA_USE_PRECOMPILED_HEADERS) + OR LUCINA3DS_USE_PRECOMPILED_HEADERS) # Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21 add_compile_options(/Z7) else() @@ -120,7 +120,7 @@ else() add_compile_options(-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2) endif() - if (CITRA_WARNINGS_AS_ERRORS) + if (LUCINA3DS_WARNINGS_AS_ERRORS) add_compile_options(-Werror) endif() @@ -181,11 +181,11 @@ if (ENABLE_TESTS) endif() if (ENABLE_SDL2 AND ENABLE_SDL2_FRONTEND) - add_subdirectory(citra) + add_subdirectory(lucina3ds) endif() if (ENABLE_QT) - add_subdirectory(citra_qt) + add_subdirectory(lucina3ds_qt) endif() if (ENABLE_DEDICATED_ROOM) @@ -194,7 +194,7 @@ endif() if (ANDROID) add_subdirectory(android/app/src/main/jni) - target_include_directories(citra-android PRIVATE android/app/src/main) + target_include_directories(lucina3ds-android PRIVATE android/app/src/main) endif() if (ENABLE_WEB_SERVICE) diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index a000e1fc..19e4af23 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -43,7 +43,7 @@ add_library(audio_core STATIC create_target_directory_groups(audio_core) -target_link_libraries(audio_core PUBLIC citra_common citra_core) +target_link_libraries(audio_core PUBLIC lucina3ds_common lucina3ds_core) target_link_libraries(audio_core PRIVATE faad2 SoundTouch teakra) if(ENABLE_SDL2) @@ -62,6 +62,6 @@ if(ENABLE_OPENAL) add_definitions(-DAL_LIBTYPE_STATIC) endif() -if (CITRA_USE_PRECOMPILED_HEADERS) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) target_precompile_headers(audio_core PRIVATE precompiled_headers.h) endif() diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 56783124..5328071e 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -52,7 +52,7 @@ add_custom_command(OUTPUT scm_rev.cpp "${CMAKE_SOURCE_DIR}/CMakeModules/GenerateSCMRev.cmake" ) -add_library(citra_common STATIC +add_library(lucina3ds_common STATIC aarch64/cpu_detect.cpp aarch64/cpu_detect.h aarch64/oaknut_abi.h @@ -151,23 +151,23 @@ add_library(citra_common STATIC ) if (UNIX AND NOT APPLE) - target_sources(citra_common PRIVATE + target_sources(lucina3ds_common PRIVATE linux/gamemode.cpp linux/gamemode.h ) - target_link_libraries(citra_common PRIVATE gamemode) + target_link_libraries(lucina3ds_common PRIVATE gamemode) endif() if (APPLE) - target_sources(citra_common PUBLIC + target_sources(lucina3ds_common PUBLIC apple_authorization.h apple_authorization.cpp ) endif() if (MSVC) - target_compile_options(citra_common PRIVATE + target_compile_options(lucina3ds_common PRIVATE /W4 /we4242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data @@ -175,30 +175,30 @@ if (MSVC) /we4800 # Implicit conversion from 'type' to bool. Possible information loss ) else() - target_compile_options(citra_common PRIVATE + target_compile_options(lucina3ds_common PRIVATE $<$:-fsized-deallocation> ) endif() -create_target_directory_groups(citra_common) +create_target_directory_groups(lucina3ds_common) -target_link_libraries(citra_common PUBLIC fmt library-headers microprofile Boost::boost Boost::serialization Boost::iostreams) -target_link_libraries(citra_common PRIVATE zstd) +target_link_libraries(lucina3ds_common PUBLIC fmt library-headers microprofile Boost::boost Boost::serialization Boost::iostreams) +target_link_libraries(lucina3ds_common PRIVATE zstd) if ("x86_64" IN_LIST ARCHITECTURE) - target_link_libraries(citra_common PRIVATE xbyak) + target_link_libraries(lucina3ds_common PRIVATE xbyak) endif() if ("arm64" IN_LIST ARCHITECTURE) - target_link_libraries(citra_common PRIVATE oaknut) + target_link_libraries(lucina3ds_common PRIVATE oaknut) endif() -if (CITRA_USE_PRECOMPILED_HEADERS) - target_precompile_headers(citra_common PRIVATE precompiled_headers.h) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) + target_precompile_headers(lucina3ds_common PRIVATE precompiled_headers.h) endif() find_library(BACKTRACE_LIBRARY backtrace) if (BACKTRACE_LIBRARY AND ${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID STREQUAL GNU) - target_link_libraries(citra_common PRIVATE ${BACKTRACE_LIBRARY} dl) - target_compile_definitions(citra_common PRIVATE CITRA_LINUX_GCC_BACKTRACE) + target_link_libraries(lucina3ds_common PRIVATE ${BACKTRACE_LIBRARY} dl) + target_compile_definitions(lucina3ds_common PRIVATE lucina3ds_LINUX_GCC_BACKTRACE) endif() diff --git a/src/common/aarch64/cpu_detect.cpp b/src/common/aarch64/cpu_detect.cpp index e98fc326..7ca3809f 100644 --- a/src/common/aarch64/cpu_detect.cpp +++ b/src/common/aarch64/cpu_detect.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include "common/arch.h" -#if CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(arm64) #include #include diff --git a/src/common/arch.h b/src/common/arch.h index 5c70b8dc..6fc76bf7 100644 --- a/src/common/arch.h +++ b/src/common/arch.h @@ -6,8 +6,8 @@ #include -#define CITRA_ARCH(NAME) (CITRA_ARCH_##NAME) +#define LUCINA3DS_ARCH(NAME) (LUCINA3DS_ARCH_##NAME) -#define CITRA_ARCH_x86_64 BOOST_ARCH_X86_64 -#define CITRA_ARCH_arm64 \ +#define LUCINA3DS_ARCH_x86_64 BOOST_ARCH_X86_64 +#define LUCINA3DS_ARCH_arm64 \ (BOOST_ARCH_ARM >= BOOST_VERSION_NUMBER(8, 0, 0) && BOOST_ARCH_WORD_BITS == 64) diff --git a/src/common/assert.h b/src/common/assert.h index 5f632a5c..3f2b2774 100644 --- a/src/common/assert.h +++ b/src/common/assert.h @@ -16,7 +16,7 @@ #define ASSERT(_a_) \ do \ if (!(_a_)) [[unlikely]] { \ - []() CITRA_NO_INLINE CITRA_NO_RETURN { \ + []() LUCINA3DS_NO_INLINE LUCINA3DS_NO_RETURN { \ LOG_CRITICAL(Debug, "Assertion Failed!"); \ Common::Log::Stop(); \ Crash(); \ @@ -28,7 +28,7 @@ #define ASSERT_MSG(_a_, ...) \ do \ if (!(_a_)) [[unlikely]] { \ - [&]() CITRA_NO_INLINE CITRA_NO_RETURN { \ + [&]() LUCINA3DS_NO_INLINE LUCINA3DS_NO_RETURN { \ LOG_CRITICAL(Debug, "Assertion Failed!\n" __VA_ARGS__); \ Common::Log::Stop(); \ Crash(); \ @@ -38,7 +38,7 @@ while (0) #define UNREACHABLE() \ - ([]() CITRA_NO_INLINE CITRA_NO_RETURN { \ + ([]() LUCINA3DS_NO_INLINE LUCINA3DS_NO_RETURN { \ LOG_CRITICAL(Debug, "Unreachable code!"); \ Common::Log::Stop(); \ Crash(); \ @@ -46,7 +46,7 @@ }()) #define UNREACHABLE_MSG(...) \ - ([&]() CITRA_NO_INLINE CITRA_NO_RETURN { \ + ([&]() LUCINA3DS_NO_INLINE LUCINA3DS_NO_RETURN { \ LOG_CRITICAL(Debug, "Unreachable code!\n" __VA_ARGS__); \ Common::Log::Stop(); \ Crash(); \ diff --git a/src/common/common_funcs.h b/src/common/common_funcs.h index 8f109a8d..7921ec1c 100644 --- a/src/common/common_funcs.h +++ b/src/common/common_funcs.h @@ -24,15 +24,15 @@ #endif #ifdef _MSC_VER -#define CITRA_NO_INLINE __declspec(noinline) +#define LUCINA3DS_NO_INLINE __declspec(noinline) #else -#define CITRA_NO_INLINE __attribute__((noinline)) +#define LUCINA3DS_NO_INLINE __attribute__((noinline)) #endif #ifdef _MSC_VER -#define CITRA_NO_RETURN __declspec(noreturn) +#define LUCINA3DS_NO_RETURN __declspec(noreturn) #else -#define CITRA_NO_RETURN __attribute__((noreturn)) +#define LUCINA3DS_NO_RETURN __attribute__((noreturn)) #endif #ifdef _MSC_VER diff --git a/src/common/common_paths.h b/src/common/common_paths.h index f12eedb0..4bbe3fda 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -18,18 +18,18 @@ #define EMU_DATA_DIR USER_DIR #else #ifdef _WIN32 -#define EMU_DATA_DIR "Citra" +#define EMU_DATA_DIR "Lucina3DS" #elif defined(__APPLE__) #include #if TARGET_OS_IPHONE -#define APPLE_EMU_DATA_DIR "Documents" DIR_SEP "Citra" +#define APPLE_EMU_DATA_DIR "Documents" DIR_SEP "Lucina3DS" #else -#define APPLE_EMU_DATA_DIR "Library" DIR_SEP "Application Support" DIR_SEP "Citra" +#define APPLE_EMU_DATA_DIR "Library" DIR_SEP "Application Support" DIR_SEP "Lucina3DS" #endif // For compatibility with XDG paths. -#define EMU_DATA_DIR "citra-emu" +#define EMU_DATA_DIR "lucina3ds" #else -#define EMU_DATA_DIR "citra-emu" +#define EMU_DATA_DIR "lucina3ds" #endif #endif @@ -55,7 +55,7 @@ // Filenames // Files in the directory returned by GetUserPath(UserPath::LogDir) -#define LOG_FILE "citra_log.txt" +#define LOG_FILE "lucina3ds_log.txt" // Files in the directory returned by GetUserPath(UserPath::ConfigDir) #define EMU_CONFIG "emu.ini" diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index cda752e5..7be7bba2 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -851,8 +851,8 @@ bool StringReplace(std::string& haystack, const std::string& a, const std::strin std::string SerializePath(const std::string& input, bool is_saving) { auto result = input; - StringReplace(result, "%CITRA_ROM_FILE%", g_currentRomPath, is_saving); - StringReplace(result, "%CITRA_USER_DIR%", GetUserPath(UserPath::UserDir), is_saving); + StringReplace(result, "%LUCINA3DS_ROM_FILE%", g_currentRomPath, is_saving); + StringReplace(result, "%LUCINA3DS_USER_DIR%", GetUserPath(UserPath::UserDir), is_saving); return result; } diff --git a/src/common/file_util.h b/src/common/file_util.h index 6595fead..6c901273 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -182,11 +182,11 @@ void SetUserPath(const std::string& path = ""); void SetCurrentRomPath(const std::string& path); -// Returns a pointer to a string with a Citra data dir in the user's home +// Returns a pointer to a string with a lucina3ds data dir in the user's home // directory. To be used in "multi-user" mode (that is, installed). [[nodiscard]] const std::string& GetUserPath(UserPath path); -// Returns a pointer to a string with the default Citra data dir in the user's home +// Returns a pointer to a string with the default lucina3ds data dir in the user's home // directory. [[nodiscard]] const std::string& GetDefaultUserPath(UserPath path); @@ -266,8 +266,8 @@ public: IOFile(); // flags is used for windows specific file open mode flags, which - // allows citra to open the logs in shared write mode, so that the file - // isn't considered "locked" while citra is open and people can open the log file and view it + // allows lucina3ds to open the logs in shared write mode, so that the file + // isn't considered "locked" while lucina3ds is open and people can open the log file and view it IOFile(const std::string& filename, const char openmode[], int flags = 0); ~IOFile(); diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 1a911fd7..8c6f0c4b 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -14,7 +14,7 @@ #define _SH_DENYWR 0 #endif -#ifdef CITRA_LINUX_GCC_BACKTRACE +#ifdef LUCINA3DS_LINUX_GCC_BACKTRACE #define BOOST_STACKTRACE_USE_BACKTRACE #include #undef BOOST_STACKTRACE_USE_BACKTRACE @@ -182,7 +182,7 @@ public: bool initialization_in_progress_suppress_logging = true; -#ifdef CITRA_LINUX_GCC_BACKTRACE +#ifdef LUCINA3DS_LINUX_GCC_BACKTRACE [[noreturn]] void SleepForever() { while (true) { pause(); @@ -276,7 +276,7 @@ public: private: Impl(const std::string& file_backend_filename, const Filter& filter_) : filter{filter_}, file_backend{file_backend_filename} { -#ifdef CITRA_LINUX_GCC_BACKTRACE +#ifdef LUCINA3DS_LINUX_GCC_BACKTRACE int waker_pipefd[2]; int done_printing_pipefd[2]; if (pipe2(waker_pipefd, O_CLOEXEC) || pipe2(done_printing_pipefd, O_CLOEXEC)) { @@ -285,7 +285,7 @@ private: backtrace_thread_waker_fd = waker_pipefd[1]; backtrace_done_printing_fd = done_printing_pipefd[0]; std::thread([this, wait_fd = waker_pipefd[0], done_fd = done_printing_pipefd[1]] { - Common::SetCurrentThreadName("citra:Crash"); + Common::SetCurrentThreadName("lucina3ds:Crash"); for (u8 ignore = 0; read(wait_fd, &ignore, 1) != 1;) ; const int sig = received_signal; @@ -330,7 +330,7 @@ private: } ~Impl() { -#ifdef CITRA_LINUX_GCC_BACKTRACE +#ifdef LUCINA3DS_LINUX_GCC_BACKTRACE if (int zero_or_ignore = 0; !received_signal.compare_exchange_strong(zero_or_ignore, SIGKILL)) { SleepForever(); @@ -340,7 +340,7 @@ private: void StartBackendThread() { backend_thread = std::jthread([this](std::stop_token stop_token) { - Common::SetCurrentThreadName("citra:Log"); + Common::SetCurrentThreadName("lucina3ds:Log"); Entry entry; const auto write_logs = [this, &entry]() { ForEachBackend([&entry](Backend& backend) { backend.Write(entry); }); @@ -399,7 +399,7 @@ private: delete ptr; } -#ifdef CITRA_LINUX_GCC_BACKTRACE +#ifdef LUCINA3DS_LINUX_GCC_BACKTRACE [[noreturn]] static void HandleSignal(int sig) { signal(SIGABRT, SIG_DFL); signal(SIGSEGV, SIG_DFL); @@ -444,7 +444,7 @@ private: std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()}; std::jthread backend_thread; -#ifdef CITRA_LINUX_GCC_BACKTRACE +#ifdef LUCINA3DS_LINUX_GCC_BACKTRACE std::atomic_int received_signal{0}; std::array backtrace_storage{}; int backtrace_thread_waker_fd; diff --git a/src/common/logging/text_formatter.cpp b/src/common/logging/text_formatter.cpp index 753e5003..9fe9ea85 100644 --- a/src/common/logging/text_formatter.cpp +++ b/src/common/logging/text_formatter.cpp @@ -137,7 +137,7 @@ void PrintMessageToLogcat([[maybe_unused]] const Entry& entry) { case Level::Count: UNREACHABLE(); } - __android_log_print(android_log_priority, "CitraNative", "%s", str.c_str()); + __android_log_print(android_log_priority, "Lucina3DSNative", "%s", str.c_str()); #endif } } // namespace Common::Log diff --git a/src/common/microprofileui.h b/src/common/microprofileui.h index 41abe6b7..4afeda3f 100644 --- a/src/common/microprofileui.h +++ b/src/common/microprofileui.h @@ -6,7 +6,7 @@ #include "common/microprofile.h" -// Customized Citra settings. +// Customized Lucina3DS settings. // This file wraps the MicroProfile header so that these are consistent everywhere. #define MICROPROFILE_TEXT_WIDTH 6 #define MICROPROFILE_TEXT_HEIGHT 12 diff --git a/src/common/scm_rev.cpp.in b/src/common/scm_rev.cpp.in index 0e52a6d2..feaf8c26 100644 --- a/src/common/scm_rev.cpp.in +++ b/src/common/scm_rev.cpp.in @@ -16,11 +16,14 @@ namespace Common { const char g_scm_rev[] = GIT_REV; -const char g_scm_branch[] = GIT_BRANCH; -const char g_scm_desc[] = GIT_DESC; +// const char g_scm_branch[] = GIT_BRANCH; +const char g_scm_branch[] = "alpha"; +// const char g_scm_desc[] = GIT_DESC; +const char g_scm_desc[] = "pre-2025-2-9"; const char g_build_name[] = BUILD_NAME; const char g_build_date[] = BUILD_DATE; -const char g_build_fullname[] = BUILD_FULLNAME; +//const char g_build_fullname[] = BUILD_FULLNAME; +const char g_build_fullname[] = "Lucina3DS"; const char g_build_version[] = BUILD_VERSION; const char g_shader_cache_version[] = SHADER_CACHE_VERSION; diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 833901a7..e675295b 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -80,7 +80,7 @@ void LogSettings() { LOG_INFO(Config, "{}: {}", name, value); }; - LOG_INFO(Config, "Citra Configuration:"); + LOG_INFO(Config, "Lucina3DS Configuration:"); log_setting("Core_UseCpuJit", values.use_cpu_jit.GetValue()); log_setting("Core_CPUClockPercentage", values.cpu_clock_percentage.GetValue()); log_setting("Controller_UseArticController", values.use_artic_base_controller.GetValue()); diff --git a/src/common/x64/cpu_detect.cpp b/src/common/x64/cpu_detect.cpp index 0791a3f2..ac1c06d7 100644 --- a/src/common/x64/cpu_detect.cpp +++ b/src/common/x64/cpu_detect.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include "common/arch.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include #include "common/common_types.h" @@ -148,4 +148,4 @@ const CPUCaps& GetCPUCaps() { } // namespace Common -#endif // CITRA_ARCH(x86_64) +#endif // LUCINA3DS_ARCH(x86_64) diff --git a/src/common/x64/cpu_detect.h b/src/common/x64/cpu_detect.h index 31ed1c58..a2a03154 100644 --- a/src/common/x64/cpu_detect.h +++ b/src/common/x64/cpu_detect.h @@ -5,7 +5,7 @@ #pragma once #include "common/arch.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) namespace Common { @@ -37,4 +37,4 @@ const CPUCaps& GetCPUCaps(); } // namespace Common -#endif // CITRA_ARCH(x86_64) +#endif // LUCINA3DS_ARCH(x86_64) diff --git a/src/common/x64/xbyak_abi.h b/src/common/x64/xbyak_abi.h index 9c361534..6eb6b2f6 100644 --- a/src/common/x64/xbyak_abi.h +++ b/src/common/x64/xbyak_abi.h @@ -5,7 +5,7 @@ #pragma once #include "common/arch.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include #include @@ -232,4 +232,4 @@ inline void ABI_PopRegistersAndAdjustStack(Xbyak::CodeGenerator& code, std::bits } // namespace Common::X64 -#endif // CITRA_ARCH(x86_64) +#endif // LUCINA3DS_ARCH(x86_64) diff --git a/src/common/x64/xbyak_util.h b/src/common/x64/xbyak_util.h index 32621b59..e187caf0 100644 --- a/src/common/x64/xbyak_util.h +++ b/src/common/x64/xbyak_util.h @@ -5,7 +5,7 @@ #pragma once #include "common/arch.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include #include @@ -49,4 +49,4 @@ inline void CallFarFunction(Xbyak::CodeGenerator& code, const T f) { } // namespace Common::X64 -#endif // CITRA_ARCH(x86_64) +#endif // LUCINA3DS_ARCH(x86_64) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 331bc9cf..8921931b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(citra_core STATIC +add_library(lucina3ds_core STATIC 3ds.h arm/arm_interface.h arm/dyncom/arm_dyncom.cpp @@ -484,19 +484,19 @@ add_library(citra_core STATIC tracer/recorder.h ) -create_target_directory_groups(citra_core) +create_target_directory_groups(lucina3ds_core) -target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network video_core) -target_link_libraries(citra_core PRIVATE Boost::boost Boost::serialization Boost::iostreams httplib) -target_link_libraries(citra_core PUBLIC dds-ktx PRIVATE cryptopp fmt lodepng open_source_archives) +target_link_libraries(lucina3ds_core PUBLIC lucina3ds_common PRIVATE audio_core network video_core) +target_link_libraries(lucina3ds_core PRIVATE Boost::boost Boost::serialization Boost::iostreams httplib) +target_link_libraries(lucina3ds_core PUBLIC dds-ktx PRIVATE cryptopp fmt lodepng open_source_archives) if (ENABLE_WEB_SERVICE) - target_link_libraries(citra_core PRIVATE web_service) + target_link_libraries(lucina3ds_core PRIVATE web_service) endif() if (ENABLE_SCRIPTING) - target_compile_definitions(citra_core PUBLIC -DENABLE_SCRIPTING) - target_sources(citra_core PRIVATE + target_compile_definitions(lucina3ds_core PUBLIC -DENABLE_SCRIPTING) + target_sources(lucina3ds_core PRIVATE rpc/packet.cpp rpc/packet.h rpc/rpc_server.cpp @@ -509,7 +509,7 @@ if (ENABLE_SCRIPTING) endif() if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE) - target_sources(citra_core PRIVATE + target_sources(lucina3ds_core PRIVATE arm/dynarmic/arm_dynarmic.cpp arm/dynarmic/arm_dynarmic.h arm/dynarmic/arm_dynarmic_cp15.cpp @@ -519,9 +519,9 @@ if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE) arm/dynarmic/arm_tick_counts.cpp arm/dynarmic/arm_tick_counts.h ) - target_link_libraries(citra_core PRIVATE dynarmic) + target_link_libraries(lucina3ds_core PRIVATE dynarmic) endif() -if (CITRA_USE_PRECOMPILED_HEADERS) - target_precompile_headers(citra_core PRIVATE precompiled_headers.h) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) + target_precompile_headers(lucina3ds_core PRIVATE precompiled_headers.h) endif() diff --git a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp index 2d333cda..61c8c4e0 100644 --- a/src/core/arm/dyncom/arm_dyncom_interpreter.cpp +++ b/src/core/arm/dyncom/arm_dyncom_interpreter.cpp @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#define CITRA_IGNORE_EXIT(x) +#define LUCINA3DS_IGNORE_EXIT(x) #include #include @@ -232,7 +232,7 @@ static unsigned int DPO(RotateRightByRegister)(ARMul_State* cpu, unsigned int sh #define DEBUG_MSG \ LOG_DEBUG(Core_ARM11, "inst is {:x}", inst); \ - CITRA_IGNORE_EXIT(0) + LUCINA3DS_IGNORE_EXIT(0) #define LnSWoUB(s) glue(LnSWoUB, s) #define MLnS(s) glue(MLnS, s) @@ -832,7 +832,7 @@ static unsigned int InterpreterTranslateInstruction(const ARMul_State* cpu, cons inst); LOG_ERROR(Core_ARM11, "cpsr={:#X}, cpu->TFlag={}, r15={:#010X}", cpu->Cpsr, cpu->TFlag, cpu->Reg[15]); - CITRA_IGNORE_EXIT(-1); + LUCINA3DS_IGNORE_EXIT(-1); } inst_base = arm_instruction_trans[idx](inst, idx); diff --git a/src/core/arm/exclusive_monitor.cpp b/src/core/arm/exclusive_monitor.cpp index 5ba6dc9a..44c589cf 100644 --- a/src/core/arm/exclusive_monitor.cpp +++ b/src/core/arm/exclusive_monitor.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "common/arch.h" -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) #include "core/arm/dynarmic/arm_exclusive_monitor.h" #endif #include "common/settings.h" @@ -15,7 +15,7 @@ ExclusiveMonitor::~ExclusiveMonitor() = default; std::unique_ptr MakeExclusiveMonitor(Memory::MemorySystem& memory, std::size_t num_cores) { -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) if (Settings::values.use_cpu_jit) { return std::make_unique(memory, num_cores); } diff --git a/src/core/cheats/gateway_cheat.cpp b/src/core/cheats/gateway_cheat.cpp index 4f945669..0ecf3fbb 100644 --- a/src/core/cheats/gateway_cheat.cpp +++ b/src/core/cheats/gateway_cheat.cpp @@ -455,7 +455,7 @@ std::string GatewayCheat::GetCode() const { } /// A special marker used to keep track of enabled cheats -static constexpr char EnabledText[] = "*citra_enabled"; +static constexpr char EnabledText[] = "*lucina3ds_enabled"; std::string GatewayCheat::ToString() const { std::string result; diff --git a/src/core/core.cpp b/src/core/core.cpp index 9f4fcf40..f07ce4cc 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -16,7 +16,7 @@ #include "core/hle/service/cam/cam.h" #include "core/hle/service/hid/hid.h" #include "core/hle/service/ir/ir_user.h" -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) #include "core/arm/dynarmic/arm_dynarmic.h" #endif #include "core/arm/dyncom/arm_dyncom.h" @@ -421,7 +421,7 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, exclusive_monitor = MakeExclusiveMonitor(*memory, num_cores); cpu_cores.reserve(num_cores); if (Settings::values.use_cpu_jit) { -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) for (u32 i = 0; i < num_cores; ++i) { cpu_cores.push_back(std::make_shared( *this, *memory, i, timing->GetTimer(i), *exclusive_monitor)); diff --git a/src/core/file_sys/plugin_3gx.cpp b/src/core/file_sys/plugin_3gx.cpp index e840a895..1450d20c 100644 --- a/src/core/file_sys/plugin_3gx.cpp +++ b/src/core/file_sys/plugin_3gx.cpp @@ -91,7 +91,7 @@ Loader::ResultStatus FileSys::Plugin3GXLoader::Load( } if (header.infos.flags.compatibility == static_cast(_3gx_Infos::Compatibility::CONSOLE)) { - LOG_ERROR(Service_PLGLDR, "Failed to load 3GX plugin. Not compatible with Citra: {}", + LOG_ERROR(Service_PLGLDR, "Failed to load 3GX plugin. Not compatible with Lucina3DS: {}", plg_context.plugin_path); return Loader::ResultStatus::Error; } diff --git a/src/core/file_sys/plugin_3gx.h b/src/core/file_sys/plugin_3gx.h index 01a9f848..1a1b1e9d 100644 --- a/src/core/file_sys/plugin_3gx.h +++ b/src/core/file_sys/plugin_3gx.h @@ -54,8 +54,8 @@ public: u32_le exe_size; // Include sizeof(PluginHeader) + .text + .rodata + .data + .bss (0x1000 // aligned too) u32_le is_default_plugin; - u32_le plgldr_event; ///< Used for synchronization, unused in citra - u32_le plgldr_reply; ///< Used for synchronization, unused in citra + u32_le plgldr_event; ///< Used for synchronization, unused + u32_le plgldr_reply; ///< Used for synchronization, unused u32_le reserved[24]; u32_le config[32]; }; @@ -79,7 +79,7 @@ private: bool no_flash); struct _3gx_Infos { - enum class Compatibility { CONSOLE = 0, CITRA = 1, CONSOLE_CITRA = 2 }; + enum class Compatibility { CONSOLE = 0, LUCINA3DS = 1, CONSOLE_LUCINA3DS = 2 }; u32_le author_len; u32_le author_msg_offset; u32_le title_len; diff --git a/src/core/hle/kernel/ipc.cpp b/src/core/hle/kernel/ipc.cpp index dcb8b8ac..ff2704fb 100644 --- a/src/core/hle/kernel/ipc.cpp +++ b/src/core/hle/kernel/ipc.cpp @@ -141,10 +141,10 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem u32 size = static_cast(descInfo.size); IPC::MappedBufferPermissions permissions = descInfo.perms; - VAddr page_start = Common::AlignDown(source_address, Memory::CITRA_PAGE_SIZE); + VAddr page_start = Common::AlignDown(source_address, Memory::LUCINA3DS_PAGE_SIZE); u32 page_offset = source_address - page_start; - u32 num_pages = Common::AlignUp(page_offset + size, Memory::CITRA_PAGE_SIZE) >> - Memory::CITRA_PAGE_BITS; + u32 num_pages = Common::AlignUp(page_offset + size, Memory::LUCINA3DS_PAGE_SIZE) >> + Memory::LUCINA3DS_PAGE_BITS; // Skip when the size is zero and num_pages == 0 if (size == 0) { @@ -174,8 +174,8 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem found->target_address, size); } - VAddr prev_reserve = page_start - Memory::CITRA_PAGE_SIZE; - VAddr next_reserve = page_start + num_pages * Memory::CITRA_PAGE_SIZE; + VAddr prev_reserve = page_start - Memory::LUCINA3DS_PAGE_SIZE; + VAddr next_reserve = page_start + num_pages * Memory::LUCINA3DS_PAGE_SIZE; auto& prev_vma = src_process->vm_manager.FindVMA(prev_reserve)->second; auto& next_vma = src_process->vm_manager.FindVMA(next_reserve)->second; @@ -184,8 +184,8 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem // Unmap the buffer and guard pages from the source process Result result = - src_process->vm_manager.UnmapRange(page_start - Memory::CITRA_PAGE_SIZE, - (num_pages + 2) * Memory::CITRA_PAGE_SIZE); + src_process->vm_manager.UnmapRange(page_start - Memory::LUCINA3DS_PAGE_SIZE, + (num_pages + 2) * Memory::LUCINA3DS_PAGE_SIZE); ASSERT(result == ResultSuccess); mapped_buffer_context.erase(found); @@ -200,9 +200,9 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem // Create a buffer which contains the mapped buffer and two additional guard pages. std::shared_ptr buffer = - std::make_shared((num_pages + 2) * Memory::CITRA_PAGE_SIZE); + std::make_shared((num_pages + 2) * Memory::LUCINA3DS_PAGE_SIZE); memory.ReadBlock(*src_process, source_address, - buffer->GetPtr() + Memory::CITRA_PAGE_SIZE + page_offset, size); + buffer->GetPtr() + Memory::LUCINA3DS_PAGE_SIZE + page_offset, size); // Map the guard pages and mapped pages at once. target_address = @@ -215,18 +215,18 @@ Result TranslateCommandBuffer(Kernel::KernelSystem& kernel, Memory::MemorySystem // Change the permissions and state of the guard pages. const VAddr low_guard_address = target_address; const VAddr high_guard_address = - low_guard_address + static_cast(buffer->GetSize()) - Memory::CITRA_PAGE_SIZE; + low_guard_address + static_cast(buffer->GetSize()) - Memory::LUCINA3DS_PAGE_SIZE; ASSERT(dst_process->vm_manager.ChangeMemoryState( - low_guard_address, Memory::CITRA_PAGE_SIZE, Kernel::MemoryState::Shared, + low_guard_address, Memory::LUCINA3DS_PAGE_SIZE, Kernel::MemoryState::Shared, Kernel::VMAPermission::ReadWrite, Kernel::MemoryState::Reserved, Kernel::VMAPermission::None) == ResultSuccess); ASSERT(dst_process->vm_manager.ChangeMemoryState( - high_guard_address, Memory::CITRA_PAGE_SIZE, Kernel::MemoryState::Shared, + high_guard_address, Memory::LUCINA3DS_PAGE_SIZE, Kernel::MemoryState::Shared, Kernel::VMAPermission::ReadWrite, Kernel::MemoryState::Reserved, Kernel::VMAPermission::None) == ResultSuccess); // Get proper mapped buffer address and store it in the cmd buffer. - target_address += Memory::CITRA_PAGE_SIZE; + target_address += Memory::LUCINA3DS_PAGE_SIZE; cmd_buf[i++] = target_address + page_offset; mapped_buffer_context.push_back({permissions, size, source_address, diff --git a/src/core/hle/kernel/process.cpp b/src/core/hle/kernel/process.cpp index 3d0a315e..10f55792 100644 --- a/src/core/hle/kernel/process.cpp +++ b/src/core/hle/kernel/process.cpp @@ -182,7 +182,7 @@ void Process::ParseKernelCaps(const u32* kernel_caps, std::size_t len) { // Mapped memory page AddressMapping mapping; mapping.address = descriptor << 12; - mapping.size = Memory::CITRA_PAGE_SIZE; + mapping.size = Memory::LUCINA3DS_PAGE_SIZE; mapping.read_only = false; mapping.unk_flag = false; @@ -459,7 +459,7 @@ ResultVal Process::AllocateThreadLocalStorage() { auto base_memory_region = kernel.GetMemoryRegion(MemoryRegion::BASE); // Allocate some memory from the end of the linear heap for this region. - auto offset = base_memory_region->LinearAllocate(Memory::CITRA_PAGE_SIZE); + auto offset = base_memory_region->LinearAllocate(Memory::LUCINA3DS_PAGE_SIZE); if (!offset) { LOG_ERROR(Kernel_SVC, "Not enough space in BASE linear region to allocate a new TLS page"); @@ -467,17 +467,17 @@ ResultVal Process::AllocateThreadLocalStorage() { } holding_tls_memory += - MemoryRegionInfo::Interval(*offset, *offset + Memory::CITRA_PAGE_SIZE); - memory_used += Memory::CITRA_PAGE_SIZE; + MemoryRegionInfo::Interval(*offset, *offset + Memory::LUCINA3DS_PAGE_SIZE); + memory_used += Memory::LUCINA3DS_PAGE_SIZE; // The page is completely available at the start. tls_slots.emplace_back(0); // Map the page to the current process' address space. auto tls_page_addr = - Memory::TLS_AREA_VADDR + static_cast(tls_page) * Memory::CITRA_PAGE_SIZE; + Memory::TLS_AREA_VADDR + static_cast(tls_page) * Memory::LUCINA3DS_PAGE_SIZE; vm_manager.MapBackingMemory(tls_page_addr, kernel.memory.GetFCRAMRef(*offset), - Memory::CITRA_PAGE_SIZE, MemoryState::Locked); + Memory::LUCINA3DS_PAGE_SIZE, MemoryState::Locked); LOG_DEBUG(Kernel, "Allocated TLS page at addr={:08X}", tls_page_addr); } else { @@ -488,7 +488,7 @@ ResultVal Process::AllocateThreadLocalStorage() { tls_slots[tls_page].set(tls_slot); auto tls_address = Memory::TLS_AREA_VADDR + - static_cast(tls_page) * Memory::CITRA_PAGE_SIZE + + static_cast(tls_page) * Memory::LUCINA3DS_PAGE_SIZE + static_cast(tls_slot) * Memory::TLS_ENTRY_SIZE; kernel.memory.ZeroBlock(*this, tls_address, Memory::TLS_ENTRY_SIZE); diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 4d8eab4e..a7c7a10f 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -76,11 +76,11 @@ enum class KernelState { */ KERNEL_STATE_REBOOT = 7, - // Special Citra only states. + // Special Lucina3DS only states. /** * Sets the emulation speed percentage. A value of 0 means unthrottled. */ - KERNEL_STATE_CITRA_EMULATION_SPEED = 0x20000 /// + KERNEL_STATE_LUCINA3DS_EMULATION_SPEED = 0x20000 /// }; struct PageInfo { @@ -125,10 +125,10 @@ enum class SystemInfoType { */ NEW_3DS_INFO = 0x10001, /** - * Gets citra related information. This parameter is not available on real systems, + * Gets lucina3ds related information. This parameter is not available on real systems, * but can be used by homebrew applications to get some emulator info. */ - CITRA_INFORMATION = 0x20000, + LUCINA3DS_INFORMATION = 0x20000, }; enum class ProcessInfoType { @@ -272,15 +272,15 @@ enum class SystemInfoMemUsageRegion { /** * Accepted by svcGetSystemInfo param with CITRA_INFORMATION type. Selects which information - * to fetch from Citra. Some string params don't fit in 7 bytes, so they are split. + * to fetch from Lucina3DS. Some string params don't fit in 7 bytes, so they are split. */ -enum class SystemInfoCitraInformation { - IS_CITRA = 0, // Always set the output to 1, signaling the app is running on Citra. +enum class SystemInfoLucina3DSInformation { + IS_LUCINA3DS = 0, // Always set the output to 1, signaling the app is running on Lucina3DS. HOST_TICK = 1, // Tick reference from the host in ns, unaffected by lag or cpu speed. EMULATION_SPEED = 2, // Gets the emulation speed set by the user or by KernelSetState. BUILD_NAME = 10, // (ie: Nightly, Canary). BUILD_VERSION = 11, // Build version. - BUILD_PLATFORM = 12, // Build platform, see SystemInfoCitraPlatform. + BUILD_PLATFORM = 12, // Build platform, see SystemInfoLucina3DSPlatform. BUILD_DATE_PART1 = 20, // Build date first 7 characters. BUILD_DATE_PART2 = 21, // Build date next 7 characters. BUILD_DATE_PART3 = 22, // Build date next 7 characters. @@ -294,12 +294,12 @@ enum class SystemInfoCitraInformation { /** * Current officially supported platforms. */ -enum class SystemInfoCitraPlatform { +enum class SystemInfoLucina3DSPlatform { PLATFORM_UNKNOWN = 0, PLATFORM_WINDOWS = 1, PLATFORM_LINUX = 2, - PLATFORM_APPLE = 3, - PLATFORM_ANDROID = 4, + PLATFORM_ANDROID = 3, + PLATFORM_APPLE = 4, }; /** @@ -495,9 +495,9 @@ Result SVC::ControlMemory(u32* out_addr, u32 addr0, u32 addr1, u32 size, u32 ope "size=0x{:X}, permissions=0x{:08X}", operation, addr0, addr1, size, permissions); - R_UNLESS((addr0 & Memory::CITRA_PAGE_MASK) == 0, ResultMisalignedAddress); - R_UNLESS((addr1 & Memory::CITRA_PAGE_MASK) == 0, ResultMisalignedAddress); - R_UNLESS((size & Memory::CITRA_PAGE_MASK) == 0, ResultMisalignedSize); + R_UNLESS((addr0 & Memory::LUCINA3DS_PAGE_MASK) == 0, ResultMisalignedAddress); + R_UNLESS((addr1 & Memory::LUCINA3DS_PAGE_MASK) == 0, ResultMisalignedAddress); + R_UNLESS((size & Memory::LUCINA3DS_PAGE_MASK) == 0, ResultMisalignedSize); const u32 region = operation & MEMOP_REGION_MASK; operation &= ~MEMOP_REGION_MASK; @@ -1440,8 +1440,8 @@ Result SVC::KernelSetState(u32 kernel_state, u32 varg1, u32 varg2) { system.RequestShutdown(); break; - // Citra specific states. - case KernelState::KERNEL_STATE_CITRA_EMULATION_SPEED: { + // Lucina3DS specific states. + case KernelState::KERNEL_STATE_LUCINA3DS_EMULATION_SPEED: { u16 new_value = static_cast(varg1); Settings::values.frame_limit.SetValue(new_value); } break; @@ -1657,7 +1657,7 @@ Result SVC::GetHandleInfo(s64* out, Handle handle, u32 type) { /// Creates a memory block at the specified address with the specified permissions and size Result SVC::CreateMemoryBlock(Handle* out_handle, u32 addr, u32 size, u32 my_permission, u32 other_permission) { - R_UNLESS(size % Memory::CITRA_PAGE_SIZE == 0, ResultMisalignedSize); + R_UNLESS(size % Memory::LUCINA3DS_PAGE_SIZE == 0, ResultMisalignedSize); std::shared_ptr shared_memory = nullptr; @@ -1810,73 +1810,73 @@ Result SVC::GetSystemInfo(s64* out, u32 type, s32 param) { LOG_ERROR(Kernel_SVC, "unimplemented GetSystemInfo type=65537 param={}", param); *out = 0; return (system.GetNumCores() == 4) ? ResultSuccess : ResultInvalidEnumValue; - case SystemInfoType::CITRA_INFORMATION: - switch ((SystemInfoCitraInformation)param) { - case SystemInfoCitraInformation::IS_CITRA: + case SystemInfoType::LUCINA3DS_INFORMATION: + switch ((SystemInfoLucina3DSInformation)param) { + case SystemInfoLucina3DSInformation::IS_LUCINA3DS: *out = 1; break; - case SystemInfoCitraInformation::HOST_TICK: + case SystemInfoLucina3DSInformation::HOST_TICK: *out = static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()) .count()); break; - case SystemInfoCitraInformation::EMULATION_SPEED: + case SystemInfoLucina3DSInformation::EMULATION_SPEED: *out = static_cast(Settings::values.frame_limit.GetValue()); break; - case SystemInfoCitraInformation::BUILD_NAME: + case SystemInfoLucina3DSInformation::BUILD_NAME: CopyStringPart(reinterpret_cast(out), Common::g_build_name, 0, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_VERSION: + case SystemInfoLucina3DSInformation::BUILD_VERSION: CopyStringPart(reinterpret_cast(out), Common::g_build_version, 0, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_PLATFORM: { + case SystemInfoLucina3DSInformation::BUILD_PLATFORM: { #if defined(_WIN32) - *out = static_cast(SystemInfoCitraPlatform::PLATFORM_WINDOWS); + *out = static_cast(SystemInfoLucina3DSPlatform::PLATFORM_WINDOWS); #elif defined(ANDROID) - *out = static_cast(SystemInfoCitraPlatform::PLATFORM_ANDROID); + *out = static_cast(SystemInfoLucina3DSPlatform::PLATFORM_ANDROID); #elif defined(__linux__) - *out = static_cast(SystemInfoCitraPlatform::PLATFORM_LINUX); + *out = static_cast(SystemInfoLucina3DSPlatform::PLATFORM_LINUX); #elif defined(__APPLE__) - *out = static_cast(SystemInfoCitraPlatform::PLATFORM_APPLE); + *out = static_cast(SystemInfoLucina3DSPlatform::PLATFORM_APPLE); #else - *out = static_cast(SystemInfoCitraPlatform::PLATFORM_UNKNOWN); + *out = static_cast(SystemInfoLucina3DSPlatform::PLATFORM_UNKNOWN); #endif break; } - case SystemInfoCitraInformation::BUILD_DATE_PART1: + case SystemInfoLucina3DSInformation::BUILD_DATE_PART1: CopyStringPart(reinterpret_cast(out), Common::g_build_date, (sizeof(s64) - 1) * 0, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_DATE_PART2: + case SystemInfoLucina3DSInformation::BUILD_DATE_PART2: CopyStringPart(reinterpret_cast(out), Common::g_build_date, (sizeof(s64) - 1) * 1, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_DATE_PART3: + case SystemInfoLucina3DSInformation::BUILD_DATE_PART3: CopyStringPart(reinterpret_cast(out), Common::g_build_date, (sizeof(s64) - 1) * 2, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_DATE_PART4: + case SystemInfoLucina3DSInformation::BUILD_DATE_PART4: CopyStringPart(reinterpret_cast(out), Common::g_build_date, (sizeof(s64) - 1) * 3, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_GIT_BRANCH_PART1: + case SystemInfoLucina3DSInformation::BUILD_GIT_BRANCH_PART1: CopyStringPart(reinterpret_cast(out), Common::g_scm_branch, (sizeof(s64) - 1) * 0, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_GIT_BRANCH_PART2: + case SystemInfoLucina3DSInformation::BUILD_GIT_BRANCH_PART2: CopyStringPart(reinterpret_cast(out), Common::g_scm_branch, (sizeof(s64) - 1) * 1, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_GIT_DESCRIPTION_PART1: + case SystemInfoLucina3DSInformation::BUILD_GIT_DESCRIPTION_PART1: CopyStringPart(reinterpret_cast(out), Common::g_scm_desc, (sizeof(s64) - 1) * 0, sizeof(s64)); break; - case SystemInfoCitraInformation::BUILD_GIT_DESCRIPTION_PART2: + case SystemInfoLucina3DSInformation::BUILD_GIT_DESCRIPTION_PART2: CopyStringPart(reinterpret_cast(out), Common::g_scm_desc, (sizeof(s64) - 1) * 1, sizeof(s64)); break; default: - LOG_ERROR(Kernel_SVC, "unknown GetSystemInfo citra info param={}", param); + LOG_ERROR(Kernel_SVC, "unknown GetSystemInfo lucina3ds info param={}", param); *out = 0; break; } @@ -1904,7 +1904,7 @@ Result SVC::GetProcessInfo(s64* out, Handle process_handle, u32 type) { // TODO(yuriks): Type 0 returns a slightly higher number than type 2, but I'm not sure // what's the difference between them. *out = process->memory_used; - if (*out % Memory::CITRA_PAGE_SIZE != 0) { + if (*out % Memory::LUCINA3DS_PAGE_SIZE != 0) { LOG_ERROR(Kernel_SVC, "called, memory size not page-aligned"); return ResultMisalignedSize; } @@ -2047,7 +2047,7 @@ Result SVC::MapProcessMemoryEx(Handle dst_process_handle, u32 dst_address, R_UNLESS(dst_process && src_process, ResultInvalidHandle); if (size & 0xFFF) { - size = (size & ~0xFFF) + Memory::CITRA_PAGE_SIZE; + size = (size & ~0xFFF) + Memory::LUCINA3DS_PAGE_SIZE; } // TODO(PabloMK7) Fix-up this svc. @@ -2081,7 +2081,7 @@ Result SVC::UnmapProcessMemoryEx(Handle process, u32 dst_address, u32 size) { R_UNLESS(dst_process, ResultInvalidHandle); if (size & 0xFFF) { - size = (size & ~0xFFF) + Memory::CITRA_PAGE_SIZE; + size = (size & ~0xFFF) + Memory::LUCINA3DS_PAGE_SIZE; } // Only linear memory supported diff --git a/src/core/hle/kernel/thread.cpp b/src/core/hle/kernel/thread.cpp index b531ae97..504ff859 100644 --- a/src/core/hle/kernel/thread.cpp +++ b/src/core/hle/kernel/thread.cpp @@ -109,9 +109,9 @@ void Thread::Stop() { ReleaseThreadMutexes(this); // Mark the TLS slot in the thread's page as free. - u32 tls_page = (tls_address - Memory::TLS_AREA_VADDR) / Memory::CITRA_PAGE_SIZE; + u32 tls_page = (tls_address - Memory::TLS_AREA_VADDR) / Memory::LUCINA3DS_PAGE_SIZE; u32 tls_slot = - ((tls_address - Memory::TLS_AREA_VADDR) % Memory::CITRA_PAGE_SIZE) / Memory::TLS_ENTRY_SIZE; + ((tls_address - Memory::TLS_AREA_VADDR) % Memory::LUCINA3DS_PAGE_SIZE) / Memory::TLS_ENTRY_SIZE; if (auto process = owner_process.lock()) { process->tls_slots[tls_page].reset(tls_slot); process->resource_limit->Release(ResourceLimitType::Thread, 1); diff --git a/src/core/hle/kernel/vm_manager.cpp b/src/core/hle/kernel/vm_manager.cpp index 1df94d89..d6c7b9ed 100644 --- a/src/core/hle/kernel/vm_manager.cpp +++ b/src/core/hle/kernel/vm_manager.cpp @@ -253,8 +253,8 @@ VMManager::VMAIter VMManager::StripIterConstness(const VMAHandle& iter) { } ResultVal VMManager::CarveVMA(VAddr base, u32 size) { - ASSERT_MSG((size & Memory::CITRA_PAGE_MASK) == 0, "non-page aligned size: {:#10X}", size); - ASSERT_MSG((base & Memory::CITRA_PAGE_MASK) == 0, "non-page aligned base: {:#010X}", base); + ASSERT_MSG((size & Memory::LUCINA3DS_PAGE_MASK) == 0, "non-page aligned size: {:#10X}", size); + ASSERT_MSG((base & Memory::LUCINA3DS_PAGE_MASK) == 0, "non-page aligned base: {:#010X}", base); VMAIter vma_handle = StripIterConstness(FindVMA(base)); if (vma_handle == vma_map.end()) { @@ -289,8 +289,8 @@ ResultVal VMManager::CarveVMA(VAddr base, u32 size) { } ResultVal VMManager::CarveVMARange(VAddr target, u32 size) { - ASSERT_MSG((size & Memory::CITRA_PAGE_MASK) == 0, "non-page aligned size: {:#10X}", size); - ASSERT_MSG((target & Memory::CITRA_PAGE_MASK) == 0, "non-page aligned base: {:#010X}", target); + ASSERT_MSG((size & Memory::LUCINA3DS_PAGE_MASK) == 0, "non-page aligned size: {:#10X}", size); + ASSERT_MSG((target & Memory::LUCINA3DS_PAGE_MASK) == 0, "non-page aligned base: {:#010X}", target); const VAddr target_end = target + size; ASSERT(target_end >= target); diff --git a/src/core/hle/service/cfg/cfg_defaults.cpp b/src/core/hle/service/cfg/cfg_defaults.cpp index c744ac1f..56eea41b 100644 --- a/src/core/hle/service/cfg/cfg_defaults.cpp +++ b/src/core/hle/service/cfg/cfg_defaults.cpp @@ -38,7 +38,7 @@ constexpr u8 DEFAULT_SOUND_OUTPUT_MODE = SOUND_STEREO; // constants. constexpr u64_le DEFAULT_CONSOLE_ID = 0; constexpr u32_le DEFAULT_CONSOLE_RANDOM_NUMBER = 0; -constexpr UsernameBlock DEFAULT_USERNAME{{u"CITRA"}, 0, 0}; +constexpr UsernameBlock DEFAULT_USERNAME{{u"LUCINA3DS"}, 0, 0}; constexpr BirthdayBlock DEFAULT_BIRTHDAY{3, 25}; // March 25th, 2014 constexpr u8 DEFAULT_LANGUAGE = LANGUAGE_EN; constexpr ConsoleCountryInfo DEFAULT_COUNTRY_INFO{ diff --git a/src/core/hle/service/csnd/csnd_snd.cpp b/src/core/hle/service/csnd/csnd_snd.cpp index cfd367ab..fbc985a1 100644 --- a/src/core/hle/service/csnd/csnd_snd.cpp +++ b/src/core/hle/service/csnd/csnd_snd.cpp @@ -193,7 +193,7 @@ static_assert(sizeof(CaptureState) == 0x8, "CaptureState structure size is wrong void CSND_SND::Initialize(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const u32 size = Common::AlignUp(rp.Pop(), Memory::CITRA_PAGE_SIZE); + const u32 size = Common::AlignUp(rp.Pop(), Memory::LUCINA3DS_PAGE_SIZE); master_state_offset = rp.Pop(); channel_state_offset = rp.Pop(); capture_state_offset = rp.Pop(); diff --git a/src/core/hle/service/ldr_ro/cro_helper.cpp b/src/core/hle/service/ldr_ro/cro_helper.cpp index ac0e9c1f..e8dfcb90 100644 --- a/src/core/hle/service/ldr_ro/cro_helper.cpp +++ b/src/core/hle/service/ldr_ro/cro_helper.cpp @@ -1500,7 +1500,7 @@ u32 CROHelper::Fix(u32 fix_level) { } } - fix_end = Common::AlignUp(fix_end, Memory::CITRA_PAGE_SIZE); + fix_end = Common::AlignUp(fix_end, Memory::LUCINA3DS_PAGE_SIZE); u32 fixed_size = fix_end - module_address; SetField(FixedSize, fixed_size); @@ -1523,8 +1523,8 @@ std::tuple CROHelper::GetExecutablePages() const { SegmentEntry entry; GetEntry(system.Memory(), i, entry); if (entry.type == SegmentType::Code && entry.size != 0) { - VAddr begin = Common::AlignDown(entry.offset, Memory::CITRA_PAGE_SIZE); - VAddr end = Common::AlignUp(entry.offset + entry.size, Memory::CITRA_PAGE_SIZE); + VAddr begin = Common::AlignDown(entry.offset, Memory::LUCINA3DS_PAGE_SIZE); + VAddr end = Common::AlignUp(entry.offset + entry.size, Memory::LUCINA3DS_PAGE_SIZE); return std::make_tuple(begin, end - begin); } } diff --git a/src/core/hle/service/ldr_ro/ldr_ro.cpp b/src/core/hle/service/ldr_ro/ldr_ro.cpp index 5fd3f36b..a8e6a441 100644 --- a/src/core/hle/service/ldr_ro/ldr_ro.cpp +++ b/src/core/hle/service/ldr_ro/ldr_ro.cpp @@ -87,19 +87,19 @@ void RO::Initialize(Kernel::HLERequestContext& ctx) { return; } - if (crs_buffer_ptr & Memory::CITRA_PAGE_MASK) { + if (crs_buffer_ptr & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRS original address is not aligned"); rb.Push(ERROR_MISALIGNED_ADDRESS); return; } - if (crs_address & Memory::CITRA_PAGE_MASK) { + if (crs_address & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRS mapping address is not aligned"); rb.Push(ERROR_MISALIGNED_ADDRESS); return; } - if (crs_size & Memory::CITRA_PAGE_MASK) { + if (crs_size & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRS size is not aligned"); rb.Push(ERROR_MISALIGNED_SIZE); return; @@ -207,21 +207,21 @@ void RO::LoadCRO(Kernel::HLERequestContext& ctx, bool link_on_load_bug_fix) { return; } - if (cro_buffer_ptr & Memory::CITRA_PAGE_MASK) { + if (cro_buffer_ptr & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRO original address is not aligned"); rb.Push(ERROR_MISALIGNED_ADDRESS); rb.Push(0); return; } - if (cro_address & Memory::CITRA_PAGE_MASK) { + if (cro_address & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRO mapping address is not aligned"); rb.Push(ERROR_MISALIGNED_ADDRESS); rb.Push(0); return; } - if (cro_size & Memory::CITRA_PAGE_MASK) { + if (cro_size & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRO size is not aligned"); rb.Push(ERROR_MISALIGNED_SIZE); rb.Push(0); @@ -354,7 +354,7 @@ void RO::UnloadCRO(Kernel::HLERequestContext& ctx) { return; } - if (cro_address & Memory::CITRA_PAGE_MASK) { + if (cro_address & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRO address is not aligned"); rb.Push(ERROR_MISALIGNED_ADDRESS); return; @@ -421,7 +421,7 @@ void RO::LinkCRO(Kernel::HLERequestContext& ctx) { return; } - if (cro_address & Memory::CITRA_PAGE_MASK) { + if (cro_address & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRO address is not aligned"); rb.Push(ERROR_MISALIGNED_ADDRESS); return; @@ -461,7 +461,7 @@ void RO::UnlinkCRO(Kernel::HLERequestContext& ctx) { return; } - if (cro_address & Memory::CITRA_PAGE_MASK) { + if (cro_address & Memory::LUCINA3DS_PAGE_MASK) { LOG_ERROR(Service_LDR, "CRO address is not aligned"); rb.Push(ERROR_MISALIGNED_ADDRESS); return; diff --git a/src/core/loader/artic.cpp b/src/core/loader/artic.cpp index 80365ed6..028f841c 100644 --- a/src/core/loader/artic.cpp +++ b/src/core/loader/artic.cpp @@ -128,13 +128,13 @@ ResultStatus Apploader_Artic::LoadExec(std::shared_ptr& process codeset->CodeSegment().offset = 0; codeset->CodeSegment().addr = program_exheader.codeset_info.text.address; codeset->CodeSegment().size = - program_exheader.codeset_info.text.num_max_pages * Memory::CITRA_PAGE_SIZE; + program_exheader.codeset_info.text.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE; codeset->RODataSegment().offset = codeset->CodeSegment().offset + codeset->CodeSegment().size; codeset->RODataSegment().addr = program_exheader.codeset_info.ro.address; codeset->RODataSegment().size = - program_exheader.codeset_info.ro.num_max_pages * Memory::CITRA_PAGE_SIZE; + program_exheader.codeset_info.ro.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE; // TODO(yuriks): Not sure if the bss size is added to the page-aligned .data size or just // to the regular size. Playing it safe for now. @@ -145,7 +145,7 @@ ResultStatus Apploader_Artic::LoadExec(std::shared_ptr& process codeset->RODataSegment().offset + codeset->RODataSegment().size; codeset->DataSegment().addr = program_exheader.codeset_info.data.address; codeset->DataSegment().size = - program_exheader.codeset_info.data.num_max_pages * Memory::CITRA_PAGE_SIZE + + program_exheader.codeset_info.data.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE + bss_page_size; // Apply patches now that the entire codeset (including .bss) has been allocated @@ -387,9 +387,9 @@ ResultStatus Apploader_Artic::ReadCode(std::vector& buffer) { if (!client_connected) return ResultStatus::ErrorArtic; - size_t code_size = program_exheader.codeset_info.text.num_max_pages * Memory::CITRA_PAGE_SIZE; - code_size += program_exheader.codeset_info.ro.num_max_pages * Memory::CITRA_PAGE_SIZE; - code_size += program_exheader.codeset_info.data.num_max_pages * Memory::CITRA_PAGE_SIZE; + size_t code_size = program_exheader.codeset_info.text.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE; + code_size += program_exheader.codeset_info.ro.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE; + code_size += program_exheader.codeset_info.data.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE; size_t read_amount = 0; buffer.clear(); diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index ca0a285d..3d1e345a 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -120,13 +120,13 @@ ResultStatus AppLoader_NCCH::LoadExec(std::shared_ptr& process) codeset->CodeSegment().offset = 0; codeset->CodeSegment().addr = overlay_ncch->exheader_header.codeset_info.text.address; codeset->CodeSegment().size = - overlay_ncch->exheader_header.codeset_info.text.num_max_pages * Memory::CITRA_PAGE_SIZE; + overlay_ncch->exheader_header.codeset_info.text.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE; codeset->RODataSegment().offset = codeset->CodeSegment().offset + codeset->CodeSegment().size; codeset->RODataSegment().addr = overlay_ncch->exheader_header.codeset_info.ro.address; codeset->RODataSegment().size = - overlay_ncch->exheader_header.codeset_info.ro.num_max_pages * Memory::CITRA_PAGE_SIZE; + overlay_ncch->exheader_header.codeset_info.ro.num_max_pages * Memory::LUCINA3DS_PAGE_SIZE; // TODO(yuriks): Not sure if the bss size is added to the page-aligned .data size or just // to the regular size. Playing it safe for now. @@ -138,7 +138,7 @@ ResultStatus AppLoader_NCCH::LoadExec(std::shared_ptr& process) codeset->DataSegment().addr = overlay_ncch->exheader_header.codeset_info.data.address; codeset->DataSegment().size = overlay_ncch->exheader_header.codeset_info.data.num_max_pages * - Memory::CITRA_PAGE_SIZE + + Memory::LUCINA3DS_PAGE_SIZE + bss_page_size; // Apply patches now that the entire codeset (including .bss) has been allocated diff --git a/src/core/memory.cpp b/src/core/memory.cpp index eb4ab42d..daa9af4e 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -54,24 +54,24 @@ public: private: bool* At(VAddr addr) { if (addr >= VRAM_VADDR && addr < VRAM_VADDR_END) { - return &vram[(addr - VRAM_VADDR) / CITRA_PAGE_SIZE]; + return &vram[(addr - VRAM_VADDR) / LUCINA3DS_PAGE_SIZE]; } if (addr >= LINEAR_HEAP_VADDR && addr < LINEAR_HEAP_VADDR_END) { - return &linear_heap[(addr - LINEAR_HEAP_VADDR) / CITRA_PAGE_SIZE]; + return &linear_heap[(addr - LINEAR_HEAP_VADDR) / LUCINA3DS_PAGE_SIZE]; } if (addr >= NEW_LINEAR_HEAP_VADDR && addr < NEW_LINEAR_HEAP_VADDR_END) { - return &new_linear_heap[(addr - NEW_LINEAR_HEAP_VADDR) / CITRA_PAGE_SIZE]; + return &new_linear_heap[(addr - NEW_LINEAR_HEAP_VADDR) / LUCINA3DS_PAGE_SIZE]; } if (addr >= PLUGIN_3GX_FB_VADDR && addr < PLUGIN_3GX_FB_VADDR_END) { - return &plugin_fb[(addr - PLUGIN_3GX_FB_VADDR) / CITRA_PAGE_SIZE]; + return &plugin_fb[(addr - PLUGIN_3GX_FB_VADDR) / LUCINA3DS_PAGE_SIZE]; } return nullptr; } - std::array vram{}; - std::array linear_heap{}; - std::array new_linear_heap{}; - std::array plugin_fb{}; + std::array vram{}; + std::array linear_heap{}; + std::array new_linear_heap{}; + std::array plugin_fb{}; static_assert(sizeof(bool) == 1); friend class boost::serialization::access; @@ -161,13 +161,13 @@ public: auto& page_table = *process.vm_manager.page_table; std::size_t remaining_size = size; - std::size_t page_index = src_addr >> CITRA_PAGE_BITS; - std::size_t page_offset = src_addr & CITRA_PAGE_MASK; + std::size_t page_index = src_addr >> LUCINA3DS_PAGE_BITS; + std::size_t page_offset = src_addr & LUCINA3DS_PAGE_MASK; while (remaining_size > 0) { - const std::size_t copy_amount = std::min(CITRA_PAGE_SIZE - page_offset, remaining_size); + const std::size_t copy_amount = std::min(LUCINA3DS_PAGE_SIZE - page_offset, remaining_size); const VAddr current_vaddr = - static_cast((page_index << CITRA_PAGE_BITS) + page_offset); + static_cast((page_index << LUCINA3DS_PAGE_BITS) + page_offset); switch (page_table.attributes[page_index]) { case PageType::Unmapped: { @@ -210,13 +210,13 @@ public: const void* src_buffer, const std::size_t size) { auto& page_table = *process.vm_manager.page_table; std::size_t remaining_size = size; - std::size_t page_index = dest_addr >> CITRA_PAGE_BITS; - std::size_t page_offset = dest_addr & CITRA_PAGE_MASK; + std::size_t page_index = dest_addr >> LUCINA3DS_PAGE_BITS; + std::size_t page_offset = dest_addr & LUCINA3DS_PAGE_MASK; while (remaining_size > 0) { - const std::size_t copy_amount = std::min(CITRA_PAGE_SIZE - page_offset, remaining_size); + const std::size_t copy_amount = std::min(LUCINA3DS_PAGE_SIZE - page_offset, remaining_size); const VAddr current_vaddr = - static_cast((page_index << CITRA_PAGE_BITS) + page_offset); + static_cast((page_index << LUCINA3DS_PAGE_BITS) + page_offset); switch (page_table.attributes[page_index]) { case PageType::Unmapped: { @@ -393,10 +393,10 @@ void MemorySystem::RasterizerFlushVirtualRegion(VAddr start, u32 size, FlushMode void MemorySystem::MapPages(PageTable& page_table, u32 base, u32 size, MemoryRef memory, PageType type) { LOG_DEBUG(HW_Memory, "Mapping {} onto {:08X}-{:08X}", (void*)memory.GetPtr(), - base * CITRA_PAGE_SIZE, (base + size) * CITRA_PAGE_SIZE); + base * LUCINA3DS_PAGE_SIZE, (base + size) * LUCINA3DS_PAGE_SIZE); if (impl->system.IsPoweredOn()) { - RasterizerFlushVirtualRegion(base << CITRA_PAGE_BITS, size * CITRA_PAGE_SIZE, + RasterizerFlushVirtualRegion(base << LUCINA3DS_PAGE_BITS, size * LUCINA3DS_PAGE_SIZE, FlushMode::FlushAndInvalidate); } @@ -408,27 +408,27 @@ void MemorySystem::MapPages(PageTable& page_table, u32 base, u32 size, MemoryRef page_table.pointers[base] = memory; // If the memory to map is already rasterizer-cached, mark the page - if (type == PageType::Memory && impl->cache_marker.IsCached(base * CITRA_PAGE_SIZE)) { + if (type == PageType::Memory && impl->cache_marker.IsCached(base * LUCINA3DS_PAGE_SIZE)) { page_table.attributes[base] = PageType::RasterizerCachedMemory; page_table.pointers[base] = nullptr; } base += 1; - if (memory != nullptr && memory.GetSize() > CITRA_PAGE_SIZE) - memory += CITRA_PAGE_SIZE; + if (memory != nullptr && memory.GetSize() > LUCINA3DS_PAGE_SIZE) + memory += LUCINA3DS_PAGE_SIZE; } } void MemorySystem::MapMemoryRegion(PageTable& page_table, VAddr base, u32 size, MemoryRef target) { - ASSERT_MSG((size & CITRA_PAGE_MASK) == 0, "non-page aligned size: {:08X}", size); - ASSERT_MSG((base & CITRA_PAGE_MASK) == 0, "non-page aligned base: {:08X}", base); - MapPages(page_table, base / CITRA_PAGE_SIZE, size / CITRA_PAGE_SIZE, target, PageType::Memory); + ASSERT_MSG((size & LUCINA3DS_PAGE_MASK) == 0, "non-page aligned size: {:08X}", size); + ASSERT_MSG((base & LUCINA3DS_PAGE_MASK) == 0, "non-page aligned base: {:08X}", base); + MapPages(page_table, base / LUCINA3DS_PAGE_SIZE, size / LUCINA3DS_PAGE_SIZE, target, PageType::Memory); } void MemorySystem::UnmapRegion(PageTable& page_table, VAddr base, u32 size) { - ASSERT_MSG((size & CITRA_PAGE_MASK) == 0, "non-page aligned size: {:08X}", size); - ASSERT_MSG((base & CITRA_PAGE_MASK) == 0, "non-page aligned base: {:08X}", base); - MapPages(page_table, base / CITRA_PAGE_SIZE, size / CITRA_PAGE_SIZE, nullptr, + ASSERT_MSG((size & LUCINA3DS_PAGE_MASK) == 0, "non-page aligned size: {:08X}", size); + ASSERT_MSG((base & LUCINA3DS_PAGE_MASK) == 0, "non-page aligned base: {:08X}", base); + MapPages(page_table, base / LUCINA3DS_PAGE_SIZE, size / LUCINA3DS_PAGE_SIZE, nullptr, PageType::Unmapped); } @@ -449,11 +449,11 @@ void MemorySystem::UnregisterPageTable(std::shared_ptr page_table) { template T MemorySystem::Read(const VAddr vaddr) { - const u8* page_pointer = impl->current_page_table->pointers[vaddr >> CITRA_PAGE_BITS]; + const u8* page_pointer = impl->current_page_table->pointers[vaddr >> LUCINA3DS_PAGE_BITS]; if (page_pointer) { // NOTE: Avoid adding any extra logic to this fast-path block T value; - std::memcpy(&value, &page_pointer[vaddr & CITRA_PAGE_MASK], sizeof(T)); + std::memcpy(&value, &page_pointer[vaddr & LUCINA3DS_PAGE_MASK], sizeof(T)); return value; } @@ -472,7 +472,7 @@ T MemorySystem::Read(const VAddr vaddr) { } } - PageType type = impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS]; + PageType type = impl->current_page_table->attributes[vaddr >> LUCINA3DS_PAGE_BITS]; switch (type) { case PageType::Unmapped: LOG_ERROR(HW_Memory, "unmapped Read{} @ 0x{:08X} at PC 0x{:08X}", sizeof(T) * 8, vaddr, @@ -497,10 +497,10 @@ T MemorySystem::Read(const VAddr vaddr) { template void MemorySystem::Write(const VAddr vaddr, const T data) { - u8* page_pointer = impl->current_page_table->pointers[vaddr >> CITRA_PAGE_BITS]; + u8* page_pointer = impl->current_page_table->pointers[vaddr >> LUCINA3DS_PAGE_BITS]; if (page_pointer) { // NOTE: Avoid adding any extra logic to this fast-path block - std::memcpy(&page_pointer[vaddr & CITRA_PAGE_MASK], &data, sizeof(T)); + std::memcpy(&page_pointer[vaddr & LUCINA3DS_PAGE_MASK], &data, sizeof(T)); return; } @@ -521,7 +521,7 @@ void MemorySystem::Write(const VAddr vaddr, const T data) { } } - PageType type = impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS]; + PageType type = impl->current_page_table->attributes[vaddr >> LUCINA3DS_PAGE_BITS]; switch (type) { case PageType::Unmapped: LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}", @@ -542,15 +542,15 @@ void MemorySystem::Write(const VAddr vaddr, const T data) { template bool MemorySystem::WriteExclusive(const VAddr vaddr, const T data, const T expected) { - u8* page_pointer = impl->current_page_table->pointers[vaddr >> CITRA_PAGE_BITS]; + u8* page_pointer = impl->current_page_table->pointers[vaddr >> LUCINA3DS_PAGE_BITS]; if (page_pointer) { const auto volatile_pointer = - reinterpret_cast(&page_pointer[vaddr & CITRA_PAGE_MASK]); + reinterpret_cast(&page_pointer[vaddr & LUCINA3DS_PAGE_MASK]); return Common::AtomicCompareAndSwap(volatile_pointer, data, expected); } - PageType type = impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS]; + PageType type = impl->current_page_table->attributes[vaddr >> LUCINA3DS_PAGE_BITS]; switch (type) { case PageType::Unmapped: LOG_ERROR(HW_Memory, "unmapped Write{} 0x{:08X} @ 0x{:08X} at PC 0x{:08X}", @@ -574,12 +574,12 @@ bool MemorySystem::WriteExclusive(const VAddr vaddr, const T data, const T expec bool MemorySystem::IsValidVirtualAddress(const Kernel::Process& process, const VAddr vaddr) { auto& page_table = *process.vm_manager.page_table; - auto page_pointer = page_table.pointers[vaddr >> CITRA_PAGE_BITS]; + auto page_pointer = page_table.pointers[vaddr >> LUCINA3DS_PAGE_BITS]; if (page_pointer) { return true; } - if (page_table.attributes[vaddr >> CITRA_PAGE_BITS] == PageType::RasterizerCachedMemory) { + if (page_table.attributes[vaddr >> LUCINA3DS_PAGE_BITS] == PageType::RasterizerCachedMemory) { return true; } @@ -591,12 +591,12 @@ bool MemorySystem::IsValidPhysicalAddress(const PAddr paddr) const { } u8* MemorySystem::GetPointer(const VAddr vaddr) { - u8* page_pointer = impl->current_page_table->pointers[vaddr >> CITRA_PAGE_BITS]; + u8* page_pointer = impl->current_page_table->pointers[vaddr >> LUCINA3DS_PAGE_BITS]; if (page_pointer) { - return page_pointer + (vaddr & CITRA_PAGE_MASK); + return page_pointer + (vaddr & LUCINA3DS_PAGE_MASK); } - if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == + if (impl->current_page_table->attributes[vaddr >> LUCINA3DS_PAGE_BITS] == PageType::RasterizerCachedMemory) { return GetPointerForRasterizerCache(vaddr); } @@ -606,12 +606,12 @@ u8* MemorySystem::GetPointer(const VAddr vaddr) { } const u8* MemorySystem::GetPointer(const VAddr vaddr) const { - const u8* page_pointer = impl->current_page_table->pointers[vaddr >> CITRA_PAGE_BITS]; + const u8* page_pointer = impl->current_page_table->pointers[vaddr >> LUCINA3DS_PAGE_BITS]; if (page_pointer) { - return page_pointer + (vaddr & CITRA_PAGE_MASK); + return page_pointer + (vaddr & LUCINA3DS_PAGE_MASK); } - if (impl->current_page_table->attributes[vaddr >> CITRA_PAGE_BITS] == + if (impl->current_page_table->attributes[vaddr >> LUCINA3DS_PAGE_BITS] == PageType::RasterizerCachedMemory) { return GetPointerForRasterizerCache(vaddr); } @@ -720,14 +720,14 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached return; } - u32 num_pages = ((start + size - 1) >> CITRA_PAGE_BITS) - (start >> CITRA_PAGE_BITS) + 1; + u32 num_pages = ((start + size - 1) >> LUCINA3DS_PAGE_BITS) - (start >> LUCINA3DS_PAGE_BITS) + 1; PAddr paddr = start; - for (unsigned i = 0; i < num_pages; ++i, paddr += CITRA_PAGE_SIZE) { + for (unsigned i = 0; i < num_pages; ++i, paddr += LUCINA3DS_PAGE_SIZE) { for (VAddr vaddr : PhysicalToVirtualAddressForRasterizer(paddr)) { impl->cache_marker.Mark(vaddr, cached); for (auto& page_table : impl->page_table_list) { - PageType& page_type = page_table->attributes[vaddr >> CITRA_PAGE_BITS]; + PageType& page_type = page_table->attributes[vaddr >> LUCINA3DS_PAGE_BITS]; if (cached) { // Switch page type to cached if now cached @@ -738,7 +738,7 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached break; case PageType::Memory: page_type = PageType::RasterizerCachedMemory; - page_table->pointers[vaddr >> CITRA_PAGE_BITS] = nullptr; + page_table->pointers[vaddr >> LUCINA3DS_PAGE_BITS] = nullptr; break; default: UNREACHABLE(); @@ -752,8 +752,8 @@ void MemorySystem::RasterizerMarkRegionCached(PAddr start, u32 size, bool cached break; case PageType::RasterizerCachedMemory: { page_type = PageType::Memory; - page_table->pointers[vaddr >> CITRA_PAGE_BITS] = - GetPointerForRasterizerCache(vaddr & ~CITRA_PAGE_MASK); + page_table->pointers[vaddr >> LUCINA3DS_PAGE_BITS] = + GetPointerForRasterizerCache(vaddr & ~LUCINA3DS_PAGE_MASK); break; } default: @@ -838,13 +838,13 @@ void MemorySystem::ZeroBlock(const Kernel::Process& process, const VAddr dest_ad const std::size_t size) { auto& page_table = *process.vm_manager.page_table; std::size_t remaining_size = size; - std::size_t page_index = dest_addr >> CITRA_PAGE_BITS; - std::size_t page_offset = dest_addr & CITRA_PAGE_MASK; + std::size_t page_index = dest_addr >> LUCINA3DS_PAGE_BITS; + std::size_t page_offset = dest_addr & LUCINA3DS_PAGE_MASK; while (remaining_size > 0) { - const std::size_t copy_amount = std::min(CITRA_PAGE_SIZE - page_offset, remaining_size); + const std::size_t copy_amount = std::min(LUCINA3DS_PAGE_SIZE - page_offset, remaining_size); const VAddr current_vaddr = - static_cast((page_index << CITRA_PAGE_BITS) + page_offset); + static_cast((page_index << LUCINA3DS_PAGE_BITS) + page_offset); switch (page_table.attributes[page_index]) { case PageType::Unmapped: { @@ -887,13 +887,13 @@ void MemorySystem::CopyBlock(const Kernel::Process& dest_process, std::size_t size) { auto& page_table = *src_process.vm_manager.page_table; std::size_t remaining_size = size; - std::size_t page_index = src_addr >> CITRA_PAGE_BITS; - std::size_t page_offset = src_addr & CITRA_PAGE_MASK; + std::size_t page_index = src_addr >> LUCINA3DS_PAGE_BITS; + std::size_t page_offset = src_addr & LUCINA3DS_PAGE_MASK; while (remaining_size > 0) { - const std::size_t copy_amount = std::min(CITRA_PAGE_SIZE - page_offset, remaining_size); + const std::size_t copy_amount = std::min(LUCINA3DS_PAGE_SIZE - page_offset, remaining_size); const VAddr current_vaddr = - static_cast((page_index << CITRA_PAGE_BITS) + page_offset); + static_cast((page_index << LUCINA3DS_PAGE_BITS) + page_offset); switch (page_table.attributes[page_index]) { case PageType::Unmapped: { diff --git a/src/core/memory.h b/src/core/memory.h index 3aaf09bb..07cca064 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -29,10 +29,10 @@ namespace Memory { * Page size used by the ARM architecture. This is the smallest granularity with which memory can * be mapped. */ -constexpr u32 CITRA_PAGE_SIZE = 0x1000; -constexpr u32 CITRA_PAGE_MASK = CITRA_PAGE_SIZE - 1; -constexpr int CITRA_PAGE_BITS = 12; -constexpr std::size_t PAGE_TABLE_NUM_ENTRIES = 1 << (32 - CITRA_PAGE_BITS); +constexpr u32 LUCINA3DS_PAGE_SIZE = 0x1000; +constexpr u32 LUCINA3DS_PAGE_MASK = LUCINA3DS_PAGE_SIZE - 1; +constexpr int LUCINA3DS_PAGE_BITS = 12; +constexpr std::size_t PAGE_TABLE_NUM_ENTRIES = 1 << (32 - LUCINA3DS_PAGE_BITS); enum class PageType { /// Page is unmapped and should cause an access error. diff --git a/src/dedicated_room/CMakeLists.txt b/src/dedicated_room/CMakeLists.txt index 8209f47e..3f7a8e70 100644 --- a/src/dedicated_room/CMakeLists.txt +++ b/src/dedicated_room/CMakeLists.txt @@ -1,34 +1,34 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules) -add_executable(citra-room +add_executable(lucina3ds-room precompiled_headers.h - citra-room.cpp - citra-room.rc + lucina3ds-room.cpp + lucina3ds-room.rc ) -create_target_directory_groups(citra-room) +create_target_directory_groups(lucina3ds-room) -target_link_libraries(citra-room PRIVATE citra_common network) +target_link_libraries(lucina3ds-room PRIVATE lucina3ds_common network) if (ENABLE_WEB_SERVICE) - target_link_libraries(citra-room PRIVATE web_service) + target_link_libraries(lucina3ds-room PRIVATE web_service) endif() -target_link_libraries(citra-room PRIVATE cryptopp) +target_link_libraries(lucina3ds-room PRIVATE cryptopp) if (MSVC) - target_link_libraries(citra-room PRIVATE getopt) + target_link_libraries(lucina3ds-room PRIVATE getopt) endif() -target_link_libraries(citra-room PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) +target_link_libraries(lucina3ds-room PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) if(UNIX AND NOT APPLE) - install(TARGETS citra-room RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") + install(TARGETS lucina3ds-room RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") endif() -if (CITRA_USE_PRECOMPILED_HEADERS) - target_precompile_headers(citra-room PRIVATE precompiled_headers.h) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) + target_precompile_headers(lucina3ds-room PRIVATE precompiled_headers.h) endif() # Bundle in-place on MSVC so dependencies can be resolved by builds. if (MSVC) include(BundleTarget) - bundle_target_in_place(citra-room) + bundle_target_in_place(lucina3ds-room) endif() diff --git a/src/dedicated_room/citra-room.cpp b/src/dedicated_room/lucina3ds-room.cpp similarity index 90% rename from src/dedicated_room/citra-room.cpp rename to src/dedicated_room/lucina3ds-room.cpp index 3d5f32a9..e213b7ff 100644 --- a/src/dedicated_room/citra-room.cpp +++ b/src/dedicated_room/lucina3ds-room.cpp @@ -56,18 +56,18 @@ static void PrintHelp(const char* argv0) { "--web-api-url Citra Web API url\n" "--ban-list-file The file for storing the room ban list\n" "--log-file The file for storing the room log\n" - "--enable-citra-mods Allow Citra Community Moderators to moderate on your room\n" + "--enable-lucina3ds-mods Allow Citra Community Moderators to moderate on your room\n" "-h, --help Display this help and exit\n" "-v, --version Output version information and exit\n"; } static void PrintVersion() { - std::cout << "Citra dedicated room " << Common::g_scm_branch << " " << Common::g_scm_desc + std::cout << "Lucina3DS dedicated room " << Common::g_scm_branch << " " << Common::g_scm_desc << " Libnetwork: " << Network::network_version << std::endl; } -/// The magic text at the beginning of a citra-room ban list file. -static constexpr char BanListMagic[] = "CitraRoom-BanList-1"; +/// The magic text at the beginning of a lucina3ds-room ban list file. +static constexpr char BanListMagic[] = "Lucina3DSRoom-BanList-1"; static constexpr char token_delimiter{':'}; @@ -169,11 +169,11 @@ int main(int argc, char** argv) { std::string token; std::string web_api_url; std::string ban_list_file; - std::string log_file = "citra-room.log"; + std::string log_file = "lucina3ds-room.log"; u64 preferred_game_id = 0; u16 port = Network::DefaultRoomPort; u32 max_members = 16; - bool enable_citra_mods = false; + bool enable_lucina3ds_mods = false; static struct option long_options[] = { {"room-name", required_argument, 0, 'n'}, @@ -188,7 +188,7 @@ int main(int argc, char** argv) { {"web-api-url", required_argument, 0, 'a'}, {"ban-list-file", required_argument, 0, 'b'}, {"log-file", required_argument, 0, 'l'}, - {"enable-citra-mods", no_argument, 0, 'e'}, + {"enable-lucina3ds-mods", no_argument, 0, 'e'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, {0, 0, 0, 0}, @@ -235,7 +235,7 @@ int main(int argc, char** argv) { log_file.assign(optarg); break; case 'e': - enable_citra_mods = true; + enable_lucina3ds_mods = true; break; case 'h': PrintHelp(argv[0]); @@ -289,19 +289,19 @@ int main(int argc, char** argv) { if (username.empty()) { std::cout << "Hosting a public room\n\n"; NetSettings::values.web_api_url = web_api_url; - NetSettings::values.citra_username = UsernameFromDisplayToken(token); - username = NetSettings::values.citra_username; - NetSettings::values.citra_token = TokenFromDisplayToken(token); + NetSettings::values.lucina3ds_username = UsernameFromDisplayToken(token); + username = NetSettings::values.lucina3ds_username; + NetSettings::values.lucina3ds_token = TokenFromDisplayToken(token); } else { std::cout << "Hosting a public room\n\n"; NetSettings::values.web_api_url = web_api_url; - NetSettings::values.citra_username = username; - NetSettings::values.citra_token = token; + NetSettings::values.lucina3ds_username = username; + NetSettings::values.lucina3ds_token = token; } } - if (!announce && enable_citra_mods) { - enable_citra_mods = false; - std::cout << "Can not enable Citra Moderators for private rooms\n\n"; + if (!announce && enable_lucina3ds_mods) { + enable_lucina3ds_mods = false; + std::cout << "Can not enable lucina3ds Moderators for private rooms\n\n"; } InitializeLogging(log_file); @@ -319,7 +319,7 @@ int main(int argc, char** argv) { std::make_unique(NetSettings::values.web_api_url); #else std::cout - << "Citra Web Services is not available with this build: validation is disabled.\n\n"; + << "Web Services is not available with this build: validation is disabled.\n\n"; verify_backend = std::make_unique(); #endif } else { @@ -330,7 +330,7 @@ int main(int argc, char** argv) { if (std::shared_ptr room = Network::GetRoom().lock()) { if (!room->Create(room_name, room_description, "", port, password, max_members, username, preferred_game, preferred_game_id, std::move(verify_backend), ban_list, - enable_citra_mods)) { + enable_lucina3ds_mods)) { std::cout << "Failed to create room: \n\n"; return -1; } diff --git a/src/citra/citra.rc b/src/dedicated_room/lucina3ds-room.rc similarity index 67% rename from src/citra/citra.rc rename to src/dedicated_room/lucina3ds-room.rc index 2c6bcd58..aa01ab5b 100644 --- a/src/citra/citra.rc +++ b/src/dedicated_room/lucina3ds-room.rc @@ -6,7 +6,7 @@ // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. -CITRA_ICON ICON "../../dist/citra.ico" +CITRA_ICON ICON "../../dist/lucina3ds.ico" ///////////////////////////////////////////////////////////////////////////// @@ -14,4 +14,4 @@ CITRA_ICON ICON "../../dist/citra.ico" // RT_MANIFEST // -0 RT_MANIFEST "../../dist/citra.manifest" +0 RT_MANIFEST "../../dist/lucina3ds.manifest" diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 70d36cbf..d27b6f5f 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -42,8 +42,8 @@ if(ENABLE_LIBUSB) endif() create_target_directory_groups(input_common) -target_link_libraries(input_common PUBLIC citra_core PRIVATE citra_common ${Boost_LIBRARIES}) +target_link_libraries(input_common PUBLIC lucina3ds_core PRIVATE lucina3ds_common ${Boost_LIBRARIES}) -if (CITRA_USE_PRECOMPILED_HEADERS) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) target_precompile_headers(input_common PRIVATE precompiled_headers.h) endif() diff --git a/src/citra/CMakeLists.txt b/src/lucina3ds/CMakeLists.txt similarity index 50% rename from src/citra/CMakeLists.txt rename to src/lucina3ds/CMakeLists.txt index 42de087c..1b9fd8be 100644 --- a/src/citra/CMakeLists.txt +++ b/src/lucina3ds/CMakeLists.txt @@ -1,8 +1,8 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules) -add_executable(citra - citra.cpp - citra.rc +add_executable(lucina3ds + lucina3ds.cpp + lucina3ds.rc config.cpp config.h default_ini.h @@ -13,47 +13,47 @@ add_executable(citra ) if (ENABLE_SOFTWARE_RENDERER) - target_sources(citra PRIVATE + target_sources(lucina3ds PRIVATE emu_window/emu_window_sdl2_sw.cpp emu_window/emu_window_sdl2_sw.h ) endif() if (ENABLE_OPENGL) - target_sources(citra PRIVATE + target_sources(lucina3ds PRIVATE emu_window/emu_window_sdl2_gl.cpp emu_window/emu_window_sdl2_gl.h ) endif() if (ENABLE_VULKAN) - target_sources(citra PRIVATE + target_sources(lucina3ds PRIVATE emu_window/emu_window_sdl2_vk.cpp emu_window/emu_window_sdl2_vk.h ) endif() -create_target_directory_groups(citra) +create_target_directory_groups(lucina3ds) -target_link_libraries(citra PRIVATE citra_common citra_core input_common network) -target_link_libraries(citra PRIVATE inih) +target_link_libraries(lucina3ds PRIVATE lucina3ds_common lucina3ds_core input_common network) +target_link_libraries(lucina3ds PRIVATE inih) if (MSVC) - target_link_libraries(citra PRIVATE getopt) + target_link_libraries(lucina3ds PRIVATE getopt) endif() -target_link_libraries(citra PRIVATE ${PLATFORM_LIBRARIES} SDL2::SDL2 Threads::Threads) +target_link_libraries(lucina3ds PRIVATE ${PLATFORM_LIBRARIES} SDL2::SDL2 Threads::Threads) if (ENABLE_OPENGL) - target_link_libraries(citra PRIVATE glad) + target_link_libraries(lucina3ds PRIVATE glad) endif() if(UNIX AND NOT APPLE) - install(TARGETS citra RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") + install(TARGETS lucina3ds RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") endif() -if (CITRA_USE_PRECOMPILED_HEADERS) - target_precompile_headers(citra PRIVATE precompiled_headers.h) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) + target_precompile_headers(lucina3ds PRIVATE precompiled_headers.h) endif() # Bundle in-place on MSVC so dependencies can be resolved by builds. if (MSVC) include(BundleTarget) - bundle_target_in_place(citra) + bundle_target_in_place(lucina3ds) endif() diff --git a/src/citra/config.cpp b/src/lucina3ds/config.cpp similarity index 98% rename from src/citra/config.cpp rename to src/lucina3ds/config.cpp index f4806f23..4113be04 100644 --- a/src/citra/config.cpp +++ b/src/lucina3ds/config.cpp @@ -8,8 +8,8 @@ #include #include #include -#include "citra/config.h" -#include "citra/default_ini.h" +#include "lucina3ds/config.h" +#include "lucina3ds/default_ini.h" #include "common/file_util.h" #include "common/logging/backend.h" #include "common/logging/log.h" @@ -334,8 +334,8 @@ void Config::ReadValues() { // Web Service NetSettings::values.web_api_url = sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org"); - NetSettings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", ""); - NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); + NetSettings::values.lucina3ds_username = sdl2_config->GetString("WebService", "citra_username", ""); + NetSettings::values.lucina3ds_token = sdl2_config->GetString("WebService", "citra_token", ""); // Video Dumping Settings::values.output_format = diff --git a/src/citra/config.h b/src/lucina3ds/config.h similarity index 100% rename from src/citra/config.h rename to src/lucina3ds/config.h diff --git a/src/citra/default_ini.h b/src/lucina3ds/default_ini.h similarity index 100% rename from src/citra/default_ini.h rename to src/lucina3ds/default_ini.h diff --git a/src/citra/emu_window/emu_window_sdl2.cpp b/src/lucina3ds/emu_window/emu_window_sdl2.cpp similarity index 98% rename from src/citra/emu_window/emu_window_sdl2.cpp rename to src/lucina3ds/emu_window/emu_window_sdl2.cpp index 698dcc3d..8f243578 100644 --- a/src/citra/emu_window/emu_window_sdl2.cpp +++ b/src/lucina3ds/emu_window/emu_window_sdl2.cpp @@ -7,7 +7,7 @@ #include #define SDL_MAIN_HANDLED #include -#include "citra/emu_window/emu_window_sdl2.h" +#include "lucina3ds/emu_window/emu_window_sdl2.h" #include "common/logging/log.h" #include "common/scm_rev.h" #include "core/core.h" @@ -243,7 +243,7 @@ void EmuWindow_SDL2::UpdateFramerateCounter() { if (current_time > last_time + 2000) { const auto results = system.GetAndResetPerfStats(); const auto title = - fmt::format("Citra {} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, + fmt::format("Lucina3DS {} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.game_fps, results.emulation_speed * 100.0f); SDL_SetWindowTitle(render_window, title.c_str()); diff --git a/src/citra/emu_window/emu_window_sdl2.h b/src/lucina3ds/emu_window/emu_window_sdl2.h similarity index 100% rename from src/citra/emu_window/emu_window_sdl2.h rename to src/lucina3ds/emu_window/emu_window_sdl2.h diff --git a/src/citra/emu_window/emu_window_sdl2_gl.cpp b/src/lucina3ds/emu_window/emu_window_sdl2_gl.cpp similarity index 97% rename from src/citra/emu_window/emu_window_sdl2_gl.cpp rename to src/lucina3ds/emu_window/emu_window_sdl2_gl.cpp index 09a7f599..b21b7e23 100644 --- a/src/citra/emu_window/emu_window_sdl2_gl.cpp +++ b/src/lucina3ds/emu_window/emu_window_sdl2_gl.cpp @@ -8,7 +8,7 @@ #define SDL_MAIN_HANDLED #include #include -#include "citra/emu_window/emu_window_sdl2_gl.h" +#include "lucina3ds/emu_window/emu_window_sdl2_gl.h" #include "common/scm_rev.h" #include "common/settings.h" #include "core/core.h" @@ -77,7 +77,7 @@ EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(Core::System& system_, bool fullscreen, boo SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); } - std::string window_title = fmt::format("Citra {} | {}-{}", Common::g_build_fullname, + std::string window_title = fmt::format("Lucina3DS | {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); // First, try to create a context with the requested type. diff --git a/src/citra/emu_window/emu_window_sdl2_gl.h b/src/lucina3ds/emu_window/emu_window_sdl2_gl.h similarity index 95% rename from src/citra/emu_window/emu_window_sdl2_gl.h rename to src/lucina3ds/emu_window/emu_window_sdl2_gl.h index 4a4d7060..79c7369c 100644 --- a/src/citra/emu_window/emu_window_sdl2_gl.h +++ b/src/lucina3ds/emu_window/emu_window_sdl2_gl.h @@ -5,7 +5,7 @@ #pragma once #include -#include "citra/emu_window/emu_window_sdl2.h" +#include "lucina3ds/emu_window/emu_window_sdl2.h" struct SDL_Window; diff --git a/src/citra/emu_window/emu_window_sdl2_sw.cpp b/src/lucina3ds/emu_window/emu_window_sdl2_sw.cpp similarity index 96% rename from src/citra/emu_window/emu_window_sdl2_sw.cpp rename to src/lucina3ds/emu_window/emu_window_sdl2_sw.cpp index 3f2cf6db..5b795676 100644 --- a/src/citra/emu_window/emu_window_sdl2_sw.cpp +++ b/src/lucina3ds/emu_window/emu_window_sdl2_sw.cpp @@ -8,7 +8,7 @@ #define SDL_MAIN_HANDLED #include #include -#include "citra/emu_window/emu_window_sdl2_sw.h" +#include "lucina3ds/emu_window/emu_window_sdl2_sw.h" #include "common/scm_rev.h" #include "common/settings.h" #include "core/core.h" @@ -20,7 +20,7 @@ class DummyContext : public Frontend::GraphicsContext {}; EmuWindow_SDL2_SW::EmuWindow_SDL2_SW(Core::System& system_, bool fullscreen, bool is_secondary) : EmuWindow_SDL2{system_, is_secondary}, system{system_} { - std::string window_title = fmt::format("Citra {} | {}-{}", Common::g_build_fullname, + std::string window_title = fmt::format("Lucina3DS | {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); render_window = SDL_CreateWindow(window_title.c_str(), diff --git a/src/citra/emu_window/emu_window_sdl2_sw.h b/src/lucina3ds/emu_window/emu_window_sdl2_sw.h similarity index 94% rename from src/citra/emu_window/emu_window_sdl2_sw.h rename to src/lucina3ds/emu_window/emu_window_sdl2_sw.h index 22fcd3bd..e88bf8bc 100644 --- a/src/citra/emu_window/emu_window_sdl2_sw.h +++ b/src/lucina3ds/emu_window/emu_window_sdl2_sw.h @@ -5,7 +5,7 @@ #pragma once #include -#include "citra/emu_window/emu_window_sdl2.h" +#include "lucina3ds/emu_window/emu_window_sdl2.h" struct SDL_Renderer; struct SDL_Surface; diff --git a/src/citra/emu_window/emu_window_sdl2_vk.cpp b/src/lucina3ds/emu_window/emu_window_sdl2_vk.cpp similarity index 95% rename from src/citra/emu_window/emu_window_sdl2_vk.cpp rename to src/lucina3ds/emu_window/emu_window_sdl2_vk.cpp index b7e46e1a..18a1383e 100644 --- a/src/citra/emu_window/emu_window_sdl2_vk.cpp +++ b/src/lucina3ds/emu_window/emu_window_sdl2_vk.cpp @@ -8,7 +8,7 @@ #include #include #include -#include "citra/emu_window/emu_window_sdl2_vk.h" +#include "lucina3ds/emu_window/emu_window_sdl2_vk.h" #include "common/logging/log.h" #include "common/scm_rev.h" #include "core/frontend/emu_window.h" @@ -17,7 +17,7 @@ class DummyContext : public Frontend::GraphicsContext {}; EmuWindow_SDL2_VK::EmuWindow_SDL2_VK(Core::System& system, bool fullscreen, bool is_secondary) : EmuWindow_SDL2{system, is_secondary} { - const std::string window_title = fmt::format("Citra {} | {}-{}", Common::g_build_fullname, + const std::string window_title = fmt::format("Lucina3DS | {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); render_window = SDL_CreateWindow(window_title.c_str(), diff --git a/src/citra/emu_window/emu_window_sdl2_vk.h b/src/lucina3ds/emu_window/emu_window_sdl2_vk.h similarity index 91% rename from src/citra/emu_window/emu_window_sdl2_vk.h rename to src/lucina3ds/emu_window/emu_window_sdl2_vk.h index be1cd135..316cbdcf 100644 --- a/src/citra/emu_window/emu_window_sdl2_vk.h +++ b/src/lucina3ds/emu_window/emu_window_sdl2_vk.h @@ -5,7 +5,7 @@ #pragma once #include -#include "citra/emu_window/emu_window_sdl2.h" +#include "lucina3ds/emu_window/emu_window_sdl2.h" namespace Frontend { class GraphicsContext; diff --git a/src/citra/citra.cpp b/src/lucina3ds/lucina3ds.cpp similarity index 97% rename from src/citra/citra.cpp rename to src/lucina3ds/lucina3ds.cpp index 52d64f1e..0054b84c 100644 --- a/src/citra/citra.cpp +++ b/src/lucina3ds/lucina3ds.cpp @@ -11,16 +11,16 @@ // This needs to be included before getopt.h because the latter #defines symbols used by it #include "common/microprofile.h" -#include "citra/config.h" -#include "citra/emu_window/emu_window_sdl2.h" +#include "lucina3ds/config.h" +#include "lucina3ds/emu_window/emu_window_sdl2.h" #ifdef ENABLE_OPENGL -#include "citra/emu_window/emu_window_sdl2_gl.h" +#include "lucina3ds/emu_window/emu_window_sdl2_gl.h" #endif #ifdef ENABLE_SOFTWARE_RENDERER -#include "citra/emu_window/emu_window_sdl2_sw.h" +#include "lucina3ds/emu_window/emu_window_sdl2_sw.h" #endif #ifdef ENABLE_VULKAN -#include "citra/emu_window/emu_window_sdl2_vk.h" +#include "lucina3ds/emu_window/emu_window_sdl2_vk.h" #endif #include "common/common_paths.h" #include "common/detached_tasks.h" @@ -83,7 +83,7 @@ static void PrintHelp(const char* argv0) { } static void PrintVersion() { - std::cout << "Citra " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl; + std::cout << "Lucina3DS" << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl; } static void OnStateChanged(const Network::RoomMember::State& state) { @@ -399,7 +399,7 @@ int main(int argc, char** argv) { const auto scope = emu_window->Acquire(); - LOG_INFO(Frontend, "Citra Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); Settings::LogSettings(); @@ -415,7 +415,7 @@ int main(int argc, char** argv) { return -1; case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: LOG_CRITICAL(Frontend, "The game that you are trying to load must be decrypted before " - "being used with Citra. \n\n For more information on dumping and " + "being used with Lucina3DS. \n\n For more information on dumping and " "decrypting games, please refer to: " "https://citra-emu.org/wiki/dumping-game-cartridges/"); return -1; diff --git a/src/dedicated_room/citra-room.rc b/src/lucina3ds/lucina3ds.rc similarity index 67% rename from src/dedicated_room/citra-room.rc rename to src/lucina3ds/lucina3ds.rc index 2c6bcd58..932d0196 100644 --- a/src/dedicated_room/citra-room.rc +++ b/src/lucina3ds/lucina3ds.rc @@ -6,7 +6,7 @@ // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. -CITRA_ICON ICON "../../dist/citra.ico" +LUCI_ICON ICON "../../dist/lucina3ds.ico" ///////////////////////////////////////////////////////////////////////////// @@ -14,4 +14,4 @@ CITRA_ICON ICON "../../dist/citra.ico" // RT_MANIFEST // -0 RT_MANIFEST "../../dist/citra.manifest" +0 RT_MANIFEST "../../dist/lucina3ds.manifest" diff --git a/src/citra/precompiled_headers.h b/src/lucina3ds/precompiled_headers.h similarity index 100% rename from src/citra/precompiled_headers.h rename to src/lucina3ds/precompiled_headers.h diff --git a/src/citra/resource.h b/src/lucina3ds/resource.h similarity index 100% rename from src/citra/resource.h rename to src/lucina3ds/resource.h diff --git a/src/citra_qt/CMakeLists.txt b/src/lucina3ds_qt/CMakeLists.txt similarity index 76% rename from src/citra_qt/CMakeLists.txt rename to src/lucina3ds_qt/CMakeLists.txt index b17a143d..f0ce06c3 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/lucina3ds_qt/CMakeLists.txt @@ -7,7 +7,7 @@ if (POLICY CMP0071) cmake_policy(SET CMP0071 NEW) endif() -add_executable(citra-qt +add_executable(lucina3ds-qt aboutdialog.cpp aboutdialog.h aboutdialog.ui @@ -27,7 +27,7 @@ add_executable(citra-qt camera/qt_camera_base.h camera/qt_multimedia_camera.cpp camera/qt_multimedia_camera.h - citra-qt.rc + lucina3ds-qt.rc compatdb.cpp compatdb.h compatdb.ui @@ -196,35 +196,35 @@ file(GLOB_RECURSE ICONS ${PROJECT_SOURCE_DIR}/dist/icons/*) file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/qt_themes/*) if (ENABLE_QT_UPDATER) - target_sources(citra-qt PRIVATE + target_sources(lucina3ds-qt PRIVATE updater/updater.cpp updater/updater.h updater/updater_p.h ) - target_compile_definitions(citra-qt PUBLIC ENABLE_QT_UPDATER) + target_compile_definitions(lucina3ds-qt PUBLIC ENABLE_QT_UPDATER) endif() if (ENABLE_QT_TRANSLATION) - set(CITRA_QT_LANGUAGES "${PROJECT_SOURCE_DIR}/dist/languages" CACHE PATH "Path to the translation bundle for the Qt frontend") + set(LUCINA3DS_QT_LANGUAGES "${PROJECT_SOURCE_DIR}/dist/languages" CACHE PATH "Path to the translation bundle for the Qt frontend") option(GENERATE_QT_TRANSLATION "Generate en.ts as the translation source file" OFF) # Update source TS file if enabled if (GENERATE_QT_TRANSLATION) - get_target_property(QT_SRCS citra-qt SOURCES) - get_target_property(QT_INCLUDES citra-qt INCLUDE_DIRECTORIES) - qt_add_lupdate(citra-qt TS_FILES ${CITRA_QT_LANGUAGES}/en.ts + get_target_property(QT_SRCS lucina3ds-qt SOURCES) + get_target_property(QT_INCLUDES lucina3ds-qt INCLUDE_DIRECTORIES) + qt_add_lupdate(lucina3ds-qt TS_FILES ${LUCINA3DS_QT_LANGUAGES}/en.ts SOURCES ${QT_SRCS} ${UIS} INCLUDE_DIRECTORIES ${QT_INCLUDES} NO_GLOBAL_TARGET) - add_custom_target(translation ALL DEPENDS citra-qt_lupdate) + add_custom_target(translation ALL DEPENDS lucina3ds-qt_lupdate) endif() # Find all TS files except en.ts - file(GLOB_RECURSE LANGUAGES_TS ${CITRA_QT_LANGUAGES}/*.ts) - list(REMOVE_ITEM LANGUAGES_TS ${CITRA_QT_LANGUAGES}/en.ts) + file(GLOB_RECURSE LANGUAGES_TS ${LUCINA3DS_QT_LANGUAGES}/*.ts) + list(REMOVE_ITEM LANGUAGES_TS ${LUCINA3DS_QT_LANGUAGES}/en.ts) # Compile TS files to QM files - qt_add_lrelease(citra-qt TS_FILES ${LANGUAGES_TS} NO_GLOBAL_TARGET QM_FILES_OUTPUT_VARIABLE LANGUAGES_QM) + qt_add_lrelease(lucina3ds-qt TS_FILES ${LANGUAGES_TS} NO_GLOBAL_TARGET QM_FILES_OUTPUT_VARIABLE LANGUAGES_QM) # Build a QRC file from the QM file list set(LANGUAGES_QRC ${CMAKE_CURRENT_BINARY_DIR}/languages.qrc) @@ -241,7 +241,7 @@ else() set(LANGUAGES) endif() -target_sources(citra-qt +target_sources(lucina3ds-qt PRIVATE ${COMPAT_LIST} ${ICONS} @@ -252,28 +252,28 @@ target_sources(citra-qt if (APPLE) set(DIST_DIR "../../dist/apple") set(APPLE_RESOURCES - "${DIST_DIR}/citra.icns" + "${DIST_DIR}/lucina3ds.icns" "${DIST_DIR}/LaunchScreen.storyboard" "${DIST_DIR}/launch_logo.png" ) - target_sources(citra-qt PRIVATE ${APPLE_RESOURCES}) + target_sources(lucina3ds-qt PRIVATE ${APPLE_RESOURCES}) # Define app bundle metadata. include(GenerateBuildInfo) - set_target_properties(citra-qt PROPERTIES + set_target_properties(lucina3ds-qt PROPERTIES MACOSX_BUNDLE TRUE MACOSX_BUNDLE_INFO_PLIST "${DIST_DIR}/Info.plist.in" - MACOSX_BUNDLE_BUNDLE_NAME "Citra" - MACOSX_BUNDLE_GUI_IDENTIFIER "com.citra-emu.citra" + MACOSX_BUNDLE_BUNDLE_NAME "Lucina3DS" + MACOSX_BUNDLE_GUI_IDENTIFIER "ink.hifuu.lucina3ds" MACOSX_BUNDLE_BUNDLE_VERSION "${BUILD_VERSION}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${BUILD_FULLNAME}" MACOSX_BUNDLE_LONG_VERSION_STRING "${BUILD_FULLNAME}" - MACOSX_BUNDLE_ICON_FILE "citra.icns" + MACOSX_BUNDLE_ICON_FILE "lucina3ds.icns" RESOURCE "${APPLE_RESOURCES}" ) if (IOS) - set_target_properties(citra-qt PROPERTIES + set_target_properties(lucina3ds-qt PROPERTIES # Have Xcode copy and sign MoltenVK into app bundle. XCODE_EMBED_FRAMEWORKS "${MOLTENVK_LIBRARY}" XCODE_EMBED_FRAMEWORKS_CODE_SIGN_ON_COPY YES @@ -284,42 +284,42 @@ if (APPLE) endif() elseif(WIN32) # compile as a win32 gui application instead of a console application - target_link_libraries(citra-qt PRIVATE Qt6::EntryPointImplementation) + target_link_libraries(lucina3ds-qt PRIVATE Qt6::EntryPointImplementation) if(MSVC) - set_target_properties(citra-qt PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") + set_target_properties(lucina3ds-qt PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") elseif(MINGW) - set_target_properties(citra-qt PROPERTIES LINK_FLAGS_RELEASE "-mwindows") + set_target_properties(lucina3ds-qt PROPERTIES LINK_FLAGS_RELEASE "-mwindows") endif() endif() if(ENABLE_SDL2) - target_link_libraries(citra-qt PRIVATE SDL2::SDL2) - target_compile_definitions(citra-qt PRIVATE HAVE_SDL2) + target_link_libraries(lucina3ds-qt PRIVATE SDL2::SDL2) + target_compile_definitions(lucina3ds-qt PRIVATE HAVE_SDL2) endif() -create_target_directory_groups(citra-qt) +create_target_directory_groups(lucina3ds-qt) -target_link_libraries(citra-qt PRIVATE audio_core citra_common citra_core input_common network video_core) -target_link_libraries(citra-qt PRIVATE Boost::boost nihstro-headers Qt6::Widgets Qt6::Multimedia Qt6::Concurrent) -target_link_libraries(citra-qt PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) +target_link_libraries(lucina3ds-qt PRIVATE audio_core lucina3ds_common lucina3ds_core input_common network video_core) +target_link_libraries(lucina3ds-qt PRIVATE Boost::boost nihstro-headers Qt6::Widgets Qt6::Multimedia Qt6::Concurrent) +target_link_libraries(lucina3ds-qt PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) if (ENABLE_OPENGL) - target_link_libraries(citra-qt PRIVATE glad) + target_link_libraries(lucina3ds-qt PRIVATE glad) endif() if (ENABLE_VULKAN) - target_link_libraries(citra-qt PRIVATE vulkan-headers) + target_link_libraries(lucina3ds-qt PRIVATE vulkan-headers) endif() if (NOT WIN32) - target_include_directories(citra-qt PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS}) + target_include_directories(lucina3ds-qt PRIVATE ${Qt6Gui_PRIVATE_INCLUDE_DIRS}) endif() if (UNIX AND NOT APPLE) - target_link_libraries(citra-qt PRIVATE Qt6::DBus gamemode) + target_link_libraries(lucina3ds-qt PRIVATE Qt6::DBus gamemode) endif() -target_compile_definitions(citra-qt PRIVATE +target_compile_definitions(lucina3ds-qt PRIVATE # Use QStringBuilder for string concatenation to reduce # the overall number of temporary strings created. -DQT_USE_QSTRINGBUILDER @@ -338,33 +338,33 @@ target_compile_definitions(citra-qt PRIVATE -DQT_NO_CAST_TO_ASCII ) -if (CITRA_ENABLE_COMPATIBILITY_REPORTING) - target_compile_definitions(citra-qt PRIVATE -DCITRA_ENABLE_COMPATIBILITY_REPORTING) +if (LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING) + target_compile_definitions(lucina3ds-qt PRIVATE -DLUCINA3DS_ENABLE_COMPATIBILITY_REPORTING) endif() if (USE_DISCORD_PRESENCE) - target_sources(citra-qt PUBLIC + target_sources(lucina3ds-qt PUBLIC discord_impl.cpp discord_impl.h ) - target_link_libraries(citra-qt PRIVATE discord-rpc) - target_compile_definitions(citra-qt PRIVATE -DUSE_DISCORD_PRESENCE) + target_link_libraries(lucina3ds-qt PRIVATE discord-rpc) + target_compile_definitions(lucina3ds-qt PRIVATE -DUSE_DISCORD_PRESENCE) endif() if (ENABLE_WEB_SERVICE) - target_link_libraries(citra-qt PRIVATE web_service) + target_link_libraries(lucina3ds-qt PRIVATE web_service) endif() if(UNIX AND NOT APPLE) - install(TARGETS citra-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") + install(TARGETS lucina3ds-qt RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") endif() -if (CITRA_USE_PRECOMPILED_HEADERS) - target_precompile_headers(citra-qt PRIVATE precompiled_headers.h) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) + target_precompile_headers(lucina3ds-qt PRIVATE precompiled_headers.h) endif() # Bundle in-place on MSVC so dependencies can be resolved by builds. if (MSVC) include(BundleTarget) - bundle_target_in_place(citra-qt) + bundle_target_in_place(lucina3ds-qt) endif() diff --git a/src/citra_qt/aboutdialog.cpp b/src/lucina3ds_qt/aboutdialog.cpp similarity index 100% rename from src/citra_qt/aboutdialog.cpp rename to src/lucina3ds_qt/aboutdialog.cpp diff --git a/src/citra_qt/aboutdialog.h b/src/lucina3ds_qt/aboutdialog.h similarity index 100% rename from src/citra_qt/aboutdialog.h rename to src/lucina3ds_qt/aboutdialog.h diff --git a/src/citra_qt/aboutdialog.ui b/src/lucina3ds_qt/aboutdialog.ui similarity index 83% rename from src/citra_qt/aboutdialog.ui rename to src/lucina3ds_qt/aboutdialog.ui index 10bc377d..f6de040c 100644 --- a/src/citra_qt/aboutdialog.ui +++ b/src/lucina3ds_qt/aboutdialog.ui @@ -11,7 +11,7 @@ - About Citra + About Lucina3DS @@ -34,7 +34,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -49,7 +49,7 @@ - + 0 @@ -57,7 +57,7 @@ - <html><head/><body><p><span style=" font-size:28pt;">Citra</span></p></body></html> + <html><head/><body><p><span style=" font-size:28pt;">Lucina3DS</span></p></body></html> @@ -84,15 +84,18 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:12pt;">Citra is a free and open source 3DS emulator licensed under GPLv2.0 or any later version.</span></p> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt;"><br /></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:12pt;">This software should not be used to play games you have not legally obtained.</span></p></body></html> +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'FOT-Matisse Pro'; font-size:10pt; font-weight:700; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:12pt; font-weight:400;">Lucina3DS is a free and open source 3DS emulator forked from Citra and licensed under GPLv2.0 or any later version.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:12pt; font-weight:400;">This software should not be used to play games you have not legally obtained.</span></p></body></html> - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter true @@ -102,7 +105,7 @@ p, li { white-space: pre-wrap; } - Qt::Vertical + Qt::Orientation::Vertical @@ -142,18 +145,16 @@ p, li { white-space: pre-wrap; } - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Ok - - - + buttonBox diff --git a/src/citra_qt/applets/mii_selector.cpp b/src/lucina3ds_qt/applets/mii_selector.cpp similarity index 98% rename from src/citra_qt/applets/mii_selector.cpp rename to src/lucina3ds_qt/applets/mii_selector.cpp index 199b3f92..3db084d3 100644 --- a/src/citra_qt/applets/mii_selector.cpp +++ b/src/lucina3ds_qt/applets/mii_selector.cpp @@ -7,7 +7,7 @@ #include #include #include -#include "citra_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/mii_selector.h" #include "common/string_util.h" QtMiiSelectorDialog::QtMiiSelectorDialog(QWidget* parent, QtMiiSelector* mii_selector_) diff --git a/src/citra_qt/applets/mii_selector.h b/src/lucina3ds_qt/applets/mii_selector.h similarity index 100% rename from src/citra_qt/applets/mii_selector.h rename to src/lucina3ds_qt/applets/mii_selector.h diff --git a/src/citra_qt/applets/swkbd.cpp b/src/lucina3ds_qt/applets/swkbd.cpp similarity index 99% rename from src/citra_qt/applets/swkbd.cpp rename to src/lucina3ds_qt/applets/swkbd.cpp index 44e6c573..fd8ef204 100644 --- a/src/citra_qt/applets/swkbd.cpp +++ b/src/lucina3ds_qt/applets/swkbd.cpp @@ -9,7 +9,7 @@ #include #include #include -#include "citra_qt/applets/swkbd.h" +#include "lucina3ds_qt/applets/swkbd.h" QtKeyboardValidator::QtKeyboardValidator(QtKeyboard* keyboard_) : keyboard(keyboard_) {} diff --git a/src/citra_qt/applets/swkbd.h b/src/lucina3ds_qt/applets/swkbd.h similarity index 100% rename from src/citra_qt/applets/swkbd.h rename to src/lucina3ds_qt/applets/swkbd.h diff --git a/src/citra_qt/bootmanager.cpp b/src/lucina3ds_qt/bootmanager.cpp similarity index 99% rename from src/citra_qt/bootmanager.cpp rename to src/lucina3ds_qt/bootmanager.cpp index 8f63f671..14ad3f75 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/lucina3ds_qt/bootmanager.cpp @@ -8,8 +8,8 @@ #include #include #include -#include "citra_qt/bootmanager.h" -#include "citra_qt/main.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/main.h" #include "common/color.h" #include "common/microprofile.h" #include "common/scm_rev.h" diff --git a/src/citra_qt/bootmanager.h b/src/lucina3ds_qt/bootmanager.h similarity index 100% rename from src/citra_qt/bootmanager.h rename to src/lucina3ds_qt/bootmanager.h diff --git a/src/citra_qt/camera/camera_util.cpp b/src/lucina3ds_qt/camera/camera_util.cpp similarity index 99% rename from src/citra_qt/camera/camera_util.cpp rename to src/lucina3ds_qt/camera/camera_util.cpp index 878ac910..4c8cffec 100644 --- a/src/citra_qt/camera/camera_util.cpp +++ b/src/lucina3ds_qt/camera/camera_util.cpp @@ -6,7 +6,7 @@ #include #include #include -#include "citra_qt/camera/camera_util.h" +#include "lucina3ds_qt/camera/camera_util.h" namespace CameraUtil { diff --git a/src/citra_qt/camera/camera_util.h b/src/lucina3ds_qt/camera/camera_util.h similarity index 100% rename from src/citra_qt/camera/camera_util.h rename to src/lucina3ds_qt/camera/camera_util.h diff --git a/src/citra_qt/camera/qt_camera_base.cpp b/src/lucina3ds_qt/camera/qt_camera_base.cpp similarity index 96% rename from src/citra_qt/camera/qt_camera_base.cpp rename to src/lucina3ds_qt/camera/qt_camera_base.cpp index 6956bdee..ca66ecd7 100644 --- a/src/citra_qt/camera/qt_camera_base.cpp +++ b/src/lucina3ds_qt/camera/qt_camera_base.cpp @@ -3,8 +3,8 @@ // Refer to the license.txt file included. #include -#include "citra_qt/camera/camera_util.h" -#include "citra_qt/camera/qt_camera_base.h" +#include "lucina3ds_qt/camera/camera_util.h" +#include "lucina3ds_qt/camera/qt_camera_base.h" #include "common/logging/log.h" #include "core/hle/service/cam/cam.h" diff --git a/src/citra_qt/camera/qt_camera_base.h b/src/lucina3ds_qt/camera/qt_camera_base.h similarity index 91% rename from src/citra_qt/camera/qt_camera_base.h rename to src/lucina3ds_qt/camera/qt_camera_base.h index 6c6095a2..37e681db 100644 --- a/src/citra_qt/camera/qt_camera_base.h +++ b/src/lucina3ds_qt/camera/qt_camera_base.h @@ -9,7 +9,7 @@ namespace Camera { -// Base class for camera interfaces of citra_qt +// Base class for camera interfaces of lucina3ds_qt class QtCameraInterface : public CameraInterface { public: QtCameraInterface(const Service::CAM::Flip& flip); @@ -27,7 +27,7 @@ private: bool basic_flip_horizontal, basic_flip_vertical; }; -// Base class for camera factories of citra_qt +// Base class for camera factories of lucina3ds_qt class QtCameraFactory : public CameraFactory { std::unique_ptr CreatePreview(const std::string& config, int width, int height, const Service::CAM::Flip& flip) override; diff --git a/src/citra_qt/camera/qt_multimedia_camera.cpp b/src/lucina3ds_qt/camera/qt_multimedia_camera.cpp similarity index 97% rename from src/citra_qt/camera/qt_multimedia_camera.cpp rename to src/lucina3ds_qt/camera/qt_multimedia_camera.cpp index 47b9f0f2..67aab8e8 100644 --- a/src/citra_qt/camera/qt_multimedia_camera.cpp +++ b/src/lucina3ds_qt/camera/qt_multimedia_camera.cpp @@ -6,8 +6,8 @@ #include #include #include -#include "citra_qt/camera/qt_multimedia_camera.h" -#include "citra_qt/main.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/main.h" #if defined(__APPLE__) #include "common/apple_authorization.h" diff --git a/src/citra_qt/camera/qt_multimedia_camera.h b/src/lucina3ds_qt/camera/qt_multimedia_camera.h similarity index 97% rename from src/citra_qt/camera/qt_multimedia_camera.h rename to src/lucina3ds_qt/camera/qt_multimedia_camera.h index 44acb7bb..8e742f9b 100644 --- a/src/citra_qt/camera/qt_multimedia_camera.h +++ b/src/lucina3ds_qt/camera/qt_multimedia_camera.h @@ -10,8 +10,8 @@ #include #include #include -#include "citra_qt/camera/camera_util.h" -#include "citra_qt/camera/qt_camera_base.h" +#include "lucina3ds_qt/camera/camera_util.h" +#include "lucina3ds_qt/camera/qt_camera_base.h" #include "core/frontend/camera/interface.h" namespace Camera { diff --git a/src/citra_qt/camera/still_image_camera.cpp b/src/lucina3ds_qt/camera/still_image_camera.cpp similarity index 97% rename from src/citra_qt/camera/still_image_camera.cpp rename to src/lucina3ds_qt/camera/still_image_camera.cpp index cd1b6713..260a4fae 100644 --- a/src/citra_qt/camera/still_image_camera.cpp +++ b/src/lucina3ds_qt/camera/still_image_camera.cpp @@ -6,7 +6,7 @@ #include #include #include -#include "citra_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" #include "common/logging/log.h" namespace Camera { diff --git a/src/citra_qt/camera/still_image_camera.h b/src/lucina3ds_qt/camera/still_image_camera.h similarity index 92% rename from src/citra_qt/camera/still_image_camera.h rename to src/lucina3ds_qt/camera/still_image_camera.h index 19ca044f..0f0a51b0 100644 --- a/src/citra_qt/camera/still_image_camera.h +++ b/src/lucina3ds_qt/camera/still_image_camera.h @@ -6,8 +6,8 @@ #include #include -#include "citra_qt/camera/camera_util.h" -#include "citra_qt/camera/qt_camera_base.h" +#include "lucina3ds_qt/camera/camera_util.h" +#include "lucina3ds_qt/camera/qt_camera_base.h" #include "core/frontend/camera/interface.h" namespace Camera { diff --git a/src/citra_qt/compatdb.cpp b/src/lucina3ds_qt/compatdb.cpp similarity index 98% rename from src/citra_qt/compatdb.cpp rename to src/lucina3ds_qt/compatdb.cpp index 62066b6f..cd0d3f06 100644 --- a/src/citra_qt/compatdb.cpp +++ b/src/lucina3ds_qt/compatdb.cpp @@ -6,7 +6,7 @@ #include #include #include -#include "citra_qt/compatdb.h" +#include "lucina3ds_qt/compatdb.h" #include "core/core.h" #include "ui_compatdb.h" diff --git a/src/citra_qt/compatdb.h b/src/lucina3ds_qt/compatdb.h similarity index 100% rename from src/citra_qt/compatdb.h rename to src/lucina3ds_qt/compatdb.h diff --git a/src/citra_qt/compatdb.ui b/src/lucina3ds_qt/compatdb.ui similarity index 100% rename from src/citra_qt/compatdb.ui rename to src/lucina3ds_qt/compatdb.ui diff --git a/src/citra_qt/compatibility_list.cpp b/src/lucina3ds_qt/compatibility_list.cpp similarity index 93% rename from src/citra_qt/compatibility_list.cpp rename to src/lucina3ds_qt/compatibility_list.cpp index 92e729de..74f1805d 100644 --- a/src/citra_qt/compatibility_list.cpp +++ b/src/lucina3ds_qt/compatibility_list.cpp @@ -4,7 +4,7 @@ #include #include -#include "citra_qt/compatibility_list.h" +#include "lucina3ds_qt/compatibility_list.h" CompatibilityList::const_iterator FindMatchingCompatibilityEntry( const CompatibilityList& compatibility_list, u64 program_id) { diff --git a/src/citra_qt/compatibility_list.h b/src/lucina3ds_qt/compatibility_list.h similarity index 100% rename from src/citra_qt/compatibility_list.h rename to src/lucina3ds_qt/compatibility_list.h diff --git a/src/citra_qt/configuration/config.cpp b/src/lucina3ds_qt/configuration/config.cpp similarity index 99% rename from src/citra_qt/configuration/config.cpp rename to src/lucina3ds_qt/configuration/config.cpp index 21271946..4d48bee7 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/lucina3ds_qt/configuration/config.cpp @@ -6,7 +6,7 @@ #include #include #include -#include "citra_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/config.h" #include "common/file_util.h" #include "common/settings.h" #include "core/hle/service/service.h" @@ -849,9 +849,9 @@ void Config::ReadWebServiceValues() { ReadSetting(QStringLiteral("web_api_url"), QStringLiteral("https://api.citra-emu.org")) .toString() .toStdString(); - NetSettings::values.citra_username = + NetSettings::values.lucina3ds_username = ReadSetting(QStringLiteral("citra_username")).toString().toStdString(); - NetSettings::values.citra_token = + NetSettings::values.lucina3ds_token = ReadSetting(QStringLiteral("citra_token")).toString().toStdString(); qt_config->endGroup(); @@ -1337,9 +1337,9 @@ void Config::SaveWebServiceValues() { QString::fromStdString(NetSettings::values.web_api_url), QStringLiteral("https://api.citra-emu.org")); WriteSetting(QStringLiteral("citra_username"), - QString::fromStdString(NetSettings::values.citra_username)); + QString::fromStdString(NetSettings::values.lucina3ds_username)); WriteSetting(QStringLiteral("citra_token"), - QString::fromStdString(NetSettings::values.citra_token)); + QString::fromStdString(NetSettings::values.lucina3ds_token)); qt_config->endGroup(); } diff --git a/src/citra_qt/configuration/config.h b/src/lucina3ds_qt/configuration/config.h similarity index 99% rename from src/citra_qt/configuration/config.h rename to src/lucina3ds_qt/configuration/config.h index 521c6baf..bea65b47 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/lucina3ds_qt/configuration/config.h @@ -8,7 +8,7 @@ #include #include #include -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/uisettings.h" #include "common/settings.h" class QSettings; diff --git a/src/citra_qt/configuration/configuration_shared.cpp b/src/lucina3ds_qt/configuration/configuration_shared.cpp similarity index 97% rename from src/citra_qt/configuration/configuration_shared.cpp rename to src/lucina3ds_qt/configuration/configuration_shared.cpp index e5039256..3cb2614b 100644 --- a/src/citra_qt/configuration/configuration_shared.cpp +++ b/src/lucina3ds_qt/configuration/configuration_shared.cpp @@ -5,8 +5,8 @@ #include #include #include -#include "citra_qt/configuration/configuration_shared.h" -#include "citra_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/configuration/configuration_shared.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" #include "common/settings.h" void ConfigurationShared::ApplyPerGameSetting(Settings::SwitchableSetting* setting, diff --git a/src/citra_qt/configuration/configuration_shared.h b/src/lucina3ds_qt/configuration/configuration_shared.h similarity index 100% rename from src/citra_qt/configuration/configuration_shared.h rename to src/lucina3ds_qt/configuration/configuration_shared.h diff --git a/src/citra_qt/configuration/configure.ui b/src/lucina3ds_qt/configuration/configure.ui similarity index 100% rename from src/citra_qt/configuration/configure.ui rename to src/lucina3ds_qt/configuration/configure.ui diff --git a/src/citra_qt/configuration/configure_audio.cpp b/src/lucina3ds_qt/configuration/configure_audio.cpp similarity index 98% rename from src/citra_qt/configuration/configure_audio.cpp rename to src/lucina3ds_qt/configuration/configure_audio.cpp index bf301d41..489e192e 100644 --- a/src/citra_qt/configuration/configure_audio.cpp +++ b/src/lucina3ds_qt/configuration/configure_audio.cpp @@ -7,8 +7,8 @@ #include "audio_core/input_details.h" #include "audio_core/sink.h" #include "audio_core/sink_details.h" -#include "citra_qt/configuration/configuration_shared.h" -#include "citra_qt/configuration/configure_audio.h" +#include "lucina3ds_qt/configuration/configuration_shared.h" +#include "lucina3ds_qt/configuration/configure_audio.h" #include "common/settings.h" #include "ui_configure_audio.h" diff --git a/src/citra_qt/configuration/configure_audio.h b/src/lucina3ds_qt/configuration/configure_audio.h similarity index 100% rename from src/citra_qt/configuration/configure_audio.h rename to src/lucina3ds_qt/configuration/configure_audio.h diff --git a/src/citra_qt/configuration/configure_audio.ui b/src/lucina3ds_qt/configuration/configure_audio.ui similarity index 100% rename from src/citra_qt/configuration/configure_audio.ui rename to src/lucina3ds_qt/configuration/configure_audio.ui diff --git a/src/citra_qt/configuration/configure_camera.cpp b/src/lucina3ds_qt/configuration/configure_camera.cpp similarity index 99% rename from src/citra_qt/configuration/configure_camera.cpp rename to src/lucina3ds_qt/configuration/configure_camera.cpp index d8d2d96a..e89c0b0a 100644 --- a/src/citra_qt/configuration/configure_camera.cpp +++ b/src/lucina3ds_qt/configuration/configure_camera.cpp @@ -10,7 +10,7 @@ #include #include #include -#include "citra_qt/configuration/configure_camera.h" +#include "lucina3ds_qt/configuration/configure_camera.h" #include "common/settings.h" #include "core/frontend/camera/factory.h" #include "core/hle/service/cam/cam.h" diff --git a/src/citra_qt/configuration/configure_camera.h b/src/lucina3ds_qt/configuration/configure_camera.h similarity index 100% rename from src/citra_qt/configuration/configure_camera.h rename to src/lucina3ds_qt/configuration/configure_camera.h diff --git a/src/citra_qt/configuration/configure_camera.ui b/src/lucina3ds_qt/configuration/configure_camera.ui similarity index 100% rename from src/citra_qt/configuration/configure_camera.ui rename to src/lucina3ds_qt/configuration/configure_camera.ui diff --git a/src/citra_qt/configuration/configure_cheats.cpp b/src/lucina3ds_qt/configuration/configure_cheats.cpp similarity index 100% rename from src/citra_qt/configuration/configure_cheats.cpp rename to src/lucina3ds_qt/configuration/configure_cheats.cpp diff --git a/src/citra_qt/configuration/configure_cheats.h b/src/lucina3ds_qt/configuration/configure_cheats.h similarity index 100% rename from src/citra_qt/configuration/configure_cheats.h rename to src/lucina3ds_qt/configuration/configure_cheats.h diff --git a/src/citra_qt/configuration/configure_cheats.ui b/src/lucina3ds_qt/configuration/configure_cheats.ui similarity index 100% rename from src/citra_qt/configuration/configure_cheats.ui rename to src/lucina3ds_qt/configuration/configure_cheats.ui diff --git a/src/citra_qt/configuration/configure_debug.cpp b/src/lucina3ds_qt/configuration/configure_debug.cpp similarity index 97% rename from src/citra_qt/configuration/configure_debug.cpp rename to src/lucina3ds_qt/configuration/configure_debug.cpp index 8f4af8bc..f57564b7 100644 --- a/src/citra_qt/configuration/configure_debug.cpp +++ b/src/lucina3ds_qt/configuration/configure_debug.cpp @@ -5,10 +5,10 @@ #include #include #include -#include "citra_qt/configuration/configuration_shared.h" -#include "citra_qt/configuration/configure_debug.h" -#include "citra_qt/debugger/console.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/configuration/configuration_shared.h" +#include "lucina3ds_qt/configuration/configure_debug.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/uisettings.h" #include "common/file_util.h" #include "common/logging/backend.h" #include "common/settings.h" diff --git a/src/citra_qt/configuration/configure_debug.h b/src/lucina3ds_qt/configuration/configure_debug.h similarity index 100% rename from src/citra_qt/configuration/configure_debug.h rename to src/lucina3ds_qt/configuration/configure_debug.h diff --git a/src/citra_qt/configuration/configure_debug.ui b/src/lucina3ds_qt/configuration/configure_debug.ui similarity index 100% rename from src/citra_qt/configuration/configure_debug.ui rename to src/lucina3ds_qt/configuration/configure_debug.ui diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/lucina3ds_qt/configuration/configure_dialog.cpp similarity index 90% rename from src/citra_qt/configuration/configure_dialog.cpp rename to src/lucina3ds_qt/configuration/configure_dialog.cpp index dd00e932..94f0bcec 100644 --- a/src/citra_qt/configuration/configure_dialog.cpp +++ b/src/lucina3ds_qt/configuration/configure_dialog.cpp @@ -4,20 +4,20 @@ #include #include -#include "citra_qt/configuration/configure_audio.h" -#include "citra_qt/configuration/configure_camera.h" -#include "citra_qt/configuration/configure_debug.h" -#include "citra_qt/configuration/configure_dialog.h" -#include "citra_qt/configuration/configure_enhancements.h" -#include "citra_qt/configuration/configure_general.h" -#include "citra_qt/configuration/configure_graphics.h" -#include "citra_qt/configuration/configure_hotkeys.h" -#include "citra_qt/configuration/configure_input.h" -#include "citra_qt/configuration/configure_storage.h" -#include "citra_qt/configuration/configure_system.h" -#include "citra_qt/configuration/configure_ui.h" -#include "citra_qt/configuration/configure_web.h" -#include "citra_qt/hotkeys.h" +#include "lucina3ds_qt/configuration/configure_audio.h" +#include "lucina3ds_qt/configuration/configure_camera.h" +#include "lucina3ds_qt/configuration/configure_debug.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_enhancements.h" +#include "lucina3ds_qt/configuration/configure_general.h" +#include "lucina3ds_qt/configuration/configure_graphics.h" +#include "lucina3ds_qt/configuration/configure_hotkeys.h" +#include "lucina3ds_qt/configuration/configure_input.h" +#include "lucina3ds_qt/configuration/configure_storage.h" +#include "lucina3ds_qt/configuration/configure_system.h" +#include "lucina3ds_qt/configuration/configure_ui.h" +#include "lucina3ds_qt/configuration/configure_web.h" +#include "lucina3ds_qt/hotkeys.h" #include "common/settings.h" #include "core/core.h" #include "ui_configure.h" diff --git a/src/citra_qt/configuration/configure_dialog.h b/src/lucina3ds_qt/configuration/configure_dialog.h similarity index 100% rename from src/citra_qt/configuration/configure_dialog.h rename to src/lucina3ds_qt/configuration/configure_dialog.h diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/lucina3ds_qt/configuration/configure_enhancements.cpp similarity index 98% rename from src/citra_qt/configuration/configure_enhancements.cpp rename to src/lucina3ds_qt/configuration/configure_enhancements.cpp index e1a831ee..b1ee02f2 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/lucina3ds_qt/configuration/configure_enhancements.cpp @@ -3,8 +3,8 @@ // Refer to the license.txt file included. #include -#include "citra_qt/configuration/configuration_shared.h" -#include "citra_qt/configuration/configure_enhancements.h" +#include "lucina3ds_qt/configuration/configuration_shared.h" +#include "lucina3ds_qt/configuration/configure_enhancements.h" #include "common/settings.h" #include "ui_configure_enhancements.h" #ifdef ENABLE_OPENGL diff --git a/src/citra_qt/configuration/configure_enhancements.h b/src/lucina3ds_qt/configuration/configure_enhancements.h similarity index 100% rename from src/citra_qt/configuration/configure_enhancements.h rename to src/lucina3ds_qt/configuration/configure_enhancements.h diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/lucina3ds_qt/configuration/configure_enhancements.ui similarity index 100% rename from src/citra_qt/configuration/configure_enhancements.ui rename to src/lucina3ds_qt/configuration/configure_enhancements.ui diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/lucina3ds_qt/configuration/configure_general.cpp similarity index 98% rename from src/citra_qt/configuration/configure_general.cpp rename to src/lucina3ds_qt/configuration/configure_general.cpp index a402523b..b41c8ede 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/lucina3ds_qt/configuration/configure_general.cpp @@ -6,9 +6,9 @@ #include #include #include -#include "citra_qt/configuration/configuration_shared.h" -#include "citra_qt/configuration/configure_general.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/configuration/configuration_shared.h" +#include "lucina3ds_qt/configuration/configure_general.h" +#include "lucina3ds_qt/uisettings.h" #include "common/file_util.h" #include "common/settings.h" #include "ui_configure_general.h" diff --git a/src/citra_qt/configuration/configure_general.h b/src/lucina3ds_qt/configuration/configure_general.h similarity index 100% rename from src/citra_qt/configuration/configure_general.h rename to src/lucina3ds_qt/configuration/configure_general.h diff --git a/src/citra_qt/configuration/configure_general.ui b/src/lucina3ds_qt/configuration/configure_general.ui similarity index 100% rename from src/citra_qt/configuration/configure_general.ui rename to src/lucina3ds_qt/configuration/configure_general.ui diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/lucina3ds_qt/configuration/configure_graphics.cpp similarity index 99% rename from src/citra_qt/configuration/configure_graphics.cpp rename to src/lucina3ds_qt/configuration/configure_graphics.cpp index 2e244b33..e67f6970 100644 --- a/src/citra_qt/configuration/configure_graphics.cpp +++ b/src/lucina3ds_qt/configuration/configure_graphics.cpp @@ -4,8 +4,8 @@ #include #include -#include "citra_qt/configuration/configuration_shared.h" -#include "citra_qt/configuration/configure_graphics.h" +#include "lucina3ds_qt/configuration/configuration_shared.h" +#include "lucina3ds_qt/configuration/configure_graphics.h" #include "common/settings.h" #include "ui_configure_graphics.h" #ifdef ENABLE_VULKAN diff --git a/src/citra_qt/configuration/configure_graphics.h b/src/lucina3ds_qt/configuration/configure_graphics.h similarity index 100% rename from src/citra_qt/configuration/configure_graphics.h rename to src/lucina3ds_qt/configuration/configure_graphics.h diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/lucina3ds_qt/configuration/configure_graphics.ui similarity index 100% rename from src/citra_qt/configuration/configure_graphics.ui rename to src/lucina3ds_qt/configuration/configure_graphics.ui diff --git a/src/citra_qt/configuration/configure_hotkeys.cpp b/src/lucina3ds_qt/configuration/configure_hotkeys.cpp similarity index 97% rename from src/citra_qt/configuration/configure_hotkeys.cpp rename to src/lucina3ds_qt/configuration/configure_hotkeys.cpp index b0737052..5a8d4f08 100644 --- a/src/citra_qt/configuration/configure_hotkeys.cpp +++ b/src/lucina3ds_qt/configuration/configure_hotkeys.cpp @@ -5,10 +5,10 @@ #include #include #include -#include "citra_qt/configuration/config.h" -#include "citra_qt/configuration/configure_hotkeys.h" -#include "citra_qt/hotkeys.h" -#include "citra_qt/util/sequence_dialog/sequence_dialog.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_hotkeys.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/util/sequence_dialog/sequence_dialog.h" #include "ui_configure_hotkeys.h" constexpr int name_column = 0; diff --git a/src/citra_qt/configuration/configure_hotkeys.h b/src/lucina3ds_qt/configuration/configure_hotkeys.h similarity index 100% rename from src/citra_qt/configuration/configure_hotkeys.h rename to src/lucina3ds_qt/configuration/configure_hotkeys.h diff --git a/src/citra_qt/configuration/configure_hotkeys.ui b/src/lucina3ds_qt/configuration/configure_hotkeys.ui similarity index 100% rename from src/citra_qt/configuration/configure_hotkeys.ui rename to src/lucina3ds_qt/configuration/configure_hotkeys.ui diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/lucina3ds_qt/configuration/configure_input.cpp similarity index 99% rename from src/citra_qt/configuration/configure_input.cpp rename to src/lucina3ds_qt/configuration/configure_input.cpp index 158885ca..647a5b96 100644 --- a/src/citra_qt/configuration/configure_input.cpp +++ b/src/lucina3ds_qt/configuration/configure_input.cpp @@ -12,9 +12,9 @@ #include #include #include -#include "citra_qt/configuration/config.h" -#include "citra_qt/configuration/configure_input.h" -#include "citra_qt/configuration/configure_motion_touch.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_input.h" +#include "lucina3ds_qt/configuration/configure_motion_touch.h" #include "common/param_package.h" #include "core/core.h" #include "ui_configure_input.h" diff --git a/src/citra_qt/configuration/configure_input.h b/src/lucina3ds_qt/configuration/configure_input.h similarity index 100% rename from src/citra_qt/configuration/configure_input.h rename to src/lucina3ds_qt/configuration/configure_input.h diff --git a/src/citra_qt/configuration/configure_input.ui b/src/lucina3ds_qt/configuration/configure_input.ui similarity index 100% rename from src/citra_qt/configuration/configure_input.ui rename to src/lucina3ds_qt/configuration/configure_input.ui diff --git a/src/citra_qt/configuration/configure_motion_touch.cpp b/src/lucina3ds_qt/configuration/configure_motion_touch.cpp similarity index 99% rename from src/citra_qt/configuration/configure_motion_touch.cpp rename to src/lucina3ds_qt/configuration/configure_motion_touch.cpp index 3bfbba71..a091f7b6 100644 --- a/src/citra_qt/configuration/configure_motion_touch.cpp +++ b/src/lucina3ds_qt/configuration/configure_motion_touch.cpp @@ -9,8 +9,8 @@ #include #include #include -#include "citra_qt/configuration/configure_motion_touch.h" -#include "citra_qt/configuration/configure_touch_from_button.h" +#include "lucina3ds_qt/configuration/configure_motion_touch.h" +#include "lucina3ds_qt/configuration/configure_touch_from_button.h" #include "common/logging/log.h" #include "input_common/main.h" #include "ui_configure_motion_touch.h" diff --git a/src/citra_qt/configuration/configure_motion_touch.h b/src/lucina3ds_qt/configuration/configure_motion_touch.h similarity index 100% rename from src/citra_qt/configuration/configure_motion_touch.h rename to src/lucina3ds_qt/configuration/configure_motion_touch.h diff --git a/src/citra_qt/configuration/configure_motion_touch.ui b/src/lucina3ds_qt/configuration/configure_motion_touch.ui similarity index 100% rename from src/citra_qt/configuration/configure_motion_touch.ui rename to src/lucina3ds_qt/configuration/configure_motion_touch.ui diff --git a/src/citra_qt/configuration/configure_per_game.cpp b/src/lucina3ds_qt/configuration/configure_per_game.cpp similarity index 91% rename from src/citra_qt/configuration/configure_per_game.cpp rename to src/lucina3ds_qt/configuration/configure_per_game.cpp index f9c24991..62b83502 100644 --- a/src/citra_qt/configuration/configure_per_game.cpp +++ b/src/lucina3ds_qt/configuration/configure_per_game.cpp @@ -7,16 +7,16 @@ #include #include #include -#include "citra_qt/configuration/config.h" -#include "citra_qt/configuration/configure_audio.h" -#include "citra_qt/configuration/configure_cheats.h" -#include "citra_qt/configuration/configure_debug.h" -#include "citra_qt/configuration/configure_enhancements.h" -#include "citra_qt/configuration/configure_general.h" -#include "citra_qt/configuration/configure_graphics.h" -#include "citra_qt/configuration/configure_per_game.h" -#include "citra_qt/configuration/configure_system.h" -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_audio.h" +#include "lucina3ds_qt/configuration/configure_cheats.h" +#include "lucina3ds_qt/configuration/configure_debug.h" +#include "lucina3ds_qt/configuration/configure_enhancements.h" +#include "lucina3ds_qt/configuration/configure_general.h" +#include "lucina3ds_qt/configuration/configure_graphics.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/configuration/configure_system.h" +#include "lucina3ds_qt/util/util.h" #include "common/file_util.h" #include "core/core.h" #include "core/loader/loader.h" diff --git a/src/citra_qt/configuration/configure_per_game.h b/src/lucina3ds_qt/configuration/configure_per_game.h similarity index 97% rename from src/citra_qt/configuration/configure_per_game.h rename to src/lucina3ds_qt/configuration/configure_per_game.h index 8f8c757f..ac664824 100644 --- a/src/citra_qt/configuration/configure_per_game.h +++ b/src/lucina3ds_qt/configuration/configure_per_game.h @@ -9,7 +9,7 @@ #include #include #include -#include "citra_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/config.h" namespace Core { class System; diff --git a/src/citra_qt/configuration/configure_per_game.ui b/src/lucina3ds_qt/configuration/configure_per_game.ui similarity index 100% rename from src/citra_qt/configuration/configure_per_game.ui rename to src/lucina3ds_qt/configuration/configure_per_game.ui diff --git a/src/citra_qt/configuration/configure_storage.cpp b/src/lucina3ds_qt/configuration/configure_storage.cpp similarity index 98% rename from src/citra_qt/configuration/configure_storage.cpp rename to src/lucina3ds_qt/configuration/configure_storage.cpp index d46cb061..f66186dd 100644 --- a/src/citra_qt/configuration/configure_storage.cpp +++ b/src/lucina3ds_qt/configuration/configure_storage.cpp @@ -5,7 +5,7 @@ #include #include #include -#include "citra_qt/configuration/configure_storage.h" +#include "lucina3ds_qt/configuration/configure_storage.h" #include "common/file_util.h" #include "common/settings.h" #include "ui_configure_storage.h" diff --git a/src/citra_qt/configuration/configure_storage.h b/src/lucina3ds_qt/configuration/configure_storage.h similarity index 100% rename from src/citra_qt/configuration/configure_storage.h rename to src/lucina3ds_qt/configuration/configure_storage.h diff --git a/src/citra_qt/configuration/configure_storage.ui b/src/lucina3ds_qt/configuration/configure_storage.ui similarity index 100% rename from src/citra_qt/configuration/configure_storage.ui rename to src/lucina3ds_qt/configuration/configure_storage.ui diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/lucina3ds_qt/configuration/configure_system.cpp similarity index 99% rename from src/citra_qt/configuration/configure_system.cpp rename to src/lucina3ds_qt/configuration/configure_system.cpp index bc7bb53b..bc0572ab 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/lucina3ds_qt/configuration/configure_system.cpp @@ -8,8 +8,8 @@ #include #include #include -#include "citra_qt/configuration/configuration_shared.h" -#include "citra_qt/configuration/configure_system.h" +#include "lucina3ds_qt/configuration/configuration_shared.h" +#include "lucina3ds_qt/configuration/configure_system.h" #include "common/file_util.h" #include "common/settings.h" #include "core/core.h" diff --git a/src/citra_qt/configuration/configure_system.h b/src/lucina3ds_qt/configuration/configure_system.h similarity index 100% rename from src/citra_qt/configuration/configure_system.h rename to src/lucina3ds_qt/configuration/configure_system.h diff --git a/src/citra_qt/configuration/configure_system.ui b/src/lucina3ds_qt/configuration/configure_system.ui similarity index 100% rename from src/citra_qt/configuration/configure_system.ui rename to src/lucina3ds_qt/configuration/configure_system.ui diff --git a/src/citra_qt/configuration/configure_touch_from_button.cpp b/src/lucina3ds_qt/configuration/configure_touch_from_button.cpp similarity index 99% rename from src/citra_qt/configuration/configure_touch_from_button.cpp rename to src/lucina3ds_qt/configuration/configure_touch_from_button.cpp index 76f2a77b..14a6f052 100644 --- a/src/citra_qt/configuration/configure_touch_from_button.cpp +++ b/src/lucina3ds_qt/configuration/configure_touch_from_button.cpp @@ -9,8 +9,8 @@ #include #include #include -#include "citra_qt/configuration/configure_touch_from_button.h" -#include "citra_qt/configuration/configure_touch_widget.h" +#include "lucina3ds_qt/configuration/configure_touch_from_button.h" +#include "lucina3ds_qt/configuration/configure_touch_widget.h" #include "common/param_package.h" #include "core/3ds.h" #include "input_common/main.h" diff --git a/src/citra_qt/configuration/configure_touch_from_button.h b/src/lucina3ds_qt/configuration/configure_touch_from_button.h similarity index 100% rename from src/citra_qt/configuration/configure_touch_from_button.h rename to src/lucina3ds_qt/configuration/configure_touch_from_button.h diff --git a/src/citra_qt/configuration/configure_touch_from_button.ui b/src/lucina3ds_qt/configuration/configure_touch_from_button.ui similarity index 98% rename from src/citra_qt/configuration/configure_touch_from_button.ui rename to src/lucina3ds_qt/configuration/configure_touch_from_button.ui index d0598bdb..dca5417c 100644 --- a/src/citra_qt/configuration/configure_touch_from_button.ui +++ b/src/lucina3ds_qt/configuration/configure_touch_from_button.ui @@ -205,7 +205,7 @@ Drag points to change position, or double-click table cells to edit values. TouchScreenPreview QFrame -
citra_qt/configuration/configure_touch_widget.h
+
lucina3ds_qt/configuration/configure_touch_widget.h
1 diff --git a/src/citra_qt/configuration/configure_touch_widget.h b/src/lucina3ds_qt/configuration/configure_touch_widget.h similarity index 100% rename from src/citra_qt/configuration/configure_touch_widget.h rename to src/lucina3ds_qt/configuration/configure_touch_widget.h diff --git a/src/citra_qt/configuration/configure_ui.cpp b/src/lucina3ds_qt/configuration/configure_ui.cpp similarity index 97% rename from src/citra_qt/configuration/configure_ui.cpp rename to src/lucina3ds_qt/configuration/configure_ui.cpp index f13fc41b..5f625db8 100644 --- a/src/citra_qt/configuration/configure_ui.cpp +++ b/src/lucina3ds_qt/configuration/configure_ui.cpp @@ -3,8 +3,8 @@ // Refer to the license.txt file included. #include -#include "citra_qt/configuration/configure_ui.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/configuration/configure_ui.h" +#include "lucina3ds_qt/uisettings.h" #include "ui_configure_ui.h" ConfigureUi::ConfigureUi(QWidget* parent) diff --git a/src/citra_qt/configuration/configure_ui.h b/src/lucina3ds_qt/configuration/configure_ui.h similarity index 100% rename from src/citra_qt/configuration/configure_ui.h rename to src/lucina3ds_qt/configuration/configure_ui.h diff --git a/src/citra_qt/configuration/configure_ui.ui b/src/lucina3ds_qt/configuration/configure_ui.ui similarity index 100% rename from src/citra_qt/configuration/configure_ui.ui rename to src/lucina3ds_qt/configuration/configure_ui.ui diff --git a/src/citra_qt/configuration/configure_web.cpp b/src/lucina3ds_qt/configuration/configure_web.cpp similarity index 90% rename from src/citra_qt/configuration/configure_web.cpp rename to src/lucina3ds_qt/configuration/configure_web.cpp index 2c61b02d..eda5016c 100644 --- a/src/citra_qt/configuration/configure_web.cpp +++ b/src/lucina3ds_qt/configuration/configure_web.cpp @@ -5,8 +5,8 @@ #include #include #include -#include "citra_qt/configuration/configure_web.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/configuration/configure_web.h" +#include "lucina3ds_qt/uisettings.h" #include "network/network_settings.h" #include "ui_configure_web.h" diff --git a/src/citra_qt/configuration/configure_web.h b/src/lucina3ds_qt/configuration/configure_web.h similarity index 100% rename from src/citra_qt/configuration/configure_web.h rename to src/lucina3ds_qt/configuration/configure_web.h diff --git a/src/citra_qt/configuration/configure_web.ui b/src/lucina3ds_qt/configuration/configure_web.ui similarity index 100% rename from src/citra_qt/configuration/configure_web.ui rename to src/lucina3ds_qt/configuration/configure_web.ui diff --git a/src/citra_qt/debugger/console.cpp b/src/lucina3ds_qt/debugger/console.cpp similarity index 95% rename from src/citra_qt/debugger/console.cpp rename to src/lucina3ds_qt/debugger/console.cpp index ed7a1cc8..d58d8163 100644 --- a/src/citra_qt/debugger/console.cpp +++ b/src/lucina3ds_qt/debugger/console.cpp @@ -8,8 +8,8 @@ #include #endif -#include "citra_qt/debugger/console.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/uisettings.h" #include "common/logging/backend.h" namespace Debugger { diff --git a/src/citra_qt/debugger/console.h b/src/lucina3ds_qt/debugger/console.h similarity index 100% rename from src/citra_qt/debugger/console.h rename to src/lucina3ds_qt/debugger/console.h diff --git a/src/citra_qt/debugger/graphics/graphics.cpp b/src/lucina3ds_qt/debugger/graphics/graphics.cpp similarity index 97% rename from src/citra_qt/debugger/graphics/graphics.cpp rename to src/lucina3ds_qt/debugger/graphics/graphics.cpp index 021e1c72..ccf753aa 100644 --- a/src/citra_qt/debugger/graphics/graphics.cpp +++ b/src/lucina3ds_qt/debugger/graphics/graphics.cpp @@ -3,8 +3,8 @@ // Refer to the license.txt file included. #include -#include "citra_qt/debugger/graphics/graphics.h" -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/util/util.h" #include "core/core.h" #include "video_core/gpu.h" diff --git a/src/citra_qt/debugger/graphics/graphics.h b/src/lucina3ds_qt/debugger/graphics/graphics.h similarity index 100% rename from src/citra_qt/debugger/graphics/graphics.h rename to src/lucina3ds_qt/debugger/graphics/graphics.h diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.cpp b/src/lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.cpp similarity index 93% rename from src/citra_qt/debugger/graphics/graphics_breakpoint_observer.cpp rename to src/lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.cpp index 5d432761..16666546 100644 --- a/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.cpp +++ b/src/lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include -#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.h" BreakPointObserverDock::BreakPointObserverDock(std::shared_ptr debug_context, const QString& title, QWidget* parent) diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoint_observer.h b/src/lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.h similarity index 100% rename from src/citra_qt/debugger/graphics/graphics_breakpoint_observer.h rename to src/lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.h diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoints.cpp b/src/lucina3ds_qt/debugger/graphics/graphics_breakpoints.cpp similarity index 98% rename from src/citra_qt/debugger/graphics/graphics_breakpoints.cpp rename to src/lucina3ds_qt/debugger/graphics/graphics_breakpoints.cpp index 9835b514..d1234fef 100644 --- a/src/citra_qt/debugger/graphics/graphics_breakpoints.cpp +++ b/src/lucina3ds_qt/debugger/graphics/graphics_breakpoints.cpp @@ -7,8 +7,8 @@ #include #include #include -#include "citra_qt/debugger/graphics/graphics_breakpoints.h" -#include "citra_qt/debugger/graphics/graphics_breakpoints_p.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints_p.h" BreakPointModel::BreakPointModel(std::shared_ptr debug_context, QObject* parent) : QAbstractListModel(parent), context_weak(debug_context), diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoints.h b/src/lucina3ds_qt/debugger/graphics/graphics_breakpoints.h similarity index 100% rename from src/citra_qt/debugger/graphics/graphics_breakpoints.h rename to src/lucina3ds_qt/debugger/graphics/graphics_breakpoints.h diff --git a/src/citra_qt/debugger/graphics/graphics_breakpoints_p.h b/src/lucina3ds_qt/debugger/graphics/graphics_breakpoints_p.h similarity index 100% rename from src/citra_qt/debugger/graphics/graphics_breakpoints_p.h rename to src/lucina3ds_qt/debugger/graphics/graphics_breakpoints_p.h diff --git a/src/citra_qt/debugger/graphics/graphics_cmdlists.cpp b/src/lucina3ds_qt/debugger/graphics/graphics_cmdlists.cpp similarity index 98% rename from src/citra_qt/debugger/graphics/graphics_cmdlists.cpp rename to src/lucina3ds_qt/debugger/graphics/graphics_cmdlists.cpp index 94a66120..fc99c9c2 100644 --- a/src/citra_qt/debugger/graphics/graphics_cmdlists.cpp +++ b/src/lucina3ds_qt/debugger/graphics/graphics_cmdlists.cpp @@ -13,8 +13,8 @@ #include #include #include -#include "citra_qt/debugger/graphics/graphics_cmdlists.h" -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/util/util.h" #include "common/vector_math.h" #include "core/core.h" #include "core/memory.h" diff --git a/src/citra_qt/debugger/graphics/graphics_cmdlists.h b/src/lucina3ds_qt/debugger/graphics/graphics_cmdlists.h similarity index 100% rename from src/citra_qt/debugger/graphics/graphics_cmdlists.h rename to src/lucina3ds_qt/debugger/graphics/graphics_cmdlists.h diff --git a/src/citra_qt/debugger/graphics/graphics_surface.cpp b/src/lucina3ds_qt/debugger/graphics/graphics_surface.cpp similarity index 99% rename from src/citra_qt/debugger/graphics/graphics_surface.cpp rename to src/lucina3ds_qt/debugger/graphics/graphics_surface.cpp index 4983d247..f0402540 100644 --- a/src/citra_qt/debugger/graphics/graphics_surface.cpp +++ b/src/lucina3ds_qt/debugger/graphics/graphics_surface.cpp @@ -12,8 +12,8 @@ #include #include #include -#include "citra_qt/debugger/graphics/graphics_surface.h" -#include "citra_qt/util/spinbox.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/util/spinbox.h" #include "common/color.h" #include "core/core.h" #include "core/memory.h" diff --git a/src/citra_qt/debugger/graphics/graphics_surface.h b/src/lucina3ds_qt/debugger/graphics/graphics_surface.h similarity index 97% rename from src/citra_qt/debugger/graphics/graphics_surface.h rename to src/lucina3ds_qt/debugger/graphics/graphics_surface.h index e5dbc30e..5788c14b 100644 --- a/src/citra_qt/debugger/graphics/graphics_surface.h +++ b/src/lucina3ds_qt/debugger/graphics/graphics_surface.h @@ -6,7 +6,7 @@ #include #include -#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.h" class QComboBox; class QSpinBox; diff --git a/src/citra_qt/debugger/graphics/graphics_tracing.cpp b/src/lucina3ds_qt/debugger/graphics/graphics_tracing.cpp similarity index 99% rename from src/citra_qt/debugger/graphics/graphics_tracing.cpp rename to src/lucina3ds_qt/debugger/graphics/graphics_tracing.cpp index e78acb09..856315c4 100644 --- a/src/citra_qt/debugger/graphics/graphics_tracing.cpp +++ b/src/lucina3ds_qt/debugger/graphics/graphics_tracing.cpp @@ -12,7 +12,7 @@ #include #include #include -#include "citra_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" #include "common/common_types.h" #include "core/core.h" #include "core/tracer/recorder.h" diff --git a/src/citra_qt/debugger/graphics/graphics_tracing.h b/src/lucina3ds_qt/debugger/graphics/graphics_tracing.h similarity index 93% rename from src/citra_qt/debugger/graphics/graphics_tracing.h rename to src/lucina3ds_qt/debugger/graphics/graphics_tracing.h index 658f3ad0..9b8856fe 100644 --- a/src/citra_qt/debugger/graphics/graphics_tracing.h +++ b/src/lucina3ds_qt/debugger/graphics/graphics_tracing.h @@ -4,7 +4,7 @@ #pragma once -#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.h" namespace Core { class System; diff --git a/src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp b/src/lucina3ds_qt/debugger/graphics/graphics_vertex_shader.cpp similarity index 99% rename from src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp rename to src/lucina3ds_qt/debugger/graphics/graphics_vertex_shader.cpp index 46f8ec5c..6663b5a2 100644 --- a/src/citra_qt/debugger/graphics/graphics_vertex_shader.cpp +++ b/src/lucina3ds_qt/debugger/graphics/graphics_vertex_shader.cpp @@ -14,8 +14,8 @@ #include #include #include -#include "citra_qt/debugger/graphics/graphics_vertex_shader.h" -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/util/util.h" #include "core/core.h" #include "video_core/gpu.h" #include "video_core/pica/pica_core.h" diff --git a/src/citra_qt/debugger/graphics/graphics_vertex_shader.h b/src/lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h similarity index 97% rename from src/citra_qt/debugger/graphics/graphics_vertex_shader.h rename to src/lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h index 29577bf4..e8be1a6c 100644 --- a/src/citra_qt/debugger/graphics/graphics_vertex_shader.h +++ b/src/lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h @@ -7,7 +7,7 @@ #include #include #include -#include "citra_qt/debugger/graphics/graphics_breakpoint_observer.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoint_observer.h" #include "video_core/pica/output_vertex.h" #include "video_core/shader/debug_data.h" diff --git a/src/citra_qt/debugger/ipc/record_dialog.cpp b/src/lucina3ds_qt/debugger/ipc/record_dialog.cpp similarity index 97% rename from src/citra_qt/debugger/ipc/record_dialog.cpp rename to src/lucina3ds_qt/debugger/ipc/record_dialog.cpp index d4668a33..b2eb553e 100644 --- a/src/citra_qt/debugger/ipc/record_dialog.cpp +++ b/src/lucina3ds_qt/debugger/ipc/record_dialog.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include -#include "citra_qt/debugger/ipc/record_dialog.h" +#include "lucina3ds_qt/debugger/ipc/record_dialog.h" #include "common/assert.h" #include "core/hle/kernel/ipc_debugger/recorder.h" #include "ui_record_dialog.h" diff --git a/src/citra_qt/debugger/ipc/record_dialog.h b/src/lucina3ds_qt/debugger/ipc/record_dialog.h similarity index 100% rename from src/citra_qt/debugger/ipc/record_dialog.h rename to src/lucina3ds_qt/debugger/ipc/record_dialog.h diff --git a/src/citra_qt/debugger/ipc/record_dialog.ui b/src/lucina3ds_qt/debugger/ipc/record_dialog.ui similarity index 100% rename from src/citra_qt/debugger/ipc/record_dialog.ui rename to src/lucina3ds_qt/debugger/ipc/record_dialog.ui diff --git a/src/citra_qt/debugger/ipc/recorder.cpp b/src/lucina3ds_qt/debugger/ipc/recorder.cpp similarity index 98% rename from src/citra_qt/debugger/ipc/recorder.cpp rename to src/lucina3ds_qt/debugger/ipc/recorder.cpp index 3a9bc8a9..de090361 100644 --- a/src/citra_qt/debugger/ipc/recorder.cpp +++ b/src/lucina3ds_qt/debugger/ipc/recorder.cpp @@ -7,8 +7,8 @@ #include #include #include -#include "citra_qt/debugger/ipc/record_dialog.h" -#include "citra_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/ipc/record_dialog.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" #include "common/assert.h" #include "common/string_util.h" #include "core/core.h" diff --git a/src/citra_qt/debugger/ipc/recorder.h b/src/lucina3ds_qt/debugger/ipc/recorder.h similarity index 100% rename from src/citra_qt/debugger/ipc/recorder.h rename to src/lucina3ds_qt/debugger/ipc/recorder.h diff --git a/src/citra_qt/debugger/ipc/recorder.ui b/src/lucina3ds_qt/debugger/ipc/recorder.ui similarity index 100% rename from src/citra_qt/debugger/ipc/recorder.ui rename to src/lucina3ds_qt/debugger/ipc/recorder.ui diff --git a/src/citra_qt/debugger/lle_service_modules.cpp b/src/lucina3ds_qt/debugger/lle_service_modules.cpp similarity index 95% rename from src/citra_qt/debugger/lle_service_modules.cpp rename to src/lucina3ds_qt/debugger/lle_service_modules.cpp index 50838132..ab0ac3ca 100644 --- a/src/citra_qt/debugger/lle_service_modules.cpp +++ b/src/lucina3ds_qt/debugger/lle_service_modules.cpp @@ -6,7 +6,7 @@ #include #include #include -#include "citra_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" #include "common/settings.h" LLEServiceModulesWidget::LLEServiceModulesWidget(QWidget* parent) diff --git a/src/citra_qt/debugger/lle_service_modules.h b/src/lucina3ds_qt/debugger/lle_service_modules.h similarity index 100% rename from src/citra_qt/debugger/lle_service_modules.h rename to src/lucina3ds_qt/debugger/lle_service_modules.h diff --git a/src/citra_qt/debugger/profiler.cpp b/src/lucina3ds_qt/debugger/profiler.cpp similarity index 99% rename from src/citra_qt/debugger/profiler.cpp rename to src/lucina3ds_qt/debugger/profiler.cpp index dbeade3c..063d9c30 100644 --- a/src/citra_qt/debugger/profiler.cpp +++ b/src/lucina3ds_qt/debugger/profiler.cpp @@ -8,8 +8,8 @@ #include #include #include -#include "citra_qt/debugger/profiler.h" -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/util/util.h" #include "common/common_types.h" #include "common/microprofile.h" diff --git a/src/citra_qt/debugger/profiler.h b/src/lucina3ds_qt/debugger/profiler.h similarity index 100% rename from src/citra_qt/debugger/profiler.h rename to src/lucina3ds_qt/debugger/profiler.h diff --git a/src/citra_qt/debugger/registers.cpp b/src/lucina3ds_qt/debugger/registers.cpp similarity index 99% rename from src/citra_qt/debugger/registers.cpp rename to src/lucina3ds_qt/debugger/registers.cpp index 872146cc..7522725c 100644 --- a/src/citra_qt/debugger/registers.cpp +++ b/src/lucina3ds_qt/debugger/registers.cpp @@ -3,8 +3,8 @@ // Refer to the license.txt file included. #include -#include "citra_qt/debugger/registers.h" -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/util/util.h" #include "core/arm/arm_interface.h" #include "core/core.h" #include "ui_registers.h" diff --git a/src/citra_qt/debugger/registers.h b/src/lucina3ds_qt/debugger/registers.h similarity index 100% rename from src/citra_qt/debugger/registers.h rename to src/lucina3ds_qt/debugger/registers.h diff --git a/src/citra_qt/debugger/registers.ui b/src/lucina3ds_qt/debugger/registers.ui similarity index 100% rename from src/citra_qt/debugger/registers.ui rename to src/lucina3ds_qt/debugger/registers.ui diff --git a/src/citra_qt/debugger/wait_tree.cpp b/src/lucina3ds_qt/debugger/wait_tree.cpp similarity index 99% rename from src/citra_qt/debugger/wait_tree.cpp rename to src/lucina3ds_qt/debugger/wait_tree.cpp index 1a2acece..e00c06cf 100644 --- a/src/citra_qt/debugger/wait_tree.cpp +++ b/src/lucina3ds_qt/debugger/wait_tree.cpp @@ -3,8 +3,8 @@ // Refer to the license.txt file included. #include -#include "citra_qt/debugger/wait_tree.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/uisettings.h" #include "common/assert.h" #include "core/hle/kernel/event.h" #include "core/hle/kernel/mutex.h" diff --git a/src/citra_qt/debugger/wait_tree.h b/src/lucina3ds_qt/debugger/wait_tree.h similarity index 100% rename from src/citra_qt/debugger/wait_tree.h rename to src/lucina3ds_qt/debugger/wait_tree.h diff --git a/src/citra_qt/discord.h b/src/lucina3ds_qt/discord.h similarity index 100% rename from src/citra_qt/discord.h rename to src/lucina3ds_qt/discord.h diff --git a/src/citra_qt/discord_impl.cpp b/src/lucina3ds_qt/discord_impl.cpp similarity index 95% rename from src/citra_qt/discord_impl.cpp rename to src/lucina3ds_qt/discord_impl.cpp index 076c15fa..57af93be 100644 --- a/src/citra_qt/discord_impl.cpp +++ b/src/lucina3ds_qt/discord_impl.cpp @@ -5,8 +5,8 @@ #include #include #include -#include "citra_qt/discord_impl.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/discord_impl.h" +#include "lucina3ds_qt/uisettings.h" #include "common/common_types.h" #include "core/core.h" #include "core/loader/loader.h" diff --git a/src/citra_qt/discord_impl.h b/src/lucina3ds_qt/discord_impl.h similarity index 93% rename from src/citra_qt/discord_impl.h rename to src/lucina3ds_qt/discord_impl.h index 25bee069..e0d151a8 100644 --- a/src/citra_qt/discord_impl.h +++ b/src/lucina3ds_qt/discord_impl.h @@ -4,7 +4,7 @@ #pragma once -#include "citra_qt/discord.h" +#include "lucina3ds_qt/discord.h" namespace Core { class System; diff --git a/src/citra_qt/dumping/dumping_dialog.cpp b/src/lucina3ds_qt/dumping/dumping_dialog.cpp similarity index 98% rename from src/citra_qt/dumping/dumping_dialog.cpp rename to src/lucina3ds_qt/dumping/dumping_dialog.cpp index cd05d87d..58eaba32 100644 --- a/src/citra_qt/dumping/dumping_dialog.cpp +++ b/src/lucina3ds_qt/dumping/dumping_dialog.cpp @@ -4,9 +4,9 @@ #include #include -#include "citra_qt/dumping/dumping_dialog.h" -#include "citra_qt/dumping/options_dialog.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/dumping/options_dialog.h" +#include "lucina3ds_qt/uisettings.h" #include "common/settings.h" #include "core/core.h" #include "ui_dumping_dialog.h" diff --git a/src/citra_qt/dumping/dumping_dialog.h b/src/lucina3ds_qt/dumping/dumping_dialog.h similarity index 100% rename from src/citra_qt/dumping/dumping_dialog.h rename to src/lucina3ds_qt/dumping/dumping_dialog.h diff --git a/src/citra_qt/dumping/dumping_dialog.ui b/src/lucina3ds_qt/dumping/dumping_dialog.ui similarity index 100% rename from src/citra_qt/dumping/dumping_dialog.ui rename to src/lucina3ds_qt/dumping/dumping_dialog.ui diff --git a/src/citra_qt/dumping/option_set_dialog.cpp b/src/lucina3ds_qt/dumping/option_set_dialog.cpp similarity index 99% rename from src/citra_qt/dumping/option_set_dialog.cpp rename to src/lucina3ds_qt/dumping/option_set_dialog.cpp index f6d6e4af..ea8409d2 100644 --- a/src/citra_qt/dumping/option_set_dialog.cpp +++ b/src/lucina3ds_qt/dumping/option_set_dialog.cpp @@ -5,7 +5,7 @@ #include #include #include -#include "citra_qt/dumping/option_set_dialog.h" +#include "lucina3ds_qt/dumping/option_set_dialog.h" #include "common/logging/log.h" #include "common/string_util.h" #include "ui_option_set_dialog.h" diff --git a/src/citra_qt/dumping/option_set_dialog.h b/src/lucina3ds_qt/dumping/option_set_dialog.h similarity index 100% rename from src/citra_qt/dumping/option_set_dialog.h rename to src/lucina3ds_qt/dumping/option_set_dialog.h diff --git a/src/citra_qt/dumping/option_set_dialog.ui b/src/lucina3ds_qt/dumping/option_set_dialog.ui similarity index 100% rename from src/citra_qt/dumping/option_set_dialog.ui rename to src/lucina3ds_qt/dumping/option_set_dialog.ui diff --git a/src/citra_qt/dumping/options_dialog.cpp b/src/lucina3ds_qt/dumping/options_dialog.cpp similarity index 96% rename from src/citra_qt/dumping/options_dialog.cpp rename to src/lucina3ds_qt/dumping/options_dialog.cpp index e75fc61c..349b9df3 100644 --- a/src/citra_qt/dumping/options_dialog.cpp +++ b/src/lucina3ds_qt/dumping/options_dialog.cpp @@ -3,8 +3,8 @@ // Refer to the license.txt file included. #include -#include "citra_qt/dumping/option_set_dialog.h" -#include "citra_qt/dumping/options_dialog.h" +#include "lucina3ds_qt/dumping/option_set_dialog.h" +#include "lucina3ds_qt/dumping/options_dialog.h" #include "ui_options_dialog.h" constexpr char UNSET_TEXT[] = QT_TR_NOOP("[not set]"); diff --git a/src/citra_qt/dumping/options_dialog.h b/src/lucina3ds_qt/dumping/options_dialog.h similarity index 100% rename from src/citra_qt/dumping/options_dialog.h rename to src/lucina3ds_qt/dumping/options_dialog.h diff --git a/src/citra_qt/dumping/options_dialog.ui b/src/lucina3ds_qt/dumping/options_dialog.ui similarity index 100% rename from src/citra_qt/dumping/options_dialog.ui rename to src/lucina3ds_qt/dumping/options_dialog.ui diff --git a/src/citra_qt/game_list.cpp b/src/lucina3ds_qt/game_list.cpp similarity index 99% rename from src/citra_qt/game_list.cpp rename to src/lucina3ds_qt/game_list.cpp index 43fcbcee..925ae08e 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/lucina3ds_qt/game_list.cpp @@ -24,12 +24,12 @@ #include #include #include -#include "citra_qt/compatibility_list.h" -#include "citra_qt/game_list.h" -#include "citra_qt/game_list_p.h" -#include "citra_qt/game_list_worker.h" -#include "citra_qt/main.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/game_list_p.h" +#include "lucina3ds_qt/game_list_worker.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/uisettings.h" #include "common/logging/log.h" #include "common/settings.h" #include "core/file_sys/archive_extsavedata.h" diff --git a/src/citra_qt/game_list.h b/src/lucina3ds_qt/game_list.h similarity index 98% rename from src/citra_qt/game_list.h rename to src/lucina3ds_qt/game_list.h index 9b9ccb05..75b89bd3 100644 --- a/src/citra_qt/game_list.h +++ b/src/lucina3ds_qt/game_list.h @@ -8,7 +8,7 @@ #include #include #include -#include "citra_qt/compatibility_list.h" +#include "lucina3ds_qt/compatibility_list.h" #include "common/common_types.h" #include "uisettings.h" diff --git a/src/citra_qt/game_list_p.h b/src/lucina3ds_qt/game_list_p.h similarity index 99% rename from src/citra_qt/game_list_p.h rename to src/lucina3ds_qt/game_list_p.h index 338d8cee..f4ca1bb8 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/lucina3ds_qt/game_list_p.h @@ -18,8 +18,8 @@ #include #include #include -#include "citra_qt/uisettings.h" -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/util/util.h" #include "common/file_util.h" #include "common/logging/log.h" #include "common/string_util.h" diff --git a/src/citra_qt/game_list_worker.cpp b/src/lucina3ds_qt/game_list_worker.cpp similarity index 97% rename from src/citra_qt/game_list_worker.cpp rename to src/lucina3ds_qt/game_list_worker.cpp index e4471b6a..03112025 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/lucina3ds_qt/game_list_worker.cpp @@ -8,11 +8,11 @@ #include #include #include -#include "citra_qt/compatibility_list.h" -#include "citra_qt/game_list.h" -#include "citra_qt/game_list_p.h" -#include "citra_qt/game_list_worker.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/game_list_p.h" +#include "lucina3ds_qt/game_list_worker.h" +#include "lucina3ds_qt/uisettings.h" #include "common/common_paths.h" #include "common/file_util.h" #include "core/hle/service/am/am.h" diff --git a/src/citra_qt/game_list_worker.h b/src/lucina3ds_qt/game_list_worker.h similarity index 97% rename from src/citra_qt/game_list_worker.h rename to src/lucina3ds_qt/game_list_worker.h index 60012e62..7b969db6 100644 --- a/src/citra_qt/game_list_worker.h +++ b/src/lucina3ds_qt/game_list_worker.h @@ -12,7 +12,7 @@ #include #include #include -#include "citra_qt/compatibility_list.h" +#include "lucina3ds_qt/compatibility_list.h" #include "common/common_types.h" namespace Service::FS { diff --git a/src/citra_qt/hotkeys.cpp b/src/lucina3ds_qt/hotkeys.cpp similarity index 96% rename from src/citra_qt/hotkeys.cpp rename to src/lucina3ds_qt/hotkeys.cpp index 8997b456..af140329 100644 --- a/src/citra_qt/hotkeys.cpp +++ b/src/lucina3ds_qt/hotkeys.cpp @@ -4,8 +4,8 @@ #include #include -#include "citra_qt/hotkeys.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/uisettings.h" HotkeyRegistry::HotkeyRegistry() = default; diff --git a/src/citra_qt/hotkeys.h b/src/lucina3ds_qt/hotkeys.h similarity index 100% rename from src/citra_qt/hotkeys.h rename to src/lucina3ds_qt/hotkeys.h diff --git a/src/citra_qt/loading_screen.cpp b/src/lucina3ds_qt/loading_screen.cpp similarity index 99% rename from src/citra_qt/loading_screen.cpp rename to src/lucina3ds_qt/loading_screen.cpp index 23d15b9d..5467c5d0 100644 --- a/src/citra_qt/loading_screen.cpp +++ b/src/lucina3ds_qt/loading_screen.cpp @@ -14,7 +14,7 @@ #include #include #include -#include "citra_qt/loading_screen.h" +#include "lucina3ds_qt/loading_screen.h" #include "common/logging/log.h" #include "core/loader/loader.h" #include "core/loader/smdh.h" diff --git a/src/citra_qt/loading_screen.h b/src/lucina3ds_qt/loading_screen.h similarity index 100% rename from src/citra_qt/loading_screen.h rename to src/lucina3ds_qt/loading_screen.h diff --git a/src/citra_qt/loading_screen.ui b/src/lucina3ds_qt/loading_screen.ui similarity index 100% rename from src/citra_qt/loading_screen.ui rename to src/lucina3ds_qt/loading_screen.ui diff --git a/src/citra_qt/citra-qt.rc b/src/lucina3ds_qt/lucina3ds-qt.rc similarity index 100% rename from src/citra_qt/citra-qt.rc rename to src/lucina3ds_qt/lucina3ds-qt.rc diff --git a/src/citra_qt/main.cpp b/src/lucina3ds_qt/main.cpp similarity index 96% rename from src/citra_qt/main.cpp rename to src/lucina3ds_qt/main.cpp index 049f99a7..c5c1247b 100644 --- a/src/citra_qt/main.cpp +++ b/src/lucina3ds_qt/main.cpp @@ -27,43 +27,43 @@ #include #include "common/linux/gamemode.h" #endif -#include "citra_qt/aboutdialog.h" -#include "citra_qt/applets/mii_selector.h" -#include "citra_qt/applets/swkbd.h" -#include "citra_qt/bootmanager.h" -#include "citra_qt/camera/qt_multimedia_camera.h" -#include "citra_qt/camera/still_image_camera.h" -#include "citra_qt/compatdb.h" -#include "citra_qt/compatibility_list.h" -#include "citra_qt/configuration/config.h" -#include "citra_qt/configuration/configure_dialog.h" -#include "citra_qt/configuration/configure_per_game.h" -#include "citra_qt/debugger/console.h" -#include "citra_qt/debugger/graphics/graphics.h" -#include "citra_qt/debugger/graphics/graphics_breakpoints.h" -#include "citra_qt/debugger/graphics/graphics_cmdlists.h" -#include "citra_qt/debugger/graphics/graphics_surface.h" -#include "citra_qt/debugger/graphics/graphics_tracing.h" -#include "citra_qt/debugger/graphics/graphics_vertex_shader.h" -#include "citra_qt/debugger/ipc/recorder.h" -#include "citra_qt/debugger/lle_service_modules.h" -#include "citra_qt/debugger/profiler.h" -#include "citra_qt/debugger/registers.h" -#include "citra_qt/debugger/wait_tree.h" -#include "citra_qt/discord.h" -#include "citra_qt/dumping/dumping_dialog.h" -#include "citra_qt/game_list.h" -#include "citra_qt/hotkeys.h" -#include "citra_qt/loading_screen.h" -#include "citra_qt/main.h" -#include "citra_qt/movie/movie_play_dialog.h" -#include "citra_qt/movie/movie_record_dialog.h" -#include "citra_qt/multiplayer/state.h" -#include "citra_qt/qt_image_interface.h" -#include "citra_qt/uisettings.h" -#include "citra_qt/updater/updater.h" -#include "citra_qt/util/clickable_label.h" -#include "citra_qt/util/graphics_device_info.h" +#include "lucina3ds_qt/aboutdialog.h" +#include "lucina3ds_qt/applets/mii_selector.h" +#include "lucina3ds_qt/applets/swkbd.h" +#include "lucina3ds_qt/bootmanager.h" +#include "lucina3ds_qt/camera/qt_multimedia_camera.h" +#include "lucina3ds_qt/camera/still_image_camera.h" +#include "lucina3ds_qt/compatdb.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/configuration/config.h" +#include "lucina3ds_qt/configuration/configure_dialog.h" +#include "lucina3ds_qt/configuration/configure_per_game.h" +#include "lucina3ds_qt/debugger/console.h" +#include "lucina3ds_qt/debugger/graphics/graphics.h" +#include "lucina3ds_qt/debugger/graphics/graphics_breakpoints.h" +#include "lucina3ds_qt/debugger/graphics/graphics_cmdlists.h" +#include "lucina3ds_qt/debugger/graphics/graphics_surface.h" +#include "lucina3ds_qt/debugger/graphics/graphics_tracing.h" +#include "lucina3ds_qt/debugger/graphics/graphics_vertex_shader.h" +#include "lucina3ds_qt/debugger/ipc/recorder.h" +#include "lucina3ds_qt/debugger/lle_service_modules.h" +#include "lucina3ds_qt/debugger/profiler.h" +#include "lucina3ds_qt/debugger/registers.h" +#include "lucina3ds_qt/debugger/wait_tree.h" +#include "lucina3ds_qt/discord.h" +#include "lucina3ds_qt/dumping/dumping_dialog.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/hotkeys.h" +#include "lucina3ds_qt/loading_screen.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/qt_image_interface.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/graphics_device_info.h" #include "common/arch.h" #include "common/common_paths.h" #include "common/detached_tasks.h" @@ -75,7 +75,7 @@ #include "common/memory_detect.h" #include "common/scm_rev.h" #include "common/scope_exit.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include "common/x64/cpu_detect.h" #endif #include "common/settings.h" @@ -102,7 +102,7 @@ #endif #ifdef USE_DISCORD_PRESENCE -#include "citra_qt/discord_impl.h" +#include "lucina3ds_qt/discord_impl.h" #endif #ifdef QT_STATICPLUGIN @@ -211,9 +211,9 @@ GMainWindow::GMainWindow(Core::System& system_) ConnectMenuEvents(); ConnectWidgetEvents(); - LOG_INFO(Frontend, "Citra Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, + LOG_INFO(Frontend, "Lucina3DS Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) const auto& caps = Common::GetCPUCaps(); std::string cpu_string = caps.cpu_string; if (caps.avx || caps.avx2 || caps.avx512) { @@ -328,7 +328,7 @@ GMainWindow::~GMainWindow() { } void GMainWindow::InitializeWidgets() { -#ifdef CITRA_ENABLE_COMPATIBILITY_REPORTING +#ifdef LUCINA3DS_ENABLE_COMPATIBILITY_REPORTING ui->action_Report_Compatibility->setVisible(true); #endif render_window = new GRenderWindow(this, emu_thread.get(), system, false); @@ -612,7 +612,7 @@ void GMainWindow::InitializeHotkeys() { link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); - link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Citra")); + link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Lucina3DS")); link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); @@ -940,7 +940,7 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); // Help - connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder); + connect_menu(ui->action_Open_Lucina3DS_Folder, &GMainWindow::OnOpenLucina3DSFolder); connect_menu(ui->action_Open_Log_Folder, []() { QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); QDesktopServices::openUrl(QUrl::fromLocalFile(path)); @@ -948,7 +948,7 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_FAQ, []() { QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/wiki/faq/"))); }); - connect_menu(ui->action_About, &GMainWindow::OnMenuAboutCitra); + connect_menu(ui->action_About, &GMainWindow::OnMenuAboutLucina3DS); #if ENABLE_QT_UPDATER connect_menu(ui->action_Check_For_Updates, &GMainWindow::OnCheckForUpdates); @@ -1097,7 +1097,7 @@ static std::optional HoldWakeLockLinux(u32 window_id = 0) { //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the //: computer from sleeping options.insert(QString::fromLatin1("reason"), - QCoreApplication::translate("GMainWindow", "Citra is running a game")); + QCoreApplication::translate("GMainWindow", "Luinca3DS is running a game")); // 0x4: Suspend lock; 0x8: Idle lock QDBusReply reply = xdp.call(QString::fromLatin1("Inhibit"), @@ -1210,7 +1210,7 @@ bool GMainWindow::LoadROM(const QString& filename) { case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: QMessageBox::critical(this, tr("Unsupported ROM"), - tr("GBA Virtual Console ROMs are not supported by Citra.")); + tr("GBA Virtual Console ROMs are not supported by Lucina3DS.")); break; case Core::System::ResultStatus::ErrorArticDisconnected: @@ -1261,7 +1261,7 @@ void GMainWindow::BootGame(const QString& filename) { show_artic_label = is_artic; - LOG_INFO(Frontend, "Citra starting..."); + LOG_INFO(Frontend, "Lucinca3DS starting..."); if (!is_artic) { StoreRecentFile(filename); // Put the filename on top of the list } @@ -1701,7 +1701,7 @@ void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { const auto& [base, update] = future_watcher->result(); if (base != Loader::ResultStatus::Success) { QMessageBox::critical( - this, tr("Citra"), + this, tr("Lucina3DS"), tr("Could not dump base RomFS.\nRefer to the log for details.")); return; } @@ -1863,7 +1863,7 @@ void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString case Service::AM::InstallStatus::ErrorEncrypted: QMessageBox::critical(this, tr("Encrypted File"), tr("%1 must be decrypted " - "before being used with Citra. A real 3DS is required.") + "before being used with Lucina3DS. A real 3DS is required.") .arg(filename)); break; case Service::AM::InstallStatus::ErrorFileNotFound: @@ -1925,9 +1925,9 @@ void GMainWindow::UninstallTitles( future_watcher.waitForFinished(); if (failed) { - QMessageBox::critical(this, tr("Citra"), tr("Failed to uninstall '%1'.").arg(failed_name)); + QMessageBox::critical(this, tr("Lucina3DS"), tr("Failed to uninstall '%1'.").arg(failed_name)); } else if (!future_watcher.isCanceled()) { - QMessageBox::information(this, tr("Citra"), + QMessageBox::information(this, tr("Lucina3DS"), tr("Successfully uninstalled '%1'.").arg(first_name)); } } @@ -2015,7 +2015,7 @@ void GMainWindow::OnLoadComplete() { } void GMainWindow::OnMenuReportCompatibility() { - if (!NetSettings::values.citra_token.empty() && !NetSettings::values.citra_username.empty()) { + if (!NetSettings::values.lucina3ds_token.empty() && !NetSettings::values.lucina3ds_username.empty()) { CompatDB compatdb{this}; compatdb.exec(); } else { @@ -2319,7 +2319,7 @@ void GMainWindow::OnRemoveAmiibo() { ui->action_Remove_Amiibo->setEnabled(false); } -void GMainWindow::OnOpenCitraFolder() { +void GMainWindow::OnOpenLucina3DSFolder() { QDesktopServices::openUrl(QUrl::fromLocalFile( QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); } @@ -2477,7 +2477,7 @@ void GMainWindow::OnDumpVideo() { message_box.setText( tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." #ifdef _WIN32 - "\n\nTo install FFmpeg to Citra, press Open and select your FFmpeg directory." + "\n\nTo install FFmpeg to Lucina3DS, press Open and select your FFmpeg directory." #endif "\n\nTo view a guide on how to install FFmpeg, press Help.")); message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help @@ -2521,7 +2521,7 @@ void GMainWindow::OnOpenFFmpeg() { for (auto& library_name : library_names) { if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { - QMessageBox::critical(this, tr("Citra"), + QMessageBox::critical(this, tr("Lucina3DS"), tr("The provided FFmpeg directory is missing %1. Please make " "sure the correct directory was selected.") .arg(QString::fromStdString(library_name))); @@ -2545,9 +2545,9 @@ void GMainWindow::OnOpenFFmpeg() { FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); if (success.load()) { - QMessageBox::information(this, tr("Citra"), tr("FFmpeg has been sucessfully installed.")); + QMessageBox::information(this, tr("Lucina3DS"), tr("FFmpeg has been sucessfully installed.")); } else { - QMessageBox::critical(this, tr("Citra"), + QMessageBox::critical(this, tr("Lucina3DS"), tr("Installation of FFmpeg failed. Check the log file for details.")); } } @@ -2577,7 +2577,7 @@ void GMainWindow::StartVideoDumping(const QString& path) { system.RegisterVideoDumper(dumper); } else { QMessageBox::critical( - this, tr("Citra"), + this, tr("Lucina3DS"), tr("Could not start video dumping.
Refer to the log for details.")); ui->action_Dump_Video->setChecked(false); } @@ -2927,7 +2927,7 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det } } -void GMainWindow::OnMenuAboutCitra() { +void GMainWindow::OnMenuAboutLucina3DS() { AboutDialog about{this}; about.exec(); } @@ -2938,7 +2938,7 @@ bool GMainWindow::ConfirmClose() { } QMessageBox::StandardButton answer = - QMessageBox::question(this, tr("Citra"), tr("Would you like to exit now?"), + QMessageBox::question(this, tr("Lucina3DS"), tr("Would you like to exit now?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); return answer != QMessageBox::No; } @@ -3031,7 +3031,7 @@ bool GMainWindow::ConfirmChangeGame() { } auto answer = QMessageBox::question( - this, tr("Citra"), tr("The game is still running. Would you like to stop emulation?"), + this, tr("Lucina3DS"), tr("The game is still running. Would you like to stop emulation?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); return answer != QMessageBox::No; } @@ -3157,12 +3157,12 @@ void GMainWindow::UpdateWindowTitle() { const QString full_name = QString::fromUtf8(Common::g_build_fullname); if (game_title.isEmpty()) { - setWindowTitle(QStringLiteral("Citra %1").arg(full_name)); + setWindowTitle(QStringLiteral("%1").arg(full_name)); } else { - setWindowTitle(QStringLiteral("Citra %1 | %2").arg(full_name, game_title)); + setWindowTitle(QStringLiteral("%1 | %2").arg(full_name, game_title)); render_window->setWindowTitle( - QStringLiteral("Citra %1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); - secondary_window->setWindowTitle(QStringLiteral("Citra %1 | %2 | %3") + QStringLiteral("%1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); + secondary_window->setWindowTitle(QStringLiteral("%1 | %2 | %3") .arg(full_name, game_title, tr("Secondary Window"))); } } @@ -3292,8 +3292,8 @@ int main(int argc, char* argv[]) { SCOPE_EXIT({ MicroProfileShutdown(); }); // Init settings params - QCoreApplication::setOrganizationName(QStringLiteral("Citra team")); - QCoreApplication::setApplicationName(QStringLiteral("Citra")); + QCoreApplication::setOrganizationName(QStringLiteral("Citra team | Lucina3DS")); + QCoreApplication::setApplicationName(QStringLiteral("Lucina3DS")); auto rounding_policy = GetHighDpiRoundingPolicy(); QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); @@ -3311,6 +3311,8 @@ int main(int argc, char* argv[]) { #endif QApplication app(argc, argv); + app.setWindowIcon(QIcon(QString::fromUtf8("/usr/share/icons/hicolor/512x512/apps/lucina3ds.png"))); + // Qt changes the locale and causes issues in float conversion using std::to_string() when // generating shaders @@ -3334,6 +3336,7 @@ int main(int argc, char* argv[]) { system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); #endif + main_window.setWindowIcon(QIcon(QString::fromUtf8("/usr/share/icons/hicolor/512x512/apps/lucina3ds.png"))); main_window.show(); QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, diff --git a/src/citra_qt/main.h b/src/lucina3ds_qt/main.h similarity index 98% rename from src/citra_qt/main.h rename to src/lucina3ds_qt/main.h index fb6fc3ab..8c062923 100644 --- a/src/citra_qt/main.h +++ b/src/lucina3ds_qt/main.h @@ -12,8 +12,8 @@ #include #include #include -#include "citra_qt/compatibility_list.h" -#include "citra_qt/hotkeys.h" +#include "lucina3ds_qt/compatibility_list.h" +#include "lucina3ds_qt/hotkeys.h" #include "core/core.h" #include "core/savestate.h" @@ -225,7 +225,7 @@ private slots: void OnConfigure(); void OnLoadAmiibo(); void OnRemoveAmiibo(); - void OnOpenCitraFolder(); + void OnOpenLucina3DSFolder(); void OnToggleFilterBar(); void OnDisplayTitleBars(bool); void InitializeHotkeys(); @@ -255,8 +255,8 @@ private slots: void StartVideoDumping(const QString& path); void OnStopVideoDumping(); void OnCoreError(Core::System::ResultStatus, std::string); - /// Called whenever a user selects Help->About Citra - void OnMenuAboutCitra(); + /// Called whenever a user selects Help->About Lucina3DS. + void OnMenuAboutLucina3DS(); #if ENABLE_QT_UPDATER void OnUpdateFound(bool found, bool error); diff --git a/src/citra_qt/main.ui b/src/lucina3ds_qt/main.ui similarity index 81% rename from src/citra_qt/main.ui rename to src/lucina3ds_qt/main.ui index 633265fc..20a7ff3b 100644 --- a/src/citra_qt/main.ui +++ b/src/lucina3ds_qt/main.ui @@ -11,14 +11,14 @@
- Citra + Lucina3DS - - dist/citra.pngdist/citra.png + + :/icons/default/256x256/lucina3ds.png:/icons/default/256x256/lucina3ds.png - QTabWidget::Rounded + QTabWidget::TabShape::Rounded true @@ -45,7 +45,7 @@ 0 0 1081 - 22 + 30 @@ -54,7 +54,7 @@
- Boot Home Menu + &Boot Home Menu @@ -66,12 +66,12 @@ - Recent Files + &Recent Files - Amiibo + &Amiibo @@ -85,7 +85,7 @@ - + @@ -95,14 +95,14 @@ - Save State + S&ave State - Load State + &Load State @@ -123,14 +123,14 @@ - Debugging + D&ebugging - Screen Layout + Screen &Layout @@ -157,7 +157,7 @@ true - Multiplayer + &Multiplayer @@ -168,11 +168,11 @@ - Tools + &Tools - Movie + &Movie @@ -183,7 +183,7 @@ - Frame Advance + &Frame Advance @@ -216,52 +216,52 @@ - Load File... + &Load File... - Install CIA... + &Install CIA... - - Connect to Artic Base... - + + &Connect to Artic Base... + - JPN + &JPN - USA + &USA - EUR + &EUR - AUS + &AUS - CHN + &CHN - KOR + &KOR - TWN + &TWN @@ -303,15 +303,15 @@ - FAQ + &FAQ - About Citra + &About Lucina3DS - QAction::AboutRole + QAction::MenuRole::AboutRole @@ -319,25 +319,25 @@ true - Single Window Mode + &Single Window Mode - Save to Oldest Slot + &Save to Oldest Slot - Load from Newest Slot + &Load from Newest Slot - Configure... + &Configure... - QAction::PreferencesRole + QAction::MenuRole::PreferencesRole @@ -345,7 +345,7 @@ true - Display Dock Widget Headers + &Display Dock Widget Headers @@ -353,7 +353,7 @@ true - Show Filter Bar + Show Filter &Bar @@ -361,27 +361,27 @@ true - Show Status Bar + S&how Status Bar - Create Pica Surface Viewer + &Create Pica Surface Viewer - Record... + &Record... - Play... + &Play... - Close + &Close @@ -389,7 +389,7 @@ false - Save without Closing + &Save without Closing @@ -400,7 +400,7 @@ true - Read-Only Mode + Read-&Only Mode @@ -408,7 +408,7 @@ true - Enable Frame Advancing + &Enable Frame Advancing @@ -416,7 +416,7 @@ false - Advance Frame + &Advance Frame @@ -424,7 +424,7 @@ false - Capture Screenshot + &Capture Screenshot @@ -432,7 +432,7 @@ true - Dump Video + &Dump Video @@ -440,7 +440,7 @@ true - Browse Public Game Lobby + &Browse Public Game Lobby @@ -448,7 +448,7 @@ true - Create Room + &Create Room @@ -456,12 +456,12 @@ false - Leave Room + &Leave Room - Direct Connect to Room + &Direct Connect to Room @@ -469,7 +469,7 @@ false - Show Current Room + &Show Current Room @@ -477,12 +477,12 @@ true - Fullscreen + &Fullscreen - Open Log Folder + &Open Log Folder Opens the Citra Log folder @@ -490,7 +490,7 @@ - Modify Citra Install + &Modify Citra Install Opens the maintenance tool to modify your Citra installation @@ -501,7 +501,7 @@ true - Default + &Default @@ -509,7 +509,7 @@ true - Single Screen + &Single Screen @@ -517,7 +517,7 @@ true - Large Screen + &Large Screen @@ -525,7 +525,7 @@ true - Hybrid Screen + &Hybrid Screen @@ -533,7 +533,7 @@ true - Side by Side + Side &by Side @@ -541,7 +541,7 @@ true - Separate Windows + Separate &Windows @@ -549,7 +549,7 @@ true - Swap Screens + Sw&ap Screens @@ -557,12 +557,12 @@ true - Rotate Upright + &Rotate Upright - Check for Updates + &Check for Updates @@ -570,7 +570,7 @@ false - Report Compatibility + &Report Compatibility false @@ -581,7 +581,7 @@ false - Restart + &Restart @@ -589,7 +589,7 @@ false - Load... + &Load... @@ -597,12 +597,18 @@ false - Remove + &Remove - + - Open Citra Folder + &Open Citra Folder + + + Open Lucina3DS Folder + + + Open Lucina3DS Folder @@ -610,13 +616,15 @@ false - Configure Current Game... + Configure Current &Game... - QAction::NoRole + QAction::MenuRole::NoRole - + + + diff --git a/src/citra_qt/movie/movie_play_dialog.cpp b/src/lucina3ds_qt/movie/movie_play_dialog.cpp similarity index 96% rename from src/citra_qt/movie/movie_play_dialog.cpp rename to src/lucina3ds_qt/movie/movie_play_dialog.cpp index 6df973af..123c61ce 100644 --- a/src/citra_qt/movie/movie_play_dialog.cpp +++ b/src/lucina3ds_qt/movie/movie_play_dialog.cpp @@ -5,10 +5,10 @@ #include #include #include -#include "citra_qt/game_list.h" -#include "citra_qt/game_list_p.h" -#include "citra_qt/movie/movie_play_dialog.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/game_list.h" +#include "lucina3ds_qt/game_list_p.h" +#include "lucina3ds_qt/movie/movie_play_dialog.h" +#include "lucina3ds_qt/uisettings.h" #include "core/core.h" #include "core/core_timing.h" #include "core/hle/service/hid/hid.h" diff --git a/src/citra_qt/movie/movie_play_dialog.h b/src/lucina3ds_qt/movie/movie_play_dialog.h similarity index 100% rename from src/citra_qt/movie/movie_play_dialog.h rename to src/lucina3ds_qt/movie/movie_play_dialog.h diff --git a/src/citra_qt/movie/movie_play_dialog.ui b/src/lucina3ds_qt/movie/movie_play_dialog.ui similarity index 100% rename from src/citra_qt/movie/movie_play_dialog.ui rename to src/lucina3ds_qt/movie/movie_play_dialog.ui diff --git a/src/citra_qt/movie/movie_record_dialog.cpp b/src/lucina3ds_qt/movie/movie_record_dialog.cpp similarity index 95% rename from src/citra_qt/movie/movie_record_dialog.cpp rename to src/lucina3ds_qt/movie/movie_record_dialog.cpp index b6891453..a7640a50 100644 --- a/src/citra_qt/movie/movie_record_dialog.cpp +++ b/src/lucina3ds_qt/movie/movie_record_dialog.cpp @@ -4,8 +4,8 @@ #include #include -#include "citra_qt/movie/movie_record_dialog.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/movie/movie_record_dialog.h" +#include "lucina3ds_qt/uisettings.h" #include "core/core.h" #include "core/movie.h" #include "ui_movie_record_dialog.h" diff --git a/src/citra_qt/movie/movie_record_dialog.h b/src/lucina3ds_qt/movie/movie_record_dialog.h similarity index 100% rename from src/citra_qt/movie/movie_record_dialog.h rename to src/lucina3ds_qt/movie/movie_record_dialog.h diff --git a/src/citra_qt/movie/movie_record_dialog.ui b/src/lucina3ds_qt/movie/movie_record_dialog.ui similarity index 100% rename from src/citra_qt/movie/movie_record_dialog.ui rename to src/lucina3ds_qt/movie/movie_record_dialog.ui diff --git a/src/citra_qt/multiplayer/chat_room.cpp b/src/lucina3ds_qt/multiplayer/chat_room.cpp similarity index 99% rename from src/citra_qt/multiplayer/chat_room.cpp rename to src/lucina3ds_qt/multiplayer/chat_room.cpp index 1e4f2ff0..9482ef95 100644 --- a/src/citra_qt/multiplayer/chat_room.cpp +++ b/src/lucina3ds_qt/multiplayer/chat_room.cpp @@ -16,9 +16,9 @@ #include #include #include -#include "citra_qt/game_list_p.h" -#include "citra_qt/multiplayer/chat_room.h" -#include "citra_qt/multiplayer/message.h" +#include "lucina3ds_qt/game_list_p.h" +#include "lucina3ds_qt/multiplayer/chat_room.h" +#include "lucina3ds_qt/multiplayer/message.h" #include "common/logging/log.h" #include "network/announce_multiplayer_session.h" #include "ui_chat_room.h" diff --git a/src/citra_qt/multiplayer/chat_room.h b/src/lucina3ds_qt/multiplayer/chat_room.h similarity index 100% rename from src/citra_qt/multiplayer/chat_room.h rename to src/lucina3ds_qt/multiplayer/chat_room.h diff --git a/src/citra_qt/multiplayer/chat_room.ui b/src/lucina3ds_qt/multiplayer/chat_room.ui similarity index 100% rename from src/citra_qt/multiplayer/chat_room.ui rename to src/lucina3ds_qt/multiplayer/chat_room.ui diff --git a/src/citra_qt/multiplayer/client_room.cpp b/src/lucina3ds_qt/multiplayer/client_room.cpp similarity index 94% rename from src/citra_qt/multiplayer/client_room.cpp rename to src/lucina3ds_qt/multiplayer/client_room.cpp index 312090cb..5b8b499b 100644 --- a/src/citra_qt/multiplayer/client_room.cpp +++ b/src/lucina3ds_qt/multiplayer/client_room.cpp @@ -10,11 +10,11 @@ #include #include #include -#include "citra_qt/game_list_p.h" -#include "citra_qt/multiplayer/client_room.h" -#include "citra_qt/multiplayer/message.h" -#include "citra_qt/multiplayer/moderation_dialog.h" -#include "citra_qt/multiplayer/state.h" +#include "lucina3ds_qt/game_list_p.h" +#include "lucina3ds_qt/multiplayer/client_room.h" +#include "lucina3ds_qt/multiplayer/message.h" +#include "lucina3ds_qt/multiplayer/moderation_dialog.h" +#include "lucina3ds_qt/multiplayer/state.h" #include "common/logging/log.h" #include "network/announce_multiplayer_session.h" #include "ui_client_room.h" diff --git a/src/citra_qt/multiplayer/client_room.h b/src/lucina3ds_qt/multiplayer/client_room.h similarity index 94% rename from src/citra_qt/multiplayer/client_room.h rename to src/lucina3ds_qt/multiplayer/client_room.h index cbbcff61..353ae8fb 100644 --- a/src/citra_qt/multiplayer/client_room.h +++ b/src/lucina3ds_qt/multiplayer/client_room.h @@ -4,7 +4,7 @@ #pragma once -#include "citra_qt/multiplayer/chat_room.h" +#include "lucina3ds_qt/multiplayer/chat_room.h" namespace Ui { class ClientRoom; diff --git a/src/citra_qt/multiplayer/client_room.ui b/src/lucina3ds_qt/multiplayer/client_room.ui similarity index 100% rename from src/citra_qt/multiplayer/client_room.ui rename to src/lucina3ds_qt/multiplayer/client_room.ui diff --git a/src/citra_qt/multiplayer/direct_connect.cpp b/src/lucina3ds_qt/multiplayer/direct_connect.cpp similarity index 92% rename from src/citra_qt/multiplayer/direct_connect.cpp rename to src/lucina3ds_qt/multiplayer/direct_connect.cpp index bdc866a7..7115cbb8 100644 --- a/src/citra_qt/multiplayer/direct_connect.cpp +++ b/src/lucina3ds_qt/multiplayer/direct_connect.cpp @@ -8,11 +8,11 @@ #include #include #include -#include "citra_qt/main.h" -#include "citra_qt/multiplayer/direct_connect.h" -#include "citra_qt/multiplayer/message.h" -#include "citra_qt/multiplayer/validation.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/multiplayer/direct_connect.h" +#include "lucina3ds_qt/multiplayer/message.h" +#include "lucina3ds_qt/multiplayer/validation.h" +#include "lucina3ds_qt/uisettings.h" #include "core/hle/service/cfg/cfg.h" #include "network/network.h" #include "network/network_settings.h" @@ -32,9 +32,9 @@ DirectConnectWindow::DirectConnectWindow(Core::System& system_, QWidget* parent) ui->nickname->setValidator(validation.GetNickname()); ui->nickname->setText(UISettings::values.nickname); - if (ui->nickname->text().isEmpty() && !NetSettings::values.citra_username.empty()) { + if (ui->nickname->text().isEmpty() && !NetSettings::values.lucina3ds_username.empty()) { // Use Citra Web Service user name as nickname by default - ui->nickname->setText(QString::fromStdString(NetSettings::values.citra_username)); + ui->nickname->setText(QString::fromStdString(NetSettings::values.lucina3ds_username)); } ui->ip->setValidator(validation.GetIP()); ui->ip->setText(UISettings::values.ip); diff --git a/src/citra_qt/multiplayer/direct_connect.h b/src/lucina3ds_qt/multiplayer/direct_connect.h similarity index 94% rename from src/citra_qt/multiplayer/direct_connect.h rename to src/lucina3ds_qt/multiplayer/direct_connect.h index b03fa06c..dbf40ea8 100644 --- a/src/citra_qt/multiplayer/direct_connect.h +++ b/src/lucina3ds_qt/multiplayer/direct_connect.h @@ -7,7 +7,7 @@ #include #include #include -#include "citra_qt/multiplayer/validation.h" +#include "lucina3ds_qt/multiplayer/validation.h" namespace Ui { class DirectConnect; diff --git a/src/citra_qt/multiplayer/direct_connect.ui b/src/lucina3ds_qt/multiplayer/direct_connect.ui similarity index 100% rename from src/citra_qt/multiplayer/direct_connect.ui rename to src/lucina3ds_qt/multiplayer/direct_connect.ui diff --git a/src/citra_qt/multiplayer/host_room.cpp b/src/lucina3ds_qt/multiplayer/host_room.cpp similarity index 93% rename from src/citra_qt/multiplayer/host_room.cpp rename to src/lucina3ds_qt/multiplayer/host_room.cpp index 8a1ca02e..cfbdfb99 100644 --- a/src/citra_qt/multiplayer/host_room.cpp +++ b/src/lucina3ds_qt/multiplayer/host_room.cpp @@ -11,12 +11,12 @@ #include #include #include -#include "citra_qt/game_list_p.h" -#include "citra_qt/multiplayer/host_room.h" -#include "citra_qt/multiplayer/message.h" -#include "citra_qt/multiplayer/state.h" -#include "citra_qt/multiplayer/validation.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/game_list_p.h" +#include "lucina3ds_qt/multiplayer/host_room.h" +#include "lucina3ds_qt/multiplayer/message.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/multiplayer/validation.h" +#include "lucina3ds_qt/uisettings.h" #include "common/logging/log.h" #include "core/hle/service/cfg/cfg.h" #include "network/announce_multiplayer_session.h" @@ -52,9 +52,9 @@ HostRoomWindow::HostRoomWindow(Core::System& system_, QWidget* parent, QStandard // Restore the settings: ui->username->setText(UISettings::values.room_nickname); - if (ui->username->text().isEmpty() && !NetSettings::values.citra_username.empty()) { + if (ui->username->text().isEmpty() && !NetSettings::values.lucina3ds_username.empty()) { // Use Citra Web Service user name as nickname by default - ui->username->setText(QString::fromStdString(NetSettings::values.citra_username)); + ui->username->setText(QString::fromStdString(NetSettings::values.lucina3ds_username)); } ui->room_name->setText(UISettings::values.room_name); ui->port->setText(UISettings::values.room_port); @@ -144,7 +144,7 @@ void HostRoomWindow::Host() { bool created = room->Create(ui->room_name->text().toStdString(), ui->room_description->toPlainText().toStdString(), "", port, password, ui->max_player->value(), - NetSettings::values.citra_username, game_name.toStdString(), + NetSettings::values.lucina3ds_username, game_name.toStdString(), game_id, CreateVerifyBackend(is_public), ban_list); if (!created) { NetworkMessage::ErrorManager::ShowError( @@ -183,8 +183,8 @@ void HostRoomWindow::Host() { #ifdef ENABLE_WEB_SERVICE if (is_public) { WebService::Client client(NetSettings::values.web_api_url, - NetSettings::values.citra_username, - NetSettings::values.citra_token); + NetSettings::values.lucina3ds_username, + NetSettings::values.lucina3ds_token); if (auto room = Network::GetRoom().lock()) { token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; } diff --git a/src/citra_qt/multiplayer/host_room.h b/src/lucina3ds_qt/multiplayer/host_room.h similarity index 97% rename from src/citra_qt/multiplayer/host_room.h rename to src/lucina3ds_qt/multiplayer/host_room.h index e481bd00..1bb941ab 100644 --- a/src/citra_qt/multiplayer/host_room.h +++ b/src/lucina3ds_qt/multiplayer/host_room.h @@ -9,7 +9,7 @@ #include #include #include -#include "citra_qt/multiplayer/validation.h" +#include "lucina3ds_qt/multiplayer/validation.h" namespace Ui { class HostRoom; diff --git a/src/citra_qt/multiplayer/host_room.ui b/src/lucina3ds_qt/multiplayer/host_room.ui similarity index 100% rename from src/citra_qt/multiplayer/host_room.ui rename to src/lucina3ds_qt/multiplayer/host_room.ui diff --git a/src/citra_qt/multiplayer/lobby.cpp b/src/lucina3ds_qt/multiplayer/lobby.cpp similarity index 95% rename from src/citra_qt/multiplayer/lobby.cpp rename to src/lucina3ds_qt/multiplayer/lobby.cpp index 00671b84..69506406 100644 --- a/src/citra_qt/multiplayer/lobby.cpp +++ b/src/lucina3ds_qt/multiplayer/lobby.cpp @@ -5,13 +5,13 @@ #include #include #include -#include "citra_qt/game_list_p.h" -#include "citra_qt/main.h" -#include "citra_qt/multiplayer/lobby.h" -#include "citra_qt/multiplayer/lobby_p.h" -#include "citra_qt/multiplayer/message.h" -#include "citra_qt/multiplayer/validation.h" -#include "citra_qt/uisettings.h" +#include "lucina3ds_qt/game_list_p.h" +#include "lucina3ds_qt/main.h" +#include "lucina3ds_qt/multiplayer/lobby.h" +#include "lucina3ds_qt/multiplayer/lobby_p.h" +#include "lucina3ds_qt/multiplayer/message.h" +#include "lucina3ds_qt/multiplayer/validation.h" +#include "lucina3ds_qt/uisettings.h" #include "common/logging/log.h" #include "core/hle/service/cfg/cfg.h" #include "network/network.h" @@ -56,9 +56,9 @@ Lobby::Lobby(Core::System& system_, QWidget* parent, QStandardItemModel* list, ui->nickname->setValidator(validation.GetNickname()); ui->nickname->setText(UISettings::values.nickname); - if (ui->nickname->text().isEmpty() && !NetSettings::values.citra_username.empty()) { + if (ui->nickname->text().isEmpty() && !NetSettings::values.lucina3ds_username.empty()) { // Use Citra Web Service user name as nickname by default - ui->nickname->setText(QString::fromStdString(NetSettings::values.citra_username)); + ui->nickname->setText(QString::fromStdString(NetSettings::values.lucina3ds_username)); } // UI Buttons @@ -160,11 +160,11 @@ void Lobby::OnJoinRoom(const QModelIndex& source) { QFuture f = QtConcurrent::run([this, nickname, ip, port, password, verify_UID] { std::string token; #ifdef ENABLE_WEB_SERVICE - if (!NetSettings::values.citra_username.empty() && - !NetSettings::values.citra_token.empty()) { + if (!NetSettings::values.lucina3ds_username.empty() && + !NetSettings::values.lucina3ds_token.empty()) { WebService::Client client(NetSettings::values.web_api_url, - NetSettings::values.citra_username, - NetSettings::values.citra_token); + NetSettings::values.lucina3ds_username, + NetSettings::values.lucina3ds_token); token = client.GetExternalJWT(verify_UID).returned_data; if (token.empty()) { LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); diff --git a/src/citra_qt/multiplayer/lobby.h b/src/lucina3ds_qt/multiplayer/lobby.h similarity index 98% rename from src/citra_qt/multiplayer/lobby.h rename to src/lucina3ds_qt/multiplayer/lobby.h index f245c7ac..1a1b95df 100644 --- a/src/citra_qt/multiplayer/lobby.h +++ b/src/lucina3ds_qt/multiplayer/lobby.h @@ -9,7 +9,7 @@ #include #include #include -#include "citra_qt/multiplayer/validation.h" +#include "lucina3ds_qt/multiplayer/validation.h" #include "common/announce_multiplayer_room.h" #include "network/announce_multiplayer_session.h" #include "network/room_member.h" diff --git a/src/citra_qt/multiplayer/lobby.ui b/src/lucina3ds_qt/multiplayer/lobby.ui similarity index 100% rename from src/citra_qt/multiplayer/lobby.ui rename to src/lucina3ds_qt/multiplayer/lobby.ui diff --git a/src/citra_qt/multiplayer/lobby_p.h b/src/lucina3ds_qt/multiplayer/lobby_p.h similarity index 100% rename from src/citra_qt/multiplayer/lobby_p.h rename to src/lucina3ds_qt/multiplayer/lobby_p.h diff --git a/src/citra_qt/multiplayer/message.cpp b/src/lucina3ds_qt/multiplayer/message.cpp similarity index 98% rename from src/citra_qt/multiplayer/message.cpp rename to src/lucina3ds_qt/multiplayer/message.cpp index beb45bbe..fab7fb97 100644 --- a/src/citra_qt/multiplayer/message.cpp +++ b/src/lucina3ds_qt/multiplayer/message.cpp @@ -5,7 +5,7 @@ #include #include -#include "citra_qt/multiplayer/message.h" +#include "lucina3ds_qt/multiplayer/message.h" namespace NetworkMessage { const ConnectionError ErrorManager::USERNAME_NOT_VALID( diff --git a/src/citra_qt/multiplayer/message.h b/src/lucina3ds_qt/multiplayer/message.h similarity index 100% rename from src/citra_qt/multiplayer/message.h rename to src/lucina3ds_qt/multiplayer/message.h diff --git a/src/citra_qt/multiplayer/moderation_dialog.cpp b/src/lucina3ds_qt/multiplayer/moderation_dialog.cpp similarity index 98% rename from src/citra_qt/multiplayer/moderation_dialog.cpp rename to src/lucina3ds_qt/multiplayer/moderation_dialog.cpp index def08466..f758148e 100644 --- a/src/citra_qt/multiplayer/moderation_dialog.cpp +++ b/src/lucina3ds_qt/multiplayer/moderation_dialog.cpp @@ -4,7 +4,7 @@ #include #include -#include "citra_qt/multiplayer/moderation_dialog.h" +#include "lucina3ds_qt/multiplayer/moderation_dialog.h" #include "network/network.h" #include "network/room_member.h" #include "ui_moderation_dialog.h" diff --git a/src/citra_qt/multiplayer/moderation_dialog.h b/src/lucina3ds_qt/multiplayer/moderation_dialog.h similarity index 100% rename from src/citra_qt/multiplayer/moderation_dialog.h rename to src/lucina3ds_qt/multiplayer/moderation_dialog.h diff --git a/src/citra_qt/multiplayer/moderation_dialog.ui b/src/lucina3ds_qt/multiplayer/moderation_dialog.ui similarity index 100% rename from src/citra_qt/multiplayer/moderation_dialog.ui rename to src/lucina3ds_qt/multiplayer/moderation_dialog.ui diff --git a/src/citra_qt/multiplayer/state.cpp b/src/lucina3ds_qt/multiplayer/state.cpp similarity index 96% rename from src/citra_qt/multiplayer/state.cpp rename to src/lucina3ds_qt/multiplayer/state.cpp index 72a36c6a..23301c23 100644 --- a/src/citra_qt/multiplayer/state.cpp +++ b/src/lucina3ds_qt/multiplayer/state.cpp @@ -7,14 +7,14 @@ #include #include #include -#include "citra_qt/multiplayer/client_room.h" -#include "citra_qt/multiplayer/direct_connect.h" -#include "citra_qt/multiplayer/host_room.h" -#include "citra_qt/multiplayer/lobby.h" -#include "citra_qt/multiplayer/message.h" -#include "citra_qt/multiplayer/state.h" -#include "citra_qt/uisettings.h" -#include "citra_qt/util/clickable_label.h" +#include "lucina3ds_qt/multiplayer/client_room.h" +#include "lucina3ds_qt/multiplayer/direct_connect.h" +#include "lucina3ds_qt/multiplayer/host_room.h" +#include "lucina3ds_qt/multiplayer/lobby.h" +#include "lucina3ds_qt/multiplayer/message.h" +#include "lucina3ds_qt/multiplayer/state.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/util/clickable_label.h" #include "common/logging/log.h" MultiplayerState::MultiplayerState(Core::System& system_, QWidget* parent, diff --git a/src/citra_qt/multiplayer/state.h b/src/lucina3ds_qt/multiplayer/state.h similarity index 100% rename from src/citra_qt/multiplayer/state.h rename to src/lucina3ds_qt/multiplayer/state.h diff --git a/src/citra_qt/multiplayer/validation.h b/src/lucina3ds_qt/multiplayer/validation.h similarity index 100% rename from src/citra_qt/multiplayer/validation.h rename to src/lucina3ds_qt/multiplayer/validation.h diff --git a/src/citra_qt/precompiled_headers.h b/src/lucina3ds_qt/precompiled_headers.h similarity index 100% rename from src/citra_qt/precompiled_headers.h rename to src/lucina3ds_qt/precompiled_headers.h diff --git a/src/citra_qt/qt_image_interface.cpp b/src/lucina3ds_qt/qt_image_interface.cpp similarity index 96% rename from src/citra_qt/qt_image_interface.cpp rename to src/lucina3ds_qt/qt_image_interface.cpp index 7124f023..5809dc97 100644 --- a/src/citra_qt/qt_image_interface.cpp +++ b/src/lucina3ds_qt/qt_image_interface.cpp @@ -5,7 +5,7 @@ #include #include #include -#include "citra_qt/qt_image_interface.h" +#include "lucina3ds_qt/qt_image_interface.h" #include "common/logging/log.h" QtImageInterface::QtImageInterface() { diff --git a/src/citra_qt/qt_image_interface.h b/src/lucina3ds_qt/qt_image_interface.h similarity index 100% rename from src/citra_qt/qt_image_interface.h rename to src/lucina3ds_qt/qt_image_interface.h diff --git a/src/citra_qt/uisettings.cpp b/src/lucina3ds_qt/uisettings.cpp similarity index 100% rename from src/citra_qt/uisettings.cpp rename to src/lucina3ds_qt/uisettings.cpp diff --git a/src/citra_qt/uisettings.h b/src/lucina3ds_qt/uisettings.h similarity index 100% rename from src/citra_qt/uisettings.h rename to src/lucina3ds_qt/uisettings.h diff --git a/src/citra_qt/updater/updater.cpp b/src/lucina3ds_qt/updater/updater.cpp similarity index 98% rename from src/citra_qt/updater/updater.cpp rename to src/lucina3ds_qt/updater/updater.cpp index 00c32479..d3bc12cf 100644 --- a/src/citra_qt/updater/updater.cpp +++ b/src/lucina3ds_qt/updater/updater.cpp @@ -11,9 +11,9 @@ #include #include #include -#include "citra_qt/uisettings.h" -#include "citra_qt/updater/updater.h" -#include "citra_qt/updater/updater_p.h" +#include "lucina3ds_qt/uisettings.h" +#include "lucina3ds_qt/updater/updater.h" +#include "lucina3ds_qt/updater/updater_p.h" #include "common/file_util.h" #include "common/logging/log.h" diff --git a/src/citra_qt/updater/updater.h b/src/lucina3ds_qt/updater/updater.h similarity index 100% rename from src/citra_qt/updater/updater.h rename to src/lucina3ds_qt/updater/updater.h diff --git a/src/citra_qt/updater/updater_p.h b/src/lucina3ds_qt/updater/updater_p.h similarity index 97% rename from src/citra_qt/updater/updater_p.h rename to src/lucina3ds_qt/updater/updater_p.h index 549c98e8..dc98c4a3 100644 --- a/src/citra_qt/updater/updater_p.h +++ b/src/lucina3ds_qt/updater/updater_p.h @@ -9,7 +9,7 @@ #pragma once #include -#include "citra_qt/updater/updater.h" +#include "lucina3ds_qt/updater/updater.h" enum class XMLParseResult { Success, diff --git a/src/citra_qt/util/clickable_label.cpp b/src/lucina3ds_qt/util/clickable_label.cpp similarity index 87% rename from src/citra_qt/util/clickable_label.cpp rename to src/lucina3ds_qt/util/clickable_label.cpp index 2a1ea5df..86c78ff9 100644 --- a/src/citra_qt/util/clickable_label.cpp +++ b/src/lucina3ds_qt/util/clickable_label.cpp @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "citra_qt/util/clickable_label.h" +#include "lucina3ds_qt/util/clickable_label.h" ClickableLabel::ClickableLabel(QWidget* parent, [[maybe_unused]] Qt::WindowFlags f) : QLabel(parent) {} diff --git a/src/citra_qt/util/clickable_label.h b/src/lucina3ds_qt/util/clickable_label.h similarity index 100% rename from src/citra_qt/util/clickable_label.h rename to src/lucina3ds_qt/util/clickable_label.h diff --git a/src/citra_qt/util/graphics_device_info.cpp b/src/lucina3ds_qt/util/graphics_device_info.cpp similarity index 96% rename from src/citra_qt/util/graphics_device_info.cpp rename to src/lucina3ds_qt/util/graphics_device_info.cpp index 8653ba8b..b851c02d 100644 --- a/src/citra_qt/util/graphics_device_info.cpp +++ b/src/lucina3ds_qt/util/graphics_device_info.cpp @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "citra_qt/util/graphics_device_info.h" +#include "lucina3ds_qt/util/graphics_device_info.h" #ifdef ENABLE_OPENGL #include diff --git a/src/citra_qt/util/graphics_device_info.h b/src/lucina3ds_qt/util/graphics_device_info.h similarity index 100% rename from src/citra_qt/util/graphics_device_info.h rename to src/lucina3ds_qt/util/graphics_device_info.h diff --git a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp b/src/lucina3ds_qt/util/sequence_dialog/sequence_dialog.cpp similarity index 95% rename from src/citra_qt/util/sequence_dialog/sequence_dialog.cpp rename to src/lucina3ds_qt/util/sequence_dialog/sequence_dialog.cpp index 72d99b6b..7732ee44 100644 --- a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp +++ b/src/lucina3ds_qt/util/sequence_dialog/sequence_dialog.cpp @@ -5,7 +5,7 @@ #include #include #include -#include "citra_qt/util/sequence_dialog/sequence_dialog.h" +#include "lucina3ds_qt/util/sequence_dialog/sequence_dialog.h" SequenceDialog::SequenceDialog(QWidget* parent) : QDialog(parent) { setWindowTitle(tr("Enter a hotkey")); diff --git a/src/citra_qt/util/sequence_dialog/sequence_dialog.h b/src/lucina3ds_qt/util/sequence_dialog/sequence_dialog.h similarity index 100% rename from src/citra_qt/util/sequence_dialog/sequence_dialog.h rename to src/lucina3ds_qt/util/sequence_dialog/sequence_dialog.h diff --git a/src/citra_qt/util/spinbox.cpp b/src/lucina3ds_qt/util/spinbox.cpp similarity index 99% rename from src/citra_qt/util/spinbox.cpp rename to src/lucina3ds_qt/util/spinbox.cpp index 722af05e..bcd3553e 100644 --- a/src/citra_qt/util/spinbox.cpp +++ b/src/lucina3ds_qt/util/spinbox.cpp @@ -31,7 +31,7 @@ #include #include #include -#include "citra_qt/util/spinbox.h" +#include "lucina3ds_qt/util/spinbox.h" #include "common/assert.h" CSpinBox::CSpinBox(QWidget* parent) diff --git a/src/citra_qt/util/spinbox.h b/src/lucina3ds_qt/util/spinbox.h similarity index 100% rename from src/citra_qt/util/spinbox.h rename to src/lucina3ds_qt/util/spinbox.h diff --git a/src/citra_qt/util/util.cpp b/src/lucina3ds_qt/util/util.cpp similarity index 97% rename from src/citra_qt/util/util.cpp rename to src/lucina3ds_qt/util/util.cpp index ec10fe47..5742dc76 100644 --- a/src/citra_qt/util/util.cpp +++ b/src/lucina3ds_qt/util/util.cpp @@ -5,7 +5,7 @@ #include #include #include -#include "citra_qt/util/util.h" +#include "lucina3ds_qt/util/util.h" QFont GetMonospaceFont() { QFont font(QStringLiteral("monospace")); diff --git a/src/citra_qt/util/util.h b/src/lucina3ds_qt/util/util.h similarity index 100% rename from src/citra_qt/util/util.h rename to src/lucina3ds_qt/util/util.h diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 3feb6f47..324eb700 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -27,8 +27,8 @@ if (ENABLE_WEB_SERVICE) target_link_libraries(network PRIVATE web_service) endif() -target_link_libraries(network PRIVATE citra_common enet Boost::serialization httplib) +target_link_libraries(network PRIVATE lucina3ds_common enet Boost::serialization httplib) -if (CITRA_USE_PRECOMPILED_HEADERS) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) target_precompile_headers(network PRIVATE precompiled_headers.h) endif() diff --git a/src/network/announce_multiplayer_session.cpp b/src/network/announce_multiplayer_session.cpp index b3d53775..4f520485 100644 --- a/src/network/announce_multiplayer_session.cpp +++ b/src/network/announce_multiplayer_session.cpp @@ -23,8 +23,8 @@ static constexpr std::chrono::seconds announce_time_interval(15); AnnounceMultiplayerSession::AnnounceMultiplayerSession() { #ifdef ENABLE_WEB_SERVICE backend = std::make_unique(NetSettings::values.web_api_url, - NetSettings::values.citra_username, - NetSettings::values.citra_token); + NetSettings::values.lucina3ds_username, + NetSettings::values.lucina3ds_token); #else backend = std::make_unique(); #endif @@ -156,8 +156,8 @@ void AnnounceMultiplayerSession::UpdateCredentials() { #ifdef ENABLE_WEB_SERVICE backend = std::make_unique(NetSettings::values.web_api_url, - NetSettings::values.citra_username, - NetSettings::values.citra_token); + NetSettings::values.lucina3ds_username, + NetSettings::values.lucina3ds_token); #endif } diff --git a/src/network/artic_base/artic_base_client.cpp b/src/network/artic_base/artic_base_client.cpp index edc587f3..be359456 100644 --- a/src/network/artic_base/artic_base_client.cpp +++ b/src/network/artic_base/artic_base_client.cpp @@ -274,7 +274,7 @@ bool Client::Connect() { closesocket(main_socket); LOG_ERROR(Network, "Incompatible server version: {}", version_value); SignalCommunicationError("\nIncompatible Artic Base Server version.\nCheck for updates " - "to Artic Base Server or Citra."); + "to Artic Base Server or Lucina3DS."); return false; } } else { diff --git a/src/network/network_settings.h b/src/network/network_settings.h index de79c778..7175d354 100644 --- a/src/network/network_settings.h +++ b/src/network/network_settings.h @@ -11,8 +11,8 @@ namespace NetSettings { struct Values { // WebService std::string web_api_url; - std::string citra_username; - std::string citra_token; + std::string lucina3ds_username; + std::string lucina3ds_token; } extern values; } // namespace NetSettings diff --git a/src/network/room.cpp b/src/network/room.cpp index 4623b31d..729fe434 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -598,7 +598,7 @@ bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { if (sending_member == members.end()) { return false; } - if (room_information.enable_citra_mods && + if (room_information.enable_lucina3ds_mods && sending_member->user_data.moderator) { // Community moderator return true; @@ -1013,7 +1013,7 @@ bool Room::Create(const std::string& name, const std::string& description, const u32 max_connections, const std::string& host_username, const std::string& preferred_game, u64 preferred_game_id, std::unique_ptr verify_backend, - const Room::BanList& ban_list, bool enable_citra_mods) { + const Room::BanList& ban_list, bool enable_lucina3ds_mods) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -1036,7 +1036,7 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; room_impl->room_information.host_username = host_username; - room_impl->room_information.enable_citra_mods = enable_citra_mods; + room_impl->room_information.enable_lucina3ds_mods = enable_lucina3ds_mods; room_impl->password = password; room_impl->verify_backend = std::move(verify_backend); room_impl->username_ban_list = ban_list.first; diff --git a/src/network/room.h b/src/network/room.h index a6798483..ae32f7cc 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -32,7 +32,7 @@ struct RoomInformation { std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game std::string host_username; ///< Forum username of the host - bool enable_citra_mods; ///< Allow Citra Moderators to moderate on this room + bool enable_lucina3ds_mods; ///< Allow Lucina3DS Moderators to moderate on this room }; struct GameInfo { @@ -148,7 +148,7 @@ public: const std::string& host_username = "", const std::string& preferred_game = "", u64 preferred_game_id = 0, std::unique_ptr verify_backend = nullptr, - const BanList& ban_list = {}, bool enable_citra_mods = false); + const BanList& ban_list = {}, bool enable_lucina3ds_mods = false); /** * Sets the verification GUID of the room. diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 4ac9368b..dd1a7034 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -23,12 +23,12 @@ add_executable(tests create_target_directory_groups(tests) -target_link_libraries(tests PRIVATE citra_common citra_core video_core audio_core) +target_link_libraries(tests PRIVATE lucina3ds_common lucina3ds_core video_core audio_core) target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch2 nihstro-headers Threads::Threads) add_test(NAME tests COMMAND tests) -if (CITRA_USE_PRECOMPILED_HEADERS) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) target_precompile_headers(tests PRIVATE precompiled_headers.h) endif() diff --git a/src/tests/core/hle/kernel/hle_ipc.cpp b/src/tests/core/hle/kernel/hle_ipc.cpp index f908e5f2..5435dc95 100644 --- a/src/tests/core/hle/kernel/hle_ipc.cpp +++ b/src/tests/core/hle/kernel/hle_ipc.cpp @@ -143,7 +143,7 @@ TEST_CASE("HLERequestContext::PopulateFromIncomingCommandBuffer", "[core][kernel } SECTION("translates StaticBuffer descriptors") { - auto mem = std::make_shared(Memory::CITRA_PAGE_SIZE); + auto mem = std::make_shared(Memory::LUCINA3DS_PAGE_SIZE); MemoryRef buffer{mem}; std::fill(buffer.GetPtr(), buffer.GetPtr() + buffer.GetSize(), 0xAB); @@ -167,7 +167,7 @@ TEST_CASE("HLERequestContext::PopulateFromIncomingCommandBuffer", "[core][kernel } SECTION("translates MappedBuffer descriptors") { - auto mem = std::make_shared(Memory::CITRA_PAGE_SIZE); + auto mem = std::make_shared(Memory::LUCINA3DS_PAGE_SIZE); MemoryRef buffer{mem}; std::fill(buffer.GetPtr(), buffer.GetPtr() + buffer.GetSize(), 0xCD); @@ -194,11 +194,11 @@ TEST_CASE("HLERequestContext::PopulateFromIncomingCommandBuffer", "[core][kernel } SECTION("translates mixed params") { - auto mem_static = std::make_shared(Memory::CITRA_PAGE_SIZE); + auto mem_static = std::make_shared(Memory::LUCINA3DS_PAGE_SIZE); MemoryRef buffer_static{mem_static}; std::fill(buffer_static.GetPtr(), buffer_static.GetPtr() + buffer_static.GetSize(), 0xCE); - auto mem_mapped = std::make_shared(Memory::CITRA_PAGE_SIZE); + auto mem_mapped = std::make_shared(Memory::LUCINA3DS_PAGE_SIZE); MemoryRef buffer_mapped{mem_mapped}; std::fill(buffer_mapped.GetPtr(), buffer_mapped.GetPtr() + buffer_mapped.GetSize(), 0xDF); @@ -332,12 +332,12 @@ TEST_CASE("HLERequestContext::WriteToOutgoingCommandBuffer", "[core][kernel]") { } SECTION("translates StaticBuffer descriptors") { - std::vector input_buffer(Memory::CITRA_PAGE_SIZE); + std::vector input_buffer(Memory::LUCINA3DS_PAGE_SIZE); std::fill(input_buffer.begin(), input_buffer.end(), 0xAB); context.AddStaticBuffer(0, input_buffer); - auto output_mem = std::make_shared(Memory::CITRA_PAGE_SIZE); + auto output_mem = std::make_shared(Memory::LUCINA3DS_PAGE_SIZE); MemoryRef output_buffer{output_mem}; VAddr target_address = 0x10000000; @@ -366,10 +366,10 @@ TEST_CASE("HLERequestContext::WriteToOutgoingCommandBuffer", "[core][kernel]") { } SECTION("translates StaticBuffer descriptors") { - std::vector input_buffer(Memory::CITRA_PAGE_SIZE); + std::vector input_buffer(Memory::LUCINA3DS_PAGE_SIZE); std::fill(input_buffer.begin(), input_buffer.end(), 0xAB); - auto output_mem = std::make_shared(Memory::CITRA_PAGE_SIZE); + auto output_mem = std::make_shared(Memory::LUCINA3DS_PAGE_SIZE); MemoryRef output_buffer{output_mem}; VAddr target_address = 0x10000000; diff --git a/src/tests/core/memory/vm_manager.cpp b/src/tests/core/memory/vm_manager.cpp index f58ad2e5..c23a408a 100644 --- a/src/tests/core/memory/vm_manager.cpp +++ b/src/tests/core/memory/vm_manager.cpp @@ -11,7 +11,7 @@ #include "core/memory.h" TEST_CASE("Memory Basics", "[kernel][memory]") { - auto mem = std::make_shared(Memory::CITRA_PAGE_SIZE); + auto mem = std::make_shared(Memory::LUCINA3DS_PAGE_SIZE); MemoryRef block{mem}; Core::Timing timing(1, 100); Core::System system; diff --git a/src/tests/video_core/shader.cpp b/src/tests/video_core/shader.cpp index 8747808d..c78f8d63 100644 --- a/src/tests/video_core/shader.cpp +++ b/src/tests/video_core/shader.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include "common/arch.h" -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) #include #include @@ -18,9 +18,9 @@ #include "video_core/pica/shader_setup.h" #include "video_core/pica/shader_unit.h" #include "video_core/shader/shader_interpreter.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include "video_core/shader/shader_jit_x64_compiler.h" -#elif CITRA_ARCH(arm64) +#elif LUCINA3DS_ARCH(arm64) #include "video_core/shader/shader_jit_a64_compiler.h" #endif @@ -800,4 +800,4 @@ SHADER_TEST_CASE("Source Swizzle", "[video_core][shader]") { Common::Vec4f(iota_vec.y, iota_vec.y, iota_vec.y, iota_vec.y)); } -#endif // CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#endif // LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 3593a271..a61443ff 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -204,7 +204,7 @@ target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE}) create_target_directory_groups(video_core) -target_link_libraries(video_core PUBLIC citra_common citra_core) +target_link_libraries(video_core PUBLIC lucina3ds_common lucina3ds_core) target_link_libraries(video_core PRIVATE Boost::serialization dds-ktx json-headers nihstro-headers tsl::robin_map) if ("x86_64" IN_LIST ARCHITECTURE) @@ -215,6 +215,6 @@ if ("arm64" IN_LIST ARCHITECTURE) target_link_libraries(video_core PUBLIC oaknut) endif() -if (CITRA_USE_PRECOMPILED_HEADERS) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) target_precompile_headers(video_core PRIVATE precompiled_headers.h) endif() diff --git a/src/video_core/custom_textures/custom_tex_manager.cpp b/src/video_core/custom_textures/custom_tex_manager.cpp index 2ec3c946..c52b74fe 100644 --- a/src/video_core/custom_textures/custom_tex_manager.cpp +++ b/src/video_core/custom_textures/custom_tex_manager.cpp @@ -190,7 +190,7 @@ void CustomTexManager::PrepareDumping(u64 title_id) { } nlohmann::ordered_json json; - json["author"] = "citra"; + json["author"] = "citra/lucina"; json["version"] = "1.0.0"; json["description"] = "A graphics pack"; diff --git a/src/video_core/rasterizer_cache/rasterizer_cache.h b/src/video_core/rasterizer_cache/rasterizer_cache.h index 0144cbd6..cfcb7354 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache.h @@ -1215,8 +1215,8 @@ void RasterizerCache::ClearAll(bool flush) { for (auto& pair : RangeFromInterval(cached_pages, flush_interval)) { const auto interval = pair.first & flush_interval; - const PAddr interval_start_addr = boost::icl::first(interval) << Memory::CITRA_PAGE_BITS; - const PAddr interval_end_addr = boost::icl::last_next(interval) << Memory::CITRA_PAGE_BITS; + const PAddr interval_start_addr = boost::icl::first(interval) << Memory::LUCINA3DS_PAGE_BITS; + const PAddr interval_end_addr = boost::icl::last_next(interval) << Memory::LUCINA3DS_PAGE_BITS; const u32 interval_size = interval_end_addr - interval_start_addr; memory.RasterizerMarkRegionCached(interval_start_addr, interval_size, false); @@ -1371,14 +1371,14 @@ void RasterizerCache::UnregisterSurface(SurfaceId surface_id) { ForEachPage(surface.addr, surface.size, [this, surface_id](u64 page) { const auto page_it = page_table.find(page); if (page_it == page_table.end()) { - ASSERT_MSG(false, "Unregistering unregistered page=0x{:x}", page << CITRA_PAGEBITS); + ASSERT_MSG(false, "Unregistering unregistered page=0x{:x}", page << LUCINA3DS_PAGEBITS); return; } std::vector& surfaces = page_it.value(); const auto vector_it = std::find(surfaces.begin(), surfaces.end(), surface_id); if (vector_it == surfaces.end()) { ASSERT_MSG(false, "Unregistering unregistered surface in page=0x{:x}", - page << CITRA_PAGEBITS); + page << LUCINA3DS_PAGEBITS); return; } surfaces.erase(vector_it); @@ -1409,8 +1409,8 @@ void RasterizerCache::UnregisterAll() { template void RasterizerCache::UpdatePagesCachedCount(PAddr addr, u32 size, int delta) { const u32 num_pages = - ((addr + size - 1) >> Memory::CITRA_PAGE_BITS) - (addr >> Memory::CITRA_PAGE_BITS) + 1; - const u32 page_start = addr >> Memory::CITRA_PAGE_BITS; + ((addr + size - 1) >> Memory::LUCINA3DS_PAGE_BITS) - (addr >> Memory::LUCINA3DS_PAGE_BITS) + 1; + const u32 page_start = addr >> Memory::LUCINA3DS_PAGE_BITS; const u32 page_end = page_start + num_pages; // Interval maps will erase segments if count reaches 0, so if delta is negative we have to @@ -1424,8 +1424,8 @@ void RasterizerCache::UpdatePagesCachedCount(PAddr addr, u32 size, int delta) const auto interval = pair.first & pages_interval; const int count = pair.second; - const PAddr interval_start_addr = boost::icl::first(interval) << Memory::CITRA_PAGE_BITS; - const PAddr interval_end_addr = boost::icl::last_next(interval) << Memory::CITRA_PAGE_BITS; + const PAddr interval_start_addr = boost::icl::first(interval) << Memory::LUCINA3DS_PAGE_BITS; + const PAddr interval_end_addr = boost::icl::last_next(interval) << Memory::LUCINA3DS_PAGE_BITS; const u32 interval_size = interval_end_addr - interval_start_addr; if (delta > 0 && count == delta) { diff --git a/src/video_core/rasterizer_cache/rasterizer_cache_base.h b/src/video_core/rasterizer_cache/rasterizer_cache_base.h index afd3625b..76cda2ce 100644 --- a/src/video_core/rasterizer_cache/rasterizer_cache_base.h +++ b/src/video_core/rasterizer_cache/rasterizer_cache_base.h @@ -60,7 +60,7 @@ class RendererBase; template class RasterizerCache { /// Address shift for caching surfaces into a hash table - static constexpr u64 CITRA_PAGEBITS = 18; + static constexpr u64 LUCINA3DS_PAGEBITS = 18; using Runtime = typename T::Runtime; using Sampler = typename T::Sampler; @@ -141,8 +141,8 @@ private: template void ForEachPage(PAddr addr, std::size_t size, Func&& func) { static constexpr bool RETURNS_BOOL = std::is_same_v, bool>; - const u64 page_end = (addr + size - 1) >> CITRA_PAGEBITS; - for (u64 page = addr >> CITRA_PAGEBITS; page <= page_end; ++page) { + const u64 page_end = (addr + size - 1) >> LUCINA3DS_PAGEBITS; + for (u64 page = addr >> LUCINA3DS_PAGEBITS; page <= page_end; ++page) { if constexpr (RETURNS_BOOL) { if (func(page)) { break; diff --git a/src/video_core/renderer_opengl/post_processing_opengl.h b/src/video_core/renderer_opengl/post_processing_opengl.h index 3e9ea3e4..a2762230 100644 --- a/src/video_core/renderer_opengl/post_processing_opengl.h +++ b/src/video_core/renderer_opengl/post_processing_opengl.h @@ -11,7 +11,7 @@ namespace OpenGL { // Returns a vector of the names of the shaders available in the -// "shaders" directory in citra's data directory +// "shaders" directory in lucina3ds's data directory std::vector GetPostProcessingShaderList(bool anaglyph); // Returns the shader code for the shader named "shader_name" diff --git a/src/video_core/renderer_vulkan/vk_platform.cpp b/src/video_core/renderer_vulkan/vk_platform.cpp index fa8f7619..79385756 100644 --- a/src/video_core/renderer_vulkan/vk_platform.cpp +++ b/src/video_core/renderer_vulkan/vk_platform.cpp @@ -305,9 +305,9 @@ vk::UniqueInstance CreateInstance(const Common::DynamicLibrary& library, const auto extensions = GetInstanceExtensions(window_type, enable_validation); const vk::ApplicationInfo application_info = { - .pApplicationName = "Citra", + .pApplicationName = "Lucina3DS", .applicationVersion = VK_MAKE_VERSION(1, 0, 0), - .pEngineName = "Citra Vulkan", + .pEngineName = "Lucina3DS Vulkan", .engineVersion = VK_MAKE_VERSION(1, 0, 0), .apiVersion = TargetVulkanApiVersion, }; diff --git a/src/video_core/shader/shader.cpp b/src/video_core/shader/shader.cpp index 180ca775..3180c400 100644 --- a/src/video_core/shader/shader.cpp +++ b/src/video_core/shader/shader.cpp @@ -4,7 +4,7 @@ #include "common/arch.h" #include "video_core/shader/shader_interpreter.h" -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) #include "video_core/shader/shader_jit.h" #endif #include "video_core/shader/shader.h" @@ -12,7 +12,7 @@ namespace Pica { std::unique_ptr CreateEngine(bool use_jit) { -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) if (use_jit) { return std::make_unique(); } diff --git a/src/video_core/shader/shader_jit.cpp b/src/video_core/shader/shader_jit.cpp index 64458622..cc634cdd 100644 --- a/src/video_core/shader/shader_jit.cpp +++ b/src/video_core/shader/shader_jit.cpp @@ -3,17 +3,17 @@ // Refer to the license.txt file included. #include "common/arch.h" -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) #include "common/assert.h" #include "common/hash.h" #include "common/microprofile.h" #include "video_core/shader/shader.h" #include "video_core/shader/shader_jit.h" -#if CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(arm64) #include "video_core/shader/shader_jit_a64_compiler.h" #endif -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include "video_core/shader/shader_jit_x64_compiler.h" #endif @@ -54,4 +54,4 @@ void JitEngine::Run(const ShaderSetup& setup, ShaderUnit& state) const { } // namespace Pica::Shader -#endif // CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#endif // LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) diff --git a/src/video_core/shader/shader_jit.h b/src/video_core/shader/shader_jit.h index 2f3e77b0..4487eb07 100644 --- a/src/video_core/shader/shader_jit.h +++ b/src/video_core/shader/shader_jit.h @@ -5,7 +5,7 @@ #pragma once #include "common/arch.h" -#if CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) #include #include @@ -30,4 +30,4 @@ private: } // namespace Pica::Shader -#endif // CITRA_ARCH(x86_64) || CITRA_ARCH(arm64) +#endif // LUCINA3DS_ARCH(x86_64) || LUCINA3DS_ARCH(arm64) diff --git a/src/video_core/shader/shader_jit_a64_compiler.cpp b/src/video_core/shader/shader_jit_a64_compiler.cpp index 18793317..45d50973 100644 --- a/src/video_core/shader/shader_jit_a64_compiler.cpp +++ b/src/video_core/shader/shader_jit_a64_compiler.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include "common/arch.h" -#if CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(arm64) #include #include @@ -1231,4 +1231,4 @@ Label JitShader::CompilePrelude_Exp2() { } // namespace Pica::Shader -#endif // CITRA_ARCH(arm64) +#endif // LUCINA3DS_ARCH(arm64) diff --git a/src/video_core/shader/shader_jit_a64_compiler.h b/src/video_core/shader/shader_jit_a64_compiler.h index 7accf66a..61016d1b 100644 --- a/src/video_core/shader/shader_jit_a64_compiler.h +++ b/src/video_core/shader/shader_jit_a64_compiler.h @@ -5,7 +5,7 @@ #pragma once #include "common/arch.h" -#if CITRA_ARCH(arm64) +#if LUCINA3DS_ARCH(arm64) #include #include diff --git a/src/video_core/shader/shader_jit_x64_compiler.cpp b/src/video_core/shader/shader_jit_x64_compiler.cpp index 2dd3720b..7e3151d0 100644 --- a/src/video_core/shader/shader_jit_x64_compiler.cpp +++ b/src/video_core/shader/shader_jit_x64_compiler.cpp @@ -3,7 +3,7 @@ // Refer to the license.txt file included. #include "common/arch.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include #include @@ -1260,4 +1260,4 @@ Xbyak::Label JitShader::CompilePrelude_Exp2() { } // namespace Pica::Shader -#endif // CITRA_ARCH(x86_64) +#endif // LUCINA3DS_ARCH(x86_64) diff --git a/src/video_core/shader/shader_jit_x64_compiler.h b/src/video_core/shader/shader_jit_x64_compiler.h index 91118329..b6c3c3ed 100644 --- a/src/video_core/shader/shader_jit_x64_compiler.h +++ b/src/video_core/shader/shader_jit_x64_compiler.h @@ -5,7 +5,7 @@ #pragma once #include "common/arch.h" -#if CITRA_ARCH(x86_64) +#if LUCINA3DS_ARCH(x86_64) #include #include diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index 7dabb7b4..6fc29e2e 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -13,12 +13,12 @@ add_library(web_service STATIC create_target_directory_groups(web_service) target_compile_definitions(web_service PUBLIC -DENABLE_WEB_SERVICE) -target_link_libraries(web_service PRIVATE citra_common network json-headers httplib cpp-jwt) +target_link_libraries(web_service PRIVATE lucina3ds_common network json-headers httplib cpp-jwt) target_link_libraries(web_service PUBLIC ${OPENSSL_LIBS}) if(WIN32) target_link_libraries(web_service PRIVATE crypt32) endif() -if (CITRA_USE_PRECOMPILED_HEADERS) +if (LUCINA3DS_USE_PRECOMPILED_HEADERS) target_precompile_headers(web_service PRIVATE precompiled_headers.h) endif() diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp index 27e08db9..dfc6b449 100644 --- a/src/web_service/verify_user_jwt.cpp +++ b/src/web_service/verify_user_jwt.cpp @@ -33,7 +33,7 @@ Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& ver using namespace jwt::params; std::error_code error; auto decoded = - jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("citra-core"), + jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("lucina3ds-core"), aud(audience), validate_iat(true), validate_jti(true)); if (error) { LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}",