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,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Libretro core loader
|
|
3
|
+
*
|
|
4
|
+
* Discovers libretro cores from directories and registers them with
|
|
5
|
+
* the core registry, making them available for auto-detection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
import { platform, homedir } from "os";
|
|
11
|
+
import { loadConfig } from "@/frontend/config";
|
|
12
|
+
import { expandPath } from "@/utils/paths";
|
|
13
|
+
import { logger } from "@/utils/logger";
|
|
14
|
+
import { getErrorMessage } from "@/utils/getErrorMessage";
|
|
15
|
+
import type { SystemInfo } from "@/core/core";
|
|
16
|
+
import { registerCore, type CoreCreateOptions } from "@/frontend/coreRegistry";
|
|
17
|
+
import { getCoresDirectory } from "@/frontend/config";
|
|
18
|
+
import { LibretroCore } from "..";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get possible RetroArch installation base directories for the current platform.
|
|
22
|
+
* These are the root directories where RetroArch stores its config and cores.
|
|
23
|
+
*
|
|
24
|
+
* Structure within each base directory:
|
|
25
|
+
* - Config: <base>/retroarch.cfg or <base>/config/retroarch.cfg
|
|
26
|
+
* - Cores: <base>/cores/
|
|
27
|
+
*/
|
|
28
|
+
const getRetroArchBasePaths = (): string[] => {
|
|
29
|
+
const paths: string[] = [];
|
|
30
|
+
|
|
31
|
+
switch (platform()) {
|
|
32
|
+
case "darwin":
|
|
33
|
+
// macOS: User Application Support directory
|
|
34
|
+
paths.push(join(homedir(), "Library", "Application Support", "RetroArch"));
|
|
35
|
+
break;
|
|
36
|
+
|
|
37
|
+
case "win32":
|
|
38
|
+
// Windows: Multiple possible install locations
|
|
39
|
+
// Portable/manual install locations
|
|
40
|
+
paths.push("C:\\RetroArch");
|
|
41
|
+
paths.push("C:\\RetroArch-Win64");
|
|
42
|
+
// Program Files locations
|
|
43
|
+
paths.push(
|
|
44
|
+
join(process.env.PROGRAMFILES || "C:\\Program Files", "RetroArch")
|
|
45
|
+
);
|
|
46
|
+
paths.push(
|
|
47
|
+
join(
|
|
48
|
+
process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)",
|
|
49
|
+
"RetroArch"
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
// User AppData location
|
|
53
|
+
paths.push(join(homedir(), "AppData", "Roaming", "RetroArch"));
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
// Linux: XDG config directory
|
|
58
|
+
paths.push(join(homedir(), ".config", "retroarch"));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return paths;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get possible locations for retroarch.cfg based on platform.
|
|
67
|
+
* These are READ-ONLY lookups to find the libretro_directory setting.
|
|
68
|
+
*
|
|
69
|
+
* Checks both <base>/retroarch.cfg and <base>/config/retroarch.cfg
|
|
70
|
+
* since different versions/platforms use different structures.
|
|
71
|
+
*/
|
|
72
|
+
const getRetroArchConfigPaths = (): string[] => {
|
|
73
|
+
const paths: string[] = [];
|
|
74
|
+
const basePaths = getRetroArchBasePaths();
|
|
75
|
+
|
|
76
|
+
for (const base of basePaths) {
|
|
77
|
+
// Some versions put config in a subdirectory
|
|
78
|
+
paths.push(join(base, "config", "retroarch.cfg"));
|
|
79
|
+
// Others put it directly in the base
|
|
80
|
+
paths.push(join(base, "retroarch.cfg"));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Platform-specific fallback locations not in a RetroArch directory
|
|
84
|
+
switch (platform()) {
|
|
85
|
+
case "win32":
|
|
86
|
+
// Legacy Windows fallback: config directly in AppData
|
|
87
|
+
if (process.env.APPDATA) {
|
|
88
|
+
paths.push(join(process.env.APPDATA, "retroarch.cfg"));
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
default:
|
|
93
|
+
// Linux: System-wide config fallback
|
|
94
|
+
paths.push("/etc/retroarch.cfg");
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return paths;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse a RetroArch config file and extract the libretro_directory value
|
|
103
|
+
* This is READ-ONLY - we never write to this file
|
|
104
|
+
*
|
|
105
|
+
* @param configPath Path to retroarch.cfg
|
|
106
|
+
* @returns The libretro_directory path if found, null otherwise
|
|
107
|
+
*/
|
|
108
|
+
const parseRetroArchConfig = (configPath: string): string | null => {
|
|
109
|
+
try {
|
|
110
|
+
const content = readFileSync(configPath, "utf-8");
|
|
111
|
+
|
|
112
|
+
// RetroArch config format: key = "value" or key = value
|
|
113
|
+
// We're looking for: libretro_directory = "/path/to/cores"
|
|
114
|
+
for (const line of content.split("\n")) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
|
|
117
|
+
// Skip comments and empty lines
|
|
118
|
+
if (trimmed.startsWith("#") || trimmed === "") {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Match libretro_directory = "value" or libretro_directory = value
|
|
123
|
+
const match = trimmed.match(/^libretro_directory\s*=\s*"?([^"]*)"?$/);
|
|
124
|
+
if (match && match[1]) {
|
|
125
|
+
const value = match[1].trim();
|
|
126
|
+
// Skip placeholder values
|
|
127
|
+
if (value && value !== "default" && value !== "~/.config/retroarch/cores") {
|
|
128
|
+
return expandPath(value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Can't read config file, skip
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the libretro_directory from RetroArch config if available
|
|
141
|
+
* This is READ-ONLY - we only read the config to find core paths
|
|
142
|
+
*
|
|
143
|
+
* @returns The libretro_directory path if found in any config, null otherwise
|
|
144
|
+
*/
|
|
145
|
+
export const getRetroArchLibretroDirectory = (): string | null => {
|
|
146
|
+
const configPaths = getRetroArchConfigPaths();
|
|
147
|
+
|
|
148
|
+
for (const configPath of configPaths) {
|
|
149
|
+
if (existsSync(configPath)) {
|
|
150
|
+
const directory = parseRetroArchConfig(configPath);
|
|
151
|
+
if (directory && existsSync(directory)) {
|
|
152
|
+
return directory;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get the platform-specific library extension
|
|
162
|
+
*/
|
|
163
|
+
const getLibraryExtension = (): string => {
|
|
164
|
+
switch (platform()) {
|
|
165
|
+
case "darwin":
|
|
166
|
+
return ".dylib";
|
|
167
|
+
case "win32":
|
|
168
|
+
return ".dll";
|
|
169
|
+
default:
|
|
170
|
+
return ".so";
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get paths to search for libretro cores (excluding RetroArch-specific paths)
|
|
176
|
+
*
|
|
177
|
+
* Search order:
|
|
178
|
+
* 1. Current directory ./cores (for development)
|
|
179
|
+
* 2. Platform-specific emoemu cores directory (matches RetroArch convention)
|
|
180
|
+
* - macOS: ~/Library/Application Support/emoemu/cores
|
|
181
|
+
* - Linux: ~/.config/emoemu/cores
|
|
182
|
+
* - Windows: %APPDATA%\emoemu\cores
|
|
183
|
+
* 3. System library paths (Homebrew on macOS, /usr/lib on Linux)
|
|
184
|
+
*/
|
|
185
|
+
export const getDefaultCorePaths = (): string[] => {
|
|
186
|
+
const paths: string[] = [];
|
|
187
|
+
|
|
188
|
+
// Current directory cores folder (for development/portable use)
|
|
189
|
+
paths.push("./cores");
|
|
190
|
+
|
|
191
|
+
// Platform-specific emoemu cores directory (primary location)
|
|
192
|
+
// This follows the same convention as RetroArch:
|
|
193
|
+
// RetroArch: ~/Library/Application Support/RetroArch/cores
|
|
194
|
+
// emoemu: ~/Library/Application Support/emoemu/cores
|
|
195
|
+
paths.push(getCoresDirectory());
|
|
196
|
+
|
|
197
|
+
// Platform-specific system library paths (not part of RetroArch installs)
|
|
198
|
+
switch (platform()) {
|
|
199
|
+
case "darwin":
|
|
200
|
+
// Homebrew paths
|
|
201
|
+
paths.push("/usr/local/lib/libretro");
|
|
202
|
+
paths.push("/opt/homebrew/lib/libretro");
|
|
203
|
+
break;
|
|
204
|
+
case "win32":
|
|
205
|
+
// No additional system paths on Windows
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
// Linux system library paths
|
|
209
|
+
paths.push("/usr/lib/libretro");
|
|
210
|
+
paths.push("/usr/local/lib/libretro");
|
|
211
|
+
paths.push("/lib/x86_64-linux-gnu/libretro");
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return paths;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get RetroArch-specific paths to search for libretro cores.
|
|
220
|
+
* Includes both standard RetroArch installation directories and
|
|
221
|
+
* custom libretro_directory from retroarch.cfg (READ-ONLY).
|
|
222
|
+
*/
|
|
223
|
+
export const getRetroArchCorePaths = (): string[] => {
|
|
224
|
+
const paths: string[] = [];
|
|
225
|
+
|
|
226
|
+
// Check RetroArch config for custom libretro_directory (READ-ONLY)
|
|
227
|
+
const retroArchDir = getRetroArchLibretroDirectory();
|
|
228
|
+
if (retroArchDir) {
|
|
229
|
+
paths.push(retroArchDir);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// RetroArch installation directories (cores subfolder)
|
|
233
|
+
for (const base of getRetroArchBasePaths()) {
|
|
234
|
+
paths.push(join(base, "cores"));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return paths;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Track loaded core paths to avoid duplicates
|
|
241
|
+
const loadedCorePaths = new Set<string>();
|
|
242
|
+
|
|
243
|
+
// Track registered core IDs to handle name collisions
|
|
244
|
+
const registeredCoreIds = new Set<string>();
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Load libretro cores from a directory and register them
|
|
248
|
+
*
|
|
249
|
+
* @param coreDirectory Path to directory containing libretro cores
|
|
250
|
+
* @param verbose If true, log loading status to console (legacy)
|
|
251
|
+
*/
|
|
252
|
+
export const loadLibretroCores = (coreDirectory: string, verbose = false): void => {
|
|
253
|
+
if (!existsSync(coreDirectory)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const ext = getLibraryExtension();
|
|
258
|
+
const suffix = `_libretro${ext}`;
|
|
259
|
+
|
|
260
|
+
let files: string[];
|
|
261
|
+
try {
|
|
262
|
+
files = readdirSync(coreDirectory).filter((f) => f.endsWith(suffix));
|
|
263
|
+
} catch {
|
|
264
|
+
// Can't read directory, skip
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (files.length > 0) {
|
|
269
|
+
logger.debug(`Scanning cores directory: ${coreDirectory}`, 'Core');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
const corePath = join(coreDirectory, file);
|
|
274
|
+
|
|
275
|
+
// Skip if already loaded
|
|
276
|
+
if (loadedCorePaths.has(corePath)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Note: mupen64plus requires a stub OpenGL library on macOS. See TRD for details.
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Create a temporary instance to get system info
|
|
284
|
+
const tempCore = new LibretroCore(corePath);
|
|
285
|
+
const info = tempCore.getSystemInfo();
|
|
286
|
+
tempCore.destroy();
|
|
287
|
+
|
|
288
|
+
// Generate unique ID if there's a collision
|
|
289
|
+
let coreId = info.id;
|
|
290
|
+
let counter = 2;
|
|
291
|
+
while (registeredCoreIds.has(coreId)) {
|
|
292
|
+
coreId = `${info.id}-${counter}`;
|
|
293
|
+
counter++;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Cache the system info to avoid recreating the core
|
|
297
|
+
const cachedInfo: SystemInfo = { ...info, id: coreId };
|
|
298
|
+
|
|
299
|
+
// Register the core factory
|
|
300
|
+
registerCore(coreId, {
|
|
301
|
+
create: (options?: CoreCreateOptions) => new LibretroCore(corePath, options),
|
|
302
|
+
extensions: info.extensions,
|
|
303
|
+
getSystemInfo: () => cachedInfo,
|
|
304
|
+
path: corePath,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
loadedCorePaths.add(corePath);
|
|
308
|
+
registeredCoreIds.add(coreId);
|
|
309
|
+
|
|
310
|
+
// Log core loading (RetroArch-style)
|
|
311
|
+
logger.info(`Loaded core: ${info.name} (${info.extensions.join(', ')})`, 'Core');
|
|
312
|
+
|
|
313
|
+
if (verbose) {
|
|
314
|
+
console.log(
|
|
315
|
+
`Loaded libretro core: ${info.name} (${coreId}) - ${info.extensions.join(", ")}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const errorMsg = getErrorMessage(error);
|
|
320
|
+
logger.warn(`Failed to load core ${file}: ${errorMsg}`, 'Core');
|
|
321
|
+
|
|
322
|
+
if (verbose) {
|
|
323
|
+
console.warn(
|
|
324
|
+
`Failed to load libretro core ${file}:`,
|
|
325
|
+
getErrorMessage(error)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
// Continue with other cores
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Load libretro cores from all default paths (excludes RetroArch paths)
|
|
335
|
+
*
|
|
336
|
+
* @param verbose If true, log loading status
|
|
337
|
+
*/
|
|
338
|
+
export const loadDefaultLibretroCores = (verbose = false): void => {
|
|
339
|
+
const paths = getDefaultCorePaths();
|
|
340
|
+
for (const path of paths) {
|
|
341
|
+
loadLibretroCores(path, verbose);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Load libretro cores from RetroArch installation directories.
|
|
347
|
+
* This includes standard RetroArch paths and the libretro_directory
|
|
348
|
+
* from retroarch.cfg (READ-ONLY config access).
|
|
349
|
+
*
|
|
350
|
+
* @param verbose If true, log loading status
|
|
351
|
+
*/
|
|
352
|
+
export const loadRetroArchCores = (verbose = false): void => {
|
|
353
|
+
const paths = getRetroArchCorePaths();
|
|
354
|
+
for (const path of paths) {
|
|
355
|
+
loadLibretroCores(path, verbose);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Load libretro cores from a custom directory specified in config.
|
|
361
|
+
* This respects the `libretro_directory` setting which is RetroArch-compatible.
|
|
362
|
+
*
|
|
363
|
+
* @param libretroDirectory Path to the custom cores directory
|
|
364
|
+
* @param verbose If true, log loading status
|
|
365
|
+
*/
|
|
366
|
+
export const loadCoresFromConfig = (libretroDirectory: string, verbose = false): void => {
|
|
367
|
+
if (libretroDirectory && libretroDirectory.trim() !== "") {
|
|
368
|
+
const expandedPath = expandPath(libretroDirectory);
|
|
369
|
+
if (existsSync(expandedPath)) {
|
|
370
|
+
loadLibretroCores(expandedPath, verbose);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Get the count of loaded libretro cores
|
|
377
|
+
*/
|
|
378
|
+
export const getLoadedLibretroCoreCount = (): number => loadedCorePaths.size;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Check if a specific core path has been loaded
|
|
382
|
+
*/
|
|
383
|
+
export const isLibretroCoreLoaded = (corePath: string): boolean => loadedCorePaths.has(corePath);
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Unload a libretro core by path, removing it from tracking sets.
|
|
387
|
+
* The core should already be unregistered from the registry before calling this.
|
|
388
|
+
*
|
|
389
|
+
* @param corePath Path to the core file
|
|
390
|
+
* @param coreId The registered core ID (e.g., "mgba")
|
|
391
|
+
* @returns true if the core was tracked and removed, false if not found
|
|
392
|
+
*/
|
|
393
|
+
export const unloadLibretroCore = (corePath: string, coreId: string): boolean => {
|
|
394
|
+
const hadPath = loadedCorePaths.delete(corePath);
|
|
395
|
+
const hadId = registeredCoreIds.delete(coreId);
|
|
396
|
+
return hadPath || hadId;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Dynamically register a single libretro core by path.
|
|
401
|
+
* Use this to register cores that were downloaded while the app is running.
|
|
402
|
+
*
|
|
403
|
+
* @param corePath Absolute path to the core file
|
|
404
|
+
* @param verbose If true, log loading status to console (legacy)
|
|
405
|
+
* @returns The registered core ID, or null if registration failed
|
|
406
|
+
*/
|
|
407
|
+
export const registerLibretroCore = (corePath: string, verbose = false): string | null => {
|
|
408
|
+
// Skip if already loaded
|
|
409
|
+
if (loadedCorePaths.has(corePath)) {
|
|
410
|
+
logger.debug(`Core already loaded: ${corePath}`, 'Core');
|
|
411
|
+
if (verbose) {
|
|
412
|
+
console.log(`Core already loaded: ${corePath}`);
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
// Create a temporary instance to get system info
|
|
419
|
+
const tempCore = new LibretroCore(corePath);
|
|
420
|
+
const info = tempCore.getSystemInfo();
|
|
421
|
+
tempCore.destroy();
|
|
422
|
+
|
|
423
|
+
// Generate unique ID if there's a collision
|
|
424
|
+
let coreId = info.id;
|
|
425
|
+
let counter = 2;
|
|
426
|
+
while (registeredCoreIds.has(coreId)) {
|
|
427
|
+
coreId = `${info.id}-${counter}`;
|
|
428
|
+
counter++;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Cache the system info to avoid recreating the core
|
|
432
|
+
const cachedInfo: SystemInfo = { ...info, id: coreId };
|
|
433
|
+
|
|
434
|
+
// Register the core factory
|
|
435
|
+
registerCore(coreId, {
|
|
436
|
+
create: (options?: CoreCreateOptions) => new LibretroCore(corePath, options),
|
|
437
|
+
extensions: info.extensions,
|
|
438
|
+
getSystemInfo: () => cachedInfo,
|
|
439
|
+
path: corePath,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
loadedCorePaths.add(corePath);
|
|
443
|
+
registeredCoreIds.add(coreId);
|
|
444
|
+
|
|
445
|
+
// Log core registration (RetroArch-style)
|
|
446
|
+
logger.info(`Registered core: ${info.name} (${info.extensions.join(', ')})`, 'Core');
|
|
447
|
+
|
|
448
|
+
if (verbose) {
|
|
449
|
+
console.log(
|
|
450
|
+
`Registered libretro core: ${info.name} (${coreId}) - ${info.extensions.join(", ")}`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return coreId;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
const errorMsg = getErrorMessage(error);
|
|
457
|
+
logger.warn(`Failed to register core ${corePath}: ${errorMsg}`, 'Core');
|
|
458
|
+
|
|
459
|
+
if (verbose) {
|
|
460
|
+
console.warn(
|
|
461
|
+
`Failed to register libretro core ${corePath}:`,
|
|
462
|
+
getErrorMessage(error)
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Check if a core path is in a user-managed cores directory.
|
|
471
|
+
* This includes the default cores directory and any configured libretro_directory.
|
|
472
|
+
* Uses path resolution to handle relative paths correctly.
|
|
473
|
+
*
|
|
474
|
+
* @param corePath The core's stored path
|
|
475
|
+
* @returns true if the core is in a user-managed directory
|
|
476
|
+
*/
|
|
477
|
+
export const isInUserCoresDirectory = (corePath: string): boolean => {
|
|
478
|
+
const resolvedCorePath = resolve(corePath);
|
|
479
|
+
|
|
480
|
+
// Check default cores directory
|
|
481
|
+
const defaultCoresDir = resolve(getCoresDirectory());
|
|
482
|
+
if (resolvedCorePath.startsWith(defaultCoresDir)) {
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check configured libretro_directory if set
|
|
487
|
+
const { config } = loadConfig();
|
|
488
|
+
if (config.libretro_directory && config.libretro_directory.trim() !== "") {
|
|
489
|
+
const resolvedConfiguredDir = resolve(expandPath(config.libretro_directory));
|
|
490
|
+
if (resolvedCorePath.startsWith(resolvedConfiguredDir)) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return false;
|
|
496
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Pixel Format Byte Sizes
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
/** Number of bytes per XRGB8888 pixel */
|
|
6
|
+
export const XRGB8888_BYTES_PER_PIXEL = 4;
|
|
7
|
+
|
|
8
|
+
/** Number of bytes per 16-bit pixel (RGB565, XRGB1555) */
|
|
9
|
+
export const RGB16_BYTES_PER_PIXEL = 2;
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// RGB565 Bit Manipulation Constants
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/** Mask for 5-bit color channel (R and B in RGB565/XRGB1555) */
|
|
16
|
+
export const MASK_5BIT = 0x1f;
|
|
17
|
+
|
|
18
|
+
/** Mask for 6-bit color channel (G in RGB565) */
|
|
19
|
+
export const MASK_6BIT = 0x3f;
|
|
20
|
+
|
|
21
|
+
/** Bit shift for red channel in RGB565 (bits 11-15) */
|
|
22
|
+
export const RGB565_RED_SHIFT = 11;
|
|
23
|
+
|
|
24
|
+
/** Bit shift for green channel in RGB565 (bits 5-10) */
|
|
25
|
+
export const RGB565_GREEN_SHIFT = 5;
|
|
26
|
+
|
|
27
|
+
/** Bit shift for red channel in XRGB1555 (bits 10-14) */
|
|
28
|
+
export const XRGB1555_RED_SHIFT = 10;
|
|
29
|
+
|
|
30
|
+
/** Bit shift for green channel in XRGB1555 (bits 5-9) */
|
|
31
|
+
export const XRGB1555_GREEN_SHIFT = 5;
|
|
32
|
+
|
|
33
|
+
/** Bit shift to scale 5-bit to 8-bit (left shift) */
|
|
34
|
+
export const SCALE_5BIT_TO_8BIT_SHIFT = 3;
|
|
35
|
+
|
|
36
|
+
/** Bit shift to scale 6-bit to 8-bit (left shift) */
|
|
37
|
+
export const SCALE_6BIT_TO_8BIT_SHIFT = 2;
|
|
38
|
+
|
|
39
|
+
/** Bit shift to replicate 5-bit upper bits into lower bits */
|
|
40
|
+
export const REPLICATE_5BIT_SHIFT = 2;
|
|
41
|
+
|
|
42
|
+
/** Bit shift to replicate 6-bit upper bits into lower bits */
|
|
43
|
+
export const REPLICATE_6BIT_SHIFT = 4;
|