diff --git a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
index 066587266..3d63a96ad 100644
--- a/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
+++ b/src/MeloNX/MeloNX.xcodeproj/project.pbxproj
@@ -662,6 +662,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
@@ -725,6 +727,10 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
@@ -774,6 +780,8 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = fast;
GENERATE_INFOPLIST_FILE = YES;
@@ -837,6 +845,10 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
+ "$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
diff --git a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate
index ae985c98a..848e330e7 100644
Binary files a/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate and b/src/MeloNX/MeloNX.xcodeproj/project.xcworkspace/xcuserdata/stossy11.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist
index 8ff6cf524..62375ba69 100644
--- a/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/src/MeloNX/MeloNX.xcodeproj/xcuserdata/stossy11.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -12,12 +12,12 @@
Ryujinx.xcscheme_^#shared#^_
orderHint
- 2
+ 1
com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_
orderHint
- 1
+ 2
SuppressBuildableAutocreation
diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
index 93236fe01..da759a4b4 100644
--- a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
+++ b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
@@ -220,7 +220,7 @@ struct GameLibraryView: View {
.onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty
}
- .fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder]) { result in
+ .fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder, .nsp, .xci]) { result in
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else {
@@ -278,35 +278,8 @@ struct GameLibraryView: View {
print("File import failed: \(err.localizedDescription)")
}
}
- .fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.nsp]) { result in
- switch result {
- case .success(let url):
- guard let gameInfo, url.startAccessingSecurityScopedResource() else {
- print("Failed to access security-scoped resource")
- return
- }
- defer { url.stopAccessingSecurityScopedResource() }
-
- do {
- let fileManager = FileManager.default
- let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
- let romUpdatedDirectory = documentsDirectory.appendingPathComponent("updates")
-
- if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
- try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
- }
-
- let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
- try? fileManager.copyItem(at: url, to: destinationURL)
-
- Ryujinx.shared.setTitleUpdate(titleId: gameInfo.titleId, updatePath: destinationURL.path)
- Ryujinx.shared.games = Ryujinx.shared.loadGames()
- } catch {
- print("Error copying game file: \(error)")
- }
- case .failure(let err):
- print("File import failed: \(err.localizedDescription)")
- }
+ .sheet(isPresented: $isSelectingGameUpdate) {
+ UpdateManagerSheet(game: $gameInfo)
}
.sheet(isPresented: Binding(
get: { isViewingGameInfo && gameInfo != nil },
@@ -523,7 +496,7 @@ struct GameListRow: View {
gameInfo = game
isSelectingGameUpdate.toggle()
} label: {
- Label("Add Game Update", systemImage: "chevron.up.circle")
+ Label("Game Update Manager", systemImage: "chevron.up.circle")
}
}
diff --git a/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift b/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift
new file mode 100644
index 000000000..da5209654
--- /dev/null
+++ b/src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift
@@ -0,0 +1,191 @@
+//
+// GameUpdateManagerSheet.swift
+// MeloNX
+//
+// Created by Stossy11 on 16/02/2025.
+//
+
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct UpdateManagerSheet: View {
+ @State private var items: [String] = []
+ @State private var paths: [URL] = []
+ @State private var selectedItem: String? = nil
+ @Binding var game: Game?
+ @State private var isSelectingGameUpdate = false
+ @State private var jsonURL: URL? = nil
+
+ var body: some View {
+ NavigationView {
+ VStack {
+ List(paths, id: \..self) { item in
+ Button(action: {
+ selectItem(item.lastPathComponent)
+ }) {
+ HStack {
+ Text(item.lastPathComponent)
+ if selectedItem == "\(game!.titleId)/\(item.lastPathComponent)" {
+ Spacer()
+ Image(systemName: "checkmark")
+ }
+ }
+ }
+ .contextMenu {
+ Button {
+ removeUpdate(item)
+ } label: {
+ Text("Remove Update")
+ }
+ }
+ }
+ }
+ .onAppear() {
+ print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
+
+ loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
+ }
+ .navigationTitle("\(game!.titleName) Updates")
+ .toolbar {
+ Button("+") {
+ isSelectingGameUpdate = true
+ }
+ }
+ }
+ .fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in
+ switch result {
+ case .success(let url):
+ guard url.startAccessingSecurityScopedResource() else {
+ print("Failed to access security-scoped resource")
+ return
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+
+ let gameInfo = game!
+
+ do {
+ let fileManager = FileManager.default
+ let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let updatedDirectory = documentsDirectory.appendingPathComponent("updates")
+ let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId)
+
+ if !fileManager.fileExists(atPath: updatedDirectory.path) {
+ try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
+ try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
+ try? fileManager.copyItem(at: url, to: destinationURL)
+
+ Ryujinx.shared.setTitleUpdate(titleId: gameInfo.titleId, updatePath: "\(gameInfo.titleId)/" + url.lastPathComponent)
+ Ryujinx.shared.games = Ryujinx.shared.loadGames()
+ loadJSON(jsonURL!)
+ } catch {
+ print("Error copying game file: \(error)")
+ }
+ case .failure(let err):
+ print("File import failed: \(err.localizedDescription)")
+ }
+ }
+ }
+
+ func removeUpdate(_ game: URL) {
+ let gameString = "\(self.game!.titleId)/\(game.lastPathComponent)"
+ paths.removeAll { $0 == game }
+ items.removeAll { $0 == gameString }
+
+ if selectedItem == gameString {
+ selectedItem = nil
+ }
+
+ do {
+ try FileManager.default.removeItem(at: game)
+ } catch {
+ print(error)
+ }
+
+ saveJSON(selectedItem: selectedItem ?? "")
+ }
+
+ func saveJSON(selectedItem: String) {
+ guard let jsonURL = jsonURL else { return }
+ do {
+ let jsonDict = ["paths": items, "selected": selectedItem] as [String: Any]
+ let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
+ try newData.write(to: jsonURL)
+ } catch {
+ print("Failed to update JSON: \(error)")
+ }
+ }
+
+ func loadJSON(_ json: URL) {
+
+ self.jsonURL = json
+ print("Failed to read JSO")
+
+ guard let jsonURL = jsonURL else { return }
+ print("Failed to read JSOK")
+
+ do {
+ let data = try Data(contentsOf: jsonURL)
+ if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
+ let list = jsonDict["paths"] as? [String] {
+ var urls: [URL] = []
+
+ for path in list {
+ urls.append(URL.documentsDirectory.appendingPathComponent("updates").appendingPathComponent(path))
+ }
+
+ self.items = list
+ self.paths = urls
+ self.selectedItem = jsonDict["selected"] as? String
+ }
+ } catch {
+ print("Failed to read JSON: \(error)")
+ createDefaultJSON()
+ }
+ }
+
+ func createDefaultJSON() {
+ guard let jsonURL = jsonURL else { return }
+ let defaultData: [String: Any] = ["selected": "", "paths": []]
+ do {
+ let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
+ try newData.write(to: jsonURL)
+ self.items = []
+ self.selectedItem = ""
+ } catch {
+ print("Failed to create default JSON: \(error)")
+ }
+ }
+
+ func selectItem(_ item: String) {
+ let newSelection = "\(game!.titleId)/\(item)"
+
+ guard let jsonURL = jsonURL else { return }
+
+ do {
+ let data = try Data(contentsOf: jsonURL)
+ var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
+
+ if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {
+ jsonDict["selected"] = ""
+ selectedItem = ""
+ } else {
+ jsonDict["selected"] = newSelection
+ selectedItem = newSelection
+ }
+
+ jsonDict["paths"] = items
+
+ let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
+ try newData.write(to: jsonURL)
+ } catch {
+ print("Failed to update JSON: \(error)")
+ }
+ }
+
+}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
index 87141ab85..0c23c843c 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
@@ -93,6 +93,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
if (File.Exists(titleUpdateMetadataPath))
{
string updatePath = PlatformRelative(JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected);
+ Logger.Info?.Print(LogClass.Application, $"Game Update Path: ${updatePath}.");
if (File.Exists(updatePath))
{
PartitionFileSystem updatePartitionFileSystem = new();
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index 54c1aee82..a5f799fac 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -148,15 +148,29 @@ namespace Ryujinx.Headless.SDL2
var updatePath = Marshal.PtrToStringAnsi(updatePathPtr);
string _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
- TitleUpdateMetadata _titleUpdateWindowData = new TitleUpdateMetadata
- {
+ TitleUpdateMetadata _titleUpdateWindowData;
+
+ if (File.Exists(_updateJsonPath)) {
+ _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, _titleSerializerContext.TitleUpdateMetadata);
+
+ _titleUpdateWindowData.Paths ??= new List();
+ if (!_titleUpdateWindowData.Paths.Contains(updatePath)) {
+ _titleUpdateWindowData.Paths.Add(updatePath);
+ }
+
+ _titleUpdateWindowData.Selected = updatePath;
+ } else {
+ _titleUpdateWindowData = new TitleUpdateMetadata {
Selected = updatePath,
- Paths = new List(),
+ Paths = new List { updatePath },
};
+ }
+
JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _titleSerializerContext.TitleUpdateMetadata);
}
+
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS()
{