diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java index 34ab49c96..b5e547266 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java @@ -12,6 +12,7 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.SubMenu; @@ -19,11 +20,13 @@ import android.view.View; import android.widget.CheckBox; import android.widget.SeekBar; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.PopupMenu; import androidx.core.app.NotificationManagerCompat; import androidx.fragment.app.FragmentActivity; @@ -74,6 +77,7 @@ public final class EmulationActivity extends AppCompatActivity { public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15; public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16; public static final int MENU_ACTION_OPEN_CHEATS = 17; + public static final int MENU_ACTION_CLOSE_GAME = 18; public static final int REQUEST_SELECT_AMIIBO = 2; private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; @@ -114,6 +118,8 @@ public final class EmulationActivity extends AppCompatActivity { EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE); buttonsActionsMap .append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS); + buttonsActionsMap + .append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME); } private View mDecorView; @@ -223,21 +229,12 @@ public final class EmulationActivity extends AppCompatActivity { @Override public void onBackPressed() { - NativeLibrary.PauseEmulation(); - new AlertDialog.Builder(this) - .setTitle(R.string.emulation_close_game) - .setMessage(R.string.emulation_close_game_message) - .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> - { - mEmulationFragment.stopEmulation(); - finish(); - }) - .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> - NativeLibrary.UnPauseEmulation()) - .setOnCancelListener(dialogInterface -> - NativeLibrary.UnPauseEmulation()) - .create() - .show(); + View anchor = findViewById(R.id.menu_anchor); + PopupMenu popupMenu = new PopupMenu(this, anchor); + onCreateOptionsMenu(popupMenu.getMenu(), popupMenu.getMenuInflater()); + updateSavestateMenuOptions(popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(this::onOptionsItemSelected); + popupMenu.show(); } @Override @@ -271,6 +268,10 @@ public final class EmulationActivity extends AppCompatActivity { } } + public void onEmulationStarted() { + Toast.makeText(this, getString(R.string.emulation_menu_help), Toast.LENGTH_LONG).show(); + } + private void enableFullscreenImmersive() { // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar. mDecorView.setSystemUiVisibility( @@ -285,7 +286,12 @@ public final class EmulationActivity extends AppCompatActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_emulation, menu); + onCreateOptionsMenu(menu, getMenuInflater()); + return true; + } + + private void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_emulation, menu); int layoutOptionMenuItem = R.id.menu_screen_layout_landscape; switch (EmulationMenuSettings.getLandscapeScreenLayout()) { @@ -306,8 +312,6 @@ public final class EmulationActivity extends AppCompatActivity { menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps()); menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens()); menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay()); - - return true; } private void DisplaySavestateWarning() { @@ -333,12 +337,16 @@ public final class EmulationActivity extends AppCompatActivity { @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); + updateSavestateMenuOptions(menu); + return true; + } + private void updateSavestateMenuOptions(Menu menu) { final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo(); if (savestates == null) { menu.findItem(R.id.menu_emulation_save_state).setVisible(false); menu.findItem(R.id.menu_emulation_load_state).setVisible(false); - return true; + return; } menu.findItem(R.id.menu_emulation_save_state).setVisible(true); menu.findItem(R.id.menu_emulation_load_state).setVisible(true); @@ -367,7 +375,6 @@ public final class EmulationActivity extends AppCompatActivity { saveStateMenu.getItem(info.slot - 1).setTitle(text); loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true); } - return true; } @SuppressWarnings("WrongConstant") @@ -480,6 +487,24 @@ public final class EmulationActivity extends AppCompatActivity { case MENU_ACTION_OPEN_CHEATS: CheatsActivity.launch(this); break; + + case MENU_ACTION_CLOSE_GAME: + NativeLibrary.PauseEmulation(); + new AlertDialog.Builder(this) + .setTitle(R.string.emulation_close_game) + .setMessage(R.string.emulation_close_game_message) + .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> + { + mEmulationFragment.stopEmulation(); + finish(); + }) + .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> + NativeLibrary.UnPauseEmulation()) + .setOnCancelListener(dialogInterface -> + NativeLibrary.UnPauseEmulation()) + .create() + .show(); + break; } return true; diff --git a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java index 8f9d215a3..e71c2cfc1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java @@ -133,6 +133,8 @@ public class DiskShaderCacheProgress { case Complete: // Workaround for when dialog is dismissed when the app is in the background fragment.dismissAllowingStateLoss(); + + emulationActivity.runOnUiThread(emulationActivity::onEmulationStarted); break; } } diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml index 7d7f36925..5f5ff777d 100644 --- a/src/android/app/src/main/res/layout/activity_emulation.xml +++ b/src/android/app/src/main/res/layout/activity_emulation.xml @@ -14,4 +14,10 @@ android:layout_height="match_parent" android:transitionName="image_game_icon" /> - \ No newline at end of file + + + diff --git a/src/android/app/src/main/res/menu/menu_emulation.xml b/src/android/app/src/main/res/menu/menu_emulation.xml index b6c0d7cc4..8492dfdb9 100644 --- a/src/android/app/src/main/res/menu/menu_emulation.xml +++ b/src/android/app/src/main/res/menu/menu_emulation.xml @@ -115,4 +115,9 @@ app:showAsAction="never" android:title="@string/emulation_open_settings" /> + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index a3200d3d2..282b59a29 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -164,6 +164,7 @@ Invalid ROM format + Press Back to access the menu. Save State Load State Slot %1$d diff --git a/src/citra_qt/cheats.cpp b/src/citra_qt/cheats.cpp index fee42d55c..343568004 100644 --- a/src/citra_qt/cheats.cpp +++ b/src/citra_qt/cheats.cpp @@ -17,7 +17,6 @@ CheatDialog::CheatDialog(QWidget* parent) : QDialog(parent), ui(std::make_unique()) { // Setup gui control settings ui->setupUi(this); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->tableCheats->setColumnWidth(0, 30); ui->tableCheats->setColumnWidth(2, 85); ui->tableCheats->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp index 80670f2af..1b00ddbe3 100644 --- a/src/citra_qt/configuration/configure_dialog.cpp +++ b/src/citra_qt/configuration/configure_dialog.cpp @@ -20,8 +20,6 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry, bool PopulateSelectionList(); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - connect(ui->uiTab, &ConfigureUi::LanguageChanged, this, &ConfigureDialog::OnLanguageChanged); connect(ui->selectorList, &QListWidget::itemSelectionChanged, this, &ConfigureDialog::UpdateVisibleTabs); diff --git a/src/citra_qt/configuration/configure_per_game.cpp b/src/citra_qt/configuration/configure_per_game.cpp index 19760fba6..fc74e9e4c 100644 --- a/src/citra_qt/configuration/configure_per_game.cpp +++ b/src/citra_qt/configuration/configure_per_game.cpp @@ -41,8 +41,6 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const QString setFocusPolicy(Qt::ClickFocus); setWindowTitle(tr("Properties")); - // remove Help question mark button from the title bar - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); scene = new QGraphicsScene; ui->icon_view->setScene(scene); diff --git a/src/citra_qt/debugger/profiler.cpp b/src/citra_qt/debugger/profiler.cpp index 9f394a020..9f195f1ee 100644 --- a/src/citra_qt/debugger/profiler.cpp +++ b/src/citra_qt/debugger/profiler.cpp @@ -50,9 +50,8 @@ MicroProfileDialog::MicroProfileDialog(QWidget* parent) : QWidget(parent, Qt::Di setObjectName(QStringLiteral("MicroProfile")); setWindowTitle(tr("MicroProfile")); resize(1000, 600); - // Remove the "?" button from the titlebar and enable the maximize button - setWindowFlags((windowFlags() & ~Qt::WindowContextHelpButtonHint) | - Qt::WindowMaximizeButtonHint); + // Enable the maximize button + setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint); #if MICROPROFILE_ENABLED diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index d16d40de3..dd4ad6857 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -1019,6 +1019,11 @@ bool GMainWindow::LoadROM(const QString& filename) { "titles.")); break; + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + QMessageBox::critical(this, tr("Unsupported ROM"), + tr("GBA Virtual Console ROMs are not supported by Citra.")); + break; + case Core::System::ResultStatus::ErrorVideoCore: QMessageBox::critical( this, tr("Video Core Error"), @@ -2659,6 +2664,10 @@ int main(int argc, char* argv[]) { #ifdef __APPLE__ std::string bin_path = FileUtil::GetBundleDirectory() + DIR_SEP + ".."; chdir(bin_path.c_str()); +#endif +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + // Disables the "?" button on all dialogs. Disabled by default on Qt6. + QCoreApplication::setAttribute(Qt::AA_DisableWindowContextHelpButton); #endif QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); diff --git a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp b/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp index 1cfe3c0bd..0bba5348e 100644 --- a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp +++ b/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp @@ -9,7 +9,6 @@ SequenceDialog::SequenceDialog(QWidget* parent) : QDialog(parent) { setWindowTitle(tr("Enter a hotkey")); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); key_sequence = new QKeySequenceEdit; diff --git a/src/common/common_paths.h b/src/common/common_paths.h index eec4dde9c..1699f14e6 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -20,6 +20,10 @@ #else #ifdef _WIN32 #define EMU_DATA_DIR "Citra" +#elif defined(__APPLE__) +#define MACOS_EMU_DATA_DIR "Library" DIR_SEP "Application Support" DIR_SEP "Citra" +// For compatibility with XDG paths. +#define EMU_DATA_DIR "citra-emu" #elif ANDROID // On Android internal storage is mounted as "/sdcard" #define SDCARD_DIR "sdcard" diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 73c606972..7a53d294a 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -709,13 +709,26 @@ void SetUserPath(const std::string& path) { g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); } else { - std::string data_dir = GetUserDirectory("XDG_DATA_HOME"); - std::string config_dir = GetUserDirectory("XDG_CONFIG_HOME"); - std::string cache_dir = GetUserDirectory("XDG_CACHE_HOME"); + std::string data_dir = GetUserDirectory("XDG_DATA_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP; + std::string config_dir = + GetUserDirectory("XDG_CONFIG_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP; + std::string cache_dir = + GetUserDirectory("XDG_CACHE_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP; - user_path = data_dir + DIR_SEP EMU_DATA_DIR DIR_SEP; - g_paths.emplace(UserPath::ConfigDir, config_dir + DIR_SEP EMU_DATA_DIR DIR_SEP); - g_paths.emplace(UserPath::CacheDir, cache_dir + DIR_SEP EMU_DATA_DIR DIR_SEP); +#if defined(__APPLE__) + // If XDG directories don't already exist from a previous setup, use standard macOS + // paths. + if (!FileUtil::Exists(data_dir) && !FileUtil::Exists(config_dir) && + !FileUtil::Exists(cache_dir)) { + data_dir = GetHomeDirectory() + DIR_SEP MACOS_EMU_DATA_DIR DIR_SEP; + config_dir = data_dir + CONFIG_DIR DIR_SEP; + cache_dir = data_dir + CACHE_DIR DIR_SEP; + } +#endif + + user_path = data_dir; + g_paths.emplace(UserPath::ConfigDir, config_dir); + g_paths.emplace(UserPath::CacheDir, cache_dir); } #endif } diff --git a/src/core/core.cpp b/src/core/core.cpp index 11cbc1282..c369611e2 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -268,6 +268,8 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st return ResultStatus::ErrorLoader_ErrorEncrypted; case Loader::ResultStatus::ErrorInvalidFormat: return ResultStatus::ErrorLoader_ErrorInvalidFormat; + case Loader::ResultStatus::ErrorGbaTitle: + return ResultStatus::ErrorLoader_ErrorGbaTitle; default: return ResultStatus::ErrorSystemMode; } @@ -292,7 +294,6 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st telemetry_session->AddInitialInfo(*app_loader); std::shared_ptr process; const Loader::ResultStatus load_result{app_loader->Load(process)}; - kernel->SetCurrentProcess(process); if (Loader::ResultStatus::Success != load_result) { LOG_CRITICAL(Core, "Failed to load ROM (Error {})!", load_result); System::Shutdown(); @@ -302,10 +303,13 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st return ResultStatus::ErrorLoader_ErrorEncrypted; case Loader::ResultStatus::ErrorInvalidFormat: return ResultStatus::ErrorLoader_ErrorInvalidFormat; + case Loader::ResultStatus::ErrorGbaTitle: + return ResultStatus::ErrorLoader_ErrorGbaTitle; default: return ResultStatus::ErrorLoader; } } + kernel->SetCurrentProcess(process); cheat_engine = std::make_unique(*this); title_id = 0; if (app_loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { @@ -537,7 +541,8 @@ void System::Shutdown(bool is_deserializing) { perf_results.emulation_speed * 100.0); telemetry_session->AddField(performance, "Shutdown_Framerate", perf_results.game_fps); telemetry_session->AddField(performance, "Shutdown_Frametime", perf_results.frametime * 1000.0); - telemetry_session->AddField(performance, "Mean_Frametime_MS", perf_stats->GetMeanFrametime()); + telemetry_session->AddField(performance, "Mean_Frametime_MS", + perf_stats ? perf_stats->GetMeanFrametime() : 0); // Shutdown emulation session VideoCore::Shutdown(); diff --git a/src/core/core.h b/src/core/core.h index 61b1005a1..cb4be20fe 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -82,10 +82,12 @@ public: ErrorSystemMode, ///< Error determining the system mode ErrorLoader, ///< Error loading the specified application ErrorLoader_ErrorEncrypted, ///< Error loading the specified application due to encryption - ErrorLoader_ErrorInvalidFormat, ///< Error loading the specified application due to an - /// invalid format - ErrorSystemFiles, ///< Error in finding system files - ErrorVideoCore, ///< Error in the video core + ErrorLoader_ErrorInvalidFormat, ///< Error loading the specified application due to an + /// invalid format + ErrorLoader_ErrorGbaTitle, ///< Error loading the specified application as it is GBA Virtual + ///< Console + ErrorSystemFiles, ///< Error in finding system files + ErrorVideoCore, ///< Error in the video core ErrorVideoCore_ErrorGenericDrivers, ///< Error in the video core due to the user having /// generic drivers installed ErrorSavestate, ///< Error saving or loading diff --git a/src/core/file_sys/plugin_3gx.cpp b/src/core/file_sys/plugin_3gx.cpp index e59d39090..61928982d 100644 --- a/src/core/file_sys/plugin_3gx.cpp +++ b/src/core/file_sys/plugin_3gx.cpp @@ -26,7 +26,8 @@ #include "core/loader/loader.h" static std::string ReadTextInfo(FileUtil::IOFile& file, std::size_t offset, std::size_t max_size) { - if (max_size > 0x400) { // Limit read string size to 0x400 bytes, just in case + if (offset == 0 || max_size == 0 || + max_size > 0x400) { // Limit read string size to 0x400 bytes, just in case return ""; } std::vector char_data(max_size); diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index d42aab3a1..9f50c79d1 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -75,6 +75,7 @@ enum class ResultStatus { ErrorAlreadyLoaded, ErrorMemoryAllocationFailed, ErrorEncrypted, + ErrorGbaTitle, }; constexpr u32 MakeMagic(char a, char b, char c, char d) { diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index e376120a7..d233aef26 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -85,6 +85,11 @@ ResultStatus AppLoader_NCCH::LoadExec(std::shared_ptr& process) u64_le program_id; if (ResultStatus::Success == ReadCode(code) && ResultStatus::Success == ReadProgramId(program_id)) { + if (IsGbaVirtualConsole(code)) { + LOG_ERROR(Loader, "Encountered unsupported GBA Virtual Console code section."); + return ResultStatus::ErrorGbaTitle; + } + std::string process_name = Common::StringFromFixedZeroTerminatedBuffer( (const char*)overlay_ncch->exheader_header.codeset_info.name, 8); @@ -177,6 +182,12 @@ void AppLoader_NCCH::ParseRegionLockoutInfo() { } } +bool AppLoader_NCCH::IsGbaVirtualConsole(const std::vector& code) { + const u32* gbaVcHeader = reinterpret_cast(code.data() + code.size() - 0x10); + return code.size() >= 0x10 && gbaVcHeader[0] == MakeMagic('.', 'C', 'A', 'A') && + gbaVcHeader[1] == 1; +} + ResultStatus AppLoader_NCCH::Load(std::shared_ptr& process) { u64_le ncch_program_id; diff --git a/src/core/loader/ncch.h b/src/core/loader/ncch.h index 6f680b063..76ce61446 100644 --- a/src/core/loader/ncch.h +++ b/src/core/loader/ncch.h @@ -78,6 +78,9 @@ private: /// Reads the region lockout info in the SMDH and send it to CFG service void ParseRegionLockoutInfo(); + /// Detects whether the NCCH contains GBA Virtual Console. + bool IsGbaVirtualConsole(const std::vector& code); + FileSys::NCCHContainer base_ncch; FileSys::NCCHContainer update_ncch; FileSys::NCCHContainer* overlay_ncch;