Add Intent to Launch Game and change how DRM works

This commit is contained in:
Stossy11 2025-02-10 17:46:24 +11:00
parent 4f3e49a90c
commit 007cb026a4
9 changed files with 164 additions and 59 deletions

View File

@ -58,6 +58,7 @@ class Ryujinx {
@Published var metalLayer: CAMetalLayer? = nil @Published var metalLayer: CAMetalLayer? = nil
@Published var firmwareversion = "0" @Published var firmwareversion = "0"
@Published var emulationUIView = UIView() @Published var emulationUIView = UIView()
@Published var games: [Game] = []
var shouldMetal: Bool { var shouldMetal: Bool {
metalLayer == nil metalLayer == nil
@ -65,7 +66,9 @@ class Ryujinx {
static let shared = Ryujinx() static let shared = Ryujinx()
private init() {} private init() {
self.games = loadGames()
}
public struct Configuration : Codable, Equatable { public struct Configuration : Codable, Equatable {
var gamepath: String var gamepath: String
@ -202,6 +205,53 @@ class Ryujinx {
} }
} }
} }
func loadGames() -> [Game] {
let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return [] }
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
if (!fileManager.fileExists(atPath: romsDirectory.path)) {
do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Failed to create roms directory: \(error)")
}
}
var games: [Game] = []
do {
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
for fileURLCandidate in files {
if fileURLCandidate.pathExtension == "zip" {
continue
}
do {
let handle = try FileHandle(forReadingFrom: fileURLCandidate)
let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
games.append(game)
} catch {
print(error)
}
}
return games
} catch {
print("Error loading games from roms folder: \(error)")
return games
}
}
private func buildCommandLineArgs(from config: Configuration) -> [String] { private func buildCommandLineArgs(from config: Configuration) -> [String] {
var args: [String] = [] var args: [String] = []

View File

@ -0,0 +1,54 @@
//
// LaunchGameIntentDef.swift
// MeloNX
//
// Created by Stossy11 on 10/02/2025.
//
import Foundation
import SwiftUI
import Intents
import AppIntents
@available(iOS 16.0, *)
struct LaunchGameIntentDef: AppIntent {
static let title: LocalizedStringResource = "Launch Game"
static var description = IntentDescription("Launches the Selected Game.")
@Parameter(title: "Game", optionsProvider: GameOptionsProvider())
var gameName: String
static var parameterSummary: some ParameterSummary {
Summary("Launch \(\.$gameName)")
}
static var openAppWhenRun: Bool = true
@MainActor
func perform() async throws -> some IntentResult {
let ryujinx = Ryujinx.shared.games
let urlString = "melonx://game?\(ryujinx.contains(where: { $0.titleName.localizedCaseInsensitiveContains(gameName) }) ? "name" : "id")=\(gameName)"
print(urlString)
if let url = URL(string: urlString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
return .result()
}
}
@available(iOS 16.0, *)
struct GameOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [String] {
Ryujinx.shared.games = Ryujinx.shared.loadGames()
let dynamicGames = Ryujinx.shared.games
return dynamicGames.map { $0.titleName }
}
}

View File

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
public struct Game: Identifiable, Equatable { public struct Game: Identifiable, Equatable, Hashable {
public var id = UUID() public var id = UUID()
var containerFolder: URL var containerFolder: URL

View File

@ -118,6 +118,16 @@ struct ContentView: View {
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts. initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
} }
.onOpenURL() { url in
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
}
}
}
} }
} }

View File

@ -52,6 +52,14 @@ struct GameInfoSheet: View {
.bold() .bold()
Text("**Version:** \(game.version)") Text("**Version:** \(game.version)")
Text("**Title ID:** \(game.titleId)")
.contextMenu {
Button {
UIPasteboard.general.string = game.titleId
} label: {
Text("Copy Title ID")
}
}
Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes") Text("**Game Size:** \(fetchFileSize(for: game.fileURL) ?? 0) bytes")
Text("**File Type:** .\(getFileType(game.fileURL))") Text("**File Type:** .\(getFileType(game.fileURL))")
Text("**Game URL:** \(trimGameURL(game.fileURL))") Text("**Game URL:** \(trimGameURL(game.fileURL))")

View File

@ -16,7 +16,6 @@ extension UTType {
struct GameLibraryView: View { struct GameLibraryView: View {
@Binding var startemu: Game? @Binding var startemu: Game?
// @State var importDLCs = false // @State var importDLCs = false
@State private var games: [Game] = []
@State private var searchText = "" @State private var searchText = ""
@State private var isSearching = false @State private var isSearching = false
@AppStorage("recentGames") private var recentGamesData: Data = Data() @AppStorage("recentGames") private var recentGamesData: Data = Data()
@ -29,13 +28,18 @@ struct GameLibraryView: View {
@State var isSelectingGameFile = false @State var isSelectingGameFile = false
@State var isViewingGameInfo: Bool = false @State var isViewingGameInfo: Bool = false
@State var gameInfo: Game? @State var gameInfo: Game?
var games: Binding<[Game]> {
Binding(
get: { Ryujinx.shared.games },
set: { Ryujinx.shared.games = $0 }
)
}
var filteredGames: [Game] { var filteredGames: [Game] {
if searchText.isEmpty { if searchText.isEmpty {
return games return Ryujinx.shared.games
} }
return games.filter { return Ryujinx.shared.games.filter {
$0.titleName.localizedCaseInsensitiveContains(searchText) || $0.titleName.localizedCaseInsensitiveContains(searchText) ||
$0.developer.localizedCaseInsensitiveContains(searchText) $0.developer.localizedCaseInsensitiveContains(searchText)
} }
@ -52,7 +56,7 @@ struct GameLibraryView: View {
.padding(.top, 12) .padding(.top, 12)
} }
if games.isEmpty { if Ryujinx.shared.games.isEmpty {
VStack(spacing: 16) { VStack(spacing: 16) {
Image(systemName: "gamecontroller.fill") Image(systemName: "gamecontroller.fill")
.font(.system(size: 64)) .font(.system(size: 64))
@ -95,7 +99,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, gameInfo: $gameInfo)
.onTapGesture { .onTapGesture {
addToRecentGames(game) addToRecentGames(game)
} }
@ -105,7 +109,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, gameInfo: $gameInfo)
.onTapGesture { .onTapGesture {
addToRecentGames(game) addToRecentGames(game)
} }
@ -115,7 +119,6 @@ struct GameLibraryView: View {
} }
} }
.onAppear { .onAppear {
loadGames()
loadRecentGames() loadRecentGames()
let firmware = Ryujinx.shared.fetchFirmwareVersion() let firmware = Ryujinx.shared.fetchFirmwareVersion()
@ -262,7 +265,7 @@ struct GameLibraryView: View {
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent) let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
try fileManager.copyItem(at: url, to: destinationURL) try fileManager.copyItem(at: url, to: destinationURL)
loadGames() Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch { } catch {
print("Error copying game file: \(error)") print("Error copying game file: \(error)")
} }
@ -317,56 +320,15 @@ struct GameLibraryView: View {
recentGames = [] recentGames = []
} }
} }
// MARK: - loads games from roms
func loadGames() {
let fileManager = FileManager.default
guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
// Check if "roms" folder exists; if not, create it
if (!fileManager.fileExists(atPath: romsDirectory.path)) {
do {
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Failed to create roms directory: \(error)")
}
}
games = []
// Load games only from "roms" folder
do {
let files = try fileManager.contentsOfDirectory(at: romsDirectory, includingPropertiesForKeys: nil)
files.forEach { fileURLCandidate in
do {
let handle = try FileHandle(forReadingFrom: fileURLCandidate)
let fileExtension = (fileURLCandidate.pathExtension as NSString).utf8String
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
let gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: fileURLCandidate)
games.append(game)
} catch {
print(error)
}
}
} catch {
print("Error loading games from roms folder: \(error)")
}
}
// MARK: - Delete Game Function // MARK: - Delete Game Function
func deleteGame(game: Game) { func deleteGame(game: Game) {
let fileManager = FileManager.default let fileManager = FileManager.default
do { do {
try fileManager.removeItem(at: game.fileURL) try fileManager.removeItem(at: game.fileURL)
games.removeAll { $0.id == game.id } Ryujinx.shared.games.removeAll { $0.id == game.id }
loadGames() Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch { } catch {
print("Error deleting game: \(error)") print("Error deleting game: \(error)")
} }

View File

@ -2,8 +2,29 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.stossy11.MeloNX</string>
<key>CFBundleURLSchemes</key>
<array>
<string>melonx</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>melonx</string>
</array>
<key>MeloID</key> <key>MeloID</key>
<string>83f67a0a96bd8628a150d7853e360db5bae64e7769524fae399c4b8e7e6aff17</string> <string>83f67a0a96bd8628a150d7853e360db5bae64e7769524fae399c4b8e7e6aff17</string>
<key>NSUserActivityTypes</key>
<array>
<string>LaunchGameIntent</string>
</array>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <true/>
<key>UTExportedTypeDeclarations</key> <key>UTExportedTypeDeclarations</key>

View File

@ -15,7 +15,7 @@ import CryptoKit
struct MeloNXApp: App { struct MeloNXApp: App {
@State var showed = false @State var showed = false
@Environment(\.scenePhase) var scenePhase
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
@ -61,7 +61,7 @@ struct MeloNXApp: App {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
InitializeRyujinx() { bool in InitializeRyujinx() { bool in
if !bool { if !bool, (scenePhase != .background || scenePhase == .inactive) {
withAnimation { withAnimation {
showed = false showed = false
} }
@ -119,7 +119,7 @@ struct MeloNXApp: App {
// Present the alert // Present the alert
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil) mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
} else { } else {
exit(0)
} }
} }
@ -133,7 +133,7 @@ func showDMCAAlert() {
mainWindow.rootViewController!.present(alertController, animated: true, completion: nil) mainWindow.rootViewController!.present(alertController, animated: true, completion: nil)
} else { } else {
exit(0) // uhoh
} }
} }
} }