594 lines
27 KiB
Objective-C
594 lines
27 KiB
Objective-C
#include <CommonCrypto/CommonDigest.h>
|
|
|
|
#import "authenticator/BaseAuthenticator.h"
|
|
#import "AFNetworking.h"
|
|
#import "LauncherNavigationController.h"
|
|
#import "LauncherPreferences.h"
|
|
#import "MinecraftResourceUtils.h"
|
|
#import "ios_uikit_bridge.h"
|
|
#import "utils.h"
|
|
|
|
@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:@[
|
|
@"assetIndex", @"assets", @"id",
|
|
@"inheritsFrom",
|
|
@"mainClass", @"minecraftArguments",
|
|
@"optifineLib", @"releaseTime", @"time", @"type"
|
|
]];
|
|
inheritsFrom[@"arguments"] = json[@"arguments"];
|
|
|
|
for (NSMutableDictionary *lib in json[@"libraries"]) {
|
|
NSString *libName = [lib[@"name"] substringToIndex:[lib[@"name"] rangeOfString:@":" options:NSBackwardsSearch].location];
|
|
int i;
|
|
for (i = 0; i < [inheritsFrom[@"libraries"] count]; i++) {
|
|
NSMutableDictionary *libAdded = inheritsFrom[@"libraries"][i];
|
|
NSString *libAddedName = [libAdded[@"name"] substringToIndex:[libAdded[@"name"] rangeOfString:@":" options:NSBackwardsSearch].location];
|
|
|
|
if ([libAdded[@"name"] hasPrefix:libName]) {
|
|
inheritsFrom[@"libraries"][i] = lib;
|
|
i = -1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i != -1) {
|
|
[inheritsFrom[@"libraries"] addObject:lib];
|
|
}
|
|
}
|
|
|
|
//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$@/%[email protected]", 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$@/%[email protected]", 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) {
|
|
targetVer[key] = fromVer[key];
|
|
} else {
|
|
NSLog(@"[MCDL] insertSafety: how to insert %@?", key);
|
|
}
|
|
}
|
|
}
|
|
|
|
+ (void)tweakVersionJson:(NSMutableDictionary *)json {
|
|
// Exclude some libraries
|
|
for (NSMutableDictionary *library in json[@"libraries"]) {
|
|
library[@"skip"] = @(
|
|
// Exclude platform-dependant libraries
|
|
library[@"downloads"][@"classifiers"] != nil ||
|
|
library[@"natives"] != nil ||
|
|
// Exclude LWJGL libraries
|
|
[library[@"name"] hasPrefix:@"org.lwjgl"]
|
|
);
|
|
|
|
NSString *versionStr = [library[@"name"] componentsSeparatedByString:@":"][2];
|
|
NSArray<NSString *> *version = [versionStr componentsSeparatedByString:@"."];
|
|
if ([library[@"name"] hasPrefix:@"net.java.dev.jna:jna:"]) {
|
|
// Special handling for LabyMod 1.8.9 and Forge 1.12.2(?)
|
|
// we have libjnidispatch 5.13.0 in Frameworks directory
|
|
if (!(version[0].intValue < 5 || version[1].intValue < 13 || version[2].intValue < 1)) {
|
|
NSLog(@"[MCDL] Warning: JNA version required by %@ is %@ > 5.13.0, compatibility might be broken.", json[@"id"], versionStr);
|
|
}
|
|
library[@"name"] = @"net.java.dev.jna:jna:5.13.0";
|
|
library[@"downloads"][@"artifact"][@"path"] = @"net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar";
|
|
library[@"downloads"][@"artifact"][@"url"] = @"https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar";
|
|
library[@"downloads"][@"artifact"][@"sha1"] = @"1200e7ebeedbe0d10062093f32925a912020e747";
|
|
} else if ([library[@"name"] hasPrefix:@"org.ow2.asm:asm-all:"]) {
|
|
// Early versions of the ASM library get repalced with 5.0.4 because Pojav's LWJGL is compiled for
|
|
// Java 8, which is not supported by old ASM versions. Mod loaders like Forge, which depend on this
|
|
// library, often include lwjgl in their class transformations, which causes errors with old ASM versions.
|
|
if(version[0].intValue >= 5) continue;
|
|
library[@"name"] = @"org.ow2.asm:asm-all:5.0.4";
|
|
library[@"downloads"][@"artifact"][@"path"] = @"org/ow2/asm/asm-all/5.0.4/asm-all-5.0.4.jar";
|
|
library[@"downloads"][@"artifact"][@"sha1"] = @"e6244859997b3d4237a552669279780876228909";
|
|
library[@"downloads"][@"artifact"][@"url"] = @"https://repo1.maven.org/maven2/org/ow2/asm/asm-all/5.0.4/asm-all-5.0.4.jar";
|
|
}
|
|
}
|
|
|
|
// Add the client as a library
|
|
NSMutableDictionary *client = [[NSMutableDictionary alloc] init];
|
|
client[@"downloads"] = [[NSMutableDictionary alloc] init];
|
|
if (json[@"downloads"][@"client"] == nil) {
|
|
client[@"downloads"][@"artifact"] = [[NSMutableDictionary alloc] init];
|
|
client[@"skip"] = @YES;
|
|
} else {
|
|
client[@"downloads"][@"artifact"] = json[@"downloads"][@"client"];
|
|
}
|
|
client[@"downloads"][@"artifact"][@"path"] = [NSString stringWithFormat:@"../versions/%1$@/%[email protected]", 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$@-%[email protected]", [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;
|
|
for (NSString *key in varArgMap.allKeys) {
|
|
argStr = [argStr stringByReplacingOccurrencesOfString:key withString:varArgMap[key]];
|
|
}
|
|
[json[@"arguments"][@"jvm_processed"] addObject:argStr];
|
|
}
|
|
}
|
|
}
|
|
|
|
+ (NSObject *)findVersion:(NSString *)version inList:(NSArray *)list {
|
|
return [list filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"(id == %@)", version]].firstObject;
|
|
}
|
|
|
|
+ (NSObject *)findNearestVersion:(NSObject *)version expectedType:(int)type {
|
|
if (type != TYPE_RELEASE && type != TYPE_SNAPSHOT) {
|
|
// Only support finding for releases and snapshot for now
|
|
return nil;
|
|
}
|
|
|
|
if ([version isKindOfClass:NSString.class]){
|
|
// Find in inheritsFrom
|
|
NSDictionary *versionDict = parseJSONFromFile([NSString stringWithFormat:@"%1$s/versions/%2$@/%[email protected]", getenv("POJAV_GAME_DIR"), version]);
|
|
NSAssert(versionDict != nil, @"version should not be null");
|
|
if (versionDict[@"inheritsFrom"] == nil) {
|
|
// How then?
|
|
return nil;
|
|
}
|
|
NSObject *inheritsFrom = [self findVersion:versionDict[@"inheritsFrom"] inList:remoteVersionList];
|
|
if (type == TYPE_RELEASE) {
|
|
return inheritsFrom;
|
|
} else if (type == TYPE_SNAPSHOT) {
|
|
return [self findNearestVersion:inheritsFrom expectedType:type];
|
|
}
|
|
}
|
|
|
|
NSString *versionType = [version valueForKey:@"type"];
|
|
int index = [remoteVersionList indexOfObject:(NSDictionary *)version];
|
|
if ([versionType isEqualToString:@"release"] && type == TYPE_SNAPSHOT) {
|
|
// Returns the (possible) latest snapshot for the version
|
|
NSDictionary *result = remoteVersionList[index + 1];
|
|
// Sometimes, a release is followed with another release (1.16->1.16.1), go lower in this case
|
|
if ([result[@"type"] isEqualToString:@"release"]) {
|
|
return [self findNearestVersion:result expectedType:type];
|
|
}
|
|
return result;
|
|
} else if ([versionType isEqualToString:@"snapshot"] && type == TYPE_RELEASE) {
|
|
while (remoteVersionList.count > abs(index)) {
|
|
// In case the snapshot has yet attached to a release, perform a reverse find
|
|
NSDictionary *result = remoteVersionList[abs(index)];
|
|
// Returns the corresponding release for the snapshot, or latest release if none found
|
|
if ([result[@"type"] isEqualToString:@"release"]) {
|
|
return result;
|
|
}
|
|
// Continue to decrement, later abs() it
|
|
index--;
|
|
}
|
|
}
|
|
|
|
// No idea on handling everything else
|
|
return nil;
|
|
}
|
|
|
|
@end
|