#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]; NSMutableArray *sizes = [NSMutableArray new]; [response enumerateObjectsUsingBlock: ^(NSDictionary *version, NSUInteger i, BOOL *stop) { NSDictionary *file = [version[@"files"] firstObject]; mcNames[i] = [version[@"game_versions"] firstObject]; sizes[i] = file[@"size"]; urls[i] = file[@"url"]; NSDictionary *hashesMap = file[@"hashes"]; hashes[i] = hashesMap[@"sha1"] ?: [NSNull null]; }]; item[@"versionNames"] = names; item[@"mcVersionNames"] = mcNames; item[@"versionSizes"] = sizes; 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"]]; NSUInteger size = [indexFile[@"fileSize"] unsignedLongLongValue]; NSURLSessionDownloadTask *task = [downloader createDownloadTask:url size:size sha:sha altName:nil toPath:path]; if (task) { [downloader.fileList addObject:indexFile[@"path"]]; [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"] size:0 sha:nil altName:nil toPath:jsonPath]; [task resume]; } // TODO: automation for Forge // Create profile NSString *tmpIconPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"icon.png"]; PLProfiles.current.profiles[indexDict[@"name"]] = @{ @"gameDir": [NSString stringWithFormat:@"./custom_gamedir/%@", destPath.lastPathComponent], @"name": indexDict[@"name"], @"lastVersionId": depInfo[@"id"], @"icon": [NSString stringWithFormat:@"data:image/png;base64,%@", [[NSData dataWithContentsOfFile:tmpIconPath] base64EncodedStringWithOptions:0]] }.mutableCopy; PLProfiles.current.selectedProfileName = indexDict[@"name"]; } @end