Add title update functionality

This commit is contained in:
Daniil Vinogradov 2025-02-15 20:32:28 +01:00
parent 27312d4f31
commit 7277e1fa9b
5 changed files with 71 additions and 6 deletions

View File

@ -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

View File

@ -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)

View File

@ -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: "")

View File

@ -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 {

View File

@ -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))
{