forked from MeloNX/MeloNX
Add game update manager
This commit is contained in:
parent
f95281899c
commit
0e80bd3d51
@ -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;
|
||||
|
Binary file not shown.
@ -12,12 +12,12 @@
|
||||
<key>Ryujinx.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>com.Stossy11.MeloNX.RyujinxAg.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
191
src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift
Normal file
191
src/MeloNX/MeloNX/App/Views/Updates/GameUpdateManagerSheet.swift
Normal file
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
@ -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<TitleUpdateMetadata>(_updateJsonPath, _titleSerializerContext.TitleUpdateMetadata);
|
||||
|
||||
_titleUpdateWindowData.Paths ??= new List<string>();
|
||||
if (!_titleUpdateWindowData.Paths.Contains(updatePath)) {
|
||||
_titleUpdateWindowData.Paths.Add(updatePath);
|
||||
}
|
||||
|
||||
_titleUpdateWindowData.Selected = updatePath;
|
||||
} else {
|
||||
_titleUpdateWindowData = new TitleUpdateMetadata {
|
||||
Selected = updatePath,
|
||||
Paths = new List<string>(),
|
||||
Paths = new List<string> { updatePath },
|
||||
};
|
||||
}
|
||||
|
||||
JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, _titleSerializerContext.TitleUpdateMetadata);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
|
||||
public static unsafe int GetFPS()
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user