20260711存档01

This commit is contained in:
2026-07-11 23:33:44 +08:00
parent 9cb6a6e571
commit 2b19144352
56 changed files with 2156 additions and 627 deletions
+219 -62
View File
@@ -35,45 +35,74 @@ var __importStar = (this && this.__importStar) || (function () {
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInstance = createInstance;
exports.listInstances = listInstances;
exports.deleteInstance = deleteInstance;
exports.getInstanceInfo = getInstanceInfo;
exports.deleteInstance = deleteInstance;
exports.updateInstance = updateInstance;
exports.installInstanceGame = installInstanceGame;
exports.launchInstance = launchInstance;
exports.diagnoseInstance = diagnoseInstance;
exports.getMinecraftVersionList = getMinecraftVersionList;
exports.getForgeVersionList = getForgeVersionList;
exports.getFabricVersionList = getFabricVersionList;
exports.getQuiltVersionList = getQuiltVersionList;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const INSTANCE_CONFIG_FILE = 'koring-instance.json';
const core_1 = require("@xmcl/core");
const installer_1 = require("@xmcl/installer");
const INSTANCE_CONFIG_FILE = 'instance.json';
function getInstanceConfigPath(instancePath) {
return path.join(instancePath, INSTANCE_CONFIG_FILE);
}
async function createInstance(name, gamePath, mcVersion, loaderType, loaderVersion, javaPath, memory) {
const instancePath = path.join(gamePath, 'instances', name);
if (!fs.existsSync(instancePath)) {
fs.mkdirSync(instancePath, { recursive: true });
function countFiles(dir, ext) {
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, runtime) {
const issues = [];
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;
}
async function createInstance(name, gamePath, runtime, options) {
const instancePath = path.join(gamePath, 'instances', name);
if (fs.existsSync(instancePath)) {
throw new Error(`Instance already exists: ${name}`);
}
fs.mkdirSync(instancePath, { recursive: true });
const now = Date.now();
const config = {
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');
// Create mods directory
const modsDir = path.join(instancePath, 'mods');
if (!fs.existsSync(modsDir)) {
fs.mkdirSync(modsDir, { recursive: true });
fs.writeFileSync(getInstanceConfigPath(instancePath), JSON.stringify(config, null, 2), 'utf-8');
// 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);
}
async function listInstances(instancesPath) {
async function listInstances(gamePath) {
const instancesPath = path.join(gamePath, 'instances');
const instances = [];
if (!fs.existsSync(instancesPath)) {
return instances;
@@ -82,57 +111,185 @@ async function listInstances(instancesPath) {
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);
// 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;
}
async function deleteInstance(name, instancesPath) {
const instancePath = path.join(instancesPath, name);
if (fs.existsSync(instancePath)) {
fs.rmSync(instancePath, { recursive: true, force: true });
}
return { deleted: name };
}
async function getInstanceInfo(name, instancesPath) {
const instancePath = path.join(instancesPath, name);
async function getInstanceInfo(name, gamePath) {
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);
// 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,
};
}
async function deleteInstance(name, gamePath) {
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 };
}
async function updateInstance(name, gamePath, patch) {
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);
const updated = { ...config, ...patch };
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2), 'utf-8');
return getInstanceInfo(name, gamePath);
}
async function installInstanceGame(name, gamePath, callbacks) {
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 (0, installer_1.getVersionList)();
const versionInfo = versionList.versions.find((v) => v.id === runtime.minecraft);
if (!versionInfo) {
throw new Error(`Minecraft version ${runtime.minecraft} not found`);
}
await (0, installer_1.install)({ 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 (0, installer_1.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 (0, installer_1.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 (0, installer_1.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 (0, installer_1.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 = await core_1.Version.parse(instancePath, runtime.minecraft);
await (0, installer_1.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);
}
async function launchInstance(name, gamePath, options) {
const instance = await getInstanceInfo(name, gamePath);
const { runtime } = instance.config;
const resolved = await core_1.Version.parse(instance.path, runtime.minecraft);
const javaPath = options.javaPath || instance.config.java || 'java';
const mcProcess = await (0, core_1.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 = (0, core_1.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,
};
}
async function diagnoseInstance(name, gamePath) {
const instance = await getInstanceInfo(name, gamePath);
return {
healthy: instance.healthy,
issues: instance.issues,
};
}
// Version list APIs
async function getMinecraftVersionList(type) {
const manifest = await (0, installer_1.getVersionList)();
let versions = manifest.versions;
if (type && type !== 'all') {
versions = versions.filter((v) => v.type === type);
}
return { versions };
}
async function getForgeVersionList(mcVersion) {
const list = await (0, installer_1.getForgeVersionList)({ minecraft: mcVersion });
return { versions: list.versions };
}
async function getFabricVersionList(mcVersion) {
if (mcVersion) {
const loaders = await (0, installer_1.getFabricLoaders)();
return { versions: loaders.map((l) => l.version) };
}
const loaders = await (0, installer_1.getFabricLoaders)();
return { versions: loaders.map((l) => l.version) };
}
async function getQuiltVersionList(mcVersion) {
const loaders = await (0, installer_1.getQuiltLoaderVersionsByMinecraft)({ minecraftVersion: mcVersion || '*' });
return { versions: loaders.map((l) => l.loader.version) };
}
+331 -87
View File
@@ -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) };
}
+27
View File
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerBackgroundHandlers = registerBackgroundHandlers;
const electron_1 = __importDefault(require("electron"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("../config");
const { ipcMain } = electron_1.default;
function registerBackgroundHandlers() {
@@ -98,4 +100,29 @@ function registerBackgroundHandlers() {
return { success: false, data: null, error: String(e) };
}
});
// Copy file to userData and return the destination path
ipcMain.handle('background:copyToUserData', async (_event, srcPath, ext) => {
try {
const userDataPath = electron_1.default.app.getPath('userData');
const destPath = path_1.default.join(userDataPath, `background-custom${ext}`);
fs_1.default.copyFileSync(srcPath, destPath);
return destPath;
}
catch {
return null;
}
});
// Get cached background file path from userData
ipcMain.handle('background:getCachedPath', async () => {
try {
const userDataPath = electron_1.default.app.getPath('userData');
const files = fs_1.default.readdirSync(userDataPath).filter(f => f.startsWith('background-custom'));
if (files.length === 0)
return null;
return path_1.default.join(userDataPath, files[0]);
}
catch {
return null;
}
});
}
+26
View File
@@ -1,4 +1,6 @@
import electron from 'electron';
import fs from 'fs';
import path from 'path';
import { loadConfig, saveConfig } from '../config';
const { ipcMain } = electron;
@@ -91,4 +93,28 @@ export function registerBackgroundHandlers() {
return { success: false, data: null, error: String(e) };
}
});
// Copy file to userData and return the destination path
ipcMain.handle('background:copyToUserData', async (_event, srcPath: string, ext: string) => {
try {
const userDataPath = electron.app.getPath('userData');
const destPath = path.join(userDataPath, `background-custom${ext}`);
fs.copyFileSync(srcPath, destPath);
return destPath;
} catch {
return null;
}
});
// Get cached background file path from userData
ipcMain.handle('background:getCachedPath', async () => {
try {
const userDataPath = electron.app.getPath('userData');
const files = fs.readdirSync(userDataPath).filter(f => f.startsWith('background-custom'));
if (files.length === 0) return null;
return path.join(userDataPath, files[0]);
} catch {
return null;
}
});
}
+117 -13
View File
@@ -7,10 +7,18 @@ exports.registerInstanceHandlers = registerInstanceHandlers;
const electron_1 = __importDefault(require("electron"));
const instance_1 = require("../core/instance");
const { ipcMain } = electron_1.default;
function registerInstanceHandlers() {
function registerInstanceHandlers(win) {
ipcMain.handle('instance:create', async (_event, payload) => {
try {
const data = await (0, instance_1.createInstance)(payload.name, payload.gamePath, payload.mcVersion, payload.loaderType, payload.loaderVersion, payload.javaPath, payload.memory);
const data = await (0, instance_1.createInstance)(payload.name, payload.gamePath, payload.runtime, {
author: payload.author,
description: payload.description,
java: payload.java,
minMemory: payload.minMemory,
maxMemory: payload.maxMemory,
vmOptions: payload.vmOptions,
mcOptions: payload.mcOptions,
});
return { success: true, data, error: null };
}
catch (e) {
@@ -19,16 +27,7 @@ function registerInstanceHandlers() {
});
ipcMain.handle('instance:list', async (_event, payload) => {
try {
const data = await (0, instance_1.listInstances)(payload.instancesPath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:delete', async (_event, payload) => {
try {
const data = await (0, instance_1.deleteInstance)(payload.name, payload.instancesPath);
const data = await (0, instance_1.listInstances)(payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
@@ -37,7 +36,112 @@ function registerInstanceHandlers() {
});
ipcMain.handle('instance:info', async (_event, payload) => {
try {
const data = await (0, instance_1.getInstanceInfo)(payload.name, payload.instancesPath);
const data = await (0, instance_1.getInstanceInfo)(payload.name, payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:delete', async (_event, payload) => {
try {
const data = await (0, instance_1.deleteInstance)(payload.name, payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:update', async (_event, payload) => {
try {
const data = await (0, instance_1.updateInstance)(payload.name, payload.gamePath, payload.patch);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:install', async (_event, payload) => {
try {
const requestId = `install-${Date.now()}`;
(0, instance_1.installInstanceGame)(payload.name, payload.gamePath, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('instance:progress', { requestId, ...progress });
},
}).then((data) => {
win.mainWindow?.webContents.send('instance:install-complete', { requestId, data });
}).catch((err) => {
win.mainWindow?.webContents.send('instance:install-error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:launch', async (_event, payload) => {
try {
const requestId = `launch-${Date.now()}`;
(0, instance_1.launchInstance)(payload.name, payload.gamePath, {
username: payload.username,
uuid: payload.uuid,
accessToken: payload.accessToken,
javaPath: payload.javaPath,
server: payload.server,
onEvent: (event) => {
win.mainWindow?.webContents.send('instance:launch-event', { requestId, ...event });
},
}).then((data) => {
win.mainWindow?.webContents.send('instance:launch-complete', { requestId, data });
}).catch((err) => {
win.mainWindow?.webContents.send('instance:launch-error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:diagnose', async (_event, payload) => {
try {
const data = await (0, instance_1.diagnoseInstance)(payload.name, payload.gamePath);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
// Version list APIs
ipcMain.handle('instance:version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getMinecraftVersionList)(payload.type);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:forge-version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getForgeVersionList)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:fabric-version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getFabricVersionList)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:quilt-version-list', async (_event, payload) => {
try {
const data = await (0, instance_1.getQuiltVersionList)(payload.mcVersion);
return { success: true, data, error: null };
}
catch (e) {
+157 -18
View File
@@ -1,27 +1,54 @@
import electron from 'electron';
import { createInstance, listInstances, deleteInstance, getInstanceInfo } from '../core/instance';
import {
createInstance,
listInstances,
getInstanceInfo,
deleteInstance,
updateInstance,
installInstanceGame,
launchInstance,
diagnoseInstance,
getMinecraftVersionList,
getForgeVersionList,
getFabricVersionList,
getQuiltVersionList,
type InstanceRuntime,
type InstanceConfig,
} from '../core/instance';
const { ipcMain } = electron;
export function registerInstanceHandlers() {
interface WinRef {
mainWindow: electron.BrowserWindow | null;
}
export function registerInstanceHandlers(win: WinRef) {
ipcMain.handle('instance:create', async (_event, payload: {
name: string;
gamePath: string;
mcVersion: string;
loaderType?: string;
loaderVersion?: string;
javaPath?: string;
memory?: { min?: string; max?: string };
runtime: InstanceRuntime;
author?: string;
description?: string;
java?: string;
minMemory?: number;
maxMemory?: number;
vmOptions?: string[];
mcOptions?: string[];
}) => {
try {
const data = await createInstance(
payload.name,
payload.gamePath,
payload.mcVersion,
payload.loaderType,
payload.loaderVersion,
payload.javaPath,
payload.memory
payload.runtime,
{
author: payload.author,
description: payload.description,
java: payload.java,
minMemory: payload.minMemory,
maxMemory: payload.maxMemory,
vmOptions: payload.vmOptions,
mcOptions: payload.mcOptions,
}
);
return { success: true, data, error: null };
} catch (e: unknown) {
@@ -29,27 +56,139 @@ export function registerInstanceHandlers() {
}
});
ipcMain.handle('instance:list', async (_event, payload: { instancesPath: string }) => {
ipcMain.handle('instance:list', async (_event, payload: { gamePath: string }) => {
try {
const data = await listInstances(payload.instancesPath);
const data = await listInstances(payload.gamePath);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:delete', async (_event, payload: { name: string; instancesPath: string }) => {
ipcMain.handle('instance:info', async (_event, payload: { name: string; gamePath: string }) => {
try {
const data = await deleteInstance(payload.name, payload.instancesPath);
const data = await getInstanceInfo(payload.name, payload.gamePath);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:info', async (_event, payload: { name: string; instancesPath: string }) => {
ipcMain.handle('instance:delete', async (_event, payload: { name: string; gamePath: string }) => {
try {
const data = await getInstanceInfo(payload.name, payload.instancesPath);
const data = await deleteInstance(payload.name, payload.gamePath);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:update', async (_event, payload: {
name: string;
gamePath: string;
patch: Partial<Omit<InstanceConfig, 'name' | 'creationDate'>>;
}) => {
try {
const data = await updateInstance(payload.name, payload.gamePath, payload.patch);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:install', async (_event, payload: { name: string; gamePath: string }) => {
try {
const requestId = `install-${Date.now()}`;
installInstanceGame(payload.name, payload.gamePath, {
onProgress: (progress) => {
win.mainWindow?.webContents.send('instance:progress', { requestId, ...progress });
},
}).then((data) => {
win.mainWindow?.webContents.send('instance:install-complete', { requestId, data });
}).catch((err) => {
win.mainWindow?.webContents.send('instance:install-error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:launch', async (_event, payload: {
name: string;
gamePath: string;
username: string;
uuid: string;
accessToken?: string;
javaPath?: string;
server?: { host: string; port?: number };
}) => {
try {
const requestId = `launch-${Date.now()}`;
launchInstance(payload.name, payload.gamePath, {
username: payload.username,
uuid: payload.uuid,
accessToken: payload.accessToken,
javaPath: payload.javaPath,
server: payload.server,
onEvent: (event) => {
win.mainWindow?.webContents.send('instance:launch-event', { requestId, ...event });
},
}).then((data) => {
win.mainWindow?.webContents.send('instance:launch-complete', { requestId, data });
}).catch((err) => {
win.mainWindow?.webContents.send('instance:launch-error', { requestId, error: String(err) });
});
return { success: true, data: { requestId }, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:diagnose', async (_event, payload: { name: string; gamePath: string }) => {
try {
const data = await diagnoseInstance(payload.name, payload.gamePath);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
// Version list APIs
ipcMain.handle('instance:version-list', async (_event, payload: { type?: string }) => {
try {
const data = await getMinecraftVersionList(payload.type);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:forge-version-list', async (_event, payload: { mcVersion?: string }) => {
try {
const data = await getForgeVersionList(payload.mcVersion);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:fabric-version-list', async (_event, payload: { mcVersion?: string }) => {
try {
const data = await getFabricVersionList(payload.mcVersion);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('instance:quilt-version-list', async (_event, payload: { mcVersion?: string }) => {
try {
const data = await getQuiltVersionList(payload.mcVersion);
return { success: true, data, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
+18 -3
View File
@@ -6,8 +6,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.registerWindowHandlers = registerWindowHandlers;
const electron_1 = __importDefault(require("electron"));
const path_1 = __importDefault(require("path"));
const { ipcMain, dialog } = electron_1.default;
const { ipcMain, dialog, shell } = electron_1.default;
const isDev = !electron_1.default.app.isPackaged;
const MIME_MAP = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif',
bmp: 'image/bmp',
svg: 'image/svg+xml',
};
function createSplashWindow() {
const splash = new electron_1.default.BrowserWindow({
width: 480,
@@ -67,7 +76,7 @@ function registerWindowHandlers(win) {
}
return { success: true };
});
// File dialog
// File dialog — returns source path and extension for preload to handle
ipcMain.handle('dialog:openFile', async (_event, payload) => {
const result = await dialog.showOpenDialog(win.mainWindow, {
properties: ['openFile'],
@@ -75,6 +84,12 @@ function registerWindowHandlers(win) {
});
if (result.canceled || result.filePaths.length === 0)
return null;
return result.filePaths[0];
const srcPath = result.filePaths[0];
const ext = path_1.default.extname(srcPath).toLowerCase() || '.png';
return { srcPath, ext };
});
// Open external URL in system browser
ipcMain.handle('shell:openExternal', async (_event, url) => {
await shell.openExternal(url);
});
}
+20 -3
View File
@@ -1,7 +1,7 @@
import electron from 'electron';
import path from 'path';
const { ipcMain, dialog } = electron;
const { ipcMain, dialog, shell } = electron;
const isDev = !electron.app.isPackaged;
@@ -10,6 +10,16 @@ interface WinRef {
splashWindow: electron.BrowserWindow | null;
}
const MIME_MAP: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif',
bmp: 'image/bmp',
svg: 'image/svg+xml',
};
function createSplashWindow(): electron.BrowserWindow {
const splash = new electron.BrowserWindow({
width: 480,
@@ -77,7 +87,7 @@ export function registerWindowHandlers(win: WinRef) {
return { success: true };
});
// File dialog
// File dialog — returns source path and extension for preload to handle
ipcMain.handle('dialog:openFile', async (_event, payload: {
filters?: { name: string; extensions: string[] }[];
}) => {
@@ -86,6 +96,13 @@ export function registerWindowHandlers(win: WinRef) {
filters: payload.filters,
});
if (result.canceled || result.filePaths.length === 0) return null;
return result.filePaths[0];
const srcPath = result.filePaths[0];
const ext = path.extname(srcPath).toLowerCase() || '.png';
return { srcPath, ext };
});
// Open external URL in system browser
ipcMain.handle('shell:openExternal', async (_event, url: string) => {
await shell.openExternal(url);
});
}
+12 -2
View File
@@ -22,6 +22,10 @@ const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.B
};
function createSplashWindow(): electron.BrowserWindow {
const iconPath = isDev
? path.join(__dirname, '../build/icon.ico')
: path.join(__dirname, '../build/icon.ico');
const splash = new electron.BrowserWindow({
width: 480,
height: 320,
@@ -30,6 +34,7 @@ function createSplashWindow(): electron.BrowserWindow {
resizable: false,
skipTaskbar: true,
alwaysOnTop: true,
icon: iconPath,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
@@ -46,15 +51,20 @@ function createSplashWindow(): electron.BrowserWindow {
}
function createMainWindow(): electron.BrowserWindow {
const iconPath = isDev
? path.join(__dirname, '../build/icon.ico')
: path.join(__dirname, '../build/icon.ico');
const main = new electron.BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 600,
transparent: true,
frame: false,
transparent: false,
resizable: true,
show: false,
icon: iconPath,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
@@ -86,7 +96,7 @@ function registerAllHandlers() {
registerInstallHandlers(win);
registerLaunchHandlers(win);
registerModsHandlers();
registerInstanceHandlers();
registerInstanceHandlers(win);
registerBackgroundHandlers();
registerTaskHandlers(win);
registerSystemHandlers();
+45
View File
@@ -1,6 +1,28 @@
import electron from 'electron';
import path from 'path';
import fs from 'fs';
const { contextBridge, ipcRenderer } = electron;
const MIME_MAP: Record<string, string> = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
};
function getFileAsDataUrl(filePath: string): string | null {
try {
const buffer = fs.readFileSync(filePath);
const ext = path.extname(filePath).toLowerCase();
const mime = MIME_MAP[ext] || 'image/png';
return `data:${mime};base64,${buffer.toString('base64')}`;
} catch {
return null;
}
}
contextBridge.exposeInMainWorld('electronAPI', {
// Generic IPC
invoke: (channel: string, ...args: unknown[]) =>
@@ -28,4 +50,27 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Theme
getTheme: () => ipcRenderer.invoke('window:getTheme'),
// Background image — pick file, copy to userData, return base64 data URL
pickBackgroundImage: async (): Promise<string | null> => {
const result = await ipcRenderer.invoke('dialog:openFile', {
filters: [{ name: '图片', extensions: ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp'] }],
});
if (!result) return null;
const { srcPath, ext } = result as { srcPath: string; ext: string };
// Copy to userData via main process
const destPath = await ipcRenderer.invoke('background:copyToUserData', srcPath, ext);
if (!destPath) return null;
return getFileAsDataUrl(destPath);
},
// Get cached background as base64 data URL
getBackgroundDataUrl: async (): Promise<string | null> => {
const filePath = await ipcRenderer.invoke('background:getCachedPath');
if (!filePath) return null;
return getFileAsDataUrl(filePath);
},
// Open external URL in system browser
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
});