1
0
forked from MeloNX/MeloNX

Compare commits

...

6 Commits

7 changed files with 453 additions and 176 deletions

View File

@ -0,0 +1,237 @@
//
// NativeController.swift
// MeloNX
//
// Created by XITRIX on 15/02/2025.
//
import CoreHaptics
import GameController
class NativeController: Hashable {
private var instanceID: SDL_JoystickID = -1
private var controller: OpaquePointer?
private var nativeController: GCController
private let controllerHaptics: CHHapticEngine?
public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" }
init(_ controller: GCController) {
nativeController = controller
controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default)
try? controllerHaptics?.start()
setupHandheldController()
}
deinit {
cleanup()
}
private func setupHandheldController() {
if SDL_WasInit(Uint32(SDL_INIT_GAMECONTROLLER)) == 0 {
SDL_InitSubSystem(Uint32(SDL_INIT_GAMECONTROLLER))
}
var joystickDesc = SDL_VirtualJoystickDesc(
version: UInt16(SDL_VIRTUAL_JOYSTICK_DESC_VERSION),
type: Uint16(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue),
naxes: 6,
nbuttons: 15,
nhats: 1,
vendor_id: 0,
product_id: 0,
padding: 0,
button_mask: 0,
axis_mask: 0,
name: (controllername as NSString).utf8String,
userdata: Unmanaged.passUnretained(self).toOpaque(),
Update: { userdata in
// Update joystick state here
},
SetPlayerIndex: { userdata, playerIndex in
print("Player index set to \(playerIndex)")
},
Rumble: { userdata, lowFreq, highFreq in
print("Rumble with \(lowFreq), \(highFreq)")
guard let userdata else { return 0 }
let _self = Unmanaged<NativeController>.fromOpaque(userdata).takeUnretainedValue()
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics)
return 0
},
RumbleTriggers: { userdata, leftRumble, rightRumble in
print("Trigger rumble with \(leftRumble), \(rightRumble)")
return 0
},
SetLED: { userdata, red, green, blue in
print("Set LED to RGB(\(red), \(green), \(blue))")
return 0
},
SendEffect: { userdata, data, size in
print("Effect sent with size \(size)")
return 0
}
)
instanceID = SDL_JoystickAttachVirtualEx(&joystickDesc)// SDL_JoystickAttachVirtual(SDL_JoystickType(SDL_JOYSTICK_TYPE_GAMECONTROLLER.rawValue), 6, 15, 1)
if instanceID < 0 {
print("Failed to create virtual joystick: \(String(cString: SDL_GetError()))")
return
}
// Open a game controller for the virtual joystick
let joystick = SDL_JoystickFromInstanceID(instanceID)
controller = SDL_GameControllerOpen(Int32(instanceID))
if controller == nil {
print("Failed to create virtual controller: \(String(cString: SDL_GetError()))")
return
}
if #available(iOS 16, *) {
guard let gamepad = nativeController.extendedGamepad
else { return }
setupButtonChangeListener(gamepad.buttonA, for: .B)
setupButtonChangeListener(gamepad.buttonB, for: .A)
setupButtonChangeListener(gamepad.buttonX, for: .Y)
setupButtonChangeListener(gamepad.buttonY, for: .X)
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
setupButtonChangeListener(gamepad.dpad.left, for: .dPadLeft)
setupButtonChangeListener(gamepad.dpad.right, for: .dPadRight)
setupButtonChangeListener(gamepad.leftShoulder, for: .leftShoulder)
setupButtonChangeListener(gamepad.rightShoulder, for: .rightShoulder)
gamepad.leftThumbstickButton.map { setupButtonChangeListener($0, for: .leftStick) }
gamepad.rightThumbstickButton.map { setupButtonChangeListener($0, for: .rightStick) }
setupButtonChangeListener(gamepad.buttonMenu, for: .start)
gamepad.buttonOptions.map { setupButtonChangeListener($0, for: .back) }
setupStickChangeListener(gamepad.leftThumbstick, for: .left)
setupStickChangeListener(gamepad.rightThumbstick, for: .right)
setupTriggerChangeListener(gamepad.leftTrigger, for: .left)
setupTriggerChangeListener(gamepad.rightTrigger, for: .right)
}
}
func setupButtonChangeListener(_ button: GCControllerButtonInput, for key: VirtualControllerButton) {
button.valueChangedHandler = { [unowned self] _, _, pressed in
setButtonState(pressed ? 1 : 0, for: key)
}
}
func setupStickChangeListener(_ button: GCControllerDirectionPad, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, xValue, yValue in
let scaledX = Sint16(xValue * 32767.0)
let scaledY = -Sint16(yValue * 32767.0)
switch key {
case .left:
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue))
case .right:
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue))
}
}
}
func setupTriggerChangeListener(_ button: GCControllerButtonInput, for key: ThumbstickType) {
button.valueChangedHandler = { [unowned self] _, value, pressed in
// print("Value: \(value), Is pressed: \(pressed)")
let axis: SDL_GameControllerAxis = (key == .left) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let scaledValue = Sint16(value * 32767.0)
updateAxisValue(value: scaledValue, forAxis: axis)
}
}
static func rumble(lowFreq: Float, highFreq: Float) {
do {
// Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], relativeTime: 0, duration: 0.2)
], parameters: [])
// High-frequency haptic pattern
let highFreqPattern = try CHHapticPattern(events: [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
], relativeTime: 0.2, duration: 0.2)
], parameters: [])
// Create and start the haptic engine
let engine = try CHHapticEngine()
try engine.start()
// Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
try lowFreqPlayer.start(atTime: 0)
// Create and play the high-frequency player after a short delay
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
try highFreqPlayer.start(atTime: 0.2)
} catch {
print("Error creating haptic patterns: \(error)")
}
}
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return }
let joystick = SDL_JoystickFromInstanceID(instanceID)
SDL_JoystickSetVirtualAxis(joystick, axis.rawValue, value)
}
func thumbstickMoved(_ stick: ThumbstickType, x: Double, y: Double) {
let scaleFactor = 32767.0 / 160
let scaledX = Int16(min(32767.0, max(-32768.0, x * scaleFactor)))
let scaledY = Int16(min(32767.0, max(-32768.0, y * scaleFactor)))
if stick == .right {
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_RIGHTY.rawValue))
} else { // ThumbstickType.left
updateAxisValue(value: scaledX, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTX.rawValue))
updateAxisValue(value: scaledY, forAxis: SDL_GameControllerAxis(SDL_CONTROLLER_AXIS_LEFTY.rawValue))
}
}
func setButtonState(_ state: Uint8, for button: VirtualControllerButton) {
guard controller != nil else { return }
// print("Button: \(button.rawValue) {state: \(state)}")
if (button == .leftTrigger || button == .rightTrigger) && (state == 1 || state == 0) {
let axis: SDL_GameControllerAxis = (button == .leftTrigger) ? SDL_CONTROLLER_AXIS_TRIGGERLEFT : SDL_CONTROLLER_AXIS_TRIGGERRIGHT
let value: Int = (state == 1) ? 32767 : 0
updateAxisValue(value: Sint16(value), forAxis: axis)
} else {
let joystick = SDL_JoystickFromInstanceID(instanceID)
SDL_JoystickSetVirtualButton(joystick, Int32(button.rawValue), state)
}
}
func cleanup() {
if let controller {
SDL_JoystickDetachVirtual(instanceID)
SDL_GameControllerClose(controller)
self.controller = nil
}
}
func hash(into hasher: inout Hasher) {
hasher.combine(nativeController)
}
static func == (lhs: NativeController, rhs: NativeController) -> Bool {
lhs.nativeController == rhs.nativeController
}
}

View File

@ -78,7 +78,7 @@ class VirtualController {
} }
} }
static func rumble(lowFreq: Float, highFreq: Float) { static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
do { do {
// Low-frequency haptic pattern // Low-frequency haptic pattern
let lowFreqPattern = try CHHapticPattern(events: [ let lowFreqPattern = try CHHapticPattern(events: [
@ -96,9 +96,23 @@ class VirtualController {
], relativeTime: 0.2, duration: 0.2) ], relativeTime: 0.2, duration: 0.2)
], parameters: []) ], parameters: [])
// Mutable engine
var engine = engine
// If no engine passed, use device engine
if engine == nil {
// Create and start the haptic engine // Create and start the haptic engine
let engine = try CHHapticEngine() if hapticEngine == nil {
try engine.start() hapticEngine = try CHHapticEngine()
try hapticEngine?.start()
}
engine = hapticEngine
}
guard let engine else {
return print("Error creating haptic patterns: hapticEngine is nil")
}
// Create and play the low-frequency player // Create and play the low-frequency player
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern) let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
@ -113,6 +127,8 @@ class VirtualController {
} }
} }
private static var hapticEngine: CHHapticEngine?
func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) { func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) {
guard controller != nil else { return } guard controller != nil else { return }

View File

@ -26,6 +26,7 @@ struct ContentView: View {
@State private var controllersList: [Controller] = [] @State private var controllersList: [Controller] = []
@State private var currentControllers: [Controller] = [] @State private var currentControllers: [Controller] = []
@State var onscreencontroller: Controller = Controller(id: "", name: "") @State var onscreencontroller: Controller = Controller(id: "", name: "")
@State var nativeControllers: [GCController: NativeController] = [:]
@State private var isVirtualControllerActive: Bool = false @State private var isVirtualControllerActive: Bool = false
@AppStorage("isVirtualController") var isVCA: Bool = true @AppStorage("isVirtualController") var isVCA: Bool = true
@ -152,6 +153,7 @@ struct ContentView: View {
queue: .main) { notification in queue: .main) { notification in
if let controller = notification.object as? GCController { if let controller = notification.object as? GCController {
print("Controller connected: \(controller.productCategory)") print("Controller connected: \(controller.productCategory)")
nativeControllers[controller] = .init(controller)
refreshControllersList() refreshControllersList()
} }
} }
@ -163,6 +165,8 @@ struct ContentView: View {
queue: .main) { notification in queue: .main) { notification in
if let controller = notification.object as? GCController { if let controller = notification.object as? GCController {
print("Controller disconnected: \(controller.productCategory)") print("Controller disconnected: \(controller.productCategory)")
nativeControllers[controller]?.cleanup()
nativeControllers[controller] = nil
refreshControllersList() refreshControllersList()
} }
} }
@ -306,7 +310,8 @@ struct ContentView: View {
self.onscreencontroller = onscreen self.onscreencontroller = onscreen
} }
controllersList.removeAll(where: { $0.id == "0"}) controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
currentControllers = [] currentControllers = []
@ -397,3 +402,10 @@ func loadSettings() -> Ryujinx.Configuration? {
} }
} }
extension Array {
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
for index in self.indices {
try body(&self[index])
}
}
}

View File

@ -14,7 +14,10 @@ struct GameInfoSheet: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
VStack { List {
Section {}
header: {
VStack(alignment: .center) {
if let icon = game.icon { if let icon = game.icon {
Image(uiImage: icon) Image(uiImage: icon)
.resizable() .resizable()
@ -36,23 +39,26 @@ struct GameInfoSheet: View {
.frame(width: 150, height: 150) .frame(width: 150, height: 150)
.padding() .padding()
} }
VStack(alignment: .center) {
VStack(alignment: .leading) {
VStack(alignment: .leading) {
Text("**\(game.titleName)** | \(game.titleId.capitalized)") Text("**\(game.titleName)** | \(game.titleId.capitalized)")
Text(game.developer) Text(game.developer)
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
.padding(.vertical, 3) .padding(.vertical, 3)
}
.frame(maxWidth: .infinity)
}
VStack(alignment: .leading, spacing: 5) { Section {
Text("Information") HStack {
.font(.title2) Text("**Version**")
.bold() Spacer()
Text(game.version)
Text("**Version:** \(game.version)") .foregroundStyle(Color.secondary)
Text("**Title ID:** \(game.titleId)") }
HStack {
Text("**Title ID**")
.contextMenu { .contextMenu {
Button { Button {
UIPasteboard.general.string = game.titleId UIPasteboard.general.string = game.titleId
@ -60,15 +66,32 @@ struct GameInfoSheet: View {
Text("Copy Title ID") Text("Copy Title ID")
} }
} }
Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes")
Text("**File Type:** .\(getFileType(game.fileURL))")
Text("**Game URL:** \(trimGameURL(game.fileURL))")
}
}
Spacer() Spacer()
Text(game.titleId)
.foregroundStyle(Color.secondary)
}
HStack {
Text("**Game Size**")
Spacer()
Text("\(fetchFileSize(for: game.fileURL) ?? 0) bytes")
.foregroundStyle(Color.secondary)
}
HStack {
Text("**File Type**")
Spacer()
Text(getFileType(game.fileURL))
.foregroundStyle(Color.secondary)
}
VStack(alignment: .leading, spacing: 4) {
Text("**Game URL**")
Text(trimGameURL(game.fileURL))
.foregroundStyle(Color.secondary)
}
} header: {
Text("Information")
}
.headerProminence(.increased)
} }
.padding(.horizontal, 5)
.navigationTitle(game.titleName) .navigationTitle(game.titleName)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
@ -103,10 +126,6 @@ struct GameInfoSheet: View {
} }
func getFileType(_ url: URL) -> String { func getFileType(_ url: URL) -> String {
let path = url.path url.pathExtension
if let range = path.range(of: ".") {
return String(path[range.upperBound...])
}
return "Unknown"
} }
} }

View File

@ -48,15 +48,7 @@ struct GameLibraryView: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
ScrollView { List {
LazyVStack(alignment: .leading, spacing: 20) {
if !isSearching {
Text("Games")
.font(.system(size: 34, weight: .bold))
.padding(.horizontal)
.padding(.top, 12)
}
if Ryujinx.shared.games.isEmpty { if Ryujinx.shared.games.isEmpty {
VStack(spacing: 16) { VStack(spacing: 16) {
Image(systemName: "gamecontroller.fill") Image(systemName: "gamecontroller.fill")
@ -108,7 +100,6 @@ struct GameLibraryView: View {
} }
} }
} else { } else {
LazyVStack(spacing: 2) {
ForEach(filteredGames) { game in ForEach(filteredGames) { game in
GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo) GameListRow(game: game, startemu: $startemu, games: games, isViewingGameInfo: $isViewingGameInfo, isSelectingGameUpdate: $isSelectingGameUpdate, gameInfo: $gameInfo)
.onTapGesture { .onTapGesture {
@ -118,7 +109,8 @@ struct GameLibraryView: View {
} }
} }
} }
} .navigationTitle("Games")
.navigationBarTitleDisplayMode(.large)
.onAppear { .onAppear {
loadRecentGames() loadRecentGames()
@ -143,9 +135,8 @@ struct GameLibraryView: View {
print(error) print(error)
} }
} }
}
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarTrailing) {
Button { Button {
isSelectingGameFile.toggle() isSelectingGameFile.toggle()
} label: { } label: {
@ -215,7 +206,6 @@ struct GameLibraryView: View {
} }
} }
} }
.background(Color(.systemGroupedBackground))
.searchable(text: $searchText) .searchable(text: $searchText)
.onChange(of: searchText) { _ in .onChange(of: searchText) { _ in
isSearching = !searchText.isEmpty isSearching = !searchText.isEmpty
@ -296,7 +286,6 @@ struct GameLibraryView: View {
} }
} }
private func addToRecentGames(_ game: Game) { private func addToRecentGames(_ game: Game) {
recentGames.removeAll { $0.id == game.id } recentGames.removeAll { $0.id == game.id }
@ -329,7 +318,6 @@ struct GameLibraryView: View {
} }
} }
// MARK: - Delete Game Function // MARK: - Delete Game Function
func deleteGame(game: Game) { func deleteGame(game: Game) {
let fileManager = FileManager.default let fileManager = FileManager.default
@ -474,9 +462,6 @@ struct GameListRow: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.opacity(0.8) .opacity(0.8)
} }
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
.contextMenu { .contextMenu {
Section { Section {
Button { Button {
@ -533,4 +518,3 @@ struct GameListRow: View {
} }
} }
} }

View File

@ -18,16 +18,22 @@ struct UpdateManagerSheet: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
VStack { List(paths, id: \..self, selection: $selectedItem) { item in
List(paths, id: \..self) { item in
Button(action: { Button(action: {
selectItem(item.lastPathComponent) selectItem(item.lastPathComponent)
}) { }) {
HStack { HStack {
Text(item.lastPathComponent) Text(item.lastPathComponent)
if selectedItem == "\(game!.titleId)/\(item.lastPathComponent)" { .foregroundStyle(Color(uiColor: .label))
Spacer() Spacer()
Image(systemName: "checkmark") if selectedItem == "\(game!.titleId)/\(item.lastPathComponent)" {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
} }
} }
} }
@ -39,15 +45,15 @@ struct UpdateManagerSheet: View {
} }
} }
} }
} .onAppear {
.onAppear() {
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json")) print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
loadJSON(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") .navigationTitle("\(game!.titleName) Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
Button("+") { Button("Add", systemImage: "plus") {
isSelectingGameUpdate = true isSelectingGameUpdate = true
} }
} }
@ -80,7 +86,8 @@ struct UpdateManagerSheet: View {
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent) let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL) try? fileManager.copyItem(at: url, to: destinationURL)
Ryujinx.shared.setTitleUpdate(titleId: gameInfo.titleId, updatePath: "\(gameInfo.titleId)/" + url.lastPathComponent) items.append(gameInfo.titleId + "/" + url.lastPathComponent)
selectItem(url.lastPathComponent)
Ryujinx.shared.games = Ryujinx.shared.loadGames() Ryujinx.shared.games = Ryujinx.shared.loadGames()
loadJSON(jsonURL!) loadJSON(jsonURL!)
} catch { } catch {
@ -108,6 +115,7 @@ struct UpdateManagerSheet: View {
} }
saveJSON(selectedItem: selectedItem ?? "") saveJSON(selectedItem: selectedItem ?? "")
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} }
func saveJSON(selectedItem: String) { func saveJSON(selectedItem: String) {
@ -122,7 +130,6 @@ struct UpdateManagerSheet: View {
} }
func loadJSON(_ json: URL) { func loadJSON(_ json: URL) {
self.jsonURL = json self.jsonURL = json
print("Failed to read JSO") print("Failed to read JSO")
@ -132,16 +139,17 @@ struct UpdateManagerSheet: View {
do { do {
let data = try Data(contentsOf: jsonURL) let data = try Data(contentsOf: jsonURL)
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let list = jsonDict["paths"] as? [String] { let list = jsonDict["paths"] as? [String]
{
var urls: [URL] = [] var urls: [URL] = []
for path in list { for path in list {
urls.append(URL.documentsDirectory.appendingPathComponent("updates").appendingPathComponent(path)) urls.append(URL.documentsDirectory.appendingPathComponent("updates").appendingPathComponent(path))
} }
self.items = list items = list
self.paths = urls paths = urls
self.selectedItem = jsonDict["selected"] as? String selectedItem = jsonDict["selected"] as? String
} }
} catch { } catch {
print("Failed to read JSON: \(error)") print("Failed to read JSON: \(error)")
@ -155,8 +163,8 @@ struct UpdateManagerSheet: View {
do { do {
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted) let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
try newData.write(to: jsonURL) try newData.write(to: jsonURL)
self.items = [] items = []
self.selectedItem = "" selectedItem = ""
} catch { } catch {
print("Failed to create default JSON: \(error)") print("Failed to create default JSON: \(error)")
} }
@ -183,9 +191,9 @@ struct UpdateManagerSheet: View {
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted) let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL) try newData.write(to: jsonURL)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch { } catch {
print("Failed to update JSON: \(error)") print("Failed to update JSON: \(error)")
} }
} }
} }

View File

@ -751,7 +751,8 @@ namespace Ryujinx.Headless.SDL2
if (File.Exists(titleUpdateMetadataPath)) if (File.Exists(titleUpdateMetadataPath))
{ {
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected; string updatePathRelative = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
updatePath = Path.Combine(AppDataManager.BaseDirPath, "updates", updatePathRelative);
if (File.Exists(updatePath)) if (File.Exists(updatePath))
{ {