From 4c075f6941b7889bd62a40d20eb725e88e04c7ba Mon Sep 17 00:00:00 2001 From: khanhduytran0 Date: Wed, 14 Feb 2024 10:16:13 +0700 Subject: [PATCH] [WIP] Add modpack installation Added Modrinth, Fabric/Quilt automated installation --- Natives/CMakeLists.txt | 6 + Natives/LauncherNavigationController.m | 73 +++++-- Natives/LauncherProfileEditorViewController.m | 6 +- Natives/LauncherProfilesViewController.m | 11 + Natives/LauncherSplitViewController.m | 1 - Natives/MinecraftResourceDownloadTask.h | 9 +- Natives/MinecraftResourceDownloadTask.m | 93 ++++++--- Natives/PLProfiles.m | 2 +- Natives/SurfaceViewController.m | 10 +- Natives/config.h.in | 7 + .../installer/FabricInstallViewController.m | 16 +- Natives/installer/FabricUtils.h | 7 + Natives/installer/FabricUtils.m | 22 ++ .../installer/ForgeInstallViewController.m | 1 + .../installer/ModpackInstallViewController.h | 4 + .../installer/ModpackInstallViewController.m | 189 ++++++++++++++++++ Natives/installer/modpack/CurseForgeAPI.h | 5 + Natives/installer/modpack/CurseForgeAPI.m | 5 + Natives/installer/modpack/ModpackAPI.h | 21 ++ Natives/installer/modpack/ModpackAPI.m | 59 ++++++ Natives/installer/modpack/ModpackUtils.h | 9 + Natives/installer/modpack/ModpackUtils.m | 46 +++++ Natives/installer/modpack/ModrinthAPI.h | 5 + Natives/installer/modpack/ModrinthAPI.m | 149 ++++++++++++++ Natives/utils.m | 2 +- 25 files changed, 682 insertions(+), 76 deletions(-) create mode 100644 Natives/installer/FabricUtils.h create mode 100644 Natives/installer/FabricUtils.m create mode 100644 Natives/installer/ModpackInstallViewController.h create mode 100644 Natives/installer/ModpackInstallViewController.m create mode 100644 Natives/installer/modpack/CurseForgeAPI.h create mode 100644 Natives/installer/modpack/CurseForgeAPI.m create mode 100644 Natives/installer/modpack/ModpackAPI.h create mode 100644 Natives/installer/modpack/ModpackAPI.m create mode 100644 Natives/installer/modpack/ModpackUtils.h create mode 100644 Natives/installer/modpack/ModpackUtils.m create mode 100644 Natives/installer/modpack/ModrinthAPI.h create mode 100644 Natives/installer/modpack/ModrinthAPI.m diff --git a/Natives/CMakeLists.txt b/Natives/CMakeLists.txt index 6ce1685b59..c036a8da1c 100644 --- a/Natives/CMakeLists.txt +++ b/Natives/CMakeLists.txt @@ -121,6 +121,12 @@ add_executable(PojavLauncher installer/FabricInstallViewController.m installer/ForgeInstallViewController.m + installer/ModpackInstallViewController.m + installer/FabricUtils.m + installer/modpack/ModpackUtils.m + installer/modpack/ModpackAPI.m + installer/modpack/CurseForgeAPI.m + installer/modpack/ModrinthAPI.m input/ControllerInput.m input/GyroInput.m diff --git a/Natives/LauncherNavigationController.m b/Natives/LauncherNavigationController.m index 1c474365fc..3e8efbe38d 100644 --- a/Natives/LauncherNavigationController.m +++ b/Natives/LauncherNavigationController.m @@ -83,6 +83,7 @@ - (void)viewDidLoad self.buttonInstall.layer.cornerRadius = 5; self.buttonInstall.frame = CGRectMake(self.toolbar.frame.size.width * 0.8, 4, self.toolbar.frame.size.width * 0.2, self.toolbar.frame.size.height - 8); self.buttonInstall.tintColor = UIColor.whiteColor; + self.buttonInstall.enabled = NO; [self.buttonInstall addTarget:self action:@selector(performInstallOrShowDetails:) forControlEvents:UIControlEventPrimaryActionTriggered]; [targetToolbar addSubview:self.buttonInstall]; @@ -94,9 +95,11 @@ - (void)viewDidLoad self.progressText.userInteractionEnabled = NO; [targetToolbar addSubview:self.progressText]; - self.buttonInstall.enabled = NO; - [self fetchRemoteVersionList]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(receiveNotification:) + name:@"InstallModpack" + object:nil]; if ([BaseAuthenticator.current isKindOfClass:MicrosoftAuthenticator.class]) { // Perform token refreshment on startup @@ -230,6 +233,7 @@ - (void)setInteractionEnabled:(BOOL)enabled forDownloading:(BOOL)downloading { self.buttonInstall.alpha = 1; self.buttonInstall.enabled = YES; } + UIApplication.sharedApplication.idleTimerDisabled = !enabled; } - (void)launchMinecraft:(UIButton *)sender { @@ -251,7 +255,10 @@ - (void)launchMinecraft:(UIButton *)sender { NSString *versionId = PLProfiles.current.profiles[self.versionTextField.text][@"lastVersionId"]; NSDictionary *object = [remoteVersionList filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(id == %@)", versionId]].firstObject; if (!object) { - object = [localVersionList filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(id == %@)", versionId]].firstObject; + object = @{ + @"id": versionId, + @"type": @"custom" + }; } self.task = [MinecraftResourceDownloadTask new]; @@ -290,20 +297,53 @@ - (void)performInstallOrShowDetails:(UIButton *)sender { } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (context == ProgressObserverContext) { - dispatch_async(dispatch_get_main_queue(), ^{ - NSProgress *progress = object; - self.progressText.text = [NSString stringWithFormat:@"(%@) %@", progress.localizedAdditionalDescription, progress.localizedDescription]; - if (progress.finished) { - self.progressViewMain.observedProgress = nil; - [self invokeAfterJITEnabled:^{ - UIKit_launchMinecraftSurfaceVC(self.task.verMetadata); - }]; - } - }); - } else { + if (context != ProgressObserverContext) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } + + dispatch_async(dispatch_get_main_queue(), ^{ + NSProgress *progress = object; + self.progressText.text = [NSString stringWithFormat:@"(%@) %@", progress.localizedAdditionalDescription, progress.localizedDescription]; + if (!progress.finished) return; + + self.progressViewMain.observedProgress = nil; + if (self.task.metadata) { + [self invokeAfterJITEnabled:^{ + UIKit_launchMinecraftSurfaceVC(self.task.metadata); + }]; + } else { + self.task = nil; + [self setInteractionEnabled:YES forDownloading:YES]; + [self reloadProfileList]; + } + }); +} + +- (void)receiveNotification:(NSNotification *)notification { + if (![notification.name isEqualToString:@"InstallModpack"]) { + return; + } + [self setInteractionEnabled:NO forDownloading:YES]; + self.task = [MinecraftResourceDownloadTask new]; + NSDictionary *userInfo = notification.userInfo; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __weak LauncherNavigationController *weakSelf = self; + self.task.handleError = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf setInteractionEnabled:YES forDownloading:YES]; + weakSelf.task = nil; + weakSelf.progressVC = nil; + }); + }; + [self.task downloadModpackFromAPI:notification.object detail:userInfo[@"detail"] atIndex:[userInfo[@"index"] unsignedLongValue]]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.progressViewMain.observedProgress = self.task.progress; + [self.task.progress addObserver:self + forKeyPath:@"fractionCompleted" + options:NSKeyValueObservingOptionInitial + context:ProgressObserverContext]; + }); + }); } - (void)invokeAfterJITEnabled:(void(^)(void))handler { @@ -387,9 +427,6 @@ - (void)versionClosePicker { } #pragma mark - View controller UI mode -- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { - return UIRectEdgeBottom; -} - (BOOL)prefersHomeIndicatorAutoHidden { return YES; diff --git a/Natives/LauncherProfileEditorViewController.m b/Natives/LauncherProfileEditorViewController.m index 1f22d76db0..8e62ffc8bc 100644 --- a/Natives/LauncherProfileEditorViewController.m +++ b/Natives/LauncherProfileEditorViewController.m @@ -300,8 +300,10 @@ - (void)changeVersionType:(UISegmentedControl *)sender { self.versionList = newVersionList; [self.versionPickerView reloadAllComponents]; - [self.versionPickerView selectRow:self.versionSelectedAt inComponent:0 animated:NO]; - [self pickerView:self.versionPickerView didSelectRow:self.versionSelectedAt inComponent:0]; + if (self.versionSelectedAt != -1) { + [self.versionPickerView selectRow:self.versionSelectedAt inComponent:0 animated:NO]; + [self pickerView:self.versionPickerView didSelectRow:self.versionSelectedAt inComponent:0]; + } } @end diff --git a/Natives/LauncherProfilesViewController.m b/Natives/LauncherProfilesViewController.m index 8ba5948a1b..c711138585 100644 --- a/Natives/LauncherProfilesViewController.m +++ b/Natives/LauncherProfilesViewController.m @@ -11,6 +11,7 @@ #import "UIKit+hook.h" #import "installer/FabricInstallViewController.h" #import "installer/ForgeInstallViewController.h" +#import "installer/ModpackInstallViewController.h" #import "ios_uikit_bridge.h" #import "utils.h" @@ -64,6 +65,11 @@ - (void)viewDidLoad actionWithTitle:@"Forge" image:nil identifier:@"forge" handler:^(UIAction *action) { [self actionCreateForgeProfile]; + }], + [UIAction + actionWithTitle:@"Modpack" image:nil + identifier:@"modpack" handler:^(UIAction *action) { + [self actionCreateModpackProfile]; }] ]]; self.createButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd menu:createMenu]; @@ -101,6 +107,11 @@ - (void)actionCreateForgeProfile { [self presentNavigatedViewController:vc]; } +- (void)actionCreateModpackProfile { + ModpackInstallViewController *vc = [ModpackInstallViewController new]; + [self presentNavigatedViewController:vc]; +} + - (void)actionEditProfile:(NSDictionary *)profile { LauncherProfileEditorViewController *vc = [LauncherProfileEditorViewController new]; vc.profile = profile.mutableCopy; diff --git a/Natives/LauncherSplitViewController.m b/Natives/LauncherSplitViewController.m index bca5164e4a..f81f754bfb 100644 --- a/Natives/LauncherSplitViewController.m +++ b/Natives/LauncherSplitViewController.m @@ -15,7 +15,6 @@ @implementation LauncherSplitViewController - (void)viewDidLoad { [super viewDidLoad]; - UIApplication.sharedApplication.idleTimerDisabled = YES; self.view.backgroundColor = UIColor.systemBackgroundColor; if ([getPrefObject(@"control.control_safe_area") length] == 0) { setPrefObject(@"control.control_safe_area", NSStringFromUIEdgeInsets(getDefaultSafeArea())); diff --git a/Natives/MinecraftResourceDownloadTask.h b/Natives/MinecraftResourceDownloadTask.h index 54b8ff426b..7fec0d5f19 100644 --- a/Natives/MinecraftResourceDownloadTask.h +++ b/Natives/MinecraftResourceDownloadTask.h @@ -1,11 +1,18 @@ #import +@class ModpackAPI; + @interface MinecraftResourceDownloadTask : NSObject @property NSProgress* progress; @property NSMutableArray *fileList, *progressList; -@property NSMutableDictionary* verMetadata; +@property NSMutableDictionary* metadata; @property(nonatomic, copy) void(^handleError)(void); +- (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url sha:(NSString *)sha altName:(NSString *)altName toPath:(NSString *)path; +- (void)addDownloadTaskToProgress:(NSURLSessionDownloadTask *)task; +- (void)finishDownloadWithErrorString:(NSString *)error; + - (void)downloadVersion:(NSDictionary *)version; +- (void)downloadModpackFromAPI:(ModpackAPI *)api detail:(NSDictionary *)modDetail atIndex:(NSUInteger)selectedVersion; @end diff --git a/Natives/MinecraftResourceDownloadTask.m b/Natives/MinecraftResourceDownloadTask.m index ad14d8d544..a49588b8aa 100644 --- a/Natives/MinecraftResourceDownloadTask.m +++ b/Natives/MinecraftResourceDownloadTask.m @@ -1,6 +1,7 @@ #include #import "authenticator/BaseAuthenticator.h" +#import "installer/modpack/ModpackAPI.h" #import "AFNetworking.h" #import "LauncherNavigationController.h" #import "LauncherPreferences.h" @@ -11,7 +12,6 @@ @interface MinecraftResourceDownloadTask () @property AFURLSessionManager* manager; -@property BOOL cancelled; @end @implementation MinecraftResourceDownloadTask @@ -47,7 +47,7 @@ - (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url sha:(NSString * [NSFileManager.defaultManager removeItemAtPath:path error:nil]; return [NSURL fileURLWithPath:path]; } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) { - if (self.cancelled) { + if (self.progress.cancelled) { // Ignore any further errors } else if (error != nil) { [self finishDownloadWithError:error file:name]; @@ -64,6 +64,13 @@ - (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url sha:(NSString * return [self createDownloadTask:url sha:sha altName:altName toPath:path success:nil]; } +- (void)addDownloadTaskToProgress:(NSURLSessionDownloadTask *)task { + NSProgress *progress = [self.manager downloadProgressForTask:task]; + progress.kind = NSProgressKindFile; + [self.progressList addObject:progress]; + [self.progress addChild:progress withPendingUnitCount:1]; +} + - (void)downloadVersionMetadata:(NSDictionary *)version success:(void (^)())success { // Download base json NSString *versionStr = version[@"id"]; @@ -78,27 +85,28 @@ - (void)downloadVersionMetadata:(NSDictionary *)version success:(void (^)())succ version = (id)[MinecraftResourceUtils findVersion:versionStr inList:remoteVersionList]; void(^completionBlock)(void) = ^{ - self.verMetadata = parseJSONFromFile(path); - if (!self.verMetadata) { - [self finishDownloadWithErrorString:@"Downloaded version json was not found"]; + self.metadata = parseJSONFromFile(path); + if (self.metadata[@"NSErrorDescription"]) { + [self finishDownloadWithErrorString:self.metadata[@"NSErrorDescription"]]; return; } - if (self.verMetadata[@"inheritsFrom"]) { - NSMutableDictionary *inheritsFromDict = parseJSONFromFile([NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), self.verMetadata[@"inheritsFrom"]]); + if (self.metadata[@"inheritsFrom"]) { + NSMutableDictionary *inheritsFromDict = parseJSONFromFile([NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), self.metadata[@"inheritsFrom"]]); if (inheritsFromDict) { - [MinecraftResourceUtils processVersion:self.verMetadata inheritsFrom:inheritsFromDict]; - self.verMetadata = inheritsFromDict; + [MinecraftResourceUtils processVersion:self.metadata inheritsFrom:inheritsFromDict]; + self.metadata = inheritsFromDict; } } - [MinecraftResourceUtils tweakVersionJson:self.verMetadata]; + [MinecraftResourceUtils tweakVersionJson:self.metadata]; success(); }; if (!version) { // This is likely local version, check if json exists and has inheritsFrom NSMutableDictionary *json = parseJSONFromFile(path); - if (!json) { - [self finishDownloadWithErrorString:@"Local version json was not found"]; + if (json[@"NSErrorDescription"]) { + [self finishDownloadWithErrorString:json[@"NSErrorDescription"]]; + return; } else if (json[@"inheritsFrom"]) { version = (id)[MinecraftResourceUtils findVersion:json[@"inheritsFrom"] inList:remoteVersionList]; path = [NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), json[@"inheritsFrom"]]; @@ -116,13 +124,15 @@ - (void)downloadVersionMetadata:(NSDictionary *)version success:(void (^)())succ [task resume]; } +#pragma mark - Minecraft installation + - (void)downloadAssetMetadataWithSuccess:(void (^)())success { - NSDictionary *assetIndex = self.verMetadata[@"assetIndex"]; + NSDictionary *assetIndex = self.metadata[@"assetIndex"]; NSString *path = [NSString stringWithFormat:@"%s/assets/indexes/%@.json", getenv("POJAV_GAME_DIR"), assetIndex[@"id"]]; NSString *url = assetIndex[@"url"]; NSString *sha = url.stringByDeletingLastPathComponent.lastPathComponent; NSURLSessionDownloadTask *task = [self createDownloadTask:url sha:sha altName:nil toPath:path success:^{ - self.verMetadata[@"assetIndexObj"] = parseJSONFromFile(path); + self.metadata[@"assetIndexObj"] = parseJSONFromFile(path); success(); }]; [task resume]; @@ -130,7 +140,7 @@ - (void)downloadAssetMetadataWithSuccess:(void (^)())success { - (NSArray *)downloadClientLibraries { NSMutableArray *tasks = [NSMutableArray new]; - for (NSDictionary *library in self.verMetadata[@"libraries"]) { + for (NSDictionary *library in self.metadata[@"libraries"]) { NSString *name = library[@"name"]; NSMutableDictionary *artifact = library[@"downloads"][@"artifact"]; @@ -154,13 +164,10 @@ - (NSArray *)downloadClientLibraries { NSURLSessionDownloadTask *task = [self createDownloadTask:url sha:sha altName:nil toPath:path success:nil]; if (task) { - NSProgress *progress = [self.manager downloadProgressForTask:task]; - progress.kind = NSProgressKindFile; [self.fileList addObject:name]; - [self.progressList addObject:progress]; - [self.progress addChild:progress withPendingUnitCount:1]; + [self addDownloadTaskToProgress:task]; [tasks addObject:task]; - } else if (self.cancelled) { + } else if (self.progress.cancelled) { return nil; } } @@ -169,7 +176,7 @@ - (NSArray *)downloadClientLibraries { - (NSArray *)downloadClientAssets { NSMutableArray *tasks = [NSMutableArray new]; - NSDictionary *assets = self.verMetadata[@"assetIndexObj"]; + NSDictionary *assets = self.metadata[@"assetIndexObj"]; for (NSString *name in assets[@"objects"]) { NSString *hash = assets[@"objects"][name][@"hash"]; NSString *pathname = [NSString stringWithFormat:@"%@/%@", [hash substringToIndex:2], hash]; @@ -193,13 +200,10 @@ - (NSArray *)downloadClientAssets { NSString *url = [NSString stringWithFormat:@"https://resources.download.minecraft.net/%@", pathname]; NSURLSessionDownloadTask *task = [self createDownloadTask:url sha:hash altName:name toPath:path success:nil]; if (task) { - NSProgress *progress = [self.manager downloadProgressForTask:task]; - progress.kind = NSProgressKindFile; [self.fileList addObject:name]; - [self.progressList addObject:progress]; - [self.progress addChild:progress withPendingUnitCount:1]; + [self addDownloadTaskToProgress:task]; [tasks addObject:task]; - } else if (self.cancelled) { + } else if (self.progress.cancelled) { return nil; } } @@ -207,10 +211,7 @@ - (NSArray *)downloadClientAssets { } - (void)downloadVersion:(NSDictionary *)version { - self.cancelled = NO; - self.progress = [NSProgress new]; - [self.fileList removeAllObjects]; - [self.progressList removeAllObjects]; + [self prepareForDownload]; [self downloadVersionMetadata:version success:^{ [self downloadAssetMetadataWithSuccess:^{ NSArray *libTasks = [self downloadClientLibraries]; @@ -224,13 +225,39 @@ - (void)downloadVersion:(NSDictionary *)version { } [libTasks makeObjectsPerformSelector:@selector(resume)]; [assetTasks makeObjectsPerformSelector:@selector(resume)]; - [self.verMetadata removeObjectForKey:@"assetIndexObj"]; + [self.metadata removeObjectForKey:@"assetIndexObj"]; }]; }]; } +#pragma mark - Modpack installation + +- (void)downloadModpackFromAPI:(ModpackAPI *)api detail:(NSDictionary *)modDetail atIndex:(NSUInteger)selectedVersion { + [self prepareForDownload]; + + NSString *url = modDetail[@"versionUrls"][selectedVersion]; + NSString *sha = modDetail[@"versionHashes"][selectedVersion]; + NSString *name = [[modDetail[@"title"] lowercaseString] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + name = [name stringByReplacingOccurrencesOfString:@" " withString:@"_"]; + NSString *packagePath = [NSTemporaryDirectory() stringByAppendingFormat:@"/%@.zip", name]; + + NSURLSessionDownloadTask *task = [self createDownloadTask:url sha:sha altName:nil toPath:packagePath success:^{ + NSString *path = [NSString stringWithFormat:@"%s/custom_gamedir/%@", getenv("POJAV_GAME_DIR"), name]; + [api downloader:self submitDownloadTasksFromPackage:packagePath toPath:path]; + }]; + [task resume]; +} + +#pragma mark - Utilities + +- (void)prepareForDownload { + self.progress = [NSProgress new]; + [self.fileList removeAllObjects]; + [self.progressList removeAllObjects]; +} + - (void)finishDownloadWithErrorString:(NSString *)error { - self.cancelled = YES; + [self.progress cancel]; [self.manager invalidateSessionCancelingTasks:YES resetSession:YES]; showDialog(localize(@"Error", nil), error); self.handleError(); @@ -247,7 +274,7 @@ - (BOOL)checkAccessWithDialog:(BOOL)show { // for now BOOL accessible = [BaseAuthenticator.current.authData[@"username"] hasPrefix:@"Demo."] || BaseAuthenticator.current.authData[@"xboxGamertag"] != nil; if (!accessible) { - self.cancelled = YES; + [self.progress cancel]; if (show) { [self finishDownloadWithErrorString:@"Minecraft can't be legally installed when logged in with a local account. Please switch to an online account to continue."]; } diff --git a/Natives/PLProfiles.m b/Natives/PLProfiles.m index 6e73a52568..5dfe958f1d 100644 --- a/Natives/PLProfiles.m +++ b/Natives/PLProfiles.m @@ -64,7 +64,7 @@ - (id)initWithCurrentInstance { self = [super init]; self.profilePath = [@(getenv("POJAV_GAME_DIR")) stringByAppendingPathComponent:@"launcher_profiles.json"]; self.profileDict = parseJSONFromFile(self.profilePath); - if (self.profileDict[@"error"]) { + if (self.profileDict[@"NSErrorDescription"]) { self.profileDict = PLProfiles.defaultProfiles; [self save]; } diff --git a/Natives/SurfaceViewController.m b/Natives/SurfaceViewController.m index e6b40f7c90..65e0034205 100644 --- a/Natives/SurfaceViewController.m +++ b/Natives/SurfaceViewController.m @@ -34,7 +34,7 @@ @interface SurfaceViewController () { } -@property(nonatomic) NSDictionary* verMetadata; +@property(nonatomic) NSDictionary* metadata; @property(nonatomic) TrackedTextField *inputTextField; @property(nonatomic) NSMutableArray* swipeableButtons; @@ -63,7 +63,7 @@ @implementation SurfaceViewController - (instancetype)initWithMetadata:(NSDictionary *)metadata { self = [super init]; - self.verMetadata = metadata; + self.metadata = metadata; return self; } @@ -421,13 +421,13 @@ - (void)updateGrabState { - (void)launchMinecraft { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - int minVersion = [self.verMetadata[@"javaVersion"][@"majorVersion"] intValue]; + int minVersion = [self.metadata[@"javaVersion"][@"majorVersion"] intValue]; if (minVersion == 0) { - minVersion = [self.verMetadata[@"javaVersion"][@"version"] intValue]; + minVersion = [self.metadata[@"javaVersion"][@"version"] intValue]; } launchJVM( BaseAuthenticator.current.authData[@"username"], - self.verMetadata, + self.metadata, windowWidth, windowHeight, minVersion ); diff --git a/Natives/config.h.in b/Natives/config.h.in index 6732350fd8..7d87720ccb 100644 --- a/Natives/config.h.in +++ b/Natives/config.h.in @@ -14,3 +14,10 @@ #cmakedefine CONFIG_BRANCH "@CONFIG_BRANCH@" #cmakedefine CONFIG_COMMIT "@CONFIG_COMMIT@" + +#cmakedefine CONFIG_CURSEFORGE_API_KEY "@CONFIG_CURSEFORGE_API_KEY@" + +#ifndef CONFIG_CURSEFORGE_API_KEY +# define CONFIG_CURSEFORGE_API_KEY nil +#endif + diff --git a/Natives/installer/FabricInstallViewController.m b/Natives/installer/FabricInstallViewController.m index aef88cbea2..5a264497d5 100644 --- a/Natives/installer/FabricInstallViewController.m +++ b/Natives/installer/FabricInstallViewController.m @@ -1,5 +1,6 @@ #import "AFNetworking.h" #import "FabricInstallViewController.h" +#import "FabricUtils.h" #import "LauncherNavigationController.h" #import "LauncherPreferences.h" #import "LauncherProfileEditorViewController.h" @@ -107,20 +108,7 @@ - (void)viewDidLoad { [super viewDidLoad]; // Init endpoint info - self.endpoints = @{ - @"Fabric": @{ - @"game": @"https://meta.fabricmc.net/v2/versions/game", - @"loader": @"https://meta.fabricmc.net/v2/versions/loader", - @"icon": @"https://avatars.githubusercontent.com/u/21025855?s=64", - @"json": @"https://meta.fabricmc.net/v2/versions/loader/%@/%@/profile/json" - }, - @"Quilt": @{ - @"game": @"https://meta.quiltmc.org/v3/versions/game", - @"loader": @"https://meta.quiltmc.org/v3/versions/loader", - @"icon": @"https://raw.githubusercontent.com/QuiltMC/art/master/brand/64png/quilt_logo_transparent.png", - @"json": @"https://meta.quiltmc.org/v3/versions/loader/%@/%@/profile/json" - } - }; + self.endpoints = FabricUtils.endpoints; [self fetchVersionEndpoints:0]; } diff --git a/Natives/installer/FabricUtils.h b/Natives/installer/FabricUtils.h new file mode 100644 index 0000000000..636985e4a2 --- /dev/null +++ b/Natives/installer/FabricUtils.h @@ -0,0 +1,7 @@ +#import + +@interface FabricUtils : NSObject + ++ (NSDictionary *)endpoints; + +@end diff --git a/Natives/installer/FabricUtils.m b/Natives/installer/FabricUtils.m new file mode 100644 index 0000000000..ba120cbc60 --- /dev/null +++ b/Natives/installer/FabricUtils.m @@ -0,0 +1,22 @@ +#import "FabricUtils.h" + +@implementation FabricUtils + ++ (NSDictionary *)endpoints { + return @{ + @"Fabric": @{ + @"game": @"https://meta.fabricmc.net/v2/versions/game", + @"loader": @"https://meta.fabricmc.net/v2/versions/loader", + @"icon": @"https://avatars.githubusercontent.com/u/21025855?s=64", + @"json": @"https://meta.fabricmc.net/v2/versions/loader/%@/%@/profile/json" + }, + @"Quilt": @{ + @"game": @"https://meta.quiltmc.org/v3/versions/game", + @"loader": @"https://meta.quiltmc.org/v3/versions/loader", + @"icon": @"https://raw.githubusercontent.com/QuiltMC/art/master/brand/64png/quilt_logo_transparent.png", + @"json": @"https://meta.quiltmc.org/v3/versions/loader/%@/%@/profile/json" + } + }; +} + +@end diff --git a/Natives/installer/ForgeInstallViewController.m b/Natives/installer/ForgeInstallViewController.m index 3a6cd73aca..a9f151e552 100644 --- a/Natives/installer/ForgeInstallViewController.m +++ b/Natives/installer/ForgeInstallViewController.m @@ -66,6 +66,7 @@ - (void)loadMetadataFromVendor:(NSString *)vendor { if (![parser parse]) { dispatch_async(dispatch_get_main_queue(), ^{ showDialog(localize(@"Error", nil), parser.parserError.localizedDescription); + [self actionClose]; }); } }); diff --git a/Natives/installer/ModpackInstallViewController.h b/Natives/installer/ModpackInstallViewController.h new file mode 100644 index 0000000000..0744d12a2b --- /dev/null +++ b/Natives/installer/ModpackInstallViewController.h @@ -0,0 +1,4 @@ +#import + +@interface ModpackInstallViewController : UITableViewController +@end diff --git a/Natives/installer/ModpackInstallViewController.m b/Natives/installer/ModpackInstallViewController.m new file mode 100644 index 0000000000..3cfdb7d14a --- /dev/null +++ b/Natives/installer/ModpackInstallViewController.m @@ -0,0 +1,189 @@ +#import "AFNetworking.h" +#import "LauncherNavigationController.h" +#import "ModpackInstallViewController.h" +#import "UIKit+AFNetworking.h" +#import "UIKit+hook.h" +#import "WFWorkflowProgressView.h" +#import "modpack/ModrinthAPI.h" +#import "config.h" +#import "ios_uikit_bridge.h" +#import "utils.h" +#include + +#define kCurseForgeGameIDMinecraft 432 +#define kCurseForgeClassIDModpack 4471 +#define kCurseForgeClassIDMod 6 + +@interface ModpackInstallViewController() +@property(nonatomic) UISearchController *searchController; +@property(nonatomic) UIMenu *currentMenu; +@property(nonatomic) NSMutableArray *list; +@property(nonatomic) NSMutableDictionary *filters; +@property ModrinthAPI *modrinth; +@end + +@implementation ModpackInstallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + //NSString *curseforgeAPIKey = CONFIG_CURSEFORGE_API_KEY; + self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil]; + self.searchController.searchResultsUpdater = self; + self.searchController.obscuresBackgroundDuringPresentation = NO; + self.navigationItem.searchController = self.searchController; + self.modrinth = [ModrinthAPI new]; + self.filters = @{ + @"isModpack": @(YES), + @"name": @" " + // mcVersion + }.mutableCopy; + [self updateSearchResults]; +} + +- (void)loadSearchResultsWithPrevList:(BOOL)prevList { + NSString *name = self.searchController.searchBar.text; + if (!prevList && [self.filters[@"name"] isEqualToString:name]) { + return; + } + + [self switchToLoadingState]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + self.filters[@"name"] = name; + self.list = [self.modrinth searchModWithFilters:self.filters previousPageResult:prevList ? self.list : nil]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.list) { + [self switchToReadyState]; + [self.tableView reloadData]; + } else { + showDialog(localize(@"Error", nil), self.modrinth.lastError.localizedDescription); + [self actionClose]; + } + }); + }); +} + +- (void)updateSearchResults { + [self loadSearchResultsWithPrevList:NO]; +} + +- (void)updateSearchResultsForSearchController:(UISearchController *)searchController { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(updateSearchResults) object:nil]; + [self performSelector:@selector(updateSearchResults) withObject:nil afterDelay:0.5]; +} + +- (void)actionClose { + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)switchToLoadingState { + UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:indicator]; + [indicator startAnimating]; + self.navigationController.modalInPresentation = YES; + self.tableView.allowsSelection = NO; +} + +- (void)switchToReadyState { + UIActivityIndicatorView *indicator = (id)self.navigationItem.rightBarButtonItem.customView; + [indicator stopAnimating]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemClose target:self action:@selector(actionClose)]; + self.navigationController.modalInPresentation = NO; + self.tableView.allowsSelection = YES; +} + +#pragma mark UIContextMenu + +- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location +{ + return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { + return self.currentMenu; + }]; +} + +- (_UIContextMenuStyle *)_contextMenuInteraction:(UIContextMenuInteraction *)interaction styleForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration +{ + _UIContextMenuStyle *style = [_UIContextMenuStyle defaultStyle]; + style.preferredLayout = 3; // _UIContextMenuLayoutCompactMenu + return style; +} + +#pragma mark UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.list.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; + cell.imageView.contentMode = UIViewContentModeScaleToFill; + cell.imageView.clipsToBounds = YES; + } + + NSDictionary *item = self.list[indexPath.row]; + cell.textLabel.text = item[@"title"]; + cell.detailTextLabel.text = item[@"description"]; + UIImage *fallbackImage = [UIImage imageNamed:@"DefaultProfile"]; + [cell.imageView setImageWithURL:[NSURL URLWithString:item[@"imageUrl"]] placeholderImage:fallbackImage]; + + if (!self.modrinth.reachedLastPage && indexPath.row == self.list.count-1) { + [self loadSearchResultsWithPrevList:YES]; + } + + return cell; +} + +- (void)showDetails:(NSDictionary *)details atIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; + + NSMutableArray *menuItems = [[NSMutableArray alloc] init]; + [details[@"versionNames"] enumerateObjectsUsingBlock: + ^(NSString *name, NSUInteger i, BOOL *stop) { + NSString *nameWithVersion = name; + NSString *mcVersion = details[@"mcVersionNames"][i]; + if (![name hasSuffix:mcVersion]) { + nameWithVersion = [NSString stringWithFormat:@"%@ - %@", name, mcVersion]; + } + [menuItems addObject:[UIAction + actionWithTitle:nameWithVersion + image:nil identifier:nil + handler:^(UIAction *action) { + [self actionClose]; + [self.modrinth installModpackFromDetail:self.list[indexPath.row] atIndex:i]; + }]]; + }]; + + self.currentMenu = [UIMenu menuWithTitle:@"" children:menuItems]; + UIContextMenuInteraction *interaction = [[UIContextMenuInteraction alloc] initWithDelegate:self]; + cell.detailTextLabel.interactions = @[interaction]; + [interaction _presentMenuAtLocation:CGPointZero]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + NSDictionary *item = self.list[indexPath.row]; + if ([item[@"versionDetailsLoaded"] boolValue]) { + [self showDetails:item atIndexPath:indexPath]; + return; + } + [tableView deselectRowAtIndexPath:indexPath animated:NO]; + [self switchToLoadingState]; +dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.modrinth loadDetailsOfMod:self.list[indexPath.row]]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self switchToReadyState]; + if ([item[@"versionDetailsLoaded"] boolValue]) { + [self showDetails:item atIndexPath:indexPath]; + } else { + showDialog(localize(@"Error", nil), self.modrinth.lastError.localizedDescription); + } + }); + }); +} + +@end diff --git a/Natives/installer/modpack/CurseForgeAPI.h b/Natives/installer/modpack/CurseForgeAPI.h new file mode 100644 index 0000000000..09c8eca7e6 --- /dev/null +++ b/Natives/installer/modpack/CurseForgeAPI.h @@ -0,0 +1,5 @@ +#import +#import "ModpackAPI.h" + +@interface CurseForgeAPI : ModpackAPI +@end diff --git a/Natives/installer/modpack/CurseForgeAPI.m b/Natives/installer/modpack/CurseForgeAPI.m new file mode 100644 index 0000000000..1545316846 --- /dev/null +++ b/Natives/installer/modpack/CurseForgeAPI.m @@ -0,0 +1,5 @@ +#import +#import "CurseForgeAPI.h" + +@implementation CurseForgeAPI +@end diff --git a/Natives/installer/modpack/ModpackAPI.h b/Natives/installer/modpack/ModpackAPI.h new file mode 100644 index 0000000000..a5f83406b6 --- /dev/null +++ b/Natives/installer/modpack/ModpackAPI.h @@ -0,0 +1,21 @@ +#import +#import "ModpackUtils.h" +#import "UnzipKit.h" + +@class MinecraftResourceDownloadTask; + +@interface ModpackAPI : NSObject +@property(nonatomic) NSString *baseURL; +@property(nonatomic) NSError *lastError; +@property(nonatomic) BOOL reachedLastPage; + +- (instancetype)initWithURL:(NSString *)url; +- (NSMutableArray *)searchModWithFilters:(NSDictionary *)filters previousPageResult:(NSMutableArray *)prevResult; +- (void)loadDetailsOfMod:(NSMutableDictionary *)item; + +- (void)installModpackFromDetail:(NSDictionary *)modDetail atIndex:(NSUInteger)selectedVersion; +- (void)downloader:(MinecraftResourceDownloadTask *)downloader submitDownloadTasksFromPackage:(NSString *)packagePath toPath:(NSString *)destPath; + +- (id)getEndpoint:(NSString *)endpoint params:(NSDictionary *)params; + +@end diff --git a/Natives/installer/modpack/ModpackAPI.m b/Natives/installer/modpack/ModpackAPI.m new file mode 100644 index 0000000000..00144ea37a --- /dev/null +++ b/Natives/installer/modpack/ModpackAPI.m @@ -0,0 +1,59 @@ +#import "AFNetworking.h" +#import "MinecraftResourceDownloadTask.h" +#import "ModpackAPI.h" +#import "utils.h" + +@implementation ModpackAPI + +#pragma mark Interface methods + +- (instancetype)initWithURL:(NSString *)url { + self = [super init]; + self.baseURL = url; + return self; +} + +- (void)loadDetailsOfMod:(NSMutableDictionary *)item { + [self doesNotRecognizeSelector:_cmd]; +} + +- (NSMutableArray *)searchModWithFilters:(NSDictionary *)searchFilters previousPageResult:(NSMutableArray *)prevResult { + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (void)downloader:(MinecraftResourceDownloadTask *)downloader submitDownloadTasksFromPackage:(NSString *)packagePath toPath:(NSString *)destPath { + [self doesNotRecognizeSelector:_cmd]; +} + +- (id)getEndpoint:(NSString *)endpoint params:(NSDictionary *)params { + __block id result; + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + NSString *url = [self.baseURL stringByAppendingPathComponent:endpoint]; + AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; + [manager GET:url parameters:params headers:nil progress:nil + success:^(NSURLSessionTask *task, id obj) { + result = obj; + dispatch_group_leave(group); + } failure:^(NSURLSessionTask *operation, NSError *error) { + self.lastError = error; + dispatch_group_leave(group); + }]; + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + //NSLog(@"%@", result); + return result; +} + +- (void)installModpackFromDetail:(NSDictionary *)modDetail atIndex:(NSUInteger)selectedVersion { + // Pass details to LauncherNavigationController + NSDictionary* userInfo = @{ + @"detail": modDetail, + @"index": @(selectedVersion) + }; + [NSNotificationCenter.defaultCenter + postNotificationName:@"InstallModpack" + object:self userInfo:userInfo]; +} + +@end diff --git a/Natives/installer/modpack/ModpackUtils.h b/Natives/installer/modpack/ModpackUtils.h new file mode 100644 index 0000000000..554dbeca41 --- /dev/null +++ b/Natives/installer/modpack/ModpackUtils.h @@ -0,0 +1,9 @@ +#import +#import "UnzipKit.h" + +@interface ModpackUtils : NSObject + ++ (void)archive:(UZKArchive *)archive extractDirectory:(NSString *)dir toPath:(NSString *)path error:(NSError **)error; ++ (NSDictionary *)infoForDependencies:(NSDictionary *)dependency; + +@end diff --git a/Natives/installer/modpack/ModpackUtils.m b/Natives/installer/modpack/ModpackUtils.m new file mode 100644 index 0000000000..630143f1ba --- /dev/null +++ b/Natives/installer/modpack/ModpackUtils.m @@ -0,0 +1,46 @@ +#import "installer/FabricUtils.h" +#import "ModpackUtils.h" + +@implementation ModpackUtils + ++ (void)archive:(UZKArchive *)archive extractDirectory:(NSString *)dir toPath:(NSString *)path error:(NSError *__autoreleasing*)error { + [archive performOnFilesInArchive:^(UZKFileInfo *fileInfo, BOOL *stop) { + if (![fileInfo.filename hasPrefix:dir] || + fileInfo.filename.length <= dir.length) { + return; + } + NSString *fileName = [fileInfo.filename substringFromIndex:dir.length+1]; + NSString *destItemPath = [path stringByAppendingPathComponent:fileName]; + NSString *destDirPath = fileInfo.isDirectory ? destItemPath : destItemPath.stringByDeletingLastPathComponent; + BOOL createdDir = [NSFileManager.defaultManager createDirectoryAtPath:destDirPath + withIntermediateDirectories:YES + attributes:nil error:error]; + if (!createdDir) { + *stop = YES; + return; + } + NSData *data = [archive extractData:fileInfo error:error]; + BOOL written = [data writeToFile:destItemPath options:NSDataWritingAtomic error:error]; + *stop = !data || !written; + if (!*stop) { + NSLog(@"[ModpackDL] Extracted %@", fileInfo.filename); + } + } error:error]; +} + ++ (NSDictionary *)infoForDependencies:(NSDictionary *)dependency { + NSMutableDictionary *info = [NSMutableDictionary new]; + NSString *minecraftVersion = dependency[@"minecraft"]; + if (dependency[@"forge"]) { + info[@"id"] = [NSString stringWithFormat:@"%@-forge-%@", minecraftVersion, dependency[@"forge"]]; + } else if (dependency[@"fabric-loader"]) { + info[@"id"] = [NSString stringWithFormat:@"fabric-loader-%@-%@", dependency[@"fabric-loader"], minecraftVersion]; + info[@"json"] = [NSString stringWithFormat:FabricUtils.endpoints[@"Fabric"][@"json"], minecraftVersion, dependency[@"fabric-loader"]]; + } else if (dependency[@"quilt-loader"]) { + info[@"id"] = [NSString stringWithFormat:@"quilt-loader-%@-%@", dependency[@"quilt-loader"], minecraftVersion]; + info[@"json"] = [NSString stringWithFormat:FabricUtils.endpoints[@"Quilt"][@"json"], minecraftVersion, dependency[@"quilt-loader"]]; + } + return info; +} + +@end diff --git a/Natives/installer/modpack/ModrinthAPI.h b/Natives/installer/modpack/ModrinthAPI.h new file mode 100644 index 0000000000..a3043c8552 --- /dev/null +++ b/Natives/installer/modpack/ModrinthAPI.h @@ -0,0 +1,5 @@ +#import +#import "ModpackAPI.h" + +@interface ModrinthAPI : ModpackAPI +@end diff --git a/Natives/installer/modpack/ModrinthAPI.m b/Natives/installer/modpack/ModrinthAPI.m new file mode 100644 index 0000000000..5efbf3e4f1 --- /dev/null +++ b/Natives/installer/modpack/ModrinthAPI.m @@ -0,0 +1,149 @@ +#import "MinecraftResourceDownloadTask.h" +#import "ModrinthAPI.h" +#import "PLProfiles.h" + +@implementation ModrinthAPI + +- (instancetype)init { + return [super initWithURL:@"https://api.modrinth.com/v2"]; +} + +- (NSMutableArray *)searchModWithFilters:(NSDictionary *)searchFilters previousPageResult:(NSMutableArray *)modrinthSearchResult { + int limit = 50; + + NSMutableString *facetString = [NSMutableString new]; + [facetString appendString:@"["]; + [facetString appendFormat:@"[\"project_type:%@\"]", searchFilters[@"isModpack"].boolValue ? @"modpack" : @"mod"]; + if (searchFilters[@"mcVersion"].length > 0) { + [facetString appendFormat:@",[\"versions:%@\"]", searchFilters[@"mcVersion"]]; + } + [facetString appendString:@"]"]; + + NSDictionary *params = @{ + @"facets": facetString, + @"query": [searchFilters[@"name"] stringByReplacingOccurrencesOfString:@" " withString:@"+"], + @"limit": @(limit), + @"index": @"relevance", + @"offset": @(modrinthSearchResult.count) + }; + NSDictionary *response = [self getEndpoint:@"search" params:params]; + if (!response) { + return nil; + } + + NSMutableArray *result = modrinthSearchResult ?: [NSMutableArray new]; + for (NSDictionary *hit in response[@"hits"]) { + BOOL isModpack = [hit[@"project_type"] isEqualToString:@"modpack"]; + [result addObject:@{ + @"apiSource": @(1), // Constant MODRINTH + @"isModpack": @(isModpack), + @"id": hit[@"project_id"], + @"title": hit[@"title"], + @"description": hit[@"description"], + @"imageUrl": hit[@"icon_url"] + }.mutableCopy]; + } + self.reachedLastPage = result.count >= [response[@"total_hits"] unsignedLongValue]; + return result; +} + +- (void)loadDetailsOfMod:(NSMutableDictionary *)item { + NSArray *response = [self getEndpoint:[NSString stringWithFormat:@"project/%@/version", item[@"id"]] params:nil]; + if (!response) { + return; + } + NSArray *names = [response valueForKey:@"name"]; + NSMutableArray *mcNames = [NSMutableArray new]; + NSMutableArray *urls = [NSMutableArray new]; + NSMutableArray *hashes = [NSMutableArray new]; + [response enumerateObjectsUsingBlock: + ^(NSDictionary *version, NSUInteger i, BOOL *stop) { + mcNames[i] = [version[@"game_versions"] firstObject]; + urls[i] = [version[@"files"] firstObject][@"url"]; + NSDictionary *hashesMap = [version[@"files"] firstObject][@"hashes"]; + hashes[i] = hashesMap[@"sha1"] ?: [NSNull null]; + }]; + item[@"versionNames"] = names; + item[@"mcVersionNames"] = mcNames; + item[@"versionUrls"] = urls; + item[@"versionHashes"] = hashes; + item[@"versionDetailsLoaded"] = @(YES); +} + +- (void)downloader:(MinecraftResourceDownloadTask *)downloader submitDownloadTasksFromPackage:(NSString *)packagePath toPath:(NSString *)destPath { + NSError *error; + UZKArchive *archive = [[UZKArchive alloc] initWithPath:packagePath error:&error]; + if (error) { + [downloader finishDownloadWithErrorString:[NSString stringWithFormat:@"Failed to open modpack package: %@", error.localizedDescription]]; + return; + } + + NSData *indexData = [archive extractDataFromFile:@"modrinth.index.json" error:&error]; + NSDictionary* indexDict = [NSJSONSerialization JSONObjectWithData:indexData options:kNilOptions error:&error]; + if (error) { + [downloader finishDownloadWithErrorString:[NSString stringWithFormat:@"Failed to parse modrinth.index.json: %@", error.localizedDescription]]; + return; + } + + downloader.progress.totalUnitCount = [indexDict[@"files"] count]; + for (NSDictionary *indexFile in indexDict[@"files"]) { + if ([indexFile[@"downloads"] count] > 1) { + [downloader finishDownloadWithErrorString:[NSString stringWithFormat:@"Unhandled multiple files download %@", indexFile[@"downloads"]]]; + return; + } + NSString *url = [indexFile[@"downloads"] firstObject]; + NSString *sha = indexFile[@"hashes"][@"sha1"]; + NSString *path = [destPath stringByAppendingPathComponent:indexFile[@"path"]]; + NSURLSessionDownloadTask *task = [downloader createDownloadTask:url sha:sha altName:nil toPath:path]; + if (task) { + [downloader.fileList addObject:indexFile[@"path"]]; + [downloader addDownloadTaskToProgress:task]; + [task resume]; + } else if (!downloader.progress.cancelled) { + downloader.progress.completedUnitCount++; + } else { + return; // cancelled + } + } + + [ModpackUtils archive:archive extractDirectory:@"overrides" toPath:destPath error:&error]; + if (error) { + [downloader finishDownloadWithErrorString:[NSString stringWithFormat:@"Failed to extract overrides from modpack package: %@", error.localizedDescription]]; + return; + } + + [ModpackUtils archive:archive extractDirectory:@"client-overrides" toPath:destPath error:&error]; + if (error) { + [downloader finishDownloadWithErrorString:[NSString stringWithFormat:@"Failed to extract client-overrides from modpack package: %@", error.localizedDescription]]; + return; + } + + // Delete package cache + [NSFileManager.defaultManager removeItemAtPath:packagePath error:nil]; + + // Download dependency client json (if available) + NSDictionary *depInfo = [ModpackUtils infoForDependencies:indexDict[@"dependencies"]]; + if (depInfo[@"json"]) { + NSString *jsonPath = [NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), depInfo[@"id"]]; + NSURLSessionDownloadTask *task = [downloader createDownloadTask:depInfo[@"json"] sha:nil altName:nil toPath:jsonPath]; + if (task) { + [downloader.fileList addObject:jsonPath.lastPathComponent]; + [downloader addDownloadTaskToProgress:task]; + [task resume]; + } else if (downloader.progress.cancelled) { + return; // cancelled + } + } + // TODO: automation for Forge + + // Create profile + PLProfiles.current.profiles[indexDict[@"name"]] = @{ + @"gameDir": [NSString stringWithFormat:@"./custom_gamedir/%@", destPath.lastPathComponent], + @"name": indexDict[@"name"], + @"lastVersionId": depInfo[@"id"], + //@"icon": + }.mutableCopy; + PLProfiles.current.selectedProfileName = indexDict[@"name"]; +} + +@end diff --git a/Natives/utils.m b/Natives/utils.m index 16d342c81b..021f3336a8 100644 --- a/Natives/utils.m +++ b/Natives/utils.m @@ -68,7 +68,7 @@ void openLink(UIViewController* sender, NSURL* link) { NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; if (content == nil) { NSLog(@"[ParseJSON] Error: could not read %@: %@", path, error.localizedDescription); - return [@{@"error": error} mutableCopy]; + return @{@"NSErrorDescription": error.localizedDescription}.mutableCopy; } NSData* data = [content dataUsingEncoding:NSUTF8StringEncoding];