From 624be242c477eae40ba6575009b3921878ecf2e1 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Sat, 16 Aug 2025 17:06:28 +0200 Subject: [PATCH] core: Add ability to insert cartridges --- dist/license.md | 1 + .../default/icons/128x128/cartridge.png | Bin 0 -> 4158 bytes dist/qt_themes/default/icons/index.theme | 7 +- .../default/icons_light/128x128/cartridge.png | Bin 0 -> 4158 bytes .../qt_themes/default/icons_light/index.theme | 7 +- dist/qt_themes/default/theme_default.qrc | 2 + license.txt | 1 + .../java/org/citra/citra_emu/NativeLibrary.kt | 6 + .../citra/citra_emu/adapters/GameAdapter.kt | 26 +++ .../citra_emu/fragments/EmulationFragment.kt | 3 + .../java/org/citra/citra_emu/model/Game.kt | 1 + .../org/citra/citra_emu/model/GameInfo.kt | 2 + .../org/citra/citra_emu/utils/GameHelper.kt | 1 + src/android/app/src/main/jni/game_info.cpp | 5 + src/android/app/src/main/jni/native.cpp | 16 +- .../app/src/main/res/drawable/cartridge.png | Bin 0 -> 4158 bytes .../app/src/main/res/layout/card_game.xml | 10 ++ .../src/main/res/layout/dialog_about_game.xml | 7 + .../app/src/main/res/values/strings.xml | 2 + src/citra_qt/citra_qt.cpp | 9 ++ src/citra_qt/configuration/config.cpp | 6 + src/citra_qt/game_list.cpp | 62 ++++++- src/citra_qt/game_list.h | 2 +- src/citra_qt/game_list_p.h | 5 +- src/citra_qt/game_list_worker.cpp | 3 +- src/citra_qt/uisettings.h | 2 + src/core/core.cpp | 13 ++ src/core/core.h | 11 ++ src/core/file_sys/archive_ncch.cpp | 33 +++- src/core/hle/service/am/am.cpp | 152 ++++++++++++------ src/core/hle/service/apt/applet_manager.cpp | 43 ++++- src/core/hle/service/apt/applet_manager.h | 5 + src/core/hle/service/apt/apt.cpp | 14 ++ src/core/hle/service/apt/apt.h | 2 + src/core/hle/service/apt/ns.cpp | 26 ++- src/core/hle/service/apt/ns_s.cpp | 4 +- src/core/hle/service/fs/fs_user.cpp | 24 ++- src/core/hle/service/fs/fs_user.h | 6 +- src/core/loader/loader.h | 4 + src/core/loader/ncch.h | 4 + 40 files changed, 445 insertions(+), 82 deletions(-) create mode 100644 dist/qt_themes/default/icons/128x128/cartridge.png create mode 100644 dist/qt_themes/default/icons_light/128x128/cartridge.png create mode 100644 src/android/app/src/main/res/drawable/cartridge.png diff --git a/dist/license.md b/dist/license.md index 207fc638e..801b0a519 100644 --- a/dist/license.md +++ b/dist/license.md @@ -16,6 +16,7 @@ qt_themes/default/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com qt_themes/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com +qt_themes/default/icons/128x128/cartridge.png | CC0 1.0 | Designed by PabloMK7 qt_themes/qdarkstyle/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com qt_themes/qdarkstyle/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com diff --git a/dist/qt_themes/default/icons/128x128/cartridge.png b/dist/qt_themes/default/icons/128x128/cartridge.png new file mode 100644 index 0000000000000000000000000000000000000000..cb669ee2917d3a8bca4b3500af3b619a374eb816 GIT binary patch literal 4158 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/48x48/no_avatar.png icons/48x48/plus.png icons/48x48/sd_card.png + icons/128x128/cartridge.png icons/256x256/azahar.png icons/48x48/star.png icons/256x256/plus_folder.png @@ -31,6 +32,7 @@ icons_light/48x48/no_avatar.png icons_light/48x48/plus.png icons_light/48x48/sd_card.png + icons_light/128x128/cartridge.png icons_light/256x256/azahar.png icons_light/48x48/star.png icons_light/256x256/plus_folder.png diff --git a/license.txt b/license.txt index f94c73f1a..7b0e5ad1b 100644 --- a/license.txt +++ b/license.txt @@ -357,3 +357,4 @@ plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 fro plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com sd_card.png | CC BY-ND 3.0 | https://icons8.com star.png | CC BY-ND 3.0 | https://icons8.com +cartridge.png | CC0 1.0 | Designed by PabloMK7 diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 56a47bbb6..20c3c0868 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -139,6 +139,12 @@ object NativeLibrary { external fun createLogFile() external fun logUserDirectory(directory: String) + /** + * Set the inserted cartridge that will appear + * in the home menu. Empty string to clear. + */ + external fun setInsertedCartridge(path: String) + /** * Begins emulation. */ diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt index a92f93401..dae538d73 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt @@ -13,6 +13,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.content.Context +import android.content.SharedPreferences import android.widget.TextView import android.widget.ImageView import android.widget.Toast @@ -69,6 +70,9 @@ class GameAdapter( private var imagePath: String? = null private var dialogShortcutBinding: DialogShortcutBinding? = null + private val preferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + fun handleShortcutImageResult(uri: Uri?) { val path = uri?.toString() if (path != null) { @@ -196,6 +200,11 @@ class GameAdapter( binding.textGameTitle.text = game.title binding.textCompany.text = game.company binding.textGameRegion.text = game.regions + binding.imageCartridge.visibility = if (preferences.getString("insertedCartridge", "") != game.path) { + View.GONE + } else { + View.VISIBLE + } val backgroundColorId = if ( @@ -345,12 +354,29 @@ class GameAdapter( val bottomSheetDialog = BottomSheetDialog(context) bottomSheetDialog.setContentView(bottomSheetView) + val insertable = game.isInsertable + val inserted = insertable && (preferences.getString("insertedCartridge", "") == game.path) + bottomSheetView.findViewById(R.id.about_game_title).text = game.title bottomSheetView.findViewById(R.id.about_game_company).text = game.company bottomSheetView.findViewById(R.id.about_game_region).text = game.regions bottomSheetView.findViewById(R.id.about_game_id).text = context.getString(R.string.game_context_id) + " " + String.format("%016X", game.titleId) bottomSheetView.findViewById(R.id.about_game_filename).text = context.getString(R.string.game_context_file) + " " + game.filename bottomSheetView.findViewById(R.id.about_game_filetype).text = context.getString(R.string.game_context_type) + " " + game.fileType + + val insertButton = bottomSheetView.findViewById(R.id.insert_cartridge_button) + insertButton.text = if (inserted) { context.getString(R.string.game_context_eject) } else { context.getString(R.string.game_context_insert) } + insertButton.visibility = if (insertable) View.VISIBLE else View.GONE + insertButton.setOnClickListener { + if (inserted) { + preferences.edit().putString("insertedCartridge", "").apply() + } else { + preferences.edit().putString("insertedCartridge", game.path).apply() + } + bottomSheetDialog.dismiss() + notifyItemRangeChanged(0, currentList.size) + } + GameIconUtils.loadGameIcon(activity, game, bottomSheetView.findViewById(R.id.game_icon)) bottomSheetView.findViewById(R.id.about_game_play).setOnClickListener { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 419919527..c27d49be7 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -144,6 +144,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram } } + val insertedCartridge = preferences.getString("insertedCartridge", "") + NativeLibrary.setInsertedCartridge(insertedCartridge ?: "") + try { game = args.game ?: intentGame!! } catch (e: NullPointerException) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt index b74635e96..797b7a262 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt @@ -25,6 +25,7 @@ class Game( val isInstalled: Boolean = false, val isSystemTitle: Boolean = false, val isVisibleSystemTitle: Boolean = false, + val isInsertable: Boolean = false, val icon: IntArray? = null, val fileType: String = "", val isCompressed: Boolean = false, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt index 817e5fdec..494d7bf75 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt @@ -37,6 +37,8 @@ class GameInfo(path: String) { external fun getFileType(): String + external fun getIsInsertable(): Boolean + companion object { @JvmStatic private external fun initialize(path: String): Long diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt index 7b7ca9fdb..90b011114 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt @@ -88,6 +88,7 @@ object GameHelper { isInstalled, gameInfo?.isSystemTitle() ?: false, gameInfo?.getIsVisibleSystemTitle() ?: false, + gameInfo?.getIsInsertable() ?: false, gameInfo?.getIcon(), gameInfo?.getFileType() ?: "", gameInfo?.getFileType()?.contains("(Z)") ?: false, diff --git a/src/android/app/src/main/jni/game_info.cpp b/src/android/app/src/main/jni/game_info.cpp index ebf61820a..7ff043fff 100644 --- a/src/android/app/src/main/jni/game_info.cpp +++ b/src/android/app/src/main/jni/game_info.cpp @@ -25,6 +25,7 @@ struct GameInfoData { bool loaded = false; bool is_encrypted = false; std::string file_type = ""; + bool is_insertable = false; }; GameInfoData* GetNewGameInfoData(const std::string& path) { @@ -89,6 +90,7 @@ GameInfoData* GetNewGameInfoData(const std::string& path) { gid->is_encrypted = is_encrypted; gid->title_id = program_id; gid->file_type = Loader::GetFileTypeString(loader->GetFileType(), loader->IsFileCompressed()); + gid->is_insertable = loader->GetFileType() == Loader::FileType::CCI; return gid; } @@ -230,4 +232,7 @@ jstring Java_org_citra_citra_1emu_model_GameInfo_getFileType(JNIEnv* env, jobjec return ToJString(env, file_type); } +jboolean Java_org_citra_citra_1emu_model_GameInfo_getIsInsertable(JNIEnv* env, jobject obj) { + return GetPointer(env, obj)->is_insertable; +} } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index a0dc4d864..779163cfc 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -101,6 +101,8 @@ std::mutex paused_mutex; std::mutex running_mutex; std::condition_variable running_cv; +std::string inserted_cartridge; + } // Anonymous namespace static jobject ToJavaCoreError(Core::System::ResultStatus result) { @@ -148,7 +150,10 @@ static void TryShutdown() { secondary_window->DoneCurrent(); } - Core::System::GetInstance().Shutdown(); + Core::System& system{Core::System::GetInstance()}; + + system.Shutdown(); + system.EjectCartridge(); window.reset(); if (secondary_window) { @@ -179,6 +184,10 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { Core::System& system{Core::System::GetInstance()}; + if (!inserted_cartridge.empty()) { + system.InsertCartridge(inserted_cartridge); + } + const auto graphics_api = Settings::values.graphics_api.GetValue(); EGLContext* shared_context; switch (graphics_api) { @@ -1090,4 +1099,9 @@ jlong Java_org_citra_citra_1emu_NativeLibrary_playTimeManagerGetCurrentTitleId(J return ptm_current_title_id; } +void Java_org_citra_citra_1emu_NativeLibrary_setInsertedCartridge(JNIEnv* env, jobject obj, + jstring path) { + inserted_cartridge = GetJString(env, path); +} + } // extern "C" diff --git a/src/android/app/src/main/res/drawable/cartridge.png b/src/android/app/src/main/res/drawable/cartridge.png new file mode 100644 index 0000000000000000000000000000000000000000..cb669ee2917d3a8bca4b3500af3b619a374eb816 GIT binary patch literal 4158 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 + + + + ID: File: Type: + Insert Cartridge + Eject Cartridge Show Performance Overlay diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index 6398d1f22..d8d37a5e6 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -1243,6 +1243,10 @@ bool GMainWindow::LoadROM(const QString& filename) { const auto scope = render_window->Acquire(); + if (!UISettings::values.inserted_cartridge.GetValue().empty()) { + system.InsertCartridge(UISettings::values.inserted_cartridge.GetValue()); + } + const Core::System::ResultStatus result{ system.Load(*render_window, filename.toStdString(), secondary_window)}; @@ -1532,6 +1536,8 @@ void GMainWindow::ShutdownGame() { emu_thread->wait(); emu_thread = nullptr; + system.EjectCartridge(); + OnCloseMovie(); discord_rpc->Update(); @@ -3813,6 +3819,9 @@ void GMainWindow::closeEvent(QCloseEvent* event) { ShutdownGame(); } + // Save settings in case they were changed from outside the configuration menu. + config->Save(); + render_window->close(); secondary_window->close(); multiplayer_state->Close(); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 4c7d99c93..2eb891a75 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -670,6 +670,11 @@ void QtConfig::ReadPathValues() { ReadSetting(QStringLiteral("last_artic_base_addr"), QString{}).toString(); UISettings::values.recent_files = ReadSetting(QStringLiteral("recentFiles")).toStringList(); UISettings::values.language = ReadSetting(QStringLiteral("language"), QString{}).toString(); + + ReadBasicSetting(UISettings::values.inserted_cartridge); + if (!FileUtil::Exists(UISettings::values.inserted_cartridge.GetValue())) { + UISettings::values.inserted_cartridge.SetValue(""); + } } qt_config->endGroup(); @@ -1204,6 +1209,7 @@ void QtConfig::SavePathValues() { UISettings::values.last_artic_base_addr, QString{}); WriteSetting(QStringLiteral("recentFiles"), UISettings::values.recent_files); WriteSetting(QStringLiteral("language"), UISettings::values.language, QString{}); + WriteBasicSetting(UISettings::values.inserted_cartridge); } qt_config->endGroup(); diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index c275090dc..d6e3efdb5 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -19,8 +19,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -307,6 +309,42 @@ void GameList::OnFilterCloseClicked() { main_window->filterBarSetChecked(false); } +class CartridgeIconDelegate : public QStyledItemDelegate { +public: + using QStyledItemDelegate::QStyledItemDelegate; + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override { + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + QStyle* style = opt.widget ? opt.widget->style() : QApplication::style(); + + // Draw the default item (background, text, selection, etc.) + style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget); + + // Draw cartridge inserted icon + quint32 can_insert = index.data(GameListItemPath::CanInsertRole).value(); + QString game_path = index.data(GameListItemPath::FullPathRole).value(); + + bool is_inserted = can_insert && UISettings::values.inserted_cartridge.GetValue() == + game_path.toStdString(); + + if (is_inserted) { + QPixmap pixmap = QIcon::fromTheme(QStringLiteral("cartridge")).pixmap(24); + + const int margin = 12; + QSize pmSize = pixmap.size() / pixmap.devicePixelRatio(); + + QRect pmRect(opt.rect.right() - pmSize.width() - margin, + opt.rect.center().y() - pmSize.height() / 2, pmSize.width(), + pmSize.height()); + + painter->drawPixmap(pmRect, pixmap); + } + } +}; + GameList::GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent) : QWidget{parent}, play_time_manager{play_time_manager_} { watcher = new QFileSystemWatcher(this); @@ -329,6 +367,7 @@ GameList::GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* p tree_view->setEditTriggers(QHeaderView::NoEditTriggers); tree_view->setContextMenuPolicy(Qt::CustomContextMenu); tree_view->setStyleSheet(QStringLiteral("QTreeView{ border: none; }")); + tree_view->setItemDelegateForColumn(0, new CartridgeIconDelegate(tree_view)); tree_view->header()->setContextMenuPolicy(Qt::CustomContextMenu); UpdateColumnVisibility(); @@ -534,7 +573,8 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { selected.data(GameListItemPath::ProgramIdRole).toULongLong(), selected.data(GameListItemPath::ExtdataIdRole).toULongLong(), static_cast( - selected.data(GameListItemPath::MediaTypeRole).toUInt())); + selected.data(GameListItemPath::MediaTypeRole).toUInt()), + selected.data(GameListItemPath::CanInsertRole).toUInt() != 0); break; case GameListItemType::CustomDir: AddPermDirPopup(context_menu, selected); @@ -604,8 +644,16 @@ void ForEachOpenGLCacheFile(u64 program_id, auto func) { #endif void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, - u64 program_id, u64 extdata_id, Service::FS::MediaType media_type) { + u64 program_id, u64 extdata_id, Service::FS::MediaType media_type, + bool can_insert) { QAction* favorite = context_menu.addAction(tr("Favorite")); + bool is_inserted = + can_insert && UISettings::values.inserted_cartridge.GetValue() == path.toStdString(); + QAction* cartridge_insert = nullptr; + if (can_insert) { + cartridge_insert = + context_menu.addAction(is_inserted ? tr("Eject Cartridge") : tr("Insert Cartridge")); + } context_menu.addSeparator(); QMenu* open_menu = context_menu.addMenu(tr("Open")); QAction* open_application_location = open_menu->addAction(tr("Application Location")); @@ -719,6 +767,16 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr connect(open_extdata_location, &QAction::triggered, this, [this, extdata_id] { emit OpenFolderRequested(extdata_id, GameListOpenTarget::EXT_DATA); }); + if (cartridge_insert) { + connect(cartridge_insert, &QAction::triggered, this, [this, path, is_inserted] { + if (is_inserted) { + UISettings::values.inserted_cartridge.SetValue(""); + } else { + UISettings::values.inserted_cartridge.SetValue(path.toStdString()); + } + tree_view->viewport()->update(); + }); + } connect(open_application_location, &QAction::triggered, this, [this, program_id] { emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); }); diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index d563f74b1..a28cf4290 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -124,7 +124,7 @@ private: void PopupContextMenu(const QPoint& menu_location); void PopupHeaderContextMenu(const QPoint& menu_location); void AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, u64 program_id, - u64 extdata_id, Service::FS::MediaType media_type); + u64 extdata_id, Service::FS::MediaType media_type, bool can_insert); void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); void AddFavoritesPopup(QMenu& context_menu); diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index 046ec7267..45df2a483 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -159,15 +159,18 @@ public: static constexpr int ExtdataIdRole = SortRole + 4; static constexpr int LongTitleRole = SortRole + 5; static constexpr int MediaTypeRole = SortRole + 6; + static constexpr int CanInsertRole = SortRole + 7; GameListItemPath() = default; GameListItemPath(const QString& game_path, std::span smdh_data, u64 program_id, - u64 extdata_id, Service::FS::MediaType media_type, bool is_encrypted) { + u64 extdata_id, Service::FS::MediaType media_type, bool is_encrypted, + bool can_insert) { setData(type(), TypeRole); setData(game_path, FullPathRole); setData(qulonglong(program_id), ProgramIdRole); setData(qulonglong(extdata_id), ExtdataIdRole); setData(quint32(media_type), MediaTypeRole); + setData(quint32(can_insert), CanInsertRole); if (UISettings::values.game_list_icon_size.GetValue() == UISettings::GameListIconSize::NoIcon) { diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp index 7e5c4650f..cc65b5082 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/citra_qt/game_list_worker.cpp @@ -113,7 +113,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign { new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, extdata_id, media_type, - res == Loader::ResultStatus::ErrorEncrypted), + res == Loader::ResultStatus::ErrorEncrypted, + loader->GetFileType() == Loader::FileType::CCI), new GameListItemCompat(compatibility), new GameListItemRegion(smdh), new GameListItem(QString::fromStdString(Loader::GetFileTypeString( diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index 393e20dd4..f9f42353a 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -85,6 +85,8 @@ struct Values { Settings::Setting hide_mouse{false, "hideInactiveMouse"}; Settings::Setting check_for_update_on_start{true, "check_for_update_on_start"}; + Settings::Setting inserted_cartridge{"", "inserted_cartridge"}; + // Discord RPC Settings::Setting enable_discord_presence{true, "enable_discord_presence"}; diff --git a/src/core/core.cpp b/src/core/core.cpp index d00de677c..1622f3941 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -24,6 +24,7 @@ #include "core/core.h" #include "core/core_timing.h" #include "core/dumping/backend.h" +#include "core/file_sys/ncch_container.h" #include "core/frontend/image_interface.h" #include "core/gdbstub/gdbstub.h" #include "core/global.h" @@ -812,6 +813,18 @@ void System::RegisterAppLoaderEarly(std::unique_ptr& loader) early_app_loader = std::move(loader); } +void System::InsertCartridge(const std::string& path) { + FileSys::NCCHContainer cartridge_container(path); + if (cartridge_container.LoadHeader() == Loader::ResultStatus::Success && + cartridge_container.IsNCSD()) { + inserted_cartridge = path; + } +} + +void System::EjectCartridge() { + inserted_cartridge.clear(); +} + bool System::IsInitialSetup() { return app_loader && app_loader->DoingInitialSetup(); } diff --git a/src/core/core.h b/src/core/core.h index 122401eed..b33fea174 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -376,6 +376,14 @@ public: void RegisterAppLoaderEarly(std::unique_ptr& loader); + void InsertCartridge(const std::string& path); + + void EjectCartridge(); + + const std::string& GetCartridge() const { + return inserted_cartridge; + } + bool IsInitialSetup(); private: @@ -399,6 +407,9 @@ private: // Temporary app loader passed from frontend std::unique_ptr early_app_loader; + /// Path for current inserted cartridge + std::string inserted_cartridge; + /// ARM11 CPU core std::vector> cpu_cores; ARM_Interface* running_core = nullptr; diff --git a/src/core/file_sys/archive_ncch.cpp b/src/core/file_sys/archive_ncch.cpp index 345ba6a0f..514a8736e 100644 --- a/src/core/file_sys/archive_ncch.cpp +++ b/src/core/file_sys/archive_ncch.cpp @@ -88,14 +88,31 @@ ResultVal> NCCHArchive::OpenFile(const Path& path, std::memcpy(&openfile_path, binary.data(), sizeof(NCCHFilePath)); std::string file_path; - if (Settings::values.is_new_3ds) { - // Try the New 3DS specific variant first. - file_path = Service::AM::GetTitleContentPath(media_type, title_id | 0x20000000, - openfile_path.content_index); - } - if (!Settings::values.is_new_3ds || !FileUtil::Exists(file_path)) { - file_path = - Service::AM::GetTitleContentPath(media_type, title_id, openfile_path.content_index); + + if (media_type == Service::FS::MediaType::GameCard) { + const auto& cartridge = Core::System::GetInstance().GetCartridge(); + if (cartridge.empty()) { + return ResultNotFound; + } + + u64 card_program_id; + auto cartridge_loader = Loader::GetLoader(cartridge); + FileSys::NCCHContainer cartridge_ncch(cartridge); + if (cartridge_ncch.ReadProgramId(card_program_id) != Loader::ResultStatus::Success || + card_program_id != title_id) { + return ResultNotFound; + } + file_path = cartridge; + } else { + if (Settings::values.is_new_3ds) { + // Try the New 3DS specific variant first. + file_path = Service::AM::GetTitleContentPath(media_type, title_id | 0x20000000, + openfile_path.content_index); + } + if (!Settings::values.is_new_3ds || !FileUtil::Exists(file_path)) { + file_path = + Service::AM::GetTitleContentPath(media_type, title_id, openfile_path.content_index); + } } auto ncch_container = NCCHContainer(file_path, 0, openfile_path.content_index); diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 3bae37cfa..645a015b6 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -1285,7 +1285,7 @@ std::string GetTitleContentPath(Service::FS::MediaType media_type, u64 tid, std: auto fs_user = Core::System::GetInstance().ServiceManager().GetService( "fs:USER"); - return fs_user->GetCurrentGamecardPath(); + return fs_user->GetRegisteredGamecardPath(); } std::string content_path = GetTitlePath(media_type, tid) + "content/"; @@ -1330,7 +1330,7 @@ std::string GetTitlePath(Service::FS::MediaType media_type, u64 tid) { auto fs_user = Core::System::GetInstance().ServiceManager().GetService( "fs:USER"); - return fs_user->GetCurrentGamecardPath(); + return fs_user->GetRegisteredGamecardPath(); } return ""; @@ -1351,7 +1351,7 @@ std::string GetMediaTitlePath(Service::FS::MediaType media_type) { auto fs_user = Core::System::GetInstance().ServiceManager().GetService( "fs:USER"); - return fs_user->GetCurrentGamecardPath(); + return fs_user->GetRegisteredGamecardPath(); } return ""; @@ -1414,39 +1414,52 @@ void Module::ScanForTitlesImpl(Service::FS::MediaType media_type) { LOG_DEBUG(Service_AM, "Starting title scan for media_type={}", static_cast(media_type)); - std::string title_path = GetMediaTitlePath(media_type); - - FileUtil::FSTEntry entries; - FileUtil::ScanDirectoryTree(title_path, entries, 1, &stop_scan_flag); - for (const FileUtil::FSTEntry& tid_high : entries.children) { - if (stop_scan_flag) { - break; + if (media_type == FS::MediaType::GameCard) { + const auto& cartridge = system.GetCartridge(); + if (!cartridge.empty()) { + u64 program_id = 0; + FileSys::NCCHContainer cartridge_ncch(cartridge); + Loader::ResultStatus res = cartridge_ncch.ReadProgramId(program_id); + if (res == Loader::ResultStatus::Success) { + am_title_list[static_cast(media_type)].push_back(program_id); + } } - for (const FileUtil::FSTEntry& tid_low : tid_high.children) { + } else { + std::string title_path = GetMediaTitlePath(media_type); + + FileUtil::FSTEntry entries; + FileUtil::ScanDirectoryTree(title_path, entries, 1, &stop_scan_flag); + for (const FileUtil::FSTEntry& tid_high : entries.children) { if (stop_scan_flag) { break; } - std::string tid_string = tid_high.virtualName + tid_low.virtualName; + for (const FileUtil::FSTEntry& tid_low : tid_high.children) { + if (stop_scan_flag) { + break; + } + std::string tid_string = tid_high.virtualName + tid_low.virtualName; - if (tid_string.length() == TITLE_ID_VALID_LENGTH) { - const u64 tid = std::stoull(tid_string, nullptr, 16); + if (tid_string.length() == TITLE_ID_VALID_LENGTH) { + const u64 tid = std::stoull(tid_string, nullptr, 16); - if (tid & TWL_TITLE_ID_FLAG) { - // TODO(PabloMK7) Move to TWL Nand, for now only check that - // the contents exists in CTR Nand as this is a SRL file - // instead of NCCH. - if (FileUtil::Exists(GetTitleContentPath(media_type, tid))) { - am_title_list[static_cast(media_type)].push_back(tid); - } - } else { - FileSys::NCCHContainer container(GetTitleContentPath(media_type, tid)); - if (container.Load() == Loader::ResultStatus::Success) { - am_title_list[static_cast(media_type)].push_back(tid); + if (tid & TWL_TITLE_ID_FLAG) { + // TODO(PabloMK7) Move to TWL Nand, for now only check that + // the contents exists in CTR Nand as this is a SRL file + // instead of NCCH. + if (FileUtil::Exists(GetTitleContentPath(media_type, tid))) { + am_title_list[static_cast(media_type)].push_back(tid); + } + } else { + FileSys::NCCHContainer container(GetTitleContentPath(media_type, tid)); + if (container.Load() == Loader::ResultStatus::Success) { + am_title_list[static_cast(media_type)].push_back(tid); + } } } } } } + LOG_DEBUG(Service_AM, "Finished title scan for media_type={}", static_cast(media_type)); } @@ -1455,6 +1468,7 @@ void Module::ScanForAllTitles() { ScanForTicketsImpl(); ScanForTitlesImpl(Service::FS::MediaType::NAND); ScanForTitlesImpl(Service::FS::MediaType::SDMC); + ScanForTitlesImpl(Service::FS::MediaType::GameCard); } else { scan_all_future = std::async([this]() { std::scoped_lock lock(am_lists_mutex); @@ -1465,6 +1479,9 @@ void Module::ScanForAllTitles() { if (!stop_scan_flag) { ScanForTitlesImpl(Service::FS::MediaType::SDMC); } + if (!stop_scan_flag) { + ScanForTitlesImpl(Service::FS::MediaType::GameCard); + } }); } } @@ -1987,30 +2004,75 @@ void Module::Interface::GetProgramList(Kernel::HLERequestContext& ctx) { } } -Result GetTitleInfoFromList(std::span title_id_list, Service::FS::MediaType media_type, +Result GetTitleInfoFromList(Core::System& system, std::span title_id_list, + Service::FS::MediaType media_type, std::vector& title_info_out) { title_info_out.reserve(title_id_list.size()); for (u32 i = 0; i < title_id_list.size(); i++) { - std::string tmd_path = GetTitleMetadataPath(media_type, title_id_list[i]); + if (media_type == Service::FS::MediaType::GameCard) { + auto& cartridge = system.GetCartridge(); + if (cartridge.empty()) { + LOG_DEBUG(Service_AM, "cartridge not inserted"); + return Result(ErrorDescription::NotFound, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + } - TitleInfo title_info = {}; - title_info.tid = title_id_list[i]; + FileSys::NCCHContainer ncch_container(cartridge); + if (ncch_container.Load() != Loader::ResultStatus::Success || + !ncch_container.IsNCSD()) { + LOG_ERROR(Service_AM, "failed to load cartridge card"); + return Result(ErrorDescription::NotFound, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + } - FileSys::TitleMetadata tmd; - if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { - // TODO(shinyquagsire23): This is the total size of all files this process owns, - // including savefiles and other content. This comes close but is off. - title_info.size = tmd.GetContentSizeByIndex(FileSys::TMDContentIndex::Main); - title_info.version = tmd.GetTitleVersion(); - title_info.type = tmd.GetTitleType(); + // This is what Process9 does for getting the information, from disassembly. + // It is still unclear what do those values mean, like the title info type. + if (ncch_container.exheader_header.arm11_system_local_caps.program_id != + title_id_list[i]) { + LOG_DEBUG(Service_AM, + "cartridge has different title ID than requested title_id={:016X} != " + "cartridge_title_id={:016X}", + title_id_list[i], + ncch_container.exheader_header.arm11_system_local_caps.program_id); + return Result(ErrorDescription::NotFound, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + } + + TitleInfo title_info = {}; + title_info.tid = title_id_list[i]; + title_info.version = + (*reinterpret_cast( + &ncch_container.exheader_header.codeset_info.flags.remaster_version) + << 10) & + 0xFC00; + title_info.size = 0; + title_info.type = 0x40; + + LOG_DEBUG(Service_AM, "found title_id={:016X} version={:04X}", title_id_list[i], + title_info.version); + title_info_out.push_back(title_info); } else { - LOG_DEBUG(Service_AM, "not found title_id={:016X}", title_id_list[i]); - return Result(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState, - ErrorLevel::Permanent); + std::string tmd_path = GetTitleMetadataPath(media_type, title_id_list[i]); + + TitleInfo title_info = {}; + title_info.tid = title_id_list[i]; + + FileSys::TitleMetadata tmd; + if (tmd.Load(tmd_path) == Loader::ResultStatus::Success) { + // TODO(shinyquagsire23): This is the total size of all files this process owns, + // including savefiles and other content. This comes close but is off. + title_info.size = tmd.GetContentSizeByIndex(FileSys::TMDContentIndex::Main); + title_info.version = tmd.GetTitleVersion(); + title_info.type = tmd.GetTitleType(); + } else { + LOG_DEBUG(Service_AM, "not found title_id={:016X}", title_id_list[i]); + return Result(ErrorDescription::NotFound, ErrorModule::AM, + ErrorSummary::InvalidState, ErrorLevel::Permanent); + } + LOG_DEBUG(Service_AM, "found title_id={:016X} version={:04X}", title_id_list[i], + title_info.version); + title_info_out.push_back(title_info); } - LOG_DEBUG(Service_AM, "found title_id={:016X} version={:04X}", title_id_list[i], - title_info.version); - title_info_out.push_back(title_info); } return ResultSuccess; @@ -2142,7 +2204,7 @@ void Module::Interface::GetProgramInfosImpl(Kernel::HLERequestContext& ctx, bool } if (async_data->res.IsSuccess()) { - async_data->res = GetTitleInfoFromList(async_data->title_id_list, + async_data->res = GetTitleInfoFromList(am->system, async_data->title_id_list, async_data->media_type, async_data->out); } return 0; @@ -2356,7 +2418,7 @@ void Module::Interface::GetDLCTitleInfos(Kernel::HLERequestContext& ctx) { } if (async_data->res.IsSuccess()) { - async_data->res = GetTitleInfoFromList(async_data->title_id_list, + async_data->res = GetTitleInfoFromList(am->system, async_data->title_id_list, async_data->media_type, async_data->out); } return 0; @@ -2503,7 +2565,7 @@ void Module::Interface::GetPatchTitleInfos(Kernel::HLERequestContext& ctx) { } if (async_data->res.IsSuccess()) { - async_data->res = GetTitleInfoFromList(async_data->title_id_list, + async_data->res = GetTitleInfoFromList(am->system, async_data->title_id_list, async_data->media_type, async_data->out); } return 0; diff --git a/src/core/hle/service/apt/applet_manager.cpp b/src/core/hle/service/apt/applet_manager.cpp index 47c0792c6..1dcbb430e 100644 --- a/src/core/hle/service/apt/applet_manager.cpp +++ b/src/core/hle/service/apt/applet_manager.cpp @@ -396,6 +396,10 @@ ResultVal AppletManager::Initialize(AppletId ap // Note: In the real console the title id of a given applet slot is set by the APT module when // calling StartApplication. slot_data->title_id = system.Kernel().GetCurrentProcess()->codeset->program_id; + if (app_id == AppletId::Application) { + slot_data->media_type = next_app_mediatype; + next_app_mediatype = static_cast(UINT32_MAX); + } slot_data->attributes.raw = attributes.raw; // Applications need to receive a Wakeup signal to actually start up, this signal is usually @@ -1207,7 +1211,14 @@ ResultVal AppletManager::GetAppletInfo(AppletId app_i ErrorLevel::Status); } - auto media_type = Service::AM::GetTitleMediaType(slot_data->title_id); + FS::MediaType media_type; + if (slot_data->media_type != static_cast(UINT32_MAX)) { + media_type = slot_data->media_type; + } else { + // Applet was not started from StartApplication, so we need to guess. + media_type = Service::AM::GetTitleMediaType(slot_data->title_id); + } + return AppletInfo{ .title_id = slot_data->title_id, .media_type = media_type, @@ -1234,7 +1245,13 @@ ResultVal AppletManager::Unknown54(u32 in_param) { in_param >= 0x40 ? Service::FS::MediaType::GameCard : Service::FS::MediaType::SDMC; auto check_update = in_param == 0x01 || in_param == 0x42; - auto app_media_type = Service::AM::GetTitleMediaType(slot_data->title_id); + FS::MediaType app_media_type; + if (slot_data->media_type != static_cast(UINT32_MAX)) { + app_media_type = slot_data->media_type; + } else { + // Applet was not started from StartApplication, so we need to guess. + app_media_type = Service::AM::GetTitleMediaType(slot_data->title_id); + } auto app_update_media_type = Service::AM::GetTitleMediaType(Service::AM::GetTitleUpdateId(slot_data->title_id)); if (app_media_type == check_target || (check_update && app_update_media_type == check_target)) { @@ -1283,8 +1300,16 @@ Result AppletManager::PrepareToDoApplicationJump(u64 title_id, FS::MediaType med // Save the title data to send it to the Home Menu when DoApplicationJump is called. auto application_slot_data = GetAppletSlot(AppletSlot::Application); app_jump_parameters.current_title_id = application_slot_data->title_id; - app_jump_parameters.current_media_type = - Service::AM::GetTitleMediaType(application_slot_data->title_id); + + FS::MediaType curr_media_type; + if (application_slot_data->media_type != static_cast(UINT32_MAX)) { + curr_media_type = application_slot_data->media_type; + } else { + // Applet was not started from StartApplication, so we need to guess. + curr_media_type = Service::AM::GetTitleMediaType(application_slot_data->title_id); + } + + app_jump_parameters.current_media_type = curr_media_type; if (flags == ApplicationJumpFlags::UseCurrentParameters) { app_jump_parameters.next_title_id = app_jump_parameters.current_title_id; app_jump_parameters.next_media_type = app_jump_parameters.current_media_type; @@ -1369,7 +1394,15 @@ Result AppletManager::PrepareToStartApplication(u64 title_id, FS::MediaType medi title_id = ConvertTitleID(system, title_id); - std::string path = AM::GetTitleContentPath(media_type, title_id); + std::string path; + if (media_type == FS::MediaType::GameCard) { + path = system.GetCartridge(); + } else { + path = AM::GetTitleContentPath(media_type, title_id); + } + + next_app_mediatype = media_type; + auto loader = Loader::GetLoader(path); if (!loader) { diff --git a/src/core/hle/service/apt/applet_manager.h b/src/core/hle/service/apt/applet_manager.h index d4d99895b..3924ec510 100644 --- a/src/core/hle/service/apt/applet_manager.h +++ b/src/core/hle/service/apt/applet_manager.h @@ -456,6 +456,7 @@ private: AppletId applet_id; AppletSlot slot; u64 title_id; + FS::MediaType media_type; bool registered; bool loaded; AppletAttributes attributes; @@ -476,6 +477,7 @@ private: ar & applet_id; ar & slot; ar & title_id; + ar & media_type; ar & registered; ar & loaded; ar & attributes.raw; @@ -514,6 +516,8 @@ private: bool last_home_button_state = false; bool last_power_button_state = false; + FS::MediaType next_app_mediatype = static_cast(UINT32_MAX); + Core::System& system; AppletSlotData* GetAppletSlot(AppletSlot slot) { @@ -569,6 +573,7 @@ private: ar & capture_info; ar & applet_slots; ar & library_applet_closing_command; + ar & next_app_mediatype; if (Archive::is_loading::value) { LoadInputDevices(); diff --git a/src/core/hle/service/apt/apt.cpp b/src/core/hle/service/apt/apt.cpp index 8e34bb0b7..14035af1a 100644 --- a/src/core/hle/service/apt/apt.cpp +++ b/src/core/hle/service/apt/apt.cpp @@ -75,6 +75,20 @@ void Module::NSInterface::SetWirelessRebootInfo(Kernel::HLERequestContext& ctx) LOG_WARNING(Service_APT, "called size={}", size); } +void Module::NSInterface::CardUpdateInitialize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + rp.Pop(); // Shared mem size + rp.Pop(); // Always 0 + rp.Pop(); // Shared mem handle + + LOG_WARNING(Service_APT, "(stubbed) called"); + const Result update_not_needed(11, ErrorModule::CUP, ErrorSummary::NothingHappened, + ErrorLevel::Status); + + auto rb = rp.MakeBuilder(1, 0); + rb.Push(update_not_needed); +} + void Module::NSInterface::ShutdownAsync(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); diff --git a/src/core/hle/service/apt/apt.h b/src/core/hle/service/apt/apt.h index 9676a94f0..376b113f0 100644 --- a/src/core/hle/service/apt/apt.h +++ b/src/core/hle/service/apt/apt.h @@ -72,6 +72,8 @@ public: */ void SetWirelessRebootInfo(Kernel::HLERequestContext& ctx); + void CardUpdateInitialize(Kernel::HLERequestContext& ctx); + /** * NS::ShutdownAsync service function. * Inputs: diff --git a/src/core/hle/service/apt/ns.cpp b/src/core/hle/service/apt/ns.cpp index 785b83198..6b36d97d1 100644 --- a/src/core/hle/service/apt/ns.cpp +++ b/src/core/hle/service/apt/ns.cpp @@ -11,11 +11,21 @@ namespace Service::NS { std::shared_ptr LaunchTitle(Core::System& system, FS::MediaType media_type, u64 title_id) { - std::string path = AM::GetTitleContentPath(media_type, title_id); - auto loader = Loader::GetLoader(path); + std::string path; - if (!loader) { - LOG_WARNING(Service_NS, "Could not find .app for title 0x{:016x}", title_id); + if (media_type == FS::MediaType::GameCard) { + path = system.GetCartridge(); + } else { + path = AM::GetTitleContentPath(media_type, title_id); + } + + auto loader = Loader::GetLoader(path); + u64 program_id; + + if (!loader || loader->ReadProgramId(program_id) != Loader::ResultStatus::Success || + program_id != title_id) { + LOG_WARNING(Service_NS, "Could not load title=0x{:016x} media_type={}", title_id, + static_cast(media_type)); return nullptr; } @@ -60,7 +70,13 @@ std::shared_ptr LaunchTitle(Core::System& system, FS::MediaType void RebootToTitle(Core::System& system, FS::MediaType media_type, u64 title_id, std::optional mem_mode) { - auto new_path = AM::GetTitleContentPath(media_type, title_id); + std::string new_path; + if (media_type == FS::MediaType::GameCard) { + new_path = system.GetCartridge(); + } else { + new_path = AM::GetTitleContentPath(media_type, title_id); + } + if (new_path.empty() || !FileUtil::Exists(new_path)) { // TODO: This can happen if the requested title is not installed. Need a way to find // non-installed titles in the game list. diff --git a/src/core/hle/service/apt/ns_s.cpp b/src/core/hle/service/apt/ns_s.cpp index dfb178d7b..ab267a0d4 100644 --- a/src/core/hle/service/apt/ns_s.cpp +++ b/src/core/hle/service/apt/ns_s.cpp @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -17,7 +17,7 @@ NS_S::NS_S(std::shared_ptr apt) {0x0004, nullptr, "TerminateProcess"}, {0x0005, nullptr, "LaunchApplicationFIRM"}, {0x0006, &NS_S::SetWirelessRebootInfo, "SetWirelessRebootInfo"}, - {0x0007, nullptr, "CardUpdateInitialize"}, + {0x0007, &NS_S::CardUpdateInitialize, "CardUpdateInitialize"}, {0x0008, nullptr, "CardUpdateShutdown"}, {0x000D, nullptr, "SetTWLBannerHMAC"}, {0x000E, &NS_S::ShutdownAsync, "ShutdownAsync"}, diff --git a/src/core/hle/service/fs/fs_user.cpp b/src/core/hle/service/fs/fs_user.cpp index 99390cd40..c9641a518 100644 --- a/src/core/hle/service/fs/fs_user.cpp +++ b/src/core/hle/service/fs/fs_user.cpp @@ -913,6 +913,14 @@ void FS_USER::GetFreeBytes(Kernel::HLERequestContext& ctx) { } } +void FS_USER::GetCardType(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(0); // CTR Card + LOG_DEBUG(Service_FS, "(STUBBED) called"); +} + void FS_USER::GetSdmcArchiveResource(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -999,8 +1007,8 @@ void FS_USER::CardSlotIsInserted(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); - rb.Push(false); - LOG_WARNING(Service_FS, "(STUBBED) called"); + rb.Push(!system.GetCartridge().empty()); + LOG_DEBUG(Service_FS, "called"); } void FS_USER::DeleteSystemSaveData(Kernel::HLERequestContext& ctx) { @@ -1681,14 +1689,20 @@ void FS_USER::GetSaveDataSecureValue(Kernel::HLERequestContext& ctx) { } void FS_USER::RegisterProgramInfo(u32 process_id, u64 program_id, const std::string& filepath) { - const MediaType media_type = GetMediaTypeFromPath(filepath); + MediaType media_type; + if (filepath == system.GetCartridge()) { + media_type = MediaType::GameCard; + } else { + media_type = GetMediaTypeFromPath(filepath); + } + program_info_map.insert_or_assign(process_id, ProgramInfo{program_id, media_type}); if (media_type == MediaType::GameCard) { current_gamecard_path = filepath; } } -std::string FS_USER::GetCurrentGamecardPath() const { +std::string FS_USER::GetRegisteredGamecardPath() const { return current_gamecard_path; } @@ -1779,7 +1793,7 @@ FS_USER::FS_USER(Core::System& system) {0x0810, &FS_USER::CreateLegacySystemSaveData, "CreateLegacySystemSaveData"}, {0x0811, nullptr, "DeleteSystemSaveData"}, {0x0812, &FS_USER::GetFreeBytes, "GetFreeBytes"}, - {0x0813, nullptr, "GetCardType"}, + {0x0813, &FS_USER::GetCardType, "GetCardType"}, {0x0814, &FS_USER::GetSdmcArchiveResource, "GetSdmcArchiveResource"}, {0x0815, &FS_USER::GetNandArchiveResource, "GetNandArchiveResource"}, {0x0816, nullptr, "GetSdmcFatfsError"}, diff --git a/src/core/hle/service/fs/fs_user.h b/src/core/hle/service/fs/fs_user.h index 11fbef4db..bdce1b7eb 100644 --- a/src/core/hle/service/fs/fs_user.h +++ b/src/core/hle/service/fs/fs_user.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -54,7 +54,7 @@ public: // loader and pm, which we HLEed, we can just directly use it here void RegisterProgramInfo(u32 process_id, u64 program_id, const std::string& filepath); - std::string GetCurrentGamecardPath() const; + std::string GetRegisteredGamecardPath() const; struct ProductInfo { std::array product_code; @@ -361,6 +361,8 @@ private: */ void GetFreeBytes(Kernel::HLERequestContext& ctx); + void GetCardType(Kernel::HLERequestContext& ctx); + /** * FS_User::GetSdmcArchiveResource service function. * Inputs: diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 392b6025d..9e4c76122 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -302,6 +302,10 @@ public: return false; } + virtual std::string GetFilePath() { + return file ? file->Filename() : ""; + } + protected: Core::System& system; std::unique_ptr file; diff --git a/src/core/loader/ncch.h b/src/core/loader/ncch.h index c5b1b45d5..f1bf69855 100644 --- a/src/core/loader/ncch.h +++ b/src/core/loader/ncch.h @@ -80,6 +80,10 @@ public: bool IsFileCompressed() override; + std::string GetFilePath() override { + return filepath; + } + private: /** * Loads .code section into memory for booting