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 000000000..cb669ee29
Binary files /dev/null and b/dist/qt_themes/default/icons/128x128/cartridge.png differ
diff --git a/dist/qt_themes/default/icons/index.theme b/dist/qt_themes/default/icons/index.theme
index 1edbe6408..a7cfb3306 100644
--- a/dist/qt_themes/default/icons/index.theme
+++ b/dist/qt_themes/default/icons/index.theme
@@ -1,13 +1,16 @@
[Icon Theme]
Name=default
Comment=default theme
-Directories=16x16,48x48,256x256
+Directories=16x16,48x48,128x128,256x256
[16x16]
Size=16
[48x48]
Size=48
-
+
+[128x128]
+Size=128
+
[256x256]
Size=256
\ No newline at end of file
diff --git a/dist/qt_themes/default/icons_light/128x128/cartridge.png b/dist/qt_themes/default/icons_light/128x128/cartridge.png
new file mode 100644
index 000000000..cb669ee29
Binary files /dev/null and b/dist/qt_themes/default/icons_light/128x128/cartridge.png differ
diff --git a/dist/qt_themes/default/icons_light/index.theme b/dist/qt_themes/default/icons_light/index.theme
index 1edbe6408..a7cfb3306 100644
--- a/dist/qt_themes/default/icons_light/index.theme
+++ b/dist/qt_themes/default/icons_light/index.theme
@@ -1,13 +1,16 @@
[Icon Theme]
Name=default
Comment=default theme
-Directories=16x16,48x48,256x256
+Directories=16x16,48x48,128x128,256x256
[16x16]
Size=16
[48x48]
Size=48
-
+
+[128x128]
+Size=128
+
[256x256]
Size=256
\ No newline at end of file
diff --git a/dist/qt_themes/default/theme_default.qrc b/dist/qt_themes/default/theme_default.qrc
index c8339f86d..90ae777aa 100644
--- a/dist/qt_themes/default/theme_default.qrc
+++ b/dist/qt_themes/default/theme_default.qrc
@@ -13,6 +13,7 @@
icons/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 000000000..cb669ee29
Binary files /dev/null and b/src/android/app/src/main/res/drawable/cartridge.png differ
diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml
index 85392a813..5b1d558ea 100644
--- a/src/android/app/src/main/res/layout/card_game.xml
+++ b/src/android/app/src/main/res/layout/card_game.xml
@@ -28,6 +28,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
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