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.
Files changed (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. 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;