/** * 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 { // 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; 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; } }