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,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
|
+
}
|