diff --git a/README.md b/README.md index 7fc5f01..74f3a63 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ OmniEmu is a cross-platform emulator manager, game launcher, and ROM library for ## Features -- **One-Click Emulator Setup** — Download, install, and auto-configure 9 emulators from inside the app +- **One-Click Emulator Setup** — Download, install, and auto-configure 10 emulators from inside the app - **ROM Library** — Scan your ROMs, browse by platform, and launch games directly - **Auto-Update** — Built-in updater keeps OmniEmu current without manual downloads - **Game Art Scraping** — Automatically fetches covers, screenshots, and titles @@ -34,6 +34,7 @@ OmniEmu is a cross-platform emulator manager, game launcher, and ROM library for | PPSSPP | PSP | ✅ | ✅ | ✅ | | DuckStation | PlayStation 1 | ✅ | ✅ | ✅ | | melonDS | Nintendo DS | ✅ | ✅ | ✅ | +| Flycast | Dreamcast, Naomi, Atomiswave | ✅ | ✅ | ✅ | | RetroArch | Multi-system (NES, SNES, N64, GB/GBA, PS1, and more) | ✅ | ✅ | ✅ | ## Quick Start diff --git a/package.json b/package.json index bf87db4..7793fa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniemu2", - "version": "0.1.1", + "version": "0.1.2", "description": "Cross-platform emulator manager, game launcher and ROM manager", "main": "dist/main/index.js", "scripts": { diff --git a/presets.json b/presets.json index bb16ce8..6af8e1f 100644 --- a/presets.json +++ b/presets.json @@ -87,5 +87,14 @@ "melonDS.ini": "[General]\nfullscreen = 1\n[Video]\nrenderer = OpenGL\nvsync = 1\n[Audio]\nvolume = 100\n[Controls]\n" } } + ], + "flycast": [ + { + "name": "OmniEmu Recommended", + "description": "Optimal Flycast settings for Dreamcast emulation", + "files": { + "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" + } + } ] } diff --git a/src/main/configurator.ts b/src/main/configurator.ts index 842a6ae..9392dd0 100644 --- a/src/main/configurator.ts +++ b/src/main/configurator.ts @@ -214,6 +214,27 @@ WidescreenHack = True ControllerBackend = SDL [ControllerPort0] MultitapPort1 = false +`, + }, + }, + ], + flycast: [ + { + name: 'OmniEmu Recommended', + description: 'Optimal Flycast settings for Dreamcast emulation', + files: { + 'emu.cfg': `[config] +renderer = vulkan +fullscreen = yes +vsync = yes +auto_region = yes +cable_type = vga +broadcast = ntsc +frameskip = 0 +[network] +enable = no +[input] +enable_mouse = no `, }, }, @@ -229,7 +250,7 @@ async function fetchRemotePresets(): Promise | nu const { get } = await import('https'); const data = await new Promise((resolve, reject) => { get(presetSourceUrl, { - headers: { 'User-Agent': 'OmniEmu/0.1.1' }, + headers: { 'User-Agent': 'OmniEmu/0.1.2' }, timeout: 10000, }, (res) => { if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; } @@ -319,6 +340,11 @@ function getConfigDir(emulatorId: string, installPath: string): string { darwin: join(require('os').homedir(), 'Library', 'Application Support', 'Eden'), linux: join(require('os').homedir(), '.config', 'Eden'), }, + flycast: { + win32: join(process.env.APPDATA || '', 'flycast'), + darwin: join(require('os').homedir(), 'Library', 'Application Support', 'flycast'), + linux: join(require('os').homedir(), '.config', 'flycast'), + }, }; return platformDirs[emulatorId]?.[platform] || dirname(installPath); } @@ -510,6 +536,133 @@ export function applyControllerConfig(emulatorId: string, installPath: string, c writeFileSync(iniPath, lines.join('\n'), 'utf-8'); return true; } + + case 'flycast': { + const cfgPath = join(configDir, 'emu.cfg'); + const existing = existsSync(cfgPath) ? readFileSync(cfgPath, 'utf-8') : ''; + const lines = existing.split('\n').filter(l => + !l.startsWith('enable_mouse') + ); + lines.push('[input]'); + lines.push('enable_mouse = no'); + writeFileSync(cfgPath, lines.join('\n'), 'utf-8'); + return true; + } } return false; } + +const raEmulatorConfigs: Record = { + retroarch: { + file: 'retroarch.cfg', + enabled: 'cheevos_enable = "true"', + username: 'cheevos_username = "%s"', + password: 'cheevos_password = "%s"', + }, + dolphin: { + file: 'Config/Dolphin.ini', + section: 'General', + enabled: 'RAEnabled = True', + username: 'RAUsername = %s', + password: 'RAPassword = %s', + }, + pcsx2: { + file: 'inis/PCSX2.ini', + section: 'EmuCore', + enabled: 'AchievementsEnabled = 1', + username: 'AchievementsUsername = %s', + password: 'AchievementsPassword = %s', + }, + duckstation: { + file: 'settings.ini', + section: 'Cheevos', + enabled: 'Enabled = True', + username: 'Username = %s', + password: 'Password = %s', + }, + flycast: { + file: 'emu.cfg', + section: 'achievements', + enabled: 'enable = yes', + username: 'username = %s', + password: 'password = %s', + }, +}; + +export function applyRetroAchievements(username: string, password: string): Record { + const results: Record = {}; + + for (const [emuId, cfg] of Object.entries(raEmulatorConfigs)) { + try { + const configDir = getConfigDir(emuId, ''); + if (!configDir || configDir === '.') { results[emuId] = false; continue; } + + const filePath = join(configDir, cfg.file); + const parentDir = dirname(filePath); + if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true }); + + let content = ''; + if (existsSync(filePath)) content = readFileSync(filePath, 'utf-8'); + + const lines = content.split('\n'); + + if (cfg.section) { + const sectionStart = lines.findIndex(l => l.trim() === `[${cfg.section}]`); + if (sectionStart === -1) { + lines.push('', `[${cfg.section}]`); + lines.push(cfg.enabled); + lines.push(cfg.username.replace('%s', username)); + lines.push(cfg.password.replace('%s', password)); + } else { + const existingEnabled = lines.slice(sectionStart).findIndex(l => /^enable|^raenabled|^achievementsenabled|^enabled\b/i.test(l.trim())); + if (existingEnabled === -1 || existingEnabled > lines.slice(sectionStart).findIndex(l => l.trim().startsWith('[') && !l.trim().startsWith(`[${cfg.section}]`))) { + lines.splice(sectionStart + 1, 0, cfg.enabled); + } + const existingUser = lines.slice(sectionStart).findIndex(l => /username/i.test(l)); + if (existingUser === -1 || existingUser > lines.slice(sectionStart).findIndex(l => l.trim().startsWith('[') && !l.trim().startsWith(`[${cfg.section}]`))) { + lines.splice(sectionStart + 1, 0, cfg.username.replace('%s', username)); + } + const existingPass = lines.slice(sectionStart).findIndex(l => /password/i.test(l)); + if (existingPass === -1 || existingPass > lines.slice(sectionStart).findIndex(l => l.trim().startsWith('[') && !l.trim().startsWith(`[${cfg.section}]`))) { + lines.splice(sectionStart + 1, 0, cfg.password.replace('%s', password)); + } + // Update existing lines + for (let i = sectionStart + 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('[')) break; + if (/^enable|^raenabled|^achievementsenabled|^enabled\b/i.test(line)) { + lines[i] = cfg.enabled; + } else if (/^\s*username/i.test(line)) { + lines[i] = cfg.username.replace('%s', username); + } else if (/^\s*password/i.test(line)) { + lines[i] = cfg.password.replace('%s', password); + } + } + } + } else { + // RetroArch-style flat config + const setLine = (prefix: string, value: string) => { + const idx = lines.findIndex(l => l.trim().startsWith(prefix)); + if (idx !== -1) lines[idx] = value; + else lines.push(value); + }; + setLine('cheevos_enable', cfg.enabled); + setLine('cheevos_username', cfg.username.replace('%s', username)); + setLine('cheevos_password', cfg.password.replace('%s', password)); + } + + writeFileSync(filePath, lines.join('\n'), 'utf-8'); + results[emuId] = true; + } catch { + results[emuId] = false; + } + } + + return results; +} diff --git a/src/main/emulators.ts b/src/main/emulators.ts index a175f38..1162e40 100644 --- a/src/main/emulators.ts +++ b/src/main/emulators.ts @@ -260,6 +260,7 @@ export const knownEmulators: EmulatorConfig[] = [ platforms: [ 'nes', 'snes', 'n64', 'gb', 'gba', 'gbc', 'ps1', 'pce', 'sega-md', 'sega-saturn', 'sega-dc', + 'dreamcast', ], defaultPath: { win32: 'C:\\Program Files\\RetroArch\\retroarch.exe', @@ -389,6 +390,45 @@ export const knownEmulators: EmulatorConfig[] = [ linux: 'https://melonds.kuribo64.net/', }, }, + { + id: 'flycast', + name: 'Flycast', + description: 'Sega Dreamcast, Naomi & Atomiswave emulator', + platforms: ['dreamcast'], + defaultPath: { + win32: 'C:\\Program Files\\Flycast\\flycast.exe', + darwin: '/Applications/Flycast.app/Contents/MacOS/Flycast', + linux: '/usr/bin/flycast', + }, + downloads: { + win32: [ + { + url: 'https://github.com/flyinghead/flycast/releases/download/v2.6/flycast-win64-2.6.zip', + format: 'zip', + executablePath: 'flycast-win64.exe', + }, + ], + darwin: [ + { + url: 'https://github.com/flyinghead/flycast/releases/download/v2.6/flycast-macOS-2.6.zip', + format: 'zip', + executablePath: 'Flycast.app/Contents/MacOS/Flycast', + }, + ], + linux: [ + { + url: 'https://github.com/flyinghead/flycast/releases/download/v2.6/flycast-x86_64.AppImage', + format: 'appimage', + }, + ], + }, + supported: true, + websiteUrl: { + win32: 'https://github.com/flyinghead/flycast/releases', + darwin: 'https://github.com/flyinghead/flycast/releases', + linux: 'https://github.com/flyinghead/flycast/releases', + }, + }, ]; export function findEmulator(id: string): EmulatorConfig | undefined { @@ -473,6 +513,12 @@ function alternativePaths(emulatorId: string): string[] { join(omniEmuDir, 'mame'), join(omniEmuDir, 'MAME.AppImage'), ], + flycast: [ + join(omniEmuDir, 'flycast-win64.exe'), + join(omniEmuDir, 'flycast'), + join(omniEmuDir, 'Flycast.app', 'Contents', 'MacOS', 'Flycast'), + join(home, 'Applications', 'Flycast.app', 'Contents', 'MacOS', 'Flycast'), + ], }; return common[emulatorId] || [join(omniEmuDir)]; } @@ -636,6 +682,8 @@ function launchArgs(emulatorId: string, romPath: string): string { } case 'duckstation': return `"${romPath}"`; + case 'flycast': + return `"${romPath}"`; default: return `"${romPath}"`; } @@ -687,7 +735,7 @@ export function scanRoms(directory: string): GameEntry[] { '.gba', '.gb', '.gbc', '.nds', '.iso', '.bin', '.cue', '.wbfs', '.wad', '.nsp', '.xci', '.pkg', '.chd', '.gcm', '.gcz', '.rvz', '.m3u', '.ps2', '.cso', - '.rom', '.zip', '.7z', '.gdi', '.pbp', + '.rom', '.zip', '.7z', '.gdi', '.pbp', '.cdi', ]; const entries: GameEntry[] = []; @@ -778,6 +826,7 @@ function guessPlatform(ext: string, dirPath?: string): string { '.gcm': 'gc', '.gcz': 'gc', '.rvz': 'gc', '.ps2': 'ps2', '.cso': 'ps2', '.gdi': 'dreamcast', + '.cdi': 'dreamcast', '.pbp': 'psp', }; return map[ext] || 'other'; @@ -804,7 +853,6 @@ function guessEmulator(ext: string, platform?: string): string { if (platform) { const platformMap: Record = { psp: 'ppsspp', - dreamcast: 'retroarch', 'sega-saturn': 'retroarch', 'sega-dc': 'retroarch', 'sega-md': 'retroarch', @@ -812,6 +860,7 @@ function guessEmulator(ext: string, platform?: string): string { nes: 'retroarch', snes: 'retroarch', n64: 'retroarch', gb: 'retroarch', gbc: 'retroarch', gba: 'retroarch', nds: 'retroarch', + dreamcast: 'flycast', }; const mapped = platformMap[platform]; if (mapped) return mapped; @@ -829,7 +878,8 @@ function guessEmulator(ext: string, platform?: string): string { '.img': 'duckstation', '.m3u': 'duckstation', '.chd': 'duckstation', '.ecm': 'duckstation', '.mds': 'duckstation', '.ps2': 'pcsx2', '.cso': 'pcsx2', - '.gdi': 'retroarch', + '.gdi': 'flycast', + '.cdi': 'flycast', '.pbp': 'ppsspp', }; return map[ext] || 'retroarch'; diff --git a/src/main/installer.ts b/src/main/installer.ts index cbd31ad..9791901 100644 --- a/src/main/installer.ts +++ b/src/main/installer.ts @@ -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.1' }, + headers: { 'User-Agent': 'OmniEmu/0.1.2' }, timeout: 30000, }; protocol(currentUrl, opts, (response) => { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index ec66fb4..71dccdc 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -16,7 +16,7 @@ import { ensureRomsStructure, } from './emulators'; import { installEmulator, findInstalledBinary } from './installer'; -import { applyRecommendedConfig, getPresets, checkConfigured, applyControllerConfig } from './configurator'; +import { applyRecommendedConfig, getPresets, checkConfigured, applyControllerConfig, applyRetroAchievements } from './configurator'; import { settings } from './settings'; import { getSystemInfo, platformName, getPlatform, getArch } from './platform'; import { checkForUpdates, downloadUpdate, quitAndInstall } from './updater'; @@ -230,6 +230,17 @@ export function registerIpcHandlers(): void { ipcMain.handle('paths:roms-directory', () => getRomsDirectory()); ipcMain.handle('paths:emulators-directory', () => getEmulatorsDirectory()); + // RetroAchievements + ipcMain.handle( + 'retroachievements:save', + async (_event, username: string, password: string) => { + const s = settings.get(); + settings.save({ retroAchievementsUsername: username, retroAchievementsPassword: password }); + const results = applyRetroAchievements(username, password); + return results; + } + ); + // Utilities ipcMain.handle('utilities:regenerate-roms-structure', () => { ensureRomsStructure(); diff --git a/src/main/preload.ts b/src/main/preload.ts index a90244d..d96774b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -109,6 +109,11 @@ const api = { ipcRenderer.invoke('utilities:regenerate-roms-structure'), }, + retroachievements: { + save: (username: string, password: string): Promise> => + ipcRenderer.invoke('retroachievements:save', username, password), + }, + updates: { check: (): Promise => ipcRenderer.invoke('updates:check'), download: (): Promise => ipcRenderer.invoke('updates:download'), diff --git a/src/main/scraper.ts b/src/main/scraper.ts index f416e6d..a979c06 100644 --- a/src/main/scraper.ts +++ b/src/main/scraper.ts @@ -120,7 +120,7 @@ export async function findValidThumbnail(title: string, platform: string): Promi function fetchText(url: string): Promise { return new Promise((resolve, reject) => { - const req = httpsGet(url, { headers: { 'User-Agent': 'OmniEmu/0.1.1' }, timeout: 10000 }, (res) => { + const req = httpsGet(url, { headers: { 'User-Agent': 'OmniEmu/0.1.2' }, 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 { return new Promise((resolve) => { const req = httpsGet(url, { method: 'GET', - headers: { 'User-Agent': 'OmniEmu/0.1.1' }, + headers: { 'User-Agent': 'OmniEmu/0.1.2' }, timeout: 10000, }, (res) => { // GitHub raw returns 200 for existing or redirects to it diff --git a/src/main/settings.ts b/src/main/settings.ts index 11cd333..127f5f2 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -28,6 +28,8 @@ const defaultSettings: AppSettings = { presetSourceUrl: 'https://raw.githubusercontent.com/mileswolfallen2/OmniEmu2.0/main/presets.json', recentGames: [], biosDirectory: '', + retroAchievementsUsername: '', + retroAchievementsPassword: '', }; let cached: AppSettings | null = null; diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx index 5f6de84..5b07ae7 100644 --- a/src/renderer/components/Sidebar.tsx +++ b/src/renderer/components/Sidebar.tsx @@ -36,7 +36,7 @@ export function Sidebar({ currentPage, onNavigate }: SidebarProps) { ))}
- v0.1.1 + v0.1.2
); diff --git a/src/renderer/pages/SettingsPage.tsx b/src/renderer/pages/SettingsPage.tsx index 6f9ba28..adec388 100644 --- a/src/renderer/pages/SettingsPage.tsx +++ b/src/renderer/pages/SettingsPage.tsx @@ -363,7 +363,7 @@ export function SettingsPage() {

About

- OmniEmu v{updateInfo?.version || '0.1.1'} · Cross-platform emulator manager + OmniEmu v{updateInfo?.version || '0.1.2'} · Cross-platform emulator manager
Built with Electron + React + TypeScript

diff --git a/src/renderer/pages/UtilitiesPage.tsx b/src/renderer/pages/UtilitiesPage.tsx index 2a6dbed..e9791c7 100644 --- a/src/renderer/pages/UtilitiesPage.tsx +++ b/src/renderer/pages/UtilitiesPage.tsx @@ -1,7 +1,18 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; export function UtilitiesPage() { const [regenerating, setRegenerating] = useState(false); + const [raUsername, setRaUsername] = useState(''); + const [raPassword, setRaPassword] = useState(''); + const [raResults, setRaResults] = useState | null>(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + window.omni.settings.get().then((s) => { + setRaUsername(s.retroAchievementsUsername || ''); + setRaPassword(s.retroAchievementsPassword || ''); + }); + }, []); const handleRecreate = async () => { setRegenerating(true); @@ -9,6 +20,26 @@ export function UtilitiesPage() { setTimeout(() => setRegenerating(false), 1500); }; + const handleRaSave = async () => { + setSaving(true); + setRaResults(null); + try { + const results = await window.omni.retroachievements.save(raUsername, raPassword); + setRaResults(results); + } catch { + setRaResults({}); + } + setTimeout(() => setSaving(false), 500); + }; + + const emuLabels: Record = { + retroarch: 'RetroArch', + dolphin: 'Dolphin', + pcsx2: 'PCSX2', + duckstation: 'DuckStation', + flycast: 'Flycast', + }; + return (
@@ -31,6 +62,84 @@ export function UtilitiesPage() {
+ +
+

RetroAchievements

+

+ Enable RetroAchievements across all supported emulators. Get your + password from{' '} + + retroachievements.org/settings + + {' '}(under "Web API Key"). +

+ +
+
+
Username
+
+ setRaUsername(e.target.value)} + placeholder="your RetroAchievements username" + style={{ width: 240 }} + /> +
+ +
+
+
Password / API Key
+
+ setRaPassword(e.target.value)} + placeholder="your password or Web API Key" + style={{ width: 240 }} + /> +
+ +
+
+
+ + + {raResults && ( +
+ {Object.keys(raResults).length === 0 ? ( + Failed to apply + ) : ( + Object.entries(raResults).map(([emu, ok]) => ( + + {emuLabels[emu] || emu}: {ok ? '✓' : '✗'} + + )) + )} +
+ )} +
+
+
); -} +} \ No newline at end of file diff --git a/src/shared/types.ts b/src/shared/types.ts index cd37dc5..d9a6d83 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -96,6 +96,10 @@ export interface AppSettings { biosDirectory: string; /** Per-system preferred emulator override: systemId -> emulatorId */ systemEmulators?: Record; + /** RetroAchievements username */ + retroAchievementsUsername?: string; + /** RetroAchievements password/token */ + retroAchievementsPassword?: string; } export interface SystemInfo {