From b4698eabfeab18101e39789fde7f2589719f8ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 28 Apr 2026 22:26:53 +0200 Subject: [PATCH] Support launching directly with the deep link, instead of just launching while already running. --- UI/NativeApp.cpp | 8 +++-- ios/AppDelegate.mm | 26 -------------- ios/SceneDelegate.mm | 85 ++++++++++++++++++++++++++++++++++---------- ios/main.mm | 4 +++ 4 files changed, 76 insertions(+), 47 deletions(-) diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index f3a84d44d3..41f7a7c24a 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -624,12 +624,14 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch boot_filename = Path(str); skipLogo = true; } + // This is needed on iOS, to fixup the path to match the current app directory, if it's stored in it. + TryUpdateSavedPath(&boot_filename); if (okToLoad && okToCheck) { std::unique_ptr fileLoader(ConstructFileLoader(boot_filename)); if (!fileLoader->Exists()) { fprintf(stderr, "File not found: %s\n", boot_filename.c_str()); -#if defined(_WIN32) || defined(__ANDROID__) - // Ignore and proceed. + +#if defined(_WIN32) || defined(__ANDROID__) || PPSSPP_PLATFORM(IOS) boot_filename.clear(); #else // Bail. @@ -639,7 +641,7 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch } } else { fprintf(stderr, "Syntax error: Can only boot one file.\nNote: Many command line args need a =, like --appendconfig=FILENAME.ini.\n"); -#if defined(_WIN32) || defined(__ANDROID__) +#if defined(_WIN32) || defined(__ANDROID__) || PPSSPP_PLATFORM(IOS) // Ignore and proceed. #else // Bail. diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index b2bfc9c434..db3fe9550c 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -121,32 +121,6 @@ __attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate clas System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, gamePath.ToString()); } -- (BOOL)application:(UIApplication *)app - openURL:(NSURL *)url - options:(NSDictionary *)options { - - // 1. Check the scheme - if (![[url scheme] isEqualToString:@"ppsspp"]) { - NSLog(@"PPSSPPUIApplication.openURL called with invalid URL: %@", url); - return NO; - } - - NSLog(@"PPSSPPUIApplication.openURL called with valid URL: %@", url); - // 2. Parse the URL (using NSURLComponents is safer than manual string parsing) - NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; - NSArray *queryItems = [components queryItems]; - NSLog(@"PPSSPPUIApplication.openURL query items: %@", queryItems); - for (NSURLQueryItem *item in queryItems) { - if ([item.name isEqualToString:@"path"]) { - NSString *receivedPath = item.value; - [self processFilePath:receivedPath]; - } else { - NSLog(@"PPSSPPUIApplication.openURL: Unrecognized query item: %@=%@", item.name, item.value); - } - } - return YES; -} - - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { switch (g_Config.iScreenRotation) { case ROTATION_LOCKED_HORIZONTAL: diff --git a/ios/SceneDelegate.mm b/ios/SceneDelegate.mm index d7aaf5e0f4..c1e52a8948 100644 --- a/ios/SceneDelegate.mm +++ b/ios/SceneDelegate.mm @@ -13,8 +13,31 @@ #import "Core/Config.h" #import "Common/Log.h" #import "IAPManager.h" +#include "Core/Util/PathUtil.h" #import +#include + +static std::string gStartupArgStorage; + +static NSString *ExtractDeepLinkPath(NSURL *url) { + if (![[url scheme] isEqualToString:@"ppsspp"]) { + return nil; + } + + NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + for (NSURLQueryItem *item in components.queryItems) { + if ([item.name isEqualToString:@"path"] && item.value.length > 0) { + return item.value; + } + } + + return nil; +} + +@interface SceneDelegate () +@property (nonatomic, strong) NSString *pendingLaunchPath; +@end @implementation SceneDelegate @@ -22,11 +45,37 @@ NSLog(@"✅ SceneDelegate class was loaded!"); } +- (void)capturePendingPathFromURLContexts:(NSSet *)URLContexts { + for (UIOpenURLContext *context in URLContexts) { + NSString *path = ExtractDeepLinkPath(context.URL); + if (path.length > 0) { + self.pendingLaunchPath = path; + NSLog(@"SceneDelegate: captured cold-start deep link path: %@", self.pendingLaunchPath); + break; + } + } +} + +- (void)handleURLContextsWhileRunning:(NSSet *)URLContexts { + AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; + for (UIOpenURLContext *context in URLContexts) { + NSLog(@"SceneDelegate: openURLContexts called with URL: %@", context.URL); + NSString *path = ExtractDeepLinkPath(context.URL); + if (path.length > 0) { + [appDelegate processFilePath:path]; + } else { + NSLog(@"SceneDelegate: ignoring deep link URL (invalid scheme or missing path): %@", context.URL); + } + } +} + - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { if (![scene isKindOfClass:[UIWindowScene class]]) { return; } + [self capturePendingPathFromURLContexts:connectionOptions.URLContexts]; + UIWindowScene *windowScene = (UIWindowScene *)scene; if (self.window) { NSLog(@"✅ Window already exists, not creating again!"); @@ -43,12 +92,26 @@ int argc = 1; char *argv[5]{}; + NSString *startupPath = nil; NSURL *nsUrl = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]; - if (nsUrl != nullptr && nsUrl.isFileURL) { + if (self.pendingLaunchPath.length > 0) { + startupPath = self.pendingLaunchPath; + self.pendingLaunchPath = nil; + } + + if (startupPath == nil && nsUrl != nullptr && nsUrl.isFileURL) { NSString *nsString = nsUrl.path; - const char *string = nsString.UTF8String; - argv[argc++] = (char*)string; + startupPath = nsString; + } + + if (startupPath.length > 0) { + // Keep cold-start argv behavior aligned with processFilePath(). + std::string startupPathUtf8(startupPath.UTF8String); + Path gamePath(startupPathUtf8); + gStartupArgStorage = gamePath.ToString(); + argv[argc++] = (char *)gStartupArgStorage.c_str(); + NSLog(@"SceneDelegate: startup path passed to argv: %s", gStartupArgStorage.c_str()); } NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; @@ -96,21 +159,7 @@ } - (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts { - for (UIOpenURLContext *context in URLContexts) { - NSLog(@"SceneDelegate: openURLContexts called with URL: %@", context.URL); - NSURL *url = context.URL; - if (![[url scheme] isEqualToString:@"ppsspp"]) { - NSLog(@"SceneDelegate: ignoring URL with unknown scheme: %@", url); - continue; - } - NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; - for (NSURLQueryItem *item in components.queryItems) { - if ([item.name isEqualToString:@"path"]) { - AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; - [appDelegate processFilePath:item.value]; - } - } - } + [self handleURLContextsWhileRunning:URLContexts]; } - (void)sceneWillResignActive:(UIScene *)scene { diff --git a/ios/main.mm b/ios/main.mm index 37e32d9653..3b9f574490 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -699,6 +699,10 @@ int main(int argc, char *argv[]) { } PROFILE_INIT(); +#if PPSSPP_PLATFORM(IOS_APP_STORE) && defined(_DEBUG) + g_logManager.SetOutputsEnabled(LogOutput::Stdio); +#endif + @autoreleasepool { return UIApplicationMain(argc, argv, NSStringFromClass([PPSSPPUIApplication class]), NSStringFromClass([AppDelegate class])); }