diff --git a/xmcl-runtime/modpack/ModpackService.ts b/xmcl-runtime/modpack/ModpackService.ts index ef0179020..c8be41c3d 100644 --- a/xmcl-runtime/modpack/ModpackService.ts +++ b/xmcl-runtime/modpack/ModpackService.ts @@ -1,5 +1,5 @@ -import { CurseforgeModpackManifest, EditInstanceOptions, ExportModpackOptions, ModpackService as IModpackService, InstanceFile, McbbsModpackManifest, ModpackException, ModpackInstallProfile, ModpackServiceKey, ModrinthModpackManifest, ResourceMetadata, RuntimeVersions, getCurseforgeModpackFromInstance, getMcbbsModpackFromInstance, getModrinthModpackFromInstance, isAllowInModrinthModpack } from '@xmcl/runtime-api' -import { open, openEntryReadStream, readAllEntries } from '@xmcl/unzip' +import { CurseforgeModpackManifest, ExportModpackOptions, ModpackService as IModpackService, InstanceFile, McbbsModpackManifest, ModpackException, ModpackInstallProfile, ModpackServiceKey, ModrinthModpackManifest, ResourceMetadata, getCurseforgeModpackFromInstance, getMcbbsModpackFromInstance, getModrinthModpackFromInstance, isAllowInModrinthModpack } from '@xmcl/runtime-api' +import { open, readAllEntries } from '@xmcl/unzip' import { stat } from 'fs-extra' import { join } from 'path' import { Entry, ZipFile } from 'yauzl' @@ -9,7 +9,6 @@ import { ResourceService, ResourceWorker, kResourceWorker } from '~/resource' import { AbstractService, ExposeServiceKey } from '~/service' import { TaskFn, kTaskExecutor } from '~/task' import { AnyError } from '../util/error' -import { checksumFromStream } from '../util/fs' import { requireObject } from '../util/object' import { ZipTask } from '../util/zip' @@ -36,9 +35,11 @@ export interface ModpackHandler { */ resolveUnpackPath(manifest: M, e: Entry): string | void - readMetadata(zipFile: ZipFile, entries: Entry[]): Promise + readManifest(zipFile: ZipFile, entries: Entry[]): Promise resolveInstanceOptions(manifest: M): ModpackInstallProfile['instance'] resolveInstanceFiles(manifest: M): Promise + + resolveModpackMetadata?(path: string, sha1: string): Promise } /** @@ -206,6 +207,8 @@ export class ModpackService extends AbstractService implements IModpackService { if (!manifest || !handler) throw new ModpackException({ type: 'invalidModpack', path: modpackFile }) + const metadata = await handler.resolveModpackMetadata?.(modpackFile, cacheOrHash) || {} + const instance = handler.resolveInstanceOptions(manifest) const instanceFiles = await handler.resolveInstanceFiles(manifest) @@ -222,6 +225,7 @@ export class ModpackService extends AbstractService implements IModpackService { }, downloads: [ `zip:///${modpackFile}?entry=${encodeURIComponent(e.fileName)}`, + `zip://${cacheOrHash}/${e.fileName}`, ], } return file @@ -239,6 +243,7 @@ export class ModpackService extends AbstractService implements IModpackService { await this.resourceService.updateResources([{ hash: cacheOrHash, metadata: { + ...metadata, instance: { instance, files, @@ -252,73 +257,9 @@ export class ModpackService extends AbstractService implements IModpackService { return files } - async getModpackInstallProfile(path: string): Promise { - const cacheOrHash = await this.getCachedInstallProfile(path) - - if (typeof cacheOrHash === 'object') return cacheOrHash - - this.log(`Parse modpack profile ${path}`) - - const zip = await open(path) - const entries = await readAllEntries(zip) - - const [manifest, handler] = await this.getManifestAndHandler(zip, entries) - - if (!manifest || !handler) throw new ModpackException({ type: 'invalidModpack', path }) - - const instance = handler.resolveInstanceOptions(manifest) - - const instanceFiles = await handler.resolveInstanceFiles(manifest) - - const files = (await Promise.all(entries - .filter((e) => !!handler.resolveUnpackPath(manifest, e) && !e.fileName.endsWith('/')) - .map(async (e) => { - const relativePath = handler.resolveUnpackPath(manifest, e)! - const file: InstanceFile = { - path: relativePath, - size: e.uncompressedSize, - hashes: { - crc32: e.crc32.toString(), - }, - downloads: [ - `zip:///${path}?entry=${encodeURIComponent(e.fileName)}`, - `zip://${cacheOrHash}/${e.fileName}`, - ], - } - return file - }))) - .concat(instanceFiles) - .filter(f => !f.path.endsWith('/')) - - for (const file of files) { - transformFile(file) - } - - try { - // Update the resource - this.log(`Update instance resource modpack profile ${path}`) - await this.resourceService.updateResources([{ - hash: cacheOrHash, - metadata: { - instance: { - instance, - files, - }, - }, - }]) - } catch (e) { - this.error(new AnyError('ModpackInstallProfileError', 'Fail to update resource', { cause: e })) - } - - return { - instance, - files, - } - } - private async getManifestAndHandler(zip: ZipFile, entries: Entry[]) { for (const handler of Object.values(this.handlers)) { - const manifest = await handler.readMetadata(zip, entries).catch(e => undefined) + const manifest = await handler.readManifest(zip, entries).catch(e => undefined) if (manifest) { return [manifest, handler] as const } diff --git a/xmcl-runtime/modpack/pluginCurseforgeModpackHandler.ts b/xmcl-runtime/modpack/pluginCurseforgeModpackHandler.ts index 13f4a955c..38c05cb60 100644 --- a/xmcl-runtime/modpack/pluginCurseforgeModpackHandler.ts +++ b/xmcl-runtime/modpack/pluginCurseforgeModpackHandler.ts @@ -8,10 +8,21 @@ import { guessCurseforgeFileUrl } from '../util/curseforge' import { ModpackService } from './ModpackService' import { getCurseforgeFiles, getCurseforgeProjects } from './getCurseforgeFiles' import { resolveHashes } from './resolveHashes' +import { kResourceWorker } from '~/resource' export const pluginCurseforgeModpackHandler: LauncherAppPlugin = async (app) => { const modpackService = await app.registry.get(ModpackService) modpackService.registerHandler('curseforge', { + async resolveModpackMetadata(path, sha1) { + const client = await app.registry.getOrCreate(CurseforgeV1Client) + const worker = await app.registry.getOrCreate(kResourceWorker) + + const print = await worker.fingerprint(path) + const result = await client.getFingerprintsMatchesByGameId(432, [print]) + const f = result.exactMatches[0] + if (!f) return undefined + return { curseforge: { projectId: f.file.modId, fileId: f.file.id } } + }, resolveUnpackPath: function (manifest: CurseforgeModpackManifest, e: Entry) { let overridePrefix = manifest.overrides ?? 'overrides/' if (!overridePrefix.endsWith('/')) overridePrefix += '/' @@ -19,7 +30,7 @@ export const pluginCurseforgeModpackHandler: LauncherAppPlugin = async (app) => return e.fileName.substring(overridePrefix.length) } }, - readMetadata: async (zipFile: ZipFile, entries: Entry[]): Promise => { + readManifest: async (zipFile: ZipFile, entries: Entry[]): Promise => { const curseforgeManifest = entries.find(e => e.fileName === 'manifest.json') if (curseforgeManifest) { const b = await readEntry(zipFile, curseforgeManifest) diff --git a/xmcl-runtime/modpack/pluginMcbbsModpackHandler.ts b/xmcl-runtime/modpack/pluginMcbbsModpackHandler.ts index 42931d90e..ca02da48b 100644 --- a/xmcl-runtime/modpack/pluginMcbbsModpackHandler.ts +++ b/xmcl-runtime/modpack/pluginMcbbsModpackHandler.ts @@ -12,7 +12,7 @@ import { resolveHashes } from './resolveHashes' export const pluginMcbbsModpackHandler: LauncherAppPlugin = async (app) => { const modpackService = await app.registry.get(ModpackService) modpackService.registerHandler('mcbbs', { - readMetadata: async (zip, entries) => { + readManifest: async (zip, entries) => { const mcbbsManifest = entries.find(e => e.fileName === 'mcbbs.packmeta') if (mcbbsManifest) { return readEntry(zip, mcbbsManifest).then(b => JSON.parse(b.toString()) as McbbsModpackManifest) diff --git a/xmcl-runtime/modpack/pluginModrinthModpackHandler.ts b/xmcl-runtime/modpack/pluginModrinthModpackHandler.ts index eb6d80214..bb8b81582 100644 --- a/xmcl-runtime/modpack/pluginModrinthModpackHandler.ts +++ b/xmcl-runtime/modpack/pluginModrinthModpackHandler.ts @@ -3,11 +3,27 @@ import { LauncherAppPlugin } from '~/app' import { ModpackService } from './ModpackService' import { readEntry } from '@xmcl/unzip' import { Entry } from 'yauzl' +import { ModrinthV2Client } from '@xmcl/modrinth' export const pluginModrinthModpackHandler: LauncherAppPlugin = async (app) => { const modpackService = await app.registry.get(ModpackService) modpackService.registerHandler('modrinth', { - async readMetadata(zip, entries) { + async resolveModpackMetadata(path, sha1) { + const client = await app.registry.getOrCreate(ModrinthV2Client) + const hashes = await client.getProjectVersionsByHash([sha1], 'sha1') + const content = hashes[sha1] + if (!content) return undefined + const file = content.files.find(f => f.hashes.sha1 === sha1) + return { + modrinth: { + projectId: content.project_id, + versionId: content.id, + filename: file?.filename, + url: file?.url, + }, + } + }, + async readManifest(zip, entries) { const modrinthManifest = entries.find(e => e.fileName === 'modrinth.index.json') if (modrinthManifest) { const b = await readEntry(zip, modrinthManifest)