@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.
@@ -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
- _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
- // 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
- // Initialize DOOM
189
- this.initDoom();
82
+ // Read WAD file first
83
+ const wadData = await readFile(this.wadPath);
84
+ const wadArray = Array.from(new Uint8Array(wadData));
190
85
 
191
- // Get framebuffer pointer
192
- this.frameBufferPtr = this.module._DG_GetFrameBuffer();
193
- this.initialized = true;
194
- }
86
+ // Dynamic import of the compiled DOOM module
87
+ const createDoomModule = require(doomJsPath);
195
88
 
196
- private initDoom(): void {
197
- if (!this.module) return;
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
- // Create argv array
222
- const argvPtr = module._malloc(argPtrs.length * 4);
223
- for (let i = 0; i < argPtrs.length; i++) {
224
- const ptr = argPtrs[i];
225
- if (ptr !== undefined) {
226
- module.setValue(argvPtr + i * 4, ptr, 'i32');
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
- // Call doomgeneric_Create
231
- module._doomgeneric_Create(args.length, argvPtr);
166
+ this.module = await createDoomModule(moduleConfig);
232
167
 
233
- // Free argv (DOOM copies the strings)
234
- for (const ptr of argPtrs) {
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
- * Run one game tick - called each frame
242
- */
243
- tick(): void {
244
- if (!this.module || !this.initialized) return;
245
- 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");
246
182
  }
247
183
 
248
- /**
249
- * Get the current frame as RGBA pixel data
250
- * DOOM uses ARGB format, so we need to convert
251
- */
252
- getFrameBuffer(): Uint8Array {
253
- if (!this.module || !this.initialized) {
254
- return new Uint8Array(DOOM_WIDTH * DOOM_HEIGHT * 4);
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
- const pixels = DOOM_WIDTH * DOOM_HEIGHT;
258
- const buffer = new Uint8Array(pixels * 4);
259
- const module = this.module;
260
-
261
- // Read ARGB data from DOOM's framebuffer using getValue
262
- for (let i = 0; i < pixels; i++) {
263
- const argb = module.getValue(this.frameBufferPtr + i * 4, 'i32');
264
- const offset = i * 4;
265
- buffer[offset + 0] = (argb >> 16) & 0xFF; // R
266
- buffer[offset + 1] = (argb >> 8) & 0xFF; // G
267
- buffer[offset + 2] = argb & 0xFF; // B
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
- 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);
272
247
  }
273
248
 
274
- /**
275
- * Push a key event to DOOM
276
- */
277
- pushKey(pressed: boolean, key: number): void {
278
- if (!this.module || !this.initialized) return;
279
- 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)
280
261
  }
281
262
 
282
- isInitialized(): boolean {
283
- 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;
284
286
  }
285
287
 
286
- /**
287
- * Sync save games from the virtual filesystem to disk (~/.opentui-doom/)
288
- * Call this periodically or after save operations to persist saves
289
- */
290
- syncSaves(): void {
291
- if (!this.module || !this.emscriptenFS) {
292
- debugLog("Engine", "syncSaves: module or FS not available");
293
- return;
294
- }
295
-
296
- // DOOM can save to different paths depending on configuration
297
- // Try multiple possible locations
298
- const savePaths = [
299
- "/", // Root
300
- "/.savegame", // Default when configdir is "."
301
- ".savegame", // Relative path (CWD)
302
- "/doom", // Our custom path
303
- "/tmp", // Temp directory
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
- const rootEntries = FS.readdir("/");
311
- debugLog("Engine", `VFS root contents: ${rootEntries.join(", ")}`);
312
-
313
- // Check each directory at root
314
- for (const entry of rootEntries) {
315
- if (entry === "." || entry === "..") continue;
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
- } catch (e) {
330
- debugLog("Engine", `Failed to list VFS root: ${e}`);
316
+ }
317
+ } catch (_e) {
318
+ // Not a directory or can't read
331
319
  }
332
-
333
- for (let slot = 0; slot <= 5; slot++) {
334
- const filename = `doomsav${slot}.dsg`;
335
-
336
- for (const basePath of savePaths) {
337
- const vfsPath = basePath === "/" ? `/${filename}` : `${basePath}/${filename}`;
338
-
339
- try {
340
- // Try to read the file from virtual FS
341
- const data = FS.readFile(vfsPath);
342
- if (data && data.length > 0) {
343
- debugLog("Engine", `Found save at ${vfsPath}, syncing slot ${slot} (${data.length} bytes)`);
344
- writeSave(slot, data);
345
- break; // Found this slot, move to next
346
- }
347
- } catch (e) {
348
- // File doesn't exist at this path, try next
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
  }