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,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration file system for emoemu
|
|
3
|
+
*
|
|
4
|
+
* Provides loading and saving of user settings in a RetroArch-compatible
|
|
5
|
+
* INI-style format. Config files are stored in platform-specific locations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import { ensureDirectory } from '../../utils/ensureDirectory';
|
|
10
|
+
import { dirname, join, resolve } from "path";
|
|
11
|
+
import {
|
|
12
|
+
getConfigDirectory,
|
|
13
|
+
getDefaultConfigPath,
|
|
14
|
+
getConfigPaths,
|
|
15
|
+
getDefaultPlaylistsDirectory,
|
|
16
|
+
getDefaultSavestatesDirectory,
|
|
17
|
+
getDefaultSavefilesDirectory,
|
|
18
|
+
} from "../../utils/paths";
|
|
19
|
+
import {
|
|
20
|
+
parseIniLine,
|
|
21
|
+
formatIniValue,
|
|
22
|
+
updateIniLine,
|
|
23
|
+
commentOutIniLine,
|
|
24
|
+
parseIniBool,
|
|
25
|
+
parseIniNumber,
|
|
26
|
+
parseIniNullableNumber,
|
|
27
|
+
} from "../../utils/ini";
|
|
28
|
+
import { pipe, filter, map, isNonNull, fromEntries } from "remeda";
|
|
29
|
+
import { logger } from "../../utils/logger";
|
|
30
|
+
import { DEFAULT_PNG_COMPRESSION } from "../../rendering";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the platform-specific cores directory path
|
|
34
|
+
*
|
|
35
|
+
* This follows the same convention as RetroArch:
|
|
36
|
+
* - macOS: ~/Library/Application Support/emoemu/cores
|
|
37
|
+
* - Linux: ~/.config/emoemu/cores
|
|
38
|
+
* - Windows: %APPDATA%\emoemu\cores
|
|
39
|
+
*/
|
|
40
|
+
export const getCoresDirectory = (): string => join(getConfigDirectory(), "cores");
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the effective playlists directory path (always absolute)
|
|
44
|
+
* Uses config value if set, otherwise platform-specific default
|
|
45
|
+
*/
|
|
46
|
+
export const getPlaylistsDirectory = (config: Config): string =>
|
|
47
|
+
resolve(config.playlist_directory || getDefaultPlaylistsDirectory());
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the effective save states directory path (always absolute)
|
|
51
|
+
* Uses config value if set, otherwise platform-specific default:
|
|
52
|
+
* - macOS: ~/Library/Application Support/emoemu/states
|
|
53
|
+
* - Linux: ~/.config/emoemu/states
|
|
54
|
+
* - Windows: %APPDATA%\emoemu\states
|
|
55
|
+
*/
|
|
56
|
+
export const getSavestatesDirectory = (config: Config): string =>
|
|
57
|
+
resolve(config.savestate_directory || getDefaultSavestatesDirectory());
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the effective save files (battery saves) directory path (always absolute)
|
|
61
|
+
* Uses config value if set, otherwise platform-specific default:
|
|
62
|
+
* - macOS: ~/Library/Application Support/emoemu/saves
|
|
63
|
+
* - Linux: ~/.config/emoemu/saves
|
|
64
|
+
* - Windows: %APPDATA%\emoemu\saves
|
|
65
|
+
*/
|
|
66
|
+
export const getSavefilesDirectory = (config: Config): string =>
|
|
67
|
+
resolve(config.savefile_directory || getDefaultSavefilesDirectory());
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Determine the directory to look for save states based on config settings.
|
|
71
|
+
* - If savestates_in_content_dir is true (or config is not provided): use content (ROM) directory
|
|
72
|
+
* - If savestates_in_content_dir is false: use configured savestate_directory or platform default
|
|
73
|
+
*
|
|
74
|
+
* @param contentDir The ROM's directory (dirname of romPath)
|
|
75
|
+
* @param config Optional config to determine save state directory
|
|
76
|
+
*/
|
|
77
|
+
export const resolveSaveStateDir = (contentDir: string, config?: Config): string => {
|
|
78
|
+
if (!config || config.savestates_in_content_dir !== false) {
|
|
79
|
+
return contentDir;
|
|
80
|
+
}
|
|
81
|
+
return getSavestatesDirectory(config);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Determine the directory to look for battery saves based on config settings.
|
|
86
|
+
* - If savefiles_in_content_dir is true (or config is not provided): use content (ROM) directory
|
|
87
|
+
* - If savefiles_in_content_dir is false: use configured savefile_directory or platform default
|
|
88
|
+
*
|
|
89
|
+
* @param contentDir The ROM's directory (dirname of romPath)
|
|
90
|
+
* @param config Optional config to determine save file directory
|
|
91
|
+
*/
|
|
92
|
+
export const resolveSaveFileDir = (contentDir: string, config?: Config): string => {
|
|
93
|
+
if (!config || config.savefiles_in_content_dir !== false) {
|
|
94
|
+
return contentDir;
|
|
95
|
+
}
|
|
96
|
+
return getSavefilesDirectory(config);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Re-export for convenience
|
|
100
|
+
export { getDefaultPlaylistsDirectory, getDefaultSavestatesDirectory, getDefaultSavefilesDirectory };
|
|
101
|
+
|
|
102
|
+
export * from './types';
|
|
103
|
+
|
|
104
|
+
import type { VideoDriver, PostProcessingMode } from './types';
|
|
105
|
+
import { isVideoDriver } from './types';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Configuration interface matching the documented format
|
|
109
|
+
*/
|
|
110
|
+
export interface Config {
|
|
111
|
+
// Video
|
|
112
|
+
video_driver: VideoDriver | null; // null = Auto (use system-specific default)
|
|
113
|
+
video_scale: number | null; // null = Auto (use system-specific default)
|
|
114
|
+
video_smooth: boolean;
|
|
115
|
+
video_fullscreen: boolean;
|
|
116
|
+
custom_viewport_width: number | null;
|
|
117
|
+
custom_viewport_height: number | null;
|
|
118
|
+
video_color_enable: boolean;
|
|
119
|
+
video_diff_render: boolean;
|
|
120
|
+
menu_scale_factor: number | null; // UI scale factor for native mode (null = auto-detect from display)
|
|
121
|
+
|
|
122
|
+
// Post-processing
|
|
123
|
+
video_postprocessing_mode: PostProcessingMode; // off, crt, or custom
|
|
124
|
+
|
|
125
|
+
// CRT preset values (used when video_postprocessing_mode is 'crt')
|
|
126
|
+
crt_gamma: number;
|
|
127
|
+
crt_scanlines: number;
|
|
128
|
+
crt_saturation: number;
|
|
129
|
+
crt_vignette: number;
|
|
130
|
+
crt_ntsc: number;
|
|
131
|
+
crt_curvature: number;
|
|
132
|
+
crt_chromatic_aberration: number;
|
|
133
|
+
|
|
134
|
+
// Custom effect values (used when video_postprocessing_mode is 'custom')
|
|
135
|
+
video_shader_enable: boolean;
|
|
136
|
+
video_gamma: number;
|
|
137
|
+
video_scanlines: number;
|
|
138
|
+
video_saturation: number;
|
|
139
|
+
video_brightness: number;
|
|
140
|
+
video_contrast: number;
|
|
141
|
+
video_vignette: number;
|
|
142
|
+
video_bloom: number;
|
|
143
|
+
video_bloom_threshold: number;
|
|
144
|
+
video_ntsc: number;
|
|
145
|
+
video_curvature: number;
|
|
146
|
+
video_chromatic_aberration: number;
|
|
147
|
+
|
|
148
|
+
// Kitty
|
|
149
|
+
kitty_png_level: number;
|
|
150
|
+
|
|
151
|
+
// Audio
|
|
152
|
+
audio_enable: boolean;
|
|
153
|
+
audio_volume: number;
|
|
154
|
+
audio_mute_enable: boolean;
|
|
155
|
+
|
|
156
|
+
// Input
|
|
157
|
+
input_joypad_enable: boolean;
|
|
158
|
+
input_autodetect_enable: boolean;
|
|
159
|
+
|
|
160
|
+
// Save data
|
|
161
|
+
savestate_auto_load: boolean;
|
|
162
|
+
savestate_auto_save: boolean;
|
|
163
|
+
savestate_compression: boolean;
|
|
164
|
+
savestate_directory: string;
|
|
165
|
+
savefile_directory: string;
|
|
166
|
+
savefiles_in_content_dir: boolean; // When true, battery saves (.srm) go to ROM directory
|
|
167
|
+
savestates_in_content_dir: boolean; // When true, save states (.state.auto) go to ROM directory
|
|
168
|
+
battery_save_enable: boolean;
|
|
169
|
+
|
|
170
|
+
// Directories
|
|
171
|
+
system_directory: string;
|
|
172
|
+
screenshot_directory: string;
|
|
173
|
+
playlist_directory: string; // Output directory for generated playlists
|
|
174
|
+
|
|
175
|
+
// Emulation
|
|
176
|
+
fps_show_enable: boolean;
|
|
177
|
+
fps_limit: number;
|
|
178
|
+
video_frame_limit: number; // Limit rendering to N fps (0=off/unlimited, 30, 60, or any positive integer)
|
|
179
|
+
|
|
180
|
+
// Core
|
|
181
|
+
core_default: string;
|
|
182
|
+
libretro_directory: string;
|
|
183
|
+
retroarch_cores_enable: boolean;
|
|
184
|
+
|
|
185
|
+
// Auto-crop
|
|
186
|
+
video_auto_crop_cores: string; // Comma-separated list of core IDs for auto-crop (e.g., "mupen64plus_next")
|
|
187
|
+
|
|
188
|
+
// Browser
|
|
189
|
+
browser_scan_depth: number; // Max depth to scan for ROMs (0=dir only, 1=+subdirs, -1=unlimited)
|
|
190
|
+
|
|
191
|
+
// Notifications
|
|
192
|
+
notifications_enable: boolean;
|
|
193
|
+
|
|
194
|
+
// UI/Menu colors
|
|
195
|
+
menu_highlight_bg: string; // Background color for highlighted menu items
|
|
196
|
+
menu_highlight_fg: string; // Foreground (text) color for highlighted menu items
|
|
197
|
+
|
|
198
|
+
// Logging
|
|
199
|
+
log_verbosity: boolean; // Enable logging (default: true)
|
|
200
|
+
log_to_file: boolean; // Write logs to file (default: true), false = output to console
|
|
201
|
+
log_to_file_timestamp: boolean; // Use timestamped log files instead of overwriting (default: false)
|
|
202
|
+
log_dir: string; // Custom log directory (empty = platform default)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Default configuration values
|
|
207
|
+
*/
|
|
208
|
+
export const DEFAULT_CONFIG: Config = {
|
|
209
|
+
// Video
|
|
210
|
+
video_driver: null, // Auto (use system-specific default)
|
|
211
|
+
video_scale: null, // Auto (use system-specific default)
|
|
212
|
+
video_smooth: false,
|
|
213
|
+
video_fullscreen: false,
|
|
214
|
+
custom_viewport_width: null,
|
|
215
|
+
custom_viewport_height: null,
|
|
216
|
+
video_color_enable: true,
|
|
217
|
+
video_diff_render: true,
|
|
218
|
+
menu_scale_factor: null, // Auto-detect from display
|
|
219
|
+
|
|
220
|
+
// Post-processing
|
|
221
|
+
video_postprocessing_mode: "off",
|
|
222
|
+
|
|
223
|
+
// CRT preset values (used when video_postprocessing_mode is 'crt')
|
|
224
|
+
crt_gamma: 1.3,
|
|
225
|
+
crt_scanlines: 0.1,
|
|
226
|
+
crt_saturation: 1.0,
|
|
227
|
+
crt_vignette: 0.5,
|
|
228
|
+
crt_ntsc: 1.0,
|
|
229
|
+
crt_curvature: 0.1,
|
|
230
|
+
crt_chromatic_aberration: 0,
|
|
231
|
+
|
|
232
|
+
video_shader_enable: false,
|
|
233
|
+
video_gamma: 1.0,
|
|
234
|
+
video_scanlines: 0,
|
|
235
|
+
video_saturation: 1.0,
|
|
236
|
+
video_brightness: 1.0,
|
|
237
|
+
video_contrast: 1.0,
|
|
238
|
+
video_vignette: 0,
|
|
239
|
+
video_bloom: 0,
|
|
240
|
+
video_bloom_threshold: 0.6,
|
|
241
|
+
video_ntsc: 0,
|
|
242
|
+
video_curvature: 0,
|
|
243
|
+
video_chromatic_aberration: 0,
|
|
244
|
+
|
|
245
|
+
// Kitty
|
|
246
|
+
kitty_png_level: DEFAULT_PNG_COMPRESSION,
|
|
247
|
+
|
|
248
|
+
// Audio
|
|
249
|
+
audio_enable: true,
|
|
250
|
+
audio_volume: 1.0,
|
|
251
|
+
audio_mute_enable: false,
|
|
252
|
+
|
|
253
|
+
// Input
|
|
254
|
+
input_joypad_enable: true,
|
|
255
|
+
input_autodetect_enable: true,
|
|
256
|
+
|
|
257
|
+
// Save data
|
|
258
|
+
savestate_auto_load: true,
|
|
259
|
+
savestate_auto_save: true,
|
|
260
|
+
savestate_compression: true,
|
|
261
|
+
savestate_directory: "",
|
|
262
|
+
savefile_directory: "",
|
|
263
|
+
savefiles_in_content_dir: true, // Default: battery saves (.srm) go to ROM directory
|
|
264
|
+
savestates_in_content_dir: true, // Default: save states (.state.auto) go to ROM directory
|
|
265
|
+
battery_save_enable: true,
|
|
266
|
+
|
|
267
|
+
// Directories
|
|
268
|
+
system_directory: "",
|
|
269
|
+
screenshot_directory: "",
|
|
270
|
+
playlist_directory: "", // Empty = use platform default (~/Library/Application Support/emoemu/playlists, etc.)
|
|
271
|
+
|
|
272
|
+
// Emulation
|
|
273
|
+
fps_show_enable: false,
|
|
274
|
+
fps_limit: 0,
|
|
275
|
+
video_frame_limit: 0, // Off by default (no render limit)
|
|
276
|
+
|
|
277
|
+
// Core
|
|
278
|
+
core_default: "",
|
|
279
|
+
libretro_directory: "",
|
|
280
|
+
retroarch_cores_enable: false,
|
|
281
|
+
|
|
282
|
+
// Auto-crop
|
|
283
|
+
video_auto_crop_cores: "mupen64plus_next", // Only N64 cores by default
|
|
284
|
+
|
|
285
|
+
// Browser
|
|
286
|
+
browser_scan_depth: 1, // Scan current dir + immediate subdirs by default
|
|
287
|
+
|
|
288
|
+
// Notifications
|
|
289
|
+
notifications_enable: true,
|
|
290
|
+
|
|
291
|
+
// UI/Menu colors
|
|
292
|
+
menu_highlight_bg: "cyan", // Cyan background
|
|
293
|
+
menu_highlight_fg: "black", // Black text
|
|
294
|
+
|
|
295
|
+
// Logging
|
|
296
|
+
log_verbosity: true, // Enable logging by default
|
|
297
|
+
log_to_file: true, // Write logs to file by default
|
|
298
|
+
log_to_file_timestamp: false, // Overwrite emoemu.log by default
|
|
299
|
+
log_dir: "", // Empty = use platform default
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** Type guard for valid config keys */
|
|
303
|
+
const isConfigKey = (key: string): key is keyof Config => key in DEFAULT_CONFIG;
|
|
304
|
+
|
|
305
|
+
/** Keys that are nullable strings (null = auto/default) */
|
|
306
|
+
const NULLABLE_STRING_KEYS: Set<keyof Config> = new Set(['video_driver']);
|
|
307
|
+
|
|
308
|
+
const parseValue = (key: keyof Config, value: string): Config[keyof Config] => {
|
|
309
|
+
const defaultValue = DEFAULT_CONFIG[key];
|
|
310
|
+
const type = typeof defaultValue;
|
|
311
|
+
|
|
312
|
+
// Handle nullable string keys (like video_driver)
|
|
313
|
+
if (NULLABLE_STRING_KEYS.has(key)) {
|
|
314
|
+
const trimmed = value.toLowerCase().trim();
|
|
315
|
+
if (trimmed === 'null' || trimmed === '') {
|
|
316
|
+
return null as Config[keyof Config];
|
|
317
|
+
}
|
|
318
|
+
// video_driver must be a known driver; unknown/legacy values (e.g. the removed "sdl") fall back to Auto
|
|
319
|
+
if (key === 'video_driver' && !isVideoDriver(trimmed)) {
|
|
320
|
+
return null as Config[keyof Config];
|
|
321
|
+
}
|
|
322
|
+
return trimmed as Config[keyof Config];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Handle nullable number keys (like video_scale, custom_viewport_width/height)
|
|
326
|
+
if (defaultValue === null) {
|
|
327
|
+
return parseIniNullableNumber(value) as Config[keyof Config];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
switch (type) {
|
|
331
|
+
case "boolean":
|
|
332
|
+
return parseIniBool(value) as Config[keyof Config];
|
|
333
|
+
case "number":
|
|
334
|
+
return parseIniNumber(value, defaultValue as number) as Config[keyof Config];
|
|
335
|
+
case "string":
|
|
336
|
+
return value as Config[keyof Config];
|
|
337
|
+
default:
|
|
338
|
+
return value as Config[keyof Config];
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Parse config file content into a partial Config object
|
|
344
|
+
*/
|
|
345
|
+
const parseConfig = (content: string): Partial<Config> => pipe(
|
|
346
|
+
content.split("\n"),
|
|
347
|
+
map(parseIniLine),
|
|
348
|
+
filter(isNonNull),
|
|
349
|
+
filter((entry): entry is { key: keyof Config; value: string } => isConfigKey(entry.key)),
|
|
350
|
+
map(({ key, value }) => [key, parseValue(key, value)] as const),
|
|
351
|
+
fromEntries
|
|
352
|
+
) as Partial<Config>;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Load configuration from file
|
|
356
|
+
*
|
|
357
|
+
* Searches config paths in order of precedence and returns the merged config.
|
|
358
|
+
* Values from higher-precedence files override lower ones.
|
|
359
|
+
*
|
|
360
|
+
* If a custom path is provided but doesn't exist, returns defaults without
|
|
361
|
+
* falling back to other paths (explicit path takes precedence).
|
|
362
|
+
*
|
|
363
|
+
* @param customPath Optional custom config path (highest precedence)
|
|
364
|
+
* @returns The loaded config merged with defaults, and the path that was loaded
|
|
365
|
+
*/
|
|
366
|
+
export const loadConfig = (customPath?: string): { config: Config; loadedFrom: string | null } => {
|
|
367
|
+
// If a custom path is explicitly provided, only check that path
|
|
368
|
+
// Don't fall back to other paths - explicit path takes precedence
|
|
369
|
+
if (customPath !== undefined) {
|
|
370
|
+
if (existsSync(customPath)) {
|
|
371
|
+
try {
|
|
372
|
+
const content = readFileSync(customPath, "utf-8");
|
|
373
|
+
const parsed = parseConfig(content);
|
|
374
|
+
logger.info(`Loading config file: "${customPath}"`, 'Config');
|
|
375
|
+
return {
|
|
376
|
+
config: { ...DEFAULT_CONFIG, ...parsed },
|
|
377
|
+
loadedFrom: customPath,
|
|
378
|
+
};
|
|
379
|
+
} catch {
|
|
380
|
+
// Failed to read/parse, return defaults
|
|
381
|
+
logger.warn(`Failed to parse config file: "${customPath}"`, 'Config');
|
|
382
|
+
return { config: { ...DEFAULT_CONFIG }, loadedFrom: null };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Custom path doesn't exist, return defaults
|
|
386
|
+
logger.debug(`Config file not found: "${customPath}"`, 'Config');
|
|
387
|
+
return { config: { ...DEFAULT_CONFIG }, loadedFrom: null };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// No custom path specified, search standard locations
|
|
391
|
+
const paths = getConfigPaths();
|
|
392
|
+
|
|
393
|
+
// Find the first existing config file
|
|
394
|
+
for (const path of paths) {
|
|
395
|
+
if (existsSync(path)) {
|
|
396
|
+
try {
|
|
397
|
+
const content = readFileSync(path, "utf-8");
|
|
398
|
+
const parsed = parseConfig(content);
|
|
399
|
+
logger.info(`Loading config file: "${path}"`, 'Config');
|
|
400
|
+
return {
|
|
401
|
+
config: { ...DEFAULT_CONFIG, ...parsed },
|
|
402
|
+
loadedFrom: path,
|
|
403
|
+
};
|
|
404
|
+
} catch {
|
|
405
|
+
// Failed to read/parse, try next path
|
|
406
|
+
logger.warn(`Failed to parse config file: "${path}"`, 'Config');
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// No config file found, use defaults
|
|
413
|
+
logger.debug('No config file found, using defaults', 'Config');
|
|
414
|
+
return { config: { ...DEFAULT_CONFIG }, loadedFrom: null };
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Generate config file template with all settings commented out.
|
|
420
|
+
*/
|
|
421
|
+
const generateConfigTemplate = (): string => {
|
|
422
|
+
const d = DEFAULT_CONFIG;
|
|
423
|
+
return `# emoemu Configuration
|
|
424
|
+
# https://github.com/tuxracer/emoemu
|
|
425
|
+
#
|
|
426
|
+
# Settings are commented out by default and use built-in defaults.
|
|
427
|
+
# Uncomment and modify settings you want to customize.
|
|
428
|
+
|
|
429
|
+
# # Video settings
|
|
430
|
+
# video_driver = null # Auto (native, kitty, terminal, ascii, emoji)
|
|
431
|
+
# video_scale = null # Auto (use system-specific default, or set to 0.25, 0.5, 1, 2, 3, 4)
|
|
432
|
+
# video_smooth = ${formatIniValue(d.video_smooth)}
|
|
433
|
+
# video_fullscreen = ${formatIniValue(d.video_fullscreen)}
|
|
434
|
+
# custom_viewport_width = null
|
|
435
|
+
# custom_viewport_height = null
|
|
436
|
+
# video_color_enable = ${formatIniValue(d.video_color_enable)}
|
|
437
|
+
# video_diff_render = ${formatIniValue(d.video_diff_render)}
|
|
438
|
+
|
|
439
|
+
# # Post-processing effects (mode: off, crt, or custom)
|
|
440
|
+
# video_postprocessing_mode = ${formatIniValue(d.video_postprocessing_mode)}
|
|
441
|
+
|
|
442
|
+
# # CRT preset values (used when video_postprocessing_mode is 'crt')
|
|
443
|
+
# crt_gamma = ${formatIniValue(d.crt_gamma)}
|
|
444
|
+
# crt_scanlines = ${formatIniValue(d.crt_scanlines)}
|
|
445
|
+
# crt_saturation = ${formatIniValue(d.crt_saturation)}
|
|
446
|
+
# crt_vignette = ${formatIniValue(d.crt_vignette)}
|
|
447
|
+
# crt_ntsc = ${formatIniValue(d.crt_ntsc)}
|
|
448
|
+
# crt_curvature = ${formatIniValue(d.crt_curvature)}
|
|
449
|
+
# crt_chromatic_aberration = ${formatIniValue(d.crt_chromatic_aberration)}
|
|
450
|
+
|
|
451
|
+
# # Custom effect values (used when video_postprocessing_mode is 'custom')
|
|
452
|
+
# video_shader_enable = ${formatIniValue(d.video_shader_enable)}
|
|
453
|
+
# video_gamma = ${formatIniValue(d.video_gamma)}
|
|
454
|
+
# video_scanlines = ${formatIniValue(d.video_scanlines)}
|
|
455
|
+
# video_saturation = ${formatIniValue(d.video_saturation)}
|
|
456
|
+
# video_brightness = ${formatIniValue(d.video_brightness)}
|
|
457
|
+
# video_contrast = ${formatIniValue(d.video_contrast)}
|
|
458
|
+
# video_vignette = ${formatIniValue(d.video_vignette)}
|
|
459
|
+
# video_bloom = ${formatIniValue(d.video_bloom)}
|
|
460
|
+
# video_bloom_threshold = ${formatIniValue(d.video_bloom_threshold)}
|
|
461
|
+
# video_ntsc = ${formatIniValue(d.video_ntsc)}
|
|
462
|
+
# video_curvature = ${formatIniValue(d.video_curvature)}
|
|
463
|
+
# video_chromatic_aberration = ${formatIniValue(d.video_chromatic_aberration)}
|
|
464
|
+
|
|
465
|
+
# # Kitty-specific settings
|
|
466
|
+
# kitty_png_level = ${formatIniValue(d.kitty_png_level)}
|
|
467
|
+
|
|
468
|
+
# # Audio settings
|
|
469
|
+
# audio_enable = ${formatIniValue(d.audio_enable)}
|
|
470
|
+
# audio_volume = ${formatIniValue(d.audio_volume)}
|
|
471
|
+
# audio_mute_enable = ${formatIniValue(d.audio_mute_enable)}
|
|
472
|
+
|
|
473
|
+
# # Input settings
|
|
474
|
+
# input_joypad_enable = ${formatIniValue(d.input_joypad_enable)}
|
|
475
|
+
# input_autodetect_enable = ${formatIniValue(d.input_autodetect_enable)}
|
|
476
|
+
|
|
477
|
+
# # Save data settings
|
|
478
|
+
# savestate_auto_load = ${formatIniValue(d.savestate_auto_load)}
|
|
479
|
+
# savestate_auto_save = ${formatIniValue(d.savestate_auto_save)}
|
|
480
|
+
# savestate_compression = ${formatIniValue(d.savestate_compression)}
|
|
481
|
+
# savestate_directory = ${formatIniValue(d.savestate_directory)}
|
|
482
|
+
# savefile_directory = ${formatIniValue(d.savefile_directory)}
|
|
483
|
+
# savefiles_in_content_dir = ${formatIniValue(d.savefiles_in_content_dir)}
|
|
484
|
+
# savestates_in_content_dir = ${formatIniValue(d.savestates_in_content_dir)}
|
|
485
|
+
# battery_save_enable = ${formatIniValue(d.battery_save_enable)}
|
|
486
|
+
|
|
487
|
+
# # Directory settings
|
|
488
|
+
# system_directory = ${formatIniValue(d.system_directory)}
|
|
489
|
+
# screenshot_directory = ${formatIniValue(d.screenshot_directory)}
|
|
490
|
+
|
|
491
|
+
# # Emulation settings
|
|
492
|
+
# fps_show_enable = ${formatIniValue(d.fps_show_enable)}
|
|
493
|
+
# fps_limit = ${formatIniValue(d.fps_limit)}
|
|
494
|
+
# video_frame_limit = ${formatIniValue(d.video_frame_limit)}
|
|
495
|
+
|
|
496
|
+
# # Core settings
|
|
497
|
+
# core_default = ${formatIniValue(d.core_default)}
|
|
498
|
+
# libretro_directory = ${formatIniValue(d.libretro_directory)}
|
|
499
|
+
# retroarch_cores_enable = ${formatIniValue(d.retroarch_cores_enable)}
|
|
500
|
+
|
|
501
|
+
# # Browser settings
|
|
502
|
+
# browser_scan_depth = ${formatIniValue(d.browser_scan_depth)}
|
|
503
|
+
|
|
504
|
+
# # Notifications
|
|
505
|
+
# notifications_enable = ${formatIniValue(d.notifications_enable)}
|
|
506
|
+
|
|
507
|
+
# # UI/Menu colors (ANSI color names: black, red, green, yellow, blue, magenta, cyan, white,
|
|
508
|
+
# # or bright variants: blackBright, redBright, greenBright, yellowBright, blueBright, etc.)
|
|
509
|
+
# menu_highlight_bg = ${formatIniValue(d.menu_highlight_bg)}
|
|
510
|
+
# menu_highlight_fg = ${formatIniValue(d.menu_highlight_fg)}
|
|
511
|
+
|
|
512
|
+
# # Logging settings
|
|
513
|
+
# log_verbosity = ${formatIniValue(d.log_verbosity)}
|
|
514
|
+
# log_to_file = ${formatIniValue(d.log_to_file)}
|
|
515
|
+
# log_to_file_timestamp = ${formatIniValue(d.log_to_file_timestamp)}
|
|
516
|
+
# log_dir = ${formatIniValue(d.log_dir)}
|
|
517
|
+
`;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Save raw content to config file
|
|
522
|
+
*
|
|
523
|
+
* @param content The content to write
|
|
524
|
+
* @param path Path to save to
|
|
525
|
+
*/
|
|
526
|
+
const saveConfigContent = (content: string, path: string): void => {
|
|
527
|
+
const dir = dirname(path);
|
|
528
|
+
|
|
529
|
+
ensureDirectory(dir);
|
|
530
|
+
|
|
531
|
+
writeFileSync(path, content, "utf-8");
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Check if a config file exists at any of the search paths
|
|
536
|
+
*/
|
|
537
|
+
export const configExists = (customPath?: string): boolean => {
|
|
538
|
+
const paths = getConfigPaths(customPath);
|
|
539
|
+
return paths.some((path) => existsSync(path));
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Create a default config file if none exists.
|
|
544
|
+
* Creates a template with all settings commented out.
|
|
545
|
+
*/
|
|
546
|
+
export const ensureConfigExists = (): string => {
|
|
547
|
+
const defaultPath = getDefaultConfigPath();
|
|
548
|
+
|
|
549
|
+
if (!existsSync(defaultPath)) {
|
|
550
|
+
saveConfigContent(generateConfigTemplate(), defaultPath);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return defaultPath;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Update a single config value and save to file
|
|
558
|
+
*
|
|
559
|
+
* This reads the existing config file, updates (or uncomments) the specified
|
|
560
|
+
* key, and writes back. If the key doesn't exist, it's appended.
|
|
561
|
+
* If no config file exists, creates one from template first.
|
|
562
|
+
*
|
|
563
|
+
* @param key The config key to update
|
|
564
|
+
* @param value The new value
|
|
565
|
+
* @param customPath Optional custom config path
|
|
566
|
+
*/
|
|
567
|
+
export const updateConfigValue = <K extends keyof Config>(key: K, value: Config[K], customPath?: string): void => {
|
|
568
|
+
const targetPath = customPath || getDefaultConfigPath();
|
|
569
|
+
|
|
570
|
+
// Read existing content or create template
|
|
571
|
+
let content: string;
|
|
572
|
+
if (existsSync(targetPath)) {
|
|
573
|
+
content = readFileSync(targetPath, "utf-8");
|
|
574
|
+
} else {
|
|
575
|
+
content = generateConfigTemplate();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Update the specific line
|
|
579
|
+
const formattedValue = formatIniValue(value);
|
|
580
|
+
const updatedContent = updateIniLine(content, key, formattedValue);
|
|
581
|
+
|
|
582
|
+
saveConfigContent(updatedContent, targetPath);
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Reset a config value to default by commenting it out
|
|
587
|
+
*
|
|
588
|
+
* Instead of writing the default value, this comments out the setting
|
|
589
|
+
* so the app will use DEFAULT_CONFIG at runtime. This ensures users
|
|
590
|
+
* get updated defaults if they change in future versions.
|
|
591
|
+
*
|
|
592
|
+
* @param key The config key to reset
|
|
593
|
+
* @param customPath Optional custom config path
|
|
594
|
+
*/
|
|
595
|
+
export const resetConfigValue = <K extends keyof Config>(key: K, customPath?: string): void => {
|
|
596
|
+
const targetPath = customPath || getDefaultConfigPath();
|
|
597
|
+
|
|
598
|
+
// Only process if config file exists
|
|
599
|
+
if (!existsSync(targetPath)) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const content = readFileSync(targetPath, "utf-8");
|
|
604
|
+
const updatedContent = commentOutIniLine(content, key);
|
|
605
|
+
|
|
606
|
+
saveConfigContent(updatedContent, targetPath);
|
|
607
|
+
};
|
|
608
|
+
|