Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
714 changes: 714 additions & 0 deletions src/main/LaunchService.ts

Large diffs are not rendered by default.

198 changes: 186 additions & 12 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import { ItemRegistryService } from "./services/ItemRegistryService";
import { FluidRegistryService } from "./services/FluidRegistryService";
import { RecipeService } from "./services/RecipeService";
import { JarLoaderService } from "./services/JarLoaderService";
// Game launchers removed due to Java compatibility issues
// TODO: Re-implement in future when stable solution is found
import { LaunchService } from './LaunchService';
import "../shared/config"; // Import to silence console.logs in production

const execAsync = promisify(exec);
Expand Down Expand Up @@ -795,21 +794,30 @@ ipcMain.handle("backup:list", async (_event, instancePath: string) => {
}
});

ipcMain.handle("backup:create", async (_event, instancePath: string) => {
ipcMain.handle("backup:create", async (_event, instancePath: string, name?: string) => {
try {
const AdmZip = require("adm-zip");
const zip = new AdmZip();

const configDir = path.join(instancePath, "config");
const backupDir = path.join(instancePath, ".mced-backups");

await fs.mkdir(backupDir, { recursive: true });

const timestamp = Date.now();
const name = `Backup-${new Date(timestamp).toISOString().split("T")[0]}`;
const backupFile = path.join(backupDir, `backup-${timestamp}-${name.replace(/ /g, "-")}.zip`);
const backupName = name || `Backup-${new Date(timestamp).toISOString().split("T")[0]}`;
const backupFile = path.join(backupDir, `backup-${timestamp}-${backupName.replace(/ /g, "-")}.zip`);

// Add config/ folder with prefix preserved
const configDir = path.join(instancePath, "config");
try { await fs.access(configDir); zip.addLocalFolder(configDir, "config"); } catch {}

// Add kubejs/ folder if it exists
const kubejsDir = path.join(instancePath, "kubejs");
try { await fs.access(kubejsDir); zip.addLocalFolder(kubejsDir, "kubejs"); } catch {}

// Add defaultconfigs/ if it exists
const defaultConfigsDir = path.join(instancePath, "defaultconfigs");
try { await fs.access(defaultConfigsDir); zip.addLocalFolder(defaultConfigsDir, "defaultconfigs"); } catch {}

zip.addLocalFolder(configDir);
zip.writeZip(backupFile);

return { success: true };
Expand All @@ -833,10 +841,10 @@ ipcMain.handle("backup:restore", async (_event, instancePath: string, backupId:
try {
const AdmZip = require("adm-zip");
const backupFile = path.join(instancePath, ".mced-backups", backupId);
const configDir = path.join(instancePath, "config");

const zip = new AdmZip(backupFile);
zip.extractAllTo(configDir, true);
// Extract to instance root so config/, kubejs/, defaultconfigs/ are all restored correctly
zip.extractAllTo(instancePath, true);

return { success: true };
} catch (error) {
Expand Down Expand Up @@ -876,6 +884,25 @@ ipcMain.handle("backup:delete", async (_event, instancePath: string, backupId: s
}
});

ipcMain.handle("backup:rename", async (_event, instancePath: string, backupId: string, newName: string) => {
try {
const backupDir = path.join(instancePath, ".mced-backups");
const oldFile = path.join(backupDir, backupId);

// Parse timestamp from existing ID: backup-TIMESTAMP-name.zip
const match = backupId.match(/^backup-(\d+)-/);
const timestamp = match ? match[1] : String(Date.now());

const newId = `backup-${timestamp}-${newName.replace(/ /g, "-")}.zip`;
const newFile = path.join(backupDir, newId);

await fs.rename(oldFile, newFile);
return { success: true, newId };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});

// Open file in external editor
ipcMain.handle("open-in-editor", async (_event, filePath: string, editorCommand?: string) => {
try {
Expand Down Expand Up @@ -969,8 +996,50 @@ ipcMain.handle("shell:openExternal", async (_event, url: string) => {

// ===== Game Launcher Handlers =====

// Game launch functionality removed due to Java compatibility issues
// TODO: Re-implement in future when stable solution is found
const launchService = new LaunchService();

launchService.on('log', (data) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('game:log', data);
}
});

ipcMain.handle(
"game:launch",
async (
_event,
instancePath: string,
launcher: string,
mcVersion: string,
loaderVersion: string,
jvmXmx: number = 4096,
jvmXms: number = 1024
) => {
try {
const result = await launchService.launch(instancePath, launcher, mcVersion, loaderVersion, jvmXmx, jvmXms);
return result;
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
);

ipcMain.handle("game:kill", async (_event, instancePath: string) => {
try {
const killed = LaunchService.kill(instancePath);
return { success: killed };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});

ipcMain.handle("game:isRunning", async (_event, instancePath: string) => {
return LaunchService.isRunning(instancePath);
});

ipcMain.handle("game:getRunning", async () => {
return LaunchService.getRunningInstances();
});

// Update Checker Handler
ipcMain.handle("check-for-updates", async () => {
Expand Down Expand Up @@ -1865,3 +1934,108 @@ ipcMain.handle("kubejs:validateScript", async (_event, code: string) => {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});

ipcMain.handle("crash:analyze", async (_event, crashLogContent: string, knownModIds: string[]) => {
try {
const lines = crashLogContent.split("\n");
const issues: Array<{ modId: string; reason: string; line: string }> = [];
const mentionedMods = new Set<string>();

// Pattern matching for common crash types
const patterns = [
{ regex: /java\.lang\.NullPointerException/i, reason: "Null Pointer Exception" },
{ regex: /java\.lang\.StackOverflowError/i, reason: "Stack Overflow" },
{ regex: /java\.lang\.OutOfMemoryError/i, reason: "Out of Memory - increase JVM heap" },
{ regex: /java\.lang\.ClassNotFoundException:\s*(.+)/i, reason: "Missing class" },
{ regex: /ModLoadingException/i, reason: "Mod loading failed" },
{ regex: /FMLLoadingException/i, reason: "FML loading error" },
{ regex: /mixin\.MixinApplyError/i, reason: "Mixin conflict" },
{ regex: /CrashReport/i, reason: "Crash report entry" },
];

// Check each line for mod mentions and error patterns
for (const line of lines) {
// Check for known mod IDs in the line
for (const modId of knownModIds) {
if (modId.length > 3 && line.toLowerCase().includes(modId.toLowerCase())) {
mentionedMods.add(modId);
}
}

// Check for error patterns
for (const pattern of patterns) {
if (pattern.regex.test(line)) {
issues.push({
modId: "unknown",
reason: pattern.reason,
line: line.trim().substring(0, 200),
});
break;
}
}
}

// Try to extract the main crash cause
const causeMatch = crashLogContent.match(/Caused by:\s*(.+)/);
const mainCause = causeMatch ? causeMatch[1].trim().substring(0, 300) : null;

// Match mentioned mods with issues (heuristic)
const suspectedMods = Array.from(mentionedMods).slice(0, 10);

return {
success: true,
mainCause,
issues: issues.slice(0, 20),
suspectedMods,
totalLines: lines.length,
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});

ipcMain.handle("export:modpack", async (_event, instancePath: string, packName: string, mcVersion: string, loaderType: string, loaderVersion: string) => {
try {
const AdmZip = require("adm-zip");
const zip = new AdmZip();

// Build mrpack manifest
const manifest = {
formatVersion: 1,
game: "minecraft",
versionId: `${packName}-1.0.0`,
name: packName,
dependencies: {
minecraft: mcVersion,
...(loaderType === "forge" ? { forge: loaderVersion } : {}),
...(loaderType === "fabric" ? { "fabric-loader": loaderVersion } : {}),
...(loaderType === "neoforge" ? { neoforge: loaderVersion } : {}),
...(loaderType === "quilt" ? { "quilt-loader": loaderVersion } : {}),
},
files: [] as any[],
};

zip.addFile("modrinth.index.json", Buffer.from(JSON.stringify(manifest, null, 2)));

// Add overrides/config folder
const configDir = path.join(instancePath, "config");
try {
await fs.access(configDir);
zip.addLocalFolder(configDir, "overrides/config");
} catch {}

// Add kubejs if present
const kubejsDir = path.join(instancePath, "kubejs");
try {
await fs.access(kubejsDir);
zip.addLocalFolder(kubejsDir, "overrides/kubejs");
} catch {}

const outputPath = path.join(instancePath, `${packName.replace(/ /g, "-")}.mrpack`);
zip.writeZip(outputPath);

return { success: true, outputPath };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
});
52 changes: 48 additions & 4 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,23 @@ contextBridge.exposeInMainWorld("api", {
discordClearInstance: () => ipcRenderer.invoke("discord:clear-instance"),

// Game Launcher
// Launch game APIs removed due to Java compatibility issues
// TODO: Re-implement in future when stable solution is found
launchGame: (
instancePath: string,
launcher: string,
mcVersion: string,
loaderVersion: string,
jvmXmx?: number,
jvmXms?: number
) => ipcRenderer.invoke("game:launch", instancePath, launcher, mcVersion, loaderVersion, jvmXmx ?? 4096, jvmXms ?? 1024),
killGame: (instancePath: string) => ipcRenderer.invoke("game:kill", instancePath),
isGameRunning: (instancePath: string) => ipcRenderer.invoke("game:isRunning", instancePath),
getRunningGames: () => ipcRenderer.invoke("game:getRunning"),
onGameLog: (callback: (data: { line: string; type: 'stdout' | 'stderr' | 'system'; instancePath: string; timestamp: number }) => void) => {
ipcRenderer.on('game:log', (_event, data) => callback(data));
},
removeGameLogListener: () => {
ipcRenderer.removeAllListeners('game:log');
},

// Update Checker
checkForUpdates: () => ipcRenderer.invoke("check-for-updates"),
Expand Down Expand Up @@ -174,6 +189,12 @@ contextBridge.exposeInMainWorld("api", {
ipcRenderer.invoke("recipe:delete", instancePath, scriptPath, recipeId),
recipeSearch: (instancePath: string, query: string) =>
ipcRenderer.invoke("recipe:search", instancePath, query),

analyzeCrashLog: (content: string, knownModIds: string[]) =>
ipcRenderer.invoke("crash:analyze", content, knownModIds),

exportModpack: (instancePath: string, packName: string, mcVersion: string, loaderType: string, loaderVersion: string) =>
ipcRenderer.invoke("export:modpack", instancePath, packName, mcVersion, loaderType, loaderVersion),
});

declare global {
Expand Down Expand Up @@ -245,8 +266,31 @@ declare global {
discordClearMod: () => Promise<{ success: boolean; error?: string }>;
discordClearInstance: () => Promise<{ success: boolean; error?: string }>;

// Game Launcher APIs removed due to Java compatibility issues
// TODO: Re-implement in future when stable solution is found
analyzeCrashLog: (content: string, knownModIds: string[]) => Promise<{
success: boolean;
mainCause?: string | null;
issues?: Array<{ modId: string; reason: string; line: string }>;
suspectedMods?: string[];
totalLines?: number;
error?: string;
}>;
exportModpack: (instancePath: string, packName: string, mcVersion: string, loaderType: string, loaderVersion: string) =>
Promise<{ success: boolean; outputPath?: string; error?: string }>;

// Game Launcher
launchGame: (
instancePath: string,
launcher: string,
mcVersion: string,
loaderVersion: string,
jvmXmx?: number,
jvmXms?: number
) => Promise<{ success: boolean; pid?: number; error?: string }>;
killGame: (instancePath: string) => Promise<{ success: boolean; error?: string }>;
isGameRunning: (instancePath: string) => Promise<boolean>;
getRunningGames: () => Promise<string[]>;
onGameLog: (callback: (data: { line: string; type: 'stdout' | 'stderr' | 'system'; instancePath: string; timestamp: number }) => void) => void;
removeGameLogListener: () => void;

checkForUpdates: () => Promise<{ success: boolean; updateInfo?: UpdateInfo; error?: string }>;
downloadUpdate: () => Promise<{ success: boolean; error?: string }>;
Expand Down
Loading