mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
Add a new xmcl task system and instance-import workflow. Introduces electron/core/task.ts with @xmcl/task executors (install, sims), task lifecycle IPC (start/pause/resume/cancel) and stage mapping. Detect mod loaders in instance scanner and add importExistingInstance with IPC. Update front-end task store, types and UI (TaskButton, TaskCard, TaskQueue, TaskDebug) to support paused state, xmcl path/state and pause/resume actions. Improve game-dir UI with loader badges and bulk import. Small UX/dialog tweaks (alert-dialog, About) and dependency/pnpm workspace updates. Adds Koring.yml theme default.
325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
// __ __ __ __ ______ __ __ ______ __ __ ______ ______
|
||
// /\ \ /\ \ /\ "-.\ \ /\ ___\ /\ \/ / /\ ___\ /\ "-.\ \ /\ ___\ /\__ _\
|
||
// \ \ \____ \ \ \ \ \ \-. \ \ \ \__ \ \ \ _"-. \ \ __\ \ \ \-. \ \ \ __\ \/_/\ \/
|
||
// \ \_____\ \ \_\ \ \_\\"\_\ \ \_____\ \ \_\ \_\ \ \_____\ \ \_\\"\_\ \ \_____\ \ \_\
|
||
// \/_____/ \/_/ \/_/ \/_/ \/_____/ \/_/\/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/
|
||
//
|
||
// 所有权利归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';
|
||
|
||
// 任务执行钩子:主进程用它向渲染进程广播日志
|
||
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<string, unknown>, hooks: TaskHooks) => Task<unknown>;
|
||
|
||
// 执行器注册表:executorName → 工厂
|
||
const executorRegistry = new Map<string, ExecutorFactory>();
|
||
|
||
// ==================== 模拟执行器(调试用)====================
|
||
|
||
// 模拟任务基类:AbortableTask 子类,支持取消 / 暂停 / 进度
|
||
abstract class SimTask extends AbortableTask<number> {
|
||
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<void> {
|
||
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<number> {
|
||
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<number> {
|
||
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<number> {
|
||
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, gamePath, runtime } = p;
|
||
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<string, unknown>,
|
||
hooks: TaskHooks,
|
||
): Task<unknown> | 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 || '执行中';
|
||
}
|