Publish Forma Engine 0.3.0 source with documentation and CI

This commit is contained in:
emil28092005
2026-09-09 15:49:08 +03:00
commit e52bc0e33b
70 changed files with 19610 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+103
View File
@@ -0,0 +1,103 @@
const {
app,
BrowserWindow,
protocol,
session,
Menu,
dialog,
} = require("electron");
const path = require("node:path");
const { readGame } = require("./protocol.cjs");
const settings = require("./game-config.json");
protocol.registerSchemesAsPrivileged([
{
scheme: "forma",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
},
},
]);
app.setName(settings.name);
let win;
async function createWindow() {
win = new BrowserWindow({
title: settings.name,
width: settings.width,
height: settings.height,
minWidth: 320,
minHeight: 320,
fullscreen: settings.fullscreen,
backgroundColor: "#151719",
show: false,
autoHideMenuBar: true,
webPreferences: {
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
nodeIntegrationInWorker: false,
webSecurity: true,
devTools: settings.mode === "debug",
},
});
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
win.webContents.on("will-navigate", (e, url) => {
if (url !== "forma://game/index.html") e.preventDefault();
});
win.webContents.on("will-attach-webview", (e) => e.preventDefault());
win.webContents.on("before-input-event", (e, input) => {
if (input.type === "keyDown" && input.key === "F11") {
win.setFullScreen(!win.isFullScreen());
e.preventDefault();
}
if (
input.type === "keyDown" &&
input.key === "Escape" &&
win.isFullScreen()
)
win.setFullScreen(false);
});
win.once("ready-to-show", () => win.show());
await win.loadURL("forma://game/index.html");
}
app
.whenReady()
.then(async () => {
if (app.commandLine.hasSwitch("no-sandbox")) {
dialog.showErrorBox(
"Sandbox unavailable",
"This game requires Chromium sandbox support. Enable unprivileged user namespaces on Linux, then restart without --no-sandbox.",
);
app.exit(1);
return;
}
Menu.setApplicationMenu(null);
session.defaultSession.setPermissionRequestHandler((_wc, _p, cb) =>
cb(false),
);
session.defaultSession.setPermissionCheckHandler(() => false);
session.defaultSession.webRequest.onBeforeRequest((details, cb) =>
cb({
cancel:
!details.url.startsWith("forma://game/") &&
!details.url.startsWith("blob:forma://game/"),
}),
);
protocol.handle("forma", async (request) => {
const r = await readGame(
path.join(__dirname, "game"),
request.url,
request.method,
);
return new Response(r.body, r);
});
await createWindow();
})
.catch((e) => {
console.error(e);
app.exit(1);
});
app.on("window-all-closed", () => app.quit());
+60
View File
@@ -0,0 +1,60 @@
const path = require("node:path");
const fs = require("node:fs/promises");
const mime = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json",
".wasm": "application/wasm",
".glb": "model/gltf-binary",
".gltf": "model/gltf+json",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml",
};
const csp =
"default-src 'none'; script-src 'self' 'unsafe-eval'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' data: blob:; font-src 'self' data:; media-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-src 'none'";
async function readGame(root, url, method = "GET") {
if (!["GET", "HEAD"].includes(method))
return { status: 405, body: "Method not allowed" };
let u, name;
try {
u = new URL(url);
name = decodeURIComponent(u.pathname);
} catch {
return { status: 400, body: "Bad URL" };
}
if (
u.protocol !== "forma:" ||
u.hostname !== "game" ||
u.port ||
u.username ||
u.password ||
name.includes("\\") ||
name.includes("\0")
)
return { status: 403, body: "Forbidden" };
const file = path.resolve(root, "." + (name === "/" ? "/index.html" : name));
if (!file.startsWith(path.resolve(root) + path.sep))
return { status: 403, body: "Forbidden" };
try {
const real = await fs.realpath(file);
if (!real.startsWith((await fs.realpath(root)) + path.sep))
return { status: 403, body: "Forbidden" };
const bytes = await fs.readFile(real);
return {
status: 200,
headers: {
"Content-Type": mime[path.extname(file)] || "application/octet-stream",
"Content-Security-Policy": csp,
"X-Content-Type-Options": "nosniff",
"Cache-Control": "no-store",
},
body: method === "HEAD" ? null : bytes,
};
} catch {
return { status: 404, body: "Not found" };
}
}
module.exports = { readGame, csp };