mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 16:04:03 +00:00
Add OverlayOptions for configurable positioning (anchor, margins, offsets, percentages). Add OverlayHandle for programmatic visibility control with hide/setHidden/isHidden. Add visible callback for responsive overlays. Extension API: ctx.ui.custom() now accepts overlayOptions and onHandle callback. Examples: overlay-qa-tests.ts (10 test commands), doom-overlay (DOOM at 35 FPS).
173 lines
4.8 KiB
TypeScript
173 lines
4.8 KiB
TypeScript
/**
|
|
* DOOM Engine - WebAssembly wrapper for doomgeneric
|
|
*/
|
|
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export interface DoomModule {
|
|
_doomgeneric_Create: (argc: number, argv: number) => void;
|
|
_doomgeneric_Tick: () => void;
|
|
_DG_GetFrameBuffer: () => number;
|
|
_DG_GetScreenWidth: () => number;
|
|
_DG_GetScreenHeight: () => number;
|
|
_DG_PushKeyEvent: (pressed: number, key: number) => void;
|
|
_malloc: (size: number) => number;
|
|
_free: (ptr: number) => void;
|
|
HEAPU8: Uint8Array;
|
|
HEAPU32: Uint32Array;
|
|
FS_createDataFile: (parent: string, name: string, data: number[], canRead: boolean, canWrite: boolean) => void;
|
|
FS_createPath: (parent: string, path: string, canRead: boolean, canWrite: boolean) => string;
|
|
setValue: (ptr: number, value: number, type: string) => void;
|
|
getValue: (ptr: number, type: string) => number;
|
|
}
|
|
|
|
export class DoomEngine {
|
|
private module: DoomModule | null = null;
|
|
private frameBufferPtr: number = 0;
|
|
private initialized = false;
|
|
private wadPath: string;
|
|
private _width = 640;
|
|
private _height = 400;
|
|
|
|
constructor(wadPath: string) {
|
|
this.wadPath = wadPath;
|
|
}
|
|
|
|
get width(): number {
|
|
return this._width;
|
|
}
|
|
|
|
get height(): number {
|
|
return this._height;
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
// Locate WASM build
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const buildDir = join(__dirname, "doom", "build");
|
|
const doomJsPath = join(buildDir, "doom.js");
|
|
|
|
if (!existsSync(doomJsPath)) {
|
|
throw new Error(`WASM not found at ${doomJsPath}. Run ./doom/build.sh first`);
|
|
}
|
|
|
|
// Read WAD file
|
|
const wadData = readFileSync(this.wadPath);
|
|
const wadArray = Array.from(new Uint8Array(wadData));
|
|
|
|
// Load WASM module - eval to bypass jiti completely
|
|
const doomJsCode = readFileSync(doomJsPath, "utf-8");
|
|
const moduleExports: { exports: unknown } = { exports: {} };
|
|
const nativeRequire = createRequire(doomJsPath);
|
|
const moduleFunc = new Function("module", "exports", "__dirname", "__filename", "require", doomJsCode);
|
|
moduleFunc(moduleExports, moduleExports.exports, buildDir, doomJsPath, nativeRequire);
|
|
const createDoomModule = moduleExports.exports as (config: unknown) => Promise<DoomModule>;
|
|
|
|
const moduleConfig = {
|
|
locateFile: (path: string) => {
|
|
if (path.endsWith(".wasm")) {
|
|
return join(buildDir, path);
|
|
}
|
|
return path;
|
|
},
|
|
print: () => {},
|
|
printErr: () => {},
|
|
preRun: [
|
|
(module: DoomModule) => {
|
|
// Create /doom directory and add WAD
|
|
module.FS_createPath("/", "doom", true, true);
|
|
module.FS_createDataFile("/doom", "doom1.wad", wadArray, true, false);
|
|
},
|
|
],
|
|
};
|
|
|
|
this.module = await createDoomModule(moduleConfig);
|
|
if (!this.module) {
|
|
throw new Error("Failed to initialize DOOM module");
|
|
}
|
|
|
|
// Initialize DOOM
|
|
this.initDoom();
|
|
|
|
// Get framebuffer info
|
|
this.frameBufferPtr = this.module._DG_GetFrameBuffer();
|
|
this._width = this.module._DG_GetScreenWidth();
|
|
this._height = this.module._DG_GetScreenHeight();
|
|
this.initialized = true;
|
|
}
|
|
|
|
private initDoom(): void {
|
|
if (!this.module) return;
|
|
|
|
const args = ["doom", "-iwad", "/doom/doom1.wad"];
|
|
const argPtrs: number[] = [];
|
|
|
|
for (const arg of args) {
|
|
const ptr = this.module._malloc(arg.length + 1);
|
|
for (let i = 0; i < arg.length; i++) {
|
|
this.module.setValue(ptr + i, arg.charCodeAt(i), "i8");
|
|
}
|
|
this.module.setValue(ptr + arg.length, 0, "i8");
|
|
argPtrs.push(ptr);
|
|
}
|
|
|
|
const argvPtr = this.module._malloc(argPtrs.length * 4);
|
|
for (let i = 0; i < argPtrs.length; i++) {
|
|
this.module.setValue(argvPtr + i * 4, argPtrs[i]!, "i32");
|
|
}
|
|
|
|
this.module._doomgeneric_Create(args.length, argvPtr);
|
|
|
|
for (const ptr of argPtrs) {
|
|
this.module._free(ptr);
|
|
}
|
|
this.module._free(argvPtr);
|
|
}
|
|
|
|
/**
|
|
* Run one game tick
|
|
*/
|
|
tick(): void {
|
|
if (!this.module || !this.initialized) return;
|
|
this.module._doomgeneric_Tick();
|
|
}
|
|
|
|
/**
|
|
* Get current frame as RGBA pixel data
|
|
* DOOM outputs ARGB, we convert to RGBA
|
|
*/
|
|
getFrameRGBA(): Uint8Array {
|
|
if (!this.module || !this.initialized) {
|
|
return new Uint8Array(this._width * this._height * 4);
|
|
}
|
|
|
|
const pixels = this._width * this._height;
|
|
const buffer = new Uint8Array(pixels * 4);
|
|
|
|
for (let i = 0; i < pixels; i++) {
|
|
const argb = this.module.getValue(this.frameBufferPtr + i * 4, "i32");
|
|
const offset = i * 4;
|
|
buffer[offset + 0] = (argb >> 16) & 0xff; // R
|
|
buffer[offset + 1] = (argb >> 8) & 0xff; // G
|
|
buffer[offset + 2] = argb & 0xff; // B
|
|
buffer[offset + 3] = 255; // A
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* Push a key event
|
|
*/
|
|
pushKey(pressed: boolean, key: number): void {
|
|
if (!this.module || !this.initialized) return;
|
|
this.module._DG_PushKeyEvent(pressed ? 1 : 0, key);
|
|
}
|
|
|
|
isInitialized(): boolean {
|
|
return this.initialized;
|
|
}
|
|
}
|