@muhammedaksam/opentui-doom 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/package.json +13 -6
- package/src/debug.ts +3 -3
- package/src/doom-audio.ts +146 -146
- package/src/doom-engine.ts +299 -304
- package/src/doom-input.ts +173 -173
- package/src/doom-mouse.ts +106 -106
- package/src/doom-saves.ts +60 -60
- package/src/index.ts +27 -28
package/src/doom-engine.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DOOM Engine - WebAssembly wrapper for doomgeneric
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Handles loading and running the DOOM WASM module,
|
|
5
5
|
* providing a TypeScript interface for the game.
|
|
6
6
|
*/
|
|
@@ -15,339 +15,334 @@ export const DOOM_WIDTH = 1280;
|
|
|
15
15
|
export const DOOM_HEIGHT = 800;
|
|
16
16
|
|
|
17
17
|
export interface DoomModule {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
) =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
46
|
-
ccall: (name: string, returnType: string | null, argTypes: string[], args: any[]) => any;
|
|
47
|
-
cwrap: (name: string, returnType: string | null, argTypes: string[]) => (...args: any[]) => any;
|
|
48
|
-
setValue: (ptr: number, value: number, type: string) => void;
|
|
49
|
-
getValue: (ptr: number, type: string) => number;
|
|
18
|
+
_doomgeneric_Create: (argc: number, argv: number) => void;
|
|
19
|
+
_doomgeneric_Tick: () => void;
|
|
20
|
+
_DG_GetFrameBuffer: () => number;
|
|
21
|
+
_DG_PushKeyEvent: (pressed: number, key: number) => void;
|
|
22
|
+
_malloc: (size: number) => number;
|
|
23
|
+
_free: (ptr: number) => void;
|
|
24
|
+
HEAPU8: Uint8Array;
|
|
25
|
+
HEAPU32: Uint32Array;
|
|
26
|
+
FS_createDataFile: (
|
|
27
|
+
parent: string,
|
|
28
|
+
name: string,
|
|
29
|
+
data: number[],
|
|
30
|
+
canRead: boolean,
|
|
31
|
+
canWrite: boolean,
|
|
32
|
+
canOwn?: boolean
|
|
33
|
+
) => void;
|
|
34
|
+
FS_createPath: (parent: string, path: string, canRead: boolean, canWrite: boolean) => string;
|
|
35
|
+
FS?: {
|
|
36
|
+
readFile: (path: string, opts?: { encoding?: string }) => Uint8Array;
|
|
37
|
+
readdir: (path: string) => string[];
|
|
38
|
+
stat: (path: string) => { mode: number };
|
|
39
|
+
isDir: (mode: number) => boolean;
|
|
40
|
+
};
|
|
41
|
+
ccall: (name: string, returnType: string | null, argTypes: string[], args: any[]) => any;
|
|
42
|
+
cwrap: (name: string, returnType: string | null, argTypes: string[]) => (...args: any[]) => any;
|
|
43
|
+
setValue: (ptr: number, value: number, type: string) => void;
|
|
44
|
+
getValue: (ptr: number, type: string) => number;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
export interface DoomEngineOptions {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
wadPath: string;
|
|
49
|
+
print?: (text: string) => void;
|
|
50
|
+
printErr?: (text: string) => void;
|
|
51
|
+
onQuit?: () => void;
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
export class DoomEngine {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
55
|
+
private module: DoomModule | null = null;
|
|
56
|
+
private frameBufferPtr: number = 0;
|
|
57
|
+
private initialized: boolean = false;
|
|
58
|
+
private wadPath: string;
|
|
59
|
+
private print: (text: string) => void;
|
|
60
|
+
private printErr: (text: string) => void;
|
|
61
|
+
private onQuit: (() => void) | null = null;
|
|
62
|
+
private emscriptenFS: any = null; // FS reference captured from Emscripten
|
|
63
|
+
|
|
64
|
+
constructor(optionsOrPath: string | DoomEngineOptions) {
|
|
65
|
+
if (typeof optionsOrPath === "string") {
|
|
66
|
+
this.wadPath = resolve(optionsOrPath);
|
|
67
|
+
this.print = (text: string) => console.log("[DOOM]", text);
|
|
68
|
+
this.printErr = (text: string) => console.error("[DOOM]", text);
|
|
69
|
+
} else {
|
|
70
|
+
this.wadPath = resolve(optionsOrPath.wadPath);
|
|
71
|
+
this.print = optionsOrPath.print || ((text: string) => console.log("[DOOM]", text));
|
|
72
|
+
this.printErr = optionsOrPath.printErr || ((text: string) => console.error("[DOOM]", text));
|
|
73
|
+
this.onQuit = optionsOrPath.onQuit || null;
|
|
80
74
|
}
|
|
75
|
+
}
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Read WAD file first
|
|
88
|
-
const wadData = await readFile(this.wadPath);
|
|
89
|
-
const wadArray = Array.from(new Uint8Array(wadData));
|
|
90
|
-
|
|
91
|
-
// Dynamic import of the compiled DOOM module
|
|
92
|
-
const createDoomModule = require(doomJsPath);
|
|
93
|
-
|
|
94
|
-
// Import audio system
|
|
95
|
-
const audio = await import("./doom-audio");
|
|
96
|
-
|
|
97
|
-
// Create module with proper callbacks
|
|
98
|
-
const moduleConfig: any = {
|
|
99
|
-
locateFile: (path: string) => {
|
|
100
|
-
if (path.endsWith('.wasm')) {
|
|
101
|
-
return join(buildDir, path);
|
|
102
|
-
}
|
|
103
|
-
return path;
|
|
104
|
-
},
|
|
105
|
-
print: (text: string) => this.print(text),
|
|
106
|
-
printErr: (text: string) => this.printErr(text),
|
|
107
|
-
|
|
108
|
-
// Audio callbacks - called from C via EM_ASM
|
|
109
|
-
initAudio: () => audio.initAudio(),
|
|
110
|
-
shutdownAudio: () => audio.shutdownAudio(),
|
|
111
|
-
playSound: (name: string, volume: number) => audio.playSound(name, volume),
|
|
112
|
-
playMusic: (name: string, looping: boolean) => audio.playMusic(name, looping),
|
|
113
|
-
stopMusic: () => audio.stopMusic(),
|
|
114
|
-
setMusicVolume: (volume: number) => audio.setMusicVolume(volume),
|
|
115
|
-
|
|
116
|
-
// Game lifecycle callbacks - called from C via EM_ASM
|
|
117
|
-
quitGame: () => {
|
|
118
|
-
debugLog('Engine', 'quitGame callback called from WASM');
|
|
119
|
-
debugLog('Engine', `this.onQuit is: ${this.onQuit ? 'defined' : 'undefined'}`);
|
|
120
|
-
if (this.onQuit) {
|
|
121
|
-
debugLog('Engine', 'calling this.onQuit()');
|
|
122
|
-
this.onQuit();
|
|
123
|
-
debugLog('Engine', 'this.onQuit() returned');
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
// preRun receives Module as first argument
|
|
128
|
-
preRun: [
|
|
129
|
-
(module: any) => {
|
|
130
|
-
// Create /doom directory for WAD
|
|
131
|
-
module.FS_createPath("/", "doom", true, true);
|
|
132
|
-
// Write WAD file to virtual filesystem
|
|
133
|
-
module.FS_createDataFile("/doom", "doom1.wad", wadArray, true, false);
|
|
134
|
-
|
|
135
|
-
// Create .savegame directory for saves (DOOM looks here by default)
|
|
136
|
-
module.FS_createPath("/", ".savegame", true, true);
|
|
137
|
-
|
|
138
|
-
// Create default.cfg with WASD key bindings
|
|
139
|
-
// This is needed because we send character codes for WASD (to allow typing in save dialogs)
|
|
140
|
-
// Key codes: w=119, a=97, s=115, d=100
|
|
141
|
-
const defaultConfig = [
|
|
142
|
-
"key_up 119", // 'w' for forward
|
|
143
|
-
"key_down 115", // 's' for backward
|
|
144
|
-
"key_strafeleft 97", // 'a' for strafe left
|
|
145
|
-
"key_straferight 100", // 'd' for strafe right
|
|
146
|
-
].join("\n") + "\n";
|
|
147
|
-
const configArray = Array.from(new TextEncoder().encode(defaultConfig));
|
|
148
|
-
try {
|
|
149
|
-
module.FS_createDataFile("/", "default.cfg", configArray, true, false);
|
|
150
|
-
debugLog("Engine", "Created default.cfg with WASD key bindings");
|
|
151
|
-
} catch (e) {
|
|
152
|
-
debugLog("Engine", `Failed to create default.cfg: ${e}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Load existing saves from ~/.opentui-doom/ into virtual filesystem
|
|
156
|
-
const existingSaves = loadExistingSaves();
|
|
157
|
-
for (const [slot, data] of existingSaves) {
|
|
158
|
-
const filename = `doomsav${slot}.dsg`;
|
|
159
|
-
try {
|
|
160
|
-
module.FS_createDataFile("/.savegame", filename, Array.from(data), true, true);
|
|
161
|
-
debugLog("Engine", `Pre-loaded save slot ${slot} to virtual FS`);
|
|
162
|
-
} catch (e) {
|
|
163
|
-
debugLog("Engine", `Failed to pre-load save slot ${slot}: ${e}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
],
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
this.module = await createDoomModule(moduleConfig);
|
|
171
|
-
|
|
172
|
-
if (!this.module) {
|
|
173
|
-
throw new Error("Failed to initialize DOOM module");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Capture FS reference after module is fully loaded
|
|
177
|
-
// Try different methods to access Emscripten's FS
|
|
178
|
-
if ((this.module as any).FS) {
|
|
179
|
-
this.emscriptenFS = (this.module as any).FS;
|
|
180
|
-
debugLog("Engine", "Captured FS from module.FS");
|
|
181
|
-
} else if (typeof (globalThis as any).FS !== 'undefined') {
|
|
182
|
-
this.emscriptenFS = (globalThis as any).FS;
|
|
183
|
-
debugLog("Engine", "Captured FS from globalThis.FS");
|
|
184
|
-
} else {
|
|
185
|
-
debugLog("Engine", "Warning: Could not find Emscripten FS object");
|
|
186
|
-
}
|
|
77
|
+
async init(): Promise<void> {
|
|
78
|
+
// Load the WASM module
|
|
79
|
+
const buildDir = join(import.meta.dir, "..", "doom", "build");
|
|
80
|
+
const doomJsPath = join(buildDir, "doom.js");
|
|
187
81
|
|
|
188
|
-
|
|
189
|
-
|
|
82
|
+
// Read WAD file first
|
|
83
|
+
const wadData = await readFile(this.wadPath);
|
|
84
|
+
const wadArray = Array.from(new Uint8Array(wadData));
|
|
190
85
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.initialized = true;
|
|
194
|
-
}
|
|
86
|
+
// Dynamic import of the compiled DOOM module
|
|
87
|
+
const createDoomModule = require(doomJsPath);
|
|
195
88
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const module = this.module;
|
|
200
|
-
|
|
201
|
-
const args = [
|
|
202
|
-
"doom",
|
|
203
|
-
"-iwad",
|
|
204
|
-
"/doom/doom1.wad"
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
// Allocate memory for argv using ccall for strings
|
|
208
|
-
const argPtrs: number[] = [];
|
|
209
|
-
for (const arg of args) {
|
|
210
|
-
// Allocate space for string + null terminator
|
|
211
|
-
const ptr = module._malloc(arg.length + 1);
|
|
212
|
-
// Use setValue to write each character
|
|
213
|
-
for (let i = 0; i < arg.length; i++) {
|
|
214
|
-
module.setValue(ptr + i, arg.charCodeAt(i), 'i8');
|
|
215
|
-
}
|
|
216
|
-
// Null terminate
|
|
217
|
-
module.setValue(ptr + arg.length, 0, 'i8');
|
|
218
|
-
argPtrs.push(ptr);
|
|
219
|
-
}
|
|
89
|
+
// Import audio system
|
|
90
|
+
const audio = await import("./doom-audio");
|
|
220
91
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
92
|
+
// Create module with proper callbacks
|
|
93
|
+
const moduleConfig: any = {
|
|
94
|
+
locateFile: (path: string) => {
|
|
95
|
+
if (path.endsWith(".wasm")) {
|
|
96
|
+
return join(buildDir, path);
|
|
97
|
+
}
|
|
98
|
+
return path;
|
|
99
|
+
},
|
|
100
|
+
print: (text: string) => this.print(text),
|
|
101
|
+
printErr: (text: string) => this.printErr(text),
|
|
102
|
+
|
|
103
|
+
// Audio callbacks - called from C via EM_ASM
|
|
104
|
+
initAudio: () => audio.initAudio(),
|
|
105
|
+
shutdownAudio: () => audio.shutdownAudio(),
|
|
106
|
+
playSound: (name: string, volume: number) => audio.playSound(name, volume),
|
|
107
|
+
playMusic: (name: string, looping: boolean) => audio.playMusic(name, looping),
|
|
108
|
+
stopMusic: () => audio.stopMusic(),
|
|
109
|
+
setMusicVolume: (volume: number) => audio.setMusicVolume(volume),
|
|
110
|
+
|
|
111
|
+
// Game lifecycle callbacks - called from C via EM_ASM
|
|
112
|
+
quitGame: () => {
|
|
113
|
+
debugLog("Engine", "quitGame callback called from WASM");
|
|
114
|
+
debugLog("Engine", `this.onQuit is: ${this.onQuit ? "defined" : "undefined"}`);
|
|
115
|
+
if (this.onQuit) {
|
|
116
|
+
debugLog("Engine", "calling this.onQuit()");
|
|
117
|
+
this.onQuit();
|
|
118
|
+
debugLog("Engine", "this.onQuit() returned");
|
|
228
119
|
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// preRun receives Module as first argument
|
|
123
|
+
preRun: [
|
|
124
|
+
(module: any) => {
|
|
125
|
+
// Create /doom directory for WAD
|
|
126
|
+
module.FS_createPath("/", "doom", true, true);
|
|
127
|
+
// Write WAD file to virtual filesystem
|
|
128
|
+
module.FS_createDataFile("/doom", "doom1.wad", wadArray, true, false);
|
|
129
|
+
|
|
130
|
+
// Create .savegame directory for saves (DOOM looks here by default)
|
|
131
|
+
module.FS_createPath("/", ".savegame", true, true);
|
|
132
|
+
|
|
133
|
+
// Create default.cfg with WASD key bindings
|
|
134
|
+
// This is needed because we send character codes for WASD (to allow typing in save dialogs)
|
|
135
|
+
// Key codes: w=119, a=97, s=115, d=100
|
|
136
|
+
const defaultConfig =
|
|
137
|
+
[
|
|
138
|
+
"key_up 119", // 'w' for forward
|
|
139
|
+
"key_down 115", // 's' for backward
|
|
140
|
+
"key_strafeleft 97", // 'a' for strafe left
|
|
141
|
+
"key_straferight 100", // 'd' for strafe right
|
|
142
|
+
].join("\n") + "\n";
|
|
143
|
+
const configArray = Array.from(new TextEncoder().encode(defaultConfig));
|
|
144
|
+
try {
|
|
145
|
+
module.FS_createDataFile("/", "default.cfg", configArray, true, false);
|
|
146
|
+
debugLog("Engine", "Created default.cfg with WASD key bindings");
|
|
147
|
+
} catch (e) {
|
|
148
|
+
debugLog("Engine", `Failed to create default.cfg: ${e}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Load existing saves from ~/.opentui-doom/ into virtual filesystem
|
|
152
|
+
const existingSaves = loadExistingSaves();
|
|
153
|
+
for (const [slot, data] of existingSaves) {
|
|
154
|
+
const filename = `doomsav${slot}.dsg`;
|
|
155
|
+
try {
|
|
156
|
+
module.FS_createDataFile("/.savegame", filename, Array.from(data), true, true);
|
|
157
|
+
debugLog("Engine", `Pre-loaded save slot ${slot} to virtual FS`);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
debugLog("Engine", `Failed to pre-load save slot ${slot}: ${e}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
229
165
|
|
|
230
|
-
|
|
231
|
-
module._doomgeneric_Create(args.length, argvPtr);
|
|
166
|
+
this.module = await createDoomModule(moduleConfig);
|
|
232
167
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
module._free(ptr);
|
|
236
|
-
}
|
|
237
|
-
module._free(argvPtr);
|
|
168
|
+
if (!this.module) {
|
|
169
|
+
throw new Error("Failed to initialize DOOM module");
|
|
238
170
|
}
|
|
239
171
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
172
|
+
// Capture FS reference after module is fully loaded
|
|
173
|
+
// Try different methods to access Emscripten's FS
|
|
174
|
+
if ((this.module as any).FS) {
|
|
175
|
+
this.emscriptenFS = (this.module as any).FS;
|
|
176
|
+
debugLog("Engine", "Captured FS from module.FS");
|
|
177
|
+
} else if (typeof (globalThis as any).FS !== "undefined") {
|
|
178
|
+
this.emscriptenFS = (globalThis as any).FS;
|
|
179
|
+
debugLog("Engine", "Captured FS from globalThis.FS");
|
|
180
|
+
} else {
|
|
181
|
+
debugLog("Engine", "Warning: Could not find Emscripten FS object");
|
|
246
182
|
}
|
|
247
183
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
184
|
+
// Initialize DOOM
|
|
185
|
+
this.initDoom();
|
|
186
|
+
|
|
187
|
+
// Get framebuffer pointer
|
|
188
|
+
this.frameBufferPtr = this.module._DG_GetFrameBuffer();
|
|
189
|
+
this.initialized = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private initDoom(): void {
|
|
193
|
+
if (!this.module) return;
|
|
194
|
+
|
|
195
|
+
const module = this.module;
|
|
196
|
+
|
|
197
|
+
const args = ["doom", "-iwad", "/doom/doom1.wad"];
|
|
198
|
+
|
|
199
|
+
// Allocate memory for argv using ccall for strings
|
|
200
|
+
const argPtrs: number[] = [];
|
|
201
|
+
for (const arg of args) {
|
|
202
|
+
// Allocate space for string + null terminator
|
|
203
|
+
const ptr = module._malloc(arg.length + 1);
|
|
204
|
+
// Use setValue to write each character
|
|
205
|
+
for (let i = 0; i < arg.length; i++) {
|
|
206
|
+
module.setValue(ptr + i, arg.charCodeAt(i), "i8");
|
|
207
|
+
}
|
|
208
|
+
// Null terminate
|
|
209
|
+
module.setValue(ptr + arg.length, 0, "i8");
|
|
210
|
+
argPtrs.push(ptr);
|
|
211
|
+
}
|
|
256
212
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
buffer[offset + 3] = 255; // A (always opaque)
|
|
269
|
-
}
|
|
213
|
+
// Create argv array
|
|
214
|
+
const argvPtr = module._malloc(argPtrs.length * 4);
|
|
215
|
+
for (let i = 0; i < argPtrs.length; i++) {
|
|
216
|
+
const ptr = argPtrs[i];
|
|
217
|
+
if (ptr !== undefined) {
|
|
218
|
+
module.setValue(argvPtr + i * 4, ptr, "i32");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Call doomgeneric_Create
|
|
223
|
+
module._doomgeneric_Create(args.length, argvPtr);
|
|
270
224
|
|
|
271
|
-
|
|
225
|
+
// Free argv (DOOM copies the strings)
|
|
226
|
+
for (const ptr of argPtrs) {
|
|
227
|
+
module._free(ptr);
|
|
228
|
+
}
|
|
229
|
+
module._free(argvPtr);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Run one game tick - called each frame
|
|
234
|
+
*/
|
|
235
|
+
tick(): void {
|
|
236
|
+
if (!this.module || !this.initialized) return;
|
|
237
|
+
this.module._doomgeneric_Tick();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get the current frame as RGBA pixel data
|
|
242
|
+
* DOOM uses ARGB format, so we need to convert
|
|
243
|
+
*/
|
|
244
|
+
getFrameBuffer(): Uint8Array {
|
|
245
|
+
if (!this.module || !this.initialized) {
|
|
246
|
+
return new Uint8Array(DOOM_WIDTH * DOOM_HEIGHT * 4);
|
|
272
247
|
}
|
|
273
248
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
249
|
+
const pixels = DOOM_WIDTH * DOOM_HEIGHT;
|
|
250
|
+
const buffer = new Uint8Array(pixels * 4);
|
|
251
|
+
const module = this.module;
|
|
252
|
+
|
|
253
|
+
// Read ARGB data from DOOM's framebuffer using getValue
|
|
254
|
+
for (let i = 0; i < pixels; i++) {
|
|
255
|
+
const argb = module.getValue(this.frameBufferPtr + i * 4, "i32");
|
|
256
|
+
const offset = i * 4;
|
|
257
|
+
buffer[offset + 0] = (argb >> 16) & 0xff; // R
|
|
258
|
+
buffer[offset + 1] = (argb >> 8) & 0xff; // G
|
|
259
|
+
buffer[offset + 2] = argb & 0xff; // B
|
|
260
|
+
buffer[offset + 3] = 255; // A (always opaque)
|
|
280
261
|
}
|
|
281
262
|
|
|
282
|
-
|
|
283
|
-
|
|
263
|
+
return buffer;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Push a key event to DOOM
|
|
268
|
+
*/
|
|
269
|
+
pushKey(pressed: boolean, key: number): void {
|
|
270
|
+
if (!this.module || !this.initialized) return;
|
|
271
|
+
this.module._DG_PushKeyEvent(pressed ? 1 : 0, key);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
isInitialized(): boolean {
|
|
275
|
+
return this.initialized;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Sync save games from the virtual filesystem to disk (~/.opentui-doom/)
|
|
280
|
+
* Call this periodically or after save operations to persist saves
|
|
281
|
+
*/
|
|
282
|
+
syncSaves(): void {
|
|
283
|
+
if (!this.module || !this.emscriptenFS) {
|
|
284
|
+
debugLog("Engine", "syncSaves: module or FS not available");
|
|
285
|
+
return;
|
|
284
286
|
}
|
|
285
287
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const FS = this.emscriptenFS;
|
|
307
|
-
|
|
308
|
-
// List root directory to see what exists
|
|
288
|
+
// DOOM can save to different paths depending on configuration
|
|
289
|
+
// Try multiple possible locations
|
|
290
|
+
const savePaths = [
|
|
291
|
+
"/", // Root
|
|
292
|
+
"/.savegame", // Default when configdir is "."
|
|
293
|
+
".savegame", // Relative path (CWD)
|
|
294
|
+
"/doom", // Our custom path
|
|
295
|
+
"/tmp", // Temp directory
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
const FS = this.emscriptenFS;
|
|
299
|
+
|
|
300
|
+
// List root directory to see what exists
|
|
301
|
+
try {
|
|
302
|
+
const rootEntries = FS.readdir("/");
|
|
303
|
+
debugLog("Engine", `VFS root contents: ${rootEntries.join(", ")}`);
|
|
304
|
+
|
|
305
|
+
// Check each directory at root
|
|
306
|
+
for (const entry of rootEntries) {
|
|
307
|
+
if (entry === "." || entry === "..") continue;
|
|
309
308
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
const stat = FS.stat(`/${entry}`);
|
|
318
|
-
if (FS.isDir(stat.mode)) {
|
|
319
|
-
const subEntries = FS.readdir(`/${entry}`);
|
|
320
|
-
const dsgFiles = subEntries.filter((e: string) => e.endsWith(".dsg"));
|
|
321
|
-
if (dsgFiles.length > 0) {
|
|
322
|
-
debugLog("Engine", `Found .dsg files in /${entry}: ${dsgFiles.join(", ")}`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
} catch (e) {
|
|
326
|
-
// Not a directory or can't read
|
|
327
|
-
}
|
|
309
|
+
const stat = FS.stat(`/${entry}`);
|
|
310
|
+
if (FS.isDir(stat.mode)) {
|
|
311
|
+
const subEntries = FS.readdir(`/${entry}`);
|
|
312
|
+
const dsgFiles = subEntries.filter((e: string) => e.endsWith(".dsg"));
|
|
313
|
+
if (dsgFiles.length > 0) {
|
|
314
|
+
debugLog("Engine", `Found .dsg files in /${entry}: ${dsgFiles.join(", ")}`);
|
|
328
315
|
}
|
|
329
|
-
|
|
330
|
-
|
|
316
|
+
}
|
|
317
|
+
} catch (_e) {
|
|
318
|
+
// Not a directory or can't read
|
|
331
319
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
debugLog("Engine", `Failed to list VFS root: ${e}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (let slot = 0; slot <= 5; slot++) {
|
|
326
|
+
const filename = `doomsav${slot}.dsg`;
|
|
327
|
+
|
|
328
|
+
for (const basePath of savePaths) {
|
|
329
|
+
const vfsPath = basePath === "/" ? `/${filename}` : `${basePath}/${filename}`;
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
// Try to read the file from virtual FS
|
|
333
|
+
const data = FS.readFile(vfsPath);
|
|
334
|
+
if (data && data.length > 0) {
|
|
335
|
+
debugLog(
|
|
336
|
+
"Engine",
|
|
337
|
+
`Found save at ${vfsPath}, syncing slot ${slot} (${data.length} bytes)`
|
|
338
|
+
);
|
|
339
|
+
writeSave(slot, data);
|
|
340
|
+
break; // Found this slot, move to next
|
|
341
|
+
}
|
|
342
|
+
} catch (_e) {
|
|
343
|
+
// File doesn't exist at this path, try next
|
|
351
344
|
}
|
|
345
|
+
}
|
|
352
346
|
}
|
|
347
|
+
}
|
|
353
348
|
}
|