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(), + }; + 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)) {