From 6c60067ba71a0d60f720ed9ed57f4618afa51609 Mon Sep 17 00:00:00 2001 From: CGH0S7 <776459475@qq.com> Date: Mon, 10 Feb 2025 00:00:38 +0800 Subject: [PATCH] Finish Linux x86_64 build --- .history/.gitmodules_20250207123101 | 93 - .history/.gitmodules_20250207123201 | 93 - .../src/lucina3ds_qt/main_20250209230836.cpp | 3345 ++++++ .../src/lucina3ds_qt/main_20250209233216.cpp | 3345 ++++++ .../src/lucina3ds_qt/main_20250209233407.cpp | 3345 ++++++ .../src/lucina3ds_qt/main_20250209234822.cpp | 3346 ++++++ .../src/lucina3ds_qt/main_20250209234858.cpp | 3348 ++++++ .../src/lucina3ds_qt/main_20250209235145.cpp | 3348 ++++++ CMakeLists.txt | 30 +- CONTRIBUTING.md | 1 - Doxyfile | 2 +- dist/Lucina3DS.png | Bin 0 -> 22624 bytes dist/citra-room.desktop | 10 - dist/citra.ico | Bin 370070 -> 0 bytes dist/citra.svg | 2 - .../compatibility_list.json | 9638 +++++++++++++++++ dist/doc-icon.png | Bin 7768 -> 2475 bytes dist/icon.png | Bin 18317 -> 22624 bytes dist/{citra-qt.6 => lucina3ds-qt.6} | 14 +- ...{citra-qt.desktop => lucina3ds-qt.desktop} | 8 +- dist/lucina3ds-room.desktop | 10 + dist/{citra.6 => lucina3ds.6} | 10 +- dist/{citra.desktop => lucina3ds.desktop} | 8 +- dist/lucina3ds.ico | Bin 0 -> 270398 bytes dist/{citra.manifest => lucina3ds.manifest} | 0 dist/lucina3ds.svg | 131 + dist/{citra.xml => lucina3ds.xml} | 8 +- dist/qt_themes/default/default.qrc | 2 +- .../qt_themes/default/icons/256x256/citra.png | Bin 26290 -> 0 bytes .../default/icons/256x256/lucina3ds.png | Bin 0 -> 9824 bytes dist/scripting/citra.py | 12 +- externals/CMakeLists.txt | 2 +- src/CMakeLists.txt | 12 +- src/audio_core/CMakeLists.txt | 4 +- src/common/CMakeLists.txt | 30 +- src/common/aarch64/cpu_detect.cpp | 2 +- src/common/arch.h | 6 +- src/common/assert.h | 8 +- src/common/common_funcs.h | 8 +- src/common/common_paths.h | 12 +- src/common/file_util.cpp | 4 +- src/common/file_util.h | 8 +- src/common/logging/backend.cpp | 16 +- src/common/logging/text_formatter.cpp | 2 +- src/common/microprofileui.h | 2 +- src/common/scm_rev.cpp.in | 9 +- src/common/settings.cpp | 2 +- src/common/x64/cpu_detect.cpp | 4 +- src/common/x64/cpu_detect.h | 4 +- src/common/x64/xbyak_abi.h | 4 +- src/common/x64/xbyak_util.h | 4 +- src/core/CMakeLists.txt | 24 +- .../arm/dyncom/arm_dyncom_interpreter.cpp | 6 +- src/core/arm/exclusive_monitor.cpp | 4 +- src/core/cheats/gateway_cheat.cpp | 2 +- src/core/core.cpp | 4 +- src/core/file_sys/plugin_3gx.cpp | 2 +- src/core/file_sys/plugin_3gx.h | 6 +- src/core/hle/kernel/ipc.cpp | 26 +- src/core/hle/kernel/process.cpp | 14 +- src/core/hle/kernel/svc.cpp | 84 +- src/core/hle/kernel/thread.cpp | 4 +- src/core/hle/kernel/vm_manager.cpp | 8 +- src/core/hle/service/cfg/cfg_defaults.cpp | 2 +- src/core/hle/service/csnd/csnd_snd.cpp | 2 +- src/core/hle/service/ldr_ro/cro_helper.cpp | 6 +- src/core/hle/service/ldr_ro/ldr_ro.cpp | 18 +- src/core/loader/artic.cpp | 12 +- src/core/loader/ncch.cpp | 6 +- src/core/memory.cpp | 116 +- src/core/memory.h | 8 +- src/dedicated_room/CMakeLists.txt | 26 +- .../{citra-room.cpp => lucina3ds-room.cpp} | 36 +- .../lucina3ds-room.rc} | 4 +- src/input_common/CMakeLists.txt | 4 +- src/{citra => lucina3ds}/CMakeLists.txt | 32 +- src/{citra => lucina3ds}/config.cpp | 8 +- src/{citra => lucina3ds}/config.h | 0 src/{citra => lucina3ds}/default_ini.h | 0 .../emu_window/emu_window_sdl2.cpp | 4 +- .../emu_window/emu_window_sdl2.h | 0 .../emu_window/emu_window_sdl2_gl.cpp | 4 +- .../emu_window/emu_window_sdl2_gl.h | 2 +- .../emu_window/emu_window_sdl2_sw.cpp | 4 +- .../emu_window/emu_window_sdl2_sw.h | 2 +- .../emu_window/emu_window_sdl2_vk.cpp | 4 +- .../emu_window/emu_window_sdl2_vk.h | 2 +- .../citra.cpp => lucina3ds/lucina3ds.cpp} | 16 +- .../citra-room.rc => lucina3ds/lucina3ds.rc} | 4 +- .../precompiled_headers.h | 0 src/{citra => lucina3ds}/resource.h | 0 src/{citra_qt => lucina3ds_qt}/CMakeLists.txt | 88 +- .../aboutdialog.cpp | 0 src/{citra_qt => lucina3ds_qt}/aboutdialog.h | 0 src/{citra_qt => lucina3ds_qt}/aboutdialog.ui | 33 +- .../applets/mii_selector.cpp | 2 +- .../applets/mii_selector.h | 0 .../applets/swkbd.cpp | 2 +- .../applets/swkbd.h | 0 .../bootmanager.cpp | 4 +- src/{citra_qt => lucina3ds_qt}/bootmanager.h | 0 .../camera/camera_util.cpp | 2 +- .../camera/camera_util.h | 0 .../camera/qt_camera_base.cpp | 4 +- .../camera/qt_camera_base.h | 4 +- .../camera/qt_multimedia_camera.cpp | 4 +- .../camera/qt_multimedia_camera.h | 4 +- .../camera/still_image_camera.cpp | 2 +- .../camera/still_image_camera.h | 4 +- src/{citra_qt => lucina3ds_qt}/compatdb.cpp | 2 +- src/{citra_qt => lucina3ds_qt}/compatdb.h | 0 src/{citra_qt => lucina3ds_qt}/compatdb.ui | 0 .../compatibility_list.cpp | 2 +- .../compatibility_list.h | 0 .../configuration/config.cpp | 10 +- .../configuration/config.h | 2 +- .../configuration/configuration_shared.cpp | 4 +- .../configuration/configuration_shared.h | 0 .../configuration/configure.ui | 0 .../configuration/configure_audio.cpp | 4 +- .../configuration/configure_audio.h | 0 .../configuration/configure_audio.ui | 0 .../configuration/configure_camera.cpp | 2 +- .../configuration/configure_camera.h | 0 .../configuration/configure_camera.ui | 0 .../configuration/configure_cheats.cpp | 0 .../configuration/configure_cheats.h | 0 .../configuration/configure_cheats.ui | 0 .../configuration/configure_debug.cpp | 8 +- .../configuration/configure_debug.h | 0 .../configuration/configure_debug.ui | 0 .../configuration/configure_dialog.cpp | 28 +- .../configuration/configure_dialog.h | 0 .../configuration/configure_enhancements.cpp | 4 +- .../configuration/configure_enhancements.h | 0 .../configuration/configure_enhancements.ui | 0 .../configuration/configure_general.cpp | 6 +- .../configuration/configure_general.h | 0 .../configuration/configure_general.ui | 0 .../configuration/configure_graphics.cpp | 4 +- .../configuration/configure_graphics.h | 0 .../configuration/configure_graphics.ui | 0 .../configuration/configure_hotkeys.cpp | 8 +- .../configuration/configure_hotkeys.h | 0 .../configuration/configure_hotkeys.ui | 0 .../configuration/configure_input.cpp | 6 +- .../configuration/configure_input.h | 0 .../configuration/configure_input.ui | 0 .../configuration/configure_motion_touch.cpp | 4 +- .../configuration/configure_motion_touch.h | 0 .../configuration/configure_motion_touch.ui | 0 .../configuration/configure_per_game.cpp | 20 +- .../configuration/configure_per_game.h | 2 +- .../configuration/configure_per_game.ui | 0 .../configuration/configure_storage.cpp | 2 +- .../configuration/configure_storage.h | 0 .../configuration/configure_storage.ui | 0 .../configuration/configure_system.cpp | 4 +- .../configuration/configure_system.h | 0 .../configuration/configure_system.ui | 0 .../configure_touch_from_button.cpp | 4 +- .../configure_touch_from_button.h | 0 .../configure_touch_from_button.ui | 2 +- .../configuration/configure_touch_widget.h | 0 .../configuration/configure_ui.cpp | 4 +- .../configuration/configure_ui.h | 0 .../configuration/configure_ui.ui | 0 .../configuration/configure_web.cpp | 4 +- .../configuration/configure_web.h | 0 .../configuration/configure_web.ui | 0 .../debugger/console.cpp | 4 +- .../debugger/console.h | 0 .../debugger/graphics/graphics.cpp | 4 +- .../debugger/graphics/graphics.h | 0 .../graphics/graphics_breakpoint_observer.cpp | 2 +- .../graphics/graphics_breakpoint_observer.h | 0 .../graphics/graphics_breakpoints.cpp | 4 +- .../debugger/graphics/graphics_breakpoints.h | 0 .../graphics/graphics_breakpoints_p.h | 0 .../debugger/graphics/graphics_cmdlists.cpp | 4 +- .../debugger/graphics/graphics_cmdlists.h | 0 .../debugger/graphics/graphics_surface.cpp | 4 +- .../debugger/graphics/graphics_surface.h | 2 +- .../debugger/graphics/graphics_tracing.cpp | 2 +- .../debugger/graphics/graphics_tracing.h | 2 +- .../graphics/graphics_vertex_shader.cpp | 4 +- .../graphics/graphics_vertex_shader.h | 2 +- .../debugger/ipc/record_dialog.cpp | 2 +- .../debugger/ipc/record_dialog.h | 0 .../debugger/ipc/record_dialog.ui | 0 .../debugger/ipc/recorder.cpp | 4 +- .../debugger/ipc/recorder.h | 0 .../debugger/ipc/recorder.ui | 0 .../debugger/lle_service_modules.cpp | 2 +- .../debugger/lle_service_modules.h | 0 .../debugger/profiler.cpp | 4 +- .../debugger/profiler.h | 0 .../debugger/registers.cpp | 4 +- .../debugger/registers.h | 0 .../debugger/registers.ui | 0 .../debugger/wait_tree.cpp | 4 +- .../debugger/wait_tree.h | 0 src/{citra_qt => lucina3ds_qt}/discord.h | 0 .../discord_impl.cpp | 4 +- src/{citra_qt => lucina3ds_qt}/discord_impl.h | 2 +- .../dumping/dumping_dialog.cpp | 6 +- .../dumping/dumping_dialog.h | 0 .../dumping/dumping_dialog.ui | 0 .../dumping/option_set_dialog.cpp | 2 +- .../dumping/option_set_dialog.h | 0 .../dumping/option_set_dialog.ui | 0 .../dumping/options_dialog.cpp | 4 +- .../dumping/options_dialog.h | 0 .../dumping/options_dialog.ui | 0 src/{citra_qt => lucina3ds_qt}/game_list.cpp | 12 +- src/{citra_qt => lucina3ds_qt}/game_list.h | 2 +- src/{citra_qt => lucina3ds_qt}/game_list_p.h | 4 +- .../game_list_worker.cpp | 10 +- .../game_list_worker.h | 2 +- src/{citra_qt => lucina3ds_qt}/hotkeys.cpp | 4 +- src/{citra_qt => lucina3ds_qt}/hotkeys.h | 0 .../loading_screen.cpp | 2 +- .../loading_screen.h | 0 .../loading_screen.ui | 0 .../lucina3ds-qt.rc} | 0 src/{citra_qt => lucina3ds_qt}/main.cpp | 139 +- src/{citra_qt => lucina3ds_qt}/main.h | 10 +- src/{citra_qt => lucina3ds_qt}/main.ui | 160 +- .../movie/movie_play_dialog.cpp | 8 +- .../movie/movie_play_dialog.h | 0 .../movie/movie_play_dialog.ui | 0 .../movie/movie_record_dialog.cpp | 4 +- .../movie/movie_record_dialog.h | 0 .../movie/movie_record_dialog.ui | 0 .../multiplayer/chat_room.cpp | 6 +- .../multiplayer/chat_room.h | 0 .../multiplayer/chat_room.ui | 0 .../multiplayer/client_room.cpp | 10 +- .../multiplayer/client_room.h | 2 +- .../multiplayer/client_room.ui | 0 .../multiplayer/direct_connect.cpp | 14 +- .../multiplayer/direct_connect.h | 2 +- .../multiplayer/direct_connect.ui | 0 .../multiplayer/host_room.cpp | 22 +- .../multiplayer/host_room.h | 2 +- .../multiplayer/host_room.ui | 0 .../multiplayer/lobby.cpp | 26 +- .../multiplayer/lobby.h | 2 +- .../multiplayer/lobby.ui | 0 .../multiplayer/lobby_p.h | 0 .../multiplayer/message.cpp | 2 +- .../multiplayer/message.h | 0 .../multiplayer/moderation_dialog.cpp | 2 +- .../multiplayer/moderation_dialog.h | 0 .../multiplayer/moderation_dialog.ui | 0 .../multiplayer/state.cpp | 16 +- .../multiplayer/state.h | 0 .../multiplayer/validation.h | 0 .../precompiled_headers.h | 0 .../qt_image_interface.cpp | 2 +- .../qt_image_interface.h | 0 src/{citra_qt => lucina3ds_qt}/uisettings.cpp | 0 src/{citra_qt => lucina3ds_qt}/uisettings.h | 0 .../updater/updater.cpp | 6 +- .../updater/updater.h | 0 .../updater/updater_p.h | 2 +- .../util/clickable_label.cpp | 2 +- .../util/clickable_label.h | 0 .../util/graphics_device_info.cpp | 2 +- .../util/graphics_device_info.h | 0 .../util/sequence_dialog/sequence_dialog.cpp | 2 +- .../util/sequence_dialog/sequence_dialog.h | 0 .../util/spinbox.cpp | 2 +- src/{citra_qt => lucina3ds_qt}/util/spinbox.h | 0 src/{citra_qt => lucina3ds_qt}/util/util.cpp | 2 +- src/{citra_qt => lucina3ds_qt}/util/util.h | 0 src/network/CMakeLists.txt | 4 +- src/network/announce_multiplayer_session.cpp | 8 +- src/network/artic_base/artic_base_client.cpp | 2 +- src/network/network_settings.h | 4 +- src/network/room.cpp | 6 +- src/network/room.h | 4 +- src/tests/CMakeLists.txt | 4 +- src/tests/core/hle/kernel/hle_ipc.cpp | 16 +- src/tests/core/memory/vm_manager.cpp | 2 +- src/tests/video_core/shader.cpp | 8 +- src/video_core/CMakeLists.txt | 4 +- .../custom_textures/custom_tex_manager.cpp | 2 +- .../rasterizer_cache/rasterizer_cache.h | 16 +- .../rasterizer_cache/rasterizer_cache_base.h | 6 +- .../renderer_opengl/post_processing_opengl.h | 2 +- .../renderer_vulkan/vk_platform.cpp | 4 +- src/video_core/shader/shader.cpp | 4 +- src/video_core/shader/shader_jit.cpp | 8 +- src/video_core/shader/shader_jit.h | 4 +- .../shader/shader_jit_a64_compiler.cpp | 4 +- .../shader/shader_jit_a64_compiler.h | 2 +- .../shader/shader_jit_x64_compiler.cpp | 4 +- .../shader/shader_jit_x64_compiler.h | 2 +- src/web_service/CMakeLists.txt | 4 +- src/web_service/verify_user_jwt.cpp | 2 +- 301 files changed, 30720 insertions(+), 1048 deletions(-) delete mode 100644 .history/.gitmodules_20250207123101 delete mode 100644 .history/.gitmodules_20250207123201 create mode 100644 .history/src/lucina3ds_qt/main_20250209230836.cpp create mode 100644 .history/src/lucina3ds_qt/main_20250209233216.cpp create mode 100644 .history/src/lucina3ds_qt/main_20250209233407.cpp create mode 100644 .history/src/lucina3ds_qt/main_20250209234822.cpp create mode 100644 .history/src/lucina3ds_qt/main_20250209234858.cpp create mode 100644 .history/src/lucina3ds_qt/main_20250209235145.cpp delete mode 100644 CONTRIBUTING.md create mode 100644 dist/Lucina3DS.png delete mode 100644 dist/citra-room.desktop delete mode 100644 dist/citra.ico delete mode 100644 dist/citra.svg rename dist/{citra-qt.6 => lucina3ds-qt.6} (78%) rename dist/{citra-qt.desktop => lucina3ds-qt.desktop} (84%) create mode 100644 dist/lucina3ds-room.desktop rename dist/{citra.6 => lucina3ds.6} (92%) rename dist/{citra.desktop => lucina3ds.desktop} (84%) create mode 100644 dist/lucina3ds.ico rename dist/{citra.manifest => lucina3ds.manifest} (100%) create mode 100644 dist/lucina3ds.svg rename dist/{citra.xml => lucina3ds.xml} (94%) delete mode 100644 dist/qt_themes/default/icons/256x256/citra.png create mode 100644 dist/qt_themes/default/icons/256x256/lucina3ds.png rename src/dedicated_room/{citra-room.cpp => lucina3ds-room.cpp} (90%) rename src/{citra/citra.rc => dedicated_room/lucina3ds-room.rc} (67%) rename src/{citra => lucina3ds}/CMakeLists.txt (50%) rename src/{citra => lucina3ds}/config.cpp (98%) rename src/{citra => lucina3ds}/config.h (100%) rename src/{citra => lucina3ds}/default_ini.h (100%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2.cpp (98%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2.h (100%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2_gl.cpp (97%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2_gl.h (95%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2_sw.cpp (96%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2_sw.h (94%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2_vk.cpp (95%) rename src/{citra => lucina3ds}/emu_window/emu_window_sdl2_vk.h (91%) rename src/{citra/citra.cpp => lucina3ds/lucina3ds.cpp} (97%) rename src/{dedicated_room/citra-room.rc => lucina3ds/lucina3ds.rc} (67%) rename src/{citra => lucina3ds}/precompiled_headers.h (100%) rename src/{citra => lucina3ds}/resource.h (100%) rename src/{citra_qt => lucina3ds_qt}/CMakeLists.txt (76%) rename src/{citra_qt => lucina3ds_qt}/aboutdialog.cpp (100%) rename src/{citra_qt => lucina3ds_qt}/aboutdialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/aboutdialog.ui (83%) rename src/{citra_qt => lucina3ds_qt}/applets/mii_selector.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/applets/mii_selector.h (100%) rename src/{citra_qt => lucina3ds_qt}/applets/swkbd.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/applets/swkbd.h (100%) rename src/{citra_qt => lucina3ds_qt}/bootmanager.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/bootmanager.h (100%) rename src/{citra_qt => lucina3ds_qt}/camera/camera_util.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/camera/camera_util.h (100%) rename src/{citra_qt => lucina3ds_qt}/camera/qt_camera_base.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/camera/qt_camera_base.h (91%) rename src/{citra_qt => lucina3ds_qt}/camera/qt_multimedia_camera.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/camera/qt_multimedia_camera.h (97%) rename src/{citra_qt => lucina3ds_qt}/camera/still_image_camera.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/camera/still_image_camera.h (92%) rename src/{citra_qt => lucina3ds_qt}/compatdb.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/compatdb.h (100%) rename src/{citra_qt => lucina3ds_qt}/compatdb.ui (100%) rename src/{citra_qt => lucina3ds_qt}/compatibility_list.cpp (93%) rename src/{citra_qt => lucina3ds_qt}/compatibility_list.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/config.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/config.h (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/configuration_shared.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/configuration/configuration_shared.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_audio.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_audio.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_audio.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_camera.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_camera.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_camera.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_cheats.cpp (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_cheats.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_cheats.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_debug.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_debug.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_debug.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_dialog.cpp (90%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_enhancements.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_enhancements.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_enhancements.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_general.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_general.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_general.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_graphics.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_graphics.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_graphics.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_hotkeys.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_hotkeys.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_hotkeys.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_input.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_input.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_input.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_motion_touch.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_motion_touch.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_motion_touch.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_per_game.cpp (91%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_per_game.h (97%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_per_game.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_storage.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_storage.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_storage.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_system.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_system.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_system.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_touch_from_button.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_touch_from_button.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_touch_from_button.ui (98%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_touch_widget.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_ui.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_ui.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_ui.ui (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_web.cpp (90%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_web.h (100%) rename src/{citra_qt => lucina3ds_qt}/configuration/configure_web.ui (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/console.cpp (95%) rename src/{citra_qt => lucina3ds_qt}/debugger/console.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_breakpoint_observer.cpp (93%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_breakpoint_observer.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_breakpoints.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_breakpoints.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_breakpoints_p.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_cmdlists.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_cmdlists.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_surface.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_surface.h (97%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_tracing.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_tracing.h (93%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_vertex_shader.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/debugger/graphics/graphics_vertex_shader.h (97%) rename src/{citra_qt => lucina3ds_qt}/debugger/ipc/record_dialog.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/debugger/ipc/record_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/ipc/record_dialog.ui (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/ipc/recorder.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/debugger/ipc/recorder.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/ipc/recorder.ui (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/lle_service_modules.cpp (95%) rename src/{citra_qt => lucina3ds_qt}/debugger/lle_service_modules.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/profiler.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/debugger/profiler.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/registers.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/debugger/registers.h (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/registers.ui (100%) rename src/{citra_qt => lucina3ds_qt}/debugger/wait_tree.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/debugger/wait_tree.h (100%) rename src/{citra_qt => lucina3ds_qt}/discord.h (100%) rename src/{citra_qt => lucina3ds_qt}/discord_impl.cpp (95%) rename src/{citra_qt => lucina3ds_qt}/discord_impl.h (93%) rename src/{citra_qt => lucina3ds_qt}/dumping/dumping_dialog.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/dumping/dumping_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/dumping/dumping_dialog.ui (100%) rename src/{citra_qt => lucina3ds_qt}/dumping/option_set_dialog.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/dumping/option_set_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/dumping/option_set_dialog.ui (100%) rename src/{citra_qt => lucina3ds_qt}/dumping/options_dialog.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/dumping/options_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/dumping/options_dialog.ui (100%) rename src/{citra_qt => lucina3ds_qt}/game_list.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/game_list.h (98%) rename src/{citra_qt => lucina3ds_qt}/game_list_p.h (99%) rename src/{citra_qt => lucina3ds_qt}/game_list_worker.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/game_list_worker.h (97%) rename src/{citra_qt => lucina3ds_qt}/hotkeys.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/hotkeys.h (100%) rename src/{citra_qt => lucina3ds_qt}/loading_screen.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/loading_screen.h (100%) rename src/{citra_qt => lucina3ds_qt}/loading_screen.ui (100%) rename src/{citra_qt/citra-qt.rc => lucina3ds_qt/lucina3ds-qt.rc} (100%) rename src/{citra_qt => lucina3ds_qt}/main.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/main.h (98%) rename src/{citra_qt => lucina3ds_qt}/main.ui (81%) rename src/{citra_qt => lucina3ds_qt}/movie/movie_play_dialog.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/movie/movie_play_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/movie/movie_play_dialog.ui (100%) rename src/{citra_qt => lucina3ds_qt}/movie/movie_record_dialog.cpp (95%) rename src/{citra_qt => lucina3ds_qt}/movie/movie_record_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/movie/movie_record_dialog.ui (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/chat_room.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/chat_room.h (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/chat_room.ui (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/client_room.cpp (94%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/client_room.h (94%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/client_room.ui (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/direct_connect.cpp (92%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/direct_connect.h (94%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/direct_connect.ui (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/host_room.cpp (93%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/host_room.h (97%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/host_room.ui (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/lobby.cpp (95%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/lobby.h (98%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/lobby.ui (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/lobby_p.h (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/message.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/message.h (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/moderation_dialog.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/moderation_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/moderation_dialog.ui (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/state.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/state.h (100%) rename src/{citra_qt => lucina3ds_qt}/multiplayer/validation.h (100%) rename src/{citra_qt => lucina3ds_qt}/precompiled_headers.h (100%) rename src/{citra_qt => lucina3ds_qt}/qt_image_interface.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/qt_image_interface.h (100%) rename src/{citra_qt => lucina3ds_qt}/uisettings.cpp (100%) rename src/{citra_qt => lucina3ds_qt}/uisettings.h (100%) rename src/{citra_qt => lucina3ds_qt}/updater/updater.cpp (98%) rename src/{citra_qt => lucina3ds_qt}/updater/updater.h (100%) rename src/{citra_qt => lucina3ds_qt}/updater/updater_p.h (97%) rename src/{citra_qt => lucina3ds_qt}/util/clickable_label.cpp (87%) rename src/{citra_qt => lucina3ds_qt}/util/clickable_label.h (100%) rename src/{citra_qt => lucina3ds_qt}/util/graphics_device_info.cpp (96%) rename src/{citra_qt => lucina3ds_qt}/util/graphics_device_info.h (100%) rename src/{citra_qt => lucina3ds_qt}/util/sequence_dialog/sequence_dialog.cpp (95%) rename src/{citra_qt => lucina3ds_qt}/util/sequence_dialog/sequence_dialog.h (100%) rename src/{citra_qt => lucina3ds_qt}/util/spinbox.cpp (99%) rename src/{citra_qt => lucina3ds_qt}/util/spinbox.h (100%) rename src/{citra_qt => lucina3ds_qt}/util/util.cpp (97%) rename src/{citra_qt => lucina3ds_qt}/util/util.h (100%) 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 0000000000000000000000000000000000000000..cc93e3550426631ca21758c205e47ec7b8635aec GIT binary patch literal 22624 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 literal 0 HcmV?d00001 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 2c408b9352295fdca33634f11df7f7f4ec227088..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370070 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 -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 9b5773214cff15cbe063b0ae0d76b5bbdb30cc39..48113c0da9e2619097a8909262a5ab1f67c9bfe0 100644 GIT binary patch delta 2458 zcmV;L31#-!JgXCsD}N5d000id0mpBsWB>pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H12{=haK~#90?VEdS6vrLMKjX9S{J`fk*g(L5ffNc&A+#k>XaXdN zB&4DeO7lcGDyl$fn@IhmrKwUvTY}V*s7+EuRZ)PdRVs*32!A9f-^^+ut+digE3LHBN-M3j(y}hm zhdM|}E{$Z-Up3lTzH8Y+g5o=L*a}E?P=a8Kl*|Jh<|;)F!%8Z2YN^i6QW?UaTu92q z0;KZe+1t;-gMmGshbd&1GFFod?G4(S$4%ndj57StH_ zIkz{QaQnhRj(PuSZHU*yCv!Yx z-;0!95CV$UfXuK5&VczK*;hD8I*`8{Asi6ejFk0=>~BHAvhL+%R{!A)hy}?$z!AeB z|5s4(yDpt}fLjkQLfas;Q(Mu9b&%msY-7|K82e{%j_*4h+bu|1gu^g!uY)_zAw28A zUzu1Y=YIi2W+_tELFYl}*c8qb&e1U9#l*H+KStc~GnjD-?%b$BS%qV(1xYIpkpp?F zVdNJG_X~&&4^kT7J&;(|vr=13=eywVgw}&d{}Fw^;-7-+8?o}u2a!e)k>Ns&{1MDN ziJ15&$Q)+zPqPz{VIp-ch2ly?!Foi-&{+N8S$_!D#>zTMV8|mn&$|tLfL}uGZ^M@q z2*zxPl@GQ+`|%siiHvL*^Bl}R1trhwmC&GCka3fni?LAnJ`}!#$ehrpBIC^bxJs@=5o0okj5V*I?FR$e(tLT9B&@icTQ17bDb= zNq;8q(Kx4_3xz+9v!0-a6NV(={BOcr2;sI$)Op0uv>r{~wkqeD33ES%iNEYUOt1}* zkqk}_xHlm@n-OZ(=}o@RV|Cq6BQl4FA<+EyI5_$cHUEM7L%Q$z--RKwv_o#K&~Efbi~nTz_91_UWQQjY&(-^21XFB?*%mh#B0PuFz+Dbln#;wiDK~VLpbLovI(9=);$Rg zYs3EK#`Tbz#v63q-1`vOQ*=htTiP*qg`Za84yva?>sA=@bmE3(;9~3n3!Vt)Nh1LD zTVp`ttSEfm?t`!NFvC>>DmGGWbX-h)uF=cjS~-;83!gm!tzX3Q^2Wiu_aQ4T6tE4D z5nUR*iEz%+V2loA^LnVB49#yqNPqRr1-Xk7>jrCL4>;rjea(9e+Uqn7KM3WA;z22E zsyh`MX08dk4gb}|wu|lqM?M$^RJ{WqPKRU9gYTNocTa{nTVjseHb7GK{c*@%7T(&G z?f-z9snGCi2(`ojI`2j}Jc)Iq`63tLEY;UKPTqhKhU4!7C!o%oVr@9`bblM2ZHa9= za=~*?><MSqchzZDtM79d=clfzNJqeG!M;^&m}Z9A*ma3<+w-o&2c zo|f~l|1sG6D2!T&6=hl~-Tu$mO?!)0Gm5*{&55q5)OzbMcpe1LTyT_v?^1F&IYm&a zUc;#X@g(@JMttBmlSk9NJ(D`Flt+fS-Lsf+vn5AtdXGvt$AW9B&VR3cQ(J@z>ZZL8 zfy|f{#ruePzlW?meSZ0GIxlYuih{m(KA|C1>4QRvFN0&4Kc&yiau~9dHv!Q zQ1e%4I{|0PkW+UcJb&|H{1RBP2(BK5Ks$__0lAT3>Z^exk3**q+;_vc2(+&1s5S>@ zK$jY_htzu_Y4z~-#iX~vF%*KWQ1LKadhI;5%A%A{yhk9d^ z>n||=MW}rj8h<|m|8)@AkTVuMvth);kX6w8GpV6i`HFcs7MB{ux0ifFWI{&X*0#3hI^CDGnRN7Vr=YO_9^Ev1Y+!AoC0V*U&hx(*QI8qKL znpUjWFA8miBSUxzrhJGA^)Z^H+%`BWZK(o(tG$*ZMY&jO{=+&;PFfh034{GeLMex1 zyU$VY%GpmwsLWEItrU(*zDI8OlcN57BAKePX5}Sl#d6Y0E3LHBN-M3j(n>2W`l0^= YRTBj+IVVJ382|tP07*qoM6N<$f(=HjW&i*H literal 7768 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 literal 18317 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 z z3ck(fQu5Yk&XReDBw^zn|~zOvd^B zyr-YZTw@Oq_x)Y@&+q@(L2pJ5q}aN#m7|eF>d}b=<_E3_3!`dV-xiLwfT0)%Fm9@_kLD> ZHz+@x_Xk|letvo-56A=ZKm~f>{{wAmUYh^_ literal 0 HcmV?d00001 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 dbbfa747336837dde5a5581bb8ce166839e577f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26290 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 FV1l+P(`f(z 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 0000000000000000000000000000000000000000..bf19e4cb53445dd01004cb5e5b9f3cf61b84e0da GIT binary patch literal 9824 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 literal 0 HcmV?d00001 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={}",