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,155 @@
1
+ import { readFileSync, writeFileSync, existsSync, utimesSync, unlinkSync } from 'fs';
2
+ import { basename, dirname, extname, join } from 'path';
3
+ import type { Core } from '../../core/core';
4
+ import type { Config } from '../../frontend/config';
5
+ import { getSavestatesDirectory, getSavefilesDirectory } from '../../frontend/config';
6
+ import { logger } from '../../utils/logger';
7
+ import { getErrorMessage } from '../../utils/getErrorMessage';
8
+ import { ensureDirectory } from '../../utils/ensureDirectory';
9
+
10
+ /**
11
+ * Get the directory for save state files.
12
+ * Uses ROM directory if savestates_in_content_dir is true (default),
13
+ * otherwise uses configured savestate_directory or platform default.
14
+ */
15
+ export const getSavestateDirectory = (config: Config | null, romPath: string): string => {
16
+ if (!config || config.savestates_in_content_dir !== false) {
17
+ return dirname(romPath);
18
+ }
19
+ return getSavestatesDirectory(config);
20
+ };
21
+
22
+ /**
23
+ * Get the path for the save state file.
24
+ * Format: [rom basename without extension].state.auto
25
+ */
26
+ export const getStatePath = (config: Config | null, romPath: string): string => {
27
+ const dir = getSavestateDirectory(config, romPath);
28
+ const name = basename(romPath, extname(romPath));
29
+ return join(dir, `${name}.state.auto`);
30
+ };
31
+
32
+ /**
33
+ * Get the directory for battery save (.srm) files.
34
+ * Uses ROM directory if savefiles_in_content_dir is true (default),
35
+ * otherwise uses configured savefile_directory or platform default.
36
+ */
37
+ export const getSavefileDirectory = (config: Config | null, romPath: string): string => {
38
+ if (!config || config.savefiles_in_content_dir !== false) {
39
+ return dirname(romPath);
40
+ }
41
+ return getSavefilesDirectory(config);
42
+ };
43
+
44
+ /**
45
+ * Get the path for the battery save (.srm) file.
46
+ * Uses RetroArch-compatible naming: [rom basename without extension].srm
47
+ */
48
+ export const getSrmPath = (config: Config | null, romPath: string): string => {
49
+ const dir = getSavefileDirectory(config, romPath);
50
+ const name = basename(romPath, extname(romPath));
51
+ return join(dir, name + '.srm');
52
+ };
53
+
54
+ /**
55
+ * Load battery save from .srm file (RetroArch-compatible format).
56
+ * Raw binary SRAM data, no header.
57
+ */
58
+ export const loadBatterySave = (core: Core, config: Config | null, romPath: string): void => {
59
+ if (!core.hasBatterySave()) {
60
+ return;
61
+ }
62
+
63
+ const srmPath = getSrmPath(config, romPath);
64
+ if (!existsSync(srmPath)) {
65
+ return;
66
+ }
67
+
68
+ try {
69
+ const data = readFileSync(srmPath);
70
+ core.setBatteryRam(new Uint8Array(data));
71
+ } catch (err) {
72
+ logger.warn(`Failed to load battery save: ${srmPath} - ${getErrorMessage(err)}`, 'SaveFile');
73
+ }
74
+ };
75
+
76
+ /**
77
+ * Save battery RAM to .srm file (RetroArch-compatible format).
78
+ * Raw binary SRAM data, no header.
79
+ */
80
+ export const saveBatterySave = (core: Core, config: Config | null, romPath: string): void => {
81
+ if (!core.hasBatterySave()) {
82
+ return;
83
+ }
84
+
85
+ const batteryRam = core.getBatteryRam();
86
+ if (!batteryRam) {
87
+ return;
88
+ }
89
+
90
+ const srmPath = getSrmPath(config, romPath);
91
+ try {
92
+ ensureDirectory(dirname(srmPath));
93
+ writeFileSync(srmPath, Buffer.from(batteryRam));
94
+ // Force update mtime even if content is identical
95
+ const now = new Date();
96
+ utimesSync(srmPath, now, now);
97
+ } catch (err) {
98
+ logger.error(`Failed to save battery save: ${srmPath} - ${getErrorMessage(err)}`, 'SaveFile');
99
+ }
100
+ };
101
+
102
+ /** Check if a save state exists for the given ROM. */
103
+ export const hasSavedState = (config: Config | null, romPath: string): boolean => {
104
+ return existsSync(getStatePath(config, romPath));
105
+ };
106
+
107
+ /**
108
+ * Save the current state to a .state.auto file.
109
+ * For libretro cores: raw binary (RetroArch-compatible)
110
+ */
111
+ export const saveState = (core: Core, config: Config | null, romPath: string): void => {
112
+ const statePath = getStatePath(config, romPath);
113
+ try {
114
+ const state = core.getState();
115
+ if (!state) {
116
+ return; // Core doesn't support save states
117
+ }
118
+
119
+ ensureDirectory(dirname(statePath));
120
+ writeFileSync(statePath, state);
121
+ } catch (err) {
122
+ logger.error(`Failed to save state: ${statePath} - ${getErrorMessage(err)}`, 'SaveState');
123
+ }
124
+ };
125
+
126
+ /**
127
+ * Load state from a save state file.
128
+ * @returns true if state was loaded successfully
129
+ */
130
+ export const loadStateFromFile = (core: Core, statePath: string): boolean => {
131
+ if (!existsSync(statePath)) {
132
+ return false;
133
+ }
134
+
135
+ try {
136
+ const fileData = readFileSync(statePath);
137
+ core.setState(fileData);
138
+ return true;
139
+ } catch (err) {
140
+ logger.error(`Failed to load state: ${statePath} - ${getErrorMessage(err)}`, 'SaveState');
141
+ return false;
142
+ }
143
+ };
144
+
145
+ /** Delete the save state file for the given ROM. */
146
+ export const deleteSavedState = (config: Config | null, romPath: string): void => {
147
+ const statePath = getStatePath(config, romPath);
148
+ if (existsSync(statePath)) {
149
+ try {
150
+ unlinkSync(statePath);
151
+ } catch (err) {
152
+ logger.warn(`Failed to delete save state: ${statePath} - ${getErrorMessage(err)}`, 'SaveState');
153
+ }
154
+ }
155
+ };
@@ -0,0 +1,160 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { basename, dirname, extname, join } from 'path';
3
+ import sharp from 'sharp';
4
+ import { VERSION_WITH_DATE as VERSION } from '../../consts';
5
+ import { isRgb15Buffer, type Core, type SystemInfo } from '../../core/core';
6
+ import type { Config } from '../../frontend/config';
7
+ import { getRomTitle } from '../../frontend/romScanner';
8
+ import { getSystemName } from '../../frontend/playlist';
9
+ import { notifyScreenshotSaved } from '../../frontend/notifications';
10
+ import { getThumbnailPath } from '../../utils/paths';
11
+ import { logger } from '../../utils/logger';
12
+ import { getErrorMessage } from '../../utils/getErrorMessage';
13
+ import { rgb15ToRgb24 } from '../../utils/color';
14
+ import { ensureDirectory } from '../../utils/ensureDirectory';
15
+ import { RGB24_BYTES_PER_PIXEL } from '../../rendering';
16
+ import { TWO_DIGIT_YEAR_SLICE_START, ISO_DATETIME_LENGTH } from '../../frontend';
17
+
18
+ /**
19
+ * Get the screenshot directory, using config setting or ROM directory as fallback.
20
+ */
21
+ export const getScreenshotDirectory = (config: Config | null, romPath: string): string => {
22
+ if (config?.screenshot_directory && config.screenshot_directory.length > 0) {
23
+ return config.screenshot_directory;
24
+ }
25
+ return dirname(romPath);
26
+ };
27
+
28
+ /**
29
+ * Generate screenshot filename using RetroArch naming convention.
30
+ * Format: GameName-YYMMDD-HHMMSS.png
31
+ */
32
+ export const generateScreenshotFilename = (romPath: string): string => {
33
+ const romName = basename(romPath, extname(romPath));
34
+ const now = new Date();
35
+ const year = String(now.getFullYear()).slice(TWO_DIGIT_YEAR_SLICE_START);
36
+ const month = String(now.getMonth() + 1).padStart(2, '0');
37
+ const day = String(now.getDate()).padStart(2, '0');
38
+ const hours = String(now.getHours()).padStart(2, '0');
39
+ const minutes = String(now.getMinutes()).padStart(2, '0');
40
+ const seconds = String(now.getSeconds()).padStart(2, '0');
41
+ return `${romName}-${year}${month}${day}-${hours}${minutes}${seconds}.png`;
42
+ };
43
+
44
+ /**
45
+ * Capture a PNG screenshot asynchronously.
46
+ * @returns Promise resolving to PNG buffer
47
+ */
48
+ export const captureScreenshotAsync = async (
49
+ core: Core,
50
+ systemInfo: SystemInfo,
51
+ romPath: string
52
+ ): Promise<Buffer | null> => {
53
+ try {
54
+ const frameBuffer = core.getFramebuffer();
55
+ const { width, height, colorSpace } = systemInfo;
56
+ const pixelCount = width * height;
57
+
58
+ // Convert framebuffer to RGB24
59
+ const rgb = new Uint8Array(pixelCount * RGB24_BYTES_PER_PIXEL);
60
+
61
+ if (isRgb15Buffer(colorSpace, frameBuffer)) {
62
+ // RGB15 (xBBBBBGGGGGRRRRR) - convert to RGB24
63
+ for (let i = 0; i < pixelCount; i++) {
64
+ const [r, g, b] = rgb15ToRgb24(frameBuffer[i]);
65
+ rgb[i * RGB24_BYTES_PER_PIXEL] = r;
66
+ rgb[i * RGB24_BYTES_PER_PIXEL + 1] = g;
67
+ rgb[i * RGB24_BYTES_PER_PIXEL + 2] = b;
68
+ }
69
+ } else {
70
+ // RGB24 - copy directly
71
+ rgb.set(frameBuffer);
72
+ }
73
+
74
+ // Encode to 256-color indexed PNG for smaller file size
75
+ // Add EXIF metadata with emoemu version, game title, core name, and timestamp
76
+ // DateTimeOriginal is stored in UTC with OffsetTimeOriginal indicating +00:00
77
+ const now = new Date();
78
+ const utcDateTime = now.toISOString().replace('T', ' ').slice(0, ISO_DATETIME_LENGTH).replace(/-/g, ':');
79
+
80
+ // Get ROM title from embedded metadata, fallback to filename without extension
81
+ const romTitle = getRomTitle(romPath) ?? basename(romPath, extname(romPath));
82
+ const imageDescription = `${romTitle} (${systemInfo.name})`;
83
+
84
+ return await sharp(Buffer.from(rgb.buffer), {
85
+ raw: { width, height, channels: 3 },
86
+ })
87
+ .withExif({
88
+ IFD0: {
89
+ Software: `emoemu ${VERSION}`,
90
+ ImageDescription: imageDescription,
91
+ },
92
+ IFD2: {
93
+ DateTimeOriginal: utcDateTime,
94
+ OffsetTimeOriginal: '+00:00',
95
+ },
96
+ })
97
+ .png({ palette: true, compressionLevel: 9, effort: 10 })
98
+ .toBuffer();
99
+ } catch (err) {
100
+ logger.error(`Failed to capture screenshot: ${getErrorMessage(err)}`, 'Screenshot');
101
+ return null;
102
+ }
103
+ };
104
+
105
+ /**
106
+ * Take a screenshot and save to file.
107
+ * Uses RetroArch naming convention: GameName-YYMMDD-HHMMSS.png
108
+ */
109
+ export const takeScreenshot = (
110
+ core: Core,
111
+ systemInfo: SystemInfo,
112
+ romPath: string,
113
+ config: Config | null
114
+ ): void => {
115
+ void captureScreenshotAsync(core, systemInfo, romPath).then((pngBuffer) => {
116
+ if (!pngBuffer) {
117
+ return;
118
+ }
119
+
120
+ const screenshotDir = getScreenshotDirectory(config, romPath);
121
+ const filename = generateScreenshotFilename(romPath);
122
+ const filepath = join(screenshotDir, filename);
123
+
124
+ try {
125
+ ensureDirectory(screenshotDir);
126
+ writeFileSync(filepath, pngBuffer);
127
+ notifyScreenshotSaved(filename);
128
+ } catch (err) {
129
+ logger.error(`Failed to save screenshot: ${filepath} - ${getErrorMessage(err)}`, 'Screenshot');
130
+ }
131
+ });
132
+ };
133
+
134
+ /**
135
+ * Save a screenshot as a RetroArch-compatible thumbnail.
136
+ * Currently saves as 'snap' type (in-game screenshot) in Named_Snaps directory.
137
+ */
138
+ export const saveThumbnailScreenshot = async (
139
+ core: Core,
140
+ systemInfo: SystemInfo,
141
+ romPath: string
142
+ ): Promise<void> => {
143
+ const romExt = extname(romPath);
144
+ const systemName = getSystemName(romExt, systemInfo.id);
145
+ const romTitle = getRomTitle(romPath) ?? basename(romPath, extname(romPath));
146
+ const thumbnailPath = getThumbnailPath(systemName, romTitle, 'snap');
147
+ const thumbnailDir = dirname(thumbnailPath);
148
+
149
+ try {
150
+ const screenshot = await captureScreenshotAsync(core, systemInfo, romPath);
151
+ if (!screenshot) {
152
+ return;
153
+ }
154
+
155
+ ensureDirectory(thumbnailDir);
156
+ writeFileSync(thumbnailPath, screenshot);
157
+ } catch {
158
+ // Silently ignore thumbnail errors - don't disrupt save state operation
159
+ }
160
+ };
@@ -0,0 +1,79 @@
1
+ import { logger } from '../../utils/logger';
2
+ import { getTerminalDimensions } from '../../utils/terminal';
3
+ import { DEFAULT_SOURCE_WIDTH, DEFAULT_SOURCE_HEIGHT } from '../../rendering';
4
+ import { ASPECT_RATIO_DECIMALS } from '../../frontend';
5
+
6
+ /**
7
+ * Calculate optimal dimensions for terminal/ASCII/emoji rendering.
8
+ * sourceWidth/sourceHeight: core framebuffer dimensions
9
+ * pixelAspectRatio: PAR for the core (e.g., 8/7 for NES, 1.0 for GBC)
10
+ */
11
+ export const calculateTerminalDimensions = (
12
+ mode: 'terminal' | 'ascii' | 'emoji',
13
+ sourceWidth: number = DEFAULT_SOURCE_WIDTH,
14
+ sourceHeight: number = DEFAULT_SOURCE_HEIGHT,
15
+ pixelAspectRatio: number = 1.0
16
+ ): { width: number; height: number } => {
17
+ const { width: termCols, height: termRows } = getTerminalDimensions();
18
+
19
+ // Leave 2 rows for status line
20
+ const availableRows = termRows - 2;
21
+
22
+ // Calculate display aspect ratio from source dimensions and PAR
23
+ // displayAspect = (sourceWidth * PAR) / sourceHeight
24
+ const displayAspect = (sourceWidth * pixelAspectRatio) / sourceHeight;
25
+
26
+ // Terminal cells are roughly 1:2 (width:height), so we multiply by 2 below
27
+ // to compensate when calculating character columns from pixel dimensions
28
+
29
+ if (mode === 'emoji') {
30
+ // Emoji: 1 emoji = 1 pixel, each emoji is 2 terminal columns wide
31
+ // Emojis appear roughly square (2 cols × 1 row ≈ square due to cell aspect)
32
+ // width / height = displayAspect
33
+ let height = availableRows;
34
+ let width = Math.floor(height * displayAspect);
35
+ const displayCols = width * 2; // Actual terminal columns needed
36
+
37
+ if (displayCols > termCols) {
38
+ width = Math.floor(termCols / 2);
39
+ height = Math.floor(width / displayAspect);
40
+ }
41
+
42
+ return { width, height };
43
+ } else if (mode === 'ascii') {
44
+ // ASCII: 1 char = 1 pixel
45
+ // To maintain display aspect, account for cell aspect ratio
46
+ // displayAspect = (cols * cellWidth) / (rows * cellHeight)
47
+ // displayAspect = cols / (rows * 2) => cols = rows * 2 * displayAspect
48
+ let height = availableRows;
49
+ let width = Math.floor(height * 2 * displayAspect);
50
+
51
+ if (width > termCols) {
52
+ width = termCols;
53
+ height = Math.floor(width / (2 * displayAspect));
54
+ }
55
+
56
+ return { width, height };
57
+ } else {
58
+ // Terminal half-block mode: 1 char = 1x2 pixels
59
+ // Each half-block character covers 2 vertical pixels
60
+ // displayAspect = (cols * cellWidth) / ((rows * 2) * cellHeight)
61
+ // With cellAspect = 0.5: displayAspect = cols / (rows * 4)
62
+ // But half-blocks double vertical resolution: cols = rows * 2 * displayAspect
63
+ let height = availableRows;
64
+ let width = Math.floor(height * 2 * displayAspect);
65
+
66
+ if (width > termCols) {
67
+ width = termCols;
68
+ height = Math.floor(width / (2 * displayAspect));
69
+ }
70
+
71
+ logger.debug(
72
+ `Terminal dims: ${width}x${height} (term: ${termCols}x${termRows}, ` +
73
+ `source: ${sourceWidth}x${sourceHeight}, aspect: ${displayAspect.toFixed(ASPECT_RATIO_DECIMALS)})`,
74
+ 'Render'
75
+ );
76
+
77
+ return { width, height };
78
+ }
79
+ };
@@ -0,0 +1,83 @@
1
+ import type { CoreFactory } from '../frontend/coreRegistry';
2
+ import type { Config } from '../frontend/config';
3
+ import type { SettingsManager } from '../frontend/SettingsManager';
4
+
5
+ // Re-export RenderMode from SettingsManager for external consumers
6
+ export type { RenderMode } from '../frontend/SettingsManager';
7
+
8
+ // Post-processing mode type
9
+ export type PostProcessingMode = 'off' | 'custom' | 'crt';
10
+
11
+ // Effect values structure
12
+ export interface EffectValues {
13
+ gamma: number;
14
+ scanlines: number;
15
+ saturation: number;
16
+ brightness: number;
17
+ contrast: number;
18
+ vignette: number;
19
+ bloom: number;
20
+ bloomThreshold: number;
21
+ ntsc: number;
22
+ curvature: number;
23
+ chromaticAberration: number;
24
+ }
25
+
26
+ // Common renderer interface
27
+ export interface Renderer {
28
+ renderRgb15(frameBuffer: Uint16Array): string; // For RGB15 cores (GBC, SNES)
29
+ renderRgb24(frameBuffer: Uint8Array): string; // For RGB24 cores (libretro)
30
+ clearScreen(): string;
31
+ hideCursor(): string;
32
+ showCursor(): string;
33
+ getStatusRow(): number;
34
+ moveCursorToRow(row: number): string;
35
+ setDimensions?(width: number, height: number): void;
36
+ destroy?(): void; // Cleanup resources (native window, etc.)
37
+ isWindowBased?: boolean; // True for window-based renderers (native)
38
+ shouldClose?(): boolean; // Check if window close was requested (native)
39
+ }
40
+
41
+ export interface EmulatorOptions {
42
+ romPath: string;
43
+ coreFactory: CoreFactory; // Core factory for creating the emulator core
44
+ width?: number;
45
+ height?: number;
46
+ colorEnabled?: boolean;
47
+ renderMode?: import('../frontend/SettingsManager').RenderMode;
48
+ scale?: number; // For Kitty renderer
49
+ enableGamepad?: boolean; // Enable gamepad/controller support
50
+ enableAudio?: boolean; // Enable audio output (default: true)
51
+ startMuted?: boolean; // Start with audio muted (default: false)
52
+ enableSaveState?: boolean; // Enable save state loading/saving (default: true)
53
+ enableBatterySave?: boolean; // Enable battery save loading/saving (default: true)
54
+ showStatusBar?: boolean; // Show status bar (default: true)
55
+ fpsLimit?: number; // Override FPS limit (0 = uncapped, undefined = core native)
56
+ enableDiffRendering?: boolean; // Enable diff-based rendering optimization (default: true)
57
+ noRender?: boolean; // Disable video rendering output (for debugging, default: false)
58
+ frameLimit?: number; // Limit rendering to N fps (0=off/unlimited, default: 0)
59
+ pngCompressionLevel?: number; // PNG compression level 1-9 for Kitty mode (default: 1)
60
+ gamma?: number; // Gamma correction for Kitty mode (default: 1.0, CRT-like: 1.1-1.4)
61
+ scanlines?: number; // Scanline intensity for Kitty mode (default: 0.0 = disabled, 0.2-0.4 = subtle)
62
+ saturation?: number; // Color saturation for Kitty mode (default: 1.0, CRT-like: 1.1-1.3)
63
+ brightness?: number; // Brightness multiplier for Kitty mode (default: 1.0)
64
+ contrast?: number; // Contrast multiplier for Kitty mode (default: 1.0)
65
+ vignette?: number; // Vignette intensity for Kitty mode (default: 0.0 = disabled)
66
+ bloom?: number; // Bloom/glow intensity for Kitty mode (default: 0.0 = disabled)
67
+ bloomThreshold?: number; // Brightness threshold for bloom (default: 0.6)
68
+ ntsc?: number; // NTSC artifact intensity for Kitty mode (default: 0.0 = disabled)
69
+ curvature?: number; // CRT curvature for Kitty mode (default: 0.0 = disabled)
70
+ chromaticAberration?: number; // Chromatic aberration for Kitty mode (default: 0.0 = disabled)
71
+ hasUserEffects?: boolean; // Whether user explicitly specified post-processing effects (default: false)
72
+ config?: Config; // Current config for saving preference changes
73
+ configPath?: string; // Path to config file for saving
74
+ settingsManager?: SettingsManager; // Centralized settings manager (if provided, handles settings sync)
75
+ // Netplay options
76
+ netplayHost?: boolean; // Start as netplay host/server
77
+ netplayConnect?: string; // Connect to netplay server (hostname or hostname:port)
78
+ netplayPort?: number; // Netplay port (default: 55435)
79
+ netplayPassword?: string; // Netplay password
80
+ netplaySpectate?: boolean; // Join as spectator
81
+ netplayNickname?: string; // Player nickname
82
+ netplayInputDelay?: number; // Input delay frames (0-16, default: 0)
83
+ }
@@ -0,0 +1,10 @@
1
+ /** Width to clear for progress line updates */
2
+ export const LINE_CLEAR_WIDTH = 60;
3
+ /** Percentage multiplier */
4
+ export const PERCENT_MULTIPLIER = 100;
5
+ /** Bytes per kilobyte */
6
+ export const BYTES_PER_KB = 1024;
7
+ /** Space reserved for percentage suffix like " (XX%)" */
8
+ export const PERCENT_SUFFIX_WIDTH = 10;
9
+ /** Length of ellipsis "..." */
10
+ export const ELLIPSIS_LENGTH = 3;