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,320 @@
|
|
|
1
|
+
import { isString, isBoolean } from 'remeda';
|
|
2
|
+
import { isVideoDriver, isPostProcessingMode, updateConfigValue, resetConfigValue } from '@/frontend/config';
|
|
3
|
+
import type { Config } from '@/frontend/config';
|
|
4
|
+
import { setNotificationsEnabled } from '@/frontend/notifications';
|
|
5
|
+
import type { SettingsOption, SettingsCategory } from '..';
|
|
6
|
+
|
|
7
|
+
/** Setter for config values via dynamic key */
|
|
8
|
+
export const setConfigField = (config: Config, key: keyof Config, value: Config[keyof Config]) => {
|
|
9
|
+
Object.assign(config, { [key]: value });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Factory for simple boolean toggle settings */
|
|
13
|
+
export const createToggleOption = (key: keyof Config, label: string): SettingsOption => ({
|
|
14
|
+
id: key,
|
|
15
|
+
label,
|
|
16
|
+
type: 'toggle',
|
|
17
|
+
getValue: (config) => isBoolean(config[key]) ? config[key] : false,
|
|
18
|
+
setValue: (config, value, configPath) => {
|
|
19
|
+
if (!isBoolean(value)) { return; }
|
|
20
|
+
setConfigField(config, key, value);
|
|
21
|
+
updateConfigValue(key, value, configPath);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** Factory for numeric select settings (parsed with parseFloat) */
|
|
26
|
+
export const createFloatSelectOption = (
|
|
27
|
+
key: keyof Config,
|
|
28
|
+
label: string,
|
|
29
|
+
options: { value: string; label: string }[],
|
|
30
|
+
): SettingsOption => ({
|
|
31
|
+
id: key,
|
|
32
|
+
label,
|
|
33
|
+
type: 'select',
|
|
34
|
+
options,
|
|
35
|
+
getValue: (config) => String(config[key]),
|
|
36
|
+
setValue: (config, value, configPath) => {
|
|
37
|
+
if (!isString(value)) { return; }
|
|
38
|
+
const parsed = parseFloat(value);
|
|
39
|
+
setConfigField(config, key, parsed);
|
|
40
|
+
updateConfigValue(key, parsed, configPath);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Factory for integer select settings (parsed with parseInt) */
|
|
45
|
+
export const createIntSelectOption = (
|
|
46
|
+
key: keyof Config,
|
|
47
|
+
label: string,
|
|
48
|
+
options: { value: string; label: string }[],
|
|
49
|
+
): SettingsOption => ({
|
|
50
|
+
id: key,
|
|
51
|
+
label,
|
|
52
|
+
type: 'select',
|
|
53
|
+
options,
|
|
54
|
+
getValue: (config) => String(config[key]),
|
|
55
|
+
setValue: (config, value, configPath) => {
|
|
56
|
+
if (!isString(value)) { return; }
|
|
57
|
+
const parsed = parseInt(value, 10);
|
|
58
|
+
setConfigField(config, key, parsed);
|
|
59
|
+
updateConfigValue(key, parsed, configPath);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const videoDriverOptions = [
|
|
64
|
+
{ value: 'auto', label: 'Auto' },
|
|
65
|
+
{ value: 'kitty', label: 'Kitty (best quality)' },
|
|
66
|
+
{ value: 'terminal', label: 'Terminal (Unicode blocks)' },
|
|
67
|
+
{ value: 'ascii', label: 'ASCII' },
|
|
68
|
+
{ value: 'emoji', label: 'Emoji' },
|
|
69
|
+
{ value: 'native', label: 'Native window (experimental)' },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export const scaleOptions = [
|
|
73
|
+
{ value: 'auto', label: 'Auto' },
|
|
74
|
+
{ value: '0.25', label: '0.25x' },
|
|
75
|
+
{ value: '0.5', label: '0.5x' },
|
|
76
|
+
{ value: '1', label: '1x' },
|
|
77
|
+
{ value: '2', label: '2x' },
|
|
78
|
+
{ value: '3', label: '3x' },
|
|
79
|
+
{ value: '4', label: '4x' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
export const menuScaleOptions = [
|
|
83
|
+
{ value: 'auto', label: 'Auto' },
|
|
84
|
+
{ value: '1.0', label: '1x' },
|
|
85
|
+
{ value: '1.5', label: '1.5x' },
|
|
86
|
+
{ value: '2.0', label: '2x' },
|
|
87
|
+
{ value: '2.5', label: '2.5x' },
|
|
88
|
+
{ value: '3.0', label: '3x' },
|
|
89
|
+
{ value: '4.0', label: '4x' },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export const postProcessingModeOptions = [
|
|
93
|
+
{ value: 'off', label: 'Off' },
|
|
94
|
+
{ value: 'crt', label: 'CRT' },
|
|
95
|
+
{ value: 'custom', label: 'Custom' },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// Post-processing effect options that are only visible in 'custom' mode
|
|
99
|
+
export const customOnlyEffectIds = new Set([
|
|
100
|
+
'video_gamma',
|
|
101
|
+
'video_scanlines',
|
|
102
|
+
'video_saturation',
|
|
103
|
+
'video_vignette',
|
|
104
|
+
'video_curvature',
|
|
105
|
+
'video_chromatic_aberration',
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
// Settings that are only visible when native render mode is selected
|
|
109
|
+
export const nativeOnlyIds = new Set([
|
|
110
|
+
'menu_scale_factor',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
export const settingsCategories: SettingsCategory[] = [
|
|
114
|
+
{
|
|
115
|
+
name: 'Emulation',
|
|
116
|
+
options: [
|
|
117
|
+
{
|
|
118
|
+
id: 'video_driver',
|
|
119
|
+
label: 'Render Mode',
|
|
120
|
+
type: 'select',
|
|
121
|
+
options: videoDriverOptions,
|
|
122
|
+
getValue: (config) => config.video_driver === null ? 'auto' : config.video_driver,
|
|
123
|
+
setValue: (config, value, configPath) => {
|
|
124
|
+
if (value === 'auto') {
|
|
125
|
+
config.video_driver = null;
|
|
126
|
+
resetConfigValue('video_driver', configPath);
|
|
127
|
+
} else if (isVideoDriver(value)) {
|
|
128
|
+
config.video_driver = value;
|
|
129
|
+
updateConfigValue('video_driver', config.video_driver, configPath);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'video_scale',
|
|
135
|
+
label: 'Video Scale',
|
|
136
|
+
type: 'select',
|
|
137
|
+
options: scaleOptions,
|
|
138
|
+
getValue: (config) => config.video_scale === null ? 'auto' : String(config.video_scale),
|
|
139
|
+
setValue: (config, value, configPath) => {
|
|
140
|
+
if (!isString(value)) { return; }
|
|
141
|
+
if (value === 'auto') {
|
|
142
|
+
config.video_scale = null;
|
|
143
|
+
resetConfigValue('video_scale', configPath);
|
|
144
|
+
} else {
|
|
145
|
+
config.video_scale = parseFloat(value);
|
|
146
|
+
updateConfigValue('video_scale', config.video_scale, configPath);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'menu_scale_factor',
|
|
152
|
+
label: 'Native UI Scale',
|
|
153
|
+
type: 'select',
|
|
154
|
+
options: menuScaleOptions,
|
|
155
|
+
getValue: (config) => config.menu_scale_factor === null ? 'auto' : config.menu_scale_factor.toFixed(1),
|
|
156
|
+
setValue: (config, value, configPath) => {
|
|
157
|
+
if (!isString(value)) { return; }
|
|
158
|
+
if (value === 'auto') {
|
|
159
|
+
config.menu_scale_factor = null;
|
|
160
|
+
resetConfigValue('menu_scale_factor', configPath);
|
|
161
|
+
} else {
|
|
162
|
+
config.menu_scale_factor = parseFloat(value);
|
|
163
|
+
updateConfigValue('menu_scale_factor', config.menu_scale_factor, configPath);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'audio_enable',
|
|
169
|
+
label: 'Audio',
|
|
170
|
+
type: 'toggle',
|
|
171
|
+
// Audio is effectively ON when enabled and not muted
|
|
172
|
+
getValue: (config) => config.audio_enable && !config.audio_mute_enable,
|
|
173
|
+
setValue: (config, value, configPath) => {
|
|
174
|
+
if (!isBoolean(value)) { return; }
|
|
175
|
+
// Toggle mute state (keep audio_enable as master switch)
|
|
176
|
+
// ON = unmute, OFF = mute
|
|
177
|
+
config.audio_mute_enable = !value;
|
|
178
|
+
updateConfigValue('audio_mute_enable', config.audio_mute_enable, configPath);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
createToggleOption('input_joypad_enable', 'Gamepad Support'),
|
|
182
|
+
{
|
|
183
|
+
id: 'notifications_enable',
|
|
184
|
+
label: 'Notifications',
|
|
185
|
+
type: 'toggle',
|
|
186
|
+
getValue: (config) => config.notifications_enable,
|
|
187
|
+
setValue: (config, value, configPath) => {
|
|
188
|
+
if (!isBoolean(value)) { return; }
|
|
189
|
+
config.notifications_enable = value;
|
|
190
|
+
updateConfigValue('notifications_enable', value, configPath);
|
|
191
|
+
setNotificationsEnabled(value);
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
createToggleOption('savestate_auto_load', 'Auto-load Save States'),
|
|
195
|
+
createToggleOption('savestate_auto_save', 'Auto-save Save States'),
|
|
196
|
+
createToggleOption('savestates_in_content_dir', 'Save States to ROM Dir'),
|
|
197
|
+
createToggleOption('savefiles_in_content_dir', 'Battery Saves to ROM Dir'),
|
|
198
|
+
createToggleOption('fps_show_enable', 'Status Bar'),
|
|
199
|
+
createIntSelectOption('video_frame_limit', 'Frame Limit', [
|
|
200
|
+
{ value: '15', label: '15 fps' },
|
|
201
|
+
{ value: '30', label: '30 fps' },
|
|
202
|
+
{ value: '0', label: 'Off' },
|
|
203
|
+
]),
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'Post-Processing',
|
|
208
|
+
options: [
|
|
209
|
+
{
|
|
210
|
+
id: 'video_postprocessing_mode',
|
|
211
|
+
label: 'Mode',
|
|
212
|
+
type: 'select',
|
|
213
|
+
options: postProcessingModeOptions,
|
|
214
|
+
getValue: (config) => config.video_postprocessing_mode,
|
|
215
|
+
setValue: (config, value, configPath) => {
|
|
216
|
+
if (!isPostProcessingMode(value)) { return; }
|
|
217
|
+
config.video_postprocessing_mode = value;
|
|
218
|
+
updateConfigValue('video_postprocessing_mode', config.video_postprocessing_mode, configPath);
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
createFloatSelectOption('video_gamma', 'Gamma', [
|
|
222
|
+
{ value: '1.0', label: '1.0 (Linear)' },
|
|
223
|
+
{ value: '1.1', label: '1.1' },
|
|
224
|
+
{ value: '1.2', label: '1.2' },
|
|
225
|
+
{ value: '1.3', label: '1.3 (CRT)' },
|
|
226
|
+
{ value: '1.4', label: '1.4' },
|
|
227
|
+
{ value: '1.8', label: '1.8 (Mac)' },
|
|
228
|
+
{ value: '2.0', label: '2.0' },
|
|
229
|
+
{ value: '2.2', label: '2.2 (sRGB)' },
|
|
230
|
+
{ value: '2.4', label: '2.4 (Rec. 709)' },
|
|
231
|
+
]),
|
|
232
|
+
createFloatSelectOption('video_scanlines', 'Scanlines', [
|
|
233
|
+
{ value: '0', label: 'Off' },
|
|
234
|
+
{ value: '0.1', label: '0.1 (Subtle)' },
|
|
235
|
+
{ value: '0.2', label: '0.2' },
|
|
236
|
+
{ value: '0.3', label: '0.3' },
|
|
237
|
+
{ value: '0.4', label: '0.4 (Heavy)' },
|
|
238
|
+
]),
|
|
239
|
+
createFloatSelectOption('video_saturation', 'Saturation', [
|
|
240
|
+
{ value: '0.8', label: '0.8' },
|
|
241
|
+
{ value: '0.9', label: '0.9' },
|
|
242
|
+
{ value: '1.0', label: '1.0 (Default)' },
|
|
243
|
+
{ value: '1.1', label: '1.1' },
|
|
244
|
+
{ value: '1.2', label: '1.2' },
|
|
245
|
+
{ value: '1.3', label: '1.3' },
|
|
246
|
+
]),
|
|
247
|
+
createFloatSelectOption('video_vignette', 'Vignette', [
|
|
248
|
+
{ value: '0', label: 'Off' },
|
|
249
|
+
{ value: '0.2', label: '0.2 (Subtle)' },
|
|
250
|
+
{ value: '0.3', label: '0.3' },
|
|
251
|
+
{ value: '0.5', label: '0.5 (CRT)' },
|
|
252
|
+
{ value: '0.7', label: '0.7 (Strong)' },
|
|
253
|
+
]),
|
|
254
|
+
createFloatSelectOption('video_curvature', 'Curvature', [
|
|
255
|
+
{ value: '0', label: 'Off' },
|
|
256
|
+
{ value: '0.05', label: '0.05 (Subtle)' },
|
|
257
|
+
{ value: '0.1', label: '0.1 (CRT)' },
|
|
258
|
+
{ value: '0.15', label: '0.15' },
|
|
259
|
+
{ value: '0.2', label: '0.2 (Strong)' },
|
|
260
|
+
]),
|
|
261
|
+
createFloatSelectOption('video_chromatic_aberration', 'Chromatic Aberration', [
|
|
262
|
+
{ value: '0', label: 'Off' },
|
|
263
|
+
{ value: '0.3', label: '0.3 (CRT)' },
|
|
264
|
+
{ value: '0.5', label: '0.5' },
|
|
265
|
+
{ value: '1.0', label: '1.0' },
|
|
266
|
+
{ value: '1.5', label: '1.5' },
|
|
267
|
+
{ value: '2.0', label: '2.0' },
|
|
268
|
+
{ value: '2.5', label: '2.5' },
|
|
269
|
+
{ value: '3.0', label: '3.0' },
|
|
270
|
+
]),
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Filter settings categories based on post-processing mode and terminal capabilities.
|
|
277
|
+
* Custom effect options are only shown when mode is 'custom'.
|
|
278
|
+
* Kitty option is hidden if Kitty graphics protocol is not supported.
|
|
279
|
+
* Native window option is hidden if the native window backend is not available.
|
|
280
|
+
*/
|
|
281
|
+
export const filterSettingsCategories = (isCustomMode: boolean, isNativeMode: boolean, kittySupported: boolean, nativeSupported: boolean): SettingsCategory[] =>
|
|
282
|
+
settingsCategories.map(cat => ({
|
|
283
|
+
...cat,
|
|
284
|
+
options: cat.options.map(opt => {
|
|
285
|
+
// Filter unsupported video drivers from options
|
|
286
|
+
if (opt.id === 'video_driver' && opt.options) {
|
|
287
|
+
return {
|
|
288
|
+
...opt,
|
|
289
|
+
options: opt.options.filter(o => {
|
|
290
|
+
if (o.value === 'kitty' && !kittySupported) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (o.value === 'native' && !nativeSupported) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
}),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
return opt;
|
|
301
|
+
}).filter(opt =>
|
|
302
|
+
(!customOnlyEffectIds.has(opt.id) || isCustomMode) &&
|
|
303
|
+
(!nativeOnlyIds.has(opt.id) || isNativeMode)
|
|
304
|
+
),
|
|
305
|
+
})).filter(cat => cat.options.length > 0);
|
|
306
|
+
|
|
307
|
+
// Flatten categories into a single list for navigation
|
|
308
|
+
export const allSettingsOptions = settingsCategories.flatMap(cat => cat.options);
|
|
309
|
+
|
|
310
|
+
// Action items for settings panel (dynamic based on whether there's a game to resume)
|
|
311
|
+
export const getSettingsActions = (hasResumeGame: boolean) => {
|
|
312
|
+
const actions = [];
|
|
313
|
+
if (hasResumeGame) {
|
|
314
|
+
actions.push({ id: 'resume', label: 'Resume Game', icon: '\u25B6' });
|
|
315
|
+
}
|
|
316
|
+
actions.push({ id: 'back', label: 'Back to Browser', icon: '\u2190' });
|
|
317
|
+
actions.push({ id: 'reset', label: 'Reset All Settings', icon: '\u21BA' });
|
|
318
|
+
actions.push({ id: 'exit', label: 'Exit emoemu', icon: '\u2717' });
|
|
319
|
+
return actions;
|
|
320
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RomInfo } from '../../frontend/romScanner';
|
|
2
|
+
import type { SaveStateDetails } from '../../frontend/saveServices';
|
|
3
|
+
import type { NetplayOptions } from '../App';
|
|
4
|
+
import type { Config } from '../../frontend/config';
|
|
5
|
+
import type { DiscoverySessionInfo } from '../../netplay/NetplayDiscovery';
|
|
6
|
+
|
|
7
|
+
export interface RomBrowserProps {
|
|
8
|
+
roms: RomInfo[];
|
|
9
|
+
playlistDirectory: string; // Directory containing playlists
|
|
10
|
+
scanDepth: number; // Max depth for scanning subdirectories
|
|
11
|
+
onSelect: (rom: RomInfo, currentFilter: string, resumeGame?: boolean, netplay?: NetplayOptions) => void;
|
|
12
|
+
onExit: (currentFilter: string) => void;
|
|
13
|
+
onRefresh: (currentFilter: string) => void; // Trigger a refresh of the ROM list
|
|
14
|
+
initialSelection?: string; // Path of ROM to select initially
|
|
15
|
+
initialFilter?: string; // Initial search filter to apply
|
|
16
|
+
showSettingsOnMount?: boolean; // Show settings panel immediately on mount
|
|
17
|
+
lastPlayedRom?: RomInfo; // ROM that was just played (for Resume Game option)
|
|
18
|
+
showNetplayOnMount?: boolean; // Show netplay panel immediately on mount
|
|
19
|
+
onScaleFactorChange?: (scaleFactor: number | null) => void; // Callback for native UI scale changes
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Action button definitions (app-wide actions)
|
|
23
|
+
export interface ActionButtonDef {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
icon: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Settings option definition (discriminated union for type-safe getValue/setValue)
|
|
30
|
+
export interface ToggleSettingsOption {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
type: 'toggle';
|
|
34
|
+
options?: undefined;
|
|
35
|
+
getValue: (config: Config) => boolean;
|
|
36
|
+
setValue: (config: Config, value: boolean, configPath?: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SelectSettingsOption {
|
|
40
|
+
id: string;
|
|
41
|
+
label: string;
|
|
42
|
+
type: 'select';
|
|
43
|
+
options: { value: string; label: string }[];
|
|
44
|
+
getValue: (config: Config) => string;
|
|
45
|
+
setValue: (config: Config, value: string, configPath?: string) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SettingsOption = ToggleSettingsOption | SelectSettingsOption;
|
|
49
|
+
|
|
50
|
+
// Settings category definition
|
|
51
|
+
export interface SettingsCategory {
|
|
52
|
+
name: string;
|
|
53
|
+
options: SettingsOption[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MetadataPanelProps {
|
|
57
|
+
rom: RomInfo | null | undefined;
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
saveStateDetails?: SaveStateDetails;
|
|
61
|
+
thumbnail?: string;
|
|
62
|
+
isKittySupported: boolean;
|
|
63
|
+
panelStartCol: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Discovered host info extended with address */
|
|
67
|
+
export type DiscoveredHost = DiscoverySessionInfo & { address: string };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save State Dialog Component
|
|
3
|
+
*
|
|
4
|
+
* Shows save state metadata and asks the user how to proceed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Box, Text } from 'ink';
|
|
8
|
+
import { DialogOptionsList } from '../DialogOptionsList';
|
|
9
|
+
import { DialogContainer } from '../DialogContainer';
|
|
10
|
+
import { useDialogNavigation } from '../hooks/useDialogNavigation';
|
|
11
|
+
import { launchDialog, type DialogRenderOptions } from '../NativeDialog';
|
|
12
|
+
import { SEPARATOR_LINE_PADDING } from '..';
|
|
13
|
+
import { CORRUPTED_DIALOG_MIN_WIDTH } from './consts';
|
|
14
|
+
|
|
15
|
+
export * from './consts';
|
|
16
|
+
|
|
17
|
+
export interface SaveStateInfo {
|
|
18
|
+
path: string;
|
|
19
|
+
romName: string;
|
|
20
|
+
coreName: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SaveStateChoice = 'resume' | 'delete' | 'cancel';
|
|
24
|
+
|
|
25
|
+
interface SaveStateDialogProps {
|
|
26
|
+
info: SaveStateInfo;
|
|
27
|
+
onChoice: (choice: SaveStateChoice) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SaveStateDialog = ({ info, onChoice }: SaveStateDialogProps) => {
|
|
31
|
+
const options: { label: string; choice: SaveStateChoice; color: string }[] = [
|
|
32
|
+
{ label: 'Resume from Save State', choice: 'resume', color: 'green' },
|
|
33
|
+
{ label: 'Delete Save & Start Fresh', choice: 'delete', color: 'red' },
|
|
34
|
+
{ label: 'Cancel', choice: 'cancel', color: 'gray' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const { selectedIndex } = useDialogNavigation({
|
|
38
|
+
itemCount: options.length,
|
|
39
|
+
onSelect: (index) => onChoice(options[index].choice),
|
|
40
|
+
onCancel: () => onChoice('cancel'),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<DialogContainer>
|
|
45
|
+
{(boxWidth) => (
|
|
46
|
+
<>
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<Box
|
|
49
|
+
flexDirection="column"
|
|
50
|
+
borderStyle="round"
|
|
51
|
+
borderColor="cyan"
|
|
52
|
+
paddingX={2}
|
|
53
|
+
paddingY={1}
|
|
54
|
+
width={boxWidth}
|
|
55
|
+
>
|
|
56
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
57
|
+
<Text bold color="cyan">{'\u{1F4BE}'} Save State Found</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
|
|
60
|
+
{/* ROM Name */}
|
|
61
|
+
<Box marginBottom={1}>
|
|
62
|
+
<Text color="gray">ROM: </Text>
|
|
63
|
+
<Text color="white" bold>{info.romName}</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
|
|
66
|
+
{/* Core Name */}
|
|
67
|
+
<Box>
|
|
68
|
+
<Text color="gray">{'Core: '}</Text>
|
|
69
|
+
<Text color="white">{info.coreName}</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
</Box>
|
|
72
|
+
|
|
73
|
+
<DialogOptionsList options={options} selectedIndex={selectedIndex} boxWidth={boxWidth} />
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
</DialogContainer>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Show the save state dialog and get user's choice
|
|
82
|
+
*/
|
|
83
|
+
export const showSaveStateDialog = (
|
|
84
|
+
info: SaveStateInfo,
|
|
85
|
+
options: DialogRenderOptions = {}
|
|
86
|
+
): Promise<SaveStateChoice> => launchDialog<SaveStateChoice>(
|
|
87
|
+
(onChoice) => <SaveStateDialog info={info} onChoice={onChoice} />,
|
|
88
|
+
'cancel',
|
|
89
|
+
{ ...options, title: options.title ?? 'emoemu - Save State' },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
export default SaveStateDialog;
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Corrupted State Dialog
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
export interface CorruptedStateInfo {
|
|
99
|
+
path: string;
|
|
100
|
+
romName: string;
|
|
101
|
+
/** Whether the file could be read at all */
|
|
102
|
+
fileReadable: boolean;
|
|
103
|
+
/** Whether it's a binary file (libretro) or JSON (native core) */
|
|
104
|
+
isBinary: boolean;
|
|
105
|
+
/** Whether it was valid JSON (only relevant for native cores) */
|
|
106
|
+
validJson: boolean;
|
|
107
|
+
/** Whether we can attempt to load it (file readable and correct format) */
|
|
108
|
+
canAttemptLoad: boolean;
|
|
109
|
+
/** The specific error that caused validation to fail */
|
|
110
|
+
errorReason: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type CorruptedStateChoice = 'try_load' | 'continue' | 'cancel';
|
|
114
|
+
|
|
115
|
+
interface CorruptedStateDialogProps {
|
|
116
|
+
info: CorruptedStateInfo;
|
|
117
|
+
onChoice: (choice: CorruptedStateChoice) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const CorruptedStateDialog = ({ info, onChoice }: CorruptedStateDialogProps) => {
|
|
121
|
+
// Build options dynamically - offer "Try Loading" if we can attempt to load
|
|
122
|
+
const options: { label: string; choice: CorruptedStateChoice; color: string }[] = [];
|
|
123
|
+
|
|
124
|
+
if (info.canAttemptLoad) {
|
|
125
|
+
options.push({ label: 'Try Loading Anyway', choice: 'try_load', color: 'green' });
|
|
126
|
+
}
|
|
127
|
+
options.push({ label: 'Start Fresh (overwrites save)', choice: 'continue', color: 'yellow' });
|
|
128
|
+
options.push({ label: 'Cancel', choice: 'cancel', color: 'gray' });
|
|
129
|
+
|
|
130
|
+
const { selectedIndex } = useDialogNavigation({
|
|
131
|
+
itemCount: options.length,
|
|
132
|
+
onSelect: (index) => onChoice(options[index].choice),
|
|
133
|
+
onCancel: () => onChoice('cancel'),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<DialogContainer minWidth={CORRUPTED_DIALOG_MIN_WIDTH}>
|
|
138
|
+
{(boxWidth) => (
|
|
139
|
+
<>
|
|
140
|
+
{/* Header */}
|
|
141
|
+
<Box
|
|
142
|
+
flexDirection="column"
|
|
143
|
+
borderStyle="round"
|
|
144
|
+
borderColor="yellow"
|
|
145
|
+
paddingX={2}
|
|
146
|
+
paddingY={1}
|
|
147
|
+
width={boxWidth}
|
|
148
|
+
>
|
|
149
|
+
<Box justifyContent="center" marginBottom={1}>
|
|
150
|
+
<Text bold color="yellow">{'\u26A0'} Corrupted Save State</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
|
|
153
|
+
{/* ROM Name */}
|
|
154
|
+
<Box marginBottom={1}>
|
|
155
|
+
<Text color="gray">ROM: </Text>
|
|
156
|
+
<Text color="white" bold>{info.romName}</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
|
|
159
|
+
{/* Parse Status Checklist */}
|
|
160
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
161
|
+
<Box>
|
|
162
|
+
<Text color="gray">{'File readable: '}</Text>
|
|
163
|
+
<Text color={info.fileReadable ? 'green' : 'red'}>
|
|
164
|
+
{info.fileReadable ? '\u2713 Yes' : '\u2717 No'}
|
|
165
|
+
</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
<Box>
|
|
168
|
+
<Text color="gray">{'Format: '}</Text>
|
|
169
|
+
<Text color="white">
|
|
170
|
+
{info.isBinary ? 'Binary (libretro)' : 'JSON (native)'}
|
|
171
|
+
</Text>
|
|
172
|
+
</Box>
|
|
173
|
+
{!info.isBinary && (
|
|
174
|
+
<Box>
|
|
175
|
+
<Text color="gray">{'Valid JSON: '}</Text>
|
|
176
|
+
<Text color={info.validJson ? 'green' : 'red'}>
|
|
177
|
+
{info.validJson ? '\u2713 Yes' : '\u2717 No'}
|
|
178
|
+
</Text>
|
|
179
|
+
</Box>
|
|
180
|
+
)}
|
|
181
|
+
</Box>
|
|
182
|
+
|
|
183
|
+
{/* Error reason */}
|
|
184
|
+
<Box flexDirection="column">
|
|
185
|
+
<Text color="red" dimColor>{'─'.repeat(boxWidth - SEPARATOR_LINE_PADDING)}</Text>
|
|
186
|
+
<Box marginTop={1}>
|
|
187
|
+
<Text color="red">Error: </Text>
|
|
188
|
+
<Text color="white">{info.errorReason}</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
</Box>
|
|
191
|
+
</Box>
|
|
192
|
+
|
|
193
|
+
{/* Warning */}
|
|
194
|
+
<Box
|
|
195
|
+
flexDirection="column"
|
|
196
|
+
borderStyle="round"
|
|
197
|
+
borderColor="yellow"
|
|
198
|
+
paddingX={2}
|
|
199
|
+
paddingY={1}
|
|
200
|
+
marginTop={1}
|
|
201
|
+
width={boxWidth}
|
|
202
|
+
>
|
|
203
|
+
<Text color="yellow">
|
|
204
|
+
{'\u26A0'} Starting fresh will overwrite this save state
|
|
205
|
+
</Text>
|
|
206
|
+
</Box>
|
|
207
|
+
|
|
208
|
+
<DialogOptionsList options={options} selectedIndex={selectedIndex} boxWidth={boxWidth} />
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</DialogContainer>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Show the corrupted state dialog and get user's choice
|
|
217
|
+
*/
|
|
218
|
+
export const showCorruptedStateDialog = (
|
|
219
|
+
info: CorruptedStateInfo,
|
|
220
|
+
options: DialogRenderOptions = {}
|
|
221
|
+
): Promise<CorruptedStateChoice> => launchDialog<CorruptedStateChoice>(
|
|
222
|
+
(onChoice) => <CorruptedStateDialog info={info} onChoice={onChoice} />,
|
|
223
|
+
'cancel',
|
|
224
|
+
{ ...options, title: options.title ?? 'emoemu - Corrupted Save State' },
|
|
225
|
+
);
|