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.
- package/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- 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;
|