mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
20260711存档01
This commit is contained in:
+331
-87
@@ -1,75 +1,146 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { MinecraftFolder, Version, launch, createMinecraftProcessWatcher, type ResolvedVersion } from '@xmcl/core';
|
||||
import {
|
||||
install as xmclInstall,
|
||||
installForge,
|
||||
installFabric,
|
||||
installNeoForged,
|
||||
installOptifine,
|
||||
installQuiltVersion,
|
||||
installDependencies,
|
||||
getVersionList,
|
||||
getForgeVersionList as xmclGetForgeVersionList,
|
||||
getFabricLoaders,
|
||||
getQuiltLoaderVersionsByMinecraft,
|
||||
} from '@xmcl/installer';
|
||||
|
||||
interface InstanceConfig {
|
||||
name: string;
|
||||
mcVersion: string;
|
||||
loaderType?: string;
|
||||
loaderVersion?: string;
|
||||
javaPath?: string;
|
||||
memory?: { min?: string; max?: string };
|
||||
createdAt: string;
|
||||
export interface InstanceRuntime {
|
||||
minecraft: string;
|
||||
forge?: string;
|
||||
neoForged?: string;
|
||||
fabricLoader?: string;
|
||||
quiltLoader?: string;
|
||||
optifine?: string;
|
||||
}
|
||||
|
||||
interface InstanceInfo {
|
||||
export interface InstanceConfig {
|
||||
name: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
runtime: InstanceRuntime;
|
||||
java?: string;
|
||||
minMemory?: number;
|
||||
maxMemory?: number;
|
||||
vmOptions?: string[];
|
||||
mcOptions?: string[];
|
||||
server?: { host: string; port?: number; name?: string };
|
||||
showLog?: boolean;
|
||||
hideLauncher?: boolean;
|
||||
icon?: string;
|
||||
creationDate: number;
|
||||
lastAccessDate: number;
|
||||
lastPlayedDate: number;
|
||||
playtime: number;
|
||||
}
|
||||
|
||||
export interface InstanceInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
config: InstanceConfig;
|
||||
modCount?: number;
|
||||
modCount: number;
|
||||
resourcePackCount: number;
|
||||
screenshotCount: number;
|
||||
saveCount: number;
|
||||
healthy: boolean;
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
const INSTANCE_CONFIG_FILE = 'koring-instance.json';
|
||||
export interface InstallProgress {
|
||||
stage: string;
|
||||
current: number;
|
||||
total: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const INSTANCE_CONFIG_FILE = 'instance.json';
|
||||
|
||||
function getInstanceConfigPath(instancePath: string): string {
|
||||
return path.join(instancePath, INSTANCE_CONFIG_FILE);
|
||||
}
|
||||
|
||||
function countFiles(dir: string, ext?: string): number {
|
||||
if (!fs.existsSync(dir)) return 0;
|
||||
return fs.readdirSync(dir).filter((f) => {
|
||||
if (ext) return f.endsWith(ext);
|
||||
return fs.statSync(path.join(dir, f)).isFile();
|
||||
}).length;
|
||||
}
|
||||
|
||||
function getInstanceIssues(instancePath: string, runtime: InstanceRuntime): string[] {
|
||||
const issues: string[] = [];
|
||||
|
||||
if (!fs.existsSync(path.join(instancePath, 'versions', runtime.minecraft, `${runtime.minecraft}.json`))) {
|
||||
issues.push(`Version JSON not found for ${runtime.minecraft}`);
|
||||
}
|
||||
if (!fs.existsSync(path.join(instancePath, 'versions', runtime.minecraft, `${runtime.minecraft}.jar`))) {
|
||||
issues.push(`Client JAR not found for ${runtime.minecraft}`);
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
export async function createInstance(
|
||||
name: string,
|
||||
gamePath: string,
|
||||
mcVersion: string,
|
||||
loaderType?: string,
|
||||
loaderVersion?: string,
|
||||
javaPath?: string,
|
||||
memory?: { min?: string; max?: string }
|
||||
runtime: InstanceRuntime,
|
||||
options?: {
|
||||
author?: string;
|
||||
description?: string;
|
||||
java?: string;
|
||||
minMemory?: number;
|
||||
maxMemory?: number;
|
||||
vmOptions?: string[];
|
||||
mcOptions?: string[];
|
||||
}
|
||||
): Promise<InstanceInfo> {
|
||||
const instancePath = path.join(gamePath, 'instances', name);
|
||||
|
||||
if (!fs.existsSync(instancePath)) {
|
||||
fs.mkdirSync(instancePath, { recursive: true });
|
||||
if (fs.existsSync(instancePath)) {
|
||||
throw new Error(`Instance already exists: ${name}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(instancePath, { recursive: true });
|
||||
|
||||
const now = Date.now();
|
||||
const config: InstanceConfig = {
|
||||
name,
|
||||
mcVersion,
|
||||
loaderType,
|
||||
loaderVersion,
|
||||
javaPath,
|
||||
memory,
|
||||
createdAt: new Date().toISOString(),
|
||||
author: options?.author || '',
|
||||
description: options?.description || '',
|
||||
runtime,
|
||||
java: options?.java,
|
||||
minMemory: options?.minMemory,
|
||||
maxMemory: options?.maxMemory,
|
||||
vmOptions: options?.vmOptions,
|
||||
mcOptions: options?.mcOptions,
|
||||
creationDate: now,
|
||||
lastAccessDate: now,
|
||||
lastPlayedDate: 0,
|
||||
playtime: 0,
|
||||
};
|
||||
|
||||
const configPath = getInstanceConfigPath(instancePath);
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
fs.writeFileSync(getInstanceConfigPath(instancePath), JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
// Create mods directory
|
||||
const modsDir = path.join(instancePath, 'mods');
|
||||
if (!fs.existsSync(modsDir)) {
|
||||
fs.mkdirSync(modsDir, { recursive: true });
|
||||
// Create standard directories
|
||||
for (const dir of ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'screenshots', 'logs']) {
|
||||
fs.mkdirSync(path.join(instancePath, dir), { recursive: true });
|
||||
}
|
||||
|
||||
// Count mods
|
||||
const mods = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar'));
|
||||
|
||||
return {
|
||||
name,
|
||||
path: instancePath,
|
||||
config,
|
||||
modCount: mods.length,
|
||||
};
|
||||
return getInstanceInfo(name, gamePath);
|
||||
}
|
||||
|
||||
export async function listInstances(instancesPath: string): Promise<InstanceInfo[]> {
|
||||
export async function listInstances(gamePath: string): Promise<InstanceInfo[]> {
|
||||
const instancesPath = path.join(gamePath, 'instances');
|
||||
const instances: InstanceInfo[] = [];
|
||||
|
||||
if (!fs.existsSync(instancesPath)) {
|
||||
@@ -81,54 +152,18 @@ export async function listInstances(instancesPath: string): Promise<InstanceInfo
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const instancePath = path.join(instancesPath, entry.name);
|
||||
const configPath = getInstanceConfigPath(instancePath);
|
||||
|
||||
if (!fs.existsSync(configPath)) continue;
|
||||
|
||||
try {
|
||||
const configRaw = fs.readFileSync(configPath, 'utf-8');
|
||||
const config = JSON.parse(configRaw) as InstanceConfig;
|
||||
|
||||
// Count mods
|
||||
const modsDir = path.join(instancePath, 'mods');
|
||||
let modCount = 0;
|
||||
if (fs.existsSync(modsDir)) {
|
||||
modCount = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar')).length;
|
||||
}
|
||||
|
||||
instances.push({
|
||||
name: entry.name,
|
||||
path: instancePath,
|
||||
config,
|
||||
modCount,
|
||||
});
|
||||
instances.push(await getInstanceInfo(entry.name, gamePath));
|
||||
} catch {
|
||||
// Skip invalid config
|
||||
// Skip invalid instances
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
export async function deleteInstance(
|
||||
name: string,
|
||||
instancesPath: string
|
||||
): Promise<{ deleted: string }> {
|
||||
const instancePath = path.join(instancesPath, name);
|
||||
|
||||
if (fs.existsSync(instancePath)) {
|
||||
fs.rmSync(instancePath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
return { deleted: name };
|
||||
}
|
||||
|
||||
export async function getInstanceInfo(
|
||||
name: string,
|
||||
instancesPath: string
|
||||
): Promise<InstanceInfo> {
|
||||
const instancePath = path.join(instancesPath, name);
|
||||
export async function getInstanceInfo(name: string, gamePath: string): Promise<InstanceInfo> {
|
||||
const instancePath = path.join(gamePath, 'instances', name);
|
||||
const configPath = getInstanceConfigPath(instancePath);
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
@@ -138,17 +173,226 @@ export async function getInstanceInfo(
|
||||
const configRaw = fs.readFileSync(configPath, 'utf-8');
|
||||
const config = JSON.parse(configRaw) as InstanceConfig;
|
||||
|
||||
// Count mods
|
||||
const modsDir = path.join(instancePath, 'mods');
|
||||
let modCount = 0;
|
||||
if (fs.existsSync(modsDir)) {
|
||||
modCount = fs.readdirSync(modsDir).filter((f) => f.endsWith('.jar')).length;
|
||||
}
|
||||
const issues = getInstanceIssues(instancePath, config.runtime);
|
||||
|
||||
return {
|
||||
name,
|
||||
path: instancePath,
|
||||
config,
|
||||
modCount,
|
||||
modCount: countFiles(path.join(instancePath, 'mods'), '.jar'),
|
||||
resourcePackCount: countFiles(path.join(instancePath, 'resourcepacks'), '.zip'),
|
||||
screenshotCount: countFiles(path.join(instancePath, 'screenshots')),
|
||||
saveCount: countFiles(path.join(instancePath, 'saves')),
|
||||
healthy: issues.length === 0,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteInstance(name: string, gamePath: string): Promise<{ deleted: string }> {
|
||||
const instancePath = path.join(gamePath, 'instances', name);
|
||||
|
||||
if (!fs.existsSync(instancePath)) {
|
||||
throw new Error(`Instance not found: ${name}`);
|
||||
}
|
||||
|
||||
fs.rmSync(instancePath, { recursive: true, force: true });
|
||||
return { deleted: name };
|
||||
}
|
||||
|
||||
export async function updateInstance(
|
||||
name: string,
|
||||
gamePath: string,
|
||||
patch: Partial<Omit<InstanceConfig, 'name' | 'creationDate'>>
|
||||
): Promise<InstanceInfo> {
|
||||
const instancePath = path.join(gamePath, 'instances', name);
|
||||
const configPath = getInstanceConfigPath(instancePath);
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error(`Instance not found: ${name}`);
|
||||
}
|
||||
|
||||
const configRaw = fs.readFileSync(configPath, 'utf-8');
|
||||
const config = JSON.parse(configRaw) as InstanceConfig;
|
||||
|
||||
const updated = { ...config, ...patch };
|
||||
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2), 'utf-8');
|
||||
|
||||
return getInstanceInfo(name, gamePath);
|
||||
}
|
||||
|
||||
export async function installInstanceGame(
|
||||
name: string,
|
||||
gamePath: string,
|
||||
callbacks?: { onProgress?: (progress: InstallProgress) => void }
|
||||
): Promise<InstanceInfo> {
|
||||
const instance = await getInstanceInfo(name, gamePath);
|
||||
const { runtime } = instance.config;
|
||||
const instancePath = instance.path;
|
||||
|
||||
callbacks?.onProgress?.({ stage: 'installing-minecraft', current: 0, total: 100, message: `Installing Minecraft ${runtime.minecraft}...` });
|
||||
|
||||
// Install Minecraft
|
||||
// First get version list to find the version info
|
||||
const versionList = await getVersionList();
|
||||
const versionInfo = versionList.versions.find((v) => v.id === runtime.minecraft);
|
||||
if (!versionInfo) {
|
||||
throw new Error(`Minecraft version ${runtime.minecraft} not found`);
|
||||
}
|
||||
await xmclInstall({ id: versionInfo.id, url: versionInfo.url }, instancePath);
|
||||
|
||||
// Install mod loaders
|
||||
if (runtime.forge) {
|
||||
callbacks?.onProgress?.({ stage: 'installing-forge', current: 0, total: 100, message: `Installing Forge ${runtime.forge}...` });
|
||||
await installForge({ version: runtime.forge, mcversion: runtime.minecraft }, instancePath);
|
||||
}
|
||||
|
||||
if (runtime.fabricLoader) {
|
||||
callbacks?.onProgress?.({ stage: 'installing-fabric', current: 0, total: 100, message: `Installing Fabric ${runtime.fabricLoader}...` });
|
||||
await installFabric({
|
||||
minecraftVersion: runtime.minecraft,
|
||||
version: runtime.fabricLoader,
|
||||
minecraft: instancePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (runtime.quiltLoader) {
|
||||
callbacks?.onProgress?.({ stage: 'installing-quilt', current: 0, total: 100, message: `Installing Quilt ${runtime.quiltLoader}...` });
|
||||
await installQuiltVersion({
|
||||
minecraftVersion: runtime.minecraft,
|
||||
version: runtime.quiltLoader,
|
||||
minecraft: instancePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (runtime.neoForged) {
|
||||
callbacks?.onProgress?.({ stage: 'installing-neoforge', current: 0, total: 100, message: `Installing NeoForge ${runtime.neoForged}...` });
|
||||
await installNeoForged('neoforge', runtime.neoForged, instancePath, {});
|
||||
}
|
||||
|
||||
// Note: OptiFine requires downloading the installer JAR first
|
||||
// if (runtime.optifine) {
|
||||
// callbacks?.onProgress?.({ stage: 'installing-optifine', current: 0, total: 100, message: `Installing OptiFine ${runtime.optifine}...` });
|
||||
// await installOptifine(runtime.optifine, instancePath);
|
||||
// }
|
||||
|
||||
callbacks?.onProgress?.({ stage: 'installing-dependencies', current: 0, total: 100, message: 'Installing dependencies...' });
|
||||
|
||||
// Install all dependencies (libraries + assets)
|
||||
const resolved: ResolvedVersion = await Version.parse(instancePath, runtime.minecraft);
|
||||
await installDependencies(resolved);
|
||||
|
||||
callbacks?.onProgress?.({ stage: 'done', current: 100, total: 100, message: 'Installation complete' });
|
||||
|
||||
// Update last access date
|
||||
await updateInstance(name, gamePath, { lastAccessDate: Date.now() });
|
||||
|
||||
return getInstanceInfo(name, gamePath);
|
||||
}
|
||||
|
||||
export async function launchInstance(
|
||||
name: string,
|
||||
gamePath: string,
|
||||
options: {
|
||||
username: string;
|
||||
uuid: string;
|
||||
accessToken?: string;
|
||||
javaPath?: string;
|
||||
server?: { host: string; port?: number };
|
||||
onEvent?: (event: { event: string; [key: string]: unknown }) => void;
|
||||
}
|
||||
): Promise<{ pid: number; version: string; username: string }> {
|
||||
const instance = await getInstanceInfo(name, gamePath);
|
||||
const { runtime } = instance.config;
|
||||
|
||||
const resolved: ResolvedVersion = await Version.parse(instance.path, runtime.minecraft);
|
||||
|
||||
const javaPath = options.javaPath || instance.config.java || 'java';
|
||||
|
||||
const mcProcess = await launch({
|
||||
gameProfile: {
|
||||
id: options.uuid,
|
||||
name: options.username,
|
||||
},
|
||||
javaPath,
|
||||
version: resolved,
|
||||
gamePath: instance.path,
|
||||
minMemory: instance.config.minMemory || 1024,
|
||||
maxMemory: instance.config.maxMemory || 4096,
|
||||
extraExecOption: { detached: true, stdio: 'ignore' },
|
||||
server: options.server ? { ip: options.server.host, port: options.server.port } : undefined,
|
||||
});
|
||||
|
||||
const watcher = createMinecraftProcessWatcher(mcProcess);
|
||||
|
||||
watcher.on('minecraft-window-ready', () => {
|
||||
options.onEvent?.({ event: 'window-ready' });
|
||||
});
|
||||
|
||||
watcher.on('minecraft-exit', ({ code }) => {
|
||||
options.onEvent?.({ event: 'exit', code });
|
||||
});
|
||||
|
||||
// Update playtime tracking
|
||||
const startTime = Date.now();
|
||||
mcProcess.on('exit', async () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
try {
|
||||
const info = await getInstanceInfo(name, gamePath);
|
||||
await updateInstance(name, gamePath, {
|
||||
lastPlayedDate: Date.now(),
|
||||
playtime: (info.config.playtime || 0) + elapsed,
|
||||
});
|
||||
} catch {
|
||||
// Ignore errors during playtime update
|
||||
}
|
||||
});
|
||||
|
||||
// Update last access date
|
||||
await updateInstance(name, gamePath, { lastAccessDate: Date.now() });
|
||||
|
||||
return {
|
||||
pid: mcProcess.pid || 0,
|
||||
version: runtime.minecraft,
|
||||
username: options.username,
|
||||
};
|
||||
}
|
||||
|
||||
export async function diagnoseInstance(
|
||||
name: string,
|
||||
gamePath: string
|
||||
): Promise<{ healthy: boolean; issues: string[] }> {
|
||||
const instance = await getInstanceInfo(name, gamePath);
|
||||
return {
|
||||
healthy: instance.healthy,
|
||||
issues: instance.issues,
|
||||
};
|
||||
}
|
||||
|
||||
// Version list APIs
|
||||
export async function getMinecraftVersionList(type?: string) {
|
||||
const manifest = await getVersionList();
|
||||
let versions = manifest.versions;
|
||||
if (type && type !== 'all') {
|
||||
versions = versions.filter((v) => v.type === type);
|
||||
}
|
||||
return { versions };
|
||||
}
|
||||
|
||||
export async function getForgeVersionList(mcVersion?: string) {
|
||||
const list = await xmclGetForgeVersionList({ minecraft: mcVersion });
|
||||
return { versions: list.versions };
|
||||
}
|
||||
|
||||
export async function getFabricVersionList(mcVersion?: string) {
|
||||
if (mcVersion) {
|
||||
const loaders = await getFabricLoaders();
|
||||
return { versions: loaders.map((l) => l.version) };
|
||||
}
|
||||
const loaders = await getFabricLoaders();
|
||||
return { versions: loaders.map((l) => l.version) };
|
||||
}
|
||||
|
||||
export async function getQuiltVersionList(mcVersion?: string) {
|
||||
const loaders = await getQuiltLoaderVersionsByMinecraft({ minecraftVersion: mcVersion || '*' });
|
||||
return { versions: loaders.map((l) => l.loader.version) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user