emoemu 0.1.0

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.
Files changed (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. package/vitest.config.ts +13 -0
@@ -0,0 +1,354 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { existsSync, unlinkSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import {
6
+ loadConfig,
7
+ updateConfigValue,
8
+ ensureConfigExists,
9
+ DEFAULT_CONFIG,
10
+ } from '.';
11
+ import {
12
+ getConfigDirectory,
13
+ getDefaultConfigPath,
14
+ } from '../../utils/paths';
15
+
16
+ describe('Config System', () => {
17
+ const testDir = join(tmpdir(), 'emoemu-config-test');
18
+ const testConfigPath = join(testDir, 'test.cfg');
19
+
20
+ beforeEach(() => {
21
+ // Create test directory
22
+ if (!existsSync(testDir)) {
23
+ mkdirSync(testDir, { recursive: true });
24
+ }
25
+ });
26
+
27
+ afterEach(() => {
28
+ // Clean up test files
29
+ if (existsSync(testConfigPath)) {
30
+ unlinkSync(testConfigPath);
31
+ }
32
+ // Clean up nested directories
33
+ const nestedDir = join(testDir, 'nested');
34
+ if (existsSync(nestedDir)) {
35
+ rmSync(nestedDir, { recursive: true });
36
+ }
37
+ });
38
+
39
+ describe('getConfigDirectory', () => {
40
+ it('should return a valid path', () => {
41
+ const dir = getConfigDirectory();
42
+ expect(dir).toBeDefined();
43
+ expect(typeof dir).toBe('string');
44
+ expect(dir.length).toBeGreaterThan(0);
45
+ });
46
+ });
47
+
48
+ describe('getDefaultConfigPath', () => {
49
+ it('should return a path ending with emoemu.cfg', () => {
50
+ const path = getDefaultConfigPath();
51
+ expect(path).toContain('emoemu.cfg');
52
+ });
53
+ });
54
+
55
+ describe('DEFAULT_CONFIG', () => {
56
+ it('should have all required keys', () => {
57
+ expect(DEFAULT_CONFIG.video_driver).toBe(null); // null = Auto (system-specific default)
58
+ expect(DEFAULT_CONFIG.video_scale).toBe(null); // null = Auto (system-specific default)
59
+ expect(DEFAULT_CONFIG.audio_enable).toBe(true);
60
+ expect(DEFAULT_CONFIG.savestate_auto_load).toBe(true);
61
+ expect(DEFAULT_CONFIG.savestate_auto_save).toBe(true);
62
+ });
63
+
64
+ it('should have valid video driver', () => {
65
+ // null = Auto, otherwise must be a valid driver
66
+ const validDrivers = [null, 'kitty', 'terminal', 'ascii', 'emoji'];
67
+ expect(validDrivers).toContain(DEFAULT_CONFIG.video_driver);
68
+ });
69
+ });
70
+
71
+ describe('loadConfig', () => {
72
+ it('should return defaults when no config file exists', () => {
73
+ const { config, loadedFrom } = loadConfig('/nonexistent/path.cfg');
74
+ expect(loadedFrom).toBeNull();
75
+ expect(config.video_driver).toBe(DEFAULT_CONFIG.video_driver);
76
+ expect(config.video_scale).toBe(DEFAULT_CONFIG.video_scale);
77
+ });
78
+
79
+ it('should load config from custom path', () => {
80
+ // Create a test config file
81
+ const configContent = `
82
+ # Test config
83
+ video_driver = "terminal"
84
+ video_scale = 3
85
+ audio_enable = false
86
+ `;
87
+ writeFileSync(testConfigPath, configContent);
88
+
89
+ const { config, loadedFrom } = loadConfig(testConfigPath);
90
+ expect(loadedFrom).toBe(testConfigPath);
91
+ expect(config.video_driver).toBe('terminal');
92
+ expect(config.video_scale).toBe(3);
93
+ expect(config.audio_enable).toBe(false);
94
+ });
95
+
96
+ it('should handle quoted and unquoted values', () => {
97
+ const configContent = `
98
+ video_driver = "terminal"
99
+ video_scale = 4
100
+ audio_enable = true
101
+ `;
102
+ writeFileSync(testConfigPath, configContent);
103
+
104
+ const { config } = loadConfig(testConfigPath);
105
+ expect(config.video_driver).toBe('terminal');
106
+ expect(config.video_scale).toBe(4);
107
+ expect(config.audio_enable).toBe(true);
108
+ });
109
+
110
+ it('should skip comment lines', () => {
111
+ const configContent = `
112
+ # This is a comment
113
+ video_driver = "ascii"
114
+ # Another comment
115
+ video_scale = 5
116
+ `;
117
+ writeFileSync(testConfigPath, configContent);
118
+
119
+ const { config } = loadConfig(testConfigPath);
120
+ expect(config.video_driver).toBe('ascii');
121
+ expect(config.video_scale).toBe(5);
122
+ });
123
+
124
+ it('should skip empty lines', () => {
125
+ const configContent = `
126
+
127
+ video_driver = "emoji"
128
+
129
+ video_scale = 1
130
+
131
+ `;
132
+ writeFileSync(testConfigPath, configContent);
133
+
134
+ const { config } = loadConfig(testConfigPath);
135
+ expect(config.video_driver).toBe('emoji');
136
+ expect(config.video_scale).toBe(1);
137
+ });
138
+
139
+ it('should handle boolean values correctly', () => {
140
+ const configContent = `
141
+ audio_enable = false
142
+ savestate_compression = true
143
+ fps_show_enable = true
144
+ `;
145
+ writeFileSync(testConfigPath, configContent);
146
+
147
+ const { config } = loadConfig(testConfigPath);
148
+ expect(config.audio_enable).toBe(false);
149
+ expect(config.savestate_compression).toBe(true);
150
+ expect(config.fps_show_enable).toBe(true);
151
+ });
152
+
153
+ it('should handle float values correctly', () => {
154
+ const configContent = `
155
+ video_gamma = 1.3
156
+ video_scanlines = 0.5
157
+ video_bloom = 0.75
158
+ `;
159
+ writeFileSync(testConfigPath, configContent);
160
+
161
+ const { config } = loadConfig(testConfigPath);
162
+ expect(config.video_gamma).toBeCloseTo(1.3);
163
+ expect(config.video_scanlines).toBeCloseTo(0.5);
164
+ expect(config.video_bloom).toBeCloseTo(0.75);
165
+ });
166
+
167
+ it('should merge with defaults for missing keys', () => {
168
+ const configContent = `
169
+ video_driver = "terminal"
170
+ `;
171
+ writeFileSync(testConfigPath, configContent);
172
+
173
+ const { config } = loadConfig(testConfigPath);
174
+ expect(config.video_driver).toBe('terminal');
175
+ // All other values should be defaults
176
+ expect(config.video_scale).toBe(DEFAULT_CONFIG.video_scale);
177
+ expect(config.audio_enable).toBe(DEFAULT_CONFIG.audio_enable);
178
+ });
179
+
180
+ it('should ignore unknown keys', () => {
181
+ const configContent = `
182
+ video_driver = "kitty"
183
+ unknown_key = "value"
184
+ another_unknown = 123
185
+ `;
186
+ writeFileSync(testConfigPath, configContent);
187
+
188
+ const { config } = loadConfig(testConfigPath);
189
+ expect(config.video_driver).toBe('kitty');
190
+ // Should not crash and should have valid config
191
+ expect(config).toBeDefined();
192
+ });
193
+
194
+ it('should fall back to Auto (null) for legacy/invalid video_driver values', () => {
195
+ const configContent = `
196
+ video_driver = "sdl"
197
+ `;
198
+ writeFileSync(testConfigPath, configContent);
199
+
200
+ const { config } = loadConfig(testConfigPath);
201
+ // "sdl" was removed in the ink-native migration; unknown values fall back to Auto
202
+ expect(config.video_driver).toBe(null);
203
+ });
204
+
205
+ it('should preserve a valid video_driver value', () => {
206
+ const configContent = `
207
+ video_driver = "native"
208
+ `;
209
+ writeFileSync(testConfigPath, configContent);
210
+
211
+ const { config } = loadConfig(testConfigPath);
212
+ expect(config.video_driver).toBe('native');
213
+ });
214
+ });
215
+
216
+ describe('updateConfigValue', () => {
217
+ it('should create config file if it does not exist', () => {
218
+ expect(existsSync(testConfigPath)).toBe(false);
219
+
220
+ updateConfigValue('video_driver', 'terminal', testConfigPath);
221
+
222
+ expect(existsSync(testConfigPath)).toBe(true);
223
+ const content = readFileSync(testConfigPath, 'utf-8');
224
+ expect(content).toContain('video_driver = "terminal"');
225
+ });
226
+
227
+ it('should uncomment and update existing commented setting', () => {
228
+ // Create a config with commented setting
229
+ const configContent = `# emoemu Configuration
230
+ # video_driver = "kitty"
231
+ # video_scale = 2
232
+ `;
233
+ writeFileSync(testConfigPath, configContent);
234
+
235
+ updateConfigValue('video_driver', 'terminal', testConfigPath);
236
+
237
+ const content = readFileSync(testConfigPath, 'utf-8');
238
+ expect(content).toContain('video_driver = "terminal"');
239
+ // Should not have the commented version anymore
240
+ expect(content).not.toContain('# video_driver');
241
+ });
242
+
243
+ it('should update existing uncommented setting', () => {
244
+ const configContent = `video_driver = "kitty"
245
+ video_scale = 2
246
+ `;
247
+ writeFileSync(testConfigPath, configContent);
248
+
249
+ updateConfigValue('video_driver', 'ascii', testConfigPath);
250
+
251
+ const content = readFileSync(testConfigPath, 'utf-8');
252
+ expect(content).toContain('video_driver = "ascii"');
253
+ expect(content).not.toContain('video_driver = "kitty"');
254
+ });
255
+
256
+ it('should append setting if not found in file', () => {
257
+ const configContent = `# emoemu Configuration
258
+ video_driver = "kitty"
259
+ `;
260
+ writeFileSync(testConfigPath, configContent);
261
+
262
+ updateConfigValue('fps_show_enable', true, testConfigPath);
263
+
264
+ const content = readFileSync(testConfigPath, 'utf-8');
265
+ expect(content).toContain('fps_show_enable = true');
266
+ });
267
+
268
+ it('should handle boolean values', () => {
269
+ writeFileSync(testConfigPath, '# audio_enable = true\n');
270
+
271
+ updateConfigValue('audio_enable', false, testConfigPath);
272
+
273
+ const content = readFileSync(testConfigPath, 'utf-8');
274
+ expect(content).toContain('audio_enable = false');
275
+ });
276
+
277
+ it('should handle numeric values', () => {
278
+ writeFileSync(testConfigPath, '# video_scale = 2\n');
279
+
280
+ updateConfigValue('video_scale', 4, testConfigPath);
281
+
282
+ const content = readFileSync(testConfigPath, 'utf-8');
283
+ expect(content).toContain('video_scale = 4');
284
+ });
285
+
286
+ it('should handle float values', () => {
287
+ writeFileSync(testConfigPath, '# video_gamma = 1.0\n');
288
+
289
+ updateConfigValue('video_gamma', 1.5, testConfigPath);
290
+
291
+ const content = readFileSync(testConfigPath, 'utf-8');
292
+ expect(content).toContain('video_gamma = 1.5');
293
+ });
294
+
295
+ it('should create parent directories if needed', () => {
296
+ const deepPath = join(testDir, 'nested', 'dir', 'test.cfg');
297
+
298
+ updateConfigValue('video_driver', 'terminal', deepPath);
299
+
300
+ expect(existsSync(deepPath)).toBe(true);
301
+ const content = readFileSync(deepPath, 'utf-8');
302
+ expect(content).toContain('video_driver = "terminal"');
303
+ });
304
+ });
305
+
306
+ describe('ensureConfigExists', () => {
307
+ // Note: This test uses the actual default path, so we skip modifying it
308
+ // to avoid affecting user's real config. Instead we test the behavior.
309
+ it('should return the default config path', () => {
310
+ const path = ensureConfigExists();
311
+ expect(path).toBe(getDefaultConfigPath());
312
+ });
313
+ });
314
+
315
+ describe('round-trip', () => {
316
+ it('should preserve values through updateConfigValue and loadConfig', () => {
317
+ // Start with empty file
318
+ writeFileSync(testConfigPath, '# emoemu Configuration\n');
319
+
320
+ // Update multiple values
321
+ updateConfigValue('video_driver', 'ascii', testConfigPath);
322
+ updateConfigValue('video_scale', 5, testConfigPath);
323
+ updateConfigValue('video_gamma', 1.5, testConfigPath);
324
+ updateConfigValue('audio_enable', false, testConfigPath);
325
+ updateConfigValue('fps_show_enable', true, testConfigPath);
326
+ updateConfigValue('savestate_compression', false, testConfigPath);
327
+
328
+ const { config: loadedConfig } = loadConfig(testConfigPath);
329
+
330
+ expect(loadedConfig.video_driver).toBe('ascii');
331
+ expect(loadedConfig.video_scale).toBe(5);
332
+ expect(loadedConfig.video_gamma).toBeCloseTo(1.5);
333
+ expect(loadedConfig.audio_enable).toBe(false);
334
+ expect(loadedConfig.fps_show_enable).toBe(true);
335
+ expect(loadedConfig.savestate_compression).toBe(false);
336
+ });
337
+
338
+ it('should preserve other settings when updating one value', () => {
339
+ const configContent = `video_driver = "terminal"
340
+ video_scale = 3
341
+ audio_enable = false
342
+ `;
343
+ writeFileSync(testConfigPath, configContent);
344
+
345
+ // Update just one value
346
+ updateConfigValue('video_scale', 5, testConfigPath);
347
+
348
+ const { config } = loadConfig(testConfigPath);
349
+ expect(config.video_driver).toBe('terminal');
350
+ expect(config.video_scale).toBe(5);
351
+ expect(config.audio_enable).toBe(false);
352
+ });
353
+ });
354
+ });
@@ -0,0 +1,36 @@
1
+ import { isString } from 'remeda';
2
+
3
+ /**
4
+ * Video driver/render mode options
5
+ */
6
+ export type VideoDriver = "native" | "kitty" | "terminal" | "ascii" | "emoji";
7
+
8
+ /**
9
+ * Post-processing mode options
10
+ */
11
+ export type PostProcessingMode = "off" | "crt" | "custom";
12
+
13
+ /** Valid video driver values (must match VideoDriver type) */
14
+ export const VIDEO_DRIVERS: readonly VideoDriver[] = ['native', 'kitty', 'terminal', 'ascii', 'emoji'];
15
+
16
+ /** Valid post-processing mode values (must match PostProcessingMode type) */
17
+ export const POST_PROCESSING_MODES: readonly PostProcessingMode[] = ['off', 'crt', 'custom'];
18
+
19
+ const videoDriverSet = new Set<string>(VIDEO_DRIVERS);
20
+ const postProcessingModeSet = new Set<string>(POST_PROCESSING_MODES);
21
+
22
+ /**
23
+ * Type guard for VideoDriver union type.
24
+ * Validates that a value is one of the valid video driver options.
25
+ */
26
+ export const isVideoDriver = (value: unknown): value is VideoDriver => {
27
+ return isString(value) && videoDriverSet.has(value);
28
+ };
29
+
30
+ /**
31
+ * Type guard for PostProcessingMode union type.
32
+ * Validates that a value is one of the valid post-processing modes.
33
+ */
34
+ export const isPostProcessingMode = (value: unknown): value is PostProcessingMode => {
35
+ return isString(value) && postProcessingModeSet.has(value);
36
+ };
@@ -0,0 +1,114 @@
1
+ // =============================================================================
2
+ // Audio Manager Constants
3
+ // =============================================================================
4
+
5
+ /**
6
+ * Maximum number of audio frames to queue ahead.
7
+ * Controls latency vs. stability tradeoff.
8
+ */
9
+ export const MAX_AUDIO_QUEUED_FRAMES = 4;
10
+
11
+ // =============================================================================
12
+ // Emulator Bootstrap Constants
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Number of bootstrap frames to run for libretro cores.
17
+ * Some cores (especially N64 with Angrylion) need many frames to stabilize
18
+ * dimensions after loading a ROM before video output begins.
19
+ */
20
+ export const LIBRETRO_BOOTSTRAP_FRAMES = 10;
21
+
22
+ /**
23
+ * Maximum number of frames to skip (run without rendering) when catching up.
24
+ * Set to 1 to ensure HID input events can process between each frame.
25
+ * Higher values improve timing accuracy but cause input delay because
26
+ * the frame skip loop blocks the event loop.
27
+ */
28
+ export const MAX_FRAME_SKIP = 1;
29
+
30
+ // =============================================================================
31
+ // Time Constants
32
+ // =============================================================================
33
+
34
+ /** Milliseconds per second (for FPS calculations) */
35
+ export const MS_PER_SECOND = 1000;
36
+
37
+ // =============================================================================
38
+ // Audio Sample Rate Constants
39
+ // =============================================================================
40
+
41
+ /** Standard CD-quality sample rate (44.1 kHz) */
42
+ export const SAMPLE_RATE_44100 = 44100;
43
+
44
+ /** Standard DVD/DAT sample rate (48 kHz) */
45
+ export const SAMPLE_RATE_48000 = 48000;
46
+
47
+ /** Audio frame duration in seconds (10ms for low latency) */
48
+ export const AUDIO_FRAME_DURATION_SEC = 0.01;
49
+
50
+ /** Number of audio channels for stereo output */
51
+ export const AUDIO_STEREO_CHANNELS = 2;
52
+
53
+ /** Bytes per 16-bit audio sample */
54
+ export const BYTES_PER_INT16_SAMPLE = 2;
55
+
56
+ /** Number of audio frames to buffer (10 frames = ~100ms) */
57
+ export const AUDIO_RING_BUFFER_FRAMES = 10;
58
+
59
+ /** Maximum 16-bit signed audio sample value */
60
+ export const INT16_MAX_VALUE = 32767;
61
+
62
+ /** Bytes per stereo sample (2 channels * 2 bytes per int16 sample) */
63
+ export const BYTES_PER_STEREO_SAMPLE = AUDIO_STEREO_CHANNELS * BYTES_PER_INT16_SAMPLE;
64
+
65
+ /**
66
+ * RtAudio error type threshold for recoverable errors.
67
+ * Error types 0-2 are warnings, 3+ are actual errors that may require recovery.
68
+ */
69
+ export const RTAUDIO_RECOVERABLE_ERROR_THRESHOLD = 3;
70
+
71
+ /** Delay in ms before attempting audio recovery after an error */
72
+ export const AUDIO_RECOVERY_DELAY_MS = 100;
73
+
74
+ /** Tolerance for floating point comparison (e.g., resample ratio equality) */
75
+ export const FLOAT_COMPARE_EPSILON = 0.001;
76
+
77
+ /** Offset to next right channel sample in interleaved stereo (current pair L=0, R=1, next pair L=2, R=3) */
78
+ export const STEREO_NEXT_RIGHT_OFFSET = 3;
79
+
80
+ // =============================================================================
81
+ // Date/Time Formatting Constants
82
+ // =============================================================================
83
+
84
+ /** Slice offset for extracting 2-digit year from full year string */
85
+ export const TWO_DIGIT_YEAR_SLICE_START = -2;
86
+
87
+ /** Length of ISO datetime string without milliseconds ("YYYY-MM-DD HH:MM:SS" = 19 chars) */
88
+ export const ISO_DATETIME_LENGTH = 19;
89
+
90
+ // =============================================================================
91
+ // Input Polling Constants
92
+ // =============================================================================
93
+
94
+ /** Interval in ms for polling gamepad input during dialogs */
95
+ export const GAMEPAD_DIALOG_POLL_INTERVAL_MS = 50;
96
+
97
+ // =============================================================================
98
+ // Numeric Base/Radix Constants
99
+ // =============================================================================
100
+
101
+ export { HEX_RADIX } from '../utils';
102
+
103
+ /** Decimal radix for parseInt/toString */
104
+ export const DECIMAL_RADIX = 10;
105
+
106
+ /** Padding width for byte values in decimal (0-255 = 3 digits max) */
107
+ export const BYTE_DECIMAL_PAD_WIDTH = 3;
108
+
109
+ // =============================================================================
110
+ // Logging/Display Constants
111
+ // =============================================================================
112
+
113
+ /** Decimal places for aspect ratio and similar floating point logging */
114
+ export const ASPECT_RATIO_DECIMALS = 3;