From 27312d4f31b4bc9d3632d57ac1c7ce033f897505 Mon Sep 17 00:00:00 2001
From: Daniil Vinogradov <xitrix@bk.ru>
Date: Sat, 15 Feb 2025 18:46:02 +0100
Subject: [PATCH 1/2] Null pointer fix

---
 src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift | 5 +++--
 src/MeloNX/MeloNX/App/Models/Game.swift          | 6 ++++--
 src/Ryujinx.Headless.SDL2/Program.cs             | 3 +++
 3 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
index 17bdd4bfe..4d4ac237a 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
@@ -227,8 +227,9 @@ class Ryujinx {
                     
                     let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
                     
-                    let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
-                    
+                    guard let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
+                    else { continue }
+
                     games.append(game)
                 } catch {
                     print(error)
diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift
index d9ed10422..b3a66782c 100644
--- a/src/MeloNX/MeloNX/App/Models/Game.swift
+++ b/src/MeloNX/MeloNX/App/Models/Game.swift
@@ -22,10 +22,12 @@ public struct Game: Identifiable, Equatable, Hashable {
     var icon: UIImage?
     
     
-    static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game {
+    static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game? {
+        guard gameInfo.FileSize != 0 else { return nil }
+
         var gameInfo = gameInfo
         var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
-        
+
         gameTemp.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
             $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
                 String(cString: $0)
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index fca399c48..345814b01 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -321,6 +321,9 @@ namespace Ryujinx.Headless.SDL2
             var stream = OpenFile(descriptor);
 
             var gameInfo = GetGameInfo(stream, extension);
+            if (gameInfo == null) {
+                return new GameInfoNative(0, "", "", "", "", new byte[0]);
+            }
 
             return new GameInfoNative(
                 (ulong)gameInfo.FileSize, 

From 7277e1fa9ba190dcdbae4b30e1cd1318336b30d5 Mon Sep 17 00:00:00 2001
From: Daniil Vinogradov <xitrix@bk.ru>
Date: Sat, 15 Feb 2025 20:32:28 +0100
Subject: [PATCH 2/2] Add title update functionality

---
 .../MeloNX/App/Core/Headers/Ryujinx-Header.h  |  2 +
 .../MeloNX/App/Core/Ryujinx/Ryujinx.swift     | 13 +++++-
 src/MeloNX/MeloNX/App/Models/Game.swift       |  2 -
 .../App/Views/GamesList/GameListView.swift    | 43 ++++++++++++++++++-
 src/Ryujinx.Headless.SDL2/Program.cs          | 17 +++++++-
 5 files changed, 71 insertions(+), 6 deletions(-)

diff --git a/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
index d7f16215b..b7e8191b4 100644
--- a/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
+++ b/src/MeloNX/MeloNX/App/Core/Headers/Ryujinx-Header.h
@@ -43,6 +43,8 @@ int main_ryujinx_sdl(int argc, char **argv);
 
 int get_current_fps();
 
+void set_title_update(const char* titleIdPtr, const char* updatePathPtr);
+
 void initialize();
 
 #ifdef __cplusplus
diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
index 4d4ac237a..76b038b5a 100644
--- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
+++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Ryujinx.swift
@@ -373,7 +373,18 @@ class Ryujinx {
             self.firmwareversion = version
         }
     }
-    
+
+    func setTitleUpdate(titleId: String, updatePath: String) {
+        guard let titleIdPtr = titleId.cString(using: .utf8),
+              let updatePathPtr = updatePath.cString(using: .utf8)
+        else {
+            print("Invalid firmware path")
+            return
+        }
+
+        set_title_update(titleIdPtr, updatePathPtr)
+    }
+
     private func generateGamepadId(joystickIndex: Int32) -> String? {
         let guid = SDL_JoystickGetDeviceGUID(joystickIndex)
 
diff --git a/src/MeloNX/MeloNX/App/Models/Game.swift b/src/MeloNX/MeloNX/App/Models/Game.swift
index b3a66782c..1b2e66d34 100644
--- a/src/MeloNX/MeloNX/App/Models/Game.swift
+++ b/src/MeloNX/MeloNX/App/Models/Game.swift
@@ -23,8 +23,6 @@ public struct Game: Identifiable, Equatable, Hashable {
     
     
     static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game? {
-        guard gameInfo.FileSize != 0 else { return nil }
-
         var gameInfo = gameInfo
         var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
 
diff --git a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
index c5547181f..93236fe01 100644
--- a/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
+++ b/src/MeloNX/MeloNX/App/Views/GamesList/GameListView.swift
@@ -27,6 +27,7 @@ struct GameLibraryView: View {
     @State var startgame = false
     @State var isSelectingGameFile = false
     @State var isViewingGameInfo: Bool = false
+    @State var isSelectingGameUpdate: Bool = false
     @State var gameInfo: Game?
     var games: Binding<[Game]> {
         Binding(
@@ -99,7 +100,7 @@ struct GameLibraryView: View {
                                 
                                 LazyVStack(spacing: 2) {
                                     ForEach(filteredGames) { game in
-                                        GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
+                                        GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
                                             .onTapGesture {
                                                 addToRecentGames(game)
                                             }
@@ -109,7 +110,7 @@ struct GameLibraryView: View {
                         } else {
                             LazyVStack(spacing: 2) {
                                 ForEach(filteredGames) { game in
-                                    GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, gameInfo: $gameInfo)
+                                    GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
                                         .onTapGesture {
                                             addToRecentGames(game)
                                         }
@@ -277,6 +278,36 @@ 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: Binding(
             get: { isViewingGameInfo && gameInfo != nil },
             set: { newValue in
@@ -421,6 +452,7 @@ struct GameListRow: View {
     @Binding var startemu: Game?
     @Binding var games: [Game] // Add this binding
     @Binding var isViewingGameInfo: Bool
+    @Binding var isSelectingGameUpdate: Bool
     @Binding var gameInfo: Game?
     @State var gametoDelete: Game?
     @State var showGameDeleteConfirmation: Bool = false
@@ -486,6 +518,13 @@ struct GameListRow: View {
                     } label: {
                         Label("Game Info", systemImage: "info.circle")
                     }
+
+                    Button {
+                        gameInfo = game
+                        isSelectingGameUpdate.toggle()
+                    } label: {
+                        Label("Add Game Update", systemImage: "chevron.up.circle")
+                    }
                 }
                 
                 Section {
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index 345814b01..2809334c2 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -115,6 +115,7 @@ namespace Ryujinx.Headless.SDL2
         private static bool _enableMouse;
 
         private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+        private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
         [UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")]
         public static unsafe int MainExternal(int argCount, IntPtr* pArgs)
@@ -141,6 +142,20 @@ namespace Ryujinx.Headless.SDL2
             return 0;
         }
 
+        [UnmanagedCallersOnly(EntryPoint = "set_title_update")]
+        public static unsafe void SetTitleUpdate(IntPtr titleIdPtr, IntPtr updatePathPtr) {
+            var titleId = Marshal.PtrToStringAnsi(titleIdPtr);
+            var updatePath = Marshal.PtrToStringAnsi(updatePathPtr);
+            string _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "updates.json");
+
+            TitleUpdateMetadata _titleUpdateWindowData = new TitleUpdateMetadata
+                {
+                    Selected = updatePath,
+                    Paths = new List<string>(),
+                };
+            JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _titleSerializerContext.TitleUpdateMetadata);
+        }
+
 
         [UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
         public static unsafe int GetFPS() 
@@ -722,7 +737,7 @@ namespace Ryujinx.Headless.SDL2
 
                     if (File.Exists(titleUpdateMetadataPath))
                     {
-                        // updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected;
+                        updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
 
                         if (File.Exists(updatePath))
                         {