Layonara Forge — NWN Development IDE
Electron desktop application for Neverwinter Nights module development. Clone, edit, build, and run a complete Layonara NWNX server with only Docker required. - React 19 + Vite frontend with Monaco editor and NWScript LSP - Node.js + Express backend managing Docker sibling containers - Electron shell with Docker availability check and auto-setup - Builder image auto-builds on first use from bundled Dockerfile - Cross-platform: Windows (.exe), macOS (.dmg), Linux (.AppImage) - Gitea Actions CI for automated release builds
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { app, BrowserWindow, shell, dialog } from "electron";
|
||||
import path from "path";
|
||||
import { checkDocker, dockerDownloadUrl } from "./docker-check";
|
||||
import { defaultWorkspacePath, detectNwnHome } from "./paths";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let serverPort: number;
|
||||
|
||||
function setEnvironment(): void {
|
||||
if (!process.env.WORKSPACE_PATH) {
|
||||
process.env.WORKSPACE_PATH = defaultWorkspacePath();
|
||||
}
|
||||
if (!process.env.NWN_HOME_PATH) {
|
||||
const detected = detectNwnHome();
|
||||
if (detected) process.env.NWN_HOME_PATH = detected;
|
||||
}
|
||||
if (!process.env.GIT_PROVIDER_URL) {
|
||||
process.env.GIT_PROVIDER_URL = "https://gitea.layonara.com";
|
||||
}
|
||||
process.env.ELECTRON = "1";
|
||||
}
|
||||
|
||||
async function startBackend(): Promise<number> {
|
||||
const { startServer } = await import(
|
||||
path.join(__dirname, "../packages/backend/dist/index.js")
|
||||
);
|
||||
const port = await findFreePort();
|
||||
await startServer(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
function findFreePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const net = require("net");
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, () => {
|
||||
const port = srv.address().port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
srv.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow(port: number): BrowserWindow {
|
||||
const win = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 900,
|
||||
minWidth: 960,
|
||||
minHeight: 600,
|
||||
title: "Layonara Forge",
|
||||
icon: path.join(__dirname, "../assets/icon.png"),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
win.loadURL(`http://localhost:${port}`);
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith("http")) shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
win.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
async function showDockerMissing(): Promise<boolean> {
|
||||
const url = dockerDownloadUrl();
|
||||
const { response } = await dialog.showMessageBox({
|
||||
type: "warning",
|
||||
title: "Docker Required",
|
||||
message: "Layonara Forge requires Docker to build modules and run the NWN server.",
|
||||
detail: "Docker Desktop must be installed and running before using the Forge.\n\nClick \"Download Docker\" to open the download page, or \"Check Again\" after installing.",
|
||||
buttons: ["Download Docker", "Check Again", "Quit"],
|
||||
defaultId: 0,
|
||||
cancelId: 2,
|
||||
});
|
||||
|
||||
if (response === 0) {
|
||||
shell.openExternal(url);
|
||||
return showDockerMissing();
|
||||
}
|
||||
if (response === 1) {
|
||||
const status = await checkDocker();
|
||||
if (status.available) return true;
|
||||
return showDockerMissing();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
setEnvironment();
|
||||
|
||||
const docker = await checkDocker();
|
||||
if (!docker.available) {
|
||||
const installed = await showDockerMissing();
|
||||
if (!installed) {
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
serverPort = await startBackend();
|
||||
} catch (err: any) {
|
||||
dialog.showErrorBox(
|
||||
"Forge Startup Error",
|
||||
`Failed to start the backend server:\n\n${err.message}`,
|
||||
);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow = createWindow(serverPort);
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (mainWindow === null && serverPort) {
|
||||
mainWindow = createWindow(serverPort);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user