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,1095 @@
1
+ /**
2
+ * Environment callback handler for libretro cores
3
+ * Handles environment commands that cores use to query frontend capabilities
4
+ */
5
+
6
+ import koffi from "koffi";
7
+ import {
8
+ RETRO_ENVIRONMENT,
9
+ RETRO_PIXEL_FORMAT,
10
+ RETRO_LANGUAGE,
11
+ RETRO_MEMDESC,
12
+ RETRO_MESSAGE_TARGET,
13
+ RETRO_LOG,
14
+ type RetroMessageExt,
15
+ HEX_RADIX,
16
+ } from "..";
17
+ import { retro_log_printf_t, type KoffiCallback } from "../api";
18
+ import { logger } from "@/utils/logger";
19
+
20
+ // Environment-specific constants
21
+ import {
22
+ MAX_INPUT_USERS,
23
+ CORE_OPTIONS_VERSION,
24
+ MESSAGE_INTERFACE_VERSION,
25
+ AUDIO_ENABLE_BIT,
26
+ VIDEO_ENABLE_BIT,
27
+ MEMORY_MAP_HEADER_SIZE,
28
+ MEMORY_MAP_NUM_DESC_OFFSET,
29
+ MEMORY_DESCRIPTOR_SIZE,
30
+ MEMORY_DESC_LEN_OFFSET,
31
+ MEMORY_DESC_LEN_HIGH_OFFSET,
32
+ MEMORY_DESC_PTR_OFFSET,
33
+ UINT32_MULTIPLIER,
34
+ MAX_DESCRIPTORS_TO_SCAN,
35
+ POINTER_SIZE_64BIT,
36
+ MESSAGE_STRUCT_SIZE,
37
+ MESSAGE_FRAMES_OFFSET,
38
+ MESSAGE_EXT_STRUCT_SIZE,
39
+ MESSAGE_EXT_DURATION_OFFSET,
40
+ MESSAGE_EXT_PRIORITY_OFFSET,
41
+ MESSAGE_EXT_LEVEL_OFFSET,
42
+ MESSAGE_EXT_TARGET_OFFSET,
43
+ MESSAGE_EXT_TYPE_OFFSET,
44
+ MESSAGE_EXT_PROGRESS_OFFSET,
45
+ UINT32_SIZE,
46
+ GEOMETRY_UINT_COUNT,
47
+ STRUCT_PADDING_4,
48
+ MAX_CONTROLLER_TYPES,
49
+ ASPECT_RATIO_DECIMALS,
50
+ } from "./consts";
51
+
52
+ export * from './consts';
53
+
54
+ // Debug logging toggle
55
+ const DEBUG_ENV: boolean = false;
56
+
57
+ // Type alias for data pointers from koffi callbacks
58
+
59
+ type DataPointer = any;
60
+
61
+ /**
62
+ * Read a uint32 from a koffi data pointer
63
+ */
64
+ const readUInt32 = (ptr: DataPointer): number => koffi.decode(ptr, "unsigned int") as number;
65
+
66
+ /**
67
+ * Read a uint8 from a koffi data pointer
68
+ */
69
+ const readUInt8 = (ptr: DataPointer): number => koffi.decode(ptr, "uint8_t") as number;
70
+
71
+ /**
72
+ * Write a uint32 to a koffi data pointer (little-endian)
73
+ */
74
+ const writeUInt32LE = (ptr: DataPointer, value: number): void => {
75
+ koffi.encode(ptr, "uint32_t", value);
76
+ };
77
+
78
+ /**
79
+ * Write a uint8 to a koffi data pointer
80
+ */
81
+ const writeUInt8 = (ptr: DataPointer, value: number): void => {
82
+ koffi.encode(ptr, "uint8_t", value);
83
+ };
84
+
85
+ /**
86
+ * EnvironmentHandler processes environment callbacks from libretro cores
87
+ */
88
+ // Memory map SRAM region info
89
+ interface MemoryMapSram {
90
+ ptr: unknown; // External pointer from koffi
91
+ size: number;
92
+ }
93
+
94
+ // Core option definition (from SET_VARIABLES or SET_CORE_OPTIONS)
95
+ interface CoreOptionDef {
96
+ key: string;
97
+ description: string;
98
+ values: string[];
99
+ defaultValue: string;
100
+ }
101
+
102
+ // Message callback type
103
+ export type MessageCallback = (message: RetroMessageExt) => void;
104
+
105
+ export class EnvironmentHandler {
106
+ private pixelFormat: number = RETRO_PIXEL_FORMAT.XRGB1555;
107
+ private systemDirectory = "./system";
108
+ private saveDirectory = "./saves";
109
+ private supportsNoGame = false;
110
+
111
+ // Audio/video enable flags (both enabled by default)
112
+ private audioEnabled = true;
113
+ private videoEnabled = true;
114
+
115
+ // Message callback for core notifications
116
+ private messageCallback: MessageCallback | null = null;
117
+
118
+ // Memory map SRAM (for cores that use SET_MEMORY_MAPS instead of retro_get_memory_data)
119
+ private memoryMapSram: MemoryMapSram | null = null;
120
+
121
+ // Debug info for memory maps
122
+ public memoryMapDebug: string = '';
123
+
124
+ // Track unhandled commands for debugging
125
+ public unhandledCommands: number[] = [];
126
+
127
+ // Geometry from SET_GEOMETRY (actual content dimensions)
128
+ private geometry: {
129
+ baseWidth: number;
130
+ baseHeight: number;
131
+ aspectRatio: number;
132
+ } | null = null;
133
+
134
+ // Keep references to allocated string buffers to prevent garbage collection.
135
+ // The native code holds pointers to these buffers, so they must stay alive.
136
+ private allocatedStrings: Buffer[] = [];
137
+
138
+ // Log callback - must keep reference to prevent GC
139
+ private logCallback: KoffiCallback | null = null;
140
+
141
+ // Recent log messages from core (circular buffer for diagnostics)
142
+ private recentLogs: Array<{ level: number; message: string }> = [];
143
+ private static readonly MAX_LOG_ENTRIES = 50;
144
+
145
+ // Core options: user-configured values (key -> value)
146
+ private coreOptions: Map<string, string> = new Map();
147
+
148
+ // Core option definitions: available options reported by the core
149
+ private coreOptionDefs: Map<string, CoreOptionDef> = new Map();
150
+
151
+ // Track if variables have been updated since last check
152
+ private variablesUpdated = false;
153
+
154
+ // Controller info per port: array of { desc, id } for each supported controller type
155
+ private controllerInfo: Array<Array<{ desc: string; id: number }>> = [];
156
+
157
+ /**
158
+ * Handle an environment callback from the core
159
+ * @param cmd The environment command
160
+ * @param data Pointer to command-specific data (may be null)
161
+ * @returns true if the command was handled, false otherwise
162
+ */
163
+ handle(cmd: number, data: DataPointer | null): boolean {
164
+ // Strip experimental flag if present
165
+ const actualCmd = cmd & ~RETRO_ENVIRONMENT.EXPERIMENTAL;
166
+
167
+ if (DEBUG_ENV) {
168
+ console.log(`[ENV] Command: ${actualCmd} (0x${actualCmd.toString(HEX_RADIX)})`);
169
+ }
170
+
171
+ switch (actualCmd) {
172
+ case RETRO_ENVIRONMENT.SET_PIXEL_FORMAT:
173
+ return this.handleSetPixelFormat(data);
174
+
175
+ case RETRO_ENVIRONMENT.GET_SYSTEM_DIRECTORY:
176
+ return this.handleGetDirectory(data, this.systemDirectory);
177
+
178
+ case RETRO_ENVIRONMENT.GET_SAVE_DIRECTORY:
179
+ return this.handleGetDirectory(data, this.saveDirectory);
180
+
181
+ case RETRO_ENVIRONMENT.GET_CORE_ASSETS_DIRECTORY:
182
+ return this.handleGetDirectory(data, this.systemDirectory);
183
+
184
+ case RETRO_ENVIRONMENT.GET_VARIABLE:
185
+ return this.handleGetVariable(data);
186
+
187
+ case RETRO_ENVIRONMENT.SET_VARIABLES:
188
+ return this.handleSetVariables(data);
189
+
190
+ case RETRO_ENVIRONMENT.GET_VARIABLE_UPDATE:
191
+ // Report if variables have been updated since last check
192
+ if (data) {
193
+ writeUInt8(data, this.variablesUpdated ? 1 : 0);
194
+ this.variablesUpdated = false;
195
+ }
196
+ return true;
197
+
198
+ case RETRO_ENVIRONMENT.SET_SUPPORT_NO_GAME:
199
+ if (data) {
200
+ this.supportsNoGame = readUInt8(data) !== 0;
201
+ }
202
+ return true;
203
+
204
+ case RETRO_ENVIRONMENT.GET_LOG_INTERFACE:
205
+ return this.handleGetLogInterface(data);
206
+
207
+ case RETRO_ENVIRONMENT.SET_INPUT_DESCRIPTORS:
208
+ // Accept input descriptors but we use our own mapping
209
+ return true;
210
+
211
+ case RETRO_ENVIRONMENT.GET_INPUT_BITMASKS:
212
+ // We support input bitmasks
213
+ if (data) {
214
+ writeUInt8(data, 1);
215
+ }
216
+ return true;
217
+
218
+ case RETRO_ENVIRONMENT.GET_CAN_DUPE:
219
+ // We can handle duplicate frames (null data in video callback)
220
+ if (data) {
221
+ writeUInt8(data, 1);
222
+ }
223
+ return true;
224
+
225
+ case RETRO_ENVIRONMENT.GET_LANGUAGE:
226
+ if (data) {
227
+ writeUInt32LE(data, RETRO_LANGUAGE.ENGLISH);
228
+ }
229
+ return true;
230
+
231
+ case RETRO_ENVIRONMENT.GET_CORE_OPTIONS_VERSION:
232
+ if (data) {
233
+ // Support up to v2 options API
234
+ writeUInt32LE(data, CORE_OPTIONS_VERSION);
235
+ }
236
+ return true;
237
+
238
+ case RETRO_ENVIRONMENT.SET_CORE_OPTIONS:
239
+ case RETRO_ENVIRONMENT.SET_CORE_OPTIONS_INTL:
240
+ case RETRO_ENVIRONMENT.SET_CORE_OPTIONS_V2:
241
+ case RETRO_ENVIRONMENT.SET_CORE_OPTIONS_V2_INTL:
242
+ // Accept core options but use defaults
243
+ return true;
244
+
245
+ case RETRO_ENVIRONMENT.SET_CORE_OPTIONS_DISPLAY:
246
+ // Accept display hints
247
+ return true;
248
+
249
+ case RETRO_ENVIRONMENT.SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK:
250
+ // Accept but don't use update callback
251
+ return true;
252
+
253
+ case RETRO_ENVIRONMENT.SET_GEOMETRY:
254
+ return this.handleSetGeometry(data);
255
+
256
+ case RETRO_ENVIRONMENT.SET_SYSTEM_AV_INFO:
257
+ // Accept AV info changes
258
+ return true;
259
+
260
+ case RETRO_ENVIRONMENT.GET_INPUT_MAX_USERS:
261
+ if (data) {
262
+ writeUInt32LE(data, MAX_INPUT_USERS);
263
+ }
264
+ return true;
265
+
266
+ case RETRO_ENVIRONMENT.SET_CONTROLLER_INFO:
267
+ return this.handleSetControllerInfo(data);
268
+
269
+ case RETRO_ENVIRONMENT.SET_MEMORY_MAPS:
270
+ return this.handleSetMemoryMaps(data);
271
+
272
+ case RETRO_ENVIRONMENT.SET_SUBSYSTEM_INFO:
273
+ // Accept subsystem info
274
+ return true;
275
+
276
+ case RETRO_ENVIRONMENT.GET_RUMBLE_INTERFACE:
277
+ // We don't support rumble
278
+ return false;
279
+
280
+ case RETRO_ENVIRONMENT.SET_HW_RENDER:
281
+ case RETRO_ENVIRONMENT.GET_HW_RENDER_INTERFACE:
282
+ case RETRO_ENVIRONMENT.SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE:
283
+ case RETRO_ENVIRONMENT.SET_HW_SHARED_CONTEXT:
284
+ case RETRO_ENVIRONMENT.GET_PREFERRED_HW_RENDER:
285
+ // We don't support hardware rendering (OpenGL/Vulkan)
286
+ return false;
287
+
288
+ case RETRO_ENVIRONMENT.GET_VFS_INTERFACE:
289
+ // We don't provide VFS interface
290
+ return false;
291
+
292
+ case RETRO_ENVIRONMENT.GET_LED_INTERFACE:
293
+ // No LED support
294
+ return false;
295
+
296
+ case RETRO_ENVIRONMENT.GET_AUDIO_VIDEO_ENABLE:
297
+ if (data) {
298
+ // Bit 0: enable video, Bit 1: enable audio
299
+ const mask = (this.videoEnabled ? VIDEO_ENABLE_BIT : 0) |
300
+ (this.audioEnabled ? AUDIO_ENABLE_BIT : 0);
301
+ writeUInt32LE(data, mask);
302
+ }
303
+ return true;
304
+
305
+ case RETRO_ENVIRONMENT.SET_AUDIO_BUFFER_STATUS_CALLBACK:
306
+ // Accept but don't use audio buffer status callback
307
+ return true;
308
+
309
+ case RETRO_ENVIRONMENT.SET_MINIMUM_AUDIO_LATENCY:
310
+ // Accept minimum audio latency setting
311
+ return true;
312
+
313
+ case RETRO_ENVIRONMENT.SET_MESSAGE:
314
+ return this.handleSetMessage(data);
315
+
316
+ case RETRO_ENVIRONMENT.SET_MESSAGE_EXT:
317
+ return this.handleSetMessageExt(data);
318
+
319
+ case RETRO_ENVIRONMENT.GET_MESSAGE_INTERFACE_VERSION:
320
+ if (data) {
321
+ writeUInt32LE(data, MESSAGE_INTERFACE_VERSION);
322
+ }
323
+ return true;
324
+
325
+ case RETRO_ENVIRONMENT.SET_PERFORMANCE_LEVEL:
326
+ // Accept performance level hints
327
+ return true;
328
+
329
+ case RETRO_ENVIRONMENT.SET_SUPPORT_ACHIEVEMENTS:
330
+ // We don't support achievements
331
+ return false;
332
+
333
+ case RETRO_ENVIRONMENT.SET_SERIALIZATION_QUIRKS:
334
+ // Accept serialization quirks info
335
+ return true;
336
+
337
+ case RETRO_ENVIRONMENT.GET_FASTFORWARDING:
338
+ if (data) {
339
+ writeUInt8(data, 0); // Not fast-forwarding
340
+ }
341
+ return true;
342
+
343
+ case RETRO_ENVIRONMENT.SET_FASTFORWARDING_OVERRIDE:
344
+ // Accept but ignore
345
+ return true;
346
+
347
+ case RETRO_ENVIRONMENT.GET_THROTTLE_STATE:
348
+ // We don't provide throttle state
349
+ return false;
350
+
351
+ case RETRO_ENVIRONMENT.GET_SAVESTATE_CONTEXT:
352
+ // We don't provide savestate context
353
+ return false;
354
+
355
+ default:
356
+ // Track unhandled commands for debugging
357
+ if (!this.unhandledCommands.includes(actualCmd)) {
358
+ this.unhandledCommands.push(actualCmd);
359
+ }
360
+ if (DEBUG_ENV) {
361
+ console.log(`[ENV] Unhandled command: ${actualCmd}`);
362
+ }
363
+ return false;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Handle SET_PIXEL_FORMAT command
369
+ */
370
+ private handleSetPixelFormat(data: DataPointer | null): boolean {
371
+ if (!data) {return false;}
372
+
373
+ const format = readUInt32(data);
374
+ if (
375
+ format === RETRO_PIXEL_FORMAT.XRGB1555 ||
376
+ format === RETRO_PIXEL_FORMAT.RGB565 ||
377
+ format === RETRO_PIXEL_FORMAT.XRGB8888
378
+ ) {
379
+ this.pixelFormat = format;
380
+ // Log pixel format (RetroArch-style)
381
+ const formatNames = ["XRGB1555", "XRGB8888", "RGB565"];
382
+ logger.info(`SET_PIXEL_FORMAT: ${formatNames[format]}`, 'Environ');
383
+ if (DEBUG_ENV) {
384
+ console.log(`[ENV] Pixel format set to: ${formatNames[format]}`);
385
+ }
386
+ return true;
387
+ }
388
+ return false;
389
+ }
390
+
391
+ /**
392
+ * Handle SET_GEOMETRY command - core is reporting actual content dimensions
393
+ */
394
+ private handleSetGeometry(data: DataPointer | null): boolean {
395
+ if (!data) {return false;}
396
+
397
+ // Read retro_game_geometry struct:
398
+ // base_width (uint), base_height (uint), max_width (uint), max_height (uint), aspect_ratio (float)
399
+ const FLOAT_SIZE = UINT32_SIZE;
400
+ const view = koffi.view(data, UINT32_SIZE * GEOMETRY_UINT_COUNT + FLOAT_SIZE);
401
+ const dataView = new DataView(view);
402
+
403
+ const baseWidth = dataView.getUint32(0, true);
404
+ const baseHeight = dataView.getUint32(UINT32_SIZE, true);
405
+ const aspectRatio = dataView.getFloat32(UINT32_SIZE * GEOMETRY_UINT_COUNT, true);
406
+
407
+ // Store geometry if valid (aspect_ratio of 0 means use base dimensions)
408
+ if (baseWidth > 0 && baseHeight > 0) {
409
+ const effectiveAspect = aspectRatio > 0 ? aspectRatio : baseWidth / baseHeight;
410
+ this.geometry = {
411
+ baseWidth,
412
+ baseHeight,
413
+ aspectRatio: effectiveAspect,
414
+ };
415
+ logger.info(
416
+ `SET_GEOMETRY: ${baseWidth}x${baseHeight}, aspect: ${effectiveAspect.toFixed(ASPECT_RATIO_DECIMALS)}`,
417
+ 'Environ'
418
+ );
419
+ }
420
+
421
+ return true;
422
+ }
423
+
424
+ /**
425
+ * Handle SET_CONTROLLER_INFO command
426
+ * Reports available controller types for each port
427
+ * struct retro_controller_info { const retro_controller_description *types; unsigned num_types; }
428
+ * struct retro_controller_description { const char *desc; unsigned id; }
429
+ */
430
+ private handleSetControllerInfo(data: DataPointer | null): boolean {
431
+ if (!data) {return true;}
432
+
433
+ try {
434
+ const POINTER_SIZE = POINTER_SIZE_64BIT;
435
+ const PORT_STRUCT_SIZE = POINTER_SIZE + UINT32_SIZE + STRUCT_PADDING_4; // pointer + uint + padding = 16 bytes
436
+ const DESC_STRUCT_SIZE = POINTER_SIZE + UINT32_SIZE + STRUCT_PADDING_4; // pointer + uint + padding = 16 bytes
437
+ const MAX_PORTS = 8; // Safety limit
438
+
439
+ this.controllerInfo = [];
440
+
441
+ // Read array of retro_controller_info until we hit one with 0 types
442
+ for (let port = 0; port < MAX_PORTS; port++) {
443
+ const portOffset = port * PORT_STRUCT_SIZE;
444
+ const portView = koffi.view(data, portOffset + PORT_STRUCT_SIZE) as ArrayBuffer;
445
+ const portData = new DataView(portView, portOffset, PORT_STRUCT_SIZE);
446
+
447
+ // Read num_types (at offset POINTER_SIZE)
448
+ const numTypes = portData.getUint32(POINTER_SIZE, true);
449
+
450
+ // 0 types means end of array
451
+ if (numTypes === 0) {break;}
452
+
453
+ // Read types pointer
454
+ const portBuf = Buffer.from(new Uint8Array(portView, portOffset, POINTER_SIZE));
455
+ const typesPtr = koffi.decode(portBuf, 'void*') as unknown;
456
+ if (!typesPtr) {break;}
457
+
458
+ const portTypes: Array<{ desc: string; id: number }> = [];
459
+
460
+ // Read each controller description
461
+ for (let t = 0; t < numTypes && t < MAX_CONTROLLER_TYPES; t++) {
462
+ const typeOffset = t * DESC_STRUCT_SIZE;
463
+ const typeView = koffi.view(typesPtr, typeOffset + DESC_STRUCT_SIZE) as ArrayBuffer;
464
+ const typeData = new DataView(typeView, typeOffset, DESC_STRUCT_SIZE);
465
+
466
+ // Read desc pointer and decode string
467
+ const descBuf = Buffer.from(new Uint8Array(typeView, typeOffset, POINTER_SIZE));
468
+ const descPtr = koffi.decode(descBuf, 'const char*') as string | null;
469
+
470
+ // Read id (at offset POINTER_SIZE)
471
+ const id = typeData.getUint32(POINTER_SIZE, true);
472
+
473
+ if (descPtr) {
474
+ portTypes.push({ desc: descPtr, id });
475
+ }
476
+ }
477
+
478
+ this.controllerInfo.push(portTypes);
479
+
480
+ // Log available controller types for this port
481
+ if (portTypes.length > 0) {
482
+ const typeStrs = portTypes.map(t => `${t.desc}(${t.id})`).join(', ');
483
+ logger.info(`Port ${port} controllers: ${typeStrs}`, 'Environ');
484
+ }
485
+ }
486
+ } catch (err) {
487
+ logger.debug(`SET_CONTROLLER_INFO error: ${err}`, 'Environ');
488
+ }
489
+
490
+ return true;
491
+ }
492
+
493
+ /**
494
+ * Get available controller types for a port
495
+ */
496
+ getControllerTypes(port: number): Array<{ desc: string; id: number }> {
497
+ return this.controllerInfo[port] ?? [];
498
+ }
499
+
500
+ /**
501
+ * Handle GET_*_DIRECTORY commands.
502
+ */
503
+ private handleGetDirectory(data: DataPointer | null, dir: string): boolean {
504
+ if (!data) {return false;}
505
+
506
+ // Allocate a null-terminated string buffer and keep reference to prevent GC
507
+ const strBuf = Buffer.from(dir + "\0", "utf8");
508
+ this.allocatedStrings.push(strBuf);
509
+
510
+ // Write the pointer to the string into data using koffi.encode
511
+ koffi.encode(data, "const char**", koffi.as(strBuf, "const char*"));
512
+
513
+ return true;
514
+ }
515
+
516
+ /**
517
+ * Handle GET_VARIABLE command
518
+ * struct retro_variable { const char *key; const char *value; }
519
+ * Core passes key, we fill in value pointer
520
+ */
521
+ private handleGetVariable(data: DataPointer | null): boolean {
522
+ if (!data) {return false;}
523
+
524
+ try {
525
+ // Read the key pointer (first field of retro_variable struct)
526
+ const key = koffi.decode(data, 'const char*') as string | null;
527
+ if (!key) {return false;}
528
+
529
+ // Look up the value: first check user-configured options, then defaults
530
+ let value = this.coreOptions.get(key);
531
+ if (value === undefined) {
532
+ // Fall back to default value from option definitions
533
+ const def = this.coreOptionDefs.get(key);
534
+ if (def) {
535
+ value = def.defaultValue;
536
+ }
537
+ }
538
+
539
+ if (value === undefined) {
540
+ // Unknown option, let core use its internal default
541
+ return false;
542
+ }
543
+
544
+ // Allocate a null-terminated string buffer for the value
545
+ const valueBuf = Buffer.from(value + "\0", "utf8");
546
+ this.allocatedStrings.push(valueBuf);
547
+
548
+ // Convert buffer to a koffi pointer type
549
+ const valuePtr = koffi.as(valueBuf, 'const char*');
550
+
551
+ // We need to write ONLY the value field without touching the key
552
+ // The struct layout is: { key: 'const char*' (8 bytes), value: 'const char*' (8 bytes) }
553
+ // So we read the original key pointer and write it back along with the new value
554
+ const POINTER_SIZE = 8; // 64-bit pointer
555
+ const structView = koffi.view(data, POINTER_SIZE * 2) as ArrayBuffer;
556
+
557
+ // Read the original key pointer (first 8 bytes)
558
+ const keyPtrBytes = new BigUint64Array(structView, 0, 1);
559
+ const originalKeyPtr = keyPtrBytes[0];
560
+
561
+ // Write both the key (unchanged) and value pointers back
562
+ // Use BigUint64Array to write both pointers
563
+ const fullView = new BigUint64Array(structView);
564
+
565
+ // Keep the original key pointer at offset 0
566
+ fullView[0] = originalKeyPtr;
567
+
568
+ // Write the value pointer at offset 1 (which is byte offset 8)
569
+ // Get the numeric address by encoding to native memory and reading back
570
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- koffi.alloc returns untyped native memory
571
+ const tempMem = koffi.alloc('const char*', 1);
572
+ koffi.encode(tempMem, 'const char*', valuePtr);
573
+ const tempView = koffi.view(tempMem, POINTER_SIZE);
574
+ const valuePtrValue = new BigUint64Array(tempView as ArrayBuffer)[0];
575
+ fullView[1] = valuePtrValue;
576
+ koffi.free(tempMem);
577
+
578
+ logger.debug(`GET_VARIABLE: ${key} = ${value}`, 'Environ');
579
+ return true;
580
+ } catch (err) {
581
+ logger.debug(`GET_VARIABLE error: ${err}`, 'Environ');
582
+ return false;
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Handle SET_VARIABLES command (legacy v0 options API)
588
+ * Array of retro_variable { const char *key; const char *value; } terminated by NULL key
589
+ * value format: "Description; value1|value2|value3" where first value is default
590
+ */
591
+ private handleSetVariables(data: DataPointer | null): boolean {
592
+ if (!data) {return false;}
593
+
594
+ try {
595
+ const POINTER_SIZE = 8; // 64-bit pointer
596
+ const STRUCT_SIZE = POINTER_SIZE * 2; // Two pointers per struct
597
+ const MAX_VARIABLES = 1000; // Safety limit
598
+ let offset = 0;
599
+
600
+ // Read variable array until we hit a NULL key
601
+ for (let i = 0; i < MAX_VARIABLES; i++) {
602
+ const structView = koffi.view(data, offset + STRUCT_SIZE) as ArrayBuffer;
603
+ const structBuf = Buffer.from(structView).subarray(offset);
604
+
605
+ // Read key pointer
606
+ const keyPtr = koffi.decode(koffi.as(structBuf, 'char**'), 'const char*') as string | null;
607
+ if (!keyPtr) {break;} // NULL key terminates the array
608
+
609
+ // Read value pointer (description + values)
610
+ const valueBuf = structBuf.subarray(POINTER_SIZE);
611
+ const valuePtr = koffi.decode(koffi.as(valueBuf, 'char**'), 'const char*') as string | null;
612
+
613
+ if (valuePtr) {
614
+ this.parseAndStoreOptionDef(keyPtr, valuePtr);
615
+ }
616
+
617
+ offset += STRUCT_SIZE;
618
+ }
619
+
620
+ return true;
621
+ } catch (err) {
622
+ logger.debug(`SET_VARIABLES error: ${err}`, 'Environ');
623
+ return true; // Accept even on parse error
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Parse option definition string and store it
629
+ * Format: "Description; value1|value2|value3"
630
+ */
631
+ private parseAndStoreOptionDef(key: string, valueStr: string): void {
632
+ // Split on "; " to separate description from values
633
+ const semicolonIndex = valueStr.indexOf('; ');
634
+ if (semicolonIndex === -1) {
635
+ // No values specified, just description
636
+ return;
637
+ }
638
+
639
+ const description = valueStr.substring(0, semicolonIndex);
640
+ const valuesStr = valueStr.substring(semicolonIndex + 2);
641
+ const values = valuesStr.split('|').map(v => v.trim());
642
+
643
+ if (values.length === 0) {return;}
644
+
645
+ const def: CoreOptionDef = {
646
+ key,
647
+ description,
648
+ values,
649
+ defaultValue: values[0], // First value is default
650
+ };
651
+
652
+ this.coreOptionDefs.set(key, def);
653
+ logger.debug(`Option defined: ${key} = [${values.join(', ')}] (default: ${def.defaultValue})`, 'Environ');
654
+ }
655
+
656
+ /**
657
+ * Handle GET_LOG_INTERFACE command
658
+ * struct retro_log_callback { retro_log_printf_t log; }
659
+ */
660
+ private handleGetLogInterface(data: DataPointer | null): boolean {
661
+ if (!data) {return false;}
662
+
663
+ try {
664
+ // Register the log callback if not already done
665
+ if (!this.logCallback) {
666
+ this.logCallback = koffi.register(
667
+ (level: number, fmt: string | null): void => {
668
+ this.handleLogMessage(level, fmt);
669
+ },
670
+ koffi.pointer(retro_log_printf_t)
671
+ );
672
+ }
673
+
674
+ // Write the callback pointer to the struct (single pointer field)
675
+ koffi.encode(data, koffi.pointer(retro_log_printf_t), this.logCallback);
676
+
677
+ return true;
678
+ } catch (err) {
679
+ if (DEBUG_ENV) {
680
+ console.error('[ENV] Failed to set up log interface:', err);
681
+ }
682
+ return false;
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Handle a log message from the core
688
+ * Note: variadic args not supported in koffi callbacks, so we only get level + format string.
689
+ * The format string often contains the full message or enough context for debugging.
690
+ */
691
+ private handleLogMessage(level: number, fmt: string | null): void {
692
+ if (!fmt) {return;}
693
+
694
+ // Clean up the message (remove trailing newlines)
695
+ const message = fmt.replace(/\n+$/, '');
696
+ if (!message) {return;}
697
+
698
+ // Store in circular buffer
699
+ this.recentLogs.push({ level, message });
700
+ if (this.recentLogs.length > EnvironmentHandler.MAX_LOG_ENTRIES) {
701
+ this.recentLogs.shift();
702
+ }
703
+
704
+ // Also log to our logger based on level
705
+ const levelName = this.getLogLevelName(level);
706
+ const logMsg = `[Core] ${message}`;
707
+
708
+ switch (level) {
709
+ case RETRO_LOG.DEBUG:
710
+ logger.debug(logMsg);
711
+ break;
712
+ case RETRO_LOG.WARN:
713
+ logger.warn(logMsg);
714
+ break;
715
+ case RETRO_LOG.ERROR:
716
+ logger.error(logMsg);
717
+ break;
718
+ default:
719
+ logger.info(logMsg);
720
+ }
721
+
722
+ if (DEBUG_ENV) {
723
+ console.log(`[Core ${levelName}] ${message}`);
724
+ }
725
+ }
726
+
727
+ /**
728
+ * Get human-readable log level name
729
+ */
730
+ private getLogLevelName(level: number): string {
731
+ switch (level) {
732
+ case RETRO_LOG.DEBUG: return 'DEBUG';
733
+ case RETRO_LOG.INFO: return 'INFO';
734
+ case RETRO_LOG.WARN: return 'WARN';
735
+ case RETRO_LOG.ERROR: return 'ERROR';
736
+ default: return `LEVEL${level}`;
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Handle SET_MESSAGE command (basic message)
742
+ * struct retro_message { const char *msg; unsigned frames; }
743
+ */
744
+ private handleSetMessage(data: DataPointer | null): boolean {
745
+ if (!data || !this.messageCallback) {return true;}
746
+
747
+ try {
748
+ // Read the pointer to the message string (first field, 8 bytes on 64-bit)
749
+ const msgPtr = koffi.decode(data, 'const char*') as string | null;
750
+ if (!msgPtr) {return true;}
751
+
752
+ // Read frames (unsigned int at MESSAGE_FRAMES_OFFSET)
753
+ const structView = koffi.view(data, MESSAGE_STRUCT_SIZE) as ArrayBuffer;
754
+ const structData = new DataView(structView);
755
+ const frames = structData.getUint32(MESSAGE_FRAMES_OFFSET, true);
756
+
757
+ // Convert frames to milliseconds (assume 60fps)
758
+ const FPS = 60;
759
+ const MS_PER_SECOND = 1000;
760
+ const durationMs = Math.round((frames / FPS) * MS_PER_SECOND);
761
+
762
+ // Create extended message format for unified handling
763
+ const message: RetroMessageExt = {
764
+ msg: msgPtr,
765
+ duration: durationMs,
766
+ priority: 0,
767
+ level: RETRO_LOG.INFO,
768
+ target: RETRO_MESSAGE_TARGET.ALL,
769
+ type: 0, // NOTIFICATION
770
+ progress: -1,
771
+ };
772
+
773
+ this.messageCallback(message);
774
+ } catch (err) {
775
+ if (DEBUG_ENV) {
776
+ console.log(`[ENV] SET_MESSAGE parse error: ${err}`);
777
+ }
778
+ }
779
+
780
+ return true;
781
+ }
782
+
783
+ /**
784
+ * Handle SET_MESSAGE_EXT command (extended message)
785
+ * struct retro_message_ext {
786
+ * const char *msg; // offset 0, 8 bytes (pointer)
787
+ * unsigned duration; // offset MESSAGE_EXT_DURATION_OFFSET, 4 bytes
788
+ * unsigned priority; // offset MESSAGE_EXT_PRIORITY_OFFSET, 4 bytes
789
+ * enum level; // offset MESSAGE_EXT_LEVEL_OFFSET, 4 bytes
790
+ * enum target; // offset MESSAGE_EXT_TARGET_OFFSET, 4 bytes
791
+ * enum type; // offset MESSAGE_EXT_TYPE_OFFSET, 4 bytes
792
+ * int8_t progress; // offset MESSAGE_EXT_PROGRESS_OFFSET, 1 byte
793
+ * }
794
+ */
795
+ private handleSetMessageExt(data: DataPointer | null): boolean {
796
+ if (!data || !this.messageCallback) {return true;}
797
+
798
+ try {
799
+ // Read the pointer to the message string
800
+ const msgPtr = koffi.decode(data, 'const char*') as string | null;
801
+ if (!msgPtr) {return true;}
802
+
803
+ // Read the rest of the struct
804
+ const structView = koffi.view(data, MESSAGE_EXT_STRUCT_SIZE) as ArrayBuffer;
805
+ const structData = new DataView(structView);
806
+
807
+ const message: RetroMessageExt = {
808
+ msg: msgPtr,
809
+ duration: structData.getUint32(MESSAGE_EXT_DURATION_OFFSET, true),
810
+ priority: structData.getUint32(MESSAGE_EXT_PRIORITY_OFFSET, true),
811
+ level: structData.getUint32(MESSAGE_EXT_LEVEL_OFFSET, true),
812
+ target: structData.getUint32(MESSAGE_EXT_TARGET_OFFSET, true),
813
+ type: structData.getUint32(MESSAGE_EXT_TYPE_OFFSET, true),
814
+ progress: structData.getInt8(MESSAGE_EXT_PROGRESS_OFFSET),
815
+ };
816
+
817
+ // Only dispatch if target includes OSD (not LOG-only)
818
+ if (message.target !== RETRO_MESSAGE_TARGET.LOG) {
819
+ this.messageCallback(message);
820
+ }
821
+ } catch (err) {
822
+ if (DEBUG_ENV) {
823
+ console.log(`[ENV] SET_MESSAGE_EXT parse error: ${err}`);
824
+ }
825
+ }
826
+
827
+ return true;
828
+ }
829
+
830
+ /**
831
+ * Handle SET_MEMORY_MAPS command
832
+ * Parses memory descriptors to find SRAM regions
833
+ */
834
+ private handleSetMemoryMaps(data: DataPointer | null): boolean {
835
+ if (!data) {return false;}
836
+
837
+ try {
838
+ // retro_memory_map struct: { descriptors: pointer, num_descriptors: uint32 }
839
+ // On 64-bit systems: pointer is 8 bytes, then 4 bytes for num_descriptors
840
+ const mapView = koffi.view(data, MEMORY_MAP_HEADER_SIZE) as ArrayBuffer;
841
+ const mapData = new DataView(mapView);
842
+
843
+ // Read pointer (64-bit) and num_descriptors (32-bit)
844
+ // Note: We need to get the actual pointer value, not read it as a number
845
+ const numDescriptors = mapData.getUint32(MEMORY_MAP_NUM_DESC_OFFSET, true); // little-endian
846
+
847
+ this.memoryMapDebug = `${numDescriptors}desc `;
848
+
849
+ if (numDescriptors === 0) {return true;}
850
+
851
+ // Get the descriptors pointer from the struct
852
+ const descriptorsPtr = koffi.decode(data, 'void*') as unknown;
853
+ if (!descriptorsPtr) {return true;}
854
+
855
+ // retro_memory_descriptor struct size (64-bit system):
856
+ // uint64_t flags (8) + void* ptr (8) + size_t offset (8) + size_t start (8) +
857
+ // size_t select (8) + size_t disconnect (8) + size_t len (8) + char* addrspace (8) = 64 bytes
858
+
859
+ const debugParts: string[] = [];
860
+ for (let i = 0; i < numDescriptors && i < MAX_DESCRIPTORS_TO_SCAN; i++) {
861
+ // Read each descriptor
862
+ const descView = koffi.view(descriptorsPtr, (i + 1) * MEMORY_DESCRIPTOR_SIZE) as ArrayBuffer;
863
+ const descData = new DataView(descView, i * MEMORY_DESCRIPTOR_SIZE, MEMORY_DESCRIPTOR_SIZE);
864
+
865
+ // Read flags (uint64_t at offset 0) - read as two 32-bit values
866
+ const flagsLow = descData.getUint32(0, true);
867
+
868
+ // Read len (size_t at offset MEMORY_DESC_LEN_OFFSET)
869
+ const lenLow = descData.getUint32(MEMORY_DESC_LEN_OFFSET, true);
870
+ const lenHigh = descData.getUint32(MEMORY_DESC_LEN_HIGH_OFFSET, true);
871
+ const len = lenLow + lenHigh * UINT32_MULTIPLIER;
872
+
873
+ debugParts.push(`${i}:f${flagsLow.toString(HEX_RADIX)}l${len}`);
874
+
875
+ // Check if this is SRAM (flag bit 3)
876
+ if ((flagsLow & RETRO_MEMDESC.SAVE_RAM) && len > 0) {
877
+ // Get the ptr field (void* at offset MEMORY_DESC_PTR_OFFSET)
878
+ // We need to read it as a pointer, not a number
879
+ const ptrOffset = i * MEMORY_DESCRIPTOR_SIZE + MEMORY_DESC_PTR_OFFSET;
880
+ const fullDescView = koffi.view(descriptorsPtr, (i + 1) * MEMORY_DESCRIPTOR_SIZE) as ArrayBuffer;
881
+ const ptrBytes = new Uint8Array(fullDescView, ptrOffset, POINTER_SIZE_64BIT);
882
+
883
+ // Create a buffer with the pointer bytes and decode it
884
+ const ptrBuf = Buffer.from(ptrBytes);
885
+ const sramPtr = koffi.decode(ptrBuf, 'void*') as unknown;
886
+
887
+ if (sramPtr) {
888
+ this.memoryMapSram = { ptr: sramPtr, size: len };
889
+ this.memoryMapDebug += `SRAM@${i}=${len}B`;
890
+ }
891
+ break; // Found SRAM, stop searching
892
+ }
893
+ }
894
+
895
+ if (!this.memoryMapSram) {
896
+ this.memoryMapDebug = debugParts.join(',');
897
+ }
898
+ } catch (err) {
899
+ this.memoryMapDebug = `ERR:${err}`;
900
+ }
901
+
902
+ return true;
903
+ }
904
+
905
+ /**
906
+ * Get the current pixel format
907
+ */
908
+ getPixelFormat(): number {
909
+ return this.pixelFormat;
910
+ }
911
+
912
+ /**
913
+ * Get geometry reported by SET_GEOMETRY (actual content dimensions)
914
+ * Returns null if core hasn't reported geometry changes
915
+ */
916
+ getGeometry(): { baseWidth: number; baseHeight: number; aspectRatio: number } | null {
917
+ return this.geometry;
918
+ }
919
+
920
+ /**
921
+ * Check if the core supports running without a game
922
+ */
923
+ getSupportsNoGame(): boolean {
924
+ return this.supportsNoGame;
925
+ }
926
+
927
+ /**
928
+ * Get the system directory path
929
+ */
930
+ getSystemDirectory(): string {
931
+ return this.systemDirectory;
932
+ }
933
+
934
+ /**
935
+ * Set the system directory path
936
+ */
937
+ setSystemDirectory(path: string): void {
938
+ this.systemDirectory = path;
939
+ }
940
+
941
+ /**
942
+ * Set the save directory path
943
+ */
944
+ setSaveDirectory(path: string): void {
945
+ this.saveDirectory = path;
946
+ }
947
+
948
+ /**
949
+ * Set audio/video enable flags
950
+ * These are reported to the core via GET_AUDIO_VIDEO_ENABLE
951
+ */
952
+ setAudioVideoEnabled(audio: boolean, video: boolean): void {
953
+ this.audioEnabled = audio;
954
+ this.videoEnabled = video;
955
+ }
956
+
957
+ /**
958
+ * Set audio enable flag
959
+ */
960
+ setAudioEnabled(enabled: boolean): void {
961
+ this.audioEnabled = enabled;
962
+ }
963
+
964
+ /**
965
+ * Set video enable flag
966
+ */
967
+ setVideoEnabled(enabled: boolean): void {
968
+ this.videoEnabled = enabled;
969
+ }
970
+
971
+ /**
972
+ * Set message callback for core notifications
973
+ */
974
+ setMessageCallback(callback: MessageCallback | null): void {
975
+ this.messageCallback = callback;
976
+ }
977
+
978
+ /**
979
+ * Get memory map SRAM info (for cores that use SET_MEMORY_MAPS)
980
+ */
981
+ getMemoryMapSram(): MemoryMapSram | null {
982
+ return this.memoryMapSram;
983
+ }
984
+
985
+ /**
986
+ * Get recent log messages from the core (for debugging ROM rejection, etc.)
987
+ * Returns messages with level and text, most recent last.
988
+ */
989
+ getRecentLogs(): Array<{ level: number; message: string }> {
990
+ return [...this.recentLogs];
991
+ }
992
+
993
+ /**
994
+ * Get recent log messages formatted as strings (for error messages)
995
+ * Filters to WARN and ERROR levels by default.
996
+ * Format: [LEVEL] [Core] message
997
+ */
998
+ getRecentLogsFormatted(minLevel: number = RETRO_LOG.WARN): string[] {
999
+ return this.recentLogs
1000
+ .filter(log => log.level >= minLevel)
1001
+ .map(log => `[${this.getLogLevelName(log.level)}] [Core] ${log.message}`);
1002
+ }
1003
+
1004
+ /**
1005
+ * Clear recent logs (call before loading a ROM to get fresh messages)
1006
+ */
1007
+ clearRecentLogs(): void {
1008
+ this.recentLogs = [];
1009
+ }
1010
+
1011
+ /**
1012
+ * Clear allocated buffers (call when done with the core)
1013
+ */
1014
+ cleanup(): void {
1015
+ this.allocatedStrings = [];
1016
+ this.memoryMapSram = null;
1017
+ this.logCallback = null;
1018
+ this.recentLogs = [];
1019
+ this.coreOptions.clear();
1020
+ this.coreOptionDefs.clear();
1021
+ this.controllerInfo = [];
1022
+ }
1023
+
1024
+ //==========================================================================
1025
+ // Core Options API
1026
+ //==========================================================================
1027
+
1028
+ /**
1029
+ * Set a core option value
1030
+ * Uses the same key format as RetroArch (e.g., "mupen64plus-rdp-plugin")
1031
+ */
1032
+ setCoreOption(key: string, value: string): void {
1033
+ this.coreOptions.set(key, value);
1034
+ this.variablesUpdated = true;
1035
+ logger.debug(`Core option set: ${key} = ${value}`, 'Environ');
1036
+ }
1037
+
1038
+ /**
1039
+ * Set multiple core options at once
1040
+ * @param options Record of key-value pairs (RetroArch format)
1041
+ */
1042
+ setCoreOptions(options: Record<string, string>): void {
1043
+ for (const [key, value] of Object.entries(options)) {
1044
+ this.coreOptions.set(key, value);
1045
+ }
1046
+ if (Object.keys(options).length > 0) {
1047
+ this.variablesUpdated = true;
1048
+ logger.debug(`Core options set: ${Object.keys(options).length} options`, 'Environ');
1049
+ }
1050
+ }
1051
+
1052
+ /**
1053
+ * Get the current value of a core option
1054
+ * Returns undefined if not set
1055
+ */
1056
+ getCoreOption(key: string): string | undefined {
1057
+ return this.coreOptions.get(key) ?? this.coreOptionDefs.get(key)?.defaultValue;
1058
+ }
1059
+
1060
+ /**
1061
+ * Get all configured core options
1062
+ */
1063
+ getCoreOptions(): Map<string, string> {
1064
+ return new Map(this.coreOptions);
1065
+ }
1066
+
1067
+ /**
1068
+ * Get all available core option definitions (reported by the core)
1069
+ */
1070
+ getCoreOptionDefs(): Map<string, CoreOptionDef> {
1071
+ return new Map(this.coreOptionDefs);
1072
+ }
1073
+
1074
+ /**
1075
+ * Get a specific option definition
1076
+ */
1077
+ getCoreOptionDef(key: string): CoreOptionDef | undefined {
1078
+ return this.coreOptionDefs.get(key);
1079
+ }
1080
+
1081
+ /**
1082
+ * Check if a core option exists (either configured or defined by core)
1083
+ */
1084
+ hasCoreOption(key: string): boolean {
1085
+ return this.coreOptions.has(key) || this.coreOptionDefs.has(key);
1086
+ }
1087
+
1088
+ /**
1089
+ * Clear all user-configured core options (revert to defaults)
1090
+ */
1091
+ clearCoreOptions(): void {
1092
+ this.coreOptions.clear();
1093
+ this.variablesUpdated = true;
1094
+ }
1095
+ }