@muhammedaksam/opentui-doom 0.3.0 → 0.3.6

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.
@@ -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,322 +15,334 @@ export const DOOM_WIDTH = 1280;
15
15
  export const DOOM_HEIGHT = 800;
16
16
 
17
17
  export interface DoomModule {
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: (
35
- parent: string,
36
- path: string,
37
- canRead: boolean,
38
- canWrite: boolean
39
- ) => string;
40
- FS?: {
41
- readFile: (path: string, opts?: { encoding?: string }) => Uint8Array;
42
- readdir: (path: string) => string[];
43
- stat: (path: string) => { mode: number };
44
- isDir: (mode: number) => boolean;
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
- wadPath: string;
54
- print?: (text: string) => void;
55
- printErr?: (text: string) => void;
56
- onQuit?: () => void;
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
- private module: DoomModule | null = null;
61
- private frameBufferPtr: number = 0;
62
- private initialized: boolean = false;
63
- private wadPath: string;
64
- private print: (text: string) => void;
65
- private printErr: (text: string) => void;
66
- private onQuit: (() => void) | null = null;
67
- private emscriptenFS: any = null; // FS reference captured from Emscripten
68
-
69
- constructor(optionsOrPath: string | DoomEngineOptions) {
70
- if (typeof optionsOrPath === "string") {
71
- this.wadPath = resolve(optionsOrPath);
72
- this.print = (text: string) => console.log('[DOOM]', text);
73
- this.printErr = (text: string) => console.error('[DOOM]', text);
74
- } else {
75
- this.wadPath = resolve(optionsOrPath.wadPath);
76
- this.print = optionsOrPath.print || ((text: string) => console.log('[DOOM]', text));
77
- this.printErr = optionsOrPath.printErr || ((text: string) => console.error('[DOOM]', text));
78
- this.onQuit = optionsOrPath.onQuit || null;
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
- async init(): Promise<void> {
83
- // Load the WASM module
84
- const buildDir = join(import.meta.dir, "..", "doom", "build");
85
- const doomJsPath = join(buildDir, "doom.js");
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
- // Load existing saves from ~/.opentui-doom/ into virtual filesystem
139
- const existingSaves = loadExistingSaves();
140
- for (const [slot, data] of existingSaves) {
141
- const filename = `doomsav${slot}.dsg`;
142
- try {
143
- module.FS_createDataFile("/.savegame", filename, Array.from(data), true, true);
144
- debugLog("Engine", `Pre-loaded save slot ${slot} to virtual FS`);
145
- } catch (e) {
146
- debugLog("Engine", `Failed to pre-load save slot ${slot}: ${e}`);
147
- }
148
- }
149
- }
150
- ],
151
- };
152
-
153
- this.module = await createDoomModule(moduleConfig);
154
-
155
- if (!this.module) {
156
- throw new Error("Failed to initialize DOOM module");
157
- }
158
-
159
- // Capture FS reference after module is fully loaded
160
- // Try different methods to access Emscripten's FS
161
- if ((this.module as any).FS) {
162
- this.emscriptenFS = (this.module as any).FS;
163
- debugLog("Engine", "Captured FS from module.FS");
164
- } else if (typeof (globalThis as any).FS !== 'undefined') {
165
- this.emscriptenFS = (globalThis as any).FS;
166
- debugLog("Engine", "Captured FS from globalThis.FS");
167
- } else {
168
- debugLog("Engine", "Warning: Could not find Emscripten FS object");
169
- }
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");
170
81
 
171
- // Initialize DOOM
172
- this.initDoom();
82
+ // Read WAD file first
83
+ const wadData = await readFile(this.wadPath);
84
+ const wadArray = Array.from(new Uint8Array(wadData));
173
85
 
174
- // Get framebuffer pointer
175
- this.frameBufferPtr = this.module._DG_GetFrameBuffer();
176
- this.initialized = true;
177
- }
86
+ // Dynamic import of the compiled DOOM module
87
+ const createDoomModule = require(doomJsPath);
178
88
 
179
- private initDoom(): void {
180
- if (!this.module) return;
181
-
182
- const module = this.module;
183
-
184
- const args = [
185
- "doom",
186
- "-iwad",
187
- "/doom/doom1.wad"
188
- ];
189
-
190
- // Allocate memory for argv using ccall for strings
191
- const argPtrs: number[] = [];
192
- for (const arg of args) {
193
- // Allocate space for string + null terminator
194
- const ptr = module._malloc(arg.length + 1);
195
- // Use setValue to write each character
196
- for (let i = 0; i < arg.length; i++) {
197
- module.setValue(ptr + i, arg.charCodeAt(i), 'i8');
198
- }
199
- // Null terminate
200
- module.setValue(ptr + arg.length, 0, 'i8');
201
- argPtrs.push(ptr);
202
- }
89
+ // Import audio system
90
+ const audio = await import("./doom-audio");
203
91
 
204
- // Create argv array
205
- const argvPtr = module._malloc(argPtrs.length * 4);
206
- for (let i = 0; i < argPtrs.length; i++) {
207
- const ptr = argPtrs[i];
208
- if (ptr !== undefined) {
209
- module.setValue(argvPtr + i * 4, ptr, 'i32');
210
- }
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");
211
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
+ };
212
165
 
213
- // Call doomgeneric_Create
214
- module._doomgeneric_Create(args.length, argvPtr);
166
+ this.module = await createDoomModule(moduleConfig);
215
167
 
216
- // Free argv (DOOM copies the strings)
217
- for (const ptr of argPtrs) {
218
- module._free(ptr);
219
- }
220
- module._free(argvPtr);
168
+ if (!this.module) {
169
+ throw new Error("Failed to initialize DOOM module");
221
170
  }
222
171
 
223
- /**
224
- * Run one game tick - called each frame
225
- */
226
- tick(): void {
227
- if (!this.module || !this.initialized) return;
228
- this.module._doomgeneric_Tick();
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");
229
182
  }
230
183
 
231
- /**
232
- * Get the current frame as RGBA pixel data
233
- * DOOM uses ARGB format, so we need to convert
234
- */
235
- getFrameBuffer(): Uint8Array {
236
- if (!this.module || !this.initialized) {
237
- return new Uint8Array(DOOM_WIDTH * DOOM_HEIGHT * 4);
238
- }
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
+ }
239
212
 
240
- const pixels = DOOM_WIDTH * DOOM_HEIGHT;
241
- const buffer = new Uint8Array(pixels * 4);
242
- const module = this.module;
243
-
244
- // Read ARGB data from DOOM's framebuffer using getValue
245
- for (let i = 0; i < pixels; i++) {
246
- const argb = module.getValue(this.frameBufferPtr + i * 4, 'i32');
247
- const offset = i * 4;
248
- buffer[offset + 0] = (argb >> 16) & 0xFF; // R
249
- buffer[offset + 1] = (argb >> 8) & 0xFF; // G
250
- buffer[offset + 2] = argb & 0xFF; // B
251
- buffer[offset + 3] = 255; // A (always opaque)
252
- }
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);
253
224
 
254
- return buffer;
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);
255
247
  }
256
248
 
257
- /**
258
- * Push a key event to DOOM
259
- */
260
- pushKey(pressed: boolean, key: number): void {
261
- if (!this.module || !this.initialized) return;
262
- this.module._DG_PushKeyEvent(pressed ? 1 : 0, key);
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)
263
261
  }
264
262
 
265
- isInitialized(): boolean {
266
- return this.initialized;
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;
267
286
  }
268
287
 
269
- /**
270
- * Sync save games from the virtual filesystem to disk (~/.opentui-doom/)
271
- * Call this periodically or after save operations to persist saves
272
- */
273
- syncSaves(): void {
274
- if (!this.module || !this.emscriptenFS) {
275
- debugLog("Engine", "syncSaves: module or FS not available");
276
- return;
277
- }
278
-
279
- // DOOM can save to different paths depending on configuration
280
- // Try multiple possible locations
281
- const savePaths = [
282
- "/", // Root
283
- "/.savegame", // Default when configdir is "."
284
- ".savegame", // Relative path (CWD)
285
- "/doom", // Our custom path
286
- "/tmp", // Temp directory
287
- ];
288
-
289
- const FS = this.emscriptenFS;
290
-
291
- // 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;
292
308
  try {
293
- const rootEntries = FS.readdir("/");
294
- debugLog("Engine", `VFS root contents: ${rootEntries.join(", ")}`);
295
-
296
- // Check each directory at root
297
- for (const entry of rootEntries) {
298
- if (entry === "." || entry === "..") continue;
299
- try {
300
- const stat = FS.stat(`/${entry}`);
301
- if (FS.isDir(stat.mode)) {
302
- const subEntries = FS.readdir(`/${entry}`);
303
- const dsgFiles = subEntries.filter((e: string) => e.endsWith(".dsg"));
304
- if (dsgFiles.length > 0) {
305
- debugLog("Engine", `Found .dsg files in /${entry}: ${dsgFiles.join(", ")}`);
306
- }
307
- }
308
- } catch (e) {
309
- // Not a directory or can't read
310
- }
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(", ")}`);
311
315
  }
312
- } catch (e) {
313
- debugLog("Engine", `Failed to list VFS root: ${e}`);
316
+ }
317
+ } catch (_e) {
318
+ // Not a directory or can't read
314
319
  }
315
-
316
- for (let slot = 0; slot <= 5; slot++) {
317
- const filename = `doomsav${slot}.dsg`;
318
-
319
- for (const basePath of savePaths) {
320
- const vfsPath = basePath === "/" ? `/${filename}` : `${basePath}/${filename}`;
321
-
322
- try {
323
- // Try to read the file from virtual FS
324
- const data = FS.readFile(vfsPath);
325
- if (data && data.length > 0) {
326
- debugLog("Engine", `Found save at ${vfsPath}, syncing slot ${slot} (${data.length} bytes)`);
327
- writeSave(slot, data);
328
- break; // Found this slot, move to next
329
- }
330
- } catch (e) {
331
- // File doesn't exist at this path, try next
332
- }
333
- }
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
334
344
  }
345
+ }
335
346
  }
347
+ }
336
348
  }