From d7302326b945b9c992311007f166bf2f7e4b6655 Mon Sep 17 00:00:00 2001 From: khanhduytran0 Date: Sat, 10 Feb 2024 18:12:40 +0700 Subject: [PATCH] Feat+Refactor[MCDL]: rewrite to allow download libraries in parallel --- Natives/CMakeLists.txt | 2 + Natives/DownloadProgressViewController.h | 9 + Natives/DownloadProgressViewController.m | 122 +++++ Natives/Info.plist | 4 + Natives/LauncherNavigationController.h | 2 +- Natives/LauncherNavigationController.m | 102 +++-- Natives/LauncherPrefManageJREViewController.m | 4 +- Natives/MinecraftResourceDownloadTask.h | 11 + Natives/MinecraftResourceDownloadTask.m | 299 +++++++++++++ Natives/MinecraftResourceUtils.h | 8 +- Natives/MinecraftResourceUtils.m | 417 +----------------- Natives/SurfaceViewController.h | 1 + Natives/SurfaceViewController.m | 32 +- .../external/Apple/WFWorkflowProgressView.h | 6 +- Natives/ios_uikit_bridge.h | 2 +- Natives/ios_uikit_bridge.m | 4 +- .../resources/en.lproj/Localizable.strings | 1 + entitlements.trollstore.xml | 8 + 18 files changed, 556 insertions(+), 478 deletions(-) create mode 100644 Natives/DownloadProgressViewController.h create mode 100644 Natives/DownloadProgressViewController.m create mode 100644 Natives/MinecraftResourceDownloadTask.h create mode 100644 Natives/MinecraftResourceDownloadTask.m diff --git a/Natives/CMakeLists.txt b/Natives/CMakeLists.txt index 6ca6d23213..6ce1685b59 100644 --- a/Natives/CMakeLists.txt +++ b/Natives/CMakeLists.txt @@ -130,6 +130,7 @@ add_executable(PojavLauncher AppDelegate.m CustomControlsViewController.m CustomControlsViewController+UndoManager.m + DownloadProgressViewController.m FileListViewController.m GameSurfaceView.m JavaGUIViewController.m @@ -144,6 +145,7 @@ add_executable(PojavLauncher LauncherProfileEditorViewController.m LauncherProfilesViewController.m LauncherSplitViewController.m + MinecraftResourceDownloadTask.m MinecraftResourceUtils.m PickTextField.m PLLogOutputView.m diff --git a/Natives/DownloadProgressViewController.h b/Natives/DownloadProgressViewController.h new file mode 100644 index 0000000000..b3fbd9a792 --- /dev/null +++ b/Natives/DownloadProgressViewController.h @@ -0,0 +1,9 @@ +#import +#import "MinecraftResourceDownloadTask.h" + +@interface DownloadProgressViewController : UITableViewController +@property MinecraftResourceDownloadTask* task; + +- (instancetype)initWithTask:(MinecraftResourceDownloadTask *)task; + +@end diff --git a/Natives/DownloadProgressViewController.m b/Natives/DownloadProgressViewController.m new file mode 100644 index 0000000000..c2a4ab089f --- /dev/null +++ b/Natives/DownloadProgressViewController.m @@ -0,0 +1,122 @@ +#import +#import +#import "DownloadProgressViewController.h" +#import "WFWorkflowProgressView.h" + +static void *CellProgressObserverContext = &CellProgressObserverContext; +static void *TotalProgressObserverContext = &TotalProgressObserverContext; + +@interface DownloadProgressViewController () +@property BOOL needsReloadData; +@end + +@implementation DownloadProgressViewController + +- (instancetype)initWithTask:(MinecraftResourceDownloadTask *)task { + self = [super init]; + self.task = task; + return self; +} + +- (void)loadView { + [super loadView]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemClose target:self action:@selector(actionClose)]; + self.tableView.allowsSelection = NO; + + // Load WFWorkflowProgressView + dlopen("/System/Library/PrivateFrameworks/WorkflowUIServices.framework/WorkflowUIServices", RTLD_GLOBAL); +} +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + +[self.task.progress addObserver:self + forKeyPath:@"fractionCompleted" + options:NSKeyValueObservingOptionInitial + context:TotalProgressObserverContext]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + +[self.task.progress removeObserver:self forKeyPath:@"fractionCompleted"]; +} + +- (void)actionClose { + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + NSProgress *progress = object; + if (context == CellProgressObserverContext) { + UITableViewCell *cell = objc_getAssociatedObject(progress, @"cell"); + if (!cell) return; + dispatch_async(dispatch_get_main_queue(), ^{ + cell.detailTextLabel.text = progress.localizedAdditionalDescription; + WFWorkflowProgressView *progressView = (id)cell.accessoryView; + progressView.fractionCompleted = progress.fractionCompleted; + if (progress.finished) { + [progressView transitionCompletedLayerToVisible:YES animated:YES haptic:NO]; + } + }); + } else if (context == TotalProgressObserverContext) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.title = [NSString stringWithFormat:@"(%@) %@", progress.localizedAdditionalDescription, progress.localizedDescription]; + static dispatch_once_t once; + if (self.needsReloadData) { + [self.tableView reloadData]; + } + }); + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + self.needsReloadData = self.task.fileList.count == 0; + return self.task.fileList.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; + + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; + WFWorkflowProgressView *progressView = [[NSClassFromString(@"WFWorkflowProgressView") alloc] initWithFrame:CGRectMake(0, 0, 30, 30)]; + progressView.resolvedTintColor = self.view.tintColor; + progressView.stopSize = 0; + cell.accessoryView = progressView; + } + + // Unset the last cell displaying the progress + NSProgress *lastProgress = objc_getAssociatedObject(cell, @"progress"); + if (lastProgress) { + objc_setAssociatedObject(lastProgress, @"cell", nil, OBJC_ASSOCIATION_ASSIGN); + @try { + [lastProgress removeObserver:self forKeyPath:@"fractionCompleted"]; + } @catch(id anException) {} + } + + NSProgress *progress = self.task.progressList[indexPath.row]; + objc_setAssociatedObject(cell, @"progress", progress, OBJC_ASSOCIATION_ASSIGN); + objc_setAssociatedObject(progress, @"cell", cell, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [progress addObserver:self + forKeyPath:@"fractionCompleted" + options:NSKeyValueObservingOptionInitial + context:CellProgressObserverContext]; + + WFWorkflowProgressView *progressView = (id)cell.accessoryView; + if (lastProgress.finished) { + [progressView reset]; + } + progressView.fractionCompleted = progress.fractionCompleted; + [progressView transitionCompletedLayerToVisible:progress.finished animated:NO haptic:NO]; + [progressView transitionRunningLayerToVisible:!progress.finished animated:NO]; + + cell.textLabel.text = self.task.fileList[indexPath.row]; + cell.detailTextLabel.text = progress.localizedAdditionalDescription; + return cell; +} + +@end diff --git a/Natives/Info.plist b/Natives/Info.plist index 43179c27cf..349e0fea94 100644 --- a/Natives/Info.plist +++ b/Natives/Info.plist @@ -157,6 +157,10 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + fetch + UIDeviceFamily 1 diff --git a/Natives/LauncherNavigationController.h b/Natives/LauncherNavigationController.h index e538887345..b0b8b23940 100644 --- a/Natives/LauncherNavigationController.h +++ b/Natives/LauncherNavigationController.h @@ -10,6 +10,6 @@ NSMutableArray *localVersionList, *remoteVersionList; - (void)enterModInstallerWithPath:(NSString *)path hitEnterAfterWindowShown:(BOOL)hitEnter; - (void)fetchLocalVersionList; -- (void)setInteractionEnabled:(BOOL)enable; +- (void)setInteractionEnabled:(BOOL)enable forDownloading:(BOOL)downloading; @end diff --git a/Natives/LauncherNavigationController.m b/Natives/LauncherNavigationController.m index 20df559f62..1c474365fc 100644 --- a/Natives/LauncherNavigationController.m +++ b/Natives/LauncherNavigationController.m @@ -3,10 +3,12 @@ #import "AFNetworking.h" #import "ALTServerConnection.h" #import "CustomControlsViewController.h" +#import "DownloadProgressViewController.h" #import "JavaGUIViewController.h" #import "LauncherMenuViewController.h" #import "LauncherNavigationController.h" #import "LauncherPreferences.h" +#import "MinecraftResourceDownloadTask.h" #import "MinecraftResourceUtils.h" #import "PickTextField.h" #import "PLPickerView.h" @@ -18,9 +20,13 @@ #define AUTORESIZE_MASKS UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin +static void *ProgressObserverContext = &ProgressObserverContext; + @interface LauncherNavigationController () { } +@property(nonatomic) MinecraftResourceDownloadTask* task; +@property(nonatomic) DownloadProgressViewController* progressVC; @property(nonatomic) PLPickerView* versionPickerView; @property(nonatomic) UITextField* versionTextField; @property(nonatomic) int profileSelectedAt; @@ -61,16 +67,13 @@ - (void)viewDidLoad self.versionTextField.inputAccessoryView = versionPickToolbar; self.versionTextField.inputView = self.versionPickerView; - UIView *targetToolbar; - targetToolbar = self.toolbar; + UIView *targetToolbar = self.toolbar; [targetToolbar addSubview:self.versionTextField]; self.progressViewMain = [[UIProgressView alloc] initWithFrame:CGRectMake(0, 0, self.toolbar.frame.size.width, 4)]; - self.progressViewSub = [[UIProgressView alloc] initWithFrame:CGRectMake(0, self.toolbar.frame.size.height - 4, self.toolbar.frame.size.width, 4)]; - self.progressViewMain.autoresizingMask = self.progressViewSub.autoresizingMask = AUTORESIZE_MASKS; - self.progressViewMain.hidden = self.progressViewSub.hidden = YES; + self.progressViewMain.autoresizingMask = AUTORESIZE_MASKS; + self.progressViewMain.hidden = YES; [targetToolbar addSubview:self.progressViewMain]; - [targetToolbar addSubview:self.progressViewSub]; self.buttonInstall = [UIButton buttonWithType:UIButtonTypeSystem]; setButtonPointerInteraction(self.buttonInstall); @@ -80,10 +83,10 @@ - (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 addTarget:self action:@selector(launchMinecraft:) forControlEvents:UIControlEventPrimaryActionTriggered]; + [self.buttonInstall addTarget:self action:@selector(performInstallOrShowDetails:) forControlEvents:UIControlEventPrimaryActionTriggered]; [targetToolbar addSubview:self.buttonInstall]; - self.progressText = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.toolbar.frame.size.width, self.toolbar.frame.size.height)]; + self.progressText = [[UILabel alloc] initWithFrame:self.versionTextField.frame]; self.progressText.adjustsFontSizeToFitWidth = YES; self.progressText.autoresizingMask = AUTORESIZE_MASKS; self.progressText.font = [self.progressText.font fontWithSize:16]; @@ -97,11 +100,11 @@ - (void)viewDidLoad if ([BaseAuthenticator.current isKindOfClass:MicrosoftAuthenticator.class]) { // Perform token refreshment on startup - [self setInteractionEnabled:NO]; + [self setInteractionEnabled:NO forDownloading:NO]; id callback = ^(NSString* status, BOOL success) { self.progressText.text = status; if (status == nil) { - [self setInteractionEnabled:YES]; + [self setInteractionEnabled:YES forDownloading:NO]; } else if (!success) { showDialog(localize(@"Error", nil), status); } @@ -213,15 +216,20 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocum [self enterModInstallerWithPath:url.path hitEnterAfterWindowShown:NO]; } -- (void)setInteractionEnabled:(BOOL)enabled { +- (void)setInteractionEnabled:(BOOL)enabled forDownloading:(BOOL)downloading { for (UIControl *view in self.toolbar.subviews) { if ([view isKindOfClass:UIControl.class]) { view.alpha = enabled ? 1 : 0.2; view.enabled = enabled; } } - - self.progressViewMain.hidden = self.progressViewSub.hidden = enabled; + self.progressViewMain.hidden = enabled; + self.progressText.text = nil; + if (downloading) { + [self.buttonInstall setTitle:localize(enabled ? @"Play" : @"Details", nil) forState:UIControlStateNormal]; + self.buttonInstall.alpha = 1; + self.buttonInstall.enabled = YES; + } } - (void)launchMinecraft:(UIButton *)sender { @@ -238,8 +246,7 @@ - (void)launchMinecraft:(UIButton *)sender { return; } - sender.alpha = 0.5; - [self setInteractionEnabled:NO]; + [self setInteractionEnabled:NO forDownloading:YES]; NSString *versionId = PLProfiles.current.profiles[self.versionTextField.text][@"lastVersionId"]; NSDictionary *object = [remoteVersionList filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(id == %@)", versionId]].firstObject; @@ -247,29 +254,56 @@ - (void)launchMinecraft:(UIButton *)sender { object = [localVersionList filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(id == %@)", versionId]].firstObject; } - [MinecraftResourceUtils downloadVersion:object callback:^(NSString *stage, NSProgress *mainProgress, NSProgress *progress) { - if (progress == nil && stage != nil) { - NSLog(@"[MCDL] %@", stage); + self.task = [MinecraftResourceDownloadTask new]; + 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 downloadVersion:object]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.progressViewMain.observedProgress = self.task.progress; + [self.task.progress addObserver:self + forKeyPath:@"fractionCompleted" + options:NSKeyValueObservingOptionInitial + context:ProgressObserverContext]; + }); + }); +} + +- (void)performInstallOrShowDetails:(UIButton *)sender { + if (self.task) { + if (!self.progressVC) { + self.progressVC = [[DownloadProgressViewController alloc] initWithTask:self.task]; } - self.progressViewMain.observedProgress = mainProgress; - self.progressViewSub.observedProgress = progress; - if (stage == nil) { - sender.alpha = 1; - self.progressText.text = nil; - [self setInteractionEnabled:YES]; - if (mainProgress != nil) { + UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:self.progressVC]; + nav.modalPresentationStyle = UIModalPresentationPopover; + nav.popoverPresentationController.sourceView = sender; + [self presentViewController:nav animated:YES completion:nil]; + } else { + [self launchMinecraft: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(); + UIKit_launchMinecraftSurfaceVC(self.task.verMetadata); }]; } - return; - } - NSString *completed = [NSByteCountFormatter stringFromByteCount:progress.completedUnitCount countStyle:NSByteCountFormatterCountStyleMemory]; - NSString *total = [NSByteCountFormatter stringFromByteCount:progress.totalUnitCount countStyle:NSByteCountFormatterCountStyleMemory]; - self.progressText.text = [NSString stringWithFormat:@"%@ (%@ / %@)", stage, completed, total]; - }]; - - //callback_LauncherViewController_installMinecraft("1.12.2"); + }); + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } } - (void)invokeAfterJITEnabled:(void(^)(void))handler { diff --git a/Natives/LauncherPrefManageJREViewController.m b/Natives/LauncherPrefManageJREViewController.m index 409006f61f..2e97220859 100644 --- a/Natives/LauncherPrefManageJREViewController.m +++ b/Natives/LauncherPrefManageJREViewController.m @@ -94,7 +94,7 @@ - (void)actionImportRuntime { - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url { LauncherNavigationController *nav = (id)self.navigationController; - [nav setInteractionEnabled:NO]; + [nav setInteractionEnabled:NO forDownloading:NO]; [url startAccessingSecurityScopedResource]; NSUInteger xzSize = [NSFileManager.defaultManager attributesOfItemAtPath:url.path error:nil].fileSize; @@ -136,7 +136,7 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocum vc.installingIndexPath = nil; [vc.tableView reloadData]; - [nav setInteractionEnabled:YES]; + [nav setInteractionEnabled:YES forDownloading:NO]; nav.progressViewMain.observedProgress = nil; nav.progressViewSub.observedProgress = nil; nav.progressText.text = @""; diff --git a/Natives/MinecraftResourceDownloadTask.h b/Natives/MinecraftResourceDownloadTask.h new file mode 100644 index 0000000000..54b8ff426b --- /dev/null +++ b/Natives/MinecraftResourceDownloadTask.h @@ -0,0 +1,11 @@ +#import + +@interface MinecraftResourceDownloadTask : NSObject +@property NSProgress* progress; +@property NSMutableArray *fileList, *progressList; +@property NSMutableDictionary* verMetadata; +@property(nonatomic, copy) void(^handleError)(void); + +- (void)downloadVersion:(NSDictionary *)version; + +@end diff --git a/Natives/MinecraftResourceDownloadTask.m b/Natives/MinecraftResourceDownloadTask.m new file mode 100644 index 0000000000..05e1ec2bb8 --- /dev/null +++ b/Natives/MinecraftResourceDownloadTask.m @@ -0,0 +1,299 @@ +#include + +#import "authenticator/BaseAuthenticator.h" +#import "AFNetworking.h" +#import "AFURLSessionOperation.h" +#import "LauncherNavigationController.h" +#import "LauncherPreferences.h" +#import "MinecraftResourceDownloadTask.h" +#import "MinecraftResourceUtils.h" +#import "ios_uikit_bridge.h" +#import "utils.h" + +@interface MinecraftResourceDownloadTask () +@property AFURLSessionManager* manager; +@property BOOL cancelled; +@end + +@implementation MinecraftResourceDownloadTask + +- (instancetype)init { + self = [super init]; + // TODO: implement background download + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + //backgroundSessionConfigurationWithIdentifier:@"net.kdt.pojavlauncher.downloadtask"]; + self.manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; + self.fileList = [NSMutableArray new]; + self.progressList = [NSMutableArray new]; + return self; +} + +// Add file to the queue +- (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url sha:(NSString *)sha altName:(NSString *)altName toPath:(NSString *)path success:(void (^)())success { + BOOL fileExists = [NSFileManager.defaultManager fileExistsAtPath:path]; + // logSuccess? + if (fileExists && [self checkSHA:sha forFile:path altName:altName]) { + if (success) success(); + return nil; + } else if (![self checkAccessWithDialog:YES]) { + return nil; + } + + NSString *name = altName ?: path.lastPathComponent; + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; + NSURLSessionDownloadTask *task = [self.manager downloadTaskWithRequest:request progress:nil + destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) { + NSLog(@"[MCDL] Downloading %@", name); + [NSFileManager.defaultManager createDirectoryAtPath:path.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil]; + [NSFileManager.defaultManager removeItemAtPath:path error:nil]; + return [NSURL fileURLWithPath:path]; + } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) { + if (self.cancelled) { + // Ignore any further errors + } else if (error != nil) { + [self finishDownloadWithError:error file:name]; + } else if (![self checkSHA:sha forFile:path altName:altName]) { + [self finishDownloadWithErrorString:[NSString stringWithFormat:@"Failed to verify file %@: SHA1 mismatch", path.lastPathComponent]]; + } else { + if (success) success(); + } + }]; + return task; +} + +- (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url sha:(NSString *)sha altName:(NSString *)altName toPath:(NSString *)path { + return [self createDownloadTask:url sha:sha altName:altName toPath:path success:nil]; +} + +- (void)downloadVersionMetadata:(NSDictionary *)version success:(void (^)())success { + // Download base json + NSString *versionStr = version[@"id"]; + if ([versionStr isEqualToString:@"latest-release"]) { + versionStr = getPrefObject(@"internal.latest_version.release"); + } else if ([versionStr isEqualToString:@"latest-snapshot"]) { + versionStr = getPrefObject(@"internal.latest_version.snapshot"); + } + + NSString *path = [NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), versionStr]; + // Find it again to resolve latest-* + version = (id)[MinecraftResourceUtils findVersion:versionStr inList:remoteVersionList]; + + void(^completionBlock)(void) = ^{ + self.verMetadata = parseJSONFromFile(path); + if (self.verMetadata[@"inheritsFrom"]) { + NSMutableDictionary *inheritsFromDict = parseJSONFromFile([NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), self.verMetadata[@"inheritsFrom"]]); + if (inheritsFromDict) { + [MinecraftResourceUtils processVersion:self.verMetadata inheritsFrom:inheritsFromDict]; + } + } + [MinecraftResourceUtils tweakVersionJson:self.verMetadata]; + 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"]; + } else if (json[@"inheritsFrom"]) { + version = (id)[MinecraftResourceUtils findVersion:json[@"inheritsFrom"] inList:remoteVersionList]; + } else { + completionBlock(); + return; + } + } + + versionStr = version[@"id"]; + NSString *url = version[@"url"]; + NSString *sha = url.stringByDeletingLastPathComponent.lastPathComponent; + + NSURLSessionDownloadTask *task = [self createDownloadTask:url sha:sha altName:nil toPath:path success:completionBlock]; + [task resume]; +} + +- (void)downloadAssetMetadataWithSuccess:(void (^)())success { + NSDictionary *assetIndex = self.verMetadata[@"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); + success(); + }]; + [task resume]; +} + +- (NSArray *)downloadClientLibraries { + NSMutableArray *tasks = [NSMutableArray new]; + for (NSDictionary *library in self.verMetadata[@"libraries"]) { + NSString *name = library[@"name"]; + + NSMutableDictionary *artifact = library[@"downloads"][@"artifact"]; + if (artifact == nil && [name containsString:@":"]) { + NSLog(@"[MCDL] Unknown artifact object for %@, attempting to generate one", name); + artifact = [[NSMutableDictionary alloc] init]; + NSString *prefix = library[@"url"] == nil ? @"https://libraries.minecraft.net/" : [library[@"url"] stringByReplacingOccurrencesOfString:@"http://" withString:@"https://"]; + NSArray *libParts = [name componentsSeparatedByString:@":"]; + artifact[@"path"] = [NSString stringWithFormat:@"%1$@/%2$@/%3$@/%2$@-%3$@.jar", [libParts[0] stringByReplacingOccurrencesOfString:@"." withString:@"/"], libParts[1], libParts[2]]; + artifact[@"url"] = [NSString stringWithFormat:@"%@%@", prefix, artifact[@"path"]]; + artifact[@"sha1"] = library[@"checksums"][0]; + } + + NSString *path = [NSString stringWithFormat:@"%s/libraries/%@", getenv("POJAV_GAME_DIR"), artifact[@"path"]]; + NSString *sha = artifact[@"sha1"]; + NSString *url = artifact[@"url"]; + if ([library[@"skip"] boolValue]) { + NSLog(@"[MDCL] Skipped library %@", name); + continue; + } + + 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]; + [tasks addObject:task]; + } else if (self.cancelled) { + return nil; + } + } + return tasks; +} + +- (NSArray *)downloadClientAssets { + NSMutableArray *tasks = [NSMutableArray new]; + NSDictionary *assets = self.verMetadata[@"assetIndexObj"]; + for (NSString *name in assets[@"objects"]) { + NSString *hash = assets[@"objects"][name][@"hash"]; + NSString *pathname = [NSString stringWithFormat:@"%@/%@", [hash substringToIndex:2], hash]; + + NSString *path; + if ([assets[@"map_to_resources"] boolValue]) { + path = [NSString stringWithFormat:@"%s/resources/%@", getenv("POJAV_GAME_DIR"), name]; + } else { + path = [NSString stringWithFormat:@"%s/assets/objects/%@", getenv("POJAV_GAME_DIR"), pathname]; + } + + /* Special case for 1.19+ + * Since 1.19-pre1, setting the window icon on macOS invokes ObjC. + * However, if an IOException occurs, it won't try to set. + * We skip downloading the icon file to workaround this. */ + if ([name hasSuffix:@"/minecraft.icns"]) { + [NSFileManager.defaultManager removeItemAtPath:path error:nil]; + continue; + } + + 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]; + [tasks addObject:task]; + } else if (self.cancelled) { + return nil; + } + } + return tasks; +} + +- (void)downloadVersion:(NSDictionary *)version { + self.cancelled = NO; + self.progress = [NSProgress new]; + [self.fileList removeAllObjects]; + [self.progressList removeAllObjects]; + [self downloadVersionMetadata:version success:^{ + [self downloadAssetMetadataWithSuccess:^{ + NSArray *libTasks = [self downloadClientLibraries]; + NSArray *assetTasks = [self downloadClientAssets]; + self.progress.totalUnitCount = libTasks.count + assetTasks.count; + if (self.progress.totalUnitCount == 0) { + // We have nothing to download, invoke completion observer + self.progress.totalUnitCount = 1; + self.progress.completedUnitCount = 1; + return; + } + [libTasks makeObjectsPerformSelector:@selector(resume)]; + [assetTasks makeObjectsPerformSelector:@selector(resume)]; + [self.verMetadata removeObjectForKey:@"assetIndexObj"]; + }]; + }]; +} + +- (void)finishDownloadWithErrorString:(NSString *)error { + self.cancelled = YES; + [self.manager invalidateSessionCancelingTasks:YES resetSession:YES]; + showDialog(localize(@"Error", nil), error); + self.handleError(); +} + +- (void)finishDownloadWithError:(NSError *)error file:(NSString *)file { + NSString *errorStr = [NSString stringWithFormat:localize(@"launcher.mcl.error_download", NULL), file, error.localizedDescription]; + NSLog(@"[MCDL] Error: %@ %@", errorStr, NSThread.callStackSymbols); + [self finishDownloadWithErrorString:errorStr]; +} + +// Check if the account has permission to download +- (BOOL)checkAccessWithDialog:(BOOL)show { + // for now + BOOL accessible = [BaseAuthenticator.current.authData[@"username"] hasPrefix:@"Demo."] || BaseAuthenticator.current.authData[@"xboxGamertag"] != nil; + if (!accessible) { + self.cancelled = YES; + 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."]; + } + } + return accessible; +} + +// Check SHA of the file +- (BOOL)checkSHAIgnorePref:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName logSuccess:(BOOL)logSuccess { + if (sha == nil) { + // When sha = skip, only check for file existence + BOOL existence = [NSFileManager.defaultManager fileExistsAtPath:path]; + if (existence) { + NSLog(@"[MCDL] Warning: couldn't find SHA for %@, have to assume it's good.", path); + } + return existence; + } + + NSData *data = [NSData dataWithContentsOfFile:path]; + if (data == nil) { + NSLog(@"[MCDL] SHA1 checker: file doesn't exist: %@", altName ? altName : path.lastPathComponent); + return NO; + } + + unsigned char digest[CC_SHA1_DIGEST_LENGTH]; + CC_SHA1(data.bytes, (CC_LONG)data.length, digest); + NSMutableString *localSHA = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; + for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { + [localSHA appendFormat:@"%02x", digest[i]]; + } + + BOOL check = [sha isEqualToString:localSHA]; + if (!check || (getPrefBool(@"general.debug_logging") && logSuccess)) { + NSLog(@"[MCDL] SHA1 %@ for %@%@", + (check ? @"passed" : @"failed"), + (altName ? altName : path.lastPathComponent), + (check ? @"" : [NSString stringWithFormat:@" (expected: %@, got: %@)", sha, localSHA])); + } + return check; +} + +- (BOOL)checkSHA:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName logSuccess:(BOOL)logSuccess { + if (getPrefBool(@"general.check_sha")) { + return [self checkSHAIgnorePref:sha forFile:path altName:altName logSuccess:logSuccess]; + } else { + return [NSFileManager.defaultManager fileExistsAtPath:path]; + } +} + +- (BOOL)checkSHA:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName { + return [self checkSHA:sha forFile:path altName:altName logSuccess:altName==nil]; +} + +@end diff --git a/Natives/MinecraftResourceUtils.h b/Natives/MinecraftResourceUtils.h index abf248dace..05a8307312 100644 --- a/Natives/MinecraftResourceUtils.h +++ b/Natives/MinecraftResourceUtils.h @@ -6,14 +6,10 @@ #define TYPE_OLDBETA 3 #define TYPE_OLDALPHA 4 -typedef void (^MDCallback)(NSString *stage, NSProgress *mainProgress, NSProgress *progress); - @interface MinecraftResourceUtils : NSObject -+ (void)downloadClientJson:(NSObject *)version progress:(NSProgress *)mainProgress callback:(MDCallback)callback success:(void (^)(NSMutableDictionary *json))success; -+ (void)downloadVersion:(NSDictionary *)version callback:(void (^)(NSString *stage, NSProgress *mainProgress, NSProgress *progress))callback; - -+ (void)processJVMArgs:(NSMutableDictionary *)json; ++ (void)processVersion:(NSMutableDictionary *)json inheritsFrom:(NSMutableDictionary *)inheritsFrom; ++ (void)tweakVersionJson:(NSMutableDictionary *)json; + (NSObject *)findVersion:(NSString *)version inList:(NSArray *)list; + (NSObject *)findNearestVersion:(NSObject *)version expectedType:(int)type; diff --git a/Natives/MinecraftResourceUtils.m b/Natives/MinecraftResourceUtils.m index 8db36244b4..0b99d5fa64 100644 --- a/Natives/MinecraftResourceUtils.m +++ b/Natives/MinecraftResourceUtils.m @@ -2,6 +2,7 @@ #import "authenticator/BaseAuthenticator.h" #import "AFNetworking.h" +#import "AFURLSessionOperation.h" #import "LauncherNavigationController.h" #import "LauncherPreferences.h" #import "MinecraftResourceUtils.h" @@ -10,64 +11,6 @@ @implementation MinecraftResourceUtils -static AFURLSessionManager* manager; - -// Check if the account has permission to download -+ (BOOL)checkAccessWithDialog:(BOOL)show { - // for now - BOOL accessible = [BaseAuthenticator.current.authData[@"username"] hasPrefix:@"Demo."] || BaseAuthenticator.current.authData[@"xboxGamertag"] != nil; - if (!accessible && show) { - showDialog(localize(@"Error", nil), @"Minecraft can't be legally installed when logged in with a local account. Please switch to an online account to continue."); - } - return accessible; -} - -// Check SHA of the file -+ (BOOL)checkSHAIgnorePref:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName logSuccess:(BOOL)logSuccess { - if (sha == nil) { - // When sha = skip, only check for file existence - BOOL existence = [NSFileManager.defaultManager fileExistsAtPath:path]; - if (existence) { - NSLog(@"[MCDL] Warning: couldn't find SHA for %@, have to assume it's good.", path); - } - return existence; - } - - NSData *data = [NSData dataWithContentsOfFile:path]; - if (data == nil) { - NSLog(@"[MCDL] SHA1 checker: file doesn't exist: %@", altName ? altName : path.lastPathComponent); - return NO; - } - - unsigned char digest[CC_SHA1_DIGEST_LENGTH]; - CC_SHA1(data.bytes, (CC_LONG)data.length, digest); - NSMutableString *localSHA = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; - for(int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) { - [localSHA appendFormat:@"%02x", digest[i]]; - } - - BOOL check = [sha isEqualToString:localSHA]; - if (!check || (getPrefBool(@"general.debug_logging") && logSuccess)) { - NSLog(@"[MCDL] SHA1 %@ for %@%@", - (check ? @"passed" : @"failed"), - (altName ? altName : path.lastPathComponent), - (check ? @"" : [NSString stringWithFormat:@" (expected: %@, got: %@)", sha, localSHA])); - } - return check; -} - -+ (BOOL)checkSHA:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName logSuccess:(BOOL)logSuccess { - if (getPrefBool(@"general.check_sha")) { - return [self checkSHAIgnorePref:sha forFile:path altName:altName logSuccess:logSuccess]; - } else { - return [NSFileManager.defaultManager fileExistsAtPath:path]; - } -} - -+ (BOOL)checkSHA:(NSString *)sha forFile:(NSString *)path altName:(NSString *)altName { - return [self checkSHA:sha forFile:path altName:altName logSuccess:YES]; -} - // Handle inheritsFrom + (void)processVersion:(NSMutableDictionary *)json inheritsFrom:(NSMutableDictionary *)inheritsFrom { [self insertSafety:inheritsFrom from:json arr:@[ @@ -100,151 +43,6 @@ + (void)processVersion:(NSMutableDictionary *)json inheritsFrom:(NSMutableDictio //inheritsFrom[@"inheritsFrom"] = nil; } -+ (void)processVersion:(NSMutableDictionary *)json inheritsFrom:(id)object progress:(NSProgress *)mainProgress callback:(MDCallback)callback success:(void (^)(NSMutableDictionary *json))success { - [self downloadClientJson:object progress:mainProgress callback:callback success:^(NSMutableDictionary *inheritsFrom){ - [self processVersion:json inheritsFrom:inheritsFrom]; - [self downloadClientJson:inheritsFrom[@"assetIndex"] progress:mainProgress callback:callback success:^(NSMutableDictionary *assetJson){ - inheritsFrom[@"assetIndexObj"] = assetJson; - success(inheritsFrom); - }]; - }]; -} - -// Download the client and assets index file -+ (void)downloadClientJson:(NSObject *)version progress:(NSProgress *)mainProgress callback:(MDCallback)callback success:(void (^)(NSMutableDictionary *json))success { - ++mainProgress.totalUnitCount; - - BOOL isAssetIndex = NO; - NSString *versionStr, *versionURL, *versionSHA; - if ([version isKindOfClass:NSDictionary.class]) { - isAssetIndex = [version valueForKey:@"totalSize"] != nil; - - versionStr = [version valueForKey:@"id"]; - if (!isAssetIndex) { - if ([versionStr isEqualToString:@"latest-release"]) { - versionStr = getPrefObject(@"internal.latest_version.release"); - } else if ([versionStr isEqualToString:@"latest-snapshot"]) { - versionStr = getPrefObject(@"internal.latest_version.snapshot"); - } - // Find it again to resolve latest-* - version = [self findVersion:versionStr inList:remoteVersionList]; - } - versionURL = [version valueForKey:@"url"]; - versionSHA = versionURL.stringByDeletingLastPathComponent.lastPathComponent; - } else { - versionStr = (NSString *)version; - versionSHA = nil; - } - - if (mainProgress) { - callback([NSString stringWithFormat:localize(@"launcher.mcl.downloading_file", nil), versionStr], mainProgress, nil); - } - - NSString *jsonPath; - if (isAssetIndex) { - versionStr = [NSString stringWithFormat:@"assets/indexes/%@", versionStr]; - jsonPath = [NSString stringWithFormat:@"%s/%@.json", getenv("POJAV_GAME_DIR"), versionStr]; - } else { - jsonPath = [NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), versionStr]; - } - - if (![self checkSHA:versionSHA forFile:jsonPath altName:nil]) { - if (![self checkAccessWithDialog:YES]) { - callback(nil, nil, nil); - return; - } - - NSString *verPath = jsonPath.stringByDeletingLastPathComponent; - NSError *error; - [NSFileManager.defaultManager createDirectoryAtPath:verPath withIntermediateDirectories:YES attributes:nil error:&error]; - if (error != nil) { - NSString *errorStr = [NSString stringWithFormat:@"Failed to create directory %@: %@", verPath, error.localizedDescription]; - NSLog(@"[MCDL] Error: %@", errorStr); - showDialog(localize(@"Error", nil), errorStr); - callback(nil, nil, nil); - return; - } - - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:versionURL]]; - - NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull progress){ - callback([NSString stringWithFormat:localize(@"launcher.mcl.downloading_file", nil), jsonPath.lastPathComponent], mainProgress, progress); - } destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) { - [NSFileManager.defaultManager removeItemAtPath:jsonPath error:nil]; - return [NSURL fileURLWithPath:jsonPath]; - } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) { - if (error != nil) { // FIXME: correct? - NSString *errorStr = [NSString stringWithFormat:localize(@"launcher.mcl.error_download", NULL), versionURL, error.localizedDescription]; - NSLog(@"[MCDL] Error: %@ %@", errorStr, NSThread.callStackSymbols); - showDialog(localize(@"Error", nil), errorStr); - callback(nil, nil, nil); - return; - } else { - // A version from the offical server won't likely to have inheritsFrom, so return immediately - if (![self checkSHA:versionSHA forFile:jsonPath altName:nil]) { - // Abort when a downloaded file's SHA mismatches - showDialog(localize(@"Error", nil), [NSString stringWithFormat:@"Failed to verify file %@: SHA1 mismatch", versionStr]); - callback(nil, nil, nil); - return; - } - ++mainProgress.completedUnitCount; - - NSMutableDictionary *json = parseJSONFromFile(jsonPath); - if (isAssetIndex) { - success(json); - return; - } - - [self downloadClientJson:json[@"assetIndex"] progress:mainProgress callback:callback success:^(NSMutableDictionary *assetJson){ - json[@"assetIndexObj"] = assetJson; - success(json); - }]; - } - }]; - [downloadTask resume]; - } else { - NSMutableDictionary *json = parseJSONFromFile(jsonPath); - if (json == nil) { - callback(nil, nil, nil); - return; - } - if (isAssetIndex) { - success(json); - return; - } - if (json[@"inheritsFrom"] == nil) { - if (json[@"assetIndex"] == nil) { - success(json); - return; - } - ++mainProgress.completedUnitCount; - [self downloadClientJson:json[@"assetIndex"] progress:mainProgress callback:callback success:^(NSMutableDictionary *assetJson){ - json[@"assetIndexObj"] = assetJson; - success(json); - }]; - return; - } - - // Find the inheritsFrom - id inheritsFrom = [self findVersion:json[@"inheritsFrom"] inList:remoteVersionList]; - if (inheritsFrom != nil) { - [self processVersion:json inheritsFrom:inheritsFrom progress:mainProgress callback:callback success:success]; - return; - } - - // Try using local json - NSMutableDictionary *inheritsFromDict = parseJSONFromFile([NSString stringWithFormat:@"%1$s/versions/%2$@/%2$@.json", getenv("POJAV_GAME_DIR"), json[@"inheritsFrom"]]); - if (inheritsFromDict != nil) { - [self processVersion:json inheritsFrom:inheritsFromDict]; - success(inheritsFromDict); - return; - } - - // If the inheritsFrom is not found, return an error - showDialog(localize(@"Error", nil), [NSString stringWithFormat:@"Could not find inheritsFrom=%@ for version %@", json[@"inheritsFrom"], versionStr]); - } -} - + (void)insertSafety:(NSMutableDictionary *)targetVer from:(NSDictionary *)fromVer arr:(NSArray *)arr { for (NSString *key in arr) { if (([fromVer[key] isKindOfClass:NSString.class] && [fromVer[key] length] > 0) || targetVer[key] == nil) { @@ -302,230 +100,17 @@ + (void)tweakVersionJson:(NSMutableDictionary *)json { client[@"downloads"][@"artifact"][@"path"] = [NSString stringWithFormat:@"../versions/%1$@/%1$@.jar", json[@"id"]]; client[@"name"] = [NSString stringWithFormat:@"%@.jar", json[@"id"]]; [json[@"libraries"] addObject:client]; -} - -+ (BOOL)downloadClientLibraries:(NSArray *)libraries progress:(NSProgress *)mainProgress callback:(void (^)(NSString *stage, NSProgress *progress))callback { - callback(@"Begin: download libraries", nil); - - __block BOOL cancel = NO; - - dispatch_group_t group = dispatch_group_create(); - - for (NSDictionary *library in libraries) { - if (cancel) { - NSLog(@"[MCDL] Task download libraries is cancelled"); - return NO; - } - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); - NSString *name = library[@"name"]; - - NSMutableDictionary *artifact = library[@"downloads"][@"artifact"]; - if (artifact == nil && [name containsString:@":"]) { - NSLog(@"[MCDL] Unknown artifact object for %@, attempting to generate one", name); - artifact = [[NSMutableDictionary alloc] init]; - NSString *prefix = library[@"url"] == nil ? @"https://libraries.minecraft.net/" : [library[@"url"] stringByReplacingOccurrencesOfString:@"http://" withString:@"https://"]; - NSArray *libParts = [name componentsSeparatedByString:@":"]; - artifact[@"path"] = [NSString stringWithFormat:@"%1$@/%2$@/%3$@/%2$@-%3$@.jar", [libParts[0] stringByReplacingOccurrencesOfString:@"." withString:@"/"], libParts[1], libParts[2]]; - artifact[@"url"] = [NSString stringWithFormat:@"%@%@", prefix, artifact[@"path"]]; - artifact[@"sha1"] = library[@"checksums"][0]; - } - - NSString *path = [NSString stringWithFormat:@"%s/libraries/%@", getenv("POJAV_GAME_DIR"), artifact[@"path"]]; - NSString *sha1 = artifact[@"sha1"]; - NSString *url = artifact[@"url"]; - if ([library[@"skip"] boolValue]) { - callback([NSString stringWithFormat:@"Skipped library %@", name], nil); - ++mainProgress.completedUnitCount; - continue; - } else if ([self checkSHA:sha1 forFile:path altName:nil]) { - ++mainProgress.completedUnitCount; - continue; - } - - if (![self checkAccessWithDialog:YES]) { - callback(nil, nil); - cancel = YES; - continue; - } - - dispatch_group_enter(group); - [NSFileManager.defaultManager createDirectoryAtPath:path.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:nil]; - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; - NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull progress){ - callback([NSString stringWithFormat:localize(@"launcher.mcl.downloading_file", nil), name], progress); - } destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) { - [NSFileManager.defaultManager removeItemAtPath:path error:nil]; - return [NSURL fileURLWithPath:path]; - } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) { - if (error != nil) { - cancel = YES; - NSString *errorStr = [NSString stringWithFormat:localize(@"launcher.mcl.error_download", NULL), url, error.localizedDescription]; - NSLog(@"[MCDL] Error: %@ %@", errorStr, NSThread.callStackSymbols); - showDialog(localize(@"Error", nil), errorStr); - callback(nil, nil); - } else if (![self checkSHA:sha1 forFile:path altName:nil]) { - // Abort when a downloaded file's SHA mismatches - cancel = YES; - showDialog(localize(@"Error", nil), [NSString stringWithFormat:@"Failed to verify file %@: SHA1 mismatch", path.lastPathComponent]); - callback(nil, nil); - } - dispatch_group_leave(group); - ++mainProgress.completedUnitCount; - }]; - [downloadTask resume]; - } - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); - callback(@"Finished: download libraries", nil); - return !cancel; -} -+ (BOOL)downloadClientAssets:(NSDictionary *)assets progress:(NSProgress *)mainProgress callback:(void (^)(NSString *stage, NSProgress *progress))callback { - callback(@"Begin: download assets", nil); - - dispatch_group_t group = dispatch_group_create(); - __block int verifiedCount = 0; - __block int jobsAvailable = 10; - for (NSString *name in assets[@"objects"]) { - if (jobsAvailable < 0) { - break; - } else if (jobsAvailable == 0) { - //dispatch_group_wait(group, DISPATCH_TIME_FOREVER); - while (jobsAvailable == 0) { - usleep(1000); - } - } - - NSString *hash = assets[@"objects"][name][@"hash"]; - NSString *pathname = [NSString stringWithFormat:@"%@/%@", [hash substringToIndex:2], hash]; - - NSString *path; - if ([assets[@"map_to_resources"] boolValue]) { - path = [NSString stringWithFormat:@"%s/resources/%@", getenv("POJAV_GAME_DIR"), name]; - } else { - path = [NSString stringWithFormat:@"%s/assets/objects/%@", getenv("POJAV_GAME_DIR"), pathname]; - } - - /* Special case for 1.19+ - * Since 1.19-pre1, setting the window icon on macOS goes through ObjC. - * However, if an IOException occurs, it won't try to set. - * We skip downloading the icon file to trigger this. */ - if ([name hasSuffix:@"/minecraft.icns"]) { - [NSFileManager.defaultManager removeItemAtPath:path error:nil]; - continue; - } - - --jobsAvailable; - dispatch_group_enter(group); - BOOL fileExists = [NSFileManager.defaultManager fileExistsAtPath:path]; - if (fileExists && [self checkSHA:hash forFile:path altName:name logSuccess:NO]) { - // update progress for each +10% - if (++verifiedCount % (mainProgress.totalUnitCount/10) == 0) { - mainProgress.completedUnitCount = verifiedCount; - } - ++jobsAvailable; - dispatch_group_leave(group); - continue; - } - - if (![self checkAccessWithDialog:NO]) { - dispatch_group_leave(group); - break; - } - - NSError *err; - [NSFileManager.defaultManager createDirectoryAtPath:path.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:&err]; - //NSLog(@"path %@ err %@", path, err); - usleep(1000); // avoid overloading queue - NSString *url = [NSString stringWithFormat:@"https://resources.download.minecraft.net/%@", pathname]; - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; - NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull progress){ - callback([NSString stringWithFormat:localize(@"launcher.mcl.downloading_file", nil), name], progress); - } destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) { - [NSFileManager.defaultManager removeItemAtPath:path error:nil]; - return [NSURL fileURLWithPath:path]; - } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) { - if (error != nil) { - if (jobsAvailable < 0) { - dispatch_group_leave(group); - return; - } - jobsAvailable = -3; - NSString *errorStr = [NSString stringWithFormat:localize(@"launcher.mcl.error_download", NULL), url, error.localizedDescription]; - NSLog(@"[MCDL] Error: %@ %@", errorStr, NSThread.callStackSymbols); - showDialog(localize(@"Error", nil), errorStr); - callback(nil, nil); - } else if (![self checkSHA:hash forFile:path altName:name logSuccess:NO]) { - // Abort when a downloaded file's SHA mismatches - if (jobsAvailable < 0) { - dispatch_group_leave(group); - return; - } - jobsAvailable = -2; - showDialog(localize(@"Error", nil), [NSString stringWithFormat:@"Failed to verify file %@: SHA1 mismatch", path.lastPathComponent]); - callback(nil, nil); - } - ++verifiedCount; - ++jobsAvailable; - mainProgress.completedUnitCount = verifiedCount; - dispatch_group_leave(group); - }]; - [downloadTask resume]; - } - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); - callback(@"Finished: download assets", nil); - if (getPrefBool(@"general.check_sha")) { - NSLog(@"[MCDL] SHA1 passed for %d/%d asset files", verifiedCount, [assets[@"objects"] count]); - if ([assets[@"objects"] count] - verifiedCount < 3) { - NSLog(@"Note: some files of 1.19+ are skipped to workaround an issue"); - } - } - return jobsAvailable != -1; -} - -+ (void)downloadVersion:(NSDictionary *)version callback:(MDCallback)callback { - manager = [[AFURLSessionManager alloc] init]; - NSProgress *mainProgress = [NSProgress progressWithTotalUnitCount:0]; - [self downloadClientJson:version progress:mainProgress callback:callback success:^(NSMutableDictionary *json) { - [self tweakVersionJson:json]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - BOOL success; - - mainProgress.totalUnitCount = [json[@"libraries"] count] + [json[@"assetIndexObj"][@"objects"] count]; - id wrappedCallback = ^(NSString *s, NSProgress *p) { - dispatch_async(dispatch_get_main_queue(), ^{ - callback(s, s?mainProgress:nil, p); - }); - }; - - success = [self downloadClientLibraries:json[@"libraries"] progress:mainProgress callback:wrappedCallback]; - if (!success) return; - - success = [self downloadClientAssets:json[@"assetIndexObj"] progress:mainProgress callback:wrappedCallback]; - if (!success) return; - - manager = nil; - - dispatch_async(dispatch_get_main_queue(), ^{ - callback(nil, mainProgress, nil); - }); - }); - }]; -} - -+ (void)processJVMArgs:(NSMutableDictionary *)json { // Parse Forge 1.17+ additional JVM Arguments if (json[@"inheritsFrom"] == nil || json[@"arguments"][@"jvm"] == nil) { return; } - json[@"arguments"][@"jvm_processed"] = [[NSMutableArray alloc] init]; - NSDictionary *varArgMap = @{ @"${classpath_separator}": @":", @"${library_directory}": [NSString stringWithFormat:@"%s/libraries", getenv("POJAV_GAME_DIR")], @"${version_name}": json[@"id"] }; - for (id arg in json[@"arguments"][@"jvm"]) { if ([arg isKindOfClass:NSString.class]) { NSString *argStr = arg; diff --git a/Natives/SurfaceViewController.h b/Natives/SurfaceViewController.h index 6ff9c77e3d..0e25eabfce 100644 --- a/Natives/SurfaceViewController.h +++ b/Natives/SurfaceViewController.h @@ -20,6 +20,7 @@ CGPoint lastVirtualMousePoint; @property(nonatomic) UIView* rootView; +- (instancetype)initWithMetadata:(NSDictionary *)metadata; - (void)sendTouchPoint:(CGPoint)location withEvent:(int)event; - (void)updateSavedResolution; - (void)updateGrabState; diff --git a/Natives/SurfaceViewController.m b/Natives/SurfaceViewController.m index 19a8b4d127..e6b40f7c90 100644 --- a/Natives/SurfaceViewController.m +++ b/Natives/SurfaceViewController.m @@ -34,6 +34,8 @@ @interface SurfaceViewController () { } +@property(nonatomic) NSDictionary* verMetadata; + @property(nonatomic) TrackedTextField *inputTextField; @property(nonatomic) NSMutableArray* swipeableButtons; @property(nonatomic) ControlButton* swipingButton; @@ -59,6 +61,12 @@ @interface SurfaceViewController () + application-identifier + net.kdt.pojavlauncher get-task-allow @@ -34,6 +36,12 @@ AGXCommandQueue AGXDevice + com.apple.security.exception.mach-lookup.global-name + + com.apple.nsurlsessiond + com.apple.nsurlsessiond.NSURLSessionProxyService + com.apple.nsurlstorage-cache + platform-application