mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
feat: Added library export functionaltiy via urlscheme
This commit is contained in:
@@ -15,6 +15,11 @@
|
|||||||
- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv;
|
- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv;
|
||||||
- (void)processFilePath:(NSString *)path;
|
- (void)processFilePath:(NSString *)path;
|
||||||
|
|
||||||
|
// Exports the game library to a caller app via a URL scheme callback.
|
||||||
|
// The caller requests it with "ppsspp://gameInfo?scheme=<callerScheme>", and
|
||||||
|
// PPSSPP responds by opening "<callerScheme>://ppsspp?games=<base64url-json>".
|
||||||
|
- (void)exportLibraryToScheme:(NSString *)callerScheme;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
void copyDeepLinkForPath(std::string_view filePath);
|
void copyDeepLinkForPath(std::string_view filePath);
|
||||||
|
|||||||
@@ -6,12 +6,20 @@
|
|||||||
#import "Core/System.h"
|
#import "Core/System.h"
|
||||||
#import "Core/Config.h"
|
#import "Core/Config.h"
|
||||||
#import "Core/Util/PathUtil.h"
|
#import "Core/Util/PathUtil.h"
|
||||||
|
#import "Core/Util/RecentFiles.h"
|
||||||
#import "Common/Log.h"
|
#import "Common/Log.h"
|
||||||
|
#import "Common/File/Path.h"
|
||||||
|
#import "Common/File/DirListing.h"
|
||||||
|
#import "Common/File/FileUtil.h"
|
||||||
#import "IAPManager.h"
|
#import "IAPManager.h"
|
||||||
|
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
#import <objc/runtime.h>
|
#import <objc/runtime.h>
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// TODO: Unfortunate hack to force link SceneDelegate class.
|
// TODO: Unfortunate hack to force link SceneDelegate class.
|
||||||
// This is necessary because otherwise classes from static libraries aren't loaded.
|
// This is necessary because otherwise classes from static libraries aren't loaded.
|
||||||
// We should move all the iOS code into the main binary directly to avoid this problem.
|
// We should move all the iOS code into the main binary directly to avoid this problem.
|
||||||
@@ -121,6 +129,105 @@ __attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate clas
|
|||||||
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, gamePath.ToString());
|
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, gamePath.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gathers the game library as a flat list of file paths. This mirrors what the
|
||||||
|
// user sees in the "Games" tab. Duplicates are
|
||||||
|
// removed while preserving order.
|
||||||
|
static std::vector<std::string> GatherGameLibrary() {
|
||||||
|
std::vector<std::string> paths;
|
||||||
|
std::set<std::string> seen;
|
||||||
|
|
||||||
|
auto addPath = [&](const std::string &p) {
|
||||||
|
if (p.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (seen.insert(p).second) {
|
||||||
|
paths.push_back(p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Path &gamesDir = g_Config.currentDirectory;
|
||||||
|
if (!gamesDir.empty()) {
|
||||||
|
std::vector<File::FileInfo> fileInfo;
|
||||||
|
if (File::GetFilesInDir(gamesDir, &fileInfo, "iso:cso:chd:pbp:elf:prx:ppdmp:")) {
|
||||||
|
for (const File::FileInfo &info : fileInfo) {
|
||||||
|
if (info.isDirectory) {
|
||||||
|
// Detect installed/extracted PSP game folders.
|
||||||
|
if (File::Exists(info.fullName / "EBOOT.PBP") ||
|
||||||
|
File::Exists(info.fullName / "PSP_GAME/SYSDIR")) {
|
||||||
|
addPath(info.fullName.ToString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addPath(info.fullName.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also include recently played games.
|
||||||
|
for (const std::string &recent : g_recentFiles.GetRecentFiles()) {
|
||||||
|
addPath(recent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)exportLibraryToScheme:(NSString *)callerScheme {
|
||||||
|
if (callerScheme.length == 0) {
|
||||||
|
NSLog(@"exportLibraryToScheme: empty caller scheme, ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a list of games. Each game's "titleId" is set to the file path so
|
||||||
|
// the receiving app can build a "ppsspp://open?path=<path>" deep link from it.
|
||||||
|
std::vector<std::string> library = GatherGameLibrary();
|
||||||
|
|
||||||
|
NSMutableArray<NSDictionary *> *games = [NSMutableArray arrayWithCapacity:library.size()];
|
||||||
|
for (const std::string &file : library) {
|
||||||
|
NSString *path = [[NSString alloc] initWithBytes:file.data()
|
||||||
|
length:file.length()
|
||||||
|
encoding:NSUTF8StringEncoding];
|
||||||
|
if (path.length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Create a human-readable title from the filename (without extension).
|
||||||
|
NSString *titleName = [[path lastPathComponent] stringByDeletingPathExtension];
|
||||||
|
if (titleName.length == 0) {
|
||||||
|
titleName = [path lastPathComponent];
|
||||||
|
}
|
||||||
|
[games addObject:@{
|
||||||
|
@"titleName": titleName ?: @"",
|
||||||
|
@"titleId": path,
|
||||||
|
@"developer": @"",
|
||||||
|
@"version": @"",
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *error = nil;
|
||||||
|
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:games options:0 error:&error];
|
||||||
|
if (!jsonData || error) {
|
||||||
|
NSLog(@"exportLibraryToScheme: failed to serialize library: %@", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64url-encode (URL-safe alphabet, no padding) to keep it URL-safe.
|
||||||
|
NSString *encoded = [jsonData base64EncodedStringWithOptions:0];
|
||||||
|
encoded = [encoded stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
|
||||||
|
encoded = [encoded stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
|
||||||
|
encoded = [encoded stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
||||||
|
|
||||||
|
NSString *urlString = [NSString stringWithFormat:@"%@://ppsspp?games=%@", callerScheme, encoded];
|
||||||
|
NSURL *returnURL = [NSURL URLWithString:urlString];
|
||||||
|
if (!returnURL) {
|
||||||
|
NSLog(@"exportLibraryToScheme: failed to build return URL for scheme '%@'", callerScheme);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"exportLibraryToScheme: returning %lu games to scheme '%@'", (unsigned long)games.count, callerScheme);
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[[UIApplication sharedApplication] openURL:returnURL options:@{} completionHandler:nil];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
|
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
|
||||||
switch (g_Config.iScreenRotation) {
|
switch (g_Config.iScreenRotation) {
|
||||||
case ROTATION_LOCKED_HORIZONTAL:
|
case ROTATION_LOCKED_HORIZONTAL:
|
||||||
|
|||||||
+44
-1
@@ -35,8 +35,29 @@ static NSString *ExtractDeepLinkPath(NSURL *url) {
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detects a library-export request of the form "ppsspp://gameInfo?scheme=<callerScheme>"
|
||||||
|
// and returns the caller's URL scheme to respond to (or nil if it's not one).
|
||||||
|
static NSString *ExtractGameInfoScheme(NSURL *url) {
|
||||||
|
if (![[url scheme] isEqualToString:@"ppsspp"]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (![[url host] isEqualToString:@"gameInfo"]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
|
||||||
|
for (NSURLQueryItem *item in components.queryItems) {
|
||||||
|
if ([item.name isEqualToString:@"scheme"] && item.value.length > 0) {
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
@interface SceneDelegate ()
|
@interface SceneDelegate ()
|
||||||
@property (nonatomic, strong) NSString *pendingLaunchPath;
|
@property (nonatomic, strong) NSString *pendingLaunchPath;
|
||||||
|
@property (nonatomic, strong) NSString *pendingExportScheme;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation SceneDelegate
|
@implementation SceneDelegate
|
||||||
@@ -47,11 +68,18 @@ static NSString *ExtractDeepLinkPath(NSURL *url) {
|
|||||||
|
|
||||||
- (void)capturePendingPathFromURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
|
- (void)capturePendingPathFromURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
|
||||||
for (UIOpenURLContext *context in URLContexts) {
|
for (UIOpenURLContext *context in URLContexts) {
|
||||||
|
NSString *exportScheme = ExtractGameInfoScheme(context.URL);
|
||||||
|
if (exportScheme.length > 0) {
|
||||||
|
// The library export is performed after PPSSPP has initialized (and loaded its config)
|
||||||
|
self.pendingExportScheme = exportScheme;
|
||||||
|
NSLog(@"SceneDelegate: captured cold-start gameInfo export scheme: %@", exportScheme);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
NSString *path = ExtractDeepLinkPath(context.URL);
|
NSString *path = ExtractDeepLinkPath(context.URL);
|
||||||
if (path.length > 0) {
|
if (path.length > 0) {
|
||||||
self.pendingLaunchPath = path;
|
self.pendingLaunchPath = path;
|
||||||
NSLog(@"SceneDelegate: captured cold-start deep link path: %@", self.pendingLaunchPath);
|
NSLog(@"SceneDelegate: captured cold-start deep link path: %@", self.pendingLaunchPath);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,6 +88,13 @@ static NSString *ExtractDeepLinkPath(NSURL *url) {
|
|||||||
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
||||||
for (UIOpenURLContext *context in URLContexts) {
|
for (UIOpenURLContext *context in URLContexts) {
|
||||||
NSLog(@"SceneDelegate: openURLContexts called with URL: %@", context.URL);
|
NSLog(@"SceneDelegate: openURLContexts called with URL: %@", context.URL);
|
||||||
|
|
||||||
|
NSString *exportScheme = ExtractGameInfoScheme(context.URL);
|
||||||
|
if (exportScheme.length > 0) {
|
||||||
|
[appDelegate exportLibraryToScheme:exportScheme];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
NSString *path = ExtractDeepLinkPath(context.URL);
|
NSString *path = ExtractDeepLinkPath(context.URL);
|
||||||
if (path.length > 0) {
|
if (path.length > 0) {
|
||||||
[appDelegate processFilePath:path];
|
[appDelegate processFilePath:path];
|
||||||
@@ -118,6 +153,14 @@ static NSString *ExtractDeepLinkPath(NSURL *url) {
|
|||||||
NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/assets/"];
|
NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/assets/"];
|
||||||
NativeInit(argc, (const char**)argv, documentsPath.UTF8String, bundlePath.UTF8String, NULL);
|
NativeInit(argc, (const char**)argv, documentsPath.UTF8String, bundlePath.UTF8String, NULL);
|
||||||
|
|
||||||
|
// If we were cold-started by a library export request, serve it now that
|
||||||
|
// the config (and recent games list) has been loaded by NativeInit.
|
||||||
|
if (self.pendingExportScheme.length > 0) {
|
||||||
|
NSString *exportScheme = self.pendingExportScheme;
|
||||||
|
self.pendingExportScheme = nil;
|
||||||
|
[appDelegate exportLibraryToScheme:exportScheme];
|
||||||
|
}
|
||||||
|
|
||||||
self.window = [[UIWindow alloc] initWithWindowScene:self.windowScene];
|
self.window = [[UIWindow alloc] initWithWindowScene:self.windowScene];
|
||||||
// self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
// self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user