Skip to content

commit f5ce6a1

abduznik edited this page May 23, 2026 · 1 revision

Feat/emulator expansion (#3)

Commit: f5ce6a18533e47a9c1f03ee59041f500320ca6e6

Author: abduznik

Date: 2026-03-24

Message

  • feat: add RPCS3, PCSX2, Azahar, Cemu, Duckstation, Xemu, Xenia emulators with GitHub release downloads

  • v0.0.2 — Windows game support, 7z extraction, expanded save sync (PCSX2, RPCS3, Xenia)

  • fix: resolve all analyzer warnings and infos (unused imports, unused field, build context sync)

Why: Adds a new feature or capability to the application.

Files Changed

README.md                                          |  81 +++++++--
 agent.md                                           |  54 ++++--
 lib/core/downloader/download_service.dart          |  27 ++-
 lib/core/emulator/emulator_download_service.dart   |  80 +++++++-
 lib/core/emulator/emulator_registry_data.dart      |  96 +++++++++-
 lib/core/emulator/github_release_service.dart      |  50 +++++
 lib/core/emulator/strategies/azahar_strategy.dart  |  43 +++++
 lib/core/emulator/strategies/cemu_strategy.dart    |  40 ++++
 .../emulator/strategies/duckstation_strategy.dart  |  40 ++++
 lib/core/emulator/strategies/pcsx2_strategy.dart   |  40 ++++
 lib/core/emulator/strategies/rpcs3_strategy.dart   |  40 ++++
 lib/core/emulator/strategies/windows_strategy.dart | 109 +++++++++++
 lib/core/emulator/strategies/xemu_strategy.dart    |  40 ++++
 lib/core/emulator/strategies/xenia_strategy.dart   |  40 ++++
 lib/core/emulator/strategy_registry.dart           |  25 ++-
 lib/core/save/save_sync_service.dart               |  45 ++++-
 lib/core/save/strategies/pcsx2_save_strategy.dart  |  89 +++++++++
 lib/core/save/strategies/rpcs3_save_strategy.dart  | 122 +++++++++++++
 .../save/strategies/windows_save_strategy.dart     | 135 ++++++++++++++
 lib/core/save/strategies/xenia_save_strategy.dart  | 109 +++++++++++
 lib/core/storage/directory_service.dart            |  42 ++++-
 lib/core/windows/pcgamingwiki_service.dart         | 202 +++++++++++++++++++++
 lib/core/windows/windows_game_service.dart         |  73 ++++++++
 lib/providers/romm_provider.dart                   |  15 +-
 lib/ui/screens/library_screen.dart                 |  74 ++++++--
 lib/ui/widgets/windows_game_config_dialog.dart     | 137 ++++++++++++++
 pubspec.yaml                                       |   3 +-
 thirdparty/7zr.exe                                 | Bin 0 -> 601088 bytes
 28 files changed, 1769 insertions(+), 82 deletions(-)
  • README.md
  • agent.md
  • lib/core/downloader/download_service.dart
  • lib/core/emulator/emulator_download_service.dart
  • lib/core/emulator/emulator_registry_data.dart
  • lib/core/emulator/github_release_service.dart
  • lib/core/emulator/strategies/azahar_strategy.dart
  • lib/core/emulator/strategies/cemu_strategy.dart
  • lib/core/emulator/strategies/duckstation_strategy.dart
  • lib/core/emulator/strategies/pcsx2_strategy.dart
  • lib/core/emulator/strategies/rpcs3_strategy.dart
  • lib/core/emulator/strategies/windows_strategy.dart
  • lib/core/emulator/strategies/xemu_strategy.dart
  • lib/core/emulator/strategies/xenia_strategy.dart
  • lib/core/emulator/strategy_registry.dart
  • lib/core/save/save_sync_service.dart
  • lib/core/save/strategies/pcsx2_save_strategy.dart
  • lib/core/save/strategies/rpcs3_save_strategy.dart
  • lib/core/save/strategies/windows_save_strategy.dart
  • lib/core/save/strategies/xenia_save_strategy.dart
  • lib/core/storage/directory_service.dart
  • lib/core/windows/pcgamingwiki_service.dart
  • lib/core/windows/windows_game_service.dart
  • lib/providers/romm_provider.dart
  • lib/ui/screens/library_screen.dart
  • lib/ui/widgets/windows_game_config_dialog.dart
  • pubspec.yaml
  • thirdparty/7zr.exe

Diff

diff --git a/README.md b/README.md
index 934e1b6..c8a824d 100644
--- a/README.md
+++ b/README.md
@@ -2,46 +2,87 @@
 
 A cross-platform Flutter app for browsing your RomM library, downloading ROMs, and launching games directly in emulators—all from one intuitive interface.
 
+## What's New in 0.0.2
+
+### Full Windows Native Game Support
+- Download Windows games (.zip and .7z) with automatic extraction
+- Auto-detection of game executables in extracted folders
+- Manual exe and save path configuration via long-press on any Windows game card
+- Crash detection on launch — if a game exits immediately, a clear error message explains likely causes (missing DirectX, Visual C++ redistributables, etc.)
+- Save sync for Windows games via PCGamingWiki automatic save path detection, with manual override support
+- All exe and save path overrides persist across app restarts
+
+### Expanded Save Sync
+- **PCSX2 (PS2)**: Syncs memory cards (Mcd001.ps2, Mcd002.ps2) and save states
+- **RPCS3 (PS3)**: Syncs save data folders by title ID
+- **Xenia Canary (Xbox 360)**: Syncs content save folders by title ID
+- **Windows games**: Full save directory packaging into zip for upload, auto-extraction on restore
+- All multi-file save strategies package saves as a single zip for clean cloud storage
+
+### 7-Zip Support
+- Bundled 7zr.exe for .7z extraction — no manual installation required
+- Automatically extracted to AppData on first run
+- Full .7z support for Windows game downloads
+
+### Quality of Life
+- Launch error messages now display for 8 seconds for better readability
+- Stale exe overrides (pointing to moved/deleted files) are automatically discarded and re-detected
+- ROM detection improved for Windows games — returns game folder instead of individual file
+
 ## Currently Working
 
-- **RomM Integration**: Browse and filter your entire RomM game library
-- **ROM Downloads**: Download games via HTTP from your RomM server (working)
+- **RomM Integration**: Browse and filter your entire RomM library by platform or search
+- **ROM Downloads**: Download games via HTTP from your RomM server with progress tracking
+- **Archive Extraction**: Automatic extraction of .zip and .7z archives on download
 - **Game Launching**: Launch games directly from the app using:
-  - RetroArch (multi-system emulator)
+  - RetroArch (GBA, GBC, GB, NES, SNES, N64, NDS, PSX, PSP, Dreamcast, Megadrive, and more)
   - Dolphin (GameCube & Wii)
-  - Eden (SNES/Genesis)
+  - Eden (Nintendo Switch)
+  - RPCS3 (PlayStation 3)
+  - PCSX2 (PlayStation 2)
+  - DuckStation (PlayStation 1)
+  - Azahar (Nintendo 3DS)
+  - Cemu (Wii U)
+  - Xemu (Xbox)
+  - Xenia Canary (Xbox 360)
+  - Windows Native (PC games)
+- **Emulator Downloads**: One-tap emulator download and installation from Settings
+- **Save Sync**: Two-way save synchronization with RomM cloud for:
+  - RetroArch (all supported platforms)
+  - Dolphin (GameCube/Wii)
+  - Eden (Switch)
+  - PCSX2 (PS2)
+  - RPCS3 (PS3)
+  - Xenia Canary (Xbox 360)
+  - Windows native games (via PCGamingWiki auto-detection)
 
 ## Roadmap
 
-### Near Term: Expanded Emulator Support
+### Near Term
 
-Expanding launcher support to cover more systems with download capabilities:
-- Dreamcast
-- RPCS3 (PlayStation 3)
-- RPCS2 (PlayStation 2)
-- Duckstation (PlayStation 1)
-- MelonDS (Nintendo DS)
-- Azure (Nintendo 3DS)
+- **Cemu save sync** — Wii U saves at mlc01/usr/save/
+- **Azahar save sync** — 3DS saves via SDMC folder
+- **MelonDS support** — better NDS emulation alternative
+- **macOS & Linux support** — platform detection and path resolution already structured for future expansion
 
 ### End-Game Features
 
-- Download emulators automatically with one tap
-- Download ROMs from RomM library (currently working)
-- Add custom ROMs for new platforms
-- Double-sided save synchronization: sync saves from RomM Cloud to device before playing, and sync back after you're done
+- Automatic emulator updates
+- Custom ROM platform tagging
+- Mobile companion app for on-the-go library browsing
 
 ### Cross-Platform Vision
 
-Freegosy is designed as a truly cross-platform experience, allowing you to manage your game library seamlessly across desktop, mobile, and tablet devices.
+Freegosy is designed as a truly cross-platform experience. The codebase is structured to support Windows, macOS, and Linux with platform-specific code isolated behind strategy patterns and service abstractions.
 
 ## Status
 
-Stay tuned for releases! Freegosy is actively under development with regular updates coming soon.
+Actively under development. Release 0.0.2 focuses on Windows game support and expanded save sync coverage.
 
 ## About RomM
 
-Freegosy is built to complement [RomM](https://github.com/rommapp/romm), a modern ROM manager. It connects to your RomM instance to provide a lightweight, portable way to access and play your games on the go.
+Freegosy is built to complement [RomM](https://github.com/rommapp/romm), a modern ROM manager. It connects to your RomM instance to provide a lightweight, portable way to access and play your games.
 
 ## Contributing
 
-We welcome contributions! Please check out our project structure in `agent.md` for guidelines on how to add new emulators or features.
+Check out `agent.md` for the full file map, coding rules, and contracts for adding new emulators, save strategies, or features.
\ No newline at end of file
diff --git a/agent.md b/agent.md
index 9e48305..de0be33 100644
--- a/agent.md
+++ b/agent.md
@@ -10,9 +10,11 @@ Freegosy is a cross-platform Flutter app for browsing a RomM library, downloadin
 - All RomM API calls go through `romm_service.dart` only. Never call the API directly from UI or providers.
 - All emulator logic goes through the strategy pattern. Never hardcode emulator behavior in UI.
 - New emulator = new file in `core/emulator/strategies/`, register in `strategy_registry.dart` only.
+- New save strategy = new file in `core/save/strategies/`, register in `save_sync_service.dart` only.
 - New screen = new file in `ui/screens/`.
 - New reusable widget = new file in `ui/widgets/`.
 - Providers are thin — they call services, they do not contain business logic.
+- Never use `Platform.environment` or `Platform.isWindows` directly — causes conflicts with flutter/foundation.dart. Use `Process.run('cmd', ['/c', 'echo %APPDATA%'])` for env vars, and `defaultTargetPlatform == TargetPlatform.windows` for platform checks.
 
 ## File Map
 
@@ -26,43 +28,61 @@ Freegosy is a cross-platform Flutter app for browsing a RomM library, downloadin
 
 ### Core — Save Sync
 - `lib/core/save/save_strategy.dart` — Abstract base class SaveStrategy. Methods: getSaveDir(), getSaveFiles(), restoreSave(). Helpers: backupSave() (3-version rotation), getRomStem().
-- `lib/core/save/save_sync_service.dart` — SaveSyncService. Methods: pushSaves(), pullSave(), getStrategyForSlug(). Wires strategies to RommService.
+- `lib/core/save/save_sync_service.dart` — SaveSyncService. Methods: pushSaves(), pullSave(), getStrategyForSlug(). Wires all strategies to RommService. Exposes windowsSaveStrategy for external access.
 - `lib/core/save/strategies/retroarch_save_strategy.dart` — RetroArch save strategy (GBA/GBC/GB/SNES/NES/N64/NDS/PSX/PSP/Dreamcast/Megadrive). Reads saves/{coreName}/{stem}.srm and .state.auto.
 - `lib/core/save/strategies/dolphin_save_strategy.dart` — Dolphin save strategy (GC/Wii). Reads User/GC/{region}/Card A/*.gci.
-- `lib/core/save/strategies/eden_save_strategy.dart` — Eden/Switch save strategy. Resolves title ID via filename regex, XCI header parse, or save folder scan.
+- `lib/core/save/strategies/eden_save_strategy.dart` — Eden/Switch save strategy. Resolves title ID via filename regex, XCI header parse, or save folder scan. Zips save folder for upload.
+- `lib/core/save/strategies/windows_save_strategy.dart` — Windows native game save strategy. Uses PCGamingWiki API for auto-detection, supports manual override. Zips entire save directory for upload, extracts on restore. Persists overrides via SharedPreferences (prefix `win_save_`).
+- `lib/core/save/strategies/pcsx2_save_strategy.dart` — PCSX2 save strategy (PS2). Memcards: {emulatorDir}/memcards/Mcd001.ps2, Mcd002.ps2. States: {emulatorDir}/sstates/{stem}.000 etc.
+- `lib/core/save/strategies/rpcs3_save_strategy.dart` — RPCS3 save strategy (PS3). Saves at %APPDATA%\rpcs3\dev_hdd0\home\00000001\savedata\{titleId}\. Extracts title ID via regex [A-Z]{4}\d{5}. Zips save folder for upload. Uses Process.run for APPDATA resolution.
+- `lib/core/save/strategies/xenia_save_strategy.dart` — Xenia Canary save strategy (Xbox 360). Saves at {emulatorDir}\content\{titleId}\00000001\. Extracts title ID via 8-char hex regex. Zips save folder for upload.
 
 ### Core — Emulator
 - `lib/core/emulator/emulator_strategy.dart` — Abstract base class. Fields: name, emulatorId, supportedSlugs, windowsExecutable, linuxExecutable. Methods: launch(Game, romPath), resolveSavePath(Game), getExecutableForPlatform().
 - `lib/core/emulator/emulator_registry_data.dart` — Static data for emulator definitions.
 - `lib/core/emulator/strategy_registry.dart` — Registry for emulator strategies. Methods: getStrategyForSlug(), getDefinition().
-- `lib/core/emulator/strategies/retroarch_strategy.dart` — RetroArch strategy implementation.
-- `lib/core/emulator/strategies/dolphin_strategy.dart` — Dolphin strategy implementation.
-- `lib/core/emulator/strategies/eden_strategy.dart` — Eden strategy implementation.
-- `lib/core/emulator/emulator_download_service.dart` — Service for downloading and extracting emulators.
+- `lib/core/emulator/emulator_download_service.dart` — Service for downloading and extracting emulators. Supports direct URL and GitHub release types. Handles .zip and .7z extraction.
+- `lib/core/emulator/github_release_service.dart` — Fetches latest release asset URL from GitHub API with required/excluded name filters.
+- `lib/core/emulator/strategies/retroarch_strategy.dart` — RetroArch strategy. Slugs: gba/gbc/gb/nes/snes/n64/nds/psx/ps1/psp/dc/dreamcast/megadrive/genesis/md etc.
+- `lib/core/emulator/strategies/dolphin_strategy.dart` — Dolphin strategy. Slugs: gc/gamecube/wii/ngc.
+- `lib/core/emulator/strategies/eden_strategy.dart` — Eden strategy. Slugs: switch/nintendo-switch/ns.
+- `lib/core/emulator/strategies/rpcs3_strategy.dart` — RPCS3 strategy. Slugs: ps3/playstation-3/playstation3.
+- `lib/core/emulator/strategies/pcsx2_strategy.dart` — PCSX2 strategy. Slugs: ps2/playstation-2/playstation2.
+- `lib/core/emulator/strategies/azahar_strategy.dart` — Azahar strategy. Slugs: 3ds/n3ds/nintendo-3ds/nintendo3ds/new-nintendo-3ds/new-nintendo-3ds-xl.
+- `lib/core/emulator/strategies/cemu_strategy.dart` — Cemu strategy. Slugs: wiiu/wii-u/nintendo-wii-u/nintendo-wiiu.
+- `lib/core/emulator/strategies/duckstation_strategy.dart` — DuckStation strategy. Slugs: ps1/playstation/psx.
+- `lib/core/emulator/strategies/xemu_strategy.dart` — Xemu strategy. Slugs: xbox.
+- `lib/core/emulator/strategies/xenia_strategy.dart` — Xenia Canary strategy. Slugs: xbox360/xbla.
+- `lib/core/emulator/strategies/windows_strategy.dart` — Windows native game strategy. Auto-detects exe in game folder, validates stored override exists on disk before using. Launches via Process.start. Monitors exit code for 5s — throws if crashed. Persists overrides via SharedPreferences (prefix `win_exe_`).
 
 ### Core — Downloader
-- `lib/core/downloader/download_service.dart` — HTTP ROM download via Dio. Exposes a Stream<DownloadProgress> for UI progress tracking. Updated to use DirectoryService.
+- `lib/core/downloader/download_service.dart` — HTTP ROM download via Dio. Stream<DownloadProgress> for UI. Handles .zip extraction via archive package and .7z via bundled 7zr.exe (resolved via DirectoryService.resolveSevenZipPath()). Windows games (.zip/.7z) always extracted regardless of isMultiFile flag.
 
 ### Core — Storage
-- `lib/core/storage/directory_service.dart` — Manages ROMs and emulator directories, including persistence via SharedPreferences.
+- `lib/core/storage/directory_service.dart` — Manages ROMs and emulator directories. Persists paths via SharedPreferences. resolveSevenZipPath() extracts bundled 7zr.exe from Flutter assets to %APPDATA%\Freegosy\thirdparty\ on first run. Uses defaultTargetPlatform for Windows check, Process.run for APPDATA resolution.
+
+### Core — Windows
+- `lib/core/windows/windows_game_service.dart` — Finds main exe in game folder (hint match then largest). Skips uninstall/setup/redist/etc. Launches via Process.start detached.
+- `lib/core/windows/pcgamingwiki_service.dart` — Queries PCGamingWiki API for Windows game save locations. Parses MediaWiki markup, expands environment variables (APPDATA, LOCALAPPDATA etc), returns resolved paths.
 
 ### Core — Updater
 - `lib/core/updater/updater_service.dart` — Checks GitHub Releases API for new version. Downloads new binary to temp, swaps, relaunches.
 
 ### Providers
-- `lib/providers/romm_provider.dart` — Riverpod providers for RomM config, connection state, DirectoryService, EmulatorDownloadService, and SaveSyncService.
-- `lib/providers/library_provider.dart` — Riverpod providers for platforms list and games list. Includes search and filtering logic.
+- `lib/providers/romm_provider.dart` — Riverpod providers for RomM config, connection state, DirectoryService, StrategyRegistry (loads WindowsStrategy persisted overrides on init), SaveSyncService (loads WindowsSaveStrategy persisted overrides on init).
+- `lib/providers/library_provider.dart` — Riverpod providers for platforms list and games list. Includes search, filtering, card aspect ratio, and RetroArch sync mode persistence.
 - `lib/providers/download_provider.dart` — Riverpod providers for active downloads and progress.
 
 ### UI — Screens
-- `lib/ui/screens/library_screen.dart` — Main screen. Shows platform filter bar, game grid, search bar, and game count display.
+- `lib/ui/screens/library_screen.dart` — Main screen. Game grid with search, platform filter, download, launch, save sync. Windows games support long-press to open config dialog and auto-show config on missing exe. Launch errors show for 8 seconds.
 - `lib/ui/screens/download_screen.dart` — Active downloads list with progress bars.
-- `lib/ui/screens/settings_screen.dart` — Storage and Emulator management section. Includes path selection, emulator installation status, and download buttons.
+- `lib/ui/screens/settings_screen.dart` — RomM server config, card aspect ratio, storage paths, RetroArch sync mode, emulator download/install status.
 
 ### UI — Widgets
-- `lib/ui/widgets/game_card.dart` — Single game tile. Shows cover, name, platform. Tappable to launch or download.
+- `lib/ui/widgets/game_card.dart` — Single game tile. Shows cover, name, download/launch/sync buttons. Green dot when downloaded.
 - `lib/ui/widgets/download_progress_card.dart` — Single download row with progress bar and cancel button.
 - `lib/ui/widgets/platform_filter_bar.dart` — Horizontal scrollable platform chip row with distinct styling for selected/unselected states.
+- `lib/ui/widgets/windows_game_config_dialog.dart` — Dialog for configuring Windows game exe path and save directory. Browse buttons for both. Returns `Map<String, String>` with keys `exe` and `save`. Only shown for Windows platform games.
 
 ## Key Contracts
 
@@ -107,7 +127,9 @@ class RomMConfig {
 - `flutter_riverpod` — state management
 - `dio` — HTTP client for API calls and downloads
 - `path_provider` — platform-safe file paths
-- `shared_preferences` — persist RomM config locally
+- `shared_preferences` — persist RomM config, card ratio, sync mode, Windows exe/save overrides
 - `package_info_plus` — read current app version for updater
-- `archive` — for zip extraction
-- `file_picker` — for directory selection
+- `archive` — zip extraction and creation (ZipDecoder, ZipFileEncoder)
+- `file_picker` — directory and file selection
+- `path` — path manipulation utilities
+- `thirdparty/7zr.exe` — bundled 7-Zip console executable for .7z extraction (Flutter asset)
\ No newline at end of file
diff --git a/lib/core/downloader/download_service.dart b/lib/core/downloader/download_service.dart
index 927d303..b3e5a42 100644
--- a/lib/core/downloader/download_service.dart
+++ b/lib/core/downloader/download_service.dart
@@ -65,7 +65,10 @@ class DownloadService {
         deleteOnError: true,
       ).then((_) async {
         try {
-          if (game.isMultiFile) {
+          final isWindowsGame = ['windows', 'pc', 'win'].contains(game.platformSlug?.toLowerCase() ?? '');
+          final isArchive = savePath.toLowerCase().endsWith('.zip') ||
+              savePath.toLowerCase().endsWith('.7z');
+          if (game.isMultiFile || (isWindowsGame && isArchive)) {
             controller.add(DownloadProgress(
               id: game.id,
               gameName: game.name,
@@ -112,11 +115,25 @@ class DownloadService {
 
     await Directory(extractDir).create(recursive: true);
 
-    final bytes = await File(zipPath).readAsBytes();
-    final archive = ZipDecoder().decodeBytes(bytes);
-    extractArchiveToDisk(archive, extractDir);
+    if (zipPath.toLowerCase().endsWith('.7z')) {
+      final sevenZipExe = await directoryService.resolveSevenZipPath();
+      if (sevenZipExe == null) {
+        throw Exception('7zr.exe could not be initialized. Try reinstalling Freegosy.');
+      }
+      final result = await Process.run(
+        sevenZipExe, ['x', zipPath, '-o$extractDir', '-y'],
+        runInShell: false,
+      );
+      if (result.exitCode != 0) {
+        throw Exception('7z extraction failed: ${result.stderr}');
+      }
+    } else {
+      final bytes = await File(zipPath).readAsBytes();
+      final archive = ZipDecoder().decodeBytes(bytes);
+      extractArchiveToDisk(archive, extractDir);
+    }
 
-    // Delete the zip after extraction
+    // Delete the archive after extraction
     await File(zipPath).delete();
 
     // Log the largest file found (main ROM)
diff --git a/lib/core/emulator/emulator_download_service.dart b/lib/core/emulator/emulator_download_service.dart
index e40b1a2..979da86 100644
--- a/lib/core/emulator/emulator_download_service.dart
+++ b/lib/core/emulator/emulator_download_service.dart
@@ -7,12 +7,16 @@ import 'package:archive/archive_io.dart';
 import '../downloader/download_service.dart';
 import '../storage/directory_service.dart';
 import 'emulator_registry_data.dart';
+import 'github_release_service.dart';
 
 class EmulatorDownloadService {
   final Dio _dio;
   final DirectoryService _directoryService;
+  late final GithubReleaseService _githubService;
 
-  EmulatorDownloadService(this._dio, this._directoryService);
+  EmulatorDownloadService(this._dio, this._directoryService) {
+    _githubService = GithubReleaseService(_dio);
+  }
 
   Stream<DownloadProgress> downloadEmulator(String emulatorId) async* {
     final definition = kEmulatorDefinitions.firstWhere(
@@ -21,17 +25,53 @@ class EmulatorDownloadService {
     );
 
     if (definition.isEmpty) {
-      yield DownloadProgress(id: emulatorId, gameName: emulatorId, error: 'Emulator definition not found');
+      yield DownloadProgress(
+        id: emulatorId,
+        gameName: emulatorId,
+        error: 'Emulator definition not found',
+      );
       return;
     }
 
     final String emulatorName = definition['name'] as String? ?? emulatorId;
-    final String? downloadUrl = Platform.isWindows
-        ? definition['windows_url'] as String?
-        : definition['linux_url'] as String?;
+    final String type = definition['type'] as String? ?? 'direct';
+
+    // Resolve download URL based on type
+    String? downloadUrl;
+    if (type == 'github') {
+      yield DownloadProgress(
+        id: emulatorId,
+        gameName: emulatorName,
+        status: 'Fetching latest release...',
+      );
+      final repo = definition['github_repo'] as String;
+      final required = List<String>.from(definition['github_asset_required'] ?? []);
+      final excluded = List<String>.from(definition['github_asset_excluded'] ?? []);
+      downloadUrl = await _githubService.getLatestReleaseUrl(
+        repo: repo,
+        required: required,
+        excluded: excluded,
+      );
+      if (downloadUrl == null) {
+        yield DownloadProgress(
+          id: emulatorId,
+          gameName: emulatorName,
+          error: 'No matching release asset found on GitHub',
+        );
+        return;
+      }
+    } else {
+      downloadUrl = Platform.isWindows
+          ? definition['windows_url'] as String?
+          : definition['linux_url'] as String?;
+    }
 
     if (downloadUrl == null) {
-      yield DownloadProgress(id: emulatorId, gameName: emulatorName, error: 'No download URL for this platform');
+      yield DownloadProgress(
+        id: emulatorId,
+        gameName: emulatorName,
+        error: 'No download URL for this platform',
+      );
       return;
     }
 
@@ -54,34 +94,54 @@ class EmulatorDownloadService {
               percent: received / total,
               bytesReceived: received,
               totalBytes: total,
+              status: 'Downloading...',
             ));
           }
         },
         deleteOnError: true,
       ).then((_) async {
         try {
+          controller.add(DownloadProgress(
+            id: emulatorId,
+            gameName: emulatorName,
+            percent: 1.0,
+            status: 'Extracting...',
+          ));
           await _extractArchive(tempFilePath, emulatorDir);
           controller.add(DownloadProgress(
             id: emulatorId,
             gameName: emulatorName,
             percent: 1.0,
             isComplete: true,
+            status: 'Done!',
           ));
         } catch (e) {
-          controller.add(DownloadProgress(id: emulatorId, gameName: emulatorName, error: 'Extraction failed: $e'));
+          controller.add(DownloadProgress(
+            id: emulatorId,
+            gameName: emulatorName,
+            error: 'Extraction failed: $e',
+          ));
         } finally {
           controller.close();
           final f = File(tempFilePath);
           if (await f.exists()) await f.delete();
         }
       }).catchError((e) {
-        controller.add(DownloadProgress(id: emulatorId, gameName: emulatorName, error: 'Download failed: $e'));
+        controller.add(DownloadProgress(
+          id: emulatorId,
+          gameName: emulatorName,
+          error: 'Download failed: $e',
+        ));
         controller.close();
       });
 
       yield* controller.stream;
     } catch (e) {
-      yield DownloadProgress(id: emulatorId, gameName: emulatorName, error: 'Error: $e');
+      yield DownloadProgress(
+        id: emulatorId,
+        gameName: emulatorName,
+        error: 'Error: $e',
+      );
     }
   }
 
@@ -102,4 +162,4 @@ class EmulatorDownloadService {
       throw Exception('Unsupported archive format: $archivePath');
     }
   }
-}
+}
\ No newline at end of file
diff --git a/lib/core/emulator/emulator_registry_data.dart b/lib/core/emulator/emulator_registry_data.dart
index e02b4c2..0aefadf 100644
--- a/lib/core/emulator/emulator_registry_data.dart
+++ b/lib/core/emulator/emulator_registry_data.dart
@@ -2,19 +2,21 @@ const List<Map<String, dynamic>> kEmulatorDefinitions = [
   {
     'id': 'retroarch',
     'name': 'RetroArch',
+    'type': 'direct',
     'windows_url': 'https://buildbot.libretro.com/stable/1.19.1/windows/x86_64/RetroArch.7z',
     'windows_executable': 'RetroArch.exe',
     'linux_executable': 'retroarch',
     'platform_slugs': [
-      'gba', 'gbc', 'gb', 'nes', 'snes', 'n64', 'nds', 'psx', 'psp',
-      'segacd', 'saturn', 'dreamcast', 'megadrive', 'genesis', 'gamegear',
-      'atari2600', 'atari7800', 'lynx', 'neogeo', 'arcade', 'mame',
+      'gba', 'gbc', 'gb', 'nes', 'snes', 'n64', 'nds', 'psx', 'ps1', 'playstation',
+      'psp', 'dc', 'dreamcast', 'segacd', 'saturn', 'megadrive', 'genesis', 'md',
+      'gamegear', 'atari2600', 'atari7800', 'lynx', 'neogeo', 'arcade', 'mame',
       'pcengine', 'wonderswan', 'virtualboy', 'msx', 'dos'
     ],
   },
   {
     'id': 'dolphin',
     'name': 'Dolphin',
+    'type': 'direct',
     'windows_url': 'https://dl.dolphin-emu.org/builds/dolphin-master-latest-x64.7z',
     'windows_executable': 'Dolphin.exe',
     'linux_executable': 'dolphin-emu',
@@ -23,9 +25,95 @@ const List<Map<String, dynamic>> kEmulatorDefinitions = [
   {
     'id': 'eden',
     'name': 'Eden (Switch)',
+    'type': 'direct',
     'windows_url': 'https://git.eden-emu.dev/eden-emu/eden/-/releases/permalink/latest/downloads/eden-windows-msvc-install.zip',
     'windows_executable': 'eden.exe',
     'linux_executable': 'eden',
     'platform_slugs': ['switch', 'nintendo-switch', 'ns'],
   },
-];
+  {
+    'id': 'rpcs3',
+    'name': 'RPCS3 (PlayStation 3)',
+    'type': 'github',
+    'github_repo': 'RPCS3/rpcs3',
+    'github_asset_required': ['win64'],
+    'github_asset_excluded': ['debug'],
+    'windows_executable': 'rpcs3.exe',
+    'linux_executable': 'rpcs3',
+    'platform_slugs': ['ps3', 'playstation-3', 'playstation3'],
+  },
+  {
+    'id': 'pcsx2',
+    'name': 'PCSX2 (PlayStation 2)',
+    'type': 'github',
+    'github_repo': 'PCSX2/pcsx2',
+    'github_asset_required': ['windows', 'Qt'],
+    'github_asset_excluded': ['debug', 'sse4', 'arm64'],
+    'windows_executable': 'pcsx2-qt.exe',
+    'linux_executable': 'pcsx2-qt',
+    'platform_slugs': ['ps2', 'playstation-2', 'playstation2'],
+  },
+  {
+    'id': 'azahar',
+    'name': 'Azahar (Nintendo 3DS)',
+    'type': 'github',
+    'github_repo': 'azahar-emu/azahar',
+    'github_asset_required': ['windows', 'msys2'],
+    'github_asset_excluded': ['debug', 'appimage'],
+    'windows_executable': 'azahar.exe',
+    'linux_executable': 'azahar',
+    'platform_slugs': ['3ds', 'n3ds', 'nintendo-3ds', 'nintendo3ds', 'new-nintendo-3ds', 'new-nintendo-3ds-xl'],
+  },
+  {
+    'id': 'cemu',
+    'name': 'Cemu (Wii U)',
+    'type': 'github',
+    'github_repo': 'cemu-project/Cemu',
+    'github_asset_required': ['windows'],
+    'github_asset_excluded': ['experimental', 'debug'],
+    'windows_executable': 'Cemu.exe',
+    'linux_executable': 'cemu',
+    'platform_slugs': ['wiiu', 'wii-u', 'nintendo-wii-u', 'nintendo-wiiu'],
+  },
+  {
+    'id': 'xemu',
+    'name': 'Xemu (Xbox)',
+    'type': 'github',
+    'github_repo': 'xemu-project/xemu',
+    'github_asset_required': ['win', 'x86_64', 'release'],
+    'github_asset_excluded': ['dbg', 'pdb', 'arm', 'macos', 'appimage', 'tar'],
+    'windows_executable': 'xemu.exe',
+    'linux_executable': 'xemu',
+    'platform_slugs': ['xbox'],
+  },
+  {
+    'id': 'xenia_canary',
+    'name': 'Xenia Canary (Xbox 360)',
+    'type': 'github',
+    'github_repo': 'xenia-canary/xenia-canary',
+    'github_asset_required': ['windows'],
+    'github_asset_excluded': ['debugoptimized', 'debug', 'pdb'],
+    'windows_executable': 'xenia_canary.exe',
+    'linux_executable': 'xenia_canary',
+    'platform_slugs': ['xbox360', 'xbla'],
+  },
+  {
+    'id': 'duckstation',
+    'name': 'DuckStation (PlayStation 1)',
+    'type': 'github',
+    'github_repo': 'stenzek/duckstation',
+    'github_asset_required': ['windows', 'x64'],
+    'github_asset_excluded': ['nogui', 'debug', 'arm64'],
+    'windows_executable': 'duckstation-qt-x64-ReleaseLTCG.exe',
+    'linux_executable': 'duckstation-qt',
+    'platform_slugs': ['ps1', 'playstation', 'psx'],
+  },
+  {
+    'id': 'windows_native',
+    'name': 'Windows (Native)',
+    'type': 'none',
+    'windows_executable': '',
+    'linux_executable': '',
+    'platform_slugs': ['windows', 'pc', 'win'],
+  },
+];
\ No newline at end of file
diff --git a/lib/core/emulator/github_release_service.dart b/lib/core/emulator/github_release_service.dart
new file mode 100644
index 0000000..404a530
--- /dev/null
+++ b/lib/core/emulator/github_release_service.dart
@@ -0,0 +1,50 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+
+class GithubReleaseService {
+  final Dio _dio;
+
+  GithubReleaseService(this._dio);
+
+  /// Fetches the latest release download URL from a GitHub repo
+  /// that matches all [required] filters and none of the [excluded] filters.
+  Future<String?> getLatestReleaseUrl({
+    required String repo,
+    required List<String> required,
+    List<String> excluded = const [],
+  }) async {
+    try {
+      final response = await _dio.get(
+        'https://api.github.com/repos/$repo/releases/latest',
+        options: Options(headers: {'Accept': 'application/vnd.github.v3+json'}),
+      );
+
+      if (response.statusCode != 200) return null;
+
+      final assets = response.data['assets'] as List<dynamic>;
+      debugPrint('[GithubReleaseService] $repo — ${assets.length} assets found');
+
+      for (final asset in assets) {
+        final name = (asset['name'] as String).toLowerCase();
+        final url = asset['browser_download_url'] as String;
+
+        // Must match all required filters
+        final matchesRequired = required.every((f) => name.contains(f.toLowerCase()));
+        if (!matchesRequired) continue;
+
+        // Must not match any excluded filters
+        final matchesExcluded = excluded.any((f) => name.contains(f.toLowerCase()));
+        if (matchesExcluded) continue;
+
+        debugPrint('[GithubReleaseService] matched asset: $name');
+        return url;
+      }
+
+      debugPrint('[GithubReleaseService] no matching asset found for $repo');
+      return null;
+    } catch (e) {
+      debugPrint('[GithubReleaseService] error fetching $repo: $e');
+      return null;
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/azahar_strategy.dart b/lib/core/emulator/strategies/azahar_strategy.dart
new file mode 100644
index 0000000..9e7b673
--- /dev/null
+++ b/lib/core/emulator/strategies/azahar_strategy.dart
@@ -0,0 +1,43 @@
+import 'dart:io';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+
+class AzaharStrategy extends EmulatorStrategy {
+  final DirectoryService _directoryService;
+
+  AzaharStrategy(this._directoryService);
+
+  @override
+  String get name => 'Azahar';
+
+  @override
+  String get emulatorId => 'azahar';
+
+  @override
+  List<String> get supportedSlugs => [
+    '3ds', 'n3ds', 'nintendo-3ds', 'nintendo3ds',
+    'new-nintendo-3ds', 'new-nintendo-3ds-xl',
+  ];
+
+  @override
+  String get windowsExecutable => 'azahar.exe';
+
+  @override
+  String get linuxExecutable => 'azahar';
+
+  @override
+  bool get supportsSaveSync => false;
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/cemu_strategy.dart b/lib/core/emulator/strategies/cemu_strategy.dart
new file mode 100644
index 0000000..60732ea
--- /dev/null
+++ b/lib/core/emulator/strategies/cemu_strategy.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+
+class CemuStrategy extends EmulatorStrategy {
+  final DirectoryService _directoryService;
+
+  CemuStrategy(this._directoryService);
+
+  @override
+  String get name => 'Cemu';
+
+  @override
+  String get emulatorId => 'cemu';
+
+  @override
+  List<String> get supportedSlugs => ['wiiu', 'wii-u', 'nintendo-wii-u', 'nintendo-wiiu'];
+
+  @override
+  String get windowsExecutable => 'Cemu.exe';
+
+  @override
+  String get linuxExecutable => 'cemu';
+
+  @override
+  bool get supportsSaveSync => false;
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    await Process.start(exePath, ['-g', romPath], mode: ProcessStartMode.detached);
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/duckstation_strategy.dart b/lib/core/emulator/strategies/duckstation_strategy.dart
new file mode 100644
index 0000000..74f2663
--- /dev/null
+++ b/lib/core/emulator/strategies/duckstation_strategy.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+
+class DuckstationStrategy extends EmulatorStrategy {
+  final DirectoryService _directoryService;
+
+  DuckstationStrategy(this._directoryService);
+
+  @override
+  String get name => 'DuckStation';
+
+  @override
+  String get emulatorId => 'duckstation';
+
+  @override
+  List<String> get supportedSlugs => ['ps1', 'playstation', 'psx'];
+
+  @override
+  String get windowsExecutable => 'duckstation-qt-x64-ReleaseLTCG.exe';
+
+  @override
+  String get linuxExecutable => 'duckstation-qt';
+
+  @override
+  bool get supportsSaveSync => false;
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    await Process.start(exePath, ['-batch', romPath], mode: ProcessStartMode.detached);
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/pcsx2_strategy.dart b/lib/core/emulator/strategies/pcsx2_strategy.dart
new file mode 100644
index 0000000..75ec8b1
--- /dev/null
+++ b/lib/core/emulator/strategies/pcsx2_strategy.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+
+class Pcsx2Strategy extends EmulatorStrategy {
+  final DirectoryService _directoryService;
+
+  Pcsx2Strategy(this._directoryService);
+
+  @override
+  String get name => 'PCSX2';
+
+  @override
+  String get emulatorId => 'pcsx2';
+
+  @override
+  List<String> get supportedSlugs => ['ps2', 'playstation-2', 'playstation2'];
+
+  @override
+  String get windowsExecutable => 'pcsx2-qt.exe';
+
+  @override
+  String get linuxExecutable => 'pcsx2-qt';
+
+  @override
+  bool get supportsSaveSync => false;
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/rpcs3_strategy.dart b/lib/core/emulator/strategies/rpcs3_strategy.dart
new file mode 100644
index 0000000..9828c67
--- /dev/null
+++ b/lib/core/emulator/strategies/rpcs3_strategy.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+
+class Rpcs3Strategy extends EmulatorStrategy {
+  final DirectoryService _directoryService;
+
+  Rpcs3Strategy(this._directoryService);
+
+  @override
+  String get name => 'RPCS3';
+
+  @override
+  String get emulatorId => 'rpcs3';
+
+  @override
+  List<String> get supportedSlugs => ['ps3', 'playstation-3', 'playstation3'];
+
+  @override
+  String get windowsExecutable => 'rpcs3.exe';
+
+  @override
+  String get linuxExecutable => 'rpcs3';
+
+  @override
+  bool get supportsSaveSync => false;
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/windows_strategy.dart b/lib/core/emulator/strategies/windows_strategy.dart
new file mode 100644
index 0000000..17cf244
--- /dev/null
+++ b/lib/core/emulator/strategies/windows_strategy.dart
@@ -0,0 +1,109 @@
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+import 'package:freegosy/core/windows/windows_game_service.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class WindowsStrategy extends EmulatorStrategy {
+  final WindowsGameService _windowsGameService;
+
+  // Manual exe overrides per game id
+  final Map<String, String> _exeOverrides = {};
+
+  WindowsStrategy(DirectoryService directoryService)
+      : _windowsGameService = WindowsGameService();
+
+  @override
+  String get name => 'Windows';
+
+  @override
+  String get emulatorId => 'windows_native';
+
+  @override
+  List<String> get supportedSlugs => ['windows', 'pc', 'win'];
+
+  @override
+  String get windowsExecutable => '';
+
+  @override
+  String get linuxExecutable => '';
+
+  @override
+  bool get supportsSaveSync => true;
+
+  static const String _prefsPrefix = 'win_exe_';
+
+  Future<void> loadPersistedOverrides() async {
+    final prefs = await SharedPreferences.getInstance();
+    final keys = prefs.getKeys().where((k) => k.startsWith(_prefsPrefix));
+    for (final key in keys) {
+      final gameId = key.substring(_prefsPrefix.length);
+      final path = prefs.getString(key);
+      if (path != null && path.isNotEmpty) _exeOverrides[gameId] = path;
+    }
+  }
+
+  Future<void> setExeOverride(String gameId, String exePath) async {
+    _exeOverrides[gameId] = exePath;
+    final prefs = await SharedPreferences.getInstance();
+    await prefs.setString('$_prefsPrefix$gameId', exePath);
+  }
+
+  String? getExeOverride(String gameId) => _exeOverrides[gameId];
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    // romPath for Windows games is the extracted game folder
+    String? exePath = _exeOverrides[game.id];
+
+    // If stored override no longer exists on disk, discard and auto-detect
+    if (exePath != null && exePath.isNotEmpty && !await File(exePath).exists()) {
+      debugPrint('[WindowsStrategy] override exe not found at $exePath, falling back to auto-detect');
+      exePath = null;
+    }
+
+    if (exePath == null || exePath.isEmpty) {
+      // Auto-detect exe in the game folder
+      final isDir = await Directory(romPath).exists();
+      final searchDir = isDir ? romPath : File(romPath).parent.path;
+      exePath = await _windowsGameService.findExecutable(
+        searchDir,
+        hint: game.name,
+      );
+    }
+
+    if (exePath == null) {
+      throw Exception(
+        'No executable found for ${game.name}. '
+        'Please set the exe path manually.',
+      );
+    }
+
+    debugPrint('[WindowsStrategy] launching: $exePath');
+    final process = await Process.start(
+      exePath,
+      [],
+      workingDirectory: File(exePath).parent.path,
+    );
+
+    // Wait up to 5 seconds — if process exits that fast it crashed
+    final exitCode = await process.exitCode
+        .timeout(const Duration(seconds: 5))
+        .catchError((_) => -99999); // timeout = still running = fine
+
+    if (exitCode != -99999 && exitCode != 0) {
+      debugPrint('[WindowsStrategy] ${game.name} exited with code $exitCode');
+      throw Exception(
+        '${game.name} crashed immediately (exit code $exitCode). '
+        'This is likely due to missing DirectX, Visual C++ redistributables, or other dependencies.',
+      );
+    }
+
+    debugPrint('[WindowsStrategy] ${game.name} still running after 5s — OK');
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/xemu_strategy.dart b/lib/core/emulator/strategies/xemu_strategy.dart
new file mode 100644
index 0000000..8d433e8
--- /dev/null
+++ b/lib/core/emulator/strategies/xemu_strategy.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+
+class XemuStrategy extends EmulatorStrategy {
+  final DirectoryService _directoryService;
+
+  XemuStrategy(this._directoryService);
+
+  @override
+  String get name => 'Xemu';
+
+  @override
+  String get emulatorId => 'xemu';
+
+  @override
+  List<String> get supportedSlugs => ['xbox'];
+
+  @override
+  String get windowsExecutable => 'xemu.exe';
+
+  @override
+  String get linuxExecutable => 'xemu';
+
+  @override
+  bool get supportsSaveSync => false;
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    await Process.start(exePath, ['-dvd_path', romPath], mode: ProcessStartMode.detached);
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategies/xenia_strategy.dart b/lib/core/emulator/strategies/xenia_strategy.dart
new file mode 100644
index 0000000..fac9475
--- /dev/null
+++ b/lib/core/emulator/strategies/xenia_strategy.dart
@@ -0,0 +1,40 @@
+import 'dart:io';
+import 'package:freegosy/core/emulator/emulator_strategy.dart';
+import 'package:freegosy/core/romm/romm_models.dart';
+import 'package:freegosy/core/storage/directory_service.dart';
+
+class XeniaStrategy extends EmulatorStrategy {
+  final DirectoryService _directoryService;
+
+  XeniaStrategy(this._directoryService);
+
+  @override
+  String get name => 'Xenia Canary';
+
+  @override
+  String get emulatorId => 'xenia_canary';
+
+  @override
+  List<String> get supportedSlugs => ['xbox360', 'xbla'];
+
+  @override
+  String get windowsExecutable => 'xenia_canary.exe';
+
+  @override
+  String get linuxExecutable => 'xenia_canary';
+
+  @override
+  bool get supportsSaveSync => false;
+
+  @override
+  Future<void> launch(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+      emulatorId, getExecutableForPlatform(),
+    );
+    if (exePath == null) throw Exception('$name not found. Please download it first.');
+    await Process.start(exePath, [romPath], mode: ProcessStartMode.detached);
+  }
+
+  @override
+  String resolveSavePath(Game game) => '';
+}
\ No newline at end of file
diff --git a/lib/core/emulator/strategy_registry.dart b/lib/core/emulator/strategy_registry.dart
index 57271de..8c46f8f 100644
--- a/lib/core/emulator/strategy_registry.dart
+++ b/lib/core/emulator/strategy_registry.dart
@@ -1,10 +1,18 @@
-import 'package:flutter/foundation.dart'; // For kIsWeb
+import 'package:flutter/foundation.dart';
 import 'package:freegosy/core/emulator/emulator_strategy.dart';
 import 'package:freegosy/core/emulator/strategies/retroarch_strategy.dart';
 import 'package:freegosy/core/emulator/strategies/dolphin_strategy.dart';
 import 'package:freegosy/core/emulator/strategies/eden_strategy.dart';
+import 'package:freegosy/core/emulator/strategies/rpcs3_strategy.dart';
+import 'package:freegosy/core/emulator/strategies/pcsx2_strategy.dart';
+import 'package:freegosy/core/emulator/strategies/azahar_strategy.dart';
+import 'package:freegosy/core/emulator/strategies/cemu_strategy.dart';
+import 'package:freegosy/core/emulator/strategies/duckstation_strategy.dart';
+import 'package:freegosy/core/emulator/strategies/xemu_strategy.dart';
+import 'package:freegosy/core/emulator/strategies/xenia_strategy.dart';
 import 'package:freegosy/core/emulator/emulator_registry_data.dart';
 import 'package:freegosy/core/storage/directory_service.dart';
+import 'package:freegosy/core/emulator/strategies/windows_strategy.dart';
 
 class StrategyRegistry {
   final DirectoryService _directoryService;
@@ -15,14 +23,19 @@ class StrategyRegistry {
       RetroArchStrategy(_directoryService),
       DolphinStrategy(_directoryService),
       EdenStrategy(_directoryService),
+      Rpcs3Strategy(_directoryService),
+      Pcsx2Strategy(_directoryService),
+      AzaharStrategy(_directoryService),
+      CemuStrategy(_directoryService),
+      DuckstationStrategy(_directoryService),
+      XemuStrategy(_directoryService),
+      XeniaStrategy(_directoryService),
+      WindowsStrategy(_directoryService),
     ];
   }
 
   EmulatorStrategy? getStrategyForSlug(String platformSlug) {
-    if (kIsWeb) {
-      return null;
-    }
-
+    if (kIsWeb) return null;
     for (final strategy in _strategies) {
       if (strategy.supportedSlugs.contains(platformSlug)) {
         return strategy;
@@ -38,4 +51,4 @@ class StrategyRegistry {
       return null;
     }
   }
-}
+}
\ No newline at end of file
diff --git a/lib/core/save/save_sync_service.dart b/lib/core/save/save_sync_service.dart
index f9d24d3..d7806f9 100644
--- a/lib/core/save/save_sync_service.dart
+++ b/lib/core/save/save_sync_service.dart
@@ -1,5 +1,5 @@
 import 'dart:io';
-import 'package:flutter/rendering.dart';
+import 'package:flutter/foundation.dart';
 import '../romm/romm_models.dart';
 import '../romm/romm_service.dart';
 import '../storage/directory_service.dart';
@@ -8,6 +8,10 @@ import 'strategies/retroarch_save_strategy.dart';
 import 'strategies/dolphin_save_strategy.dart';
 import 'strategies/eden_save_strategy.dart';
 import 'package:archive/archive_io.dart';
+import 'strategies/windows_save_strategy.dart';
+import 'strategies/pcsx2_save_strategy.dart';
+import 'strategies/rpcs3_save_strategy.dart';
+import 'strategies/xenia_save_strategy.dart';
 
 class SaveSyncService {
   final RommService _rommService;
@@ -16,11 +20,18 @@ class SaveSyncService {
   late final RetroArchSaveStrategy _retroarch;
   late final DolphinSaveStrategy _dolphin;
   late final EdenSaveStrategy _eden;
+  late final WindowsSaveStrategy _windows;
+  late final Pcsx2SaveStrategy _pcsx2;
+  late final Rpcs3SaveStrategy _rpcs3;
+  late final XeniaSaveStrategy _xenia;
 
   SaveSyncService(this._rommService, this._directoryService) {
     _retroarch = RetroArchSaveStrategy(_directoryService);
     _dolphin = DolphinSaveStrategy(_directoryService);
     _eden = EdenSaveStrategy();
+    _windows = WindowsSaveStrategy();
+    _pcsx2 = Pcsx2SaveStrategy(_directoryService);
+    _rpcs3 = Rpcs3SaveStrategy();
   }
 
   /// Returns the appropriate save strategy for [platformSlug], or null if unsupported.
@@ -43,15 +54,43 @@ class SaveSyncService {
       case 'genesis':
       case 'md':
         return _retroarch;
+      case 'gc':
       case 'ngc':
       case 'gamecube':
       case 'wii':
-      case 'gc':
         return _dolphin;
       case 'switch':
       case 'nintendo-switch':
       case 'ns':
         return _eden;
+      case 'windows':
+      case 'pc':
+      case 'win':
+        return _windows;
+      // These emulators don't have save sync yet
+      case 'ps2':
+      case 'playstation-2':
+      case 'playstation2':
+        return _pcsx2;
+      case 'ps3':
+      case 'playstation-3':
+      case 'playstation3':
+        return _rpcs3;
+      case 'xbox360':
+      case 'xbla':
+        return _xenia;
+      case '3ds':
+      case 'n3ds':
+      case 'nintendo-3ds':
+      case 'nintendo3ds':
+      case 'new-nintendo-3ds':
+      case 'new-nintendo-3ds-xl':
+      case 'wiiu':
+      case 'wii-u':
+      case 'nintendo-wii-u':
+      case 'nintendo-wiiu':
+      case 'xbox':
+        return null;
       default:
         return null;
     }
@@ -150,4 +189,6 @@ class SaveSyncService {
       rethrow;
     }
   }
+
+  WindowsSaveStrategy get windowsSaveStrategy => _windows;
 }
diff --git a/lib/core/save/strategies/pcsx2_save_strategy.dart b/lib/core/save/strategies/pcsx2_save_strategy.dart
new file mode 100644
index 0000000..a042690
--- /dev/null
+++ b/lib/core/save/strategies/pcsx2_save_strategy.dart
@@ -0,0 +1,89 @@
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import '../../romm/romm_models.dart';
+import '../../storage/directory_service.dart';
+import '../save_strategy.dart';
+
+/// Save strategy for PCSX2 (PlayStation 2).
+/// Memcards: {emulatorDir}/memcards/Mcd001.ps2, Mcd002.ps2
+/// States:   {emulatorDir}/sstates/{stem}.000 etc.
+class Pcsx2SaveStrategy extends SaveStrategy {
+  final DirectoryService _directoryService;
+
+  Pcsx2SaveStrategy(this._directoryService);
+
+  @override
+  String get strategyId => 'pcsx2';
+
+  @override
+  Future<String?> getSaveDir(Game game, String romPath) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+        'pcsx2', 'pcsx2-qt.exe');
+    if (exePath == null) return null;
+    return '${File(exePath).parent.path}/memcards';
+  }
+
+  @override
+  Future<List<File>> getSaveFiles(Game game, String romPath,
+      {DateTime? sessionStart, String syncMode = 'both'}) async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+        'pcsx2', 'pcsx2-qt.exe');
+    if (exePath == null) return [];
+    final exeDir = File(exePath).parent.path;
+
+    final candidates = <File>[];
+
+    // Memory cards — shared across all PS2 games
+    if (syncMode == 'saves' || syncMode == 'both') {
+      candidates.add(File('$exeDir/memcards/Mcd001.ps2'));
+      candidates.add(File('$exeDir/memcards/Mcd002.ps2'));
+    }
+
+    // Save states — named after ROM stem
+    if (syncMode == 'states' || syncMode == 'both') {
+      final stem = getRomStem(game);
+      final statesDir = '$exeDir/sstates';
+      for (int i = 0; i <= 9; i++) {
+        candidates.add(File('$statesDir/$stem.${'$i'.padLeft(3, '0')}'));
+      }
+    }
+
+    final result = <File>[];
+    for (final f in candidates) {
+      if (!await f.exists()) continue;
+      if (sessionStart != null) {
+        final stat = await f.stat();
+        if (stat.modified.isBefore(sessionStart)) continue;
+      }
+      result.add(f);
+    }
+    return result;
+  }
+
+  @override
+  Future<bool> restoreSave(
+      Game game, String destPath, Uint8List data, String filename) async {
+    try {
+      final exePath = await _directoryService.findEmulatorExecutable(
+          'pcsx2', 'pcsx2-qt.exe');
+      if (exePath == null) return false;
+      final exeDir = File(exePath).parent.path;
+
+      final isState = filename.contains('.') &&
+          int.tryParse(filename.split('.').last) != null;
+      final targetDir = isState ? '$exeDir/sstates' : '$exeDir/memcards';
+
+      final dir = Directory(targetDir);
+      if (!await dir.exists()) await dir.create(recursive: true);
+
+      final targetPath = '$targetDir/$filename';
+      await backupSave(targetPath);
+      await File(targetPath).writeAsBytes(data);
+      debugPrint('[Pcsx2SaveStrategy] restored $filename to $targetPath');
+      return true;
+    } catch (e) {
+      debugPrint('[Pcsx2SaveStrategy] restoreSave error: $e');
+      return false;
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/core/save/strategies/rpcs3_save_strategy.dart b/lib/core/save/strategies/rpcs3_save_strategy.dart
new file mode 100644
index 0000000..3361212
--- /dev/null
+++ b/lib/core/save/strategies/rpcs3_save_strategy.dart
@@ -0,0 +1,122 @@
+import 'dart:io';
+import 'package:archive/archive_io.dart';
+import 'package:flutter/foundation.dart';
+import '../../romm/romm_models.dart';
+import '../save_strategy.dart';
+
+/// Save strategy for RPCS3 (PlayStation 3).
+/// Saves: %APPDATA%\rpcs3\dev_hdd0\home\00000001\savedata\{titleId}\
+class Rpcs3SaveStrategy extends SaveStrategy {
+  @override
+  String get strategyId => 'rpcs3';
+
+  Future<String?> _getRpcs3SaveRoot() async {
+    try {
+      final result = await Process.run(
+        'cmd', ['/c', 'echo %APPDATA%'],
+        runInShell: false,
+      );
+      final appData = result.stdout.toString().trim();
+      if (appData.isEmpty || appData.contains('%APPDATA%')) return null;
+      return '$appData\\rpcs3\\dev_hdd0\\home\\00000001\\savedata';
+    } catch (e) {
+      debugPrint('[Rpcs3SaveStrategy] failed to get APPDATA: $e');
+      return null;
+    }
+  }
+
+  /// PS3 title IDs are in the ROM folder name e.g. BLES01234
+  String? _extractTitleId(Game game) {
+    final name = game.fsName ?? game.fileName ?? game.name;
+    final match = RegExp(r'[A-Z]{4}\d{5}').firstMatch(name.toUpperCase());
+    return match?.group(0);
+  }
+
+  @override
+  Future<String?> getSaveDir(Game game, String romPath) async {
+    final saveRoot = await _getRpcs3SaveRoot();
+    if (saveRoot == null) return null;
+    final titleId = _extractTitleId(game);
+    if (titleId == null) return null;
+    return '$saveRoot\\$titleId';
+  }
+
+  @override
+  Future<List<File>> getSaveFiles(Game game, String romPath,
+      {DateTime? sessionStart, String syncMode = 'both'}) async {
+    final saveRoot = await _getRpcs3SaveRoot();
+    if (saveRoot == null) return [];
+
+    final titleId = _extractTitleId(game);
+    if (titleId == null) {
+      debugPrint('[Rpcs3SaveStrategy] could not extract title ID for ${game.name}');
+      return [];
+    }
+
+    final saveDir = Directory('$saveRoot\\$titleId');
+    if (!await saveDir.exists()) return [];
+
+    // Package entire save folder as zip (same approach as Windows saves)
+    bool hasFiles = false;
+    await for (final entity in saveDir.list(recursive: true)) {
+      if (entity is! File) continue;
+      if (sessionStart != null) {
+        final stat = await entity.stat();
+        if (stat.modified.isBefore(sessionStart)) continue;
+      }
+      hasFiles = true;
+      break;
+    }
+    if (!hasFiles) return [];
+
+    final zipPath = '${saveDir.path}.saves.zip';
+    final encoder = ZipFileEncoder();
+    encoder.create(zipPath);
+    await encoder.addDirectory(saveDir);
+    encoder.close();
+
+    debugPrint('[Rpcs3SaveStrategy] packaged saves to $zipPath');
+    return [File(zipPath)];
+  }
+
+  @override
+  Future<bool> restoreSave(
+      Game game, String destPath, Uint8List data, String filename) async {
+    try {
+      final saveRoot = await _getRpcs3SaveRoot();
+      if (saveRoot == null) return false;
+
+      final titleId = _extractTitleId(game);
+      if (titleId == null) return false;
+
+      final saveDir = '$saveRoot\\$titleId';
+      await Directory(saveDir).create(recursive: true);
+
+      if (filename.toLowerCase().endsWith('.zip')) {
+        final archive = ZipDecoder().decodeBytes(data);
+        for (final entry in archive) {
+          final entryPath = '$saveDir\\${entry.name}';
+          if (entry.isFile) {
+            await backupSave(entryPath);
+            final outFile = File(entryPath);
+            await outFile.parent.create(recursive: true);
+            await outFile.writeAsBytes(entry.content as List<int>);
+          } else {
+            await Directory(entryPath).create(recursive: true);
+          }
+        }
+        debugPrint('[Rpcs3SaveStrategy] extracted $filename to $saveDir');
+        return true;
+      }
+
+      final targetPath = '$saveDir\\$filename';
+      await backupSave(targetPath);
+      await File(targetPath).writeAsBytes(data);
+      debugPrint('[Rpcs3SaveStrategy] restored $filename to $targetPath');
+      return true;
+    } catch (e) {
+      debugPrint('[Rpcs3SaveStrategy] restoreSave error: $e');
+      return false;
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/core/save/strategies/windows_save_strategy.dart b/lib/core/save/strategies/windows_save_strategy.dart
new file mode 100644
index 0000000..68dfc3c
--- /dev/null
+++ b/lib/core/save/strategies/windows_save_strategy.dart
@@ -0,0 +1,135 @@
+import 'dart:io';
+import 'package:archive/archive_io.dart';
+import 'package:flutter/foundation.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../../romm/romm_models.dart';
+import '../../windows/pcgamingwiki_service.dart';
+import '../save_strategy.dart';
+import 'package:dio/dio.dart';
+
+class WindowsSaveStrategy extends SaveStrategy {
+  final PcGamingWikiService _wikiService;
+
+  // Manual override paths set by user per game id
+  final Map<String, String> _manualOverrides = {};
+
+  WindowsSaveStrategy() : _wikiService = PcGamingWikiService(Dio());
+
+  @override
+  String get strategyId => 'windows';
+
+  /// Allows the user to manually set a save path for a game.
+  static const String _prefsPrefix = 'win_save_';
+
+  Future<void> loadPersistedOverrides() async {
+    final prefs = await SharedPreferences.getInstance();
+    final keys = prefs.getKeys().where((k) => k.startsWith(_prefsPrefix));
+    for (final key in keys) {
+      final gameId = key.substring(_prefsPrefix.length);
+      final path = prefs.getString(key);
+      if (path != null && path.isNotEmpty) _manualOverrides[gameId] = path;
+    }
+  }
+
+  /// Allows the user to manually set a save path for a game.
+  Future<void> setManualOverride(String gameId, String path) async {
+    _manualOverrides[gameId] = path;
+    final prefs = await SharedPreferences.getInstance();
+    await prefs.setString('$_prefsPrefix$gameId', path);
+  }
+
+  String? getManualOverride(String gameId) => _manualOverrides[gameId];
+
+  @override
+  Future<String?> getSaveDir(Game game, String romPath) async {
+    // Manual override takes priority
+    final manual = _manualOverrides[game.id];
+    if (manual != null && manual.isNotEmpty) return manual;
+
+    // Try PCGamingWiki
+    try {
+      final locations = await _wikiService.getSaveLocations(game.name);
+      if (locations.isNotEmpty) {
+        debugPrint('[WindowsSaveStrategy] wiki found ${locations.length} locations for ${game.name}');
+        return locations.first['path'];
+      }
+    } catch (e) {
+      debugPrint('[WindowsSaveStrategy] wiki lookup failed: $e');
+    }
+
+    return null;
+  }
+
+  @override
+  Future<List<File>> getSaveFiles(Game game, String romPath, {DateTime? sessionStart, String syncMode = 'both'}) async {
+    final saveDir = await getSaveDir(game, romPath);
+    if (saveDir == null) return [];
+
+    final dir = Directory(saveDir);
+    if (!await dir.exists()) return [];
+
+    // Check if any files exist (respecting sessionStart)
+    bool hasFiles = false;
+    await for (final entity in dir.list(recursive: true)) {
+      if (entity is! File) continue;
+      if (sessionStart != null) {
+        final stat = await entity.stat();
+        if (stat.modified.isBefore(sessionStart)) continue;
+      }
+      hasFiles = true;
+      break;
+    }
+    if (!hasFiles) return [];
+
+    // Package the entire save directory as a single zip
+    final zipPath = '${dir.path}.saves.zip';
+    final encoder = ZipFileEncoder();
+    encoder.create(zipPath);
+    await encoder.addDirectory(dir);
+    encoder.close();
+
+    debugPrint('[WindowsSaveStrategy] packaged saves to $zipPath');
+    return [File(zipPath)];
+  }
+
+  @override
+  Future<bool> restoreSave(Game game, String destPath, Uint8List data, String filename) async {
+    try {
+      final saveDir = await getSaveDir(game, destPath);
+      if (saveDir == null) {
+        throw Exception('No save location found for ${game.name}. Set one manually in game settings.');
+      }
+
+      final dir = Directory(saveDir);
+      if (!await dir.exists()) await dir.create(recursive: true);
+
+      // Always extract zip into the save directory
+      if (filename.toLowerCase().endsWith('.zip')) {
+        final archive = ZipDecoder().decodeBytes(data);
+        for (final entry in archive) {
+          final entryPath = '$saveDir/${entry.name}';
+          if (entry.isFile) {
+            await backupSave(entryPath);
+            final outFile = File(entryPath);
+            await outFile.parent.create(recursive: true);
+            await outFile.writeAsBytes(entry.content as List<int>);
+          } else {
+            await Directory(entryPath).create(recursive: true);
+          }
+        }
+        debugPrint('[WindowsSaveStrategy] extracted $filename to $saveDir');
+        return true;
+      }
+
+      // Fallback for non-zip (single file)
+      final targetPath = '$saveDir/$filename';
+      await backupSave(targetPath);
+      await File(targetPath).writeAsBytes(data);
+      debugPrint('[WindowsSaveStrategy] restored $filename to $targetPath');
+      return true;
+    } catch (e) {
+      debugPrint('[WindowsSaveStrategy] restoreSave error: $e');
+      rethrow;
+    }
+  }
+}
diff --git a/lib/core/save/strategies/xenia_save_strategy.dart b/lib/core/save/strategies/xenia_save_strategy.dart
new file mode 100644
index 0000000..7958407
--- /dev/null
+++ b/lib/core/save/strategies/xenia_save_strategy.dart
@@ -0,0 +1,109 @@
+import 'dart:io';
+import 'package:archive/archive_io.dart';
+import 'package:flutter/foundation.dart';
+import '../../romm/romm_models.dart';
+import '../../storage/directory_service.dart';
+import '../save_strategy.dart';
+
+/// Save strategy for Xenia Canary (Xbox 360).
+/// Saves: {emulatorDir}\content\{titleId}\00000001\
+class XeniaSaveStrategy extends SaveStrategy {
+  final DirectoryService _directoryService;
+
+  XeniaSaveStrategy(this._directoryService);
+
+  @override
+  String get strategyId => 'xenia_canary';
+
+  /// Xbox 360 title IDs are 8 hex characters e.g. 4D5307E6
+  String? _extractTitleId(Game game) {
+    final name = game.fsName ?? game.fileName ?? game.name;
+    final match = RegExp(r'[0-9A-Fa-f]{8}').firstMatch(name);
+    return match?.group(0)?.toUpperCase();
+  }
+
+  Future<String?> _getContentDir() async {
+    final exePath = await _directoryService.findEmulatorExecutable(
+        'xenia_canary', 'xenia_canary.exe');
+    if (exePath == null) return null;
+    return '${File(exePath).parent.path}\\content';
+  }
+
+  @override
+  Future<String?> getSaveDir(Game game, String romPath) async {
+    final contentDir = await _getContentDir();
+    if (contentDir == null) return null;
+    final titleId = _extractTitleId(game);
+    if (titleId == null) return null;
+    return '$contentDir\\$titleId\\00000001';
+  }
+
+  @override
+  Future<List<File>> getSaveFiles(Game game, String romPath,
+      {DateTime? sessionStart, String syncMode = 'both'}) async {
+    final saveDir = await getSaveDir(game, romPath);
+    if (saveDir == null) return [];
+
+    final dir = Directory(saveDir);
+    if (!await dir.exists()) return [];
+
+    bool hasFiles = false;
+    await for (final entity in dir.list(recursive: true)) {
+      if (entity is! File) continue;
+      if (sessionStart != null) {
+        final stat = await entity.stat();
+        if (stat.modified.isBefore(sessionStart)) continue;
+      }
+      hasFiles = true;
+      break;
+    }
+    if (!hasFiles) return [];
+
+    // Package entire save folder as zip
+    final zipPath = '${dir.path}.saves.zip';
+    final encoder = ZipFileEncoder();
+    encoder.create(zipPath);
+    await encoder.addDirectory(dir);
+    encoder.close();
+
+    debugPrint('[XeniaSaveStrategy] packaged saves to $zipPath');
+    return [File(zipPath)];
+  }
+
+  @override
+  Future<bool> restoreSave(
+      Game game, String destPath, Uint8List data, String filename) async {
+    try {
+      final saveDir = await getSaveDir(game, destPath);
+      if (saveDir == null) return false;
+
+      await Directory(saveDir).create(recursive: true);
+
+      if (filename.toLowerCase().endsWith('.zip')) {
+        final archive = ZipDecoder().decodeBytes(data);
+        for (final entry in archive) {
+          final entryPath = '$saveDir\\${entry.name}';
+          if (entry.isFile) {
+            await backupSave(entryPath);
+            final outFile = File(entryPath);
+            await outFile.parent.create(recursive: true);
+            await outFile.writeAsBytes(entry.content as List<int>);
+          } else {
+            await Directory(entryPath).create(recursive: true);
+          }
+        }
+        debugPrint('[XeniaSaveStrategy] extracted $filename to $saveDir');
+        return true;
+      }
+
+      final targetPath = '$saveDir\\$filename';
+      await backupSave(targetPath);
+      await File(targetPath).writeAsBytes(data);
+      debugPrint('[XeniaSaveStrategy] restored $filename to $targetPath');
+      return true;
+    } catch (e) {
+      debugPrint('[XeniaSaveStrategy] restoreSave error: $e');
+      return false;
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/core/storage/directory_service.dart b/lib/core/storage/directory_service.dart
index 303cfc9..2ee7855 100644
--- a/lib/core/storage/directory_service.dart
+++ b/lib/core/storage/directory_service.dart
@@ -1,4 +1,6 @@
 import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:freegosy/core/romm/romm_models.dart';
 
@@ -95,7 +97,11 @@ class DirectoryService {
     final folderName = game.name.replaceAll(RegExp(r'[<>:"/\\|?*]'), '_');
     final multiFileDir = Directory('$romDir/$folderName');
     if (await multiFileDir.exists()) {
-      // Find largest file inside — that's the main ROM
+      // Windows games: return the folder itself so WindowsStrategy can find the exe
+      final isWindowsGame = ['windows', 'pc', 'win'].contains(game.platformSlug?.toLowerCase() ?? '');
+      if (isWindowsGame) return multiFileDir.path;
+
+      // Other platforms: find largest file inside — that's the main ROM
       File? largestFile;
       int largestSize = 0;
       await for (final entity in multiFileDir.list(recursive: true)) {
@@ -135,6 +141,40 @@ class DirectoryService {
     return null;
   }
 
+  Future<String?> resolveSevenZipPath() async {
+    String appData = '';
+
+    if (defaultTargetPlatform == TargetPlatform.windows) {
+      try {
+        final result = await Process.run('cmd', ['/c', 'echo %APPDATA%'], runInShell: false);
+        appData = result.stdout.toString().trim();
+      } catch (e) {
+        debugPrint('[DirectoryService] failed to get APPDATA: $e');
+        return null;
+      }
+      if (appData.isEmpty || appData.contains('%APPDATA%')) return null;
+    } else {
+      // macOS/Linux: no 7zr needed, system 7z is installable via package managers
+      // Return null for now — future: resolve via `which 7z`
+      return null;
+    }
+
+    final dest = File('$appData\\Freegosy\\thirdparty\\7zr.exe');
+
+    if (await dest.exists()) return dest.path;
+
+    try {
+      await dest.parent.create(recursive: true);
+      final byteData = await rootBundle.load('thirdparty/7zr.exe');
+      await dest.writeAsBytes(byteData.buffer.asUint8List());
+      debugPrint('[DirectoryService] 7zr.exe extracted to ${dest.path}');
+      return dest.path;
+    } catch (e) {
+      debugPrint('[DirectoryService] failed to extract 7zr.exe: $e');
+      return null;
+    }
+  }
+
   Future<String> getEmulatorDirectory(String emulatorId) async {
     final dirPath = '$emulatorsRootPath/$emulatorId';
     await _ensureDirectoryExists(dirPath);
diff --git a/lib/core/windows/pcgamingwiki_service.dart b/lib/core/windows/pcgamingwiki_service.dart
new file mode 100644
index 0000000..bb74ec2
--- /dev/null
+++ b/lib/core/windows/pcgamingwiki_service.dart
@@ -0,0 +1,202 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'dart:io';
+
+class PcGamingWikiService {
+  final Dio _dio;
+
+  PcGamingWikiService(this._dio);
+
+  static const String _apiUrl = 'https://www.pcgamingwiki.com/w/api.php';
+
+  /// Finds the PCGamingWiki page title for [gameTitle].
+  Future<String?> findPageTitle(String gameTitle) async {
+    try {
+      // Try exact match first
+      final exactResponse = await _dio.get(_apiUrl, queryParameters: {
+        'action': 'query',
+        'titles': gameTitle,
+        'format': 'json',
+      });
+
+      if (exactResponse.statusCode == 200) {
+        final pages = exactResponse.data['query']['pages'] as Map;
+        for (final pageId in pages.keys) {
+          if (pageId != '-1') return pages[pageId]['title'] as String;
+        }
+      }
+
+      // Fall back to search
+      final searchResponse = await _dio.get(_apiUrl, queryParameters: {
+        'action': 'query',
+        'list': 'search',
+        'srsearch': gameTitle,
+        'format': 'json',
+      });
+
+      if (searchResponse.statusCode == 200) {
+        final results = searchResponse.data['query']['search'] as List;
+        if (results.isNotEmpty) return results.first['title'] as String;
+      }
+    } catch (e) {
+      debugPrint('[PcGamingWiki] findPageTitle error: $e');
+    }
+    return null;
+  }
+
+  /// Fetches the wikitext for [pageTitle].
+  Future<String?> getWikitext(String pageTitle) async {
+    try {
+      final response = await _dio.get(_apiUrl, queryParameters: {
+        'action': 'parse',
+        'page': pageTitle,
+        'prop': 'wikitext',
+        'format': 'json',
+      });
+      if (response.statusCode == 200) {
+        return response.data['parse']['wikitext']['*'] as String?;
+      }
+    } catch (e) {
+      debugPrint('[PcGamingWiki] getWikitext error: $e');
+    }
+    return null;
+  }
+
+  /// Returns expanded save locations for [gameTitle].
+  Future<List<Map<String, String>>> getSaveLocations(String gameTitle, {String gameDir = ''}) async {
+    try {
+      final pageTitle = await findPageTitle(gameTitle);
+      if (pageTitle == null) return [];
+
+      final wikitext = await getWikitext(pageTitle);
+      if (wikitext == null) return [];
+
+      return _parseSaveLocations(wikitext, gameTitle, gameDir);
+    } catch (e) {
+      debugPrint('[PcGamingWiki] getSaveLocations error: $e');
+      return [];
+    }
+  }
+
+  List<Map<String, String>> _parseSaveLocations(String wikitext, String gameTitle, String gameDir) {
+    final results = <Map<String, String>>[];
+    final seen = <String>{};
+
+    for (final line in wikitext.split('\n')) {
+      if (!line.contains('Game data/saves')) continue;
+      if (!line.contains('|Windows|')) continue;
+
+      try {
+        final after = line.split('|Windows|').last;
+        final cleaned = after.endsWith('}}') ? after.substring(0, after.length - 2) : after;
+        final paths = _safeSplitPaths(cleaned.trim());
+
+        for (final raw in paths) {
+          final trimmed = raw.trim();
+          if (trimmed.isEmpty) continue;
+
+          final lower = trimmed.toLowerCase();
+          if (_shouldSkipPath(lower)) continue;
+
+          final expanded = _expandWikiPath(trimmed, gameTitle, gameDir);
+          if (expanded == null) continue;
+
+          final normalizedLower = expanded.toLowerCase();
+          if (seen.contains(normalizedLower)) continue;
+          seen.add(normalizedLower);
+
+          results.add({
+            'raw': trimmed,
+            'path': expanded,
+          });
+        }
+      } catch (e) {
+        debugPrint('[PcGamingWiki] parse error on line: $e');
+      }
+    }
+    return results;
+  }
+
+  bool _shouldSkipPath(String lower) {
+    return lower.contains('steam') ||
+        lower.contains('linux') ||
+        lower.contains('wine') ||
+        lower.contains('{{p|uid}}') ||
+        lower.contains('{{p|hkcu}}') ||
+        lower.contains('{{p|osxhome}}') ||
+        lower.contains('{{p|xdg') ||
+        lower.contains('{{p|linux');
+  }
+
+  List<String> _safeSplitPaths(String s) {
+    final parts = <String>[];
+    int depth = 0;
+    final current = StringBuffer();
+    int i = 0;
+    while (i < s.length) {
+      if (i + 1 < s.length && s[i] == '{' && s[i + 1] == '{') {
+        depth++;
+        current.write('{{');
+        i += 2;
+        continue;
+      }
+      if (i + 1 < s.length && s[i] == '}' && s[i + 1] == '}') {
+        depth--;
+        current.write('}}');
+        i += 2;
+        continue;
+      }
+      if (s[i] == '|' && depth == 0) {
+        parts.add(current.toString().trim());
+        current.clear();
+        i++;
+        continue;
+      }
+      current.write(s[i]);
+      i++;
+    }
+    if (current.isNotEmpty) parts.add(current.toString().trim());
+    return parts.where((p) => p.isNotEmpty).toList();
+  }
+
+  String? _expandWikiPath(String path, String gameTitle, String gameDir) {
+    final appData = Platform.environment['APPDATA'] ?? '';
+    final localAppData = Platform.environment['LOCALAPPDATA'] ?? '';
+    final userProfile = Platform.environment['USERPROFILE'] ?? '';
+    final programData = Platform.environment['PROGRAMDATA'] ?? '';
+    final public = Platform.environment['PUBLIC'] ?? '';
+
+    final subs = <String, String>{
+      '{{p|appdata}}': appData,
+      '{{p|localappdata}}': localAppData,
+      '{{p|userprofile}}': userProfile,
+      '{{p|programdata}}': programData,
+      '{{p|public}}': public,
+      '{{p|game}}': gameDir.isNotEmpty ? '$gameDir/$gameTitle' : '',
+    };
+
+    String expanded = path;
+    for (final entry in subs.entries) {
+      if (entry.value.isEmpty && expanded.toLowerCase().contains(entry.key)) {
+        return null;
+      }
+      expanded = expanded.replaceAll(
+        RegExp(RegExp.escape(entry.key), caseSensitive: false),
+        entry.value,
+      );
+    }
+
+    // If any unresolved templates remain, skip
+    if (expanded.toLowerCase().contains('{{p|')) return null;
+
+    // Strip wildcard filenames e.g. \*.dat
+    expanded = expanded.replaceAll(RegExp(r'[/\\]\*\.[a-zA-Z0-9]+$'), '');
+
+    // Strip bare filenames with extension at end
+    if (RegExp(r'\.[a-zA-Z0-9]{2,4}$').hasMatch(expanded)) {
+      expanded = File(expanded).parent.path;
+    }
+
+    return expanded.replaceAll('/', '\\');
+  }
+}
\ No newline at end of file
diff --git a/lib/core/windows/windows_game_service.dart b/lib/core/windows/windows_game_service.dart
new file mode 100644
index 0000000..db76fb2
--- /dev/null
+++ b/lib/core/windows/windows_game_service.dart
@@ -0,0 +1,73 @@
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:path/path.dart' as p;
+
+class WindowsGameService {
+  /// Finds the main executable in [gameDir].
+  /// First tries to match [hint] (game name), then falls back to largest .exe.
+  Future<String?> findExecutable(String gameDir, {String? hint}) async {
+    final dir = Directory(gameDir);
+    if (!await dir.exists()) return null;
+
+    final exeFiles = <File>[];
+    await for (final entity in dir.list(recursive: true)) {
+      if (entity is File && entity.path.toLowerCase().endsWith('.exe')) {
+        // Skip known non-game executables
+        final name = p.basename(entity.path).toLowerCase();
+        if (_shouldSkipExe(name)) continue;
+        exeFiles.add(entity);
+      }
+    }
+
+    if (exeFiles.isEmpty) return null;
+
+    // Try hint match first (game name similarity)
+    if (hint != null) {
+      final hintLower = hint.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
+      for (final exe in exeFiles) {
+        final exeName = p.basenameWithoutExtension(exe.path)
+            .toLowerCase()
+            .replaceAll(RegExp(r'[^a-z0-9]'), '');
+        if (exeName.contains(hintLower) || hintLower.contains(exeName)) {
+          debugPrint('[WindowsGameService] matched exe by hint: ${exe.path}');
+          return exe.path;
+        }
+      }
+    }
+
+    // Fall back to largest exe
+    File? largest;
+    int largestSize = 0;
+    for (final exe in exeFiles) {
+      final size = await exe.length();
+      if (size > largestSize) {
+        largestSize = size;
+        largest = exe;
+      }
+    }
+
+    debugPrint('[WindowsGameService] largest exe: ${largest?.path}');
+    return largest?.path;
+  }
+
+  bool _shouldSkipExe(String name) {
+    const skipList = [
+      'uninstall', 'uninst', 'setup', 'install', 'redist',
+      'vc_redist', 'vcredist', 'directx', 'dxsetup',
+      'dotnet', 'crashreport', 'crashhandler', 'bugsplat',
+      'upc', 'easyanticheat', 'battleye', 'launcher_helper',
+    ];
+    return skipList.any((s) => name.contains(s));
+  }
+
+  /// Launches the exe at [exePath].
+  Future<void> launch(String exePath) async {
+    final dir = File(exePath).parent.path;
+    await Process.start(
+      exePath,
+      [],
+      workingDirectory: dir,
+      mode: ProcessStartMode.detached,
+    );
+  }
+}
\ No newline at end of file
diff --git a/lib/providers/romm_provider.dart b/lib/providers/romm_provider.dart
index 6c14d0c..989865d 100644
--- a/lib/providers/romm_provider.dart
+++ b/lib/providers/romm_provider.dart
@@ -6,6 +6,8 @@ import 'package:freegosy/core/romm/romm_models.dart';
 import 'package:freegosy/core/romm/romm_service.dart';
 import 'package:freegosy/core/emulator/strategy_registry.dart';
 import 'package:freegosy/core/save/save_sync_service.dart';
+import 'package:freegosy/core/emulator/strategies/windows_strategy.dart';
+
 
 // Provider for loading RomMConfig (including stored Bearer token)
 final rommConfigProvider = FutureProvider<RomMConfig>((ref) async {
@@ -44,7 +46,13 @@ final strategyRegistryProvider = Provider<StrategyRegistry?>((ref) {
   final directoryService = ref.watch(directoryServiceProvider).value;
   if (directoryService != null) {
     try {
-      return StrategyRegistry(directoryService);
+      final registry = StrategyRegistry(directoryService);
+      // Load persisted Windows exe overrides
+      final winStrategy = registry.getStrategyForSlug('windows');
+      if (winStrategy is WindowsStrategy) {
+        winStrategy.loadPersistedOverrides();
+      }
+      return registry;
     } catch (e) {
       return null;
     }
@@ -57,7 +65,10 @@ final saveSyncServiceProvider = Provider<SaveSyncService?>((ref) {
   final rommService = ref.watch(rommServiceProvider);
   final directoryService = ref.watch(directoryServiceProvider).asData?.value;
   if (rommService == null || directoryService == null) return null;
-  return SaveSyncService(rommService, directoryService);
+  final service = SaveSyncService(rommService, directoryService);
+  // Load persisted Windows save path overrides
+  service.windowsSaveStrategy.loadPersistedOverrides();
+  return service;
 });
 
 // Simplified RommService provider
diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart
index 1888a83..25155c5 100644
--- a/lib/ui/screens/library_screen.dart
+++ b/lib/ui/screens/library_screen.dart
@@ -6,6 +6,8 @@ import '../../providers/romm_provider.dart';
 import '../../core/romm/romm_models.dart';
 import '../widgets/game_card.dart';
 import '../widgets/platform_filter_bar.dart';
+import '../widgets/windows_game_config_dialog.dart';
+import '../../core/emulator/strategies/windows_strategy.dart';
 import 'dart:convert';
 
 class LibraryScreen extends ConsumerWidget {
@@ -124,17 +126,52 @@ class LibraryScreen extends ConsumerWidget {
       await strategy.launch(game, existingRomPath);
     } catch (e) {
       if (!context.mounted) return;
-      ScaffoldMessenger.of(context).showSnackBar(
-        SnackBar(content: Text('Launch failed: $e')),
-      );
+      final isWindows = ['windows', 'pc', 'win'].contains(game.platformSlug?.toLowerCase() ?? '');
+      final isMissingExe = e.toString().contains('No executable') || e.toString().contains('not found');
+      if (isWindows && isMissingExe) {
+        await _handleWindowsConfig(context, ref, game);
+      } else {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(
+            content: Text('Launch failed: $e'),
+            duration: const Duration(seconds: 8),
+            ),
+        );
+      }
     }
   }
 
+  Future<void> _handleWindowsConfig(BuildContext context, WidgetRef ref, Game game) async {
+    final registry = ref.read(strategyRegistryProvider);
+    final windowsStrategy = registry?.getStrategyForSlug(game.platformSlug ?? '') as WindowsStrategy?;
+    final syncService = ref.read(saveSyncServiceProvider);
+
+    final result = await showDialog<Map<String, String>>(
+      context: context,
+      builder: (ctx) => WindowsGameConfigDialog(
+        game: game,
+        currentExePath: windowsStrategy?.getExeOverride(game.id),
+        currentSavePath: syncService?.windowsSaveStrategy.getManualOverride(game.id),
+      ),
+    );
+    if (result == null) return; // user cancelled
+
+    final exe = result['exe'] ?? '';
+    final save = result['save'] ?? '';
+
+    if (exe.isNotEmpty) await windowsStrategy?.setExeOverride(game.id, exe);
+    if (save.isNotEmpty) await syncService?.windowsSaveStrategy.setManualOverride(game.id, save);
+
+    if (!context.mounted) return;
+    await _handleLaunch(context, ref, game);
+  }
+
   Future<void> _handleSyncSaves(BuildContext context, WidgetRef ref, Game game) async {
     final syncService = ref.read(saveSyncServiceProvider);
     if (syncService == null) {
       ScaffoldMessenger.of(context).showSnackBar(
-        const SnackBar(content: Text('Save sync not available')),
+        const SnackBar(
+          content: Text('Save sync not available')),
       );
       return;
     }
@@ -276,23 +313,30 @@ class LibraryScreen extends ConsumerWidget {
                               itemBuilder: (context, index) {
                                 final game = filteredGames[index];
                                 final dirService = directoryServiceAsync.asData?.value;
+                                final isWindowsGame = ['windows', 'pc', 'win'].contains(game.platformSlug?.toLowerCase() ?? '');
                                 if (dirService == null) {
-                                  return GameCard(
-                                    game: game,
-                                    onDownload: () => _startDownload(context, ref, game),
-                                    onLaunch: () => _handleLaunch(context, ref, game),
-                                    onSyncSaves: () => _handleSyncSaves(context, ref, game),
+                                  return GestureDetector(
+                                    onLongPress: isWindowsGame ? () => _handleWindowsConfig(context, ref, game) : null,
+                                    child: GameCard(
+                                      game: game,
+                                      onDownload: () => _startDownload(context, ref, game),
+                                      onLaunch: () => _handleLaunch(context, ref, game),
+                                      onSyncSaves: () => _handleSyncSaves(context, ref, game),
+                                    ),
                                   );
                                 }
                                 return FutureBuilder<bool>(
                                   future: dirService.isRomDownloaded(game),
                                   builder: (context, snapshot) {
-                                    return GameCard(
-                                      game: game,
-                                      isDownloaded: snapshot.data ?? false,
-                                      onDownload: () => _startDownload(context, ref, game),
-                                      onLaunch: () => _handleLaunch(context, ref, game),
-                                      onSyncSaves: () => _handleSyncSaves(context, ref, game),
+                                    return GestureDetector(
+                                      onLongPress: isWindowsGame ? () => _handleWindowsConfig(context, ref, game) : null,
+                                      child: GameCard(
+                                        game: game,
+                                        isDownloaded: snapshot.data ?? false,
+                                        onDownload: () => _startDownload(context, ref, game),
+                                        onLaunch: () => _handleLaunch(context, ref, game),
+                                        onSyncSaves: () => _handleSyncSaves(context, ref, game),
+                                      ),
                                     );
                                   },
                                 );
diff --git a/lib/ui/widgets/windows_game_config_dialog.dart b/lib/ui/widgets/windows_game_config_dialog.dart
new file mode 100644
index 0000000..e2863f8
--- /dev/null
+++ b/lib/ui/widgets/windows_game_config_dialog.dart
@@ -0,0 +1,137 @@
+import 'package:flutter/material.dart';
+import 'package:file_picker/file_picker.dart';
+import '../../core/romm/romm_models.dart';
+
+class WindowsGameConfigDialog extends StatefulWidget {
+  final Game game;
+  final String? currentExePath;
+  final String? currentSavePath;
+
+  const WindowsGameConfigDialog({
+    super.key,
+    required this.game,
+    this.currentExePath,
+    this.currentSavePath,
+  });
+
+  @override
+  State<WindowsGameConfigDialog> createState() => _WindowsGameConfigDialogState();
+}
+
+class _WindowsGameConfigDialogState extends State<WindowsGameConfigDialog> {
+  late TextEditingController _exeController;
+  late TextEditingController _saveController;
+
+  @override
+  void initState() {
+    super.initState();
+    _exeController = TextEditingController(text: widget.currentExePath ?? '');
+    _saveController = TextEditingController(text: widget.currentSavePath ?? '');
+  }
+
+  @override
+  void dispose() {
+    _exeController.dispose();
+    _saveController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: Text('Configure ${widget.game.name}'),
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          const Text(
+            'Executable (.exe)',
+            style: TextStyle(fontWeight: FontWeight.bold),
+          ),
+          const SizedBox(height: 4),
+          Row(
+            children: [
+              Expanded(
+                child: TextField(
+                  controller: _exeController,
+                  decoration: const InputDecoration(
+                    hintText: 'Auto-detect or browse...',
+                    border: OutlineInputBorder(),
+                    isDense: true,
+                  ),
+                  style: const TextStyle(fontSize: 12),
+                ),
+              ),
+              const SizedBox(width: 8),
+              ElevatedButton(
+                onPressed: () async {
+                  final result = await FilePicker.platform.pickFiles(
+                    type: FileType.custom,
+                    allowedExtensions: ['exe'],
+                  );
+                  if (result != null && result.files.single.path != null) {
+                    setState(() {
+                      _exeController.text = result.files.single.path!;
+                    });
+                  }
+                },
+                child: const Text('Browse'),
+              ),
+            ],
+          ),
+          const SizedBox(height: 16),
+          const Text(
+            'Save Directory',
+            style: TextStyle(fontWeight: FontWeight.bold),
+          ),
+          const SizedBox(height: 4),
+          const Text(
+            'Leave empty to use PCGamingWiki auto-detection',
+            style: TextStyle(fontSize: 11, color: Colors.grey),
+          ),
+          const SizedBox(height: 4),
+          Row(
+            children: [
+              Expanded(
+                child: TextField(
+                  controller: _saveController,
+                  decoration: const InputDecoration(
+                    hintText: 'Auto-detect or browse...',
+                    border: OutlineInputBorder(),
+                    isDense: true,
+                  ),
+                  style: const TextStyle(fontSize: 12),
+                ),
+              ),
+              const SizedBox(width: 8),
+              ElevatedButton(
+                onPressed: () async {
+                  final path = await FilePicker.platform.getDirectoryPath();
+                  if (path != null) {
+                    setState(() {
+                      _saveController.text = path;
+                    });
+                  }
+                },
+                child: const Text('Browse'),
+              ),
+            ],
+          ),
+        ],
+      ),
+      actions: [
+        TextButton(
+          onPressed: () => Navigator.of(context).pop(null),
+          child: const Text('Cancel'),
+        ),
+        ElevatedButton(
+          onPressed: () => Navigator.of(context).pop({
+            'exe': _exeController.text.trim(),
+            'save': _saveController.text.trim(),
+          }),
+          child: const Text('Save'),
+        ),
+      ],
+    );
+  }
+}
\ No newline at end of file
diff --git a/pubspec.yaml b/pubspec.yaml
index 8f7eef8..ebbfe54 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -59,7 +59,8 @@ dev_dependencies:
 
 # The following section is specific to Flutter packages.
 flutter:
-
+  assets:
+    - thirdparty/7zr.exe
   # The following line ensures that the Material Icons font is
   # included with your application, so that you can use the icons in
   # the material Icons class.
diff --git a/thirdparty/7zr.exe b/thirdparty/7zr.exe
new file mode 100644
index 0000000..2934156
Binary files /dev/null and b/thirdparty/7zr.exe differ

Clone this wiki locally