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,58 @@
1
+ /** RGB15 format bit mask for 5-bit red channel (bits 0-4) */
2
+ export const RGB15_RED_MASK = 0x001f;
3
+
4
+ /** RGB15 format bit mask for 5-bit green channel (bits 5-9) */
5
+ export const RGB15_GREEN_MASK = 0x1f;
6
+
7
+ /** RGB15 format bit mask for 5-bit blue channel (bits 10-14) */
8
+ export const RGB15_BLUE_MASK = 0x1f;
9
+
10
+ /** Bit shift for green channel in RGB15 format */
11
+ export const RGB15_GREEN_SHIFT = 5;
12
+
13
+ /** Bit shift for blue channel in RGB15 format */
14
+ export const RGB15_BLUE_SHIFT = 10;
15
+
16
+ /** Bit shift for expanding 5-bit to 8-bit (left shift) */
17
+ export const RGB5_TO_8_LEFT_SHIFT = 3;
18
+
19
+ /** Bit shift for expanding 5-bit to 8-bit (right shift for replication) */
20
+ export const RGB5_TO_8_RIGHT_SHIFT = 2;
21
+
22
+ /** Maximum 8-bit color value */
23
+ export const MAX_8BIT = 255;
24
+
25
+ /** LUT size for 8-bit color operations (256 entries) */
26
+ export const LUT_SIZE_8BIT = 256;
27
+
28
+ /** Default gamma correction value (no change) */
29
+ export const DEFAULT_GAMMA = 1.0;
30
+
31
+ /** Weight factor for green in color distance (human eye sensitivity) */
32
+ export const GREEN_WEIGHT = 1.5;
33
+
34
+ /** Luminance threshold for grayscale emoji selection */
35
+ export const GRAYSCALE_THRESHOLD = 0.5;
36
+
37
+ /** ANSI 256-color palette: first index of the 6x6x6 color cube */
38
+ export const ANSI_COLOR_CUBE_START = 16;
39
+
40
+ /** ANSI 256-color palette: levels per channel in the 6x6x6 cube */
41
+ export const ANSI_COLOR_CUBE_LEVELS = 6;
42
+
43
+ /** Multiplier for red channel in ANSI 256-color cube index calculation */
44
+ export const ANSI_RED_MULTIPLIER = 36;
45
+
46
+ /** Multiplier for green channel in ANSI 256-color cube index calculation */
47
+ export const ANSI_GREEN_MULTIPLIER = 6;
48
+
49
+ // ITU-R BT.601 luminance coefficients
50
+
51
+ /** Red luminance coefficient for grayscale conversion */
52
+ export const LUMINANCE_R = 0.299;
53
+
54
+ /** Green luminance coefficient for grayscale conversion */
55
+ export const LUMINANCE_G = 0.587;
56
+
57
+ /** Blue luminance coefficient for grayscale conversion */
58
+ export const LUMINANCE_B = 0.114;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Shared color conversion utilities used across renderers.
3
+ */
4
+
5
+ export * from './consts';
6
+
7
+ import { firstBy } from 'remeda';
8
+ import {
9
+ RGB15_RED_MASK,
10
+ RGB15_GREEN_MASK,
11
+ RGB15_GREEN_SHIFT,
12
+ RGB15_BLUE_SHIFT,
13
+ RGB5_TO_8_LEFT_SHIFT,
14
+ RGB5_TO_8_RIGHT_SHIFT,
15
+ MAX_8BIT,
16
+ LUT_SIZE_8BIT,
17
+ DEFAULT_GAMMA,
18
+ GREEN_WEIGHT,
19
+ GRAYSCALE_THRESHOLD,
20
+ ANSI_COLOR_CUBE_START,
21
+ ANSI_COLOR_CUBE_LEVELS,
22
+ ANSI_RED_MULTIPLIER,
23
+ ANSI_GREEN_MULTIPLIER,
24
+ LUMINANCE_R,
25
+ LUMINANCE_G,
26
+ LUMINANCE_B,
27
+ } from './consts';
28
+
29
+ /**
30
+ * Extract RGB components from RGB15 format (XBBBBBGGGGGRRRRR).
31
+ * Returns 5-bit values (0-31).
32
+ */
33
+ export const extractRgb15Components = (color: number): [number, number, number] => {
34
+ const r5 = color & RGB15_RED_MASK;
35
+ const g5 = (color >> RGB15_GREEN_SHIFT) & RGB15_GREEN_MASK;
36
+ const b5 = (color >> RGB15_BLUE_SHIFT) & RGB15_GREEN_MASK;
37
+ return [r5, g5, b5];
38
+ };
39
+
40
+ /**
41
+ * Expand 5-bit color component to 8-bit.
42
+ * Uses bit replication for proper range mapping (0-31 -> 0-255).
43
+ */
44
+ export const expand5to8 = (value: number): number => (value << RGB5_TO_8_LEFT_SHIFT) | (value >> RGB5_TO_8_RIGHT_SHIFT);
45
+
46
+ /**
47
+ * Convert RGB15 color to RGB24.
48
+ * Returns 8-bit RGB components (0-255).
49
+ */
50
+ export const rgb15ToRgb24 = (color: number): [number, number, number] => {
51
+ const r5 = color & RGB15_RED_MASK;
52
+ const g5 = (color >> RGB15_GREEN_SHIFT) & RGB15_GREEN_MASK;
53
+ const b5 = (color >> RGB15_BLUE_SHIFT) & RGB15_GREEN_MASK;
54
+ return [
55
+ (r5 << RGB5_TO_8_LEFT_SHIFT) | (r5 >> RGB5_TO_8_RIGHT_SHIFT),
56
+ (g5 << RGB5_TO_8_LEFT_SHIFT) | (g5 >> RGB5_TO_8_RIGHT_SHIFT),
57
+ (b5 << RGB5_TO_8_LEFT_SHIFT) | (b5 >> RGB5_TO_8_RIGHT_SHIFT),
58
+ ];
59
+ };
60
+
61
+ /**
62
+ * Calculate luminance from RGB values (0-255).
63
+ * Returns normalized luminance (0.0 - 1.0).
64
+ */
65
+ export const calculateLuminance = (r: number, g: number, b: number): number => (LUMINANCE_R * r + LUMINANCE_G * g + LUMINANCE_B * b) / MAX_8BIT;
66
+
67
+ /**
68
+ * Calculate luminance as 8-bit integer (0-255).
69
+ */
70
+ export const calculateLuminance8 = (r: number, g: number, b: number): number => Math.round(LUMINANCE_R * r + LUMINANCE_G * g + LUMINANCE_B * b);
71
+
72
+ /**
73
+ * Calculate luminance from RGB15 color.
74
+ * Returns normalized luminance (0.0 - 1.0).
75
+ */
76
+ export const rgb15ToLuminance = (color: number): number => {
77
+ const [r, g, b] = rgb15ToRgb24(color);
78
+ return calculateLuminance(r, g, b);
79
+ };
80
+
81
+ /**
82
+ * Convert RGB to grayscale value (0-255).
83
+ */
84
+ export const rgbToGrayscale = (r: number, g: number, b: number): number => Math.round(LUMINANCE_R * r + LUMINANCE_G * g + LUMINANCE_B * b);
85
+
86
+ // Emoji color definitions for color matching
87
+ export interface EmojiColor {
88
+ emoji: string;
89
+ rgb: [number, number, number];
90
+ }
91
+
92
+ /* eslint-disable @typescript-eslint/no-magic-numbers */
93
+ // Emoji colors tuned for general RGB matching
94
+ export const EMOJI_COLORS: EmojiColor[] = [
95
+ { emoji: '\u2b1c', rgb: [255, 255, 255] }, // White square
96
+ { emoji: '\ud83d\udfe8', rgb: [250, 220, 80] }, // Yellow
97
+ { emoji: '\ud83d\udfe7', rgb: [240, 140, 20] }, // Orange
98
+ { emoji: '\ud83d\udfe5', rgb: [220, 40, 40] }, // Red
99
+ { emoji: '\ud83d\udfeb', rgb: [130, 80, 30] }, // Brown
100
+ { emoji: '\ud83d\udfe9', rgb: [50, 160, 30] }, // Green
101
+ { emoji: '\ud83d\udfe6', rgb: [50, 120, 220] }, // Blue
102
+ { emoji: '\ud83d\udfea', rgb: [160, 70, 200] }, // Purple
103
+ { emoji: '\u2b1b', rgb: [0, 0, 0] }, // Black square
104
+ ];
105
+ /* eslint-enable @typescript-eslint/no-magic-numbers */
106
+
107
+ /**
108
+ * Calculate squared color distance (no sqrt needed for comparison).
109
+ * Green is weighted more heavily (human eye sensitivity).
110
+ */
111
+ export const colorDistanceSquared = (r1: number, g1: number, b1: number, r2: number, g2: number, b2: number): number => {
112
+ const dr = r1 - r2;
113
+ const dg = g1 - g2;
114
+ const db = b1 - b2;
115
+ // Weight green more heavily
116
+ return dr * dr + dg * dg * GREEN_WEIGHT + db * db;
117
+ };
118
+
119
+ /**
120
+ * Find the closest matching emoji for an RGB color.
121
+ */
122
+ export const findClosestEmoji = (r: number, g: number, b: number): string => {
123
+ const closest = firstBy(EMOJI_COLORS, [({ rgb }) => colorDistanceSquared(r, g, b, rgb[0], rgb[1], rgb[2]), 'asc']);
124
+ return closest?.emoji ?? EMOJI_COLORS[0].emoji;
125
+ };
126
+
127
+ /**
128
+ * Get grayscale emoji (black or white) based on luminance.
129
+ */
130
+ export const getGrayscaleEmoji = (luminance: number): string => luminance >= GRAYSCALE_THRESHOLD ? '\u2b1c' : '\u2b1b';
131
+
132
+ /**
133
+ * Find closest emoji for RGB15 color.
134
+ */
135
+ export const rgb15ToEmoji = (color: number): string => {
136
+ const [r, g, b] = rgb15ToRgb24(color);
137
+ return findClosestEmoji(r, g, b);
138
+ };
139
+
140
+ /**
141
+ * Get grayscale emoji for RGB15 color.
142
+ */
143
+ export const rgb15ToGrayscaleEmoji = (color: number): string => getGrayscaleEmoji(rgb15ToLuminance(color));
144
+
145
+ /**
146
+ * Find closest emoji for RGB24 color.
147
+ */
148
+ export const rgb24ToEmoji = (r: number, g: number, b: number): string => findClosestEmoji(r, g, b);
149
+
150
+ /**
151
+ * Get grayscale emoji for RGB24 color.
152
+ */
153
+ export const rgb24ToGrayscaleEmoji = (r: number, g: number, b: number): string => getGrayscaleEmoji(calculateLuminance(r, g, b));
154
+
155
+ /**
156
+ * Convert RGB to ANSI 256-color code (6x6x6 color cube).
157
+ */
158
+ export const rgbToAnsi256 = (r: number, g: number, b: number): number => {
159
+ const r6 = Math.round((r / MAX_8BIT) * (ANSI_COLOR_CUBE_LEVELS - 1));
160
+ const g6 = Math.round((g / MAX_8BIT) * (ANSI_COLOR_CUBE_LEVELS - 1));
161
+ const b6 = Math.round((b / MAX_8BIT) * (ANSI_COLOR_CUBE_LEVELS - 1));
162
+ return ANSI_COLOR_CUBE_START + (ANSI_RED_MULTIPLIER * r6) + (ANSI_GREEN_MULTIPLIER * g6) + b6;
163
+ };
164
+
165
+ /**
166
+ * Build gamma correction lookup table.
167
+ * gamma = 1.0: no change
168
+ * gamma > 1.0: darkens midtones (CRT-like)
169
+ * gamma < 1.0: brightens midtones
170
+ */
171
+ export const buildGammaLUT = (gamma: number): Uint8Array => {
172
+ const lut = new Uint8Array(LUT_SIZE_8BIT);
173
+ if (gamma === DEFAULT_GAMMA) {
174
+ for (let i = 0; i < LUT_SIZE_8BIT; i++) {
175
+ lut[i] = i;
176
+ }
177
+ } else {
178
+ for (let i = 0; i < LUT_SIZE_8BIT; i++) {
179
+ lut[i] = Math.round(Math.pow(i / MAX_8BIT, gamma) * MAX_8BIT);
180
+ }
181
+ }
182
+ return lut;
183
+ };
@@ -0,0 +1,50 @@
1
+ //==========================================================================
2
+ // Gzip (0x1f 0x8b)
3
+ //==========================================================================
4
+
5
+ /** First byte of gzip magic number (0x1f) */
6
+ export const GZIP_MAGIC_BYTE_1 = 0x1f;
7
+
8
+ /** Second byte of gzip magic number (0x8b) */
9
+ export const GZIP_MAGIC_BYTE_2 = 0x8b;
10
+
11
+ /** Minimum bytes needed to detect gzip compression */
12
+ export const GZIP_MAGIC_SIZE = 2;
13
+
14
+ //==========================================================================
15
+ // Zstandard (0x28 0xb5 0x2f 0xfd)
16
+ //==========================================================================
17
+
18
+ /** First byte of Zstandard magic number */
19
+ export const ZSTD_MAGIC_BYTE_1 = 0x28;
20
+
21
+ /** Second byte of Zstandard magic number */
22
+ export const ZSTD_MAGIC_BYTE_2 = 0xb5;
23
+
24
+ /** Third byte of Zstandard magic number */
25
+ export const ZSTD_MAGIC_BYTE_3 = 0x2f;
26
+
27
+ /** Fourth byte of Zstandard magic number */
28
+ export const ZSTD_MAGIC_BYTE_4 = 0xfd;
29
+
30
+ /** Minimum bytes needed to detect Zstandard compression */
31
+ export const ZSTD_MAGIC_SIZE = 4;
32
+
33
+ //==========================================================================
34
+ // Zlib (0x78 followed by 0x01, 0x9c, or 0xda)
35
+ //==========================================================================
36
+
37
+ /** First byte of zlib header (CMF - Compression Method and Flags) */
38
+ export const ZLIB_CMF_BYTE = 0x78;
39
+
40
+ /** Zlib FLG byte for low compression */
41
+ export const ZLIB_FLG_LOW = 0x01;
42
+
43
+ /** Zlib FLG byte for default compression */
44
+ export const ZLIB_FLG_DEFAULT = 0x9c;
45
+
46
+ /** Zlib FLG byte for best compression */
47
+ export const ZLIB_FLG_BEST = 0xda;
48
+
49
+ /** Minimum bytes needed to detect zlib compression */
50
+ export const ZLIB_MAGIC_SIZE = 2;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Compression Utilities
3
+ *
4
+ * Helpers for working with compressed data formats.
5
+ * Supports detection and decompression of RetroArch save state formats:
6
+ * - Zstandard (default in newer RetroArch)
7
+ * - Zlib (older default)
8
+ * - Gzip (rare)
9
+ * - Uncompressed (raw binary)
10
+ */
11
+
12
+ import { gunzipSync, inflateSync } from 'zlib';
13
+ import { decompress as decompressZstd } from 'fzstd';
14
+ import {
15
+ GZIP_MAGIC_BYTE_1,
16
+ GZIP_MAGIC_BYTE_2,
17
+ GZIP_MAGIC_SIZE,
18
+ ZSTD_MAGIC_BYTE_1,
19
+ ZSTD_MAGIC_BYTE_2,
20
+ ZSTD_MAGIC_BYTE_3,
21
+ ZSTD_MAGIC_BYTE_4,
22
+ ZSTD_MAGIC_SIZE,
23
+ ZLIB_CMF_BYTE,
24
+ ZLIB_FLG_LOW,
25
+ ZLIB_FLG_DEFAULT,
26
+ ZLIB_FLG_BEST,
27
+ ZLIB_MAGIC_SIZE,
28
+ } from './consts';
29
+
30
+ export * from './consts';
31
+
32
+ /** Compression format of a data buffer */
33
+ export type CompressionFormat = 'zstd' | 'zlib' | 'gzip' | 'none';
34
+
35
+ /**
36
+ * Check if a buffer contains gzip-compressed data.
37
+ * Gzip files start with magic bytes 0x1f 0x8b.
38
+ */
39
+ export const isGzipped = (data: Buffer | Uint8Array): boolean =>
40
+ data.length >= GZIP_MAGIC_SIZE &&
41
+ data[0] === GZIP_MAGIC_BYTE_1 &&
42
+ data[1] === GZIP_MAGIC_BYTE_2;
43
+
44
+ /**
45
+ * Check if a buffer contains Zstandard-compressed data.
46
+ * Zstandard files start with magic bytes 0x28 0xb5 0x2f 0xfd.
47
+ */
48
+ export const isZstd = (data: Buffer | Uint8Array): boolean =>
49
+ data.length >= ZSTD_MAGIC_SIZE &&
50
+ data[0] === ZSTD_MAGIC_BYTE_1 &&
51
+ data[1] === ZSTD_MAGIC_BYTE_2 &&
52
+ data[2] === ZSTD_MAGIC_BYTE_3 &&
53
+ data[3] === ZSTD_MAGIC_BYTE_4;
54
+
55
+ /**
56
+ * Check if a buffer contains zlib-compressed data.
57
+ * Zlib data starts with 0x78 followed by 0x01 (low), 0x9c (default), or 0xda (best).
58
+ */
59
+ export const isZlib = (data: Buffer | Uint8Array): boolean =>
60
+ data.length >= ZLIB_MAGIC_SIZE &&
61
+ data[0] === ZLIB_CMF_BYTE &&
62
+ (data[1] === ZLIB_FLG_LOW || data[1] === ZLIB_FLG_DEFAULT || data[1] === ZLIB_FLG_BEST);
63
+
64
+ /**
65
+ * Detect the compression format of a data buffer.
66
+ * Checks magic bytes to identify the format.
67
+ */
68
+ export const detectCompressionFormat = (data: Buffer | Uint8Array): CompressionFormat => {
69
+ if (isZstd(data)) {
70
+ return 'zstd';
71
+ }
72
+ if (isZlib(data)) {
73
+ return 'zlib';
74
+ }
75
+ if (isGzipped(data)) {
76
+ return 'gzip';
77
+ }
78
+ return 'none';
79
+ };
80
+
81
+ /**
82
+ * Decompress data that may be in any supported format.
83
+ * Auto-detects the compression format and decompresses accordingly.
84
+ * Returns the original data unchanged if not compressed.
85
+ */
86
+ export const decompress = (data: Buffer | Uint8Array): Buffer => {
87
+ const format = detectCompressionFormat(data);
88
+
89
+ switch (format) {
90
+ case 'zstd': {
91
+ const decompressed = decompressZstd(data);
92
+ return Buffer.from(decompressed);
93
+ }
94
+ case 'zlib':
95
+ return inflateSync(data);
96
+ case 'gzip':
97
+ return gunzipSync(data);
98
+ case 'none':
99
+ return Buffer.isBuffer(data) ? data : Buffer.from(data);
100
+ }
101
+ };
@@ -0,0 +1,2 @@
1
+ /** Hexadecimal radix for Number.toString(radix) */
2
+ export const HEX_RADIX = 16;
@@ -0,0 +1,22 @@
1
+ /** CRC32 polynomial (reversed) */
2
+ export const CRC32_POLYNOMIAL = 0xedb88320;
3
+
4
+ /** Initial CRC value */
5
+ export const CRC32_INITIAL = 0xffffffff;
6
+
7
+ /** Number of bits to process for table generation */
8
+ export const CRC32_BIT_COUNT = 8;
9
+
10
+ /** Size of the CRC32 lookup table */
11
+ export const CRC32_TABLE_SIZE = 256;
12
+
13
+ /** Byte mask for table lookup */
14
+ export const BYTE_MASK = 0xff;
15
+
16
+ export { HEX_RADIX } from '..';
17
+
18
+ /** CRC32 hex string length (8 characters) */
19
+ export const CRC32_HEX_LENGTH = 8;
20
+
21
+ /** Chunk size for streaming file reads (64KB) */
22
+ export const CHUNK_SIZE = 65536;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * CRC32 Checksum Utilities
3
+ *
4
+ * General-purpose CRC32 calculation for buffers and files.
5
+ * Uses the standard CRC32 polynomial (0xEDB88320).
6
+ */
7
+
8
+ import { openSync, readSync, closeSync } from 'fs';
9
+
10
+ export * from './consts';
11
+
12
+ import {
13
+ CRC32_POLYNOMIAL,
14
+ CRC32_INITIAL,
15
+ CRC32_BIT_COUNT,
16
+ CRC32_TABLE_SIZE,
17
+ BYTE_MASK,
18
+ HEX_RADIX,
19
+ CRC32_HEX_LENGTH,
20
+ CHUNK_SIZE,
21
+ } from './consts';
22
+
23
+ // =============================================================================
24
+ // CRC32 Table Generation
25
+ // =============================================================================
26
+
27
+ const CRC32_TABLE = new Uint32Array(CRC32_TABLE_SIZE);
28
+ for (let i = 0; i < CRC32_TABLE_SIZE; i++) {
29
+ let c = i;
30
+ for (let j = 0; j < CRC32_BIT_COUNT; j++) {
31
+ c = (c & 1) ? (CRC32_POLYNOMIAL ^ (c >>> 1)) : (c >>> 1);
32
+ }
33
+ CRC32_TABLE[i] = c >>> 0;
34
+ }
35
+
36
+ // =============================================================================
37
+ // Functions
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Calculate CRC32 checksum for a buffer.
42
+ * Returns the checksum as an unsigned 32-bit integer.
43
+ */
44
+ export const crc32 = (data: Buffer): number => {
45
+ let crc = CRC32_INITIAL;
46
+ for (let i = 0; i < data.length; i++) {
47
+ crc = CRC32_TABLE[(crc ^ data[i]) & BYTE_MASK] ^ (crc >>> CRC32_BIT_COUNT);
48
+ }
49
+ return (crc ^ CRC32_INITIAL) >>> 0;
50
+ };
51
+
52
+ /**
53
+ * Calculate CRC32 checksum of a file using streaming reads.
54
+ * Uses a fixed 64KB buffer to minimize memory usage for large files.
55
+ * Returns the 8-character uppercase hex string, or undefined if the file cannot be read.
56
+ */
57
+ export const calculateFileCrc32 = (filePath: string): string | undefined => {
58
+ let fd: number;
59
+ try {
60
+ fd = openSync(filePath, 'r');
61
+ } catch {
62
+ return undefined;
63
+ }
64
+
65
+ try {
66
+ const buffer = Buffer.alloc(CHUNK_SIZE);
67
+ let crc = CRC32_INITIAL;
68
+ let bytesRead: number;
69
+
70
+ while ((bytesRead = readSync(fd, buffer, 0, CHUNK_SIZE, null)) > 0) {
71
+ for (let i = 0; i < bytesRead; i++) {
72
+ crc = CRC32_TABLE[(crc ^ buffer[i]) & BYTE_MASK] ^ (crc >>> CRC32_BIT_COUNT);
73
+ }
74
+ }
75
+
76
+ const checksum = (crc ^ CRC32_INITIAL) >>> 0;
77
+ return checksum.toString(HEX_RADIX).toUpperCase().padStart(CRC32_HEX_LENGTH, '0');
78
+ } catch {
79
+ return undefined;
80
+ } finally {
81
+ closeSync(fd);
82
+ }
83
+ };
@@ -0,0 +1,10 @@
1
+ import { existsSync, mkdirSync } from 'fs';
2
+
3
+ /**
4
+ * Ensure a directory exists, creating it (and parents) if necessary.
5
+ */
6
+ export const ensureDirectory = (dir: string): void => {
7
+ if (!existsSync(dir)) {
8
+ mkdirSync(dir, { recursive: true });
9
+ }
10
+ };
@@ -0,0 +1,8 @@
1
+ /** Default frames per second for playtime calculation */
2
+ export const DEFAULT_FPS = 60;
3
+
4
+ /** Seconds per hour */
5
+ export const SECONDS_PER_HOUR = 3600;
6
+
7
+ /** Seconds per minute */
8
+ export const SECONDS_PER_MINUTE = 60;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Formatting Utilities
3
+ *
4
+ * General-purpose formatting functions.
5
+ */
6
+
7
+ import {
8
+ DEFAULT_FPS,
9
+ SECONDS_PER_HOUR,
10
+ SECONDS_PER_MINUTE,
11
+ } from './consts';
12
+
13
+ export * from './consts';
14
+
15
+ /**
16
+ * Format frame count as estimated playtime.
17
+ * Calculates hours/minutes/seconds based on frame count and FPS.
18
+ *
19
+ * @param frames Total frame count
20
+ * @param fps Frames per second (default: 60)
21
+ * @returns Formatted playtime string (e.g., "2h 30m 15s", "45m 30s", "15s")
22
+ */
23
+ export const formatPlayTime = (frames: number, fps: number = DEFAULT_FPS): string => {
24
+ const totalSeconds = Math.floor(frames / fps);
25
+ return formatRuntimeSeconds(totalSeconds);
26
+ };
27
+
28
+ /**
29
+ * Decompose total seconds into hours, minutes, and seconds.
30
+ */
31
+ export const secondsToHms = (totalSeconds: number): { hours: number; minutes: number; seconds: number } => ({
32
+ hours: Math.floor(totalSeconds / SECONDS_PER_HOUR),
33
+ minutes: Math.floor((totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE),
34
+ seconds: totalSeconds % SECONDS_PER_MINUTE,
35
+ });
36
+
37
+ /**
38
+ * Format runtime in seconds as a human-readable string.
39
+ *
40
+ * @param totalSeconds Total runtime in seconds
41
+ * @returns Formatted runtime string (e.g., "2h 30m 15s", "45m 30s", "15s")
42
+ */
43
+ export const formatRuntimeSeconds = (totalSeconds: number): string => {
44
+ const { hours, minutes, seconds } = secondsToHms(totalSeconds);
45
+
46
+ if (hours > 0) {
47
+ return `${hours}h ${minutes}m ${seconds}s`;
48
+ } else if (minutes > 0) {
49
+ return `${minutes}m ${seconds}s`;
50
+ } else {
51
+ return `${seconds}s`;
52
+ }
53
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Extracts a human-readable message from an unknown caught error.
3
+ * Handles Error instances, strings, and other values.
4
+ */
5
+ export const getErrorMessage = (err: unknown): string => {
6
+ if (err instanceof Error) {
7
+ return err.message;
8
+ }
9
+ return String(err);
10
+ };