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
package/src/index.ts
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { VERSION_WITH_DATE as VERSION, BUILD_DATE } from "./consts";
|
|
4
|
+
import { statSync } from "fs";
|
|
5
|
+
import { expandPath } from "./utils/paths";
|
|
6
|
+
import { detectKittyGraphicsSupport } from "./utils/kitty";
|
|
7
|
+
import { showWarningDialog } from "./ui";
|
|
8
|
+
import { logger } from "./utils/logger";
|
|
9
|
+
import { cpus } from "os";
|
|
10
|
+
import { loadConfig, getPlaylistsDirectory } from "./frontend/config";
|
|
11
|
+
import { updateServices } from "./frontend/serviceProvider";
|
|
12
|
+
|
|
13
|
+
import { isFensterAvailable, getWindowManager } from "./rendering/nativeUi";
|
|
14
|
+
import {
|
|
15
|
+
generatePlaylistsBySystem,
|
|
16
|
+
updatePlaylistRuntime,
|
|
17
|
+
buildPlaylistIndex,
|
|
18
|
+
normalizePath,
|
|
19
|
+
} from "./frontend/playlist";
|
|
20
|
+
|
|
21
|
+
// Load libretro cores from default paths (not RetroArch - use --retroarch flag for that)
|
|
22
|
+
import {
|
|
23
|
+
loadDefaultLibretroCores,
|
|
24
|
+
loadRetroArchCores,
|
|
25
|
+
loadCoresFromConfig,
|
|
26
|
+
} from "./cores/libretro/loader";
|
|
27
|
+
import { launchBrowser, importDirectory, validateRomFile } from "./ui";
|
|
28
|
+
import type { RomInfo } from "./ui";
|
|
29
|
+
import { STDIN_SETTLE_DELAY_MS } from "./ui";
|
|
30
|
+
|
|
31
|
+
import { parseArgs, updateOptionsFromConfig } from "./cli/parseArgs";
|
|
32
|
+
import {
|
|
33
|
+
printUsage,
|
|
34
|
+
debugGamepad,
|
|
35
|
+
listGamepads,
|
|
36
|
+
listCoresCommand,
|
|
37
|
+
installCoreCommand,
|
|
38
|
+
removeCoreCommand,
|
|
39
|
+
clearLogsCommand,
|
|
40
|
+
generatePlaylistCommand,
|
|
41
|
+
} from "./cli/commands";
|
|
42
|
+
import { runEmulator } from "./cli/runEmulator";
|
|
43
|
+
|
|
44
|
+
loadDefaultLibretroCores();
|
|
45
|
+
|
|
46
|
+
// Build date format constants (YYYYMMDD)
|
|
47
|
+
const BUILD_DATE_LENGTH = 8;
|
|
48
|
+
const BUILD_DATE_YEAR_END = 4;
|
|
49
|
+
const BUILD_DATE_MONTH_END = 6;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format build date from YYYYMMDD to "Mon DD YYYY" format
|
|
53
|
+
* e.g., "20260121" -> "Jan 21 2026"
|
|
54
|
+
*/
|
|
55
|
+
const formatBuildDate = (dateStr: string): string => {
|
|
56
|
+
if (!dateStr || dateStr.length !== BUILD_DATE_LENGTH) {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
const year = parseInt(dateStr.slice(0, BUILD_DATE_YEAR_END), 10);
|
|
60
|
+
const month = parseInt(dateStr.slice(BUILD_DATE_YEAR_END, BUILD_DATE_MONTH_END), 10) - 1;
|
|
61
|
+
const day = parseInt(dateStr.slice(BUILD_DATE_MONTH_END, BUILD_DATE_LENGTH), 10);
|
|
62
|
+
const date = new Date(year, month, day);
|
|
63
|
+
if (isNaN(date.getTime())) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
// Format without comma: "Jan 21 2026"
|
|
67
|
+
const formatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
68
|
+
const parts = formatter.formatToParts(date);
|
|
69
|
+
const monthPart = parts.find(p => p.type === 'month')?.value ?? '';
|
|
70
|
+
const dayPart = parts.find(p => p.type === 'day')?.value ?? '';
|
|
71
|
+
const yearPart = parts.find(p => p.type === 'year')?.value ?? '';
|
|
72
|
+
return `${monthPart} ${dayPart} ${yearPart}`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const main = async (): Promise<void> => {
|
|
76
|
+
const args = process.argv.slice(2);
|
|
77
|
+
const options = parseArgs(args);
|
|
78
|
+
|
|
79
|
+
// Ensure the shared native window is torn down on any exit path.
|
|
80
|
+
process.on("exit", () => {
|
|
81
|
+
const wm = getWindowManager();
|
|
82
|
+
if (wm.isInitialized()) {
|
|
83
|
+
wm.destroy();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Check native window availability if native mode is requested
|
|
88
|
+
const nativeRequested = options.renderMode === "native" || options.config.video_driver === "native";
|
|
89
|
+
if (nativeRequested && !isFensterAvailable()) {
|
|
90
|
+
const warnMsg = "Native window mode is not available. Falling back to Kitty graphics protocol.";
|
|
91
|
+
logger.warn(warnMsg, "Video");
|
|
92
|
+
const choice = await showWarningDialog(warnMsg, { title: "Native Mode Not Available" });
|
|
93
|
+
if (choice === "exit") {
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (options.renderMode === "native") {
|
|
97
|
+
options.renderMode = "kitty";
|
|
98
|
+
}
|
|
99
|
+
if (options.config.video_driver === "native") {
|
|
100
|
+
options.config.video_driver = "kitty";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check Kitty graphics support if using Kitty mode (explicit or auto/default)
|
|
105
|
+
const willUseKitty = options.renderMode === "kitty" ||
|
|
106
|
+
options.renderMode === undefined ||
|
|
107
|
+
options.config.video_driver === "kitty" ||
|
|
108
|
+
options.config.video_driver === null;
|
|
109
|
+
if (willUseKitty && !await detectKittyGraphicsSupport()) {
|
|
110
|
+
const warnMsg = "Your terminal does not support the Kitty graphics protocol. " +
|
|
111
|
+
"For the best experience, we recommend using a terminal that supports it:\n\n" +
|
|
112
|
+
" \u2022 Ghostty (recommended): https://ghostty.org\n" +
|
|
113
|
+
" \u2022 iTerm2: https://iterm2.com\n" +
|
|
114
|
+
" \u2022 kitty: https://sw.kovidgoyal.net/kitty\n\n" +
|
|
115
|
+
"The emulator will fall back to Unicode half-block rendering.";
|
|
116
|
+
logger.warn("Terminal does not support Kitty graphics protocol", "Video");
|
|
117
|
+
const choice = await showWarningDialog(warnMsg, { title: "Terminal Compatibility" });
|
|
118
|
+
if (choice === "exit") {
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
// Fall back to terminal (Unicode half-block) mode
|
|
122
|
+
if (options.renderMode === "kitty" || options.renderMode === undefined) {
|
|
123
|
+
options.renderMode = "terminal";
|
|
124
|
+
}
|
|
125
|
+
if (options.config.video_driver === "kitty" || options.config.video_driver === null) {
|
|
126
|
+
options.config.video_driver = "terminal";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Configure logging from config options
|
|
131
|
+
// Set custom log directory if specified (with ~ expansion)
|
|
132
|
+
if (options.config.log_dir) {
|
|
133
|
+
logger.setLogDirectory(expandPath(options.config.log_dir));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Set whether to log to file or console
|
|
137
|
+
logger.setLogToFile(options.config.log_to_file);
|
|
138
|
+
|
|
139
|
+
// Set timestamped file mode (only applies when log_to_file is true)
|
|
140
|
+
logger.setUseTimestampedFile(options.config.log_to_file_timestamp);
|
|
141
|
+
|
|
142
|
+
// Enable logging based on config (log_verbosity)
|
|
143
|
+
logger.setEnabled(options.config.log_verbosity);
|
|
144
|
+
|
|
145
|
+
// Enable stderr output if --verbose flag is set
|
|
146
|
+
if (options.verbose) {
|
|
147
|
+
logger.setEnabled(true); // --verbose implies logging enabled
|
|
148
|
+
logger.setLogToStderr(true);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Log startup information (RetroArch-style)
|
|
152
|
+
logger.info(`emoemu ${VERSION}`, 'emoemu');
|
|
153
|
+
logger.info('=== Build =======================================');
|
|
154
|
+
logger.info(`Version: ${VERSION}`);
|
|
155
|
+
const builtDate = formatBuildDate(BUILD_DATE);
|
|
156
|
+
if (builtDate) {
|
|
157
|
+
logger.info(`Built: ${builtDate}`);
|
|
158
|
+
}
|
|
159
|
+
logger.info(`CPU Model Name: ${cpus()[0]?.model ?? 'Unknown'}`);
|
|
160
|
+
logger.info(`Node.js: ${process.version}`);
|
|
161
|
+
logger.info('=================================================');
|
|
162
|
+
|
|
163
|
+
// Handle --clear-logs early (clears logs and continues)
|
|
164
|
+
if (options.clearLogs) {
|
|
165
|
+
clearLogsCommand();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Load cores from config's libretro_directory if specified (RetroArch-compatible)
|
|
169
|
+
if (options.config.libretro_directory) {
|
|
170
|
+
loadCoresFromConfig(options.config.libretro_directory);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Load RetroArch cores if requested (before listing or detecting cores)
|
|
174
|
+
if (options.loadRetroArch) {
|
|
175
|
+
loadRetroArchCores();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Handle --list-gamepads before checking for ROM
|
|
179
|
+
if (options.listGamepads) {
|
|
180
|
+
listGamepads();
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle --list-cores before checking for ROM
|
|
185
|
+
if (options.listCoresFlag) {
|
|
186
|
+
listCoresCommand();
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle --install-core before checking for ROM
|
|
191
|
+
if (options.installCore) {
|
|
192
|
+
await installCoreCommand(options.installCore);
|
|
193
|
+
// If no ROM specified, exit after installation
|
|
194
|
+
if (!options.romPath) {
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
// Otherwise continue to run the emulator with the installed core
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle --remove-core before checking for ROM
|
|
201
|
+
if (options.removeCore) {
|
|
202
|
+
removeCoreCommand(options.removeCore);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle --debug-gamepad before checking for ROM
|
|
207
|
+
if (options.debugGamepad) {
|
|
208
|
+
debugGamepad();
|
|
209
|
+
// debugGamepad() doesn't return - runs until Ctrl+C
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Handle --generate-playlist before checking for ROM
|
|
214
|
+
if (options.generatePlaylist) {
|
|
215
|
+
const scanPath = typeof options.generatePlaylist === 'string'
|
|
216
|
+
? options.generatePlaylist
|
|
217
|
+
: process.cwd();
|
|
218
|
+
generatePlaylistCommand(
|
|
219
|
+
scanPath,
|
|
220
|
+
options.scanDepth,
|
|
221
|
+
options.playlistOutput,
|
|
222
|
+
options.singlePlaylist,
|
|
223
|
+
options.windowsPaths
|
|
224
|
+
);
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (options.help) {
|
|
229
|
+
printUsage();
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (options.showVersion) {
|
|
234
|
+
console.log(`emoemu ${VERSION}`);
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Track the last played ROM and filter for restoring browser state
|
|
239
|
+
let lastPlayedRom: string | undefined;
|
|
240
|
+
let lastPlayedRomInfo: RomInfo | undefined;
|
|
241
|
+
let lastPlayedCoreId: string | undefined; // Core ID used for last played game (for resume)
|
|
242
|
+
let lastFilter: string | undefined;
|
|
243
|
+
let showSettingsOnMount = false; // Show settings menu after exiting a game
|
|
244
|
+
let showNetplayOnMount = false; // Show netplay panel after netplay connection failure
|
|
245
|
+
|
|
246
|
+
// Handle CLI path argument (directory or ROM file)
|
|
247
|
+
if (options.romPath) {
|
|
248
|
+
const playlistDir = getPlaylistsDirectory(options.config);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const stats = statSync(options.romPath);
|
|
252
|
+
if (stats.isDirectory()) {
|
|
253
|
+
// Directory provided: import ROMs with progress bar UI, then show browser
|
|
254
|
+
await importDirectory(options.romPath, options.scanDepth, options.config);
|
|
255
|
+
} else {
|
|
256
|
+
// ROM file provided: auto-import silently and launch immediately
|
|
257
|
+
const playlistIndex = buildPlaylistIndex(playlistDir);
|
|
258
|
+
const normalizedPath = normalizePath(options.romPath);
|
|
259
|
+
if (!playlistIndex.has(normalizedPath)) {
|
|
260
|
+
const validateResult = validateRomFile(options.romPath);
|
|
261
|
+
if (validateResult.valid) {
|
|
262
|
+
generatePlaylistsBySystem([validateResult.rom], playlistDir);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = await runEmulator(options.romPath, options);
|
|
267
|
+
if (!result.shouldContinue) {
|
|
268
|
+
// Still update runtime even when exiting (user might press Esc to quit)
|
|
269
|
+
if (result.gameWasPlayed && result.sessionSeconds !== undefined) {
|
|
270
|
+
updatePlaylistRuntime(options.romPath, playlistDir, result.sessionSeconds);
|
|
271
|
+
}
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
// Check if netplay disconnected - show netplay panel instead of settings
|
|
275
|
+
if (result.showNetplayOnReturn) {
|
|
276
|
+
showNetplayOnMount = true;
|
|
277
|
+
showSettingsOnMount = false;
|
|
278
|
+
lastPlayedRom = options.romPath;
|
|
279
|
+
lastPlayedCoreId = result.coreId;
|
|
280
|
+
} else if (result.gameWasPlayed) {
|
|
281
|
+
lastPlayedRom = options.romPath;
|
|
282
|
+
lastPlayedCoreId = result.coreId; // Track which core was used
|
|
283
|
+
// Get RomInfo for the resume game feature
|
|
284
|
+
const validateResult = validateRomFile(options.romPath);
|
|
285
|
+
if (validateResult.valid) {
|
|
286
|
+
lastPlayedRomInfo = validateResult.rom;
|
|
287
|
+
}
|
|
288
|
+
showSettingsOnMount = true; // Show settings when returning from a game
|
|
289
|
+
|
|
290
|
+
// Update playlist runtime (RetroArch compatible)
|
|
291
|
+
if (result.sessionSeconds !== undefined) {
|
|
292
|
+
updatePlaylistRuntime(options.romPath, playlistDir, result.sessionSeconds);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
// Path doesn't exist - treat as ROM path and let runEmulator handle the error
|
|
298
|
+
const playlistIndex = buildPlaylistIndex(playlistDir);
|
|
299
|
+
const normalizedPath = normalizePath(options.romPath);
|
|
300
|
+
if (!playlistIndex.has(normalizedPath)) {
|
|
301
|
+
const validateResult = validateRomFile(options.romPath);
|
|
302
|
+
if (validateResult.valid) {
|
|
303
|
+
generatePlaylistsBySystem([validateResult.rom], playlistDir);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = await runEmulator(options.romPath, options);
|
|
308
|
+
if (!result.shouldContinue) {
|
|
309
|
+
if (result.gameWasPlayed && result.sessionSeconds !== undefined) {
|
|
310
|
+
updatePlaylistRuntime(options.romPath, playlistDir, result.sessionSeconds);
|
|
311
|
+
}
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
// Check if netplay disconnected - show netplay panel instead of settings
|
|
315
|
+
if (result.showNetplayOnReturn) {
|
|
316
|
+
showNetplayOnMount = true;
|
|
317
|
+
showSettingsOnMount = false;
|
|
318
|
+
lastPlayedRom = options.romPath;
|
|
319
|
+
lastPlayedCoreId = result.coreId;
|
|
320
|
+
} else if (result.gameWasPlayed) {
|
|
321
|
+
lastPlayedRom = options.romPath;
|
|
322
|
+
lastPlayedCoreId = result.coreId;
|
|
323
|
+
const validateResult = validateRomFile(options.romPath);
|
|
324
|
+
if (validateResult.valid) {
|
|
325
|
+
lastPlayedRomInfo = validateResult.rom;
|
|
326
|
+
}
|
|
327
|
+
showSettingsOnMount = true;
|
|
328
|
+
if (result.sessionSeconds !== undefined) {
|
|
329
|
+
updatePlaylistRuntime(options.romPath, playlistDir, result.sessionSeconds);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Main browser loop - user can only exit the app from here
|
|
336
|
+
for (;;) {
|
|
337
|
+
// Reset stdin state for Ink to take over
|
|
338
|
+
process.stdin.removeAllListeners('data');
|
|
339
|
+
process.stdin.removeAllListeners('keypress');
|
|
340
|
+
process.stdin.removeAllListeners('readable');
|
|
341
|
+
|
|
342
|
+
// Small delay to let event loop settle before launching browser
|
|
343
|
+
await new Promise(resolve => setTimeout(resolve, STDIN_SETTLE_DELAY_MS));
|
|
344
|
+
|
|
345
|
+
const result = await launchBrowser(options.scanDepth, lastPlayedRom, lastFilter, options.config, options.configPath, showSettingsOnMount, lastPlayedRomInfo, lastPlayedCoreId, showNetplayOnMount);
|
|
346
|
+
|
|
347
|
+
// Reset mount flags after launching (only show once)
|
|
348
|
+
showSettingsOnMount = false;
|
|
349
|
+
showNetplayOnMount = false;
|
|
350
|
+
|
|
351
|
+
// Always track the filter for next time
|
|
352
|
+
lastFilter = result.filter;
|
|
353
|
+
|
|
354
|
+
// Check if user triggered a refresh (e.g., after adding ROMs)
|
|
355
|
+
if (result.shouldRefresh) {
|
|
356
|
+
continue; // Re-launch browser with fresh ROM list
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!result.path) {
|
|
360
|
+
// User exited browser - quit the app
|
|
361
|
+
process.exit(0);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Thoroughly reset stdin for emulator to take over
|
|
365
|
+
// Remove all listeners that Ink may have attached
|
|
366
|
+
process.stdin.removeAllListeners();
|
|
367
|
+
|
|
368
|
+
// Drain any pending input that might be buffered
|
|
369
|
+
process.stdin.read();
|
|
370
|
+
|
|
371
|
+
// Reset TTY state - must be done in this order
|
|
372
|
+
if (process.stdin.isTTY) {
|
|
373
|
+
process.stdin.setRawMode(false);
|
|
374
|
+
}
|
|
375
|
+
process.stdin.pause();
|
|
376
|
+
|
|
377
|
+
// Small delay for stdin to fully settle
|
|
378
|
+
await new Promise(resolve => setTimeout(resolve, STDIN_SETTLE_DELAY_MS));
|
|
379
|
+
|
|
380
|
+
// Now pre-configure stdin for the emulator
|
|
381
|
+
// The emulator's setupStdin will set rawMode to true, but we need stdin resumed first
|
|
382
|
+
process.stdin.resume();
|
|
383
|
+
|
|
384
|
+
// Reload config from disk in case settings were changed in the browser
|
|
385
|
+
const { config: freshConfig } = loadConfig(options.configPath);
|
|
386
|
+
options.config = freshConfig;
|
|
387
|
+
updateServices(freshConfig);
|
|
388
|
+
|
|
389
|
+
// Update runtime options from fresh config (for settings changed in browser)
|
|
390
|
+
updateOptionsFromConfig(options, freshConfig);
|
|
391
|
+
|
|
392
|
+
const emulatorResult = await runEmulator(result.path, options, result.resumeGame, result.resumeCoreId, result.netplay);
|
|
393
|
+
|
|
394
|
+
// Check if user wants to exit the app entirely (e.g., CTRL-C on netplay disconnect dialog)
|
|
395
|
+
if (!emulatorResult.shouldContinue) {
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check if netplay failed - show netplay panel instead of settings
|
|
400
|
+
if (emulatorResult.showNetplayOnReturn) {
|
|
401
|
+
showNetplayOnMount = true;
|
|
402
|
+
showSettingsOnMount = false;
|
|
403
|
+
// Still track last played ROM so it's selected when netplay panel opens
|
|
404
|
+
lastPlayedRom = result.path;
|
|
405
|
+
lastPlayedCoreId = emulatorResult.coreId;
|
|
406
|
+
} else if (emulatorResult.gameWasPlayed) {
|
|
407
|
+
// Only update state if game was actually played (not cancelled from dialog)
|
|
408
|
+
// Track for next browser launch
|
|
409
|
+
lastPlayedRom = result.path;
|
|
410
|
+
lastPlayedCoreId = emulatorResult.coreId; // Track which core was used
|
|
411
|
+
// Get RomInfo for the resume game feature
|
|
412
|
+
const validateResult = validateRomFile(result.path);
|
|
413
|
+
if (validateResult.valid) {
|
|
414
|
+
lastPlayedRomInfo = validateResult.rom;
|
|
415
|
+
}
|
|
416
|
+
// Show settings menu when returning from a game
|
|
417
|
+
showSettingsOnMount = true;
|
|
418
|
+
|
|
419
|
+
// Update playlist runtime (RetroArch compatible)
|
|
420
|
+
if (emulatorResult.sessionSeconds !== undefined) {
|
|
421
|
+
const playlistDir = getPlaylistsDirectory(freshConfig);
|
|
422
|
+
updatePlaylistRuntime(result.path, playlistDir, emulatorResult.sessionSeconds);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
void main();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Button } from '.';
|
|
2
|
+
|
|
3
|
+
// Default keyboard mappings
|
|
4
|
+
// Layout designed for comfortable two-handed play:
|
|
5
|
+
// Left hand: WASD (D-pad), Q/E (L/R shoulders), Space (Select)
|
|
6
|
+
// Right hand: IJKL cluster (Y/X/A/B face buttons), Enter (Start)
|
|
7
|
+
export const DEFAULT_KEY_MAP: Record<string, Button> = {
|
|
8
|
+
// WASD for D-Pad
|
|
9
|
+
w: Button.Up,
|
|
10
|
+
s: Button.Down,
|
|
11
|
+
a: Button.Left,
|
|
12
|
+
d: Button.Right,
|
|
13
|
+
W: Button.Up,
|
|
14
|
+
S: Button.Down,
|
|
15
|
+
A: Button.Left,
|
|
16
|
+
D: Button.Right,
|
|
17
|
+
|
|
18
|
+
// Arrow keys (escape sequences from terminal)
|
|
19
|
+
'\u001b[A': Button.Up,
|
|
20
|
+
'\u001b[B': Button.Down,
|
|
21
|
+
'\u001b[D': Button.Left,
|
|
22
|
+
'\u001b[C': Button.Right,
|
|
23
|
+
|
|
24
|
+
// Face buttons - IJKL cluster (matches SNES diamond layout)
|
|
25
|
+
// I = top (X), J = left (Y), K = bottom (B), L = right (A)
|
|
26
|
+
i: Button.X,
|
|
27
|
+
I: Button.X,
|
|
28
|
+
j: Button.Y,
|
|
29
|
+
J: Button.Y,
|
|
30
|
+
k: Button.B,
|
|
31
|
+
K: Button.B,
|
|
32
|
+
l: Button.A,
|
|
33
|
+
L: Button.A,
|
|
34
|
+
|
|
35
|
+
// Alternative face buttons (Z/X for B/A like NES emulators)
|
|
36
|
+
z: Button.B,
|
|
37
|
+
Z: Button.B,
|
|
38
|
+
x: Button.A,
|
|
39
|
+
X: Button.A,
|
|
40
|
+
|
|
41
|
+
// Shoulder buttons
|
|
42
|
+
q: Button.L,
|
|
43
|
+
Q: Button.L,
|
|
44
|
+
e: Button.R,
|
|
45
|
+
E: Button.R,
|
|
46
|
+
|
|
47
|
+
// Start/Select
|
|
48
|
+
'\r': Button.Start, // Enter key
|
|
49
|
+
' ': Button.Select, // Space key
|
|
50
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTROLLER_BUTTON_COUNT,
|
|
3
|
+
NES_BUTTON_COUNT,
|
|
4
|
+
CONTROLLER_SHIFT_REGISTER_HIGH_BIT,
|
|
5
|
+
} from '..';
|
|
6
|
+
|
|
7
|
+
export * from './consts';
|
|
8
|
+
|
|
9
|
+
export enum Button {
|
|
10
|
+
// NES buttons (0-7)
|
|
11
|
+
A = 0,
|
|
12
|
+
B = 1,
|
|
13
|
+
Select = 2,
|
|
14
|
+
Start = 3,
|
|
15
|
+
Up = 4,
|
|
16
|
+
Down = 5,
|
|
17
|
+
Left = 6,
|
|
18
|
+
Right = 7,
|
|
19
|
+
// SNES additional buttons (8-11)
|
|
20
|
+
X = 8,
|
|
21
|
+
Y = 9,
|
|
22
|
+
L = 10,
|
|
23
|
+
R = 11,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class Controller {
|
|
27
|
+
private buttons: boolean[] = new Array<boolean>(CONTROLLER_BUTTON_COUNT).fill(false);
|
|
28
|
+
private shiftRegister: number = 0;
|
|
29
|
+
private strobe: boolean = false;
|
|
30
|
+
|
|
31
|
+
setButton(button: Button, pressed: boolean): void {
|
|
32
|
+
this.buttons[button] = pressed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getButton(button: Button): boolean {
|
|
36
|
+
return this.buttons[button];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Called when writing to $4016
|
|
40
|
+
write(data: number): void {
|
|
41
|
+
this.strobe = (data & 1) !== 0;
|
|
42
|
+
if (this.strobe) {
|
|
43
|
+
this.reload();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Called when reading from $4016 or $4017
|
|
48
|
+
read(): number {
|
|
49
|
+
if (this.strobe) {
|
|
50
|
+
return this.buttons[Button.A] ? 1 : 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const value = (this.shiftRegister & 1);
|
|
54
|
+
this.shiftRegister >>= 1;
|
|
55
|
+
this.shiftRegister |= CONTROLLER_SHIFT_REGISTER_HIGH_BIT; // Fill with 1s after all bits read
|
|
56
|
+
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private reload(): void {
|
|
61
|
+
this.shiftRegister = 0;
|
|
62
|
+
for (let i = 0; i < NES_BUTTON_COUNT; i++) {
|
|
63
|
+
if (this.buttons[i]) {
|
|
64
|
+
this.shiftRegister |= (1 << i);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get string representation of pressed buttons for display
|
|
70
|
+
getPressedButtons(): string {
|
|
71
|
+
const buttonNames = ['A', 'B', 'Sel', 'Sta', '\u2191', '\u2193', '\u2190', '\u2192', 'X', 'Y', 'L', 'R'];
|
|
72
|
+
const pressed: string[] = [];
|
|
73
|
+
for (let i = 0; i < CONTROLLER_BUTTON_COUNT; i++) {
|
|
74
|
+
if (this.buttons[i]) {
|
|
75
|
+
pressed.push(buttonNames[i]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return pressed.length > 0 ? pressed.join(' ') : '-';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StandardButton } from '../../core/button';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* All StandardButton values we need to track
|
|
5
|
+
*/
|
|
6
|
+
export const ALL_STANDARD_BUTTONS: StandardButton[] = [
|
|
7
|
+
StandardButton.A,
|
|
8
|
+
StandardButton.B,
|
|
9
|
+
StandardButton.X,
|
|
10
|
+
StandardButton.Y,
|
|
11
|
+
StandardButton.L,
|
|
12
|
+
StandardButton.R,
|
|
13
|
+
StandardButton.L2,
|
|
14
|
+
StandardButton.R2,
|
|
15
|
+
StandardButton.Start,
|
|
16
|
+
StandardButton.Select,
|
|
17
|
+
StandardButton.Up,
|
|
18
|
+
StandardButton.Down,
|
|
19
|
+
StandardButton.Left,
|
|
20
|
+
StandardButton.Right,
|
|
21
|
+
StandardButton.Guide,
|
|
22
|
+
];
|