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,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;
|