mirror of
https://github.com/mileswolfallen2/OmniEmu2.0.git
synced 2026-09-08 11:23:17 +00:00
testing
This commit is contained in:
@@ -1,2 +1,96 @@
|
||||
# OmniEmu2.0
|
||||
idk weeee will see
|
||||
# OmniEmu
|
||||
|
||||
Cross-platform emulator manager and ROM library — for Windows, macOS, and Linux.
|
||||
|
||||
## Features
|
||||
|
||||
- **Emulator Management** — Detect installed emulators, download new ones, manage versions
|
||||
- **ROM Library** — Scan directories for ROMs, organise by platform, launch games
|
||||
- **Cross-Platform** — Same experience on Windows, macOS, and Linux (x86-64 & ARM64)
|
||||
- **Built-in Launcher** — Launch games directly from the library with proper emulator arguments
|
||||
|
||||
## Supported Emulators
|
||||
|
||||
| Emulator | Systems | Windows | macOS | Linux |
|
||||
|------------|-------------------|---------|-------|-------|
|
||||
| Dolphin | GameCube, Wii | ✅ | ✅ | ✅ |
|
||||
| RPCS3 | PlayStation 3 | ✅ | ✅ | ✅ |
|
||||
| Ryujinx | Nintendo Switch | ✅ | ✅ | ✅ |
|
||||
| PCSX2 | PlayStation 2 | ✅ | ✅ | ✅ |
|
||||
| MAME | Arcade | ✅ | ✅ | ✅ |
|
||||
| RetroArch | Multi-system | ✅ | ✅ | ✅ |
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+ (recommended: 22)
|
||||
- npm 10+
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This starts the Vite dev server for the renderer and the TypeScript compiler for the main process in watch mode.
|
||||
|
||||
### Build for production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Package for distribution
|
||||
|
||||
```bash
|
||||
# Current platform
|
||||
npm run package:mac
|
||||
npm run package:win
|
||||
npm run package:linux
|
||||
|
||||
# All platforms
|
||||
npm run package:all
|
||||
```
|
||||
|
||||
Output goes to `./release/`.
|
||||
|
||||
### Platform-specific scripts
|
||||
|
||||
```bash
|
||||
# macOS (specify arch: x64 or arm64)
|
||||
./scripts/build-mac.sh arm64
|
||||
|
||||
# Windows (PowerShell)
|
||||
.\scripts\build-win.ps1 -Arch x64
|
||||
|
||||
# Linux
|
||||
./scripts/build-linux.sh x64
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/ # Electron main process
|
||||
│ ├── index.ts # App entry, window creation, tray
|
||||
│ ├── preload.ts # Context bridge API
|
||||
│ ├── ipc.ts # IPC handler registration
|
||||
│ ├── platform.ts # OS/arch detection utilities
|
||||
│ ├── emulators.ts # Emulator detection, ROM scanning, launching
|
||||
│ └── settings.ts # Persistent settings
|
||||
├── renderer/ # React UI (Vite)
|
||||
│ ├── App.tsx
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ └── styles.css
|
||||
└── shared/ # Types shared between main & renderer
|
||||
└── types.ts
|
||||
scripts/ # Build and install scripts
|
||||
.github/workflows/ # CI configuration
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
Generated
+7160
File diff suppressed because it is too large
Load Diff
Regular → Executable
Regular → Executable
Regular → Executable
@@ -0,0 +1,282 @@
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { app } from 'electron';
|
||||
import { ConfigPreset, InstallProgress, Platform } from '../shared/types';
|
||||
import { getPlatform, isWindows, isMacOS } from './platform';
|
||||
|
||||
const platform = getPlatform();
|
||||
|
||||
/**
|
||||
* Built-in recommended presets for each emulator.
|
||||
* These are known-good configs curated by the EmuDeck community and
|
||||
* adapted for OmniEmu. They set optimal performance/quality balances.
|
||||
*/
|
||||
const builtInPresets: Record<string, ConfigPreset[]> = {
|
||||
dolphin: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Best balance of performance and quality for most systems',
|
||||
files: {
|
||||
'Config/Dolphin.ini': `[General]
|
||||
LastFilename =
|
||||
ShowLag = False
|
||||
ShowRenderTimes = False
|
||||
[Display]
|
||||
FullscreenDisplayRes = Auto
|
||||
Fullscreen = True
|
||||
RenderToMain = True
|
||||
[Interface]
|
||||
ConfirmStop = False
|
||||
PauseOnFocusLost = False
|
||||
[Core]
|
||||
CPUCore = 3
|
||||
Fastmem = True
|
||||
MMU = False
|
||||
[Enhancements]
|
||||
InternalResolution = 3
|
||||
MaxAnisotropy = 4
|
||||
`,
|
||||
'Config/GFX.ini': `[Settings]
|
||||
Backend = Vulkan
|
||||
ShaderCompilationMode = 2
|
||||
WaitForShadersBeforeStarting = True
|
||||
HiresTextures = False
|
||||
CacheHiresTextures = True
|
||||
[Enhancements]
|
||||
ForceFiltering = False
|
||||
WidescreenHack = True
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
rpcs3: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Optimized settings for RPCS3 with Vulkan backend',
|
||||
files: {
|
||||
'config.yml': `Video:
|
||||
Format: Vulkan
|
||||
FrameLimit: Auto
|
||||
Resolution: 1920x1080
|
||||
AntiAliasing: Disabled
|
||||
RenderScale: 100
|
||||
Audio:
|
||||
Format: XAudio2
|
||||
Device: Default
|
||||
Input:
|
||||
PadHandler: DualShock4
|
||||
MouseHandler: Basic
|
||||
CPU:
|
||||
PPUDecoder: LLVM
|
||||
SPUDecoder: ASMJIT
|
||||
ThreadScheduler: OS
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
ryujinx: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Best settings for Ryujinx Switch emulation',
|
||||
files: {
|
||||
'Config.json': `{
|
||||
"graphics_backend": "Vulkan",
|
||||
"resolution_scale": 2,
|
||||
"docked_mode": true,
|
||||
"anisotropy_filtering": 4,
|
||||
"aspect_ratio": "16:9",
|
||||
"enable_vsync": true,
|
||||
"shader_cache": true,
|
||||
"audio_backend": "OpenAL"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
pcsx2: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Optimal PCSX2 configuration for modern hardware',
|
||||
files: {
|
||||
'inis/PCSX2.ini': `[Filenames]
|
||||
[Settings]
|
||||
UserMode = 0
|
||||
EnableVSync = 1
|
||||
[EmuCore]
|
||||
GSRenderer = 13
|
||||
EnableCheats = 0
|
||||
EnableWideScreenPatches = 1
|
||||
[GS]
|
||||
Renderer = Vulkan
|
||||
UpscaleMultiplier = 3
|
||||
BilinearFilter = 1
|
||||
TrilinearFilter = 1
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
retroarch: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Universal RetroArch config with optimal defaults',
|
||||
files: {
|
||||
'retroarch.cfg': `video_driver = "vulkan"
|
||||
audio_driver = "pulseaudio"
|
||||
input_driver = "udev"
|
||||
video_fullscreen = true
|
||||
video_vsync = true
|
||||
video_scale_integer = false
|
||||
video_smooth = true
|
||||
audio_sync = true
|
||||
savestate_thumbnail_enable = true
|
||||
notification_show_autoconfig = false
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
mame: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'MAME optimized settings',
|
||||
files: {
|
||||
'mame.ini': `#
|
||||
# OmniEmu Recommended MAME Config
|
||||
#
|
||||
video auto
|
||||
screen auto
|
||||
aspect 4:3
|
||||
effect none
|
||||
waitvsync 1
|
||||
syncrefresh 0
|
||||
sleep 0
|
||||
autosave 0
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const presetSourceUrl = 'https://raw.githubusercontent.com/mileswolfallen2/omniemu-presets/main/presets.json';
|
||||
|
||||
/** Fetch remote presets, falling back to built-in */
|
||||
async function fetchRemotePresets(): Promise<Record<string, ConfigPreset[]> | null> {
|
||||
try {
|
||||
const { get } = await import('https');
|
||||
const data = await new Promise<string>((resolve, reject) => {
|
||||
get(presetSourceUrl, (res) => {
|
||||
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (c: Buffer) => chunks.push(c));
|
||||
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
||||
}).on('error', reject);
|
||||
});
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuiltInPresets(): Record<string, ConfigPreset[]> {
|
||||
return builtInPresets;
|
||||
}
|
||||
|
||||
export async function getPresets(emulatorId: string): Promise<ConfigPreset[]> {
|
||||
// Try remote first, fall back to built-in
|
||||
const remote = await fetchRemotePresets();
|
||||
if (remote && remote[emulatorId]) return remote[emulatorId];
|
||||
return builtInPresets[emulatorId] || [];
|
||||
}
|
||||
|
||||
/** Get the config directory for an emulator given its install path */
|
||||
function getConfigDir(emulatorId: string, installPath: string): string {
|
||||
const platformDirs: Record<string, Record<string, string>> = {
|
||||
dolphin: {
|
||||
win32: join(process.env.APPDATA || join(require('os').homedir(), 'AppData', 'Roaming'), 'Dolphin'),
|
||||
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'Dolphin'),
|
||||
linux: join(require('os').homedir(), '.config', 'dolphin-emu'),
|
||||
},
|
||||
rpcs3: {
|
||||
win32: join(process.env.APPDATA || '', 'RPCS3'),
|
||||
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'rpcs3'),
|
||||
linux: join(require('os').homedir(), '.config', 'rpcs3'),
|
||||
},
|
||||
ryujinx: {
|
||||
win32: join(process.env.APPDATA || '', 'Ryujinx'),
|
||||
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'Ryujinx'),
|
||||
linux: join(require('os').homedir(), '.config', 'Ryujinx'),
|
||||
},
|
||||
pcsx2: {
|
||||
win32: join(process.env.APPDATA || '', 'PCSX2'),
|
||||
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'PCSX2'),
|
||||
linux: join(require('os').homedir(), '.config', 'PCSX2'),
|
||||
},
|
||||
retroarch: {
|
||||
win32: join(process.env.APPDATA || '', 'RetroArch'),
|
||||
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'RetroArch'),
|
||||
linux: join(require('os').homedir(), '.config', 'retroarch'),
|
||||
},
|
||||
mame: {
|
||||
win32: dirname(installPath),
|
||||
darwin: dirname(installPath),
|
||||
linux: join(require('os').homedir(), '.mame'),
|
||||
},
|
||||
};
|
||||
return platformDirs[emulatorId]?.[platform] || dirname(installPath);
|
||||
}
|
||||
|
||||
/** Check if an emulator has been configured with OmniEmu presets */
|
||||
export function checkConfigured(emulatorId: string, installPath?: string): boolean {
|
||||
if (!installPath) return false;
|
||||
const marker = join(app.getPath('userData'), 'configs', `${emulatorId}.configured`);
|
||||
return existsSync(marker);
|
||||
}
|
||||
|
||||
/** Apply a config preset to the emulator's config directory */
|
||||
export async function applyPreset(
|
||||
emulatorId: string,
|
||||
preset: ConfigPreset,
|
||||
installPath: string,
|
||||
onProgress?: (p: InstallProgress) => void
|
||||
): Promise<void> {
|
||||
const configDir = getConfigDir(emulatorId, installPath);
|
||||
const report = onProgress
|
||||
? (percent: number, message: string) =>
|
||||
onProgress({ emulatorId, stage: 'configuring', percent, message })
|
||||
: () => {};
|
||||
|
||||
report(0, `Configuring ${emulatorId} with "${preset.name}"...`);
|
||||
|
||||
const totalFiles = Object.keys(preset.files).length;
|
||||
let done = 0;
|
||||
|
||||
for (const [relativePath, content] of Object.entries(preset.files)) {
|
||||
const fullPath = join(configDir, relativePath);
|
||||
const parentDir = dirname(fullPath);
|
||||
|
||||
if (!existsSync(parentDir)) {
|
||||
mkdirSync(parentDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(fullPath, content, 'utf-8');
|
||||
|
||||
done++;
|
||||
report(Math.round((done / totalFiles) * 100), `Wrote ${relativePath}`);
|
||||
}
|
||||
|
||||
// Write marker so we know this emulator was configured
|
||||
const markerDir = join(app.getPath('userData'), 'configs');
|
||||
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
||||
writeFileSync(join(markerDir, `${emulatorId}.configured`), new Date().toISOString(), 'utf-8');
|
||||
|
||||
report(100, `${emulatorId} configured with "${preset.name}"`);
|
||||
}
|
||||
|
||||
/** Apply the recommended preset (first preset available) */
|
||||
export async function applyRecommendedConfig(
|
||||
emulatorId: string,
|
||||
installPath: string,
|
||||
onProgress?: (p: InstallProgress) => void
|
||||
): Promise<boolean> {
|
||||
const presets = await getPresets(emulatorId);
|
||||
if (presets.length === 0) return false;
|
||||
await applyPreset(emulatorId, presets[0], installPath, onProgress);
|
||||
return true;
|
||||
}
|
||||
+215
-110
@@ -1,18 +1,18 @@
|
||||
import { execSync, exec, ChildProcess } from 'child_process';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { join, dirname, basename, extname } from 'path';
|
||||
import { app } from 'electron';
|
||||
import {
|
||||
EmulatorConfig,
|
||||
EmulatorState,
|
||||
Platform,
|
||||
GameEntry,
|
||||
RomFile,
|
||||
InstallProgress,
|
||||
} from '../shared/types';
|
||||
import { getPlatform, getArch, isWindows, isMacOS, isLinux } from './platform';
|
||||
import { settings } from './settings';
|
||||
|
||||
const platform = getPlatform();
|
||||
const arch = getArch();
|
||||
|
||||
export const knownEmulators: EmulatorConfig[] = [
|
||||
{
|
||||
@@ -25,13 +25,41 @@ export const knownEmulators: EmulatorConfig[] = [
|
||||
darwin: '/Applications/Dolphin.app/Contents/MacOS/Dolphin',
|
||||
linux: '/usr/bin/dolphin-emu',
|
||||
},
|
||||
installUrl: {
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-x64.7z',
|
||||
format: '7z',
|
||||
executablePath: 'Dolphin.exe',
|
||||
arch: 'x64',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-universal.dmg',
|
||||
format: 'dmg',
|
||||
arch: 'arm64',
|
||||
},
|
||||
{
|
||||
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-x64.dmg',
|
||||
format: 'dmg',
|
||||
arch: 'x64',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://dl.dolphin-emu.org/releases/2412/dolphin-2412-x86_64.AppImage',
|
||||
format: 'appimage',
|
||||
},
|
||||
],
|
||||
},
|
||||
packageNames: { linux: 'dolphin-emu' },
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://dolphin-emu.org/download/',
|
||||
darwin: 'https://dolphin-emu.org/download/',
|
||||
linux: null,
|
||||
linux: 'https://dolphin-emu.org/download/',
|
||||
},
|
||||
installVia: 'download',
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
id: 'rpcs3',
|
||||
@@ -43,31 +71,27 @@ export const knownEmulators: EmulatorConfig[] = [
|
||||
darwin: '/Applications/RPCS3.app/Contents/MacOS/rpcs3',
|
||||
linux: '/usr/bin/rpcs3',
|
||||
},
|
||||
installUrl: {
|
||||
win32: 'https://rpcs3.net/download',
|
||||
darwin: null,
|
||||
linux: null,
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://github.com/RPCS3/rpcs3-binaries-win/releases/download/build-d2c3b344332efc6e545a09ad44b9f083c2a1a519/rpcs3-v0.0.34-17491-d2c3b344_win64.7z',
|
||||
format: '7z',
|
||||
executablePath: 'rpcs3.exe',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://github.com/RPCS3/rpcs3-binaries-linux/releases/download/build-d2c3b344332efc6e545a09ad44b9f083c2a1a519/rpcs3-v0.0.34-17491-d2c3b344_linux64.AppImage',
|
||||
format: 'appimage',
|
||||
},
|
||||
],
|
||||
},
|
||||
installVia: 'download',
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
id: 'yuzu',
|
||||
name: 'Yuzu',
|
||||
description: 'Nintendo Switch emulator',
|
||||
platforms: ['switch'],
|
||||
defaultPath: {
|
||||
win32: 'C:\\Program Files\\Yuzu\\yuzu.exe',
|
||||
darwin: '/Applications/Yuzu.app/Contents/MacOS/yuzu',
|
||||
linux: '/usr/bin/yuzu',
|
||||
websiteUrl: {
|
||||
win32: 'https://rpcs3.net/download',
|
||||
darwin: 'https://rpcs3.net/download',
|
||||
linux: 'https://rpcs3.net/download',
|
||||
},
|
||||
installUrl: {
|
||||
win32: 'https://yuzu-emu.org/downloads/',
|
||||
darwin: null,
|
||||
linux: null,
|
||||
},
|
||||
installVia: 'download',
|
||||
supported: false,
|
||||
},
|
||||
{
|
||||
id: 'ryujinx',
|
||||
@@ -79,13 +103,35 @@ export const knownEmulators: EmulatorConfig[] = [
|
||||
darwin: '/Applications/Ryujinx.app/Contents/MacOS/Ryujinx',
|
||||
linux: '/usr/bin/Ryujinx',
|
||||
},
|
||||
installUrl: {
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://github.com/Ryujinx/release-channel-master/releases/latest/download/ryujinx-1.2.0-win_x64.zip',
|
||||
format: 'zip',
|
||||
executablePath: 'Ryujinx.exe',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://github.com/Ryujinx/release-channel-master/releases/latest/download/ryujinx-1.2.0-mac_universal.zip',
|
||||
format: 'zip',
|
||||
executablePath: 'Ryujinx.app/Contents/MacOS/Ryujinx',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://github.com/Ryujinx/release-channel-master/releases/latest/download/ryujinx-1.2.0-linux_x64.zip',
|
||||
format: 'zip',
|
||||
executablePath: 'Ryujinx',
|
||||
},
|
||||
],
|
||||
},
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://ryujinx.org/download',
|
||||
darwin: 'https://ryujinx.org/download',
|
||||
linux: 'https://ryujinx.org/download',
|
||||
},
|
||||
installVia: 'download',
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
id: 'pcsx2',
|
||||
@@ -97,13 +143,39 @@ export const knownEmulators: EmulatorConfig[] = [
|
||||
darwin: '/Applications/PCSX2.app/Contents/MacOS/PCSX2',
|
||||
linux: '/usr/bin/pcsx2',
|
||||
},
|
||||
installUrl: {
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-windows-x64-installer.exe',
|
||||
format: 'exe',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-macos-arm64.dmg',
|
||||
format: 'dmg',
|
||||
arch: 'arm64',
|
||||
},
|
||||
{
|
||||
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-macos-x64.dmg',
|
||||
format: 'dmg',
|
||||
arch: 'x64',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://github.com/PCSX2/pcsx2/releases/download/v2.3.200/pcsx2-v2.3.200-linux-x86_64.AppImage',
|
||||
format: 'appimage',
|
||||
},
|
||||
],
|
||||
},
|
||||
packageNames: { linux: 'pcsx2' },
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://pcsx2.net/downloads/',
|
||||
darwin: 'https://pcsx2.net/downloads/',
|
||||
linux: null,
|
||||
linux: 'https://pcsx2.net/downloads/',
|
||||
},
|
||||
installVia: 'downloacd',
|
||||
supported: true,
|
||||
},
|
||||
{
|
||||
id: 'mame',
|
||||
@@ -115,18 +187,33 @@ export const knownEmulators: EmulatorConfig[] = [
|
||||
darwin: '/Applications/MAME.app/Contents/MacOS/mame',
|
||||
linux: '/usr/bin/mame',
|
||||
},
|
||||
installUrl: {
|
||||
win32: 'https://www.mamedev.org/release.html',
|
||||
darwin: null,
|
||||
linux: null,
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://github.com/mamedev/mame/releases/download/mame0276/mame0276_64bit.7z',
|
||||
format: '7z',
|
||||
executablePath: 'mame64.exe',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://github.com/mamedev/mame/releases/download/mame0276/mame0276-x86_64.AppImage',
|
||||
format: 'appimage',
|
||||
},
|
||||
],
|
||||
},
|
||||
installVia: 'download',
|
||||
packageNames: { linux: 'mame' },
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://www.mamedev.org/release.html',
|
||||
darwin: 'https://www.mamedev.org/release.html',
|
||||
linux: 'https://www.mamedev.org/release.html',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'retroarch',
|
||||
name: 'RetroArch',
|
||||
description: 'Multi-system emulator frontend',
|
||||
description: 'Multi-system emulator frontend (NES, SNES, N64, GB, GBA, PS1, etc.)',
|
||||
platforms: [
|
||||
'nes', 'snes', 'n64', 'gb', 'gba', 'gbc',
|
||||
'ps1', 'pce', 'sega-md', 'sega-saturn', 'sega-dc',
|
||||
@@ -136,17 +223,42 @@ export const knownEmulators: EmulatorConfig[] = [
|
||||
darwin: '/Applications/RetroArch.app/Contents/MacOS/RetroArch',
|
||||
linux: '/usr/bin/retroarch',
|
||||
},
|
||||
installUrl: {
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://buildbot.libretro.com/stable/1.19.1/windows/x86_64/RetroArch.7z',
|
||||
format: '7z',
|
||||
executablePath: 'RetroArch.exe',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://buildbot.libretro.com/stable/1.19.1/apple/osx/universal/RetroArch.dmg',
|
||||
format: 'dmg',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://buildbot.libretro.com/stable/1.19.1/linux/x86_64/RetroArch.7z',
|
||||
format: '7z',
|
||||
executablePath: 'retroarch',
|
||||
},
|
||||
],
|
||||
},
|
||||
packageNames: {
|
||||
linux: 'retroarch',
|
||||
darwin: 'retroarch',
|
||||
},
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://retroarch.com/?page=platforms',
|
||||
darwin: 'https://retroarch.com/?page=platforms',
|
||||
linux: 'https://retroarch.com/?page=platforms',
|
||||
},
|
||||
installVia: 'download',
|
||||
supported: true,
|
||||
},
|
||||
];
|
||||
|
||||
function findEmulator(id: string): EmulatorConfig | undefined {
|
||||
export function findEmulator(id: string): EmulatorConfig | undefined {
|
||||
return knownEmulators.find((e) => e.id === id);
|
||||
}
|
||||
|
||||
@@ -154,20 +266,18 @@ function detectEmulatorPath(config: EmulatorConfig): string | undefined {
|
||||
const path = config.defaultPath[platform];
|
||||
if (path && existsSync(path)) return path;
|
||||
|
||||
// Search common alternative paths
|
||||
const alternatives = alternativePaths(config.id);
|
||||
for (const alt of alternatives) {
|
||||
if (existsSync(alt)) return alt;
|
||||
}
|
||||
|
||||
// Try which/where command
|
||||
try {
|
||||
const cmd = isWindows() ? 'where' : 'which';
|
||||
const result = execSync(`${cmd} ${config.id} 2>${isWindows() ? 'nul' : '/dev/null'}`)
|
||||
.toString().trim();
|
||||
if (result) return result.split('\n')[0];
|
||||
} catch {
|
||||
// not found via path
|
||||
// not found
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -175,19 +285,47 @@ function detectEmulatorPath(config: EmulatorConfig): string | undefined {
|
||||
|
||||
function alternativePaths(emulatorId: string): string[] {
|
||||
const home = require('os').homedir();
|
||||
const userData = app.getPath('userData');
|
||||
const omniEmuDir = join(userData, 'emulators', emulatorId);
|
||||
|
||||
const common: Record<string, string[]> = {
|
||||
dolphin: [
|
||||
join(omniEmuDir, 'Dolphin.exe'),
|
||||
join(omniEmuDir, 'Dolphin'),
|
||||
join(omniEmuDir, 'dolphin-emu'),
|
||||
join(home, 'Applications', 'Dolphin.app', 'Contents', 'MacOS', 'Dolphin'),
|
||||
'/usr/local/bin/dolphin-emu',
|
||||
'/snap/bin/dolphin-emu',
|
||||
],
|
||||
retroarch: [
|
||||
join(omniEmuDir, 'RetroArch.exe'),
|
||||
join(omniEmuDir, 'RetroArch'),
|
||||
join(omniEmuDir, 'retroarch'),
|
||||
join(home, 'Applications', 'RetroArch.app', 'Contents', 'MacOS', 'RetroArch'),
|
||||
'/usr/local/bin/retroarch',
|
||||
'/snap/bin/retroarch',
|
||||
],
|
||||
rpcs3: [
|
||||
join(omniEmuDir, 'rpcs3.exe'),
|
||||
join(omniEmuDir, 'rpcs3'),
|
||||
join(omniEmuDir, 'RPCS3.AppImage'),
|
||||
],
|
||||
ryujinx: [
|
||||
join(omniEmuDir, 'Ryujinx.exe'),
|
||||
join(omniEmuDir, 'Ryujinx'),
|
||||
],
|
||||
pcsx2: [
|
||||
join(omniEmuDir, 'pcsx2.exe'),
|
||||
join(omniEmuDir, 'PCSX2'),
|
||||
join(omniEmuDir, 'pcsx2.AppImage'),
|
||||
],
|
||||
mame: [
|
||||
join(omniEmuDir, 'mame64.exe'),
|
||||
join(omniEmuDir, 'mame'),
|
||||
join(omniEmuDir, 'MAME.AppImage'),
|
||||
],
|
||||
};
|
||||
return common[emulatorId] || [];
|
||||
return common[emulatorId] || [join(omniEmuDir)];
|
||||
}
|
||||
|
||||
export function checkEmulator(id: string): EmulatorState {
|
||||
@@ -195,14 +333,14 @@ export function checkEmulator(id: string): EmulatorState {
|
||||
if (!config) {
|
||||
return {
|
||||
installed: false,
|
||||
configured: false,
|
||||
config: {
|
||||
id,
|
||||
name: id,
|
||||
description: '',
|
||||
platforms: [],
|
||||
defaultPath: { win32: '', darwin: '', linux: '' },
|
||||
installUrl: { win32: null, darwin: null, linux: null },
|
||||
installVia: 'manual',
|
||||
downloads: {},
|
||||
supported: false,
|
||||
},
|
||||
};
|
||||
@@ -226,6 +364,7 @@ export function checkEmulator(id: string): EmulatorState {
|
||||
version,
|
||||
path,
|
||||
config,
|
||||
configured: !!path && existsSync(join(app.getPath('userData'), 'configs', `${id}.configured`)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -244,7 +383,7 @@ export function launchGame(emulatorId: string, romPath: string): ChildProcess |
|
||||
const cmd = `"${state.path}" ${args}`;
|
||||
|
||||
const proc = exec(cmd, {
|
||||
cwd: require('path').dirname(state.path),
|
||||
cwd: dirname(state.path),
|
||||
});
|
||||
|
||||
return proc;
|
||||
@@ -285,18 +424,12 @@ export function getEmulatorsDirectory(): string {
|
||||
const s = settings.get();
|
||||
if (s.emulatorsDirectory && existsSync(s.emulatorsDirectory)) return s.emulatorsDirectory;
|
||||
|
||||
const home = require('os').homedir();
|
||||
const defaultDir = join(home, 'OmniEmu', 'emulators');
|
||||
if (!existsSync(defaultDir)) {
|
||||
mkdirSync(defaultDir, { recursive: true });
|
||||
}
|
||||
return defaultDir;
|
||||
const dir = join(app.getPath('userData'), 'emulators');
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
export function scanRoms(directory: string): GameEntry[] {
|
||||
const { readdirSync, statSync } = require('fs');
|
||||
const { extname, basename, join: pathJoin } = require('path');
|
||||
|
||||
const romExtensions = [
|
||||
'.nes', '.sfc', '.smc', '.n64', '.z64', '.v64',
|
||||
'.gba', '.gb', '.gbc', '.nds', '.iso', '.bin', '.cue',
|
||||
@@ -309,10 +442,10 @@ export function scanRoms(directory: string): GameEntry[] {
|
||||
|
||||
function scanDir(dir: string) {
|
||||
try {
|
||||
const files = readdirSync(dir);
|
||||
const files = require('fs').readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = pathJoin(dir, file);
|
||||
const stat = statSync(fullPath);
|
||||
const fullPath = join(dir, file);
|
||||
const stat = require('fs').statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
scanDir(fullPath);
|
||||
} else {
|
||||
@@ -330,9 +463,7 @@ export function scanRoms(directory: string): GameEntry[] {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// skip unreadable dirs
|
||||
}
|
||||
} catch { /* skip unreadable */ }
|
||||
}
|
||||
|
||||
scanDir(directory);
|
||||
@@ -341,55 +472,29 @@ export function scanRoms(directory: string): GameEntry[] {
|
||||
|
||||
function guessPlatform(ext: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'.nes': 'nes',
|
||||
'.sfc': 'snes',
|
||||
'.smc': 'snes',
|
||||
'.n64': 'n64',
|
||||
'.z64': 'n64',
|
||||
'.v64': 'n64',
|
||||
'.gba': 'gba',
|
||||
'.gb': 'gb',
|
||||
'.gbc': 'gbc',
|
||||
'.nds': 'nds',
|
||||
'.iso': 'ps1',
|
||||
'.bin': 'ps1',
|
||||
'.cue': 'ps1',
|
||||
'.wbfs': 'wii',
|
||||
'.wad': 'wii',
|
||||
'.nsp': 'switch',
|
||||
'.xci': 'switch',
|
||||
'.pkg': 'ps3',
|
||||
'.chd': 'ps1',
|
||||
'.gcm': 'gc',
|
||||
'.gcz': 'gc',
|
||||
'.rvz': 'gc',
|
||||
'.ps2': 'ps2',
|
||||
'.cso': 'ps2',
|
||||
'.nes': 'nes', '.sfc': 'snes', '.smc': 'snes',
|
||||
'.n64': 'n64', '.z64': 'n64', '.v64': 'n64',
|
||||
'.gba': 'gba', '.gb': 'gb', '.gbc': 'gbc', '.nds': 'nds',
|
||||
'.iso': 'ps1', '.bin': 'ps1', '.cue': 'ps1',
|
||||
'.wbfs': 'wii', '.wad': 'wii',
|
||||
'.nsp': 'switch', '.xci': 'switch',
|
||||
'.pkg': 'ps3', '.chd': 'ps1',
|
||||
'.gcm': 'gc', '.gcz': 'gc', '.rvz': 'gc',
|
||||
'.ps2': 'ps2', '.cso': 'ps2',
|
||||
};
|
||||
return map[ext] || 'other';
|
||||
}
|
||||
|
||||
function guessEmulator(ext: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'.nes': 'retroarch',
|
||||
'.sfc': 'retroarch',
|
||||
'.smc': 'retroarch',
|
||||
'.n64': 'retroarch',
|
||||
'.z64': 'retroarch',
|
||||
'.v64': 'retroarch',
|
||||
'.gba': 'retroarch',
|
||||
'.gb': 'retroarch',
|
||||
'.gbc': 'retroarch',
|
||||
'.wbfs': 'dolphin',
|
||||
'.wad': 'dolphin',
|
||||
'.gcm': 'dolphin',
|
||||
'.gcz': 'dolphin',
|
||||
'.rvz': 'dolphin',
|
||||
'.nsp': 'ryujinx',
|
||||
'.xci': 'ryujinx',
|
||||
'.nes': 'retroarch', '.sfc': 'retroarch', '.smc': 'retroarch',
|
||||
'.n64': 'retroarch', '.z64': 'retroarch', '.v64': 'retroarch',
|
||||
'.gba': 'retroarch', '.gb': 'retroarch', '.gbc': 'retroarch',
|
||||
'.wbfs': 'dolphin', '.wad': 'dolphin', '.gcm': 'dolphin',
|
||||
'.gcz': 'dolphin', '.rvz': 'dolphin',
|
||||
'.nsp': 'ryujinx', '.xci': 'ryujinx',
|
||||
'.pkg': 'rpcs3',
|
||||
'.ps2': 'pcsx2',
|
||||
'.cso': 'pcsx2',
|
||||
'.ps2': 'pcsx2', '.cso': 'pcsx2',
|
||||
};
|
||||
return map[ext] || 'retroarch';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { createWriteStream, existsSync, mkdirSync, createReadStream } from 'fs';
|
||||
import { join, dirname, basename, extname } from 'path';
|
||||
import { get } from 'https';
|
||||
import { get as getHttp } from 'http';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { createGunzip, createBrotliDecompress, createInflate } from 'zlib';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { app } from 'electron';
|
||||
import { EmulatorDownload, InstallProgress, Platform, Arch } from '../shared/types';
|
||||
import { getPlatform, getArch, isWindows, isMacOS, isLinux } from './platform';
|
||||
|
||||
type ProgressCallback = (progress: InstallProgress) => void;
|
||||
|
||||
function downloadDir(): string {
|
||||
const d = join(app.getPath('userData'), 'downloads');
|
||||
if (!existsSync(d)) mkdirSync(d, { recursive: true });
|
||||
return d;
|
||||
}
|
||||
|
||||
function tempName(suffix: string): string {
|
||||
return join(downloadDir(), `${randomBytes(8).toString('hex')}${suffix}`);
|
||||
}
|
||||
|
||||
function downloadFile(url: string, dest: string, onProgress: (pct: number) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https') ? get : getHttp;
|
||||
protocol(url, (response) => {
|
||||
const code = response.statusCode ?? 500;
|
||||
if (code >= 300 && code < 400 && response.headers.location) {
|
||||
downloadFile(response.headers.location, dest, onProgress).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
if (code !== 200) {
|
||||
reject(new Error(`HTTP ${code} downloading ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const total = parseInt(response.headers['content-length'] ?? '0', 10);
|
||||
let downloaded = 0;
|
||||
const file = createWriteStream(dest);
|
||||
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
downloaded += chunk.length;
|
||||
if (total > 0) onProgress(Math.round((downloaded / total) * 100));
|
||||
});
|
||||
|
||||
pipeline(response, file)
|
||||
.then(() => {
|
||||
onProgress(100);
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function extractArchive(archivePath: string, destDir: string, format: string, emulatorId: string, onProgress: (msg: string) => void): void {
|
||||
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
||||
|
||||
onProgress(`Extracting ${basename(archivePath)}...`);
|
||||
|
||||
switch (format) {
|
||||
case 'zip': {
|
||||
execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: 'pipe' });
|
||||
break;
|
||||
}
|
||||
case 'tar.gz':
|
||||
case 'tar.bz2': {
|
||||
const decompress = format === 'tar.bz2' ? '-j' : '-z';
|
||||
execSync(`tar -x${decompress}f "${archivePath}" -C "${destDir}"`, { stdio: 'pipe' });
|
||||
break;
|
||||
}
|
||||
case '7z': {
|
||||
execSync(`7z x "${archivePath}" -o"${destDir}" -y`, { stdio: 'pipe' });
|
||||
break;
|
||||
}
|
||||
case 'dmg': {
|
||||
// Attach DMG and copy .app out
|
||||
const mountPoint = `/tmp/omniemu_${emulatorId}`;
|
||||
execSync(`hdiutil attach "${archivePath}" -mountpoint "${mountPoint}" -nobrowse -quiet`, { stdio: 'pipe' });
|
||||
execSync(`cp -R "${mountPoint}"/*.app "${destDir}/" 2>/dev/null; cp -R "${mountPoint}"/*/*.app "${destDir}/" 2>/dev/null; true`, { stdio: 'pipe' });
|
||||
execSync(`hdiutil detach "${mountPoint}" -quiet 2>/dev/null; true`, { stdio: 'pipe' });
|
||||
break;
|
||||
}
|
||||
case 'exe':
|
||||
case 'msi':
|
||||
case 'appimage':
|
||||
case 'pkg': {
|
||||
// These are direct installers, not archives
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown archive format: ${format}`);
|
||||
}
|
||||
|
||||
onProgress('Extraction complete');
|
||||
}
|
||||
|
||||
function runInstaller(installerPath: string, format: string, emulatorId: string, installDir: string, onProgress: (msg: string) => void): void {
|
||||
onProgress(`Running installer for ${emulatorId}...`);
|
||||
|
||||
switch (format) {
|
||||
case 'exe': {
|
||||
if (isWindows()) {
|
||||
execSync(`"${installerPath}" /S /D="${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||
} else {
|
||||
execSync(`chmod +x "${installerPath}" && "${installerPath}" -- "${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'msi': {
|
||||
execSync(`msiexec /i "${installerPath}" /quiet /norestart INSTALLDIR="${installDir}"`, { stdio: 'pipe', timeout: 300000 });
|
||||
break;
|
||||
}
|
||||
case 'appimage': {
|
||||
execSync(`chmod +x "${installerPath}"`, { stdio: 'pipe' });
|
||||
// Copy AppImage to install dir
|
||||
execSync(`cp "${installerPath}" "${installDir}/${emulatorId}.AppImage"`, { stdio: 'pipe' });
|
||||
break;
|
||||
}
|
||||
case 'pkg': {
|
||||
execSync(`installer -pkg "${installerPath}" -target /`, { stdio: 'pipe', timeout: 300000 });
|
||||
break;
|
||||
}
|
||||
case 'dmg': {
|
||||
// Already handled in extractArchive
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Cannot install format: ${format}`);
|
||||
}
|
||||
|
||||
onProgress('Installer finished');
|
||||
}
|
||||
|
||||
export type { ProgressCallback };
|
||||
|
||||
export async function installEmulator(
|
||||
emulatorId: string,
|
||||
downloads: EmulatorDownload[],
|
||||
platform: Platform,
|
||||
arch: Arch,
|
||||
onProgress: ProgressCallback
|
||||
): Promise<string> {
|
||||
// Pick the right download for this platform + arch
|
||||
const candidates = downloads.filter((d) => !d.arch || d.arch === arch);
|
||||
const download = candidates[0];
|
||||
if (!download) throw new Error(`No download available for ${platform}/${arch}`);
|
||||
|
||||
const report = (stage: InstallProgress['stage'], percent: number, message: string) => {
|
||||
onProgress({ emulatorId, stage, percent, message });
|
||||
};
|
||||
|
||||
report('downloading', 0, `Downloading ${download.url}...`);
|
||||
|
||||
const downloadPath = tempName(`.${download.format}`);
|
||||
await downloadFile(download.url, downloadPath, (pct) => {
|
||||
report('downloading', pct, `Downloading ${emulatorId}... ${pct}%`);
|
||||
});
|
||||
|
||||
const installDir = join(app.getPath('userData'), 'emulators', emulatorId);
|
||||
if (!existsSync(installDir)) mkdirSync(installDir, { recursive: true });
|
||||
|
||||
// Determine if this is an archive or an installer
|
||||
const archiveFormats = ['zip', 'tar.gz', 'tar.bz2', '7z', 'dmg'];
|
||||
const installerFormats = ['exe', 'msi', 'pkg', 'appimage'];
|
||||
|
||||
if (archiveFormats.includes(download.format)) {
|
||||
report('extracting', 0, `Extracting ${emulatorId}...`);
|
||||
extractArchive(downloadPath, installDir, download.format, emulatorId, (msg) => {
|
||||
report('extracting', 50, msg);
|
||||
});
|
||||
report('extracting', 100, 'Extraction complete');
|
||||
}
|
||||
|
||||
if (installerFormats.includes(download.format)) {
|
||||
report('installing', 0, `Installing ${emulatorId}...`);
|
||||
runInstaller(downloadPath, download.format, emulatorId, installDir, (msg) => {
|
||||
report('installing', 50, msg);
|
||||
});
|
||||
report('installing', 100, 'Installation complete');
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
try { execSync(`rm -f "${downloadPath}"`); } catch { /* ignore */ }
|
||||
|
||||
report('done', 100, `${emulatorId} installed successfully`);
|
||||
|
||||
return installDir;
|
||||
}
|
||||
+65
-11
@@ -1,16 +1,19 @@
|
||||
import { ipcMain, shell, dialog } from 'electron';
|
||||
import { ipcMain, shell, dialog, BrowserWindow } from 'electron';
|
||||
import {
|
||||
getAllEmulatorStates,
|
||||
checkEmulator,
|
||||
launchGame,
|
||||
scanRoms,
|
||||
knownEmulators,
|
||||
findEmulator,
|
||||
getRomsDirectory,
|
||||
getEmulatorsDirectory,
|
||||
} from './emulators';
|
||||
import { installEmulator } from './installer';
|
||||
import { applyRecommendedConfig, getPresets, checkConfigured } from './configurator';
|
||||
import { settings } from './settings';
|
||||
import { getSystemInfo, platformName } from './platform';
|
||||
import { GameEntry, AppSettings } from '../shared/types';
|
||||
import { getSystemInfo, platformName, getPlatform, getArch } from './platform';
|
||||
import { InstallProgress, AppSettings } from '../shared/types';
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
// System
|
||||
@@ -21,15 +24,66 @@ export function registerIpcHandlers(): void {
|
||||
ipcMain.handle('emulators:list', () => knownEmulators);
|
||||
ipcMain.handle('emulators:states', () => getAllEmulatorStates());
|
||||
ipcMain.handle('emulators:check', (_event, id: string) => checkEmulator(id));
|
||||
ipcMain.handle('emulators:install-url', (_event, id: string) => {
|
||||
const emu = knownEmulators.find((e) => e.id === id);
|
||||
if (emu?.installUrl) {
|
||||
const platform = getSystemInfo().platform;
|
||||
const url = emu.installUrl[platform];
|
||||
if (url) shell.openExternal(url);
|
||||
return url;
|
||||
|
||||
// Install emulator - direct download + install
|
||||
ipcMain.handle(
|
||||
'emulators:install',
|
||||
async (event, emulatorId: string) => {
|
||||
const config = findEmulator(emulatorId);
|
||||
if (!config) throw new Error(`Unknown emulator: ${emulatorId}`);
|
||||
if (!config.downloads) throw new Error(`No downloads configured for ${emulatorId}`);
|
||||
|
||||
const platform = getPlatform();
|
||||
const arch = getArch();
|
||||
const downloads = config.downloads[platform];
|
||||
if (!downloads || downloads.length === 0)
|
||||
throw new Error(`No downloads available for ${emulatorId} on ${platform}`);
|
||||
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
const sendProgress = (p: InstallProgress) => {
|
||||
win?.webContents.send('emulators:install-progress', p);
|
||||
};
|
||||
|
||||
await installEmulator(emulatorId, downloads, platform, arch, sendProgress);
|
||||
|
||||
// Re-check state after install
|
||||
return checkEmulator(emulatorId);
|
||||
}
|
||||
return null;
|
||||
);
|
||||
|
||||
// Configure emulator with recommended settings
|
||||
ipcMain.handle(
|
||||
'emulators:configure',
|
||||
async (_event, emulatorId: string, installPath: string) => {
|
||||
const success = await applyRecommendedConfig(emulatorId, installPath);
|
||||
return { success, state: checkEmulator(emulatorId) };
|
||||
}
|
||||
);
|
||||
|
||||
// Get available presets for an emulator
|
||||
ipcMain.handle(
|
||||
'emulators:presets',
|
||||
async (_event, emulatorId: string) => {
|
||||
return getPresets(emulatorId);
|
||||
}
|
||||
);
|
||||
|
||||
// Check if configured
|
||||
ipcMain.handle(
|
||||
'emulators:configured',
|
||||
(_event, emulatorId: string, installPath?: string) => {
|
||||
return checkConfigured(emulatorId, installPath);
|
||||
}
|
||||
);
|
||||
|
||||
// Open website (fallback for manual download)
|
||||
ipcMain.handle('emulators:open-website', (_event, id: string) => {
|
||||
const emu = findEmulator(id);
|
||||
if (emu?.websiteUrl?.[getPlatform()]) {
|
||||
shell.openExternal(emu.websiteUrl[getPlatform()]!);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// ROMs / Games
|
||||
|
||||
+30
-3
@@ -1,10 +1,12 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import {
|
||||
import type {
|
||||
EmulatorConfig,
|
||||
EmulatorState,
|
||||
GameEntry,
|
||||
SystemInfo,
|
||||
AppSettings,
|
||||
InstallProgress,
|
||||
ConfigPreset,
|
||||
} from '../shared/types';
|
||||
|
||||
const api = {
|
||||
@@ -18,8 +20,33 @@ const api = {
|
||||
states: (): Promise<EmulatorState[]> => ipcRenderer.invoke('emulators:states'),
|
||||
check: (id: string): Promise<EmulatorState> =>
|
||||
ipcRenderer.invoke('emulators:check', id),
|
||||
openInstallUrl: (id: string): Promise<string | null> =>
|
||||
ipcRenderer.invoke('emulators:install-url', id),
|
||||
|
||||
/** Download + install emulator directly. Returns final EmulatorState */
|
||||
install: (id: string): Promise<EmulatorState> =>
|
||||
ipcRenderer.invoke('emulators:install', id),
|
||||
|
||||
/** Apply recommended config preset to an installed emulator */
|
||||
configure: (id: string, installPath: string): Promise<{ success: boolean; state: EmulatorState }> =>
|
||||
ipcRenderer.invoke('emulators:configure', id, installPath),
|
||||
|
||||
/** Get available config presets for an emulator */
|
||||
presets: (id: string): Promise<ConfigPreset[]> =>
|
||||
ipcRenderer.invoke('emulators:presets', id),
|
||||
|
||||
/** Check if an emulator has been configured */
|
||||
configured: (id: string, installPath?: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke('emulators:configured', id, installPath),
|
||||
|
||||
/** Open the emulator's website in browser (fallback) */
|
||||
openWebsite: (id: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke('emulators:open-website', id),
|
||||
|
||||
/** Listen for install progress updates */
|
||||
onInstallProgress: (cb: (progress: InstallProgress) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, p: InstallProgress) => cb(p);
|
||||
ipcRenderer.on('emulators:install-progress', handler);
|
||||
return () => ipcRenderer.removeListener('emulators:install-progress', handler);
|
||||
},
|
||||
},
|
||||
|
||||
roms: {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { app } from 'electron';
|
||||
import { AppSettings } from '../shared/types';
|
||||
import { getPlatform } from './platform';
|
||||
|
||||
const SETTINGS_FILE = 'settings.json';
|
||||
|
||||
@@ -17,6 +16,7 @@ const defaultSettings: AppSettings = {
|
||||
minimiseToTray: true,
|
||||
launchInFullscreen: false,
|
||||
closeToTray: true,
|
||||
presetSourceUrl: 'https://raw.githubusercontent.com/mileswolfallen2/omniemu-presets/main/presets.json',
|
||||
};
|
||||
|
||||
let cached: AppSettings | null = null;
|
||||
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
export type Platform = 'win32' | 'darwin' | 'linux';
|
||||
export type Arch = 'x64' | 'arm64';
|
||||
export interface EmulatorConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
platforms: string[];
|
||||
defaultPath: Record<Platform, string>;
|
||||
installUrl: Record<Platform, string | null>;
|
||||
installVia: 'download' | 'brew' | 'apt' | 'winget' | 'manual';
|
||||
supported: boolean;
|
||||
}
|
||||
export interface RomFile {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
format: string;
|
||||
platform: string;
|
||||
lastPlayed?: string;
|
||||
playCount: number;
|
||||
}
|
||||
export interface GameEntry {
|
||||
id: string;
|
||||
romPath: string;
|
||||
title: string;
|
||||
platform: string;
|
||||
emulatorId: string;
|
||||
coverUrl?: string;
|
||||
lastPlayed?: string;
|
||||
playCount: number;
|
||||
addedAt: string;
|
||||
}
|
||||
export interface ScanResult {
|
||||
emulatorId: string;
|
||||
games: GameEntry[];
|
||||
}
|
||||
export interface AppSettings {
|
||||
romsDirectory: string;
|
||||
emulatorsDirectory: string;
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
minimiseToTray: boolean;
|
||||
launchInFullscreen: boolean;
|
||||
closeToTray: boolean;
|
||||
}
|
||||
export interface SystemInfo {
|
||||
platform: Platform;
|
||||
arch: Arch;
|
||||
homeDir: string;
|
||||
appDataDir: string;
|
||||
}
|
||||
export interface EmulatorState {
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
path?: string;
|
||||
config: EmulatorConfig;
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AACpD,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,OAAO,CAAC;AAEnC,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,UAAU,EAAE,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC9D,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IACnC,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;CACxB"}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=types.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}
|
||||
+45
-2
@@ -1,15 +1,55 @@
|
||||
export type Platform = 'win32' | 'darwin' | 'linux';
|
||||
export type Arch = 'x64' | 'arm64';
|
||||
|
||||
export interface EmulatorDownload {
|
||||
url: string;
|
||||
/** Archive type: 'exe' = direct installer, 'msi' = windows msi,
|
||||
* 'dmg' = macOS disk image, 'appimage' = linux appimage,
|
||||
* 'tar.gz' | 'tar.bz2' | 'zip' = archive to extract,
|
||||
* 'pkg' = macOS installer, '7z' = 7zip archive */
|
||||
format: string;
|
||||
/** Path inside archive to the executable (if archive format) */
|
||||
executablePath?: string;
|
||||
/** Expected file size in bytes (for validation) */
|
||||
size?: number;
|
||||
/** Only for this arch (omitted = all arches) */
|
||||
arch?: Arch;
|
||||
}
|
||||
|
||||
export interface EmulatorConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
platforms: string[];
|
||||
defaultPath: Record<Platform, string>;
|
||||
installUrl: Record<Platform, string | null>;
|
||||
installVia: 'download' | 'brew' | 'apt' | 'winget' | 'manual';
|
||||
/** Direct download sources per platform */
|
||||
downloads: Partial<Record<Platform, EmulatorDownload[]>>;
|
||||
/** Package manager names for auto-install via system pkg manager */
|
||||
packageNames?: Partial<Record<Platform, string>>;
|
||||
supported: boolean;
|
||||
/** URL to fetch recommended config presets from */
|
||||
presetUrl?: string;
|
||||
/** Website URL for manual/fallback */
|
||||
websiteUrl?: Record<Platform, string>;
|
||||
}
|
||||
|
||||
export interface InstallProgress {
|
||||
emulatorId: string;
|
||||
stage: 'downloading' | 'extracting' | 'installing' | 'configuring' | 'done' | 'error';
|
||||
percent: number;
|
||||
message: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ConfigPreset {
|
||||
/** Name of the preset (e.g. "Performance", "Quality", "Recommended") */
|
||||
name: string;
|
||||
/** Description of what this preset does */
|
||||
description: string;
|
||||
/** Platform-specific config files to write: relative path -> file content */
|
||||
files: Record<string, string>;
|
||||
/** Registry/settings changes for Windows */
|
||||
registry?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface RomFile {
|
||||
@@ -46,6 +86,8 @@ export interface AppSettings {
|
||||
minimiseToTray: boolean;
|
||||
launchInFullscreen: boolean;
|
||||
closeToTray: boolean;
|
||||
/** URL to fetch recommended config presets from */
|
||||
presetSourceUrl: string;
|
||||
}
|
||||
|
||||
export interface SystemInfo {
|
||||
@@ -60,4 +102,5 @@ export interface EmulatorState {
|
||||
version?: string;
|
||||
path?: string;
|
||||
config: EmulatorConfig;
|
||||
configured: boolean;
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "dist/main",
|
||||
"rootDir": "src/main",
|
||||
"moduleResolution": "node10",
|
||||
"outDir": "dist",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/main/**/*", "src/shared/**/*"]
|
||||
|
||||
Reference in New Issue
Block a user