mirror of
https://github.com/skoelle/marcer-gamedvd-launcher.git
synced 2026-09-17 18:50:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d9a73ebf6 | ||
|
|
0cc389ff5d | ||
|
|
305588da8f | ||
|
|
bc39039c3c | ||
|
|
d5d7cfb7c4 | ||
|
|
12586262cb | ||
|
|
9c65dafdcf | ||
|
|
03f7e5eed4 | ||
|
|
6e32223eef | ||
|
|
2bb2b09c1d | ||
|
|
bc9bb8e862 | ||
|
|
4c7592582c | ||
|
|
5e2ab6581c | ||
|
|
e839c01a1e | ||
|
|
0117ba90c3 | ||
|
|
9649910df6 | ||
|
|
7971385beb | ||
|
|
410da6a98b | ||
|
|
dd17b60784 | ||
|
|
5d8db1347b | ||
|
|
7b40cbc331 | ||
|
|
4660cc6be8 | ||
|
|
148c2bb484 | ||
|
|
607d0df28c | ||
|
|
4b3321e812 | ||
|
|
6a4be2e397 | ||
|
|
61a8ae8457 | ||
|
|
5995a96516 | ||
|
|
e704261ea4 | ||
|
|
1634ce806c | ||
|
|
292b642d76 |
@@ -0,0 +1,6 @@
|
|||||||
|
title: "Marcer GameDVD Launcher"
|
||||||
|
emoji: "🕹️"
|
||||||
|
category: code
|
||||||
|
subcategory: "Retro"
|
||||||
|
status: active
|
||||||
|
stack: [C#, .NET, Console]
|
||||||
@@ -14,22 +14,27 @@ Create a release tag and push it to origin. The GitHub Action will automatically
|
|||||||
- Run `git status --porcelain`
|
- Run `git status --porcelain`
|
||||||
- If any output, warn the user and stop (commit first)
|
- If any output, warn the user and stop (commit first)
|
||||||
|
|
||||||
3. Analyze changes since last release:
|
3. Find the previous tag dynamically:
|
||||||
- Run `git log --oneline $(git describe --tags --abbrev=0 HEAD)..HEAD` to list all commits
|
- Run `git describe --tags --abbrev=0 HEAD` to find the most recent tag before HEAD
|
||||||
|
- If no previous tag exists (first release), note this and skip to step 5
|
||||||
|
- Report the previous tag to the user (e.g. "Previous tag found: v0.9.5")
|
||||||
|
|
||||||
|
4. Analyze changes since the previous tag:
|
||||||
|
- Run `git log --oneline <PREV_TAG>..HEAD` to list all commits between the previous tag and HEAD
|
||||||
- Read the changed files to understand context
|
- Read the changed files to understand context
|
||||||
- Write a concise, well-structured release summary in English with:
|
- Write a concise, well-structured release summary in English with:
|
||||||
- A one-line overview
|
- A one-line overview
|
||||||
- Bullet points for each notable change (features, fixes, breaking changes)
|
- Bullet points for each notable change (features, fixes, breaking changes)
|
||||||
- Keep it developer-friendly, no fluff
|
- Keep it developer-friendly, no fluff
|
||||||
|
|
||||||
4. Create release commit with the summary as message:
|
5. Create release commit with the summary as message:
|
||||||
- Run `git commit --allow-empty -m "release v$ARGUMENTS\n\n<summary>"`
|
- Run `git commit --allow-empty -m "release v$ARGUMENTS\n\n<summary>"`
|
||||||
- The commit message IS the release notes — the GitHub Action picks it up automatically
|
- The commit message IS the release notes — the GitHub Action picks it up automatically
|
||||||
|
|
||||||
5. Create annotated tag on that commit:
|
6. Create annotated tag on that commit:
|
||||||
- Run `git tag -a v$ARGUMENTS -m "Release v$ARGUMENTS"`
|
- Run `git tag -a v$ARGUMENTS -m "Release v$ARGUMENTS"`
|
||||||
|
|
||||||
6. Push commit and tag:
|
7. Push commit and tag:
|
||||||
- Run `git push origin main --tags` (or current branch)
|
- Run `git push origin main --tags` (or current branch)
|
||||||
|
|
||||||
7. Confirm success with the version number
|
8. Confirm success with the version number
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ The implementation is split into focused modules (files) under the `src/MarcerGa
|
|||||||
- MarcerGameDvdLauncher/OverlayDirectoryBrowser.cs: Filesystem overlay and browsing logic — merges root and patch directories, enumerates folders and ZIPs, protects against path traversal and ensures navigation cannot leave the configured roots.
|
- MarcerGameDvdLauncher/OverlayDirectoryBrowser.cs: Filesystem overlay and browsing logic — merges root and patch directories, enumerates folders and ZIPs, protects against path traversal and ensures navigation cannot leave the configured roots.
|
||||||
- MarcerGameDvdLauncher/NavigationController.cs: Encapsulates selection, scrolling and relative-path navigation logic (cursor, page up/down, per-directory remembered selection/state); uses named scroll-fraction constants.
|
- MarcerGameDvdLauncher/NavigationController.cs: Encapsulates selection, scrolling and relative-path navigation logic (cursor, page up/down, per-directory remembered selection/state); uses named scroll-fraction constants.
|
||||||
- MarcerGameDvdLauncher/MenuRenderer.cs: Console rendering logic — efficient per-line redraw, double-buffering, configurable color selection via injected `AppColorConfig`, and the help box overlay.
|
- MarcerGameDvdLauncher/MenuRenderer.cs: Console rendering logic — efficient per-line redraw, double-buffering, configurable color selection via injected `AppColorConfig`, and the help box overlay.
|
||||||
- MarcerGameDvdLauncher/InputController.cs: Handles key events (arrow keys, Enter, Backspace, PageUp/Down, `*`, `?`, ESC) and the associated navigation/drawing logic; owns `ReloadGameEntries` and the virtual `Favorites` folder integration.
|
- MarcerGameDvdLauncher/InputController.cs: Handles key events (arrow keys, Enter, Backspace, PageUp/Down, `*`, `?`, ESC, RightArrow) and the associated navigation/drawing logic; owns `ReloadGameEntries` and the virtual `Favorites` folder integration.
|
||||||
- MarcerGameDvdLauncher/HatariLauncher.cs: Responsible for validating the Hatari executable and starting Hatari with the configured argument template (replaces `{cfg}` and `{zip}`).
|
- MarcerGameDvdLauncher/HatariLauncher.cs: Responsible for validating the Hatari executable and starting Hatari with the configured argument template (replaces `{cfg}` and `{zip}`). Exposes `DefaultConfigFile` constant (`MarcerGameDvd-Hatari.cfg`).
|
||||||
- MarcerGameDvdLauncher/FavoritesService.cs: Manages the favorites/bookmark system — toggling favorites on ZIPs, persisting them to `favorites.txt` (via `DefaultFileName` constant), and providing the virtual `Favorites` folder view (via `FavoritesRootName` constant).
|
- MarcerGameDvdLauncher/FavoritesService.cs: Manages the favorites/bookmark system — toggling favorites on ZIPs, persisting them to `favorites.txt` (via `DefaultFileName` constant), and providing the virtual `Favorites` folder view (via `FavoritesRootName` constant).
|
||||||
- MarcerGameDvdLauncher/UIErrorService.cs: Centralized UI error presentation using the console message helper.
|
- MarcerGameDvdLauncher/UIErrorService.cs: Centralized UI error presentation using the console message helper.
|
||||||
|
|
||||||
@@ -32,15 +32,6 @@ The Launcher cannot be executed or tested via `scripts/start.cmd` from this envi
|
|||||||
4. The GitHub Action handles the rest (build, ZIP, release notes, GitHub Release).
|
4. The GitHub Action handles the rest (build, ZIP, release notes, GitHub Release).
|
||||||
- Local release artifacts in `release/` are optional and no longer required for the release process.
|
- Local release artifacts in `release/` are optional and no longer required for the release process.
|
||||||
|
|
||||||
|
|
||||||
Additional policy:
|
|
||||||
- README.md must be written in English. Any functional change that affects usage, configuration, or behavior MUST update README.md in English immediately after the change. If there are consequential changes to developer-facing policies, build steps, or requirements, AGENTS.md must be updated as well.
|
|
||||||
|
|
||||||
Developer note: Visual Studio Solution
|
|
||||||
- A Visual Studio solution file exists at `src/marcer-gamedvd-launcher.sln`. Developers may open this solution in Visual Studio to work on the project, debug and build from the IDE. The solution references `MarcerGameDvdLauncher\MarcerGameDvdLauncher.csproj` and includes Debug and Release configurations. Use `scripts/build.cmd` (Windows) or `scripts/build.sh` (Linux/macOS) and `scripts/start.cmd` (Windows) or `scripts/start.sh` (Linux/macOS) for consistent command-line builds/releases as described elsewhere in this document.
|
|
||||||
|
|
||||||
With this, it is ensured that binary/release files never end up in git, and the release process is always traceable and performed exclusively manually in the web interface.
|
|
||||||
|
|
||||||
# Requirements for the Marcer GameDVD Launcher (AGENTS.md)
|
# Requirements for the Marcer GameDVD Launcher (AGENTS.md)
|
||||||
|
|
||||||
## Basic Function / Purpose
|
## Basic Function / Purpose
|
||||||
@@ -55,8 +46,8 @@ The console launcher is meant for browsing a games directory and can launch ZIP
|
|||||||
- Only display subfolders and ZIP files; ignore other file types and hidden files.
|
- Only display subfolders and ZIP files; ignore other file types and hidden files.
|
||||||
- Navigation and control exclusively with these keys:
|
- Navigation and control exclusively with these keys:
|
||||||
- Arrow down/up: scroll by single entries
|
- Arrow down/up: scroll by single entries
|
||||||
- Enter: open folder or launch ZIP with Hatari
|
- Enter / RightArrow: open folder or launch ZIP with Hatari
|
||||||
- Backspace: jump to parent directory (never outside root)
|
- Backspace / LeftArrow: jump to parent directory (never outside root)
|
||||||
- ESC: exit the program
|
- ESC: exit the program
|
||||||
- PageUp/PageDown: jump by one page up/down through the file list
|
- PageUp/PageDown: jump by one page up/down through the file list
|
||||||
- `?`: show a help box with key bindings
|
- `?`: show a help box with key bindings
|
||||||
@@ -71,6 +62,7 @@ The console launcher is meant for browsing a games directory and can launch ZIP
|
|||||||
|
|
||||||
### Launching a Game
|
### Launching a Game
|
||||||
- If Enter is pressed on a ZIP, Hatari is called with the configured executable, config file, and the full path to the ZIP file as arguments.
|
- If Enter is pressed on a ZIP, Hatari is called with the configured executable, config file, and the full path to the ZIP file as arguments.
|
||||||
|
- `Hatari.ConfigFile` is optional. When empty, the bundled `MarcerGameDvd-Hatari.cfg` (shipped with the launcher) is used automatically.
|
||||||
|
|
||||||
### Error Handling & Edge Cases
|
### Error Handling & Edge Cases
|
||||||
- Paths with special characters or spaces must work robustly.
|
- Paths with special characters or spaces must work robustly.
|
||||||
@@ -78,7 +70,7 @@ The console launcher is meant for browsing a games directory and can launch ZIP
|
|||||||
- Empty directories must be displayed correctly (or reported correctly).
|
- Empty directories must be displayed correctly (or reported correctly).
|
||||||
- In the root directory, Backspace must have no effect (no error, do not leave the program).
|
- In the root directory, Backspace must have no effect (no error, do not leave the program).
|
||||||
- Navigation (Backspace, Enter, etc.) must remain robust even for very deep or large directory trees.
|
- Navigation (Backspace, Enter, etc.) must remain robust even for very deep or large directory trees.
|
||||||
- Hatari.Executable is validated during startup: the path is resolved (relative to the EXE directory when applicable) and must point to an existing file. If validation fails the program must present a clear error and exit.
|
- Hatari.Executable is validated during startup: the path is resolved (relative to the EXE directory when applicable) and must point to an existing file. If validation fails the program must present a clear error and exit.
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
- Build and start scripts (`scripts/build.cmd` / `scripts/build.sh` / `scripts/start.cmd` / `scripts/start.sh`) are present and must be used.
|
- Build and start scripts (`scripts/build.cmd` / `scripts/build.sh` / `scripts/start.cmd` / `scripts/start.sh`) are present and must be used.
|
||||||
@@ -86,9 +78,8 @@ The console launcher is meant for browsing a games directory and can launch ZIP
|
|||||||
- After making any code changes that affect behavior or touch source files, run the platform build script (`scripts/build.cmd` on Windows, `scripts/build.sh` on Linux/macOS) and ensure the build completes successfully before committing. Additionally, perform a manual functional test on a Windows, Linux, or macOS machine prior to pushing a release.
|
- After making any code changes that affect behavior or touch source files, run the platform build script (`scripts/build.cmd` on Windows, `scripts/build.sh` on Linux/macOS) and ensure the build completes successfully before committing. Additionally, perform a manual functional test on a Windows, Linux, or macOS machine prior to pushing a release.
|
||||||
- The console window can have any number of lines; display/navigation must adapt dynamically.
|
- The console window can have any number of lines; display/navigation must adapt dynamically.
|
||||||
- For every release, a Release Notes file must be maintained that summarizes all changes, bugfixes, and new features in that version; Release Notes must be provided with the release asset.
|
- For every release, a Release Notes file must be maintained that summarizes all changes, bugfixes, and new features in that version; Release Notes must be provided with the release asset.
|
||||||
|
- README.md must be written in English. Any functional change that affects usage, configuration, or behavior MUST update README.md in English immediately after the change. If there are consequential changes to developer-facing policies, build steps, or requirements, AGENTS.md must be updated as well.
|
||||||
- **IMPORTANT:** With any functional change to the launcher, BOTH this file (AGENTS.md) AND the README.md must always be updated and kept current. Immediately after, a successful build must be executed. This is mandatory for all development on the project.
|
- **IMPORTANT:** With any functional change to the launcher, BOTH this file (AGENTS.md) AND the README.md must always be updated and kept current. Immediately after, a successful build must be executed. This is mandatory for all development on the project.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Known Bugs & Issues
|
||||||
|
|
||||||
|
## Critical Bugs
|
||||||
|
|
||||||
|
_No open critical bugs._
|
||||||
|
|
||||||
|
## Medium Bugs
|
||||||
|
|
||||||
|
_No open medium bugs._
|
||||||
|
|
||||||
|
## Documentation Inconsistencies
|
||||||
|
|
||||||
|
_No open documentation inconsistencies._
|
||||||
|
|
||||||
|
## Code Quality / Minor
|
||||||
|
|
||||||
|
_No open code quality issues._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fixed / Non-Issues (Verified)
|
||||||
|
|
||||||
|
| Item | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| `demo.sh` location | ✅ OK | Exists at repo root, README link correct |
|
||||||
|
| Magic strings centralized | ✅ Done | `FavoritesRootName`, `DefaultFileName`, `DefaultTitle` |
|
||||||
|
| Scroll fractions as constants | ✅ Done | `BottomScrollFraction`, `TopScrollFraction` |
|
||||||
|
| `AvailableLines` helper | ✅ Done | `ProgramHelpers.AvailableLines` |
|
||||||
|
| Colors configurable | ✅ Done | `Colors` section in config |
|
||||||
|
| Win32 constants documented | ✅ Done | Marked "intentionally hardcoded" |
|
||||||
|
| `Hatari.ConfigFile` optional with bundled fallback | ✅ Done | Empty ConfigFile falls back to bundled `MarcerGameDvd-Hatari.cfg` |
|
||||||
|
| `FavoritesService.Save()` swallow comment | ✅ Done | Explains intentional behavior |
|
||||||
|
| `UIErrorService` no-rethrow doc | ✅ Done | Explains design decision |
|
||||||
|
| ArgsTemplate `{cfg}` empty ConfigFile fallback | ✅ Done | `HatariLauncher.Launch()` falls back to bundled config |
|
||||||
|
| `MenuRenderer.RedrawEntry()` resize bounds check | ✅ Done | Row validated against `availableLines` + `Console.WindowHeight`, catch in `WriteConsoleLine` |
|
||||||
|
| `HatariLauncher` ArgsTemplate validation | ✅ Done | Constructor validates both `{zip}` and `{cfg}` placeholders |
|
||||||
|
| ZIP launch file existence check | ✅ Done | `File.Exists` defense-in-depth in `InputController` |
|
||||||
|
| `RightArrow`/`LeftArrow` in AGENTS.md | ✅ Done | Added to key bindings |
|
||||||
|
| `OverlayDirectoryBrowser` path traversal | ✅ Non-issue | `..` segments filtered at entry, `IsSubPathOf` as additional safety net |
|
||||||
|
| `ShowConsoleMessage` params overload | ✅ Done | Added convenience overload with flat argument list |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority Recommendation
|
||||||
|
|
||||||
|
All identified issues have been resolved. See the Fixed table above for details.
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# PLAN
|
|
||||||
|
|
||||||
Ziel: Das Repo aufräumen (Doku, Code, Config)
|
|
||||||
|
|
||||||
## 1. Code-Qualität – Schwächen beheben (harmlos, rein refaktoriell)
|
|
||||||
|
|
||||||
- [x] **LauncherApp.cs zerlegen:** Die ~300-Zeilen-Klasse in separate Klassen aufteilen
|
|
||||||
(InputController.cs extrahiert mit Key-Handling + ReloadGameEntries; Lifecycle & Main-Loop bleiben in AppHost).
|
|
||||||
Ziel: keine Funktionsänderung, nur Struktur.
|
|
||||||
- [x] **Magische Strings entfernen / zentralisieren (nur im Code, siehe 2c):**
|
|
||||||
- [x] Virtueller Ordnername `"Favorites"` → `FavoritesService.FavoritesRootName` Konstante (nicht konfigurierbar).
|
|
||||||
- [x] Dateiname `"favorites.txt"` → `FavoritesService.DefaultFileName` Konstante (nicht konfigurierbar).
|
|
||||||
- [x] Console-Titel `"Marcer GameDVD Launcher"` (Program.cs) → `DefaultTitle` Konstante (nicht konfigurierbar).
|
|
||||||
- [x] Default-ArgsTemplate `-c "{cfg}" --disk-a "{zip}"` (LauncherApp.cs) → redundanten Fallback entfernt; Config liefert das Template (Validation `{zip}` existiert schon).
|
|
||||||
- [x] **Scroll-Trigger magische Zahlen** (NavigationController.cs: `2/3`, `1/3`) in benannte Konstanten
|
|
||||||
(`BottomScrollFraction = 2.0/3.0`, `TopScrollFraction = 1.0/3.0`) mit Kommentar aufzählen.
|
|
||||||
- [x] Redundanz beseitigen: `RedrawEntry` — überflüssige `maxRow`-Logik entfernt, `EnsureCacheForRow` durch `EnsureCache` ersetzt (klare Formulierung).
|
|
||||||
- [x] **Fehler-Schlucken besprechen:** `FavoritesService.Save()` (catch leer) — Kommentar korrigiert ("bewusst still", erklärt warum Persistence-Fehler nicht zum Absturz führen); `UIErrorService` — DocComment erweitert (Fehler werden gezeigt, nicht regeworfen, App bleibt im Loop).
|
|
||||||
- [ ] (Optional) Testprojekt hinzufügen für NavigationController & OverlayDirectoryBrowser –
|
|
||||||
vorab mit Nutzer klären, da Policy bisher keine Tests vorsieht.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Hardcoded-Werte – Aufgaben
|
|
||||||
|
|
||||||
### 2a. Colorschema konfigurierbar machen (entschieden)
|
|
||||||
|
|
||||||
- Aktuell: `MenuRenderer.cs` (`GetColorForEntry`) und README/agents-Doku (6 Entry-Typen +
|
|
||||||
Auswahl-Schema schwarz/blau).
|
|
||||||
- Ziel: Farben (Foreground pro Entry-Typ + Selection-Farben) in `launcher.config.json` konfigurierbar
|
|
||||||
machen (**Entscheidung: ja, konfigurierbar**), mit Default-Fallback auf heutige Werte.
|
|
||||||
|
|
||||||
- [x] `AppConfig` erweitern: neuen Abschnitt `"Colors"` hinzugefügt:
|
|
||||||
- [x] POCO `AppColorConfig` mit `ConsoleColor`-Werten (als String, z.B. `"Yellow"`):
|
|
||||||
FolderBoth, FolderPatchOnly, FolderRootOnly, ZipBoth, ZipRootOnly, ZipPatchOnly,
|
|
||||||
SelectedForeground, SelectedBackground, VirtualEntry.
|
|
||||||
- [x] Deserialisierung per `Enum.TryParse<ConsoleColor>` + Default-Fallback (manuell via `ParseAppColors`/`ParseColorField`).
|
|
||||||
- [x] `MenuRenderer` bekommt `AppColorConfig` via Konstruktor-Injection; `GetColorForEntry`/`GetColors` nutzen Config statt Konstanten.
|
|
||||||
- [x] Falls Config-Werte fehlen → heutiges Verhalten beibehalten (fallback).
|
|
||||||
- [x] Doku synchronisiert: README + AGENTS.md Farbtabellen auf Config-Felder verlinkt.
|
|
||||||
- [x] Beispielwerte ins `launcher.config.example.json` aufgenommen.
|
|
||||||
|
|
||||||
### 2b. Benutzer-Config (User-Config) – entschieden
|
|
||||||
|
|
||||||
Ziel: die echte lokale Config gehört NICHT ins Repo, sondern bleibt lokal.
|
|
||||||
|
|
||||||
- [x] **Entscheidung getroffen (Variante 1):** `launcher.config.json` bleibt im Build-Output/EXE-Ordner (lokal, gitignored). Die `launcher.config.example.json` wird beim Release mitgeliefert und der Nutzer kopiert sie manuell zu `launcher.config.json`.
|
|
||||||
- [x] KEINE zusätzliche Suchreihenfolge (`%APPDATA%`, `~/.config`) implementiert — Pfad ist ausschließlich `AppContext.BaseDirectory`.
|
|
||||||
- [x] KEIN CLI-Parameter `--config` eingeführt.
|
|
||||||
- [x] `.gitignore` ergänzt: `launcher.config.json` und `favorites.txt` werden nie committet.
|
|
||||||
- [x] README: Abschnitt „Configuration" beschreibt manuelles Kopieren der example.
|
|
||||||
|
|
||||||
### 2c. Platzhalterwerte zentralisieren (sehr detailliert)
|
|
||||||
|
|
||||||
**Was bedeutet das Problem?**
|
|
||||||
Einige Werte stehen direkt („hartkodiert") im Code statt an einer zentralen Stelle. Bei Wiederverwendung
|
|
||||||
oder Änderung des Verhaltens sucht man sie an mehreren Stellen im Quelltext — und es passieren schnell
|
|
||||||
Inkonsistenzen. Die folgenden Werte sind betroffen:
|
|
||||||
|
|
||||||
| # | Wert | Aktuelle Stelle | Was es tut | Geplanter Fix |
|
|
||||||
|---|------|-----------------|------------|---------------|
|
|
||||||
| 1 | `"Favorites"` | `LauncherApp.cs` (2×) | Virtueller Ordnername, der dem Nutzer als Eintrag an der Wurzel angezeigt wird. Wenn zwei voneinander abweichende Literale im Codestand bleiben, funktioniert Enter/Anzeige dieses Ordners nicht mehr. | Eine einzige Konstante `const string FavoritesRootName = "Favorites";` (z.B. in `FavoritesService`); beide Verwendungen darauf zurückführen. **Nur Code-Konstante, nicht konfigurierbar.** |
|
|
||||||
| 2 | `"favorites.txt"` | `LauncherApp.cs` | Dateiname der Favoriten-Persistenz (liegt im Patch-Dir oder neben EXE). | Konstante im `FavoritesService` (z.B. `public const string DefaultFileName = "favorites.txt";`); Aufrufer nutzen diese statt Literal. **Nur Code-Konstante, nicht konfigurierbar.** |
|
|
||||||
| 3 | `"Marcer GameDVD Launcher"` | `Program.cs` (`Console.Title`) | Konsolen-Titel, der im Fenstertitel erscheint. | Konstante `const string DefaultTitle = "Marcer GameDVD Launcher";` (Nutzer-visible, aber **nicht konfigurierbar** — fixer Anzeige-Titel). |
|
|
||||||
| 4 | Default-ArgsTemplate `-c "{cfg}" --disk-a "{zip}"` | `LauncherApp.cs` | Redundanter Fallback, falls `Hatari.ArgsTemplate` in der Config nicht gesetzt ist. Ist bereits in der example definiert → das Hardcode ist nur ein Sicherheitsnetz. | Redundanz entfernen: Fallback streichen und erzwingen, dass die Config den Template liefert (Validation `{zip}` existiert schon). **Config-Feld bleibt die einzige Quelle.** |
|
|
||||||
| 5 | Scroll-Anteile `2/3` und `1/3` | `NavigationController.cs` | Bestimmt, ab welcher relativen Position im sichtbaren Fenster automatisch gescrollt wird (Cursor bei 2/3 unten → scrollen, bei 1/3 oben → zurückscrollen). | Benannte Konstanten `BottomScrollTriggerFraction = 2f/3f` und `TopScrollTriggerFraction = 1f/3f` mit Kommentar; Logik bleibt identisch. **Nur Code-Konstanten, nicht konfigurierbar.** |
|
|
||||||
| 6 | `WindowHeight - 1` / `WindowWidth` | `LauncherApp.cs` / `MenuRenderer.cs` | Die Liste ist immer eine Zeile weniger als die Konsolenhöhe (Policy, verhindert Auto-Scroll am Fensterrand). Wird an mehreren Stellen erneut berechnet. | Zentrale Helfer-Berechnung (z.B. statische Methode/Property `AvailableLines`), damit die Policy an genau einer Stelle kodiert ist; Logik unverändert (Resize-Handling bleibt). **Nur Code, nicht konfigurierbar.** |
|
|
||||||
| 7 | `STD_INPUT_HANDLE = -10`, `VK_RETURN = 0x0D` | `ProgramHelpers.cs` | Win32-Konstanten für FlushConsoleInputBuffer bzw. GetAsyncKeyState. Diese sind per Definition konstant. | KEINE Änderung nötig — nur per Kommentar als „bewusst hartkodiert" markieren (Win32-API-Konstanten). |
|
|
||||||
|
|
||||||
**Entscheidung:** Alle o.g. Werte werden **nur im Code zentralisiert** (Konstanten/Helfer).
|
|
||||||
Sie werden **NICHT** als Konfigurationsfelder in `launcher.config.json` angeboten — es gibt keine
|
|
||||||
externe Anpassungsmöglichkeit dafür.
|
|
||||||
|
|
||||||
**Verbindliche To-dos für 2c:**
|
|
||||||
|
|
||||||
- [x] Konstanten/Helfer eingeführt für: `FavoritesRootName` (FavoritesService), `DefaultFileName` (FavoritesService), `DefaultTitle` (Program), `BottomScrollFraction`/`TopScrollFraction` (NavigationController), `AvailableLines`-Helfer (ProgramHelpers).
|
|
||||||
- [x] Default-ArgsTemplate-Fallback entfernt; Config muss `ArgsTemplate` immer liefern (Validation existiert bereits).
|
|
||||||
- [x] Alle String-Literale auf Konstanten zurückgeführt (kein doppeltes `"Favorites"` mehr — nur die Konstantendefinition).
|
|
||||||
- [x] Kommentare ergänzt, die erklären, WARUM Werte fest sind (`AvailableLines` als Policy, Win32-Konstanten als API-konstant, Scroll-Anteile als feste Navigation).
|
|
||||||
- [x] Win32-Konstanten (Punkt 7) NICHT angefasst, nur per Kommentar als „bewusst hartkodiert" markiert.
|
|
||||||
- [x] README/agents nicht um diese rein implementativen Werte erweitert (keine Config-Felder dokumentiert);
|
|
||||||
Doku bleibt bei nutzersichtbaren, konfigurierbaren Werten (Farben, Titel, RootDir/Patch/Hatari).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Abschlusskriterien
|
|
||||||
|
|
||||||
- [x] Commit mit aussagekräftiger Message (nur echter Autor, kein Co-Author).
|
|
||||||
- [x] Nach Doku- und Code-Änderungen: `scripts/build.sh` (Linux) läuft fehlerfrei.
|
|
||||||
@@ -46,9 +46,33 @@ Download the ZIP for your platform from the [Releases](https://github.com/anomal
|
|||||||
### 💻 System Requirements
|
### 💻 System Requirements
|
||||||
|
|
||||||
- **.NET Runtime 10** or later ([download](https://dotnet.microsoft.com/download/dotnet/10.0))
|
- **.NET Runtime 10** or later ([download](https://dotnet.microsoft.com/download/dotnet/10.0))
|
||||||
- **Hatari Emulator** with a working configuration file
|
- **Hatari Emulator** (Windows native, or Linux/macOS via Wine or native build) — a Hatari configuration file (`MarcerGameDvd-Hatari.cfg`) is bundled with the launcher and used automatically when `Hatari.ConfigFile` is empty.
|
||||||
- Windows: native Hatari
|
- **TOS 1.04** ROM image (`TOS.IMG`) — required by Hatari to boot the Atari ST system.
|
||||||
- Linux / macOS: Hatari via Wine or native build
|
|
||||||
|
### 🕹️ Setting Up Hatari
|
||||||
|
|
||||||
|
Before using the launcher, you need to install Hatari and provide a TOS ROM:
|
||||||
|
|
||||||
|
1. **Download Hatari** from the official site: [https://hatari.tuxfamily.org/download.html](https://hatari.tuxfamily.org/download.html)
|
||||||
|
- **Windows:** Download and extract the latest Windows binary.
|
||||||
|
- **Linux:** Install via your package manager (e.g. `sudo apt install hatari`) or build from source.
|
||||||
|
- **macOS:** Download the macOS build or install via Homebrew: `brew install hatari`.
|
||||||
|
|
||||||
|
2. **Get TOS 1.04** (also known as TOS UK or TOS 1.04):
|
||||||
|
- TOS is copyrighted Atari ROM software — you must own a legal copy.
|
||||||
|
- Place the ROM file as `TOS.IMG` in the same directory as the launcher executable (or the Hatari working directory).
|
||||||
|
- The bundled `MarcerGameDvd-Hatari.cfg` is preconfigured to look for `TOS.IMG` in the current directory.
|
||||||
|
|
||||||
|
3. **Configure the path** in `launcher.config.json`:
|
||||||
|
```json
|
||||||
|
"Hatari": {
|
||||||
|
"Executable": "C:\\Tools\\hatari\\hatari.exe",
|
||||||
|
"ConfigFile": "",
|
||||||
|
"ArgsTemplate": "-c \"{cfg}\" --disk-a \"{zip}\""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Set `Hatari.Executable` to the full path of your `hatari` (or `hatari.exe`) binary.
|
||||||
|
- Leave `Hatari.ConfigFile` empty to use the bundled configuration automatically.
|
||||||
|
|
||||||
### ⚡ Quick Start
|
### ⚡ Quick Start
|
||||||
|
|
||||||
@@ -71,7 +95,7 @@ The launcher reads `launcher.config.json` from the same directory as the executa
|
|||||||
"PatchDirectory": "C:\\Games\\Hatari\\PATCH",
|
"PatchDirectory": "C:\\Games\\Hatari\\PATCH",
|
||||||
"Hatari": {
|
"Hatari": {
|
||||||
"Executable": "C:\\Tools\\hatari\\hatari.exe",
|
"Executable": "C:\\Tools\\hatari\\hatari.exe",
|
||||||
"ConfigFile": "C:\\Tools\\hatari\\hatari-st.cfg",
|
"ConfigFile": "",
|
||||||
"ArgsTemplate": "-c \"{cfg}\" --disk-a \"{zip}\""
|
"ArgsTemplate": "-c \"{cfg}\" --disk-a \"{zip}\""
|
||||||
},
|
},
|
||||||
"Colors": {
|
"Colors": {
|
||||||
@@ -93,7 +117,7 @@ The launcher reads `launcher.config.json` from the same directory as the executa
|
|||||||
| `RootDirectory` | ✅ | Game root folder. Navigation never leaves this directory. |
|
| `RootDirectory` | ✅ | Game root folder. Navigation never leaves this directory. |
|
||||||
| `PatchDirectory` | ❌ | Optional overlay/patch directory merged at runtime. |
|
| `PatchDirectory` | ❌ | Optional overlay/patch directory merged at runtime. |
|
||||||
| `Hatari.Executable` | ✅ | Path to the Hatari executable. Validated at startup. |
|
| `Hatari.Executable` | ✅ | Path to the Hatari executable. Validated at startup. |
|
||||||
| `Hatari.ConfigFile` | ✅ | Path to the Hatari configuration file. |
|
| `Hatari.ConfigFile` | ❌ | Path to a Hatari configuration file. If empty, the bundled `MarcerGameDvd-Hatari.cfg` (shipped with the launcher) is used automatically. |
|
||||||
| `Hatari.ArgsTemplate` | ✅ | Argument template. Must contain `{zip}`, optionally `{cfg}`. |
|
| `Hatari.ArgsTemplate` | ✅ | Argument template. Must contain `{zip}`, optionally `{cfg}`. |
|
||||||
| `Colors` | ❌ | Optional color overrides. See [Color Scheme](#-color-scheme) below. Missing or invalid values fall back to defaults. |
|
| `Colors` | ❌ | Optional color overrides. See [Color Scheme](#-color-scheme) below. Missing or invalid values fall back to defaults. |
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
DEMO_DIR="$SCRIPT_DIR/.demo"
|
DEMO_DIR="$SCRIPT_DIR/.demo"
|
||||||
ROOT_DIR="$DEMO_DIR/root"
|
ROOT_DIR="$DEMO_DIR/root"
|
||||||
PATCH_DIR="$DEMO_DIR/patch"
|
PATCH_DIR="$DEMO_DIR/patch"
|
||||||
CONFIG_FILE="$SCRIPT_DIR/launcher.config.json"
|
CONFIG_FILE="$DEMO_DIR/launcher.config.json"
|
||||||
|
|
||||||
# --- Step 1: Build ---
|
# --- Step 1: Build ---
|
||||||
echo "=== Building MarcerGameDvdLauncher ==="
|
echo "=== Building MarcerGameDvdLauncher ==="
|
||||||
@@ -83,6 +83,11 @@ fake_zip "$ROOT_DIR/E/Racing/Pole Position.zip"
|
|||||||
fake_zip "$ROOT_DIR/E/Racing/Out Run.zip"
|
fake_zip "$ROOT_DIR/E/Racing/Out Run.zip"
|
||||||
fake_zip "$ROOT_DIR/E/Racing/Daytona USA.zip"
|
fake_zip "$ROOT_DIR/E/Racing/Daytona USA.zip"
|
||||||
|
|
||||||
|
# === Folders G-Z: empty placeholder folders ===
|
||||||
|
for letter in {G..Z}; do
|
||||||
|
mkdir -p "$ROOT_DIR/$letter"
|
||||||
|
done
|
||||||
|
|
||||||
# --- PATCH layer (overlay/additions) ---
|
# --- PATCH layer (overlay/additions) ---
|
||||||
echo "Creating patch layer..."
|
echo "Creating patch layer..."
|
||||||
|
|
||||||
@@ -160,7 +165,8 @@ echo "=== Writing launcher.config.json ==="
|
|||||||
HATARI_FAKE="$DEMO_DIR/hatari.exe"
|
HATARI_FAKE="$DEMO_DIR/hatari.exe"
|
||||||
cat > "$HATARI_FAKE" <<'HATEXEC'
|
cat > "$HATARI_FAKE" <<'HATEXEC'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo "[DEMO] Hatari would launch with: $@"
|
echo "[DEMO] Hatari would launch with: $0 $@"
|
||||||
|
read -n 1 -r -p $'Press any key to continue... '
|
||||||
HATEXEC
|
HATEXEC
|
||||||
chmod +x "$HATARI_FAKE"
|
chmod +x "$HATARI_FAKE"
|
||||||
|
|
||||||
@@ -171,7 +177,7 @@ cat > "$CONFIG_FILE" <<EOF
|
|||||||
"Hatari": {
|
"Hatari": {
|
||||||
"Executable": "$HATARI_FAKE",
|
"Executable": "$HATARI_FAKE",
|
||||||
"ConfigFile": "",
|
"ConfigFile": "",
|
||||||
"ArgsTemplate": "{zip}"
|
"ArgsTemplate": "-c \"{cfg}\" --disk-a \"{zip}\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
{
|
{
|
||||||
// Manages loading, saving and querying favorite ZIP paths.
|
// Manages loading, saving and querying favorite ZIP paths.
|
||||||
public class FavoritesService
|
public class FavoritesService(string filePath)
|
||||||
{
|
{
|
||||||
// Virtual folder name displayed at the root when favorites exist.
|
// Virtual folder name displayed at the root when favorites exist.
|
||||||
// Not configurable — fixed UI element.
|
// Not configurable — fixed UI element.
|
||||||
@@ -14,15 +14,10 @@ namespace MarcerGameDvdLauncher
|
|||||||
// Not configurable — fixed persistence file.
|
// Not configurable — fixed persistence file.
|
||||||
public const string DefaultFileName = "favorites.txt";
|
public const string DefaultFileName = "favorites.txt";
|
||||||
|
|
||||||
private readonly string _filePath;
|
private readonly string _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
||||||
// Use a SortedSet so favorites are kept in sorted order in memory.
|
// Use a SortedSet so favorites are kept in sorted order in memory.
|
||||||
private SortedSet<string> _favorites = new(StringComparer.OrdinalIgnoreCase);
|
private SortedSet<string> _favorites = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public FavoritesService(string filePath)
|
|
||||||
{
|
|
||||||
_filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load favorites from disk (no-op if file missing)
|
// Load favorites from disk (no-op if file missing)
|
||||||
public void Load()
|
public void Load()
|
||||||
{
|
{
|
||||||
@@ -31,7 +26,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
var lines = File.ReadAllLines(_filePath, System.Text.Encoding.UTF8);
|
var lines = File.ReadAllLines(_filePath, System.Text.Encoding.UTF8);
|
||||||
foreach (var l in lines)
|
foreach (var l in lines)
|
||||||
{
|
{
|
||||||
var t = l?.Trim();
|
var t = l.Trim();
|
||||||
if (string.IsNullOrEmpty(t)) continue;
|
if (string.IsNullOrEmpty(t)) continue;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -71,14 +66,13 @@ namespace MarcerGameDvdLauncher
|
|||||||
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
|
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
|
||||||
var full = Path.GetFullPath(path);
|
var full = Path.GetFullPath(path);
|
||||||
bool added;
|
bool added;
|
||||||
if (_favorites.Contains(full))
|
if (!_favorites.Add(full))
|
||||||
{
|
{
|
||||||
_favorites.Remove(full);
|
_favorites.Remove(full);
|
||||||
added = false;
|
added = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_favorites.Add(full);
|
|
||||||
added = true;
|
added = true;
|
||||||
}
|
}
|
||||||
Save();
|
Save();
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
public class HatariLauncher
|
public class HatariLauncher
|
||||||
{
|
{
|
||||||
|
// The Hatari configuration file that is shipped with the launcher.
|
||||||
|
// When Hatari.ConfigFile is not set in launcher.config.json, this file
|
||||||
|
// (resolved relative to the executable directory) is used automatically.
|
||||||
|
private const string DEFAULT_CONFIG_FILE = "MarcerGameDvd-Hatari.cfg";
|
||||||
|
|
||||||
private readonly string _exePath;
|
private readonly string _exePath;
|
||||||
private readonly string _cfgPath;
|
private readonly string _cfgPath;
|
||||||
private readonly string _argsTemplate;
|
private readonly string _argsTemplate;
|
||||||
@@ -18,6 +23,12 @@ namespace MarcerGameDvdLauncher
|
|||||||
if (!File.Exists(exePath))
|
if (!File.Exists(exePath))
|
||||||
throw new ArgumentException($"Hatari executable not found: {exePath}", nameof(exePath));
|
throw new ArgumentException($"Hatari executable not found: {exePath}", nameof(exePath));
|
||||||
|
|
||||||
|
// Validate argsTemplate
|
||||||
|
if (string.IsNullOrWhiteSpace(argsTemplate) || !argsTemplate.Contains("{zip}"))
|
||||||
|
throw new ArgumentException("Hatari.ArgsTemplate must contain the {zip} placeholder.", nameof(argsTemplate));
|
||||||
|
if (string.IsNullOrWhiteSpace(argsTemplate) || !argsTemplate.Contains("{cfg}"))
|
||||||
|
throw new ArgumentException("Hatari.ArgsTemplate must contain the {cfg} placeholder.", nameof(argsTemplate));
|
||||||
|
|
||||||
_exePath = exePath;
|
_exePath = exePath;
|
||||||
_cfgPath = cfgPath;
|
_cfgPath = cfgPath;
|
||||||
_argsTemplate = argsTemplate;
|
_argsTemplate = argsTemplate;
|
||||||
@@ -33,20 +44,21 @@ namespace MarcerGameDvdLauncher
|
|||||||
throw new ArgumentException("ZIP archive path must not be empty.", nameof(zipFilePath));
|
throw new ArgumentException("ZIP archive path must not be empty.", nameof(zipFilePath));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string args = _argsTemplate.Replace("{cfg}", _cfgPath).Replace("{zip}", zipFilePath);
|
var args = _argsTemplate;
|
||||||
|
args = args.Replace("{cfg}", !string.IsNullOrWhiteSpace(_cfgPath) ? _cfgPath : Path.Combine(Directory.GetCurrentDirectory(),DEFAULT_CONFIG_FILE));
|
||||||
|
args = args.Replace("{zip}", zipFilePath);
|
||||||
|
|
||||||
var psi = new System.Diagnostics.ProcessStartInfo
|
var psi = new System.Diagnostics.ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = _exePath,
|
FileName = _exePath,
|
||||||
Arguments = args,
|
Arguments = args,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
WorkingDirectory = Path.GetDirectoryName(_exePath) ?? string.Empty
|
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||||
};
|
};
|
||||||
System.Diagnostics.Process.Start(psi);
|
var process = System.Diagnostics.Process.Start(psi);
|
||||||
// Show a modal indicating the emulator was started and wait until
|
// Show a modal indicating the emulator is running and wait until
|
||||||
// the user releases the Return key before clearing the modal. This
|
// the process has exited before clearing the modal.
|
||||||
// prevents accidental key repeats from triggering other actions.
|
ProgramHelpers.ShowModalUntilProcessExited(process, "Hatari is running...");
|
||||||
ProgramHelpers.ShowModalUntilReturnReleased("Hatari started. Release Return to continue...");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
// Licensed under the MIT License. See LICENSE file in project root for details.
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -13,42 +8,25 @@ namespace MarcerGameDvdLauncher
|
|||||||
/// Extracted from AppHost so that LauncherApp stays focused on lifecycle management.
|
/// Extracted from AppHost so that LauncherApp stays focused on lifecycle management.
|
||||||
/// No functional change - all key-handling behaviour is preserved exactly.
|
/// No functional change - all key-handling behaviour is preserved exactly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class InputController
|
internal class InputController(
|
||||||
|
OverlayDirectoryBrowser directoryBrowser,
|
||||||
|
MenuRenderer menuRenderer,
|
||||||
|
NavigationController navigationController,
|
||||||
|
HatariLauncher hatariLauncher,
|
||||||
|
FavoritesService favoritesService,
|
||||||
|
UIErrorService errorService)
|
||||||
{
|
{
|
||||||
private readonly OverlayDirectoryBrowser _directoryBrowser;
|
|
||||||
private readonly MenuRenderer _menuRenderer;
|
|
||||||
private readonly NavigationController _navigationController;
|
|
||||||
private readonly HatariLauncher _hatariLauncher;
|
|
||||||
private readonly FavoritesService _favoritesService;
|
|
||||||
private readonly UIErrorService _errorService;
|
|
||||||
|
|
||||||
private List<GameEntry> _gameEntries = new();
|
private List<GameEntry> _gameEntries = new();
|
||||||
|
|
||||||
public IReadOnlyList<GameEntry> GameEntries => _gameEntries;
|
|
||||||
|
|
||||||
public InputController(OverlayDirectoryBrowser directoryBrowser, MenuRenderer menuRenderer,
|
|
||||||
NavigationController navigationController, HatariLauncher hatariLauncher,
|
|
||||||
FavoritesService favoritesService, UIErrorService errorService)
|
|
||||||
{
|
|
||||||
_directoryBrowser = directoryBrowser;
|
|
||||||
_menuRenderer = menuRenderer;
|
|
||||||
_navigationController = navigationController;
|
|
||||||
_hatariLauncher = hatariLauncher;
|
|
||||||
_favoritesService = favoritesService;
|
|
||||||
_errorService = errorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsFavorite(GameEntry e)
|
private bool IsFavorite(GameEntry e)
|
||||||
{
|
{
|
||||||
return e.Kind == EntryKind.Zip
|
return e.Kind == EntryKind.Zip && favoritesService.IsFavorite(e.InPatch ? e.PatchPath : e.RootPath);
|
||||||
? _favoritesService.IsFavorite(e.InPatch ? e.PatchPath : e.RootPath)
|
|
||||||
: false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMenu(int availableLines)
|
private void DrawMenu(int availableLines)
|
||||||
{
|
{
|
||||||
_menuRenderer.DrawMenu(_gameEntries, _navigationController.ScrollOffset,
|
menuRenderer.DrawMenu(_gameEntries, navigationController.ScrollOffset,
|
||||||
_navigationController.SelectedIndex, availableLines, IsFavorite);
|
navigationController.SelectedIndex, availableLines, IsFavorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,7 +34,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RefreshView(int availableLines)
|
public void RefreshView(int availableLines)
|
||||||
{
|
{
|
||||||
_navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +46,9 @@ namespace MarcerGameDvdLauncher
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If we are at the virtual Favorites folder, produce the flat list from the favorites service
|
// If we are at the virtual Favorites folder, produce the flat list from the favorites service
|
||||||
if (string.Equals(_navigationController.CurrentRelativePath, FavoritesService.FavoritesRootName, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(navigationController.CurrentRelativePath, FavoritesService.FavoritesRootName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var favs = _favoritesService.GetAll();
|
var favs = favoritesService.GetAll();
|
||||||
_gameEntries = new List<GameEntry>();
|
_gameEntries = new List<GameEntry>();
|
||||||
foreach (var p in favs)
|
foreach (var p in favs)
|
||||||
{
|
{
|
||||||
@@ -85,15 +63,15 @@ namespace MarcerGameDvdLauncher
|
|||||||
IsVirtual = false
|
IsVirtual = false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_navigationController.SetEntriesCount(_gameEntries.Count);
|
navigationController.SetEntriesCount(_gameEntries.Count);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise use the overlay directory browser for normal folders
|
// Otherwise use the overlay directory browser for normal folders
|
||||||
_gameEntries = _directoryBrowser.GetEntries(_navigationController.CurrentRelativePath);
|
_gameEntries = directoryBrowser.GetEntries(navigationController.CurrentRelativePath);
|
||||||
|
|
||||||
// If we are at the root and there are favorites, prepend a virtual Favorites folder
|
// If we are at the root and there are favorites, prepend a virtual Favorites folder
|
||||||
if (string.IsNullOrEmpty(_navigationController.CurrentRelativePath) && _favoritesService.HasFavorites())
|
if (string.IsNullOrEmpty(navigationController.CurrentRelativePath) && favoritesService.HasFavorites())
|
||||||
{
|
{
|
||||||
var virtualEntry = new GameEntry
|
var virtualEntry = new GameEntry
|
||||||
{
|
{
|
||||||
@@ -108,14 +86,14 @@ namespace MarcerGameDvdLauncher
|
|||||||
_gameEntries.Insert(0, virtualEntry);
|
_gameEntries.Insert(0, virtualEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
_navigationController.SetEntriesCount(_gameEntries.Count);
|
navigationController.SetEntriesCount(_gameEntries.Count);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Show the error to the user and continue with an empty list
|
// Show the error to the user and continue with an empty list
|
||||||
_errorService.ShowError(ex.Message);
|
errorService.ShowError(ex.Message);
|
||||||
_gameEntries = new List<GameEntry>();
|
_gameEntries = new List<GameEntry>();
|
||||||
_navigationController.SetEntriesCount(0);
|
navigationController.SetEntriesCount(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +104,9 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
if (key.KeyChar == '?')
|
if (key.KeyChar == '?')
|
||||||
{
|
{
|
||||||
_menuRenderer.ShowHelpBox(availableLines);
|
menuRenderer.ShowHelpBox(availableLines);
|
||||||
Console.ReadKey(intercept: true);
|
Console.ReadKey(intercept: true);
|
||||||
_menuRenderer.InvalidateCache();
|
menuRenderer.InvalidateCache();
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
return false;
|
return false;
|
||||||
@@ -138,16 +116,16 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
case ConsoleKey.UpArrow:
|
case ConsoleKey.UpArrow:
|
||||||
{
|
{
|
||||||
int previousSelectedIndex = _navigationController.SelectedIndex;
|
int previousSelectedIndex = navigationController.SelectedIndex;
|
||||||
bool didScroll = _navigationController.MoveUp(_gameEntries, availableLines);
|
bool didScroll = navigationController.MoveUp(_gameEntries, availableLines);
|
||||||
if (didScroll)
|
if (didScroll)
|
||||||
{
|
{
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - _navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, _navigationController.SelectedIndex, _navigationController.SelectedIndex - _navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, navigationController.SelectedIndex, navigationController.SelectedIndex - navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
||||||
}
|
}
|
||||||
// flush input to avoid key repeat
|
// flush input to avoid key repeat
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
@@ -155,16 +133,16 @@ namespace MarcerGameDvdLauncher
|
|||||||
break;
|
break;
|
||||||
case ConsoleKey.DownArrow:
|
case ConsoleKey.DownArrow:
|
||||||
{
|
{
|
||||||
int previousSelectedIndex = _navigationController.SelectedIndex;
|
int previousSelectedIndex = navigationController.SelectedIndex;
|
||||||
bool didScroll = _navigationController.MoveDown(_gameEntries, availableLines);
|
bool didScroll = navigationController.MoveDown(_gameEntries, availableLines);
|
||||||
if (didScroll)
|
if (didScroll)
|
||||||
{
|
{
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - _navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, _navigationController.SelectedIndex, _navigationController.SelectedIndex - _navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, navigationController.SelectedIndex, navigationController.SelectedIndex - navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
||||||
}
|
}
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
@@ -172,27 +150,39 @@ namespace MarcerGameDvdLauncher
|
|||||||
case ConsoleKey.Enter:
|
case ConsoleKey.Enter:
|
||||||
case ConsoleKey.RightArrow:
|
case ConsoleKey.RightArrow:
|
||||||
{
|
{
|
||||||
var oldRelativePath = _navigationController.CurrentRelativePath;
|
var oldRelativePath = navigationController.CurrentRelativePath;
|
||||||
var isDirectory = _gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Directory;
|
var isDirectory = _gameEntries.Count > 0 && _gameEntries[navigationController.SelectedIndex].Kind == EntryKind.Directory;
|
||||||
_navigationController.HandleEnter(_gameEntries);
|
navigationController.HandleEnter(_gameEntries);
|
||||||
if (isDirectory && oldRelativePath != _navigationController.CurrentRelativePath)
|
if (isDirectory && oldRelativePath != navigationController.CurrentRelativePath)
|
||||||
{
|
{
|
||||||
ReloadGameEntries();
|
ReloadGameEntries();
|
||||||
_navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
||||||
}
|
}
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
// Only start a ZIP if NOT switching to a directory
|
// Only start a ZIP if NOT switching to a directory
|
||||||
if (!isDirectory && _gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
if (!isDirectory && _gameEntries.Count > 0 && _gameEntries[navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
||||||
{
|
{
|
||||||
string zipToLaunch = _gameEntries[_navigationController.SelectedIndex].InPatch ? _gameEntries[_navigationController.SelectedIndex].PatchPath : _gameEntries[_navigationController.SelectedIndex].RootPath;
|
string zipToLaunch = _gameEntries[navigationController.SelectedIndex].InPatch ? _gameEntries[navigationController.SelectedIndex].PatchPath : _gameEntries[navigationController.SelectedIndex].RootPath;
|
||||||
|
if (!File.Exists(zipToLaunch))
|
||||||
|
{
|
||||||
|
errorService.ShowError($"ZIP file not found: {zipToLaunch}");
|
||||||
|
DrawMenu(availableLines);
|
||||||
|
ProgramHelpers.FlushInputBuffer();
|
||||||
|
break;
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_hatariLauncher.Launch(zipToLaunch);
|
hatariLauncher.Launch(zipToLaunch);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_errorService.ShowError(ex.Message);
|
errorService.ShowError(ex.Message);
|
||||||
}
|
}
|
||||||
|
// Redraw menu after Hatari has exited.
|
||||||
|
// Invalidate the cache because the modal overwrote
|
||||||
|
// a console row that the cache still considers valid.
|
||||||
|
menuRenderer.InvalidateCache();
|
||||||
|
DrawMenu(availableLines);
|
||||||
}
|
}
|
||||||
// flush input to avoid leftover key events after an enter/navigation
|
// flush input to avoid leftover key events after an enter/navigation
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
@@ -201,23 +191,23 @@ namespace MarcerGameDvdLauncher
|
|||||||
case ConsoleKey.Backspace:
|
case ConsoleKey.Backspace:
|
||||||
case ConsoleKey.LeftArrow:
|
case ConsoleKey.LeftArrow:
|
||||||
{
|
{
|
||||||
_navigationController.GoUpDirectory();
|
navigationController.GoUpDirectory();
|
||||||
ReloadGameEntries();
|
ReloadGameEntries();
|
||||||
_navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.PageDown:
|
case ConsoleKey.PageDown:
|
||||||
{
|
{
|
||||||
_navigationController.PageDown(_gameEntries, availableLines);
|
navigationController.PageDown(_gameEntries, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.PageUp:
|
case ConsoleKey.PageUp:
|
||||||
{
|
{
|
||||||
_navigationController.PageUp(_gameEntries, availableLines);
|
navigationController.PageUp(_gameEntries, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
@@ -226,17 +216,17 @@ namespace MarcerGameDvdLauncher
|
|||||||
case ConsoleKey.Oem8:
|
case ConsoleKey.Oem8:
|
||||||
{
|
{
|
||||||
// Toggle favorite for selected ZIP (handles numpad * and some layouts)
|
// Toggle favorite for selected ZIP (handles numpad * and some layouts)
|
||||||
if (_gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
if (_gameEntries.Count > 0 && _gameEntries[navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
||||||
{
|
{
|
||||||
var ge = _gameEntries[_navigationController.SelectedIndex];
|
var ge = _gameEntries[navigationController.SelectedIndex];
|
||||||
string path = ge.InPatch ? ge.PatchPath : ge.RootPath;
|
string path = ge.InPatch ? ge.PatchPath : ge.RootPath;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_favoritesService.Toggle(path);
|
favoritesService.Toggle(path);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_errorService.ShowError("Failed to toggle favorite: " + ex.Message);
|
errorService.ShowError("Failed to toggle favorite: " + ex.Message);
|
||||||
}
|
}
|
||||||
// Redraw the whole menu so the '*' marker updates immediately
|
// Redraw the whole menu so the '*' marker updates immediately
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
|
|||||||
@@ -63,9 +63,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
if (!File.Exists(cfg.Hatari.Executable))
|
if (!File.Exists(cfg.Hatari.Executable))
|
||||||
throw new InvalidOperationException($"Hatari executable not found: {cfg.Hatari.Executable}");
|
throw new InvalidOperationException($"Hatari executable not found: {cfg.Hatari.Executable}");
|
||||||
|
|
||||||
// Also validate the Hatari config file (if provided)
|
// Validate the Hatari config file (either user-specified or bundled fallback)
|
||||||
if (!string.IsNullOrWhiteSpace(cfg.Hatari.ConfigFile) && !File.Exists(cfg.Hatari.ConfigFile))
|
if (!string.IsNullOrEmpty(cfg.Hatari.ConfigFile) && !File.Exists(cfg.Hatari.ConfigFile))
|
||||||
throw new InvalidOperationException($"Hatari configuration file not found: {cfg.Hatari.ConfigFile}");
|
throw new InvalidOperationException($"Hatari configuration file not found: {cfg.Hatari.ConfigFile}");
|
||||||
|
if (string.IsNullOrWhiteSpace(cfg.Hatari.ArgsTemplate) || !cfg.Hatari.ArgsTemplate.Contains("{cfg}"))
|
||||||
|
throw new InvalidOperationException("Hatari.ArgsTemplate must contain the {cfg} placeholder.");
|
||||||
if (string.IsNullOrWhiteSpace(cfg.Hatari.ArgsTemplate) || !cfg.Hatari.ArgsTemplate.Contains("{zip}"))
|
if (string.IsNullOrWhiteSpace(cfg.Hatari.ArgsTemplate) || !cfg.Hatari.ArgsTemplate.Contains("{zip}"))
|
||||||
throw new InvalidOperationException("Hatari.ArgsTemplate must contain the {zip} placeholder.");
|
throw new InvalidOperationException("Hatari.ArgsTemplate must contain the {zip} placeholder.");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
[Log]
|
||||||
|
sLogFileName = stderr
|
||||||
|
sTraceFileName = stderr
|
||||||
|
nTextLogLevel = 3
|
||||||
|
nAlertDlgLogLevel = 1
|
||||||
|
bConfirmQuit = TRUE
|
||||||
|
bNatFeats = FALSE
|
||||||
|
bConsoleWindow = FALSE
|
||||||
|
|
||||||
|
[Debugger]
|
||||||
|
nNumberBase = 10
|
||||||
|
nSymbolLines = -1
|
||||||
|
nMemdumpLines = -1
|
||||||
|
nFindLines = -1
|
||||||
|
nDisasmLines = -1
|
||||||
|
nBacktraceLines = 0
|
||||||
|
nExceptionDebugMask = 1073741830
|
||||||
|
nDisasmOptions = 7
|
||||||
|
nMemConvLocale = FALSE
|
||||||
|
bDisasmUAE = TRUE
|
||||||
|
nSymbolsAutoLoad = 1
|
||||||
|
bMatchAllSymbols = FALSE
|
||||||
|
|
||||||
|
[Screen]
|
||||||
|
nMonitorType = 1
|
||||||
|
nFrameSkips = 5
|
||||||
|
bFullScreen = FALSE
|
||||||
|
bKeepResolution = FALSE
|
||||||
|
bResizable = FALSE
|
||||||
|
bAllowOverscan = TRUE
|
||||||
|
nSpec512Threshold = 1
|
||||||
|
bAspectCorrect = TRUE
|
||||||
|
bUseExtVdiResolutions = FALSE
|
||||||
|
nVdiWidth = 1264
|
||||||
|
nVdiHeight = 912
|
||||||
|
nVdiColors = 1
|
||||||
|
bMouseWarp = TRUE
|
||||||
|
bShowStatusbar = TRUE
|
||||||
|
bShowDriveLed = FALSE
|
||||||
|
bCrop = FALSE
|
||||||
|
bForceMax = FALSE
|
||||||
|
nMaxWidth = 1280
|
||||||
|
nMaxHeight = 1024
|
||||||
|
nZoomFactor = 3
|
||||||
|
bUseSdlRenderer = FALSE
|
||||||
|
ScreenShotFormat = 2
|
||||||
|
szScreenShotDir =
|
||||||
|
bUseVsync = FALSE
|
||||||
|
|
||||||
|
[Joystick0]
|
||||||
|
nJoystickMode = 0
|
||||||
|
bEnableAutoFire = FALSE
|
||||||
|
bEnableJumpOnFire2 = TRUE
|
||||||
|
nJoyId = 0
|
||||||
|
nJoyBut1Index = 0
|
||||||
|
nJoyBut2Index = 1
|
||||||
|
nJoyBut3Index = 2
|
||||||
|
kUp = Up
|
||||||
|
kDown = Down
|
||||||
|
kLeft = Left
|
||||||
|
kRight = Right
|
||||||
|
kFire = Right Ctrl
|
||||||
|
|
||||||
|
[Joystick1]
|
||||||
|
nJoystickMode = 1
|
||||||
|
bEnableAutoFire = FALSE
|
||||||
|
bEnableJumpOnFire2 = TRUE
|
||||||
|
nJoyId = 0
|
||||||
|
nJoyBut1Index = 0
|
||||||
|
nJoyBut2Index = 1
|
||||||
|
nJoyBut3Index = 2
|
||||||
|
kUp = Up
|
||||||
|
kDown = Down
|
||||||
|
kLeft = Left
|
||||||
|
kRight = Right
|
||||||
|
kFire = Right Ctrl
|
||||||
|
|
||||||
|
[Joystick2]
|
||||||
|
nJoystickMode = 0
|
||||||
|
bEnableAutoFire = FALSE
|
||||||
|
bEnableJumpOnFire2 = TRUE
|
||||||
|
nJoyId = 0
|
||||||
|
nJoyBut1Index = 0
|
||||||
|
nJoyBut2Index = 1
|
||||||
|
nJoyBut3Index = 2
|
||||||
|
kUp = Up
|
||||||
|
kDown = Down
|
||||||
|
kLeft = Left
|
||||||
|
kRight = Right
|
||||||
|
kFire = Right Ctrl
|
||||||
|
kButtonB = B
|
||||||
|
kButtonC = C
|
||||||
|
kButtonOption = O
|
||||||
|
kButtonPause = P
|
||||||
|
kButtonStar = +
|
||||||
|
kButtonHash = #
|
||||||
|
kButton0 = 0
|
||||||
|
kButton1 = 1
|
||||||
|
kButton2 = 2
|
||||||
|
kButton3 = 3
|
||||||
|
kButton4 = 4
|
||||||
|
kButton5 = 5
|
||||||
|
kButton6 = 6
|
||||||
|
kButton7 = 7
|
||||||
|
kButton8 = 8
|
||||||
|
kButton9 = 9
|
||||||
|
|
||||||
|
[Joystick3]
|
||||||
|
nJoystickMode = 0
|
||||||
|
bEnableAutoFire = FALSE
|
||||||
|
bEnableJumpOnFire2 = TRUE
|
||||||
|
nJoyId = 0
|
||||||
|
nJoyBut1Index = 0
|
||||||
|
nJoyBut2Index = 1
|
||||||
|
nJoyBut3Index = 2
|
||||||
|
kUp = Up
|
||||||
|
kDown = Down
|
||||||
|
kLeft = Left
|
||||||
|
kRight = Right
|
||||||
|
kFire = Right Ctrl
|
||||||
|
kButtonB =
|
||||||
|
kButtonC =
|
||||||
|
kButtonOption =
|
||||||
|
kButtonPause =
|
||||||
|
kButtonStar =
|
||||||
|
kButtonHash =
|
||||||
|
kButton0 =
|
||||||
|
kButton1 =
|
||||||
|
kButton2 =
|
||||||
|
kButton3 =
|
||||||
|
kButton4 =
|
||||||
|
kButton5 =
|
||||||
|
kButton6 =
|
||||||
|
kButton7 =
|
||||||
|
kButton8 =
|
||||||
|
kButton9 =
|
||||||
|
|
||||||
|
[Joystick4]
|
||||||
|
nJoystickMode = 0
|
||||||
|
bEnableAutoFire = FALSE
|
||||||
|
bEnableJumpOnFire2 = TRUE
|
||||||
|
nJoyId = 0
|
||||||
|
nJoyBut1Index = 0
|
||||||
|
nJoyBut2Index = 1
|
||||||
|
nJoyBut3Index = 2
|
||||||
|
kUp = Up
|
||||||
|
kDown = Down
|
||||||
|
kLeft = Left
|
||||||
|
kRight = Right
|
||||||
|
kFire = Right Ctrl
|
||||||
|
|
||||||
|
[Joystick5]
|
||||||
|
nJoystickMode = 0
|
||||||
|
bEnableAutoFire = FALSE
|
||||||
|
bEnableJumpOnFire2 = TRUE
|
||||||
|
nJoyId = 0
|
||||||
|
nJoyBut1Index = 0
|
||||||
|
nJoyBut2Index = 1
|
||||||
|
nJoyBut3Index = 2
|
||||||
|
kUp = Up
|
||||||
|
kDown = Down
|
||||||
|
kLeft = Left
|
||||||
|
kRight = Right
|
||||||
|
kFire = Right Ctrl
|
||||||
|
|
||||||
|
[Keyboard]
|
||||||
|
bFastForwardKeyRepeat = TRUE
|
||||||
|
nKeymapType = 0
|
||||||
|
nCountryCode = -1
|
||||||
|
nKbdLayout = -1
|
||||||
|
nLanguage = -1
|
||||||
|
szMappingFileName =
|
||||||
|
|
||||||
|
[KeyShortcutsWithMod]
|
||||||
|
kOptions = O
|
||||||
|
kFullScreen = F
|
||||||
|
kBorders = B
|
||||||
|
kMouseMode = M
|
||||||
|
kColdReset = C
|
||||||
|
kWarmReset = R
|
||||||
|
kScreenShot = G
|
||||||
|
kBossKey = I
|
||||||
|
kCursorEmu = J
|
||||||
|
kFastForward = X
|
||||||
|
kRecAnim = A
|
||||||
|
kRecSound = Y
|
||||||
|
kSound = S
|
||||||
|
kPause =
|
||||||
|
kDebugger = Pause
|
||||||
|
kQuit = Q
|
||||||
|
kLoadMem = L
|
||||||
|
kSaveMem = K
|
||||||
|
kInsertDiskA = D
|
||||||
|
kSwitchJoy0 = F1
|
||||||
|
kSwitchJoy1 = F2
|
||||||
|
kSwitchPadA = F3
|
||||||
|
kSwitchPadB = F4
|
||||||
|
|
||||||
|
[KeyShortcutsWithoutMod]
|
||||||
|
kOptions = F12
|
||||||
|
kFullScreen = F11
|
||||||
|
kBorders =
|
||||||
|
kMouseMode =
|
||||||
|
kColdReset =
|
||||||
|
kWarmReset =
|
||||||
|
kScreenShot =
|
||||||
|
kBossKey =
|
||||||
|
kCursorEmu =
|
||||||
|
kFastForward =
|
||||||
|
kRecAnim =
|
||||||
|
kRecSound =
|
||||||
|
kSound =
|
||||||
|
kPause = Pause
|
||||||
|
kDebugger =
|
||||||
|
kQuit =
|
||||||
|
kLoadMem =
|
||||||
|
kSaveMem =
|
||||||
|
kInsertDiskA =
|
||||||
|
kSwitchJoy0 =
|
||||||
|
kSwitchJoy1 =
|
||||||
|
kSwitchPadA =
|
||||||
|
kSwitchPadB =
|
||||||
|
|
||||||
|
[Sound]
|
||||||
|
bEnableMicrophone = TRUE
|
||||||
|
bEnableSound = TRUE
|
||||||
|
bEnableSoundSync = FALSE
|
||||||
|
nPlaybackFreq = 44100
|
||||||
|
nSdlAudioBufferSize = 0
|
||||||
|
szYMCaptureFileName =
|
||||||
|
YmVolumeMixing = 2
|
||||||
|
|
||||||
|
[Memory]
|
||||||
|
nMemorySize = 1024
|
||||||
|
nTTRamSize = 28672
|
||||||
|
bAutoSave = FALSE
|
||||||
|
szMemoryCaptureFileName =
|
||||||
|
szAutoSaveFileName =
|
||||||
|
|
||||||
|
[Floppy]
|
||||||
|
bAutoInsertDiskB = FALSE
|
||||||
|
FastFloppy = FALSE
|
||||||
|
EnableDriveA = TRUE
|
||||||
|
DriveA_NumberOfHeads = 2
|
||||||
|
EnableDriveB = TRUE
|
||||||
|
DriveB_NumberOfHeads = 2
|
||||||
|
nWriteProtection = 0
|
||||||
|
szDiskAZipPath =
|
||||||
|
szDiskAFileName =
|
||||||
|
szDiskBZipPath =
|
||||||
|
szDiskBFileName =
|
||||||
|
szDiskImageDirectory =
|
||||||
|
|
||||||
|
[HardDisk]
|
||||||
|
nGemdosDrive = 0
|
||||||
|
bBootFromHardDisk = FALSE
|
||||||
|
bUseHardDiskDirectory = FALSE
|
||||||
|
szHardDiskDirectory = R:
|
||||||
|
nGemdosCase = 0
|
||||||
|
nWriteProtection = 0
|
||||||
|
bFilenameConversion = FALSE
|
||||||
|
bGemdosHostTime = FALSE
|
||||||
|
|
||||||
|
[ACSI]
|
||||||
|
bUseDevice0 = FALSE
|
||||||
|
sDeviceFile0 =
|
||||||
|
nBlockSize0 = 512
|
||||||
|
nAcsiVersion0 = 1
|
||||||
|
|
||||||
|
|
||||||
|
[SCSI]
|
||||||
|
|
||||||
|
|
||||||
|
[IDE]
|
||||||
|
|
||||||
|
|
||||||
|
[ROM]
|
||||||
|
szTosImageFileName = TOS.IMG
|
||||||
|
bPatchTos = TRUE
|
||||||
|
szCartridgeImageFileName =
|
||||||
|
|
||||||
|
[LILO]
|
||||||
|
Args = root=/dev/ram video=atafb:vga16 load_ramdisk=1
|
||||||
|
Kernel =
|
||||||
|
Symbols =
|
||||||
|
Ramdisk =
|
||||||
|
HaltOnReboot = TRUE
|
||||||
|
KernelToFastRam = TRUE
|
||||||
|
RamdiskToFastRam = TRUE
|
||||||
|
|
||||||
|
[RS232]
|
||||||
|
bEnableRS232 = FALSE
|
||||||
|
szOutFileName =
|
||||||
|
szInFileName =
|
||||||
|
EnableSccA = FALSE
|
||||||
|
SccAOutFileName =
|
||||||
|
SccAInFileName =
|
||||||
|
EnableSccALan = FALSE
|
||||||
|
SccALanOutFileName =
|
||||||
|
SccALanInFileName =
|
||||||
|
EnableSccB = FALSE
|
||||||
|
SccBOutFileName =
|
||||||
|
SccBInFileName =
|
||||||
|
|
||||||
|
[Printer]
|
||||||
|
bEnablePrinting = FALSE
|
||||||
|
szPrintToFileName =
|
||||||
|
|
||||||
|
[Midi]
|
||||||
|
bEnableMidi = FALSE
|
||||||
|
sMidiInFileName =
|
||||||
|
sMidiOutFileName =
|
||||||
|
sMidiInPortName = Off
|
||||||
|
sMidiOutPortName = Off
|
||||||
|
|
||||||
|
[System]
|
||||||
|
nCpuLevel = 0
|
||||||
|
nCpuFreq = 8
|
||||||
|
bCompatibleCpu = TRUE
|
||||||
|
nModelType = 0
|
||||||
|
bBlitter = FALSE
|
||||||
|
nDSPType = 0
|
||||||
|
nRtcYear = 0
|
||||||
|
bPatchTimerD = FALSE
|
||||||
|
bFastBoot = FALSE
|
||||||
|
bFastForward = FALSE
|
||||||
|
bAddressSpace24 = TRUE
|
||||||
|
bCycleExactCpu = TRUE
|
||||||
|
bCpuDataCache = TRUE
|
||||||
|
n_FPUType = 0
|
||||||
|
bSoftFloatFPU = FALSE
|
||||||
|
bMMU = TRUE
|
||||||
|
VideoTiming = 3
|
||||||
|
|
||||||
|
[Video]
|
||||||
|
AviRecordVcodec = 2
|
||||||
|
AviRecordFps = 0
|
||||||
|
AviRecordFile =
|
||||||
|
|
||||||
@@ -10,10 +10,13 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="launcher.config.example.json">
|
<Content Include="launcher.config.example.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="MarcerGameDvd-Hatari.cfg">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="../../LICENSE">
|
<Content Include="../../LICENSE">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Link>LICENSE</Link>
|
<Link>LICENSE</Link>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
{
|
{
|
||||||
public class MenuRenderer
|
public class MenuRenderer(AppColorConfig? colors = null)
|
||||||
{
|
{
|
||||||
// Simple line-based double buffer to avoid full Clear() flicker.
|
// Simple line-based double buffer to avoid full Clear() flicker.
|
||||||
// cachedBuffer holds the last rendered text and colors for each visible row.
|
// cachedBuffer holds the last rendered text and colors for each visible row.
|
||||||
@@ -12,12 +12,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
private int _cachedWidth = -1;
|
private int _cachedWidth = -1;
|
||||||
|
|
||||||
// Color configuration (injected; defaults to built-in scheme if null)
|
// Color configuration (injected; defaults to built-in scheme if null)
|
||||||
private readonly AppColorConfig _colors;
|
private readonly AppColorConfig _colors = colors ?? new AppColorConfig();
|
||||||
|
|
||||||
public MenuRenderer(AppColorConfig? colors = null)
|
|
||||||
{
|
|
||||||
_colors = colors ?? new AppColorConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
// availableLines is provided per-draw so the renderer adapts to console resizes
|
// availableLines is provided per-draw so the renderer adapts to console resizes
|
||||||
public void DrawMenu(List<GameEntry> entries, int scrollOffset, int selectedIndex, int availableLines, Func<GameEntry, bool>? isFavorite = null)
|
public void DrawMenu(List<GameEntry> entries, int scrollOffset, int selectedIndex, int availableLines, Func<GameEntry, bool>? isFavorite = null)
|
||||||
@@ -93,7 +88,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// out of cache bounds - attempt a direct write
|
// out of cache bounds - attempt a direct write
|
||||||
WriteConsoleLine(row, newLine);
|
// Validate row against current console height to prevent ArgumentOutOfRangeException
|
||||||
|
if (row < Console.WindowHeight)
|
||||||
|
{
|
||||||
|
WriteConsoleLine(row, newLine);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,55 +133,34 @@ namespace MarcerGameDvdLauncher
|
|||||||
_cachedBuffer[i].Text = null!;
|
_cachedBuffer[i].Text = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders a centered, bordered help box with key bindings inside the
|
// Renders a centered help box with key bindings inside the
|
||||||
// available console area. The caller is responsible for waiting on a
|
// available console area. The caller is responsible for waiting on a
|
||||||
// key and redrawing the menu afterwards.
|
// key and redrawing the menu afterward.
|
||||||
public void ShowHelpBox(int availableLines)
|
public void ShowHelpBox(int availableLines)
|
||||||
{
|
{
|
||||||
try
|
var width = Console.WindowWidth;
|
||||||
|
var helpLines = GetHelpLines();
|
||||||
|
var boxHeight = Math.Min(helpLines.Length + 2, Math.Max(3, availableLines));
|
||||||
|
var boxWidth = Math.Max(1, width);
|
||||||
|
var topRow = Math.Max(0, (availableLines - boxHeight) / 2);
|
||||||
|
|
||||||
|
Console.BackgroundColor = ConsoleColor.DarkGray;
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
|
||||||
|
// Fill the box area without borders for cleaner appearance
|
||||||
|
for (var i = 0; i < boxHeight; i++)
|
||||||
{
|
{
|
||||||
int width = Console.WindowWidth;
|
var row = topRow + i;
|
||||||
string[] helpLines = GetHelpLines();
|
if (row < Console.WindowHeight)
|
||||||
int boxHeight = Math.Min(helpLines.Length + 2, Math.Max(3, availableLines));
|
|
||||||
int boxWidth = Math.Max(1, width);
|
|
||||||
int topRow = Math.Max(0, (availableLines - boxHeight) / 2);
|
|
||||||
|
|
||||||
Console.BackgroundColor = ConsoleColor.DarkGray;
|
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
|
||||||
|
|
||||||
string topBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
|
||||||
Console.SetCursorPosition(0, topRow);
|
|
||||||
Console.Write(topBorder);
|
|
||||||
|
|
||||||
for (int i = 0; i < boxHeight - 2; i++)
|
|
||||||
{
|
{
|
||||||
int row = topRow + 1 + i;
|
|
||||||
string content;
|
|
||||||
if (i < helpLines.Length)
|
|
||||||
{
|
|
||||||
content = PadToWidth(helpLines[i], boxWidth - 2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
content = new string(' ', Math.Max(0, boxWidth - 2));
|
|
||||||
}
|
|
||||||
Console.SetCursorPosition(0, row);
|
Console.SetCursorPosition(0, row);
|
||||||
Console.Write("|" + content + "|");
|
var content = i == 0 || i == boxHeight - 1 || i >= helpLines.Length + 2
|
||||||
|
? new string(' ', boxWidth)
|
||||||
|
: PadToWidth(helpLines[i - 1], boxWidth);
|
||||||
|
Console.Write(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
int bottomRow = topRow + boxHeight - 1;
|
|
||||||
if (bottomRow < Console.WindowHeight)
|
|
||||||
{
|
|
||||||
string bottomBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
|
||||||
Console.SetCursorPosition(0, bottomRow);
|
|
||||||
Console.Write(bottomBorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
Console.ResetColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string[] GetHelpLines()
|
private static string[] GetHelpLines()
|
||||||
@@ -190,14 +168,13 @@ namespace MarcerGameDvdLauncher
|
|||||||
return [
|
return [
|
||||||
" Help — Key Bindings",
|
" Help — Key Bindings",
|
||||||
" ",
|
" ",
|
||||||
" ↑ / ↓ Move selection up / down",
|
" ↑ / ↓ Move selection up / down",
|
||||||
" Enter / → Open folder / launch ZIP with Hatari",
|
" PgUp / PgDn Jump one page up / down",
|
||||||
" ← / BS Go up one directory (never exceeds root)",
|
" Enter / → Open folder / launch ZIP with Hatari",
|
||||||
" ESC / Q Exit the program",
|
" * Toggle favorite on selected ZIP",
|
||||||
" PgUp Jump one page up",
|
" ← / Backspace Go up one directory (never exceeds root)",
|
||||||
" PgDn Jump one page down",
|
" ? Show this help",
|
||||||
" * Toggle favorite on selected ZIP",
|
" ESC / q Exit the program",
|
||||||
" ? Show this help",
|
|
||||||
" ",
|
" ",
|
||||||
" Navigation is strictly limited to RootDirectory.",
|
" Navigation is strictly limited to RootDirectory.",
|
||||||
" The overlay shows both root and patch layers combined.",
|
" The overlay shows both root and patch layers combined.",
|
||||||
@@ -231,7 +208,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
if (width <= 0) return string.Empty;
|
if (width <= 0) return string.Empty;
|
||||||
|
|
||||||
string label = GetLabel(e, isFavorite);
|
var label = GetLabel(e, isFavorite);
|
||||||
|
|
||||||
// If the console width is smaller than the label, truncate the label
|
// If the console width is smaller than the label, truncate the label
|
||||||
if (width <= label.Length)
|
if (width <= label.Length)
|
||||||
@@ -239,7 +216,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
return label.Substring(0, width);
|
return label.Substring(0, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
int maxNameLen = width - label.Length; // space left for name
|
var maxNameLen = width - label.Length; // space left for name
|
||||||
string displayName;
|
string displayName;
|
||||||
if (e.Name.Length <= maxNameLen)
|
if (e.Name.Length <= maxNameLen)
|
||||||
{
|
{
|
||||||
@@ -253,7 +230,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
displayName = e.Name.Substring(0, Math.Max(0, maxNameLen));
|
displayName = e.Name.Substring(0, Math.Max(0, maxNameLen));
|
||||||
}
|
}
|
||||||
|
|
||||||
int padding = Math.Max(0, width - label.Length - displayName.Length);
|
var padding = Math.Max(0, width - label.Length - displayName.Length);
|
||||||
var result = label + displayName + new string(' ', padding);
|
var result = label + displayName + new string(' ', padding);
|
||||||
// Ensure exact width (defensive): truncate or pad if needed
|
// Ensure exact width (defensive): truncate or pad if needed
|
||||||
if (result.Length > width) return result.Substring(0, width);
|
if (result.Length > width) return result.Substring(0, width);
|
||||||
@@ -269,7 +246,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
// Layer label (7 chars)
|
// Layer label (7 chars)
|
||||||
string layer;
|
string layer;
|
||||||
if (e.InRoot && e.InPatch) layer = "[BOTH] ";
|
if (e is {InRoot: true, InPatch: true}) layer = "[BOTH] ";
|
||||||
else if (e.InPatch) layer = "[PTCH] ";
|
else if (e.InPatch) layer = "[PTCH] ";
|
||||||
else if (e.InRoot) layer = "[ROOT] ";
|
else if (e.InRoot) layer = "[ROOT] ";
|
||||||
else layer = " ";
|
else layer = " ";
|
||||||
@@ -295,15 +272,15 @@ namespace MarcerGameDvdLauncher
|
|||||||
|
|
||||||
if (e.Kind == EntryKind.Directory)
|
if (e.Kind == EntryKind.Directory)
|
||||||
{
|
{
|
||||||
if (e.InRoot && e.InPatch) return _colors.FolderBoth; // Both layers
|
if (e is {InRoot: true, InPatch: true}) return _colors.FolderBoth; // Both layers
|
||||||
if (e.InPatch && !e.InRoot) return _colors.FolderPatchOnly; // Only patch
|
if (e is {InPatch: true, InRoot: false}) return _colors.FolderPatchOnly; // Only patch
|
||||||
if (e.InRoot && !e.InPatch) return _colors.FolderRootOnly; // Only root
|
if (e is {InRoot: true, InPatch: false}) return _colors.FolderRootOnly; // Only root
|
||||||
}
|
}
|
||||||
else if (e.Kind == EntryKind.Zip)
|
else if (e.Kind == EntryKind.Zip)
|
||||||
{
|
{
|
||||||
if (e.InRoot && e.InPatch) return _colors.ZipBoth; // Both layers
|
if (e is {InRoot: true, InPatch: true}) return _colors.ZipBoth; // Both layers
|
||||||
if (e.InRoot && !e.InPatch) return _colors.ZipRootOnly; // Only root
|
if (e is { InRoot: true, InPatch: false }) return _colors.ZipRootOnly; // Only root
|
||||||
if (e.InPatch && !e.InRoot) return _colors.ZipPatchOnly; // Only patch
|
if (e is {InPatch: true, InRoot: false}) return _colors.ZipPatchOnly; // Only patch
|
||||||
}
|
}
|
||||||
return ConsoleColor.DarkGray;
|
return ConsoleColor.DarkGray;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
// Scroll fractions: when the selection cursor reaches 2/3 of the visible window
|
// Scroll fractions: when the selection cursor reaches 2/3 of the visible window
|
||||||
// height from the top, the list scrolls down; when it reaches 1/3, it scrolls up.
|
// height from the top, the list scrolls down; when it reaches 1/3, it scrolls up.
|
||||||
// Not configurable — these ratios are the established navigation behaviour.
|
// Not configurable — these ratios are the established navigation behaviour.
|
||||||
private const double BottomScrollFraction = 2.0 / 3.0;
|
private const double BOTTOM_SCROLL_FRACTION = 2.0 / 3.0;
|
||||||
private const double TopScrollFraction = 1.0 / 3.0;
|
private const double TOP_SCROLL_FRACTION = 1.0 / 3.0;
|
||||||
|
|
||||||
public int SelectedIndex { get; private set; } = 0;
|
public int SelectedIndex { get; private set; }
|
||||||
public int ScrollOffset { get; private set; } = 0;
|
public int ScrollOffset { get; private set; }
|
||||||
public string CurrentRelativePath { get; private set; } = "";
|
public string CurrentRelativePath { get; private set; } = "";
|
||||||
// Stores the last selection & scroll position for each directory
|
// Stores the last selection & scroll position for each directory
|
||||||
private readonly Dictionary<string, (int sel, int scroll)> _lastSelections = new();
|
private readonly Dictionary<string, (int sel, int scroll)> _lastSelections = new();
|
||||||
@@ -105,12 +105,12 @@ namespace MarcerGameDvdLauncher
|
|||||||
if (availableLines < 1) availableLines = 1;
|
if (availableLines < 1) availableLines = 1;
|
||||||
if (entryCount <= availableLines) { ScrollOffset = 0; return; }
|
if (entryCount <= availableLines) { ScrollOffset = 0; return; }
|
||||||
if (SelectedIndex == 0) { ScrollOffset = 0; return; }
|
if (SelectedIndex == 0) { ScrollOffset = 0; return; }
|
||||||
int bottomScrollTrigger = ScrollOffset + (int)(availableLines * BottomScrollFraction);
|
int bottomScrollTrigger = ScrollOffset + (int)(availableLines * BOTTOM_SCROLL_FRACTION);
|
||||||
int topScrollTrigger = ScrollOffset + (int)(availableLines * TopScrollFraction);
|
int topScrollTrigger = ScrollOffset + (int)(availableLines * TOP_SCROLL_FRACTION);
|
||||||
if (SelectedIndex >= bottomScrollTrigger && (ScrollOffset + availableLines) < entryCount)
|
if (SelectedIndex >= bottomScrollTrigger && (ScrollOffset + availableLines) < entryCount)
|
||||||
ScrollOffset = SelectedIndex - (int)(availableLines * BottomScrollFraction);
|
ScrollOffset = SelectedIndex - (int)(availableLines * BOTTOM_SCROLL_FRACTION);
|
||||||
else if (SelectedIndex < topScrollTrigger && ScrollOffset > 0)
|
else if (SelectedIndex < topScrollTrigger && ScrollOffset > 0)
|
||||||
ScrollOffset = SelectedIndex - (int)(availableLines * TopScrollFraction);
|
ScrollOffset = SelectedIndex - (int)(availableLines * TOP_SCROLL_FRACTION);
|
||||||
if (ScrollOffset < 0) ScrollOffset = 0;
|
if (ScrollOffset < 0) ScrollOffset = 0;
|
||||||
if (ScrollOffset > entryCount - availableLines)
|
if (ScrollOffset > entryCount - availableLines)
|
||||||
ScrollOffset = entryCount - availableLines;
|
ScrollOffset = entryCount - availableLines;
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ namespace MarcerGameDvdLauncher
|
|||||||
|
|
||||||
public class OverlayDirectoryBrowser(string root, string patch)
|
public class OverlayDirectoryBrowser(string root, string patch)
|
||||||
{
|
{
|
||||||
public List<GameEntry> GetEntries(string currentRelativePath)
|
public List<GameEntry> GetEntries(string? currentRelativePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Normalize and protect against path traversal or absolute paths in the relative path
|
// Normalize and protect against path traversal or absolute paths in the relative path
|
||||||
string rel = currentRelativePath ?? string.Empty;
|
var rel = currentRelativePath ?? string.Empty;
|
||||||
// Remove any leading directory separators
|
// Remove any leading directory separators
|
||||||
rel = rel.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
rel = rel.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
// If the relative path is rooted or contains parent directory segments, reset to root
|
// If the relative path is rooted or contains parent directory segments, reset to root
|
||||||
if (Path.IsPathRooted(rel) || rel.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries).Any(p => p == ".."))
|
if (Path.IsPathRooted(rel) || rel.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries).Any(p => p == ".."))
|
||||||
{
|
{
|
||||||
rel = string.Empty;
|
rel = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
// Console window title. Not configurable — fixed application display name.
|
// Console window title. Not configurable — fixed application display name.
|
||||||
private const string DefaultTitle = "Marcer GameDVD Launcher";
|
private const string DEFAULT_TITLE = "Marcer GameDVD Launcher";
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Console.Title = DefaultTitle;
|
Console.Title = DEFAULT_TITLE;
|
||||||
var app = new LauncherApp();
|
var app = new LauncherApp();
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
// Licensed under the MIT License. See LICENSE file in project root for details.
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
@@ -40,11 +39,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
// These are defined by the Windows API and do not change.
|
// These are defined by the Windows API and do not change.
|
||||||
private const int STD_INPUT_HANDLE = -10;
|
private const int STD_INPUT_HANDLE = -10;
|
||||||
|
|
||||||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern IntPtr GetStdHandle(int nStdHandle);
|
private static extern IntPtr GetStdHandle(int nStdHandle);
|
||||||
|
|
||||||
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool FlushConsoleInputBuffer(IntPtr hConsoleInput);
|
private static extern bool FlushConsoleInputBuffer(IntPtr hConsoleInput);
|
||||||
|
|
||||||
// P/Invoke to query key state (used to detect physical key release)
|
// P/Invoke to query key state (used to detect physical key release)
|
||||||
@@ -54,37 +53,36 @@ namespace MarcerGameDvdLauncher
|
|||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
private static extern short GetAsyncKeyState(int vKey);
|
private static extern short GetAsyncKeyState(int vKey);
|
||||||
|
|
||||||
// Shows a simple modal message on the reserved last console line and
|
// Shows a simple modal message in the center of the console and
|
||||||
// blocks until the Return key is physically released. The message is
|
// blocks until the given process has exited. The message is then
|
||||||
// then removed and the method returns. This is designed to be a
|
// removed and the method returns.
|
||||||
// lightweight modal for the console UI and uses the last console line
|
public static void ShowModalUntilProcessExited(System.Diagnostics.Process? process, string? message)
|
||||||
// which the application reserves for transient messages.
|
|
||||||
public static void ShowModalUntilReturnReleased(string message)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int lastRow = AvailableLines;
|
|
||||||
int width = Console.WindowWidth;
|
int width = Console.WindowWidth;
|
||||||
string text = message ?? string.Empty;
|
int height = Console.WindowHeight;
|
||||||
if (text.Length > width) text = text.Substring(0, Math.Max(0, width - 3)) + "...";
|
int centerRow = height / 2;
|
||||||
int padding = Math.Max(0, width - text.Length);
|
|
||||||
string line = text + new string(' ', padding);
|
var text = message ?? string.Empty;
|
||||||
|
if (text.Length > width - 4) text = text.Substring(0, Math.Max(0, width - 7)) + "...";
|
||||||
|
int leftPad = Math.Max(0, (width - text.Length) / 2);
|
||||||
|
var line = new string(' ', leftPad) + text;
|
||||||
|
|
||||||
Console.BackgroundColor = ConsoleColor.DarkGray;
|
Console.BackgroundColor = ConsoleColor.DarkGray;
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
try { Console.SetCursorPosition(0, lastRow); } catch { }
|
try { Console.SetCursorPosition(0, centerRow); } catch { }
|
||||||
try { Console.Write(line); } catch { }
|
try { Console.Write(line.PadRight(width)); } catch { }
|
||||||
Console.ResetColor();
|
Console.ResetColor();
|
||||||
|
|
||||||
// Wait until Return key is not pressed
|
// Wait until the external process has exited
|
||||||
// GetAsyncKeyState returns a short where the high-order bit is set when key is down
|
while (process != null && !process.HasExited)
|
||||||
while ((GetAsyncKeyState(VK_RETURN) & 0x8000) != 0)
|
|
||||||
{
|
{
|
||||||
Thread.Sleep(10);
|
Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the line
|
// Clear the line
|
||||||
try { Console.SetCursorPosition(0, lastRow); } catch { }
|
try { Console.SetCursorPosition(0, centerRow); } catch { }
|
||||||
try { Console.Write(new string(' ', width)); } catch { }
|
try { Console.Write(new string(' ', width)); } catch { }
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -122,5 +120,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
}
|
}
|
||||||
if (waitForKey) Console.ReadKey(intercept: true);
|
if (waitForKey) Console.ReadKey(intercept: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convenience overload: flate argument list instead of explicit array.
|
||||||
|
public static void ShowConsoleMessage(params string[] lines)
|
||||||
|
{
|
||||||
|
ShowConsoleMessage(lines, ConsoleColor.Gray, clear: true, waitForKey: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"PatchDirectory": "C:\\Games\\Hatari\\PATCH",
|
"PatchDirectory": "C:\\Games\\Hatari\\PATCH",
|
||||||
"Hatari": {
|
"Hatari": {
|
||||||
"Executable": "C:\\Tools\\hatari\\hatari.exe",
|
"Executable": "C:\\Tools\\hatari\\hatari.exe",
|
||||||
"ConfigFile": "C:\\Tools\\hatari\\hatari-st.cfg",
|
"ConfigFile": "",
|
||||||
"ArgsTemplate": "-c \"{cfg}\" --disk-a \"{zip}\""
|
"ArgsTemplate": "-c \"{cfg}\" --disk-a \"{zip}\""
|
||||||
},
|
},
|
||||||
"Colors": {
|
"Colors": {
|
||||||
|
|||||||
Reference in New Issue
Block a user