Merge pull request 'Added title update functionality' (#7) from XITRIX/MeloNX:Title-update into XC-ios-ht

Reviewed-on: MeloNX/MeloNX#7
This commit is contained in:
stossy11 2025-02-16 00:04:42 +00:00
commit 802a8d7bae
5 changed files with 79 additions and 8 deletions

View File

@ -43,6 +43,8 @@ int main_ryujinx_sdl(int argc, char **argv);
int get_current_fps(); int get_current_fps();
void set_title_update(const char* titleIdPtr, const char* updatePathPtr);
void initialize(); void initialize();
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -227,8 +227,9 @@ class Ryujinx {
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr) 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) games.append(game)
} catch { } catch {
print(error) print(error)
@ -372,7 +373,18 @@ class Ryujinx {
self.firmwareversion = version 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? { private func generateGamepadId(joystickIndex: Int32) -> String? {
let guid = SDL_JoystickGetDeviceGUID(joystickIndex) let guid = SDL_JoystickGetDeviceGUID(joystickIndex)

View File

@ -22,10 +22,10 @@ public struct Game: Identifiable, Equatable, Hashable {
var icon: UIImage? var icon: UIImage?
static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game { static func convertGameInfoToGame(gameInfo: GameInfo, url: URL) -> Game? {
var gameInfo = gameInfo var gameInfo = gameInfo
var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "") var gameTemp = Game(containerFolder: url.deletingLastPathComponent(), fileType: .item, fileURL: url, titleName: "", titleId: "", developer: "", version: "")
gameTemp.titleName = withUnsafePointer(to: &gameInfo.TitleName) { gameTemp.titleName = withUnsafePointer(to: &gameInfo.TitleName) {
$0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) { $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
String(cString: $0) String(cString: $0)

View File

@ -27,6 +27,7 @@ struct GameLibraryView: View {
@State var startgame = false @State var startgame = false
@State var isSelectingGameFile = false @State var isSelectingGameFile = false
@State var isViewingGameInfo: Bool = false @State var isViewingGameInfo: Bool = false
@State var isSelectingGameUpdate: Bool = false
@State var gameInfo: Game? @State var gameInfo: Game?
var games: Binding<[Game]> { var games: Binding<[Game]> {
Binding( Binding(
@ -99,7 +100,7 @@ struct GameLibraryView: View {
LazyVStack(spacing: 2) { LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in 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 { .onTapGesture {
addToRecentGames(game) addToRecentGames(game)
} }
@ -109,7 +110,7 @@ struct GameLibraryView: View {
} else { } else {
LazyVStack(spacing: 2) { LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in 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 { .onTapGesture {
addToRecentGames(game) addToRecentGames(game)
} }
@ -277,6 +278,36 @@ struct GameLibraryView: View {
print("File import failed: \(err.localizedDescription)") 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( .sheet(isPresented: Binding(
get: { isViewingGameInfo && gameInfo != nil }, get: { isViewingGameInfo && gameInfo != nil },
set: { newValue in set: { newValue in
@ -421,6 +452,7 @@ struct GameListRow: View {
@Binding var startemu: Game? @Binding var startemu: Game?
@Binding var games: [Game] // Add this binding @Binding var games: [Game] // Add this binding
@Binding var isViewingGameInfo: Bool @Binding var isViewingGameInfo: Bool
@Binding var isSelectingGameUpdate: Bool
@Binding var gameInfo: Game? @Binding var gameInfo: Game?
@State var gametoDelete: Game? @State var gametoDelete: Game?
@State var showGameDeleteConfirmation: Bool = false @State var showGameDeleteConfirmation: Bool = false
@ -486,6 +518,13 @@ struct GameListRow: View {
} label: { } label: {
Label("Game Info", systemImage: "info.circle") Label("Game Info", systemImage: "info.circle")
} }
Button {
gameInfo = game
isSelectingGameUpdate.toggle()
} label: {
Label("Add Game Update", systemImage: "chevron.up.circle")
}
} }
Section { Section {

View File

@ -115,6 +115,7 @@ namespace Ryujinx.Headless.SDL2
private static bool _enableMouse; private static bool _enableMouse;
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
[UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")] [UnmanagedCallersOnly(EntryPoint = "main_ryujinx_sdl")]
public static unsafe int MainExternal(int argCount, IntPtr* pArgs) public static unsafe int MainExternal(int argCount, IntPtr* pArgs)
@ -141,6 +142,20 @@ namespace Ryujinx.Headless.SDL2
return 0; 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")] [UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS() public static unsafe int GetFPS()
@ -321,6 +336,9 @@ namespace Ryujinx.Headless.SDL2
var stream = OpenFile(descriptor); var stream = OpenFile(descriptor);
var gameInfo = GetGameInfo(stream, extension); var gameInfo = GetGameInfo(stream, extension);
if (gameInfo == null) {
return new GameInfoNative(0, "", "", "", "", new byte[0]);
}
return new GameInfoNative( return new GameInfoNative(
(ulong)gameInfo.FileSize, (ulong)gameInfo.FileSize,
@ -719,7 +737,7 @@ namespace Ryujinx.Headless.SDL2
if (File.Exists(titleUpdateMetadataPath)) if (File.Exists(titleUpdateMetadataPath))
{ {
// updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath).Selected; updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
if (File.Exists(updatePath)) if (File.Exists(updatePath))
{ {