// __ __ __ __ ______ __ __ ______ __ __ ______ ______ // /\ \ /\ \ /\ "-.\ \ /\ ___\ /\ \/ / /\ ___\ /\ "-.\ \ /\ ___\ /\__ _\ // \ \ \____ \ \ \ \ \ \-. \ \ \ \__ \ \ \ _"-. \ \ __\ \ \ \-. \ \ \ __\ \/_/\ \/ // \ \_____\ \ \_\ \ \_\\"\_\ \ \_____\ \ \_\ \_\ \ \_____\ \ \_\\"\_\ \ \_____\ \ \_\ // \/_____/ \/_/ \/_/ \/_/ \/_____/ \/_/\/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/ // // 所有权利归Lingke Network (china)所有 | dream_pep拥有其艺术改变权力 // 未经允许的情况下删除此版权头可能会受到民事指控 // 请勿在未经Lingke Network (china)允许的范围内修改代码并分发 import * as path from 'path'; import { AbortableTask, CancelledError, task, type Task } from '@xmcl/task'; import { installTask, installDependenciesTask, installForgeTask, installNeoForgedTask, installFabric, installQuiltVersion, getVersionList, } from '@xmcl/installer'; import { Version } from '@xmcl/core'; import { rewriteToMirror } from './installer'; import { createInstance, updateInstance, BMCLAPI_VERSION_MANIFEST, BMCLAPI_MAVEN, BMCLAPI_ASSETS, mirrorFetch, type InstanceRuntime, } from './instance'; import { resolveGamePath } from './paths'; // 任务执行钩子:主进程用它向渲染进程广播日志 export interface TaskHooks { log: (level: 'info' | 'warn' | 'error', message: string) => void; } // 实例安装任务参数(与前端 addSidecarTask 的 params 对齐) export interface InstallTaskParams { name: string; gamePath: string; runtime: InstanceRuntime; description?: string; } // 模拟任务参数(调试页使用) export interface SimTaskParams { duration?: number; failAt?: number; failMessage?: string; total?: number; threads?: number; steps?: number; } // 执行器工厂:根据参数与钩子创建 @xmcl/task 任务 type ExecutorFactory = (params: Record, hooks: TaskHooks) => Task; // 执行器注册表:executorName → 工厂 const executorRegistry = new Map(); // ==================== 模拟执行器(调试用)==================== // 模拟任务基类:AbortableTask 子类,支持取消 / 暂停 / 进度 abstract class SimTask extends AbortableTask { protected aborted = false; protected stepIndex = 0; protected stepTotal = 20; // 进度展示:已推进的步数 / 总步数 override get progress(): number { return this.stepIndex; } override get total(): number { return this.stepTotal; } // 每个步骤推进一次进度并抛出日志 protected step(hooks: TaskHooks, message?: string): void { this.stepIndex += 1; if (message) hooks.log('info', message); } // 等待恢复:暂停期间每 50ms 轮询一次,直到恢复或取消 protected async waitResume(): Promise { while (this.isPaused && !this.aborted) { await new Promise((r) => setTimeout(r, 50)); } if (this.aborted) throw new CancelledError(); } // 取消回调:标记中止标志 protected abort(): void { this.aborted = true; } // 判断错误是否为取消导致 protected isAbortedError(e: unknown): boolean { return e instanceof CancelledError || this.aborted; } } // sleep:按毫秒数模拟耗时任务 class SleepTask extends SimTask { constructor( private readonly durationMs: number, private readonly failAt: number, private readonly failMessage: string | undefined, private readonly hooks: TaskHooks, ) { super(); } override async process(): Promise { const interval = this.durationMs / this.stepTotal; for (let i = 0; i < this.stepTotal; i++) { await this.waitResume(); this.step(this.hooks, `进度 ${Math.round(((i + 1) / this.stepTotal) * 100)}%`); if (i + 1 === this.failAt) { this.hooks.log('error', this.failMessage ?? '模拟失败:网络连接超时'); throw new Error(this.failMessage ?? '网络连接超时'); } await new Promise((r) => setTimeout(r, interval)); } this.hooks.log('info', '任务完成'); return this.stepIndex; } } // download:模拟分段下载(total 单位,threads 并发) class DownloadSimTask extends SimTask { private done = 0; constructor( private readonly totalUnits: number, private readonly threads: number, private readonly hooks: TaskHooks, ) { super(); this.stepTotal = totalUnits; } override get progress(): number { return this.done; } override get total(): number { return this.totalUnits; } override async process(): Promise { const chunk = Math.max(1, Math.round(this.totalUnits / this.threads)); while (this.done < this.totalUnits) { await this.waitResume(); this.done = Math.min(this.totalUnits, this.done + chunk); this.hooks.log('info', `已下载 ${this.done}/${this.totalUnits}`); await new Promise((r) => setTimeout(r, 120)); } this.hooks.log('info', '下载完成'); return this.done; } } // install-sim:模拟多步骤安装(steps 步) class InstallSimTask extends SimTask { constructor( private readonly installSteps: number, private readonly hooks: TaskHooks, ) { super(); this.stepTotal = installSteps; } override async process(): Promise { for (let i = 1; i <= this.installSteps; i++) { await this.waitResume(); this.step(this.hooks, `安装步骤 ${i}/${this.installSteps}`); await new Promise((r) => setTimeout(r, 150)); } this.hooks.log('info', '安装完成'); return this.stepIndex; } } // 注册模拟执行器 executorRegistry.set('sleep', (raw, hooks) => { const p = raw as unknown as SimTaskParams; return new SleepTask(p.duration ?? 3000, p.failAt ?? -1, p.failMessage, hooks); }); executorRegistry.set('download', (raw, hooks) => { const p = raw as unknown as SimTaskParams; return new DownloadSimTask(p.total ?? 100, p.threads ?? 4, hooks); }); executorRegistry.set('install-sim', (raw, hooks) => { const p = raw as unknown as SimTaskParams; return new InstallSimTask(p.steps ?? 10, hooks); }); // ==================== 真实执行器(实例安装)==================== // install:使用 @xmcl/installer 的 Task 版本函数安装 Minecraft 实例 // 任务树:install.create → install.minecraft → (forge|neoforge|fabric|quilt) → install.dependencies executorRegistry.set('install', (rawParams, hooks) => { const p = rawParams as unknown as InstallTaskParams; const { name, runtime } = p; // 相对 gameDir 归一化(与 runStartupChecks 基准一致) const gamePath = resolveGamePath(p.gamePath); const instancePath = path.join(gamePath, 'instances', name); return task('install', async function () { // 子任务 1:创建实例目录 hooks.log('info', `创建实例: ${name}`); await this.yield( task('create', async () => { await createInstance(name, gamePath, runtime, { description: p.description }); }), ); // 预步骤:获取版本清单,定位 Minecraft 版本元数据(走 BMCLAPI 镜像) const versionList = await getVersionList({ remote: BMCLAPI_VERSION_MANIFEST, fetch: mirrorFetch }); const versionInfo = versionList.versions.find((v) => v.id === runtime.minecraft); if (!versionInfo) { throw new Error(`Minecraft 版本 ${runtime.minecraft} 不存在`); } // 子任务 2:安装 Minecraft 本体(版本 JSON / 客户端 JAR) hooks.log('info', `下载 Minecraft ${runtime.minecraft}...`); await this.yield( installTask( { id: versionInfo.id, url: versionInfo.url }, instancePath, { // 版本 JSON / 客户端 JAR 走 BMCLAPI 镜像 json: (v) => rewriteToMirror(v.url), client: (v) => (v.downloads?.client ? rewriteToMirror(v.downloads.client.url) : []), mavenHost: BMCLAPI_MAVEN, assetsHost: BMCLAPI_ASSETS, }, ).setName('minecraft'), ); // 子任务 3:模组加载器(Forge / NeoForge 有 Task 版本;Fabric / Quilt 为 Promise) if (runtime.forge) { hooks.log('info', `安装 Forge ${runtime.forge}...`); await this.yield( installForgeTask( { version: runtime.forge, mcversion: runtime.minecraft }, instancePath, { mavenHost: BMCLAPI_MAVEN }, ).setName('forge'), ); } if (runtime.fabricLoader) { hooks.log('info', `安装 Fabric ${runtime.fabricLoader}...`); await installFabric({ minecraftVersion: runtime.minecraft, version: runtime.fabricLoader, minecraft: instancePath, fetch: mirrorFetch, }); } if (runtime.quiltLoader) { hooks.log('info', `安装 Quilt ${runtime.quiltLoader}...`); await installQuiltVersion({ minecraftVersion: runtime.minecraft, version: runtime.quiltLoader, minecraft: instancePath, }); } if (runtime.neoForged) { hooks.log('info', `安装 NeoForge ${runtime.neoForged}...`); await this.yield( installNeoForgedTask('neoforge', runtime.neoForged, instancePath, { mavenHost: BMCLAPI_MAVEN }).setName('neoforge'), ); } // 子任务 4:安装依赖(libraries + assets) hooks.log('info', '安装依赖...'); const resolved = await Version.parse(instancePath, runtime.minecraft); await this.yield( installDependenciesTask(resolved, { mavenHost: BMCLAPI_MAVEN, assetsHost: BMCLAPI_ASSETS }).setName('dependencies'), ); // 收尾:更新实例最近访问时间 await updateInstance(name, gamePath, { lastAccessDate: Date.now() }); hooks.log('info', `实例「${name}」创建完成`); }); }); // ==================== 对外接口 ==================== // 根据执行器名称创建 @xmcl/task 任务;未知执行器返回 undefined export function createXmclTask( executorName: string, params: Record, hooks: TaskHooks, ): Task | undefined { const factory = executorRegistry.get(executorName); return factory ? factory(params, hooks) : undefined; } // 将 @xmcl/task 的 path 映射为前端展示的阶段文案 const STAGE_MAP: [RegExp, string][] = [ [/^install\.create/, '创建实例'], [/^install\.minecraft/, '下载 Minecraft'], [/^install\.forge/, '安装 Forge'], [/^install\.neoforge/, '安装 NeoForge'], [/^install\.fabric/, '安装 Fabric'], [/^install\.quilt/, '安装 Quilt'], [/^install\.dependencies/, '安装依赖'], [/^sleep/, '模拟计时'], [/^download/, '模拟下载'], [/^install-sim/, '模拟安装'], ]; export function stageFromPath(taskPath: string): string { for (const [pattern, label] of STAGE_MAP) { if (pattern.test(taskPath)) return label; } return taskPath || '执行中'; }