mirror of
https://github.com/mileswolfallen2/OmniEmu2.0.git
synced 2026-09-08 11:23:17 +00:00
feat: release v0.2.0 with beta features toggle and new frontend support
- Added support for experimental emulators (ES-DE, NeoStation, Pegasus) behind a beta toggle in settings. - Implemented manual installation prompts for ES-DE and NeoStation on macOS. - Updated user agent to v0.1.3 across various modules. - Enhanced the Emulators page with conditional install buttons for beta emulators. - Introduced a new settings section for enabling/disabling beta features. - Updated release notes for v0.2.0 detailing new features and improvements. - Added project specification document for OmniEmu 2.0.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
# **OmniEmu v0.2.0 — Fourth Pre-Release**
|
||||
|
||||
Cross-platform emulator manager, game launcher, and ROM manager.
|
||||
|
||||
---
|
||||
|
||||
## **What's New**
|
||||
|
||||
### **Frontend Support**
|
||||
|
||||
OmniEmu now supports multiple emulation frontends — install and configure them alongside your emulators.
|
||||
|
||||
**ES-DE (EmulationStation Desktop Edition)** — available in Beta
|
||||
|
||||
- Multi-system frontend with a polished UI for browsing and launching games
|
||||
- Auto-configured with your ROM directory and installed emulators
|
||||
- Supports all 20+ systems
|
||||
- macOS: requires manual install (OmniEmu guides you through it)
|
||||
|
||||
**NeoStation** — available in Beta
|
||||
|
||||
- Modern Flutter-based frontend with ScreenScraper integration
|
||||
- Auto-discovers emulators and ROMs
|
||||
- RetroAchievements built in
|
||||
- macOS: requires manual install (OmniEmu guides you through it)
|
||||
|
||||
**EmuBuddy**
|
||||
|
||||
- Cross-platform frontend with automated setup and game downloading
|
||||
- One-click controller configuration
|
||||
- Supports 14 systems
|
||||
- Available on Windows, macOS, and Linux
|
||||
|
||||
**Pegasus Frontend** — available in Beta
|
||||
|
||||
- Lightweight, themeable game launcher
|
||||
- OmniEmu auto-generates collection files (`metadata.pegasus.txt`) in each ROM subdirectory
|
||||
- Writes Pegasus settings to point at your ROM library
|
||||
|
||||
---
|
||||
|
||||
### **Beta Features Toggle**
|
||||
|
||||
A new **Settings > Experimental** section lets you opt into beta features.
|
||||
|
||||
Enable **Beta Features** to unlock ES-DE, NeoStation, and Pegasus in the Emulators page. EmuBuddy is available without the toggle.
|
||||
|
||||
---
|
||||
|
||||
### **Pegasus Collection File Generation**
|
||||
|
||||
When Pegasus is configured (or on app startup with beta features enabled), OmniEmu now writes `metadata.pegasus.txt` files directly into each ROM subdirectory (e.g. `roms/nes/metadata.pegasus.txt`). This is where Pegasus discovers collections — not in a central config folder.
|
||||
|
||||
Each collection file includes:
|
||||
|
||||
- Collection name and shortname
|
||||
- File extensions for that system
|
||||
- Launch command pointing to the correct emulator
|
||||
- Individual game entries with filenames
|
||||
|
||||
---
|
||||
|
||||
### **ROM Library Auto-Scan**
|
||||
|
||||
The Library page now scans for games automatically on startup — no need to manually select a folder first. If your ROMs directory is set (defaults to `~/Documents/roms`), games appear immediately.
|
||||
|
||||
---
|
||||
|
||||
## **Bug Fixes & Improvements**
|
||||
|
||||
### **macOS Frontend Installation**
|
||||
|
||||
- ES-DE and NeoStation require manual DMG installation on macOS due to license dialogs
|
||||
- OmniEmu now shows a guided install flow: detects the DMG, opens it for you, then detects the installed app
|
||||
- **"Install Manually"** button in the Emulators page lets you skip the automated flow and install at your own pace
|
||||
|
||||
### **Emulator Detection**
|
||||
|
||||
- Added alternative install paths for all emulators (Homebrew, snap, Flatpak, common directories)
|
||||
- Improved binary discovery with recursive directory scanning
|
||||
|
||||
### **ROM Scanning**
|
||||
|
||||
- ROMs are automatically organized into the correct subdirectory on scan
|
||||
- Platform guessing improved: `.iso` files in a `ps2/` folder are classified as PS2, not PS1
|
||||
- Supported extensions now include `.cdi` (Dreamcast), `.pbp` (PSP), `.chd` (multiple systems)
|
||||
|
||||
---
|
||||
|
||||
## **Supported Emulators**
|
||||
|
||||
Emulator | Systems | Platforms
|
||||
--- | --- | ---
|
||||
Dolphin | GameCube, Wii | Win, macOS, Linux
|
||||
RPCS3 | PlayStation 3 | Win, Linux
|
||||
Eden | Nintendo Switch | Win, macOS, Linux
|
||||
PCSX2 | PlayStation 2 | Win, macOS, Linux
|
||||
MAME | Arcade | Win, Linux
|
||||
DuckStation | PlayStation 1 | Win, macOS, Linux
|
||||
RetroArch | NES, SNES, N64, GB, GBC, GBA, PS1, PCE, Sega MD, Sega Saturn, Dreamcast | Win, macOS, Linux
|
||||
PPSSPP | PlayStation Portable | Win, macOS, Linux
|
||||
melonDS | Nintendo DS | Win, macOS, Linux
|
||||
Flycast | Dreamcast, Naomi, Naomi 2, Atomiswave | Win, macOS, Linux
|
||||
|
||||
### **Frontends (Beta)**
|
||||
|
||||
Frontend | Type | Platforms
|
||||
--- | --- | ---
|
||||
ES-DE | Multi-system frontend | Win, macOS, Linux
|
||||
NeoStation | Flutter-based frontend | Win, macOS, Linux
|
||||
Pegasus | Themeable game launcher | Win, macOS, Linux
|
||||
EmuBuddy | Automated emulation frontend | Win, macOS, Linux
|
||||
|
||||
---
|
||||
|
||||
## **Auto-Update**
|
||||
|
||||
If you're downloading OmniEmu from GitHub, you can safely ignore the `.yml` files included with each release.
|
||||
|
||||
These are used internally by OmniEmu's built-in updater and are **not required** for manual installation.
|
||||
|
||||
Simply download the installer for your platform.
|
||||
|
||||
---
|
||||
|
||||
# **Installation**
|
||||
|
||||
## **macOS**
|
||||
|
||||
Download:
|
||||
|
||||
- `OmniEmu-0.2.0-mac-universal.dmg`
|
||||
|
||||
Install:
|
||||
|
||||
1. Drag **OmniEmu** into **Applications**.
|
||||
2. Right-click **OmniEmu** > **Open** on first launch.
|
||||
|
||||
If macOS blocks the app, run:
|
||||
|
||||
```bash
|
||||
sudo xattr -rd com.apple.quarantine /Applications/OmniEmu.app
|
||||
```
|
||||
|
||||
## **Windows**
|
||||
|
||||
Download:
|
||||
|
||||
- `OmniEmu-0.2.0-win-x64.exe` *(Intel / AMD)*
|
||||
- `OmniEmu-0.2.0-win-arm64.exe` *(ARM64)*
|
||||
|
||||
Portable ZIP builds are also available.
|
||||
|
||||
If Windows SmartScreen appears:
|
||||
|
||||
**More info > Run anyway**
|
||||
|
||||
## **Linux**
|
||||
|
||||
Download:
|
||||
|
||||
- `OmniEmu-0.2.0-linux-x86_64.AppImage`
|
||||
- `OmniEmu-0.2.0-linux-arm64.AppImage`
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
chmod +x OmniEmu*.AppImage
|
||||
./OmniEmu*.AppImage
|
||||
```
|
||||
|
||||
If the AppImage doesn't launch, install **libfuse2**.
|
||||
|
||||
---
|
||||
|
||||
## **Thanks for Testing!**
|
||||
|
||||
OmniEmu is still in **pre-release**, so bug reports, feature requests, and feedback are greatly appreciated.
|
||||
|
||||
Thank you for helping improve OmniEmu, and enjoy the update!
|
||||
@@ -0,0 +1,566 @@
|
||||
# OmniEmu 2.0 — Project Specification
|
||||
|
||||
> All your games. One place.
|
||||
|
||||
OmniEmu is a cross-platform desktop application for managing emulators, launching games, and organizing ROM libraries. Built with Electron, React, and TypeScript.
|
||||
|
||||
**Repository:** [github.com/mileswolfallen2/OmniEmu2.0](https://github.com/mileswolfallen2/OmniEmu2.0)
|
||||
**Version:** 0.2.0
|
||||
**License:** MIT (Copyright 2026 mileswa1q22)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Tech Stack](#tech-stack)
|
||||
2. [Licenses](#licenses)
|
||||
3. [Architecture](#architecture)
|
||||
4. [Supported Emulators](#supported-emulators)
|
||||
5. [Supported Frontends](#supported-frontends)
|
||||
6. [Supported Systems](#supported-systems)
|
||||
7. [ROM Format Support](#rom-format-support)
|
||||
8. [Configuration Presets](#configuration-presets)
|
||||
9. [IPC API Reference](#ipc-api-reference)
|
||||
10. [File Structure](#file-structure)
|
||||
11. [Build & Development](#build--development)
|
||||
12. [Release Artifacts](#release-artifacts)
|
||||
13. [Security](#security)
|
||||
14. [Contributing](#contributing)
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Technology | Version | Purpose | License |
|
||||
|---|---|---|---|
|
||||
| Electron | 43.1.0 | Desktop app shell | MIT |
|
||||
| React | 19.2.7 | UI framework | MIT |
|
||||
| React DOM | 19.2.7 | DOM rendering | MIT |
|
||||
| TypeScript | 5.9.3 | Type-safe language | Apache-2.0 |
|
||||
| Vite | 6.4.3 | Renderer bundler | MIT |
|
||||
| electron-builder | 25.1.8 | Packaging & distribution | MIT |
|
||||
| electron-updater | 6.8.9 | Auto-update support | MIT |
|
||||
| @vitejs/plugin-react | 4.7.0 | React Fast Refresh for Vite | MIT |
|
||||
| concurrently | 9.2.3 | Parallel dev scripts | MIT |
|
||||
| 7zip-bin | 5.2.0 | Archive extraction (7z) | MIT |
|
||||
|
||||
All dependencies are MIT-licensed except TypeScript (Apache-2.0).
|
||||
|
||||
---
|
||||
|
||||
## Licenses
|
||||
|
||||
### OmniEmu
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 mileswa1q22
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
### Third-Party Licenses
|
||||
|
||||
| Component | License | SPDX |
|
||||
|---|---|---|
|
||||
| Electron | MIT | MIT |
|
||||
| React | MIT | MIT |
|
||||
| React DOM | MIT | MIT |
|
||||
| TypeScript | Apache-2.0 | Apache-2.0 |
|
||||
| Vite | MIT | MIT |
|
||||
| electron-builder | MIT | MIT |
|
||||
| electron-updater | MIT | MIT |
|
||||
| @vitejs/plugin-react | MIT | MIT |
|
||||
| concurrently | MIT | MIT |
|
||||
| 7zip-bin | MIT | MIT |
|
||||
|
||||
### External Software (Not Bundled)
|
||||
|
||||
OmniEmu can install and configure the following third-party software. Each has its own license:
|
||||
|
||||
| Software | License | Website |
|
||||
|---|---|---|
|
||||
| RetroArch | GPLv3 | https://retroarch.com |
|
||||
| Dolphin | GPLv2+ | https://dolphin-emu.org |
|
||||
| RPCS3 | GPLv2 | https://rpcs3.net |
|
||||
| Eden | GPLv3 | https://eden-emu.dev |
|
||||
| PCSX2 | GPLv2+ | https://pcsx2.net |
|
||||
| MAME | GPLv2+ | https://mamedev.org |
|
||||
| DuckStation | GPLv3 | https://github.com/stenzek/duckstation |
|
||||
| PPSSPP | GPLv2+ | https://ppsspp.org |
|
||||
| melonDS | GPLv2 | https://melonds.kuribo64.net |
|
||||
| Flycast | GPLv2 | https://github.com/flyinghead/flycast |
|
||||
| ES-DE | GPLv3 | https://es-de.org |
|
||||
| NeoStation | MIT | https://neostation.dev |
|
||||
| Pegasus Frontend | MIT | https://pegasus-frontend.org |
|
||||
| EmuBuddy | Unknown | https://github.com/computerex/EmuBuddy |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
OmniEmu uses a standard Electron two-process architecture:
|
||||
|
||||
```
|
||||
+------------------+ IPC (ipcMain/ipcRenderer) +------------------+
|
||||
| | <---------------------------------> | |
|
||||
| Main Process | | Renderer Process |
|
||||
| (Node.js) | | (Chromium) |
|
||||
| | | |
|
||||
| - Emulator mgmt | | - React UI |
|
||||
| - ROM scanning | | - Pages |
|
||||
| - Installation | | - Components |
|
||||
| - Config writing | | - Gamepad nav |
|
||||
| - Auto-updater | | - Modals |
|
||||
| - Settings | | |
|
||||
| - RetroAchieve. | | |
|
||||
| - BIOS checks | | |
|
||||
+------------------+ +------------------+
|
||||
| |
|
||||
v v
|
||||
File system window.omni API
|
||||
(settings, configs, (contextBridge)
|
||||
ROMs, cache, emulators)
|
||||
```
|
||||
|
||||
### Main Process (`src/main/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `index.ts` | App entry point. Creates window (1200x800, min 900x600), system tray, single-instance lock, startup tasks |
|
||||
| `emulators.ts` | Core emulator registry. 14 emulators with download URLs, detection, version checking, ROM scanning, game launching |
|
||||
| `configurator.ts` | Configuration engine. Presets, config file writing, ES-DE XML generation, NeoStation JSON generation, Pegasus collection generation, controller bindings, RetroAchievements setup |
|
||||
| `ipc.ts` | IPC handler registry. 35+ channels exposed to the renderer |
|
||||
| `preload.ts` | Context bridge. Exposes `window.omni` API with typed wrappers |
|
||||
| `platform.ts` | Platform detection (`win32`/`darwin`/`linux`, `x64`/`arm64`) |
|
||||
| `settings.ts` | Settings persistence (`settings.json` in userData) |
|
||||
| `installer.ts` | Download, extract, and install emulators (zip, 7z, tar.gz, DMG, NSIS, AppImage, Homebrew, snap) |
|
||||
| `scraper.ts` | Cover art (libretro-thumbnails) and metadata (MobyGames) scraping with local cache |
|
||||
| `updater.ts` | Auto-update via electron-updater |
|
||||
| `bios.ts` | BIOS file detection and management |
|
||||
| `ra.ts` | RetroAchievements integration (game lookup, achievement data) |
|
||||
|
||||
### Renderer Process (`src/renderer/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `App.tsx` | Root component. Page routing, theme application, gamepad nav |
|
||||
| `main.tsx` | React entry point |
|
||||
| `index.html` | HTML shell with CSP |
|
||||
| `styles.css` | Complete CSS design system (dark/light themes) |
|
||||
| `types.ts` | `window.omni` type declarations |
|
||||
|
||||
### Pages
|
||||
|
||||
| Page | Purpose |
|
||||
|---|---|
|
||||
| `Dashboard.tsx` | System info, emulator overview, recent games, quick actions |
|
||||
| `EmulatorsPage.tsx` | Emulator cards with install/configure/launch/uninstall actions |
|
||||
| `LibraryPage.tsx` | ROM library grid with cover art, auto-scan |
|
||||
| `ControllerPage.tsx` | Gamepad detection, button states, controller config |
|
||||
| `SettingsPage.tsx` | Directories, BIOS, emulator assignments, appearance, beta features, updates, about |
|
||||
| `UtilitiesPage.tsx` | ROM folder structure, RetroAchievements credentials |
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `Sidebar.tsx` | Navigation sidebar (6 items + version) |
|
||||
| `BiosCheckPanel.tsx` | BIOS file scanner |
|
||||
| `GameDetailModal.tsx` | Game detail view with metadata, achievements, screenshots |
|
||||
| `ReleaseNotesModal.tsx` | Markdown release notes display |
|
||||
|
||||
### Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|---|---|
|
||||
| `useGamepadNav.ts` | 60fps gamepad polling, D-Pad/button navigation, controller detection |
|
||||
|
||||
---
|
||||
|
||||
## Supported Emulators
|
||||
|
||||
### Core Emulators
|
||||
|
||||
| Emulator | ID | Description | Systems | Download Formats |
|
||||
|---|---|---|---|---|
|
||||
| RetroArch | `retroarch` | Multi-system emulator frontend | NES, SNES, N64, GB, GBC, GBA, PS1, PCE, Sega MD, Sega Saturn, Dreamcast | 7z |
|
||||
| Dolphin | `dolphin` | GameCube & Wii emulator | GC, Wii | 7z, DMG, AppImage |
|
||||
| RPCS3 | `rpcs3` | PlayStation 3 emulator | PS3 | 7z, AppImage (no macOS) |
|
||||
| Eden | `eden` | Nintendo Switch emulator | Switch | ZIP, DMG, AppImage |
|
||||
| PCSX2 | `pcsx2` | PlayStation 2 emulator | PS2 | 7z, tar.xz, AppImage |
|
||||
| DuckStation | `duckstation` | PlayStation 1 emulator | PS1 | ZIP, AppImage |
|
||||
| MAME | `mame` | Multi Arcade Machine Emulator | Arcade | EXE, ZIP (no macOS download) |
|
||||
| PPSSPP | `ppsspp` | PlayStation Portable emulator | PSP | 7z, ZIP, AppImage |
|
||||
| melonDS | `melonds` | Nintendo DS emulator | NDS | ZIP, AppImage |
|
||||
| Flycast | `flycast` | Dreamcast/Naomi/Atomiswave emulator | Dreamcast | ZIP, AppImage |
|
||||
|
||||
### Frontends
|
||||
|
||||
| Frontend | ID | Description | Gated |
|
||||
|---|---|---|---|
|
||||
| ES-DE | `esde` | EmulationStation Desktop Edition | Beta |
|
||||
| NeoStation | `neostation` | Flutter-based frontend with ScreenScraper | Beta |
|
||||
| Pegasus | `pegasus` | Themeable game launcher | Beta |
|
||||
| EmuBuddy | `emubuddy` | Automated emulation frontend | No |
|
||||
|
||||
### Detection
|
||||
|
||||
OmniEmu detects installed emulators via:
|
||||
|
||||
1. Default hardcoded paths per platform
|
||||
2. Marker file (`userData/emulators/<id>/.installed`)
|
||||
3. Alternative common paths (Homebrew, snap, Flatpak, etc.)
|
||||
4. System `which`/`where` lookup
|
||||
|
||||
---
|
||||
|
||||
## Supported Systems
|
||||
|
||||
| System ID | Name | Primary Emulator | Alternatives |
|
||||
|---|---|---|---|
|
||||
| `nes` | Nintendo Entertainment System | RetroArch (Nestopia) | RetroArch (Mesen, FCEUX) |
|
||||
| `snes` | Super Nintendo | RetroArch (Snes9x) | RetroArch (bsnes HD) |
|
||||
| `n64` | Nintendo 64 | RetroArch (Mupen64Plus-Next) | RetroArch (Parallel) |
|
||||
| `gb` | Game Boy | RetroArch (Gambatte) | RetroArch (SameBoy) |
|
||||
| `gbc` | Game Boy Color | RetroArch (Gambatte) | RetroArch (SameBoy) |
|
||||
| `gba` | Game Boy Advance | RetroArch (mGBA) | RetroArch (VBA-M) |
|
||||
| `nds` | Nintendo DS | melonDS | RetroArch (melonDS) |
|
||||
| `ps1` | PlayStation 1 | DuckStation | RetroArch (SwanStation) |
|
||||
| `ps2` | PlayStation 2 | PCSX2 | — |
|
||||
| `ps3` | PlayStation 3 | RPCS3 | — |
|
||||
| `psp` | PlayStation Portable | PPSSPP | — |
|
||||
| `gc` | Nintendo GameCube | Dolphin | — |
|
||||
| `wii` | Nintendo Wii | Dolphin | — |
|
||||
| `switch` | Nintendo Switch | Eden | — |
|
||||
| `arcade` | Arcade | MAME | RetroArch (MAME) |
|
||||
| `dreamcast` | Sega Dreamcast | Flycast | RetroArch (Flycast) |
|
||||
| `sega-md` | Sega Mega Drive / Genesis | RetroArch (Genesis Plus GX) | — |
|
||||
| `sega-saturn` | Sega Saturn | RetroArch (Yaba Sanshiro) | — |
|
||||
| `pce` | PC Engine / TurboGrafx-16 | RetroArch (Mednafen PCE) | — |
|
||||
|
||||
---
|
||||
|
||||
## ROM Format Support
|
||||
|
||||
| Extension | System(s) |
|
||||
|---|---|
|
||||
| `.nes` | NES |
|
||||
| `.sfc`, `.smc` | SNES |
|
||||
| `.n64`, `.z64`, `.v64` | N64 |
|
||||
| `.gb` | Game Boy |
|
||||
| `.gbc` | Game Boy Color |
|
||||
| `.gba` | Game Boy Advance |
|
||||
| `.nds` | Nintendo DS |
|
||||
| `.iso`, `.bin`, `.cue` | PS1, PS2, PS3, PSP, GC, Wii, Saturn |
|
||||
| `.chd` | PS1, PS2, PSP, Dreamcast, Saturn |
|
||||
| `.pbp` | PSP |
|
||||
| `.cso` | PSP |
|
||||
| `.gcm`, `.gcz`, `.rvz` | GameCube |
|
||||
| `.wbfs`, `.wad` | Wii |
|
||||
| `.nsp`, `.xci` | Switch |
|
||||
| `.pkg` | PS3 |
|
||||
| `.m3u` | Multi-disc games |
|
||||
| `.zip`, `.7z` | Arcade (MAME), various |
|
||||
| `.gdi`, `.cdi` | Dreamcast |
|
||||
| `.rom` | Various |
|
||||
|
||||
Platform is guessed from file extension, with directory name as override (e.g. `.iso` in `ps2/` is classified as PS2).
|
||||
|
||||
---
|
||||
|
||||
## Configuration Presets
|
||||
|
||||
### Built-In Presets
|
||||
|
||||
| Emulator | Preset Name | Config Files |
|
||||
|---|---|---|
|
||||
| RetroArch | OmniEmu Recommended | `retroarch.cfg` |
|
||||
| RetroArch | OmniEmu Performance | `retroarch.cfg` |
|
||||
| Dolphin | OmniEmu Recommended | `Config/Dolphin.ini`, `Config/GFX.ini` |
|
||||
| RPCS3 | OmniEmu Recommended | `config.yml` |
|
||||
| Eden | OmniEmu Recommended | `Config.json` |
|
||||
| PCSX2 | OmniEmu Recommended | `inis/PCSX2.ini` |
|
||||
| DuckStation | OmniEmu Recommended | `settings.ini` |
|
||||
| MAME | OmniEmu Recommended | `mame.ini` |
|
||||
| melonDS | OmniEmu Recommended | `melonDS.ini` |
|
||||
| Flycast | OmniEmu Recommended | `emu.cfg` |
|
||||
| ES-DE | OmniEmu Recommended | Generates `es_settings.xml`, `es_find_rules.xml` |
|
||||
| NeoStation | OmniEmu Recommended | Generates `omniemu_config.json` |
|
||||
| Pegasus | OmniEmu Recommended | Generates `metadata.pegasus.txt` per ROM dir |
|
||||
|
||||
### Preset Loading Priority
|
||||
|
||||
1. Remote URL (`https://raw.githubusercontent.com/mileswolfallen2/OmniEmu2.0/main/presets.json`)
|
||||
2. Local file (`userData/presets.json`)
|
||||
3. Built-in presets (compiled into the app)
|
||||
|
||||
### Remote Preset Source
|
||||
|
||||
Configurable in **Settings > Presets**. Point to any JSON file following the `presets.json` schema.
|
||||
|
||||
---
|
||||
|
||||
## IPC API Reference
|
||||
|
||||
### System
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `system:info` | — | `SystemInfo` |
|
||||
| `system:platform-name` | — | `string` |
|
||||
|
||||
### Emulators
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `emulators:list` | — | `EmulatorConfig[]` |
|
||||
| `emulators:states` | — | `EmulatorState[]` |
|
||||
| `emulators:check` | `id: string` | `EmulatorState` |
|
||||
| `emulators:install` | `id: string` | `EmulatorState` |
|
||||
| `emulators:configure` | `id, installPath` | `{ success, state }` |
|
||||
| `emulators:presets` | `id: string` | `ConfigPreset[]` |
|
||||
| `emulators:configured` | `id, installPath?` | `boolean` |
|
||||
| `emulators:launch` | `id: string` | `boolean` |
|
||||
| `emulators:uninstall` | `id: string` | `{ removed, state }` |
|
||||
| `emulators:open-website` | `id: string` | `boolean` |
|
||||
| `emulators:system-assignments` | — | `Record<string, string[]>` |
|
||||
| `emulators:update-controller-config` | `id, installPath, controllerName?` | `boolean` |
|
||||
|
||||
### ROMs / Games
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `roms:scan` | `directory?: string` | `GameEntry[]` |
|
||||
| `roms:select-directory` | — | `string \| null` |
|
||||
| `game:launch` | `emulatorId, romPath` | `boolean` |
|
||||
| `games:recent` | — | `GameEntry[]` |
|
||||
| `games:clear-recent` | — | `boolean` |
|
||||
| `games:scrape-art` | `title, platform` | `string \| undefined` |
|
||||
| `games:cache-covers` | `{ romPath, coverUrl }[]` | `boolean` |
|
||||
| `games:scrape-metadata` | `romPath, title, platform` | `GameMetadata` |
|
||||
| `games:achievements` | `romPath, title, platform` | `AchievementInfo \| null` |
|
||||
|
||||
### Settings
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `settings:get` | — | `AppSettings` |
|
||||
| `settings:save` | `Partial<AppSettings>` | `AppSettings` |
|
||||
| `settings:reset` | — | `AppSettings` |
|
||||
|
||||
### BIOS
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `bios:list-known` | — | `BiosEntry[]` |
|
||||
| `bios:scan` | `directory?: string` | `BiosCheckResult[]` |
|
||||
| `bios:select-directory` | — | `string \| null` |
|
||||
| `bios:configure-retroarch` | `configDir, biosDir` | `boolean` |
|
||||
|
||||
### RetroAchievements
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `retroachievements:save` | `username, password` | `Record<string, boolean>` |
|
||||
|
||||
### Utilities
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `utilities:regenerate-roms-structure` | — | `boolean` |
|
||||
|
||||
### Updates
|
||||
|
||||
| Channel | Arguments | Returns |
|
||||
|---|---|---|
|
||||
| `updates:check` | — | `boolean` |
|
||||
| `updates:download` | — | `boolean` |
|
||||
| `updates:quit-and-install` | — | `boolean` |
|
||||
|
||||
### Outbound Events (Main -> Renderer)
|
||||
|
||||
| Event | Data |
|
||||
|---|---|
|
||||
| `emulators:install-progress` | `InstallProgress` |
|
||||
| `updates:status` | `{ status, version, releaseNotes, message, manualLink }` |
|
||||
| `updates:download-progress` | `{ percent, bytesPerSecond, transferred, total }` |
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
OmniEmu2.0/
|
||||
├── src/
|
||||
│ ├── main/ # Electron main process
|
||||
│ │ ├── index.ts # Entry point
|
||||
│ │ ├── emulators.ts # Emulator registry & ROM scanning
|
||||
│ │ ├── configurator.ts # Config presets & emulator setup
|
||||
│ │ ├── ipc.ts # IPC handler registration
|
||||
│ │ ├── preload.ts # Context bridge (window.omni)
|
||||
│ │ ├── platform.ts # OS/arch detection
|
||||
│ │ ├── settings.ts # Settings persistence
|
||||
│ │ ├── installer.ts # Download & install emulators
|
||||
│ │ ├── scraper.ts # Art & metadata scraping
|
||||
│ │ ├── updater.ts # Auto-update
|
||||
│ │ ├── bios.ts # BIOS detection
|
||||
│ │ └── ra.ts # RetroAchievements
|
||||
│ ├── renderer/ # React UI
|
||||
│ │ ├── App.tsx # Root component
|
||||
│ │ ├── main.tsx # React entry point
|
||||
│ │ ├── index.html # HTML shell
|
||||
│ │ ├── styles.css # Design system
|
||||
│ │ ├── types.ts # window.omni types
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ ├── components/ # Shared components
|
||||
│ │ ├── hooks/ # Custom hooks
|
||||
│ │ └── utils/ # Utilities (markdown)
|
||||
│ └── shared/
|
||||
│ └── types.ts # Shared TypeScript types
|
||||
├── assets/ # App icons
|
||||
├── scripts/ # Build & install scripts
|
||||
├── presets.json # Remote preset source
|
||||
├── package.json # Project config & electron-builder
|
||||
├── vite.config.ts # Vite build config
|
||||
├── tsconfig.json # Base TS config
|
||||
├── tsconfig.main.json # Main process TS config
|
||||
├── tsconfig.renderer.json # Renderer TS config
|
||||
├── LICENSE # MIT
|
||||
└── README.md # Project docs
|
||||
```
|
||||
|
||||
### Runtime Directories
|
||||
|
||||
| Path | Platform | Contents |
|
||||
|---|---|---|
|
||||
| `userData/settings.json` | All | App settings |
|
||||
| `userData/cache/scrape-cache.json` | All | Cover art cache |
|
||||
| `userData/cache/metadata-cache.json` | All | Game metadata cache |
|
||||
| `userData/cache/ra-game-list-cache.json` | All | RetroAchievements game lists |
|
||||
| `userData/emulators/<id>/.installed` | All | Installed emulator marker |
|
||||
| `userData/configs/` | All | Config state markers |
|
||||
| `~/Documents/roms/` | Default | ROM library root |
|
||||
| `~/Documents/roms/<system>/` | Default | System-specific ROM folders |
|
||||
|
||||
---
|
||||
|
||||
## Build & Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm
|
||||
- Python 3 (for icon generation)
|
||||
- Platform-specific tools:
|
||||
- macOS: Xcode Command Line Tools
|
||||
- Windows: NSIS (for installer)
|
||||
- Linux: libfuse2 (for AppImage)
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Development (hot reload)
|
||||
npm run dev
|
||||
|
||||
# Build renderer + main
|
||||
npm run build
|
||||
|
||||
# Type checking
|
||||
npm run typecheck
|
||||
|
||||
# Package for current platform
|
||||
npm run package:mac # macOS universal
|
||||
npm run package:win # Windows x64
|
||||
npm run package:linux # Linux x64
|
||||
|
||||
# Package all platforms
|
||||
npm run build:all # macOS universal + Win x64/arm64 + Linux x64/arm64
|
||||
```
|
||||
|
||||
### Build Configuration
|
||||
|
||||
- **App ID:** `com.omniemu.app`
|
||||
- **Product Name:** `OmniEmu`
|
||||
- **Output:** `release/`
|
||||
- **Renderer:** Vite builds to `dist/renderer/`
|
||||
- **Main:** TypeScript compiles to `dist/main/`
|
||||
- **Publish:** GitHub Releases
|
||||
|
||||
---
|
||||
|
||||
## Release Artifacts
|
||||
|
||||
| Platform | Format | Architecture | Filename Pattern |
|
||||
|---|---|---|---|
|
||||
| macOS | DMG | Universal (x64 + ARM64) | `OmniEmu-{ver}-mac-universal.dmg` |
|
||||
| macOS | ZIP | Universal | `OmniEmu-{ver}-mac-universal.zip` |
|
||||
| Windows | NSIS EXE | x64 | `OmniEmu-{ver}-win-x64.exe` |
|
||||
| Windows | NSIS EXE | ARM64 | `OmniEmu-{ver}-win-arm64.exe` |
|
||||
| Windows | ZIP | x64 | `OmniEmu-{ver}-win-x64.zip` |
|
||||
| Windows | ZIP | ARM64 | `OmniEmu-{ver}-win-arm64.zip` |
|
||||
| Linux | AppImage | x86_64 | `OmniEmu-{ver}-linux-x86_64.AppImage` |
|
||||
| Linux | AppImage | ARM64 | `OmniEmu-{ver}-linux-arm64.AppImage` |
|
||||
|
||||
`.yml` files in releases are for electron-updater and can be ignored for manual installs.
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
- **Context Isolation:** Enabled
|
||||
- **Node Integration:** Disabled
|
||||
- **Sandbox:** Disabled (required for native module access)
|
||||
- **CSP:** `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'`
|
||||
- **Single Instance:** Enforced via `app.requestSingleInstanceLock()`
|
||||
- **No secrets or API keys** are committed to the repository
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
### Code Style
|
||||
|
||||
- TypeScript strict mode
|
||||
- React functional components with hooks
|
||||
- No comments unless requested
|
||||
- Follow existing patterns and naming conventions
|
||||
|
||||
### Key Conventions
|
||||
|
||||
- Emulators are defined in `knownEmulators` array in `src/main/emulators.ts`
|
||||
- Config presets are in `builtInPresets` in `src/main/configurator.ts`
|
||||
- All IPC channels are registered in `src/main/ipc.ts`
|
||||
- All renderer API calls go through `window.omni` (defined in `src/main/preload.ts`)
|
||||
- Shared types are in `src/shared/types.ts`
|
||||
|
||||
### Testing Changes
|
||||
|
||||
1. Run `npm run typecheck` to verify types
|
||||
2. Run `npm run build` to verify compilation
|
||||
3. Run `npm run dev` to test in development mode
|
||||
4. Test on all target platforms if possible
|
||||
|
||||
---
|
||||
|
||||
*Last updated: July 2026*
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniemu2",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniemu2",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniemu2",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "Cross-platform emulator manager, game launcher and ROM manager",
|
||||
"main": "dist/main/index.js",
|
||||
"scripts": {
|
||||
@@ -44,7 +44,6 @@
|
||||
"build": {
|
||||
"appId": "com.omniemu.app",
|
||||
"productName": "OmniEmu",
|
||||
"afterPack": "./scripts/adhoc-sign.cjs",
|
||||
"publish": ["github"],
|
||||
"directories": {
|
||||
"output": "release",
|
||||
|
||||
@@ -96,5 +96,12 @@
|
||||
"emu.cfg": "[config]\nrenderer = vulkan\nfullscreen = yes\nvsync = yes\nauto_region = yes\ncable_type = vga\nbroadcast = ntsc\nframeskip = 0\n[network]\nenable = no\n[input]\nenable_mouse = no\n"
|
||||
}
|
||||
}
|
||||
],
|
||||
"esde": [
|
||||
{
|
||||
"name": "OmniEmu Recommended",
|
||||
"description": "Pre-configures ES-DE with your ROM directory and installed emulators",
|
||||
"files": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+432
-2
@@ -1,8 +1,10 @@
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, statSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { app } from 'electron';
|
||||
import { ConfigPreset, InstallProgress, Platform } from '../shared/types';
|
||||
import { getPlatform, isWindows, isMacOS } from './platform';
|
||||
import { knownEmulators, checkEmulator } from './emulators';
|
||||
import { settings } from './settings';
|
||||
|
||||
const platform = getPlatform();
|
||||
|
||||
@@ -256,6 +258,27 @@ volume = 100
|
||||
},
|
||||
},
|
||||
],
|
||||
esde: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Pre-configures ES-DE with your ROM directory and installed emulators',
|
||||
files: {},
|
||||
},
|
||||
],
|
||||
neostation: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Pre-configures NeoStation with your ROM directory and installed emulators',
|
||||
files: {},
|
||||
},
|
||||
],
|
||||
pegasus: [
|
||||
{
|
||||
name: 'OmniEmu Recommended',
|
||||
description: 'Generates Pegasus collection files for all your ROMs with correct launch commands',
|
||||
files: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ---- Remote presets URL ----
|
||||
@@ -267,7 +290,7 @@ async function fetchRemotePresets(): Promise<Record<string, ConfigPreset[]> | nu
|
||||
const { get } = await import('https');
|
||||
const data = await new Promise<string>((resolve, reject) => {
|
||||
get(presetSourceUrl, {
|
||||
headers: { 'User-Agent': 'OmniEmu/0.1.2' },
|
||||
headers: { 'User-Agent': 'OmniEmu/0.1.3' },
|
||||
timeout: 10000,
|
||||
}, (res) => {
|
||||
if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
|
||||
@@ -367,6 +390,21 @@ function getConfigDir(emulatorId: string, installPath: string): string {
|
||||
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'melonDS'),
|
||||
linux: join(require('os').homedir(), '.config', 'melonDS'),
|
||||
},
|
||||
esde: {
|
||||
win32: join(require('os').homedir(), 'ES-DE'),
|
||||
darwin: join(require('os').homedir(), 'ES-DE'),
|
||||
linux: join(require('os').homedir(), 'ES-DE'),
|
||||
},
|
||||
neostation: {
|
||||
win32: join(process.env.LOCALAPPDATA || join(require('os').homedir(), 'AppData', 'Local'), 'neostation'),
|
||||
darwin: join(require('os').homedir(), 'Library', 'Application Support', 'neostation'),
|
||||
linux: join(require('os').homedir(), '.local', 'share', 'neostation'),
|
||||
},
|
||||
pegasus: {
|
||||
win32: join(process.env.LOCALAPPDATA || join(require('os').homedir(), 'AppData', 'Local'), 'pegasus-frontend'),
|
||||
darwin: join(require('os').homedir(), 'Library', 'Preferences', 'pegasus-frontend'),
|
||||
linux: join(require('os').homedir(), '.config', 'pegasus-frontend'),
|
||||
},
|
||||
};
|
||||
return platformDirs[emulatorId]?.[platform] || dirname(installPath);
|
||||
}
|
||||
@@ -419,12 +457,404 @@ export async function applyRecommendedConfig(
|
||||
installPath: string,
|
||||
onProgress?: (p: InstallProgress) => void
|
||||
): Promise<boolean> {
|
||||
if (emulatorId === 'esde') {
|
||||
return configureEsde(installPath, onProgress);
|
||||
}
|
||||
if (emulatorId === 'neostation') {
|
||||
return configureNeostation(installPath, onProgress);
|
||||
}
|
||||
if (emulatorId === 'pegasus') {
|
||||
return configurePegasus(installPath, onProgress);
|
||||
}
|
||||
const presets = await getPresets(emulatorId);
|
||||
if (presets.length === 0) return false;
|
||||
await applyPreset(emulatorId, presets[0], installPath, onProgress);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- ES-DE Configuration ----
|
||||
|
||||
/** Map OmniEmu emulator IDs to ES-DE find-rules emulator names */
|
||||
const esdeEmulatorNames: Record<string, string> = {
|
||||
retroarch: 'RETROARCH',
|
||||
dolphin: 'DOLPHIN',
|
||||
rpcs3: 'RPCS3',
|
||||
pcsx2: 'PCSX2',
|
||||
duckstation: 'DUCKSTATION',
|
||||
mame: 'MAME',
|
||||
ppsspp: 'PPSSPP',
|
||||
melonds: 'MELONDS',
|
||||
flycast: 'FLYCAST',
|
||||
eden: 'EDEN',
|
||||
};
|
||||
|
||||
function getEsdeEmulatorName(emulatorId: string): string | undefined {
|
||||
return esdeEmulatorNames[emulatorId];
|
||||
}
|
||||
|
||||
function generateEsFindRules(): string {
|
||||
const lines: string[] = [
|
||||
'<?xml version="1.0"?>',
|
||||
'<!-- ES-DE find rules generated by OmniEmu -->',
|
||||
'<ruleList>',
|
||||
];
|
||||
|
||||
for (const emu of knownEmulators) {
|
||||
if (emu.id === 'esde') continue;
|
||||
const esdeName = getEsdeEmulatorName(emu.id);
|
||||
if (!esdeName) continue;
|
||||
|
||||
const state = checkEmulator(emu.id);
|
||||
if (!state.installed || !state.path) continue;
|
||||
|
||||
lines.push(` <emulator name="${esdeName}">`);
|
||||
lines.push(' <rule type="staticpath">');
|
||||
lines.push(` <entry>${state.path}</entry>`);
|
||||
lines.push(' </rule>');
|
||||
lines.push(' </emulator>');
|
||||
}
|
||||
|
||||
lines.push('</ruleList>');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function generateEsSettingsXml(romDir: string, mediaDir: string): string {
|
||||
return [
|
||||
'<?xml version="1.0"?>',
|
||||
'<!-- ES-DE settings generated by OmniEmu -->',
|
||||
'<viewSettings>',
|
||||
` <string name="ROMDirectory" value="${romDir}" />`,
|
||||
` <string name="MediaDirectory" value="${mediaDir}" />`,
|
||||
'</viewSettings>',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function configureEsde(
|
||||
installPath: string,
|
||||
onProgress?: (p: InstallProgress) => void
|
||||
): Promise<boolean> {
|
||||
const report = onProgress
|
||||
? (percent: number, message: string) =>
|
||||
onProgress({ emulatorId: 'esde', stage: 'configuring', percent, message })
|
||||
: () => {};
|
||||
|
||||
report(0, 'Configuring ES-DE...');
|
||||
|
||||
const esdeConfigDir = getConfigDir('esde', installPath);
|
||||
const settingsDir = join(esdeConfigDir, 'settings');
|
||||
const customSystemsDir = join(esdeConfigDir, 'custom_systems');
|
||||
|
||||
if (!existsSync(settingsDir)) mkdirSync(settingsDir, { recursive: true });
|
||||
if (!existsSync(customSystemsDir)) mkdirSync(customSystemsDir, { recursive: true });
|
||||
|
||||
report(25, 'Writing ES-DE settings...');
|
||||
|
||||
const romDir = settings.get().romsDirectory || join(require('os').homedir(), 'Documents', 'roms');
|
||||
const mediaDir = join(esdeConfigDir, 'downloaded_media');
|
||||
|
||||
const settingsXml = generateEsSettingsXml(romDir, mediaDir);
|
||||
writeFileSync(join(settingsDir, 'es_settings.xml'), settingsXml, 'utf-8');
|
||||
|
||||
report(50, 'Writing emulator find rules...');
|
||||
|
||||
const findRules = generateEsFindRules();
|
||||
writeFileSync(join(customSystemsDir, 'es_find_rules.xml'), findRules, 'utf-8');
|
||||
|
||||
report(75, 'Creating media directory...');
|
||||
|
||||
if (!existsSync(mediaDir)) mkdirSync(mediaDir, { recursive: true });
|
||||
|
||||
report(100, 'ES-DE configured');
|
||||
|
||||
const markerDir = join(app.getPath('userData'), 'configs');
|
||||
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
||||
writeFileSync(join(markerDir, 'esde.configured'), new Date().toISOString(), 'utf-8');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- NeoStation Configuration ----
|
||||
|
||||
function generateNeostationConfig(romDir: string): string {
|
||||
const emulatorsDir = settings.get().emulatorsDirectory || join(app.getPath('userData'), 'emulators');
|
||||
|
||||
// Build emulator paths map from installed emulators
|
||||
const emulatorPaths: Record<string, string> = {};
|
||||
for (const emu of knownEmulators) {
|
||||
if (emu.id === 'neostation' || emu.id === 'esde') continue;
|
||||
const state = checkEmulator(emu.id);
|
||||
if (state.installed && state.path) {
|
||||
emulatorPaths[emu.id] = state.path;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
roms_directory: romDir,
|
||||
emulators_directory: emulatorsDir,
|
||||
emulators: emulatorPaths,
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
async function configureNeostation(
|
||||
installPath: string,
|
||||
onProgress?: (p: InstallProgress) => void
|
||||
): Promise<boolean> {
|
||||
const report = onProgress
|
||||
? (percent: number, message: string) =>
|
||||
onProgress({ emulatorId: 'neostation', stage: 'configuring', percent, message })
|
||||
: () => {};
|
||||
|
||||
report(0, 'Configuring NeoStation...');
|
||||
|
||||
const configDir = getConfigDir('neostation', installPath);
|
||||
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
|
||||
|
||||
report(25, 'Writing NeoStation configuration...');
|
||||
|
||||
const romDir = settings.get().romsDirectory || join(require('os').homedir(), 'Documents', 'roms');
|
||||
const configContent = generateNeostationConfig(romDir);
|
||||
writeFileSync(join(configDir, 'omniemu_config.json'), configContent, 'utf-8');
|
||||
|
||||
report(50, 'Discovering installed emulators...');
|
||||
|
||||
// Create symlinks to ROM directories for NeoStation to discover
|
||||
const romsLink = join(configDir, 'roms');
|
||||
try {
|
||||
if (!existsSync(romsLink)) {
|
||||
const { symlinkSync } = require('fs');
|
||||
symlinkSync(romDir, romsLink);
|
||||
}
|
||||
} catch { /* ignore if symlink fails */ }
|
||||
|
||||
report(75, 'Finalizing...');
|
||||
|
||||
report(100, 'NeoStation configured');
|
||||
|
||||
const markerDir = join(app.getPath('userData'), 'configs');
|
||||
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
||||
writeFileSync(join(markerDir, 'neostation.configured'), new Date().toISOString(), 'utf-8');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Pegasus Frontend Configuration ----
|
||||
|
||||
interface PegasusSystemMeta {
|
||||
name: string;
|
||||
shortname: string;
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
const pegasusSystemMeta: Record<string, PegasusSystemMeta> = {
|
||||
nes: { name: 'Nintendo Entertainment System', shortname: 'nes', extensions: ['nes', 'fds'] },
|
||||
snes: { name: 'Super Nintendo Entertainment System', shortname: 'snes', extensions: ['smc', 'sfc', 'fig', 'swc'] },
|
||||
n64: { name: 'Nintendo 64', shortname: 'n64', extensions: ['n64', 'z64', 'v64'] },
|
||||
gb: { name: 'Game Boy', shortname: 'gb', extensions: ['gb'] },
|
||||
gbc: { name: 'Game Boy Color', shortname: 'gbc', extensions: ['gbc'] },
|
||||
gba: { name: 'Game Boy Advance', shortname: 'gba', extensions: ['gba'] },
|
||||
nds: { name: 'Nintendo DS', shortname: 'nds', extensions: ['nds'] },
|
||||
ps1: { name: 'PlayStation', shortname: 'psx', extensions: ['bin', 'cue', 'iso', 'chd', 'pbp'] },
|
||||
ps2: { name: 'PlayStation 2', shortname: 'ps2', extensions: ['iso', 'bin', 'chd', 'gz'] },
|
||||
ps3: { name: 'PlayStation 3', shortname: 'ps3', extensions: ['iso'] },
|
||||
psp: { name: 'PlayStation Portable', shortname: 'psp', extensions: ['iso', 'cso', 'pbp'] },
|
||||
gc: { name: 'GameCube', shortname: 'gamecube', extensions: ['gcm', 'gcz', 'iso', 'rvz'] },
|
||||
wii: { name: 'Wii', shortname: 'wii', extensions: ['wbfs', 'iso', 'gcz', 'rvz'] },
|
||||
switch: { name: 'Nintendo Switch', shortname: 'switch', extensions: ['nsp', 'xci'] },
|
||||
arcade: { name: 'Arcade', shortname: 'arcade', extensions: ['zip', '7z'] },
|
||||
dreamcast: { name: 'Sega Dreamcast', shortname: 'dreamcast', extensions: ['gdi', 'cdi', 'chd'] },
|
||||
'sega-md': { name: 'Sega Mega Drive', shortname: 'genesis', extensions: ['bin', 'md', 'smd'] },
|
||||
'sega-saturn': { name: 'Sega Saturn', shortname: 'saturn', extensions: ['bin', 'cue', 'chd'] },
|
||||
pce: { name: 'PC Engine', shortname: 'pce', extensions: ['pce'] },
|
||||
};
|
||||
|
||||
function getPegasusLaunchCommand(emulatorPath: string, emulatorId: string): string {
|
||||
const fp = '{file.path}';
|
||||
const esc = (s: string) => `"${s}"`;
|
||||
switch (emulatorId) {
|
||||
case 'retroarch': {
|
||||
const core = isWindows() ? '_libretro.dll' : '_libretro.so';
|
||||
return esc(emulatorPath) + ' -L ' + esc('{core_dir}/' + core) + ' ' + esc(fp);
|
||||
}
|
||||
case 'dolphin': return esc(emulatorPath) + ' --exec=' + esc(fp);
|
||||
case 'rpcs3': return esc(emulatorPath) + ' ' + esc(fp);
|
||||
case 'pcsx2': return esc(emulatorPath) + ' ' + esc(fp);
|
||||
case 'duckstation':return esc(emulatorPath) + ' ' + esc(fp);
|
||||
case 'mame': return esc(emulatorPath) + ' ' + esc(fp);
|
||||
case 'flycast': return esc(emulatorPath) + ' ' + esc(fp);
|
||||
case 'melonDS': return esc(emulatorPath) + ' ' + esc(fp);
|
||||
case 'eden': return esc(emulatorPath) + ' ' + esc(fp);
|
||||
case 'ppsspp': return esc(emulatorPath) + ' ' + esc(fp);
|
||||
default: return esc(emulatorPath) + ' ' + esc(fp);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePegasusCollection(systemId: string, romDir: string, launchCmd: string): string {
|
||||
const meta = pegasusSystemMeta[systemId];
|
||||
if (!meta) return '';
|
||||
|
||||
const systemDir = join(romDir, systemId);
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`collection: ${meta.name}`);
|
||||
lines.push(`shortname: ${meta.shortname}`);
|
||||
lines.push(`extensions: ${meta.extensions.join(', ')}`);
|
||||
lines.push(`launch: ${launchCmd}`);
|
||||
lines.push('');
|
||||
|
||||
// Scan for ROM files in this system directory
|
||||
if (existsSync(systemDir)) {
|
||||
try {
|
||||
const files = readdirSync(systemDir);
|
||||
for (const file of files) {
|
||||
const filePath = join(systemDir, file);
|
||||
const stat = statSync(filePath);
|
||||
if (!stat.isFile()) continue;
|
||||
|
||||
const ext = file.split('.').pop()?.toLowerCase() || '';
|
||||
if (!meta.extensions.includes(ext)) continue;
|
||||
|
||||
const title = file.replace(/\.[^/.]+$/, '').replace(/ \(.*\)$/, '');
|
||||
|
||||
lines.push(`game: ${title}`);
|
||||
lines.push(`file: ${file}`);
|
||||
lines.push(`launch: ${launchCmd}`);
|
||||
lines.push('');
|
||||
}
|
||||
} catch { /* skip unreadable */ }
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Generate Pegasus collection files in each ROM subdirectory. Called on app startup. */
|
||||
export function generatePegasusCollectionsForRomDir(romDir: string): void {
|
||||
if (!existsSync(romDir)) return;
|
||||
|
||||
const emuPaths: Record<string, string> = {};
|
||||
for (const emu of knownEmulators) {
|
||||
if (emu.id === 'pegasus' || emu.id === 'esde' || emu.id === 'neostation' || emu.id === 'emubuddy') continue;
|
||||
const state = checkEmulator(emu.id);
|
||||
if (state.installed && state.path) {
|
||||
emuPaths[emu.id] = state.path;
|
||||
}
|
||||
}
|
||||
const defaultEmu = emuPaths['retroarch'] || Object.values(emuPaths)[0] || 'retroarch';
|
||||
|
||||
const emuPrefs: Record<string, string> = {
|
||||
nes: 'retroarch', snes: 'retroarch', n64: 'retroarch',
|
||||
gb: 'retroarch', gbc: 'retroarch', gba: 'retroarch', nds: 'retroarch',
|
||||
ps1: 'duckstation', ps2: 'pcsx2', psp: 'ppsspp',
|
||||
gc: 'dolphin', wii: 'dolphin', switch: 'eden',
|
||||
arcade: 'mame', dreamcast: 'flycast',
|
||||
'sega-md': 'retroarch', 'sega-saturn': 'retroarch', pce: 'retroarch',
|
||||
};
|
||||
|
||||
for (const [systemId, meta] of Object.entries(pegasusSystemMeta)) {
|
||||
const systemDir = join(romDir, systemId);
|
||||
if (!existsSync(systemDir)) continue;
|
||||
|
||||
const preferredEmu = emuPrefs[systemId] || 'retroarch';
|
||||
const emuPath = emuPaths[preferredEmu] || defaultEmu;
|
||||
const launchCmd = getPegasusLaunchCommand(emuPath, preferredEmu);
|
||||
|
||||
const collection = generatePegasusCollection(systemId, romDir, launchCmd);
|
||||
if (!collection) continue;
|
||||
|
||||
writeFileSync(join(systemDir, 'metadata.pegasus.txt'), collection, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
async function configurePegasus(
|
||||
installPath: string,
|
||||
onProgress?: (p: InstallProgress) => void
|
||||
): Promise<boolean> {
|
||||
const report = onProgress
|
||||
? (percent: number, message: string) =>
|
||||
onProgress({ emulatorId: 'pegasus', stage: 'configuring', percent, message })
|
||||
: () => {};
|
||||
|
||||
report(0, 'Configuring Pegasus Frontend...');
|
||||
|
||||
const configDir = getConfigDir('pegasus', installPath);
|
||||
const metafilesDir = join(configDir, 'metafiles');
|
||||
if (!existsSync(metafilesDir)) mkdirSync(metafilesDir, { recursive: true });
|
||||
|
||||
report(10, 'Scanning ROM directories...');
|
||||
|
||||
const romDir = settings.get().romsDirectory || join(require('os').homedir(), 'Documents', 'roms');
|
||||
|
||||
// Get installed emulator paths
|
||||
const emuPaths: Record<string, string> = {};
|
||||
for (const emu of knownEmulators) {
|
||||
if (emu.id === 'pegasus' || emu.id === 'esde' || emu.id === 'neostation' || emu.id === 'emubuddy') continue;
|
||||
const state = checkEmulator(emu.id);
|
||||
if (state.installed && state.path) {
|
||||
emuPaths[emu.id] = state.path;
|
||||
}
|
||||
}
|
||||
const defaultEmu = emuPaths['retroarch'] || Object.values(emuPaths)[0] || 'retroarch';
|
||||
|
||||
const emuPrefs: Record<string, string> = {
|
||||
nes: 'retroarch', snes: 'retroarch', n64: 'retroarch',
|
||||
gb: 'retroarch', gbc: 'retroarch', gba: 'retroarch', nds: 'retroarch',
|
||||
ps1: 'duckstation', ps2: 'pcsx2', psp: 'ppsspp',
|
||||
gc: 'dolphin', wii: 'dolphin', switch: 'eden',
|
||||
arcade: 'mame', dreamcast: 'flycast',
|
||||
'sega-md': 'retroarch', 'sega-saturn': 'retroarch', pce: 'retroarch',
|
||||
};
|
||||
|
||||
report(25, 'Writing collection files to ROM directories...');
|
||||
|
||||
let systemsProcessed = 0;
|
||||
const totalSystems = Object.keys(pegasusSystemMeta).length;
|
||||
|
||||
// Write a metadata.pegasus.txt directly into each ROM subdirectory
|
||||
for (const [systemId, meta] of Object.entries(pegasusSystemMeta)) {
|
||||
systemsProcessed++;
|
||||
report(25 + Math.round((systemsProcessed / totalSystems) * 50),
|
||||
`Processing ${meta.name}...`);
|
||||
|
||||
const systemDir = join(romDir, systemId);
|
||||
if (!existsSync(systemDir)) continue;
|
||||
|
||||
// Pick the best emulator for this system
|
||||
const preferredEmu = emuPrefs[systemId] || 'retroarch';
|
||||
const emuPath = emuPaths[preferredEmu] || defaultEmu;
|
||||
const launchCmd = getPegasusLaunchCommand(emuPath, preferredEmu);
|
||||
|
||||
// Generate collection content for this system
|
||||
const collection = generatePegasusCollection(systemId, romDir, launchCmd);
|
||||
if (!collection) continue;
|
||||
|
||||
// Write metadata.pegasus.txt directly into the ROM subdirectory
|
||||
writeFileSync(join(systemDir, 'metadata.pegasus.txt'), collection, 'utf-8');
|
||||
|
||||
// Also write a copy to Pegasus's central metafiles dir as backup
|
||||
writeFileSync(join(metafilesDir, `${systemId}.pegasus.txt`), collection, 'utf-8');
|
||||
}
|
||||
|
||||
report(80, 'Writing Pegasus settings...');
|
||||
|
||||
// Point Pegasus at the ROMs directory
|
||||
const settingsFile = join(configDir, 'settings.txt');
|
||||
const settingsContent = [
|
||||
'# Pegasus settings generated by OmniEmu',
|
||||
'game_dirs:',
|
||||
` ${romDir}`,
|
||||
].join('\n');
|
||||
writeFileSync(settingsFile, settingsContent, 'utf-8');
|
||||
|
||||
report(100, 'Pegasus Frontend configured');
|
||||
|
||||
const markerDir = join(app.getPath('userData'), 'configs');
|
||||
if (!existsSync(markerDir)) mkdirSync(markerDir, { recursive: true });
|
||||
writeFileSync(join(markerDir, 'pegasus.configured'), new Date().toISOString(), 'utf-8');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Controller Configuration ----
|
||||
|
||||
/** Write controller config for a given emulator — PS-aware */
|
||||
export function applyControllerConfig(emulatorId: string, installPath: string, controllerName?: string): boolean {
|
||||
const configDir = getConfigDir(emulatorId, installPath);
|
||||
|
||||
+229
-1
@@ -436,6 +436,186 @@ export const knownEmulators: EmulatorConfig[] = [
|
||||
linux: 'https://github.com/flyinghead/flycast/releases',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'esde',
|
||||
name: 'ES-DE',
|
||||
description: 'EmulationStation Desktop Edition — multi-system frontend & game launcher',
|
||||
platforms: [
|
||||
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
|
||||
'ps1', 'ps2', 'ps3', 'psp', 'gc', 'wii', 'switch',
|
||||
'arcade', 'dreamcast', 'sega-md', 'sega-saturn', 'sega-dc', 'pce',
|
||||
],
|
||||
defaultPath: {
|
||||
win32: 'C:\\Program Files\\ES-DE\\esde.exe',
|
||||
darwin: '/Applications/ES-DE.app/Contents/MacOS/esde',
|
||||
linux: '/usr/bin/es-de',
|
||||
},
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288156889/download',
|
||||
format: 'exe',
|
||||
installerType: 'nsis',
|
||||
executablePath: 'esde.exe',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288889626/download',
|
||||
format: 'dmg',
|
||||
arch: 'arm64',
|
||||
},
|
||||
{
|
||||
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288889701/download',
|
||||
format: 'dmg',
|
||||
arch: 'x64',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288156961/download',
|
||||
format: 'appimage',
|
||||
arch: 'x64',
|
||||
},
|
||||
{
|
||||
url: 'https://gitlab.com/es-de/emulationstation-de/-/package_files/288156935/download',
|
||||
format: 'appimage',
|
||||
arch: 'arm64',
|
||||
},
|
||||
],
|
||||
},
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://es-de.org/download/',
|
||||
darwin: 'https://es-de.org/download/',
|
||||
linux: 'https://es-de.org/download/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'neostation',
|
||||
name: 'NeoStation',
|
||||
description: 'Modern multi-platform emulation frontend — auto-discovers emulators and ROMs',
|
||||
platforms: [
|
||||
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
|
||||
'ps1', 'ps2', 'ps3', 'psp', 'gc', 'wii', 'switch',
|
||||
'arcade', 'dreamcast', 'sega-md', 'sega-saturn', 'sega-dc', 'pce',
|
||||
],
|
||||
defaultPath: {
|
||||
win32: 'C:\\Program Files\\NeoStation\\neostation.exe',
|
||||
darwin: '/Applications/NeoStation.app/Contents/MacOS/neostation',
|
||||
linux: '/usr/bin/neostation',
|
||||
},
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://github.com/misobadev/neostation-frontend/releases/download/v0.9.3%2B114/neostation-windows-x64-0.9.3+114.zip',
|
||||
format: 'zip',
|
||||
executablePath: 'neostation.exe',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://github.com/misobadev/neostation-frontend/releases/download/v0.9.3%2B114/neostation-macos-universal-0.9.3+114.dmg',
|
||||
format: 'dmg',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://github.com/misobadev/neostation-frontend/releases/download/v0.9.3%2B114/neostation-linux-x86_64-0.9.3+114.AppImage',
|
||||
format: 'appimage',
|
||||
},
|
||||
],
|
||||
},
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://neostation.dev/downloads/',
|
||||
darwin: 'https://neostation.dev/downloads/',
|
||||
linux: 'https://neostation.dev/downloads/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'emubuddy',
|
||||
name: 'EmuBuddy',
|
||||
description: 'Cross-platform emulation frontend — one-click setup, game downloading, controller support',
|
||||
platforms: [
|
||||
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
|
||||
'ps1', 'ps2', 'psp', 'gc', 'wii', 'dreamcast', 'sega-saturn',
|
||||
],
|
||||
defaultPath: {
|
||||
win32: 'C:\\Program Files\\EmuBuddy\\EmuBuddyLauncher.exe',
|
||||
darwin: '/Applications/EmuBuddy.app/Contents/MacOS/EmuBuddy',
|
||||
linux: '/usr/bin/emubuddy',
|
||||
},
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://github.com/computerex/EmuBuddy/releases/download/1.0.0/EmuBuddy-Windows-v1.0.0.zip',
|
||||
format: 'zip',
|
||||
executablePath: 'EmuBuddyLauncher.exe',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://github.com/computerex/EmuBuddy/releases/download/1.0.0/EmuBuddy-macOS-v1.0.0.zip',
|
||||
format: 'zip',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://github.com/computerex/EmuBuddy/releases/download/1.0.0/EmuBuddy-Linux-v1.0.0.zip',
|
||||
format: 'zip',
|
||||
},
|
||||
],
|
||||
},
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://github.com/computerex/EmuBuddy',
|
||||
darwin: 'https://github.com/computerex/EmuBuddy',
|
||||
linux: 'https://github.com/computerex/EmuBuddy',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pegasus',
|
||||
name: 'Pegasus Frontend',
|
||||
description: 'Cross-platform customizable game launcher with theme support',
|
||||
platforms: [
|
||||
'nes', 'snes', 'n64', 'gb', 'gbc', 'gba', 'nds',
|
||||
'ps1', 'ps2', 'ps3', 'psp', 'gc', 'wii', 'switch',
|
||||
'arcade', 'dreamcast', 'sega-md', 'sega-saturn', 'sega-dc', 'pce',
|
||||
],
|
||||
defaultPath: {
|
||||
win32: 'C:\\Program Files\\Pegasus\\pegasus-fe.exe',
|
||||
darwin: '/Applications/Pegasus.app/Contents/MacOS/pegasus-fe',
|
||||
linux: '/usr/bin/pegasus-fe',
|
||||
},
|
||||
downloads: {
|
||||
win32: [
|
||||
{
|
||||
url: 'https://github.com/mmatyas/pegasus-frontend/releases/download/weekly_2024w38/pegasus-fe_alpha16-82-gc3462e68_win-mingw-static.zip',
|
||||
format: 'zip',
|
||||
executablePath: 'pegasus-fe.exe',
|
||||
},
|
||||
],
|
||||
darwin: [
|
||||
{
|
||||
url: 'https://github.com/mmatyas/pegasus-frontend/releases/download/weekly_2024w38/pegasus-fe_alpha16-82-gc3462e68_macos-static.zip',
|
||||
format: 'zip',
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
url: 'https://github.com/mmatyas/pegasus-frontend/releases/download/weekly_2024w38/pegasus-fe_alpha16-82-gc3462e68_x11-static.zip',
|
||||
format: 'zip',
|
||||
},
|
||||
],
|
||||
},
|
||||
supported: true,
|
||||
websiteUrl: {
|
||||
win32: 'https://pegasus-frontend.org/download/',
|
||||
darwin: 'https://pegasus-frontend.org/download/',
|
||||
linux: 'https://pegasus-frontend.org/download/',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function findEmulator(id: string): EmulatorConfig | undefined {
|
||||
@@ -525,6 +705,42 @@ function alternativePaths(emulatorId: string): string[] {
|
||||
join(omniEmuDir, 'flycast'),
|
||||
join(omniEmuDir, 'Flycast.app', 'Contents', 'MacOS', 'Flycast'),
|
||||
join(home, 'Applications', 'Flycast.app', 'Contents', 'MacOS', 'Flycast'),
|
||||
'/usr/local/bin/flycast',
|
||||
],
|
||||
esde: [
|
||||
join(omniEmuDir, 'esde.exe'),
|
||||
join(omniEmuDir, 'ES-DE', 'esde.exe'),
|
||||
join(omniEmuDir, 'es-de'),
|
||||
join(omniEmuDir, 'ES-DE.app', 'Contents', 'MacOS', 'esde'),
|
||||
join(home, 'Applications', 'ES-DE.app', 'Contents', 'MacOS', 'esde'),
|
||||
'/usr/local/bin/es-de',
|
||||
'/usr/bin/es-de',
|
||||
],
|
||||
neostation: [
|
||||
join(omniEmuDir, 'neostation.exe'),
|
||||
join(omniEmuDir, 'NeoStation', 'neostation.exe'),
|
||||
join(omniEmuDir, 'neostation'),
|
||||
join(omniEmuDir, 'NeoStation.app', 'Contents', 'MacOS', 'neostation'),
|
||||
join(home, 'Applications', 'NeoStation.app', 'Contents', 'MacOS', 'neostation'),
|
||||
'/usr/local/bin/neostation',
|
||||
'/usr/bin/neostation',
|
||||
],
|
||||
emubuddy: [
|
||||
join(omniEmuDir, 'EmuBuddyLauncher.exe'),
|
||||
join(omniEmuDir, 'EmuBuddy', 'EmuBuddyLauncher.exe'),
|
||||
join(omniEmuDir, 'EmuBuddyLauncher'),
|
||||
join(omniEmuDir, 'EmuBuddy.app', 'Contents', 'MacOS', 'EmuBuddy'),
|
||||
join(home, 'Applications', 'EmuBuddy.app', 'Contents', 'MacOS', 'EmuBuddy'),
|
||||
'/usr/local/bin/emubuddy',
|
||||
'/usr/bin/emubuddy',
|
||||
],
|
||||
pegasus: [
|
||||
join(omniEmuDir, 'pegasus-fe.exe'),
|
||||
join(omniEmuDir, 'pegasus-fe'),
|
||||
join(omniEmuDir, 'Pegasus.app', 'Contents', 'MacOS', 'pegasus-fe'),
|
||||
join(home, 'Applications', 'Pegasus.app', 'Contents', 'MacOS', 'pegasus-fe'),
|
||||
'/usr/local/bin/pegasus-fe',
|
||||
'/usr/bin/pegasus-fe',
|
||||
],
|
||||
melonds: [
|
||||
join(omniEmuDir, 'melonDS.exe'),
|
||||
@@ -597,7 +813,11 @@ export function checkEmulator(id: string): EmulatorState {
|
||||
}
|
||||
|
||||
export function getAllEmulatorStates(): EmulatorState[] {
|
||||
return knownEmulators.map((e) => checkEmulator(e.id));
|
||||
const s = settings.get();
|
||||
const beta = !!s.betaFeatures;
|
||||
return knownEmulators
|
||||
.filter(e => (e.id === 'esde' || e.id === 'neostation' || e.id === 'pegasus') ? beta : true)
|
||||
.map((e) => checkEmulator(e.id));
|
||||
}
|
||||
|
||||
export function launchEmulator(emulatorId: string): boolean {
|
||||
@@ -697,6 +917,14 @@ function launchArgs(emulatorId: string, romPath: string): string {
|
||||
return `"${romPath}"`;
|
||||
case 'flycast':
|
||||
return `"${romPath}"`;
|
||||
case 'esde':
|
||||
return '';
|
||||
case 'neostation':
|
||||
return '';
|
||||
case 'emubuddy':
|
||||
return '';
|
||||
case 'pegasus':
|
||||
return '';
|
||||
default:
|
||||
return `"${romPath}"`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { settings } from './settings';
|
||||
import { isMacOS } from './platform';
|
||||
import { ensureRomsStructure } from './emulators';
|
||||
import { setupAutoUpdater } from './updater';
|
||||
import { generatePegasusCollectionsForRomDir } from './configurator';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let tray: Tray | null = null;
|
||||
@@ -123,6 +124,14 @@ app.whenReady().then(() => {
|
||||
createWindow();
|
||||
createTray();
|
||||
|
||||
// Auto-generate Pegasus collection files if Pegasus is configured
|
||||
try {
|
||||
const s = settings.get();
|
||||
if (s.betaFeatures && s.romsDirectory) {
|
||||
generatePegasusCollectionsForRomDir(s.romsDirectory);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
else mainWindow?.show();
|
||||
|
||||
+54
-9
@@ -26,7 +26,7 @@ function downloadFile(url: string, dest: string, onProgress: (pct: number) => vo
|
||||
const doRequest = (currentUrl: string) => {
|
||||
const protocol = currentUrl.startsWith('https') ? httpsGet : httpGet;
|
||||
const opts: RequestOptions = {
|
||||
headers: { 'User-Agent': 'OmniEmu/0.1.2' },
|
||||
headers: { 'User-Agent': 'OmniEmu/0.1.3' },
|
||||
timeout: 30000,
|
||||
};
|
||||
protocol(currentUrl, opts, (response) => {
|
||||
@@ -85,14 +85,18 @@ function findAppInDir(dir: string): string | undefined {
|
||||
|
||||
for (const e of dirs) {
|
||||
if (e.name.endsWith('.app')) {
|
||||
const macosBin = join(dir, e.name, 'Contents', 'MacOS', basename(e.name, '.app'));
|
||||
if (existsSync(macosBin)) return macosBin;
|
||||
const macosDir = join(dir, e.name, 'Contents', 'MacOS');
|
||||
if (existsSync(macosDir)) {
|
||||
const candidates = readdirSync(macosDir);
|
||||
const bin = candidates.find(b => statSync(join(macosDir, b)).mode & 0o111);
|
||||
if (bin) return join(macosDir, bin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of files) {
|
||||
const lowerName = e.name.toLowerCase();
|
||||
const knownNames = ['retroarch', 'dolphin-emu', 'dolphin', 'rpcs3', 'ryujinx', 'eden', 'mame', 'mame64', 'pcsx2', 'duckstation'];
|
||||
const knownNames = ['retroarch', 'dolphin-emu', 'dolphin', 'rpcs3', 'ryujinx', 'eden', 'mame', 'mame64', 'pcsx2', 'duckstation', 'esde'];
|
||||
const isExec = e.name.endsWith('.exe') || e.name.endsWith('.AppImage')
|
||||
|| knownNames.includes(lowerName);
|
||||
if (isExec) return join(dir, e.name);
|
||||
@@ -152,18 +156,56 @@ async function extractArchive(
|
||||
}
|
||||
case 'dmg': {
|
||||
if (!isMacOS()) throw new Error('DMG files can only be extracted on macOS');
|
||||
const mountPoint = `/tmp/omniemu_${emulatorId}_${randomBytes(4).toString('hex')}`;
|
||||
onProgress('Opening DMG (please accept the license in the dialog)...');
|
||||
execSync(`open "${archivePath}"`);
|
||||
let mountPoint = '';
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
try {
|
||||
const lines = execSync('ls /Volumes/').toString().split('\n').filter(Boolean);
|
||||
for (const vol of lines) {
|
||||
const volPath = join('/Volumes', vol);
|
||||
if (existsSync(join(volPath, 'ES-DE.app')) || existsSync(join(volPath, '..app'))) {
|
||||
mountPoint = volPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mountPoint) {
|
||||
for (const vol of lines) {
|
||||
const volPath = join('/Volumes', vol);
|
||||
if (statSync(volPath).isDirectory() && vol.toLowerCase().includes('es-de')) {
|
||||
mountPoint = volPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mountPoint) {
|
||||
for (const vol of lines) {
|
||||
if (vol === 'Macintosh HD' || vol.startsWith('Recovery') || vol === 'OpenCode' || vol.startsWith('com.apple')) continue;
|
||||
const volPath = join('/Volumes', vol);
|
||||
try {
|
||||
const entries = readdirSync(volPath);
|
||||
if (entries.some(e => e.endsWith('.app'))) {
|
||||
mountPoint = volPath;
|
||||
break;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
if (mountPoint) break;
|
||||
} catch { /* retry */ }
|
||||
}
|
||||
if (!mountPoint) throw new Error('DMG did not mount within 60 seconds. Please accept the license dialog and try again.');
|
||||
try {
|
||||
execOrThrow(`hdiutil attach "${archivePath}" -mountpoint "${mountPoint}" -nobrowse -quiet`);
|
||||
const result = execSync(`ls "${mountPoint}" 2>/dev/null || true`).toString();
|
||||
for (const item of result.split('\n').filter(Boolean)) {
|
||||
const entries = readdirSync(mountPoint);
|
||||
for (const item of entries) {
|
||||
const src = join(mountPoint, item);
|
||||
if (item.endsWith('.app') || (statSync(src).isDirectory() && !item.startsWith('.'))) {
|
||||
execOrThrow(`cp -R "${src}" "${destDir}/"`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
execOrThrow(`hdiutil detach "${mountPoint}" -quiet 2>/dev/null; true`);
|
||||
try { execSync(`hdiutil detach "${mountPoint}" -quiet 2>/dev/null`); } catch { /* ignore */ }
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -344,6 +386,9 @@ export function findInstalledBinary(emulatorId: string, installDir: string, exec
|
||||
if (emulatorId === 'retroarch') {
|
||||
appNames.unshift('RetroArch.app');
|
||||
}
|
||||
if (emulatorId === 'esde') {
|
||||
appNames.unshift('ES-DE.app');
|
||||
}
|
||||
|
||||
for (const appName of appNames) {
|
||||
const appDir = join(installDir, appName);
|
||||
|
||||
@@ -33,7 +33,11 @@ export function registerIpcHandlers(): void {
|
||||
// Emulators
|
||||
ipcMain.handle('emulators:list', () => {
|
||||
const plat = getPlatform();
|
||||
const s = settings.get();
|
||||
const beta = !!s.betaFeatures;
|
||||
return knownEmulators.filter(e => {
|
||||
// Gate beta emulators behind beta toggle
|
||||
if ((e.id === 'esde' || e.id === 'neostation' || e.id === 'pegasus') && !beta) return false;
|
||||
const hasDownload = e.downloads?.[plat] && e.downloads[plat]!.length > 0;
|
||||
const hasPackage = !!e.packageNames?.[plat];
|
||||
return hasDownload || hasPackage;
|
||||
@@ -49,6 +53,39 @@ export function registerIpcHandlers(): void {
|
||||
async (event, emulatorId: string) => {
|
||||
const config = findEmulator(emulatorId);
|
||||
if (!config) throw new Error(`Unknown emulator: ${emulatorId}`);
|
||||
|
||||
// ES-DE on macOS: manual install (DMG requires license acceptance in Finder)
|
||||
if (emulatorId === 'esde' && getPlatform() === 'darwin') {
|
||||
const result = await dialog.showMessageBox(BrowserWindow.fromWebContents(event.sender)!, {
|
||||
type: 'info',
|
||||
title: 'Install ES-DE',
|
||||
message: 'ES-DE must be installed manually on macOS',
|
||||
detail: 'OmniEmu will open the ES-DE download page. Please download and install ES-DE, then return to OmniEmu and click Configure.',
|
||||
buttons: ['Open Download Page', 'Cancel'],
|
||||
defaultId: 0,
|
||||
});
|
||||
if (result.response === 0) {
|
||||
shell.openExternal('https://es-de.org/#Download');
|
||||
}
|
||||
return checkEmulator(emulatorId);
|
||||
}
|
||||
|
||||
// NeoStation on macOS: manual install (DMG requires license acceptance in Finder)
|
||||
if (emulatorId === 'neostation' && getPlatform() === 'darwin') {
|
||||
const result = await dialog.showMessageBox(BrowserWindow.fromWebContents(event.sender)!, {
|
||||
type: 'info',
|
||||
title: 'Install NeoStation',
|
||||
message: 'NeoStation must be installed manually on macOS',
|
||||
detail: 'OmniEmu will open the NeoStation download page. Please download and install NeoStation, then return to OmniEmu and click Configure.',
|
||||
buttons: ['Open Download Page', 'Cancel'],
|
||||
defaultId: 0,
|
||||
});
|
||||
if (result.response === 0) {
|
||||
shell.openExternal('https://neostation.dev/downloads/');
|
||||
}
|
||||
return checkEmulator(emulatorId);
|
||||
}
|
||||
|
||||
if (!config.downloads) throw new Error(`No downloads configured for ${emulatorId}`);
|
||||
|
||||
const platform = getPlatform();
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ const raConsoleIds: Record<string, number> = {
|
||||
dreamcast: 27, arcade: 99,
|
||||
};
|
||||
|
||||
const userAgent = 'OmniEmu/0.1.2';
|
||||
const userAgent = 'OmniEmu/0.1.3';
|
||||
|
||||
function fetchText(url: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
+3
-3
@@ -120,7 +120,7 @@ export async function findValidThumbnail(title: string, platform: string): Promi
|
||||
|
||||
function fetchText(url: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = httpsGet(url, { headers: { 'User-Agent': 'OmniEmu/0.1.2' }, timeout: 10000 }, (res) => {
|
||||
const req = httpsGet(url, { headers: { 'User-Agent': 'OmniEmu/0.1.3' }, timeout: 10000 }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: string) => data += chunk);
|
||||
res.on('end', () => resolve(data));
|
||||
@@ -134,7 +134,7 @@ function urlExists(url: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const req = httpsGet(url, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': 'OmniEmu/0.1.2' },
|
||||
headers: { 'User-Agent': 'OmniEmu/0.1.3' },
|
||||
timeout: 10000,
|
||||
}, (res) => {
|
||||
// GitHub raw returns 200 for existing or redirects to it
|
||||
@@ -238,7 +238,7 @@ export function cacheMetadata(romPath: string, metadata: GameMetadata): void {
|
||||
writeMetadataCache(cache);
|
||||
}
|
||||
|
||||
const mobygamesUA = 'Mozilla/5.0 (compatible; OmniEmu/0.1.2; +https://github.com/mileswolfallen2/OmniEmu2.0)';
|
||||
const mobygamesUA = 'Mozilla/5.0 (compatible; OmniEmu/0.1.3; +https://github.com/mileswolfallen2/OmniEmu2.0)';
|
||||
|
||||
function mobyFetch(url: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -36,7 +36,7 @@ export function Sidebar({ currentPage, onNavigate }: SidebarProps) {
|
||||
))}
|
||||
</nav>
|
||||
<div className="sidebar-footer">
|
||||
v0.1.2
|
||||
v0.1.3
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ export function useGamepadNav(onNavigate: (page: Page) => void, currentPage: Pag
|
||||
const lastTime = useRef<Record<string, number>>({});
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [showLegend, setShowLegend] = useState(false);
|
||||
const legendTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const legendTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const showLegendTemporarily = useCallback(() => {
|
||||
setShowLegend(true);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { EmulatorState, SystemInfo, GameEntry } from '../../shared/types';
|
||||
|
||||
export function Dashboard({ onNavigate }: { onNavigate?: (tab: string) => void }) {
|
||||
export function Dashboard({ onNavigate }: { onNavigate?: (tab: 'dashboard' | 'emulators' | 'library' | 'settings' | 'controller' | 'utilities') => void }) {
|
||||
const [emulators, setEmulators] = useState<EmulatorState[]>([]);
|
||||
const [system, setSystem] = useState<SystemInfo | null>(null);
|
||||
const [recent, setRecent] = useState<GameEntry[]>([]);
|
||||
|
||||
@@ -5,6 +5,8 @@ interface ProgressMap {
|
||||
[emulatorId: string]: InstallProgress;
|
||||
}
|
||||
|
||||
const isMacOS = navigator.userAgent.includes('Mac');
|
||||
|
||||
export function EmulatorsPage() {
|
||||
const [states, setStates] = useState<EmulatorState[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -209,20 +211,40 @@ export function EmulatorsPage() {
|
||||
>
|
||||
{!state.installed && state.config.supported && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstallAndConfigure(state.config.id)}
|
||||
>
|
||||
{isActioning ? 'Working...' : 'Install & Configure'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstall(state.config.id)}
|
||||
>
|
||||
Install Only
|
||||
</button>
|
||||
{state.config.id === 'esde' && isMacOS ? (
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstall(state.config.id)}
|
||||
>
|
||||
{isActioning ? 'Working...' : 'Install Manually'}
|
||||
</button>
|
||||
) : state.config.id === 'neostation' && isMacOS ? (
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstall(state.config.id)}
|
||||
>
|
||||
{isActioning ? 'Working...' : 'Install Manually'}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstallAndConfigure(state.config.id)}
|
||||
>
|
||||
{isActioning ? 'Working...' : 'Install & Configure'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
disabled={isActioning}
|
||||
onClick={() => handleInstall(state.config.id)}
|
||||
>
|
||||
Install Only
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -224,6 +224,27 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Experimental</h3>
|
||||
|
||||
<div className="setting-row">
|
||||
<div>
|
||||
<div className="setting-label">Enable Beta Features</div>
|
||||
<div className="setting-desc">
|
||||
Unlocks experimental emulators and frontends (ES-DE, NeoStation)
|
||||
</div>
|
||||
</div>
|
||||
<label className="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!settings.betaFeatures}
|
||||
onChange={(e) => update({ betaFeatures: e.target.checked })}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Presets</h3>
|
||||
|
||||
@@ -390,7 +411,7 @@ export function SettingsPage() {
|
||||
<div className="settings-section">
|
||||
<h3>About</h3>
|
||||
<p className="text-sm text-muted">
|
||||
OmniEmu v{updateInfo?.version || '0.1.2'} · Cross-platform emulator manager
|
||||
OmniEmu v{updateInfo?.version || '0.1.3'} · Cross-platform emulator manager
|
||||
<br />
|
||||
Built with Electron + React + TypeScript
|
||||
</p>
|
||||
|
||||
@@ -19,13 +19,17 @@ declare global {
|
||||
};
|
||||
emulators: {
|
||||
list: () => Promise<EmulatorConfig[]>;
|
||||
systemAssignments: () => Promise<Record<string, string[]>>;
|
||||
states: () => Promise<EmulatorState[]>;
|
||||
check: (id: string) => Promise<EmulatorState>;
|
||||
install: (id: string) => Promise<EmulatorState>;
|
||||
launch: (id: string) => Promise<boolean>;
|
||||
uninstall: (id: string) => Promise<{ removed: boolean; state: EmulatorState }>;
|
||||
configure: (id: string, installPath: string) => Promise<{ success: boolean; state: EmulatorState }>;
|
||||
presets: (id: string) => Promise<ConfigPreset[]>;
|
||||
configured: (id: string, installPath?: string) => Promise<boolean>;
|
||||
openWebsite: (id: string) => Promise<boolean>;
|
||||
updateControllerConfig: (id: string, installPath: string, controllerName?: string) => Promise<boolean>;
|
||||
onInstallProgress: (cb: (p: InstallProgress) => void) => () => void;
|
||||
};
|
||||
roms: {
|
||||
@@ -34,11 +38,19 @@ declare global {
|
||||
};
|
||||
game: {
|
||||
launch: (emulatorId: string, romPath: string) => Promise<boolean>;
|
||||
recent: () => Promise<GameEntry[]>;
|
||||
clearRecent: () => Promise<boolean>;
|
||||
scrapeArt: (title: string, platform: string) => Promise<string | undefined>;
|
||||
cacheCovers: (entries: { romPath: string; coverUrl: string }[]) => Promise<boolean>;
|
||||
scrapeMetadata: (romPath: string, title: string, platform: string) => Promise<GameMetadata>;
|
||||
achievements: (romPath: string, title: string, platform: string) => Promise<AchievementInfo | null>;
|
||||
};
|
||||
bios: {
|
||||
listKnown: () => Promise<any[]>;
|
||||
scan: (directory?: string) => Promise<any[]>;
|
||||
selectDirectory: () => Promise<string | null>;
|
||||
configureRetroArch: (configDir: string, biosDir: string) => Promise<boolean>;
|
||||
};
|
||||
settings: {
|
||||
get: () => Promise<AppSettings>;
|
||||
save: (s: Partial<AppSettings>) => Promise<AppSettings>;
|
||||
@@ -48,6 +60,19 @@ declare global {
|
||||
romsDirectory: () => Promise<string>;
|
||||
emulatorsDirectory: () => Promise<string>;
|
||||
};
|
||||
utilities: {
|
||||
regenerateRomsStructure: () => Promise<boolean>;
|
||||
};
|
||||
retroachievements: {
|
||||
save: (username: string, password: string) => Promise<Record<string, boolean>>;
|
||||
};
|
||||
updates: {
|
||||
check: () => Promise<boolean>;
|
||||
download: () => Promise<boolean>;
|
||||
quitAndInstall: () => Promise<boolean>;
|
||||
onStatus: (cb: (status: Record<string, unknown>) => void) => () => void;
|
||||
onDownloadProgress: (cb: (progress: Record<string, unknown>) => void) => () => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,8 @@ export interface AppSettings {
|
||||
retroAchievementsPassword?: string;
|
||||
/** RetroAchievements Web API Key (from retroachievements.org/settings) */
|
||||
retroAchievementsApiKey?: string;
|
||||
/** Enable experimental/beta features (ES-DE, NeoStation, etc.) */
|
||||
betaFeatures?: boolean;
|
||||
}
|
||||
|
||||
export interface RetroAchievement {
|
||||
|
||||
Reference in New Issue
Block a user