20 Commits
Author SHA1 Message Date
stefankoelle 9f12ec9075 release v0.9.2
Release v0.9.2: MIT License + automated release notes

- Add MIT License to project root and license headers across all source files (C#, shell, batch)
- Include LICENSE in publish output via .csproj so it ships in release ZIPs
- Update GitHub Actions to latest versions (checkout v7, setup-dotnet v6, upload/download-artifact v7/v8, action-gh-release v3)
- Release workflow now uses the commit message body as release notes (via /create-release command)
- Add renovate.json for automated dependency updates
- Fix demo.sh setup script
- Update README.md and AGENTS.md with License sections and refreshed module overview
- Add .gitignore entry for Python virtual environments (.venv/)
2026-08-11 00:52:28 +02:00
stefankoelle c79ff80a84 add LICENSE to build 2026-08-11 00:50:40 +02:00
Stefan Koelle 8cb3b1a624 Merge pull request #1 from skoelle/renovate/major-github-actions
Update GitHub Actions (major)
2026-08-11 00:49:44 +02:00
renovate[bot] 393290e37e Update GitHub Actions 2026-08-10 22:49:16 +00:00
stefankoelle ba8004ef50 renovate 2026-08-11 00:47:51 +02:00
stefankoelle 6bb086516c LICENSE 2026-08-11 00:46:55 +02:00
stefankoelle 48a08f9bb1 fix demo.sh 2026-08-10 23:11:42 +02:00
stefankoelle d5cf0cd723 AGENTS.md 2026-08-10 22:34:18 +02:00
stefankoelle 45a7c3ace2 README.md 2026-08-10 22:29:54 +02:00
stefankoelle eabf2066b6 README.md 2026-08-10 22:22:57 +02:00
stefankoelle 86dcf7bd32 README.md 2026-08-10 22:20:57 +02:00
stefankoelle c8229dd0e1 release command 2026-08-10 22:13:55 +02:00
stefankoelle 26d3530b86 move all files 2026-08-10 21:45:47 +02:00
stefankoelle 94b4ed7fb3 ui help function 2026-08-10 21:37:05 +02:00
stefankoelle 87720e6db4 fix release pipeline 2026-08-10 21:31:39 +02:00
stefankoelle d06ee381be fix hatari exists 2026-08-10 21:28:01 +02:00
stefankoelle ea66414496 fix AGENTS.md 2026-08-10 21:21:26 +02:00
stefankoelle 7360687da7 demo mode 2026-08-10 21:15:32 +02:00
stefankoelle ca2e46a8a6 fix: zip from publish/ subfolder to avoid duplicate files 2026-08-10 20:31:52 +02:00
stefankoelle a9e9aecf3e add community link to Facebook group in README 2026-08-10 20:27:26 +02:00
28 changed files with 767 additions and 210 deletions
+18 -10
View File
@@ -26,10 +26,10 @@ jobs:
artifact_name: osx-x64
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
@@ -41,22 +41,23 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build
working-directory: src/MarcerGameDvdLauncher
run: dotnet publish -c Release -r ${{ matrix.rid }} -p:Version=${{ steps.version.outputs.version }} --self-contained false
- name: Create ZIP (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
Compress-Archive -Path "MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}/*" -DestinationPath "MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip"
Compress-Archive -Path "src/MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}/publish/*" -DestinationPath "MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip"
- name: Create ZIP (Linux/macOS)
if: matrix.os != 'windows-latest'
run: |
cd MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}
zip -r ../../../MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip .
cd src/MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}/publish
zip -r $GITHUB_WORKSPACE/MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip .
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}
path: MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip
@@ -65,12 +66,12 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: artifacts
@@ -80,7 +81,14 @@ jobs:
CURRENT_TAG="${GITHUB_REF_NAME}"
PREV_TAG=$(git describe --tags --abbrev=0 ${CURRENT_TAG}^ 2>/dev/null || echo "")
if [ -n "$PREV_TAG" ]; then
# Get the full commit message body of the release commit
# (the empty commit created by /create-release)
COMMIT_HASH=$(git rev-list -n 1 ${CURRENT_TAG})
BODY=$(git log -1 --format=%b ${COMMIT_HASH})
if [ -n "$BODY" ]; then
echo "$BODY" > release-notes.md
elif [ -n "$PREV_TAG" ]; then
echo "## Changes since ${PREV_TAG}" > release-notes.md
echo "" >> release-notes.md
git log --oneline ${PREV_TAG}..${CURRENT_TAG} >> release-notes.md
@@ -89,7 +97,7 @@ jobs:
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
name: MarcerGameDvdLauncher ${{ github.ref_name }}
body_path: release-notes.md
+6
View File
@@ -12,6 +12,9 @@ obj/
.DS_Store
Thumbs.db
# Python virtual environment
.venv/
# Release-Verzeichnis (keine ZIPs o.ä. ins Git!)
# Only ignore top-level release/ directory. Release directories in subfolders are allowed.
/release/
@@ -19,3 +22,6 @@ Thumbs.db
# User-specific configuration (real config, not example)
launcher.config.json
favorites.txt
# Demo directory (generated by demo.sh)
.demo/
+35
View File
@@ -0,0 +1,35 @@
---
description: Create and push a release tag (e.g. /create-release 1.0.0)
---
Create a release tag and push it to origin. The GitHub Action will automatically build for all platforms and create the GitHub Release.
## Steps
1. Validate the version argument ($ARGUMENTS):
- Must be provided, otherwise show error and stop
- Must match semver format (e.g. 1.0.0, 0.9.1, 2.0.0-beta.1)
2. Check for uncommitted changes:
- Run `git status --porcelain`
- If any output, warn the user and stop (commit first)
3. Analyze changes since last release:
- Run `git log --oneline $(git describe --tags --abbrev=0 HEAD)..HEAD` to list all commits
- Read the changed files to understand context
- Write a concise, well-structured release summary in English with:
- A one-line overview
- Bullet points for each notable change (features, fixes, breaking changes)
- Keep it developer-friendly, no fluff
4. Create release commit with the summary as message:
- 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
5. Create annotated tag on that commit:
- Run `git tag -a v$ARGUMENTS -m "Release v$ARGUMENTS"`
6. Push commit and tag:
- Run `git push origin main --tags` (or current branch)
7. Confirm success with the version number
+36 -22
View File
@@ -4,7 +4,7 @@ applyTo: '**'
## Module Overview (Marcer GameDVD Launcher)
The implementation is split into focused modules (files) under the `MarcerGameDvdLauncher/` folder. Keep this section up to date when files are added, removed or responsibilities change.
The implementation is split into focused modules (files) under the `src/MarcerGameDvdLauncher/` folder. Keep this section up to date when files are added, removed or responsibilities change.
- MarcerGameDvdLauncher/Program.cs: Minimal entry point. Sets console title and starts the application by creating `LauncherApp`.
- MarcerGameDvdLauncher/LauncherApp.cs: Application lifecycle host — loads configuration, initializes components and runs the main directory navigation loop (contains `AppHost` internal class).
@@ -12,19 +12,21 @@ The implementation is split into focused modules (files) under the `MarcerGameDv
- MarcerGameDvdLauncher/ProgramHelpers.cs: Small shared helpers (resolve relative paths, centralized console message helper) used across modules.
- 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).
- MarcerGameDvdLauncher/MenuRenderer.cs: Console rendering logic — efficient per-line redraw, double-buffering and color selection according to overlay rules.
- MarcerGameDvdLauncher/MenuRenderer.cs: Console rendering logic — efficient per-line redraw, double-buffering, color selection according to overlay rules, and the help box overlay.
- MarcerGameDvdLauncher/HatariLauncher.cs: Responsible for validating the Hatari executable and starting Hatari with the configured argument template (replaces `{cfg}` and `{zip}`).
- MarcerGameDvdLauncher/FavoritesService.cs: Manages the favorites/bookmark system — toggling favorites on ZIPs, persisting them to `favorites.txt`, and providing the virtual `Favorites` folder view.
- MarcerGameDvdLauncher/DirectoryService.cs: Filesystem service layer — enumerates directories and ZIPs, resolves paths, and provides the underlying I/O operations used by OverlayDirectoryBrowser.
- MarcerGameDvdLauncher/UIErrorService.cs: Centralized UI error presentation using the console message helper.
Note: This overview is intentionally concise. For behavioral changes (navigation, color scheme, launch command or config schema), update this file (agents.md) and README.md as required by project policy.
Note: This overview is intentionally concise. For behavioral changes (navigation, color scheme, launch command or config schema), update this file (AGENTS.md) and README.md as required by project policy.
**Note for Automated Tests/CI:**
The Launcher cannot be executed or tested via `start.cmd` from this environment (build system, automation agent) since no Windows console environment is present. For release workflows and developer validation, it is ALWAYS required to do a manual test run via start.cmd per documentation and policy before delivery.
The Launcher cannot be executed or tested via `scripts/start.cmd` from this environment (build system, automation agent) since no Windows console environment is present. For release workflows and developer validation, it is ALWAYS required to do a manual test run via `scripts/start.cmd` (Windows) or `scripts/start.sh` (Linux/macOS) per documentation and policy before delivery.
**Release Process (automated via GitHub Actions):**
- Pushing a tag (`v*`) triggers the GitHub Action workflow (`.github/workflows/release.yml`).
- The workflow builds platform-specific artifacts (Windows, Linux, macOS), generates release notes from git log, and creates a GitHub Release with all ZIPs attached.
- Developer steps for a release:
1. Ensure `README.md` and `agents.md` are up to date.
1. Ensure `README.md` and `AGENTS.md` are up to date.
2. Commit all changes.
3. Create and push a tag: `git tag v{version} && git push origin v{version}`.
4. The GitHub Action handles the rest (build, ZIP, release notes, GitHub Release).
@@ -32,17 +34,17 @@ The Launcher cannot be executed or tested via `start.cmd` from this environment
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.
- 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 the repository root: `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 `build.cmd` (Windows) or `build.sh` (Linux) and `start.cmd` for consistent command-line builds/releases as described elsewhere in this document.
- 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
The console launcher is meant for browsing a games directory and can launch ZIP files with the Hatari emulator under Windows. Control is exclusively via keyboard in the console window.
The console launcher is meant for browsing a games directory and can launch ZIP files with the Hatari emulator. It runs on Windows, Linux, and macOS. Control is exclusively via keyboard in the console window.
## Detailed Requirements
@@ -57,6 +59,7 @@ The console launcher is meant for browsing a games directory and can launch ZIP
- Backspace: jump to parent directory (never outside root)
- ESC: exit the program
- PageUp/PageDown: jump by one page up/down through the file list
- `?`: show a help box with key bindings
- The file list always shows exactly as many lines as fit the screen ALWAYS **one line less** than the console height (`Console.WindowHeight - 1`). This avoids overflow at the bottom and ensures the selection never enters the non-visible area.
Rationale: writing to the very last console line can cause the Windows console to auto-scroll or produce visual jumps when the cursor reaches the bottom row. Reserving one line prevents unintended scrolling/flicker and keeps the selection cursor strictly within the visible area.
Maintenance: when changing rendering or navigation logic, always compute the displayed page size as `availableLines = Console.WindowHeight - 1` and keep this value consistent across MenuRenderer, NavigationController and any other code that references the console height.
@@ -75,17 +78,16 @@ The console launcher is meant for browsing a games directory and can launch ZIP
- 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).
- 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 .exe 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
- Optional: Build and start scripts (build.cmd / build.sh / start.cmd) are present, adapt as needed.
- For ALL builds, tests, and releases, ONLY the platform build script may be used: `build.cmd` (Windows) or `build.sh` (Linux). Direct `dotnet build`/`dotnet run` calls are NOT allowed, as they can lead to version/runtime conflicts. The application must always be started and tested using `start.cmd`.
- After making any code changes that affect behavior or touch source files, run the platform build script (`build.cmd` on Windows, `build.sh` on Linux) and ensure the build completes successfully before committing. Additionally, perform a manual functional test using `start.cmd` on a Windows machine prior to pushing a release.
- Build and start scripts (`scripts/build.cmd` / `scripts/build.sh` / `scripts/start.cmd` / `scripts/start.sh`) are present and must be used.
- For ALL builds, tests, and releases, ONLY the platform build script may be used: `scripts/build.cmd` (Windows) or `scripts/build.sh` (Linux/macOS). Direct `dotnet build`/`dotnet run` calls are NOT allowed, as they can lead to version/runtime conflicts. The application must always be started and tested using `scripts/start.cmd` (Windows) or `scripts/start.sh` (Linux/macOS).
- 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.
- After each build for a release, the entire build output directory (`bin/Release/net10.0/`) must be zipped in the `release/` directory, and the ZIP must be uploaded as a release asset in Gitea.
- 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.
- **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.
---
@@ -106,13 +108,19 @@ Note on PatchDirectory semantics:
Implementation note (input flushing):
- To avoid undesired key-repeat / input "afterglow" when the user holds navigation keys, the application performs a best-effort flush of the console input buffer after navigation events. This is implemented by ProgramHelpers.FlushInputBuffer(), which uses the Win32 FlushConsoleInputBuffer API on Windows. This behaviour is intentional and required to provide a responsive navigation experience.
### Color Scheme
- Folder in both layers: **ConsoleColor.Yellow**
- Folder only in patch layer: **ConsoleColor.DarkYellow**
- Folder only in main layer: **ConsoleColor.Gray**
- ZIP in both layers: **ConsoleColor.Green**
- ZIP only in main layer: **ConsoleColor.DarkGreen**
- ZIP only in patch layer: **ConsoleColor.Magenta**
### Color Scheme and Layer Labels
Each entry is displayed with a left label indicating its layer status:
- **`[BOTH]`**: Entry exists in both main and patch layer
- **`[ROOT]`**: Entry exists only in main (root) layer
- **`[PTCH]`**: Entry exists only in patch layer
Color mapping:
- Folder in both layers: **ConsoleColor.Yellow** (`[BOTH]`)
- Folder only in patch layer: **ConsoleColor.DarkYellow** (`[PTCH]`)
- Folder only in main layer: **ConsoleColor.Gray** (`[ROOT]`)
- ZIP in both layers: **ConsoleColor.Green** (`[BOTH]`)
- ZIP only in main layer: **ConsoleColor.DarkGreen** (`[ROOT]`)
- ZIP only in patch layer: **ConsoleColor.Magenta** (`[PTCH]`)
Note: The ConsoleColor mapping above is authoritative for the application. If you change color values in code (MenuRenderer/GetColorForEntry), update this section to keep documentation and implementation in sync.
@@ -120,3 +128,9 @@ Note: The ConsoleColor mapping above is authoritative for the application. If yo
- Navigation is always based strictly on the **relative path from root** and is consistent on all levels (Backspace always moves up one level, Enter always moves one level deeper, regardless of which layer).
---
## License
MIT License - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
- Full text in `LICENSE`
- License headers in all source code files
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -2
View File
@@ -42,7 +42,7 @@ Ziel: Das Repo aufräumen (Doku, Code, Config)
- [ ] `MenuRenderer` bekommt optional passende `AppColorConfig` (Konstruktor-Injection);
`GetColorForEntry`/`GetColors` nutzen Config statt Konstanten.
- [ ] Falls Config-Werte fehlen → heutiges Verhalten beibehalten (fallback).
- [ ] Doku synchronisieren: README + agents.md Farbtabellen auf Config-Felder verlinken.
- [ ] Doku synchronisieren: README + AGENTS.md Farbtabellen auf Config-Felder verlinken.
- [ ] Beispielwerte ins `launcher.config.example.json` aufnehmen.
### 2b. Benutzer-Config (User-Config) entschieden
@@ -94,4 +94,4 @@ externe Anpassungsmöglichkeit dafür.
## 4. Abschlusskriterien
- [ ] Commit mit aussagekräftiger Message (nur echter Autor, kein Co-Author).
- [ ] Nach Doku- und Code-Änderungen: `build.cmd` (Windows) bzw. `build.sh` (Linux) läuft fehlerfrei.
- [ ] Nach Doku- und Code-Änderungen: `scripts/build.cmd` (Windows) bzw. `scripts/build.sh` (Linux) läuft fehlerfrei.
+184 -109
View File
@@ -1,99 +1,65 @@
# Marcer GameDVD Launcher
A performant, consistent console launcher for the Hatari emulator on Windows. Control is entirely via keyboard—using only the arrow keys, Enter, Backspace, ESC, PageUp, and PageDown, you can browse your game archive quickly and comfortably. Navigation is strictly limited to the configured root directory. ZIPs are seamlessly launched via Hatari. Thanks to overlay/patch mode, a consistent color scheme, and robust cursor/scroll logic, even the largest archives or deeply nested directory trees are handled smoothly and reliably.
A fast, keyboard-driven console launcher for the Hatari emulator. Browse your Atari ST game archive, navigate folders, and launch ZIPs — all from the terminal. Supports overlay/patch mode for comparing and merging game directories.
## Features
- **Overlay/Patch Union:** Recursively merges main and patch directory at every level. Each object/name is shown only once (patch takes precedence).
- **Dynamic, practical color scheme:**
| Entry | ConsoleColor | Meaning
|------------------------|--------------|---------------------------------------------|
| Folder in both | ConsoleColor.Yellow | Directory in both layers (patch/main)
| Patch-only folder | ConsoleColor.DarkYellow | Directory only in patch layer
| Main-only folder | ConsoleColor.Gray | Directory only in main layer
| ZIP in both | ConsoleColor.Green | ZIP archive in both layers
| Main-only ZIP | ConsoleColor.DarkGreen | ZIP archive only in main layer
| Patch-only ZIP | ConsoleColor.Magenta | ZIP archive only in patch layer
## Roadmap
### Released Features
- **Favorites/bookmark system:**
- Press `*` on a ZIP to toggle it as a favorite. Favorites are shown in a virtual `Favorites` folder at the top of the root listing when any favorites exist.
- Favorites are persisted in `favorites.txt` in the configured `PatchDirectory`, or next to the EXE if no patch directory is set.
### Planned Features
- **ZIP database & metadata extraction** (from version 2.0)
- Builds a database (e.g. as a local file), stores all known ZIPs
- Enables full-text search, filters, later analysis
- **Search/filter (quicksearch) over ZIPs** (from v2.0, via database)
- Fast name search inside launcher, with history
- **History/list of recently launched games** (from v2.0, via database)
- Automatic access to recently played titles
- **Overlay hot swap** (from version 3.0)
- Overlay/patch folder can be switched at runtime, instant comparison
**More ideas will be added iteratively!**
Built for the [Marcer GameDVD](https://www.facebook.com/groups/360493904888475/) community on Facebook.
---
**Display Performance Note:**
- The current rendering logic follows state-of-the-art principles for performant C# console apps:
- Minimal redraw: Only the truly changed line is redrawn, never the whole screen.
- No Console.Clear or full redraw on cursor movement—just targeted SetCursorPosition and Write.
- This method (per StackOverflow, Spectre.Console, Terminal.Gui, etc.) is optimal for smooth navigation in large lists.
- Further performance can be gained with shadow buffers/string-diffs per line, but currently there's no practical performance issue.
- Thus, display performance is at “best practice” level for .NET TUIs.
- Long file names and narrow console widths are handled defensively: MenuRenderer truncates file names so that each rendered line is exactly Console.WindowWidth characters long. This prevents Console.Write from overflowing the line and avoids visual artifacts when names are longer than the available width.
## 🎮 Features
- **Overlay/Patch Mode:** Recursively merges a main game directory with an optional patch directory. If a file or folder exists in both, the patch version takes precedence.
- **Layer Labels:** Each entry shows its source — `[BOTH]`, `[ROOT]`, or `[PTCH]` — with a matching color scheme.
- **Favorites:** Press `*` on any ZIP to bookmark it. Bookmarked games appear in a virtual `Favorites` folder at the top of the root listing.
- **Robust Navigation:** Cursor position is remembered per directory. Scrolling and page jumps adapt dynamically to any console height.
- **Minimal Redraw:** Only changed lines are redrawn — no flicker, no `Console.Clear`, smooth even in huge directory trees.
### Color Scheme
| Entry | Label | Color | Meaning |
|---|---|---|---|
| Folder in both layers | `[BOTH]` | Yellow | Exists in main + patch |
| Patch-only folder | `[PTCH]` | DarkYellow | Only in patch layer |
| Main-only folder | `[ROOT]` | Gray | Only in main layer |
| ZIP in both layers | `[BOTH]` | Green | Exists in main + patch |
| Main-only ZIP | `[ROOT]` | DarkGreen | Only in main layer |
| Patch-only ZIP | `[PTCH]` | Magenta | Only in patch layer |
---
### Features we will NOT implement
- User-configurable key bindings (keymap)
- Display & import of screenshots/cover images
- Music/Sound player integration (YM/MOD/SND, etc.)
- Persistent UI settings, window size management (not relevant in console mode)
## Operation and Display
## 🕹️ End Users
- **Consistent navigation & controls:**
- Arrow up/down: move selection (always visible)
- Enter: open folder / launch ZIP with Hatari (patch variant always preferred if present)
- Backspace: exactly one level up (never exceeds root)
- ESC: exit the program immediately
- PageUp/PageDown: jump exactly one screen full (window height - 1)
- Display always one line less than console height; no overflow/cut-off
- **Cursor position saving per directory:**
- The last position/selection of each directory is retained, even after Backspace
- **Robust, smooth redraw:**
- Optimized full redraw on scrolling/paging
- Minimal redraw on cursor move
- **Minimal resource usage (handles huge trees efficiently)**
- **Navigation can NEVER leave the configured root**
### 📥 Download
## Usage
1. Edit `launcher.config.example.json` to set your `RootDirectory`, optional `PatchDirectory` and the `Hatari` settings, then copy it to `launcher.config.json` for local use. Relative paths are resolved against the EXE folder (build output).
2. **Windows:** Build via `build.cmd`.
3. **Linux:** Build via `build.sh` (run `chmod +x build.sh` first to make it executable).
4. **Always start using `start.cmd`.**
5. In the console, all subfolders and ZIPs in root (and recursively below) will be shown; other file types/hidden files are always ignored.
6. Complete navigation/control with arrow keys, Enter, Backspace, ESC, PgUp/PgDn, as described above.
7. **IMPORTANT:** Navigation/scroll/backspace:
- Backspace never escapes the root
- In root, Backspace has no effect
- Empty directories are reported (display stays stable)
8. **Overlay/patch logic:**
- If a ZIP/folder exists in both patch and main, always the patch version opens/launches
- All navigation is relative to root path—for consistent experience
Download the ZIP for your platform from the [Releases](https://github.com/anomalyco/marcer-gamedvd-launcher/releases) page:
## System Requirements
- **Windows:** .NET Desktop Runtime 10 or later, Hatari Emulator with configured CFG
- **Linux:** .NET Runtime 10 or later, Hatari Emulator with configured CFG (Wine/compatible version)
| Platform | Archive |
|---|---|
| Windows | `*-win-x64.zip` |
| Linux | `*-linux-x64.zip` |
| macOS | `*-osx-x64.zip` |
## Configuration
### 💻 System Requirements
The application reads settings from `launcher.config.json` (the local, user-specific file). A template `launcher.config.example.json` is shipped with the release — copy it to `launcher.config.json` and adjust the paths for your environment.
- **.NET Runtime 10** or later ([download](https://dotnet.microsoft.com/download/dotnet/10.0))
- **Hatari Emulator** with a working configuration file
- Windows: native Hatari
- Linux / macOS: Hatari via Wine or native build
Example `launcher.config.example.json`:
### ⚡ Quick Start
1. Extract the release ZIP to any folder.
2. Copy `launcher.config.example.json` to `launcher.config.json`.
3. Edit `launcher.config.json` — set your game directory and Hatari paths (see [Configuration](#%EF%B8%8F-configuration) below).
4. Run the launcher:
- **Windows:** Double-click `MarcerGameDvdLauncher.exe` or run from a terminal.
- **Linux:** `chmod +x MarcerGameDvdLauncher && ./MarcerGameDvdLauncher`
- **macOS:** `chmod +x MarcerGameDvdLauncher && ./MarcerGameDvdLauncher`
5. Browse and launch games with your keyboard.
### ⚙️ Configuration
The launcher reads `launcher.config.json` from the same directory as the executable. A template is included in the release — copy it and adjust:
```json
{
@@ -107,39 +73,148 @@ Example `launcher.config.example.json`:
}
```
Fields:
- RootDirectory: Absolute (or relative) path to the game root. Navigation must never leave this root directory.
- PatchDirectory: Optional overlay/patch directory (merged with the main root at runtime).
- Hatari.Executable: Full path to `hatari.exe`.
- Hatari.ConfigFile: Full path to the Hatari configuration file.
- Hatari.ArgsTemplate: Argument template used to start Hatari. Use `{cfg}` for the Hatari config file path and `{zip}` for the ZIP file to launch.
| Field | Required | Description |
|---|---|---|
| `RootDirectory` | ✅ | Game root folder. Navigation never leaves this directory. |
| `PatchDirectory` | ❌ | Optional overlay/patch directory merged at runtime. |
| `Hatari.Executable` | ✅ | Path to the Hatari executable. Validated at startup. |
| `Hatari.ConfigFile` | ✅ | Path to the Hatari configuration file. |
| `Hatari.ArgsTemplate` | ✅ | Argument template. Must contain `{zip}`, optionally `{cfg}`. |
Notes:
- Relative paths are resolved relative to the EXE directory (AppContext.BaseDirectory). This makes behavior consistent when running from the build output folder.
- Hatari.Executable is validated at startup: the file must exist and have an .exe extension. Relative paths for Hatari settings are resolved against the EXE folder.
- `Hatari.ArgsTemplate` must contain at least the `{zip}` placeholder. Example: `-c "{cfg}" --disk-a "{zip}"`.
- The program performs a straight string substitution of `{cfg}` and `{zip}`; it does not add additional quoting logic. Therefore include quotes around placeholders in the template if your paths contain spaces (as in the example).
- `launcher.config.example.json` is copied to the output directory by the csproj (`CopyToOutputDirectory=PreserveNewest`).
- After modifying `launcher.config.json`, restart the application for changes to take effect.
**Notes:**
- Relative paths are resolved relative to the executable's directory.
- Include quotes around `{cfg}` and `{zip}` in the template if your paths contain spaces.
- After editing `launcher.config.json`, restart the application.
## Release Workflow
### ⌨️ Controls
Releases are automated via GitHub Actions. When a tag matching `v*` is pushed, the workflow (`.github/workflows/release.yml`) automatically:
1. Builds platform-specific artifacts (Windows, Linux, macOS)
2. Generates release notes from git log
3. Creates a GitHub Release with all ZIPs attached
| Key | Action |
|---|---|
| `↑` / `↓` | Move selection |
| `Enter` / `→` | Open folder or launch ZIP |
| `Backspace` / `←` | Go up one directory level |
| `PageUp` / `PageDown` | Jump one page |
| `*` | Toggle favorite on selected ZIP |
| `?` | Show help overlay |
| `ESC` / `Q` | Exit |
**Navigation rules:**
- Backspace in the root directory has no effect — you can never leave it.
- Empty directories are displayed correctly.
- When a ZIP or folder exists in both layers, the patch version is always launched/opened.
### 🔄 Keeping Your Patch Directory Updated
The community uses [ftp-sync](https://github.com/slippyex/ftp-sync) to keep the patch directory in sync with Marcer's FTP server. This downloads only changed or new files — fast and bandwidth-friendly.
**Setup:**
1. Clone and install ftp-sync:
```bash
git clone https://github.com/slippyex/ftp-sync.git
cd ftp-sync
npm install
```
2. Create a `config.json` with your paths and the FTP credentials from the community:
```json
{
"ftpConfig": {
"host": "<ftp-host>",
"user": "<username>",
"password": "<password>",
"port": 2121
},
"localDir": "C:\\Games\\MarcersGameDVD\\",
"remoteDir": "/GameDVD",
"patchDir": "C:\\Games\\MarcersGameDVD-Patch\\"
}
```
> 💡 Ask in the [Facebook group](https://www.facebook.com/groups/360493904888475/) for the current FTP credentials.
3. Run the sync:
```bash
npm run sync config.json
```
Press `s` to start syncing. Press `q` to exit when done.
4. Point the launcher's `PatchDirectory` in `launcher.config.json` to the `patchDir` from your ftp-sync config.
---
## 🛠️ Developers
### 📋 Prerequisites
- [.NET SDK 10](https://dotnet.microsoft.com/download/dotnet/10.0)
- Windows: `build.cmd` / `start.cmd`
- Linux / macOS: `build.sh` / `start.sh` (run `chmod +x scripts/*.sh` first)
### 🔨 Build & Run
**Windows:**
```bat
scripts\build.cmd
scripts\start.cmd
```
**Linux / macOS:**
```bash
scripts/build.sh
scripts/start.sh
```
> ⚠️ Do **not** use `dotnet build` or `dotnet run` directly — always use the platform build script to ensure consistent output.
### 🧪 Demo Mode
`demo.sh` creates a complete test environment with fake ZIPs and two layers (root + patch), then launches the launcher:
```bash
scripts/demo.sh
```
This builds the project, generates a structured `.demo/` directory with sample folders and ZIPs, writes a matching `launcher.config.json`, and starts the launcher. Useful for quickly testing overlay behavior and navigation without setting up real game files.
> ⚠️ Windows is not supported for `demo.sh`. Use `start.cmd` with your own game files instead.
### 🚀 Release Process
Releases are automated via GitHub Actions (`.github/workflows/release.yml`).
**To create a release:**
1. Ensure `README.md` and `agents.md` are up to date.
1. Ensure `README.md` and `AGENTS.md` are up to date.
2. Commit all changes.
3. Create and push a tag: `git tag v{version} && git push origin v{version}`.
4. The GitHub Action handles the rest.
3. Tag and push:
```bash
git tag v1.2.3
git push origin v1.2.3
```
4. The workflow builds platform ZIPs, generates release notes, and creates a GitHub Release.
**Local builds** (for development/testing):
- **Windows:** `build.cmd`
- **Linux/macOS:** `build.sh` (run `chmod +x build.sh` first)
---
## Notes
- Full requirements, features and build rules are always up to date in `agents.md`.
- After every code or feature change and every release, README.md and agents.md must be reviewed and kept up to date.
- For every release, release notes **must** be present listing all changes and bugfixes; this is required by agents.md!
## 🗺️ Roadmap
| Version | Feature |
|---|---|
| 2.0 | ZIP database & metadata extraction |
| 2.0 | Quicksearch / filter over ZIPs |
| 2.0 | History of recently launched games |
| 3.0 | Overlay hot-swap at runtime |
**Not planned:** Configurable keybindings, screenshot/cover display, sound/music integration, persistent UI settings.
---
## 📝 Notes
- Full technical requirements and build rules are in [`AGENTS.md`](AGENTS.md).
- After any functional change, both `README.md` and `AGENTS.md` must be updated.
- Every release **must** include release notes listing all changes and bugfixes.
---
## License
Licensed under the [MIT License](LICENSE) - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
# === Build script for MarcerGameDvdLauncher (requires .NET SDK 6 or newer) ===
echo "Building MarcerGameDvdLauncher..."
if ! command -v dotnet &> /dev/null; then
echo "[ERROR] .NET SDK not found. Please install from https://dotnet.microsoft.com/download"
exit 1
fi
# Build in current directory (where build.sh is located)
cd "$(dirname "$0")"
# Change to MarcerGameDvdLauncher subdirectory
cd "MarcerGameDvdLauncher"
dotnet build -c Release
if [ $? -ne 0 ]; then
echo "[ERROR] Build failed!"
exit 2
fi
# Check for the built .exe file
EXEPATH=$(find "bin/Release" -name "MarcerGameDvdLauncher*.exe" -print -quit 2>/dev/null)
if [ -n "$EXEPATH" ] && [ -f "$EXEPATH" ]; then
echo "[OK] Build complete. EXE: \"$EXEPATH\""
else
echo "[WARNING] Build appears successful but .exe not found!"
fi
Executable
+192
View File
@@ -0,0 +1,192 @@
#!/bin/bash
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
# Licensed under the MIT License. See LICENSE file in project root for details.
# === Demo setup and run script for MarcerGameDvdLauncher ===
# Creates a structured test directory with main and patch layers,
# populates them with fake ZIPs, then launches the launcher.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DEMO_DIR="$SCRIPT_DIR/.demo"
ROOT_DIR="$DEMO_DIR/root"
PATCH_DIR="$DEMO_DIR/patch"
CONFIG_FILE="$SCRIPT_DIR/launcher.config.json"
# --- Step 1: Build ---
echo "=== Building MarcerGameDvdLauncher ==="
cd "$SCRIPT_DIR/src/MarcerGameDvdLauncher"
dotnet build -c Release --verbosity quiet
echo "[OK] Build successful."
# --- Step 2: Create demo directory structure ---
echo ""
echo "=== Setting up demo directories ==="
# Clean previous demo if it exists
rm -rf "$DEMO_DIR"
mkdir -p "$ROOT_DIR" "$PATCH_DIR"
# Helper: create a fake ZIP (just an empty file with .zip extension)
fake_zip() {
touch "$1"
}
# --- ROOT layer (main DVD content) ---
echo "Creating root layer..."
# === Folder A: TEST-ME — Atari Classics (shared with patch — rich mix + subdirs) ===
mkdir -p "$ROOT_DIR/A/TEST-ME Atari Classics"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Pac-Man.zip"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Donkey Kong.zip"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Galaga.zip"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Space Invaders.zip"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Frogger.zip"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Bomberman.zip"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Tetris.zip"
# Subdirs: Original and Manual
mkdir -p "$ROOT_DIR/A/TEST-ME Atari Classics/Original"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Original/Pac-Man (Original).zip"
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Original/Donkey Kong (Original).zip"
mkdir -p "$ROOT_DIR/A/TEST-ME Atari Classics/Manual"
touch "$ROOT_DIR/A/TEST-ME Atari Classics/Manual/Pac-Man.pdf" # PDF — should NOT appear
touch "$ROOT_DIR/A/TEST-ME Atari Classics/Manual/Donkey Kong.pdf" # PDF — should NOT appear
# === Folder B: Platformer (root only) ===
mkdir -p "$ROOT_DIR/B/Platformer"
fake_zip "$ROOT_DIR/B/Platformer/Super Mario Bros.zip"
fake_zip "$ROOT_DIR/B/Platformer/Sonic the Hedgehog.zip"
fake_zip "$ROOT_DIR/B/Platformer/Mega Man.zip"
fake_zip "$ROOT_DIR/B/Platformer/Castlevania.zip"
# === Folder C: Puzzle (root only) ===
mkdir -p "$ROOT_DIR/C/Puzzle"
fake_zip "$ROOT_DIR/C/Puzzle/Columns.zip"
fake_zip "$ROOT_DIR/C/Puzzle/Puyo Puyo.zip"
fake_zip "$ROOT_DIR/C/Puzzle/Klax.zip"
# === Folder D: Shoot'em'up (shared with patch — rich mix) ===
mkdir -p "$ROOT_DIR/D/Shoot'em'up"
fake_zip "$ROOT_DIR/D/Shoot'em'up/R-Type.zip"
fake_zip "$ROOT_DIR/D/Shoot'em'up/Gradius.zip"
fake_zip "$ROOT_DIR/D/Shoot'em'up/1942.zip"
fake_zip "$ROOT_DIR/D/Shoot'em'up/Defender.zip"
fake_zip "$ROOT_DIR/D/Shoot'em'up/Centipede.zip"
fake_zip "$ROOT_DIR/D/Shoot'em'up/Galaxian.zip"
# === Folder E: Racing (root only) ===
mkdir -p "$ROOT_DIR/E/Racing"
fake_zip "$ROOT_DIR/E/Racing/Pole Position.zip"
fake_zip "$ROOT_DIR/E/Racing/Out Run.zip"
fake_zip "$ROOT_DIR/E/Racing/Daytona USA.zip"
# --- PATCH layer (overlay/additions) ---
echo "Creating patch layer..."
# === Folder A: TEST-ME — Atari Classics — overrides + new games ===
mkdir -p "$PATCH_DIR/A/TEST-ME Atari Classics"
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Pac-Man.zip" # [BOTH] override
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Donkey Kong.zip" # [BOTH] override
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Pac-Man Championship.zip" # [PTCH] new
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Donkey Kong Jr.zip" # [PTCH] new
# Subdir Original in patch — adds one more
mkdir -p "$PATCH_DIR/A/TEST-ME Atari Classics/Original"
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Original/Galaga (Original).zip" # [PTCH] in subdir
# === Folder D: Shoot'em'up — overrides + new games ===
mkdir -p "$PATCH_DIR/D/Shoot'em'up"
fake_zip "$PATCH_DIR/D/Shoot'em'up/R-Type.zip" # [BOTH] override
fake_zip "$PATCH_DIR/D/Shoot'em'up/R-Type II.zip" # [PTCH] new
fake_zip "$PATCH_DIR/D/Shoot'em'up/Salamander.zip" # [PTCH] new
# === Folder F: Patch-only folder (not in root) ===
mkdir -p "$PATCH_DIR/F/Hack & Translation"
fake_zip "$PATCH_DIR/F/Hack & Translation/Pac-Man MSX.zip"
fake_zip "$PATCH_DIR/F/Hack & Translation/Donkey Kong Remix.zip"
fake_zip "$PATCH_DIR/F/Hack & Translation/Galaga Special.zip"
echo "[OK] Demo structure created."
echo ""
echo " ROOT (DVD) PATCH (Overlay)"
echo " ────────── ───────────────"
echo " A/TEST-ME Atari Classics/ A/TEST-ME Atari Classics/"
echo " ├── Pac-Man.zip [BOTH] ├── Pac-Man.zip"
echo " ├── Donkey Kong.zip [BOTH] ├── Donkey Kong.zip"
echo " ├── Galaga.zip [ROOT] ├── Pac-Man Championship.zip [PTCH]"
echo " ├── Space Invaders.zip [ROOT] ├── Donkey Kong Jr.zip [PTCH]"
echo " ├── Frogger.zip [ROOT] │"
echo " ├── Bomberman.zip [ROOT] └── Original/"
echo " ├── Tetris.zip [ROOT] └── Galaga (Original).zip [PTCH]"
echo " ├── Original/"
echo " │ ├── Pac-Man (Original).zip [ROOT]"
echo " │ └── Donkey Kong (Original).zip [ROOT]"
echo " └── Manual/ (PDFs — should NOT appear)"
echo " ├── Pac-Man.pdf"
echo " └── Donkey Kong.pdf"
echo " B/Platformer/ (no patch)"
echo " ├── Super Mario Bros.zip [ROOT]"
echo " ├── Sonic.zip [ROOT]"
echo " ├── Mega Man.zip [ROOT]"
echo " └── Castlevania.zip [ROOT]"
echo " C/Puzzle/ (no patch)"
echo " ├── Columns.zip [ROOT]"
echo " ├── Puyo Puyo.zip [ROOT]"
echo " └── Klax.zip [ROOT]"
echo " D/Shoot'em'up/ D/Shoot'em'up/"
echo " ├── R-Type.zip [BOTH] ├── R-Type.zip"
echo " ├── Gradius.zip [ROOT] ├── R-Type II.zip [PTCH]"
echo " ├── 1942.zip [ROOT] └── Salamander.zip [PTCH]"
echo " ├── Defender.zip [ROOT]"
echo " ├── Centipede.zip [ROOT]"
echo " └── Galaxian.zip [ROOT]"
echo " E/Racing/ (no patch)"
echo " ├── Pole Position.zip [ROOT]"
echo " ├── Out Run.zip [ROOT]"
echo " └── Daytona USA.zip [ROOT]"
echo " F/Hack & Translation/ [PTCH]"
echo " ├── Pac-Man MSX.zip"
echo " ├── DK Remix.zip"
echo " └── Galaga Special.zip"
echo ""
# --- Step 3: Create launcher.config.json ---
echo "=== Writing launcher.config.json ==="
# Create a fake Hatari executable for demo (Linux: shell script with .exe extension)
HATARI_FAKE="$DEMO_DIR/hatari.exe"
cat > "$HATARI_FAKE" <<'HATEXEC'
#!/bin/bash
echo "[DEMO] Hatari would launch with: $@"
HATEXEC
chmod +x "$HATARI_FAKE"
cat > "$CONFIG_FILE" <<EOF
{
"RootDirectory": "$ROOT_DIR",
"PatchDirectory": "$PATCH_DIR",
"Hatari": {
"Executable": "$HATARI_FAKE",
"ConfigFile": "",
"ArgsTemplate": "{zip}"
}
}
EOF
echo "[OK] Config written to $CONFIG_FILE"
# Copy config to EXE output directory (app looks for it there)
EXE_DIR="$SCRIPT_DIR/src/MarcerGameDvdLauncher/bin/Release/net10.0"
cp "$CONFIG_FILE" "$EXE_DIR/launcher.config.json"
echo "[OK] Config copied to $EXE_DIR"
# --- Step 4: Launch the application ---
echo ""
echo "=== Launching MarcerGameDvdLauncher ==="
echo "Controls: Arrow keys, Enter, Backspace, ESC to exit"
echo ""
cd "$SCRIPT_DIR/src/MarcerGameDvdLauncher"
dotnet run -c Release
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"schedule": ["before 6am on Monday"],
"packageRules": [
{
"matchManagers": ["nuget"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "nuget minor/patch",
"automerge": true
},
{
"matchManagers": ["github-actions"],
"groupName": "GitHub Actions",
"automerge": true
},
{
"matchManagers": ["dockerfile"],
"groupName": "Docker",
"automerge": false
},
{
"matchManagers": ["docker-compose"],
"groupName": "Docker Compose",
"automerge": false
},
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"labels": ["major-update"],
"automerge": false
}
]
}
+4 -2
View File
@@ -1,3 +1,5 @@
REM Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
REM Licensed under the MIT License. See LICENSE file in project root for details.
@echo off
REM === Build script for MarcerGameDvdLauncher (requires .NET SDK 6 or newer) ===
@@ -8,9 +10,9 @@ if errorlevel 1 (
exit /b 1
)
REM Im aktuellen Ordner (wo build.cmd liegt) bauen
REM Im src-Ordner bauen (build.cmd liegt in scripts/)
cd /d %~dp0
cd MarcerGameDvdLauncher
cd ..\src\MarcerGameDvdLauncher
dotnet build -c Release
if errorlevel 1 (
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
# Licensed under the MIT License. See LICENSE file in project root for details.
# === Build script for MarcerGameDvdLauncher (requires .NET SDK 6 or newer) ===
echo "Building MarcerGameDvdLauncher..."
if ! command -v dotnet &> /dev/null; then
echo "[ERROR] .NET SDK not found. Please install from https://dotnet.microsoft.com/download"
exit 1
fi
# Build in MarcerGameDvdLauncher subdirectory (script is in scripts/, code in src/)
cd "$(dirname "$0")"
cd "../src/MarcerGameDvdLauncher"
dotnet build -c Release
if [ $? -ne 0 ]; then
echo "[ERROR] Build failed!"
exit 2
fi
# Check for the built binary
BINPATH=$(find "bin/Release" \( -name "MarcerGameDvdLauncher" -o -name "MarcerGameDvdLauncher.exe" \) -print -quit 2>/dev/null)
if [ -n "$BINPATH" ] && [ -f "$BINPATH" ]; then
echo "[OK] Build complete. Binary: \"$BINPATH\""
else
echo "[WARNING] Build appears successful but binary not found!"
fi
+16
View File
@@ -0,0 +1,16 @@
REM Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
REM Licensed under the MIT License. See LICENSE file in project root for details.
@echo off
REM Starts MarcerGameDvdLauncher.exe (script is in scripts/, code in src/)
setlocal
set EXE_PATH=%~dp0..\src\MarcerGameDvdLauncher\bin\Release\net10.0\MarcerGameDvdLauncher.exe
if not exist "%EXE_PATH%" (
echo [ERROR] Application not built. Please run build.cmd first.
exit /b 1
)
pushd "%~dp0..\src\MarcerGameDvdLauncher\bin\Release\net10.0"
"MarcerGameDvdLauncher.exe"
popd
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
# Licensed under the MIT License. See LICENSE file in project root for details.
# Starts MarcerGameDvdLauncher (script is in scripts/, code in src/)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
EXE_PATH="$SCRIPT_DIR/../src/MarcerGameDvdLauncher/bin/Release/net10.0/MarcerGameDvdLauncher"
if [ ! -f "$EXE_PATH" ]; then
echo "[ERROR] Application not built. Please run build.sh first."
exit 1
fi
cd "$SCRIPT_DIR/../src/MarcerGameDvdLauncher/bin/Release/net10.0"
./MarcerGameDvdLauncher
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
// Configuration POCOs separated into their own file for clarity
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
// Manages loading, saving and querying favorite ZIP paths.
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
public class HatariLauncher
@@ -11,11 +14,9 @@ namespace MarcerGameDvdLauncher
if (string.IsNullOrWhiteSpace(exePath))
throw new ArgumentNullException(nameof(exePath));
// Defensive validation: ensure the executable exists and looks like an .exe
// Defensive validation: ensure the executable exists
if (!File.Exists(exePath))
throw new ArgumentException($"Hatari executable not found: {exePath}", nameof(exePath));
if (!string.Equals(Path.GetExtension(exePath), ".exe", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException($"Hatari executable must be an .exe file: {exePath}", nameof(exePath));
_exePath = exePath;
_cfgPath = cfgPath;
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
// Encapsulates application lifecycle: load config, initialize components, run navigation
@@ -142,6 +145,15 @@ namespace MarcerGameDvdLauncher
}
var key = Console.ReadKey(intercept: true);
if (key.KeyChar == '?')
{
_menuRenderer.ShowHelpBox(currentAvailableLines);
Console.ReadKey(intercept: true);
_menuRenderer.InvalidateCache();
_menuRenderer.DrawMenu(_gameEntries, _navigationController.ScrollOffset, _navigationController.SelectedIndex, currentAvailableLines, isFav);
ProgramHelpers.FlushInputBuffer();
continue;
}
switch (key.Key)
{
case ConsoleKey.UpArrow:
@@ -174,6 +186,7 @@ namespace MarcerGameDvdLauncher
ProgramHelpers.FlushInputBuffer();
break;
case ConsoleKey.Enter:
case ConsoleKey.RightArrow:
var oldRelativePath = _navigationController.CurrentRelativePath;
var isDirectory = _gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Directory;
_navigationController.HandleEnter(_gameEntries);
@@ -199,11 +212,11 @@ namespace MarcerGameDvdLauncher
ProgramHelpers.FlushInputBuffer();
break;
case ConsoleKey.Backspace:
case ConsoleKey.LeftArrow:
_navigationController.GoUpDirectory();
ReloadGameEntries();
_navigationController.UpdateScrollOffset(_gameEntries.Count, currentAvailableLines);
_menuRenderer.DrawMenu(_gameEntries, _navigationController.ScrollOffset, _navigationController.SelectedIndex, currentAvailableLines, isFav);
// flush input to avoid leftover key events after directory change
ProgramHelpers.FlushInputBuffer();
break;
case ConsoleKey.PageDown:
@@ -237,6 +250,7 @@ namespace MarcerGameDvdLauncher
ProgramHelpers.FlushInputBuffer();
break;
case ConsoleKey.Escape:
case ConsoleKey.Q:
exitRequested = true;
break;
}
@@ -14,6 +14,10 @@
<Content Include="launcher.config.example.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="../../LICENSE">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>LICENSE</Link>
</Content>
</ItemGroup>
</Project>
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
public class MenuRenderer
@@ -139,6 +142,93 @@ namespace MarcerGameDvdLauncher
}
}
// Invalidates the internal line cache so the next DrawMenu call
// performs a full redraw of every line. Useful after an overlay
// (e.g. help box) has overwritten the console directly.
public void InvalidateCache()
{
for (int i = 0; i < _cachedBuffer.Length; i++)
_cachedBuffer[i].Text = null!;
}
// Renders a centered, bordered help box with key bindings inside the
// available console area. The caller is responsible for waiting on a
// key and redrawing the menu afterwards.
public void ShowHelpBox(int availableLines)
{
try
{
int width = Console.WindowWidth;
string[] helpLines = GetHelpLines();
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.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
{
}
}
private static string[] GetHelpLines()
{
return [
" Help — Key Bindings",
" ",
" ↑ / ↓ Move selection up / down",
" Enter / → Open folder / launch ZIP with Hatari",
" ← / BS Go up one directory (never exceeds root)",
" ESC / Q Exit the program",
" PgUp Jump one page up",
" PgDn Jump one page down",
" * Toggle favorite on selected ZIP",
" ? Show this help",
" ",
" Navigation is strictly limited to RootDirectory.",
" The overlay shows both root and patch layers combined.",
" ",
" Press any key to continue...",
];
}
private static string PadToWidth(string text, int width)
{
if (text.Length > width) return text.Substring(0, width);
return text + new string(' ', width - text.Length);
}
// Returns foreground and background colors for an entry depending on selection state
private (ConsoleColor fg, ConsoleColor bg) GetColors(GameEntry e, bool selected)
{
@@ -156,18 +246,8 @@ namespace MarcerGameDvdLauncher
private string BuildLineText(GameEntry e, int width, bool isFavorite)
{
if (width <= 0) return string.Empty;
// Reserve 6 characters for the label area. For directories we show "[DIR] ",
// for ZIPs we use the same width and optionally show a leading '*' when favorited.
string label;
if (e.Kind == EntryKind.Directory)
{
label = "[DIR] ";
}
else
{
// For ZIPs, show '*' after 4 spaces when favorited (keeps 6-char label area).
label = isFavorite ? " * " : new string(' ', 6);
}
string label = GetLabel(e, isFavorite);
// If the console width is smaller than the label, truncate the label
if (width <= label.Length)
@@ -197,21 +277,49 @@ namespace MarcerGameDvdLauncher
return result;
}
// Returns the left label for an entry based on kind, layer status and favorite state.
// Format: [LayerLabel][TypeIndicator] where
// LayerLabel = [BOTH] / [ROOT] / [PTCH] (7 chars)
// TypeIndicator = [DIR] for dirs, " * " or " " for ZIPs (6 chars)
private static string GetLabel(GameEntry e, bool isFavorite)
{
// Layer label (7 chars)
string layer;
if (e.InRoot && e.InPatch) layer = "[BOTH] ";
else if (e.InPatch) layer = "[PTCH] ";
else if (e.InRoot) layer = "[ROOT] ";
else layer = " ";
// Type indicator (6 chars)
string type;
if (e.Kind == EntryKind.Directory)
{
type = "[DIR] ";
}
else
{
type = isFavorite ? " * " : " ";
}
return layer + type; // 13 chars total
}
private ConsoleColor GetColorForEntry(GameEntry e)
{
// Virtual entries (like the Favorites pseudo-folder) should be white
if (e.IsVirtual) return ConsoleColor.White;
if (e.Kind == EntryKind.Directory)
{
if (e.InRoot && e.InPatch) return ConsoleColor.Yellow; // Both layers
if (e.InPatch && !e.InRoot) return ConsoleColor.DarkYellow; // Only patch
if (e.InRoot && !e.InPatch) return ConsoleColor.Gray; // Only root
if (e.InRoot && e.InPatch) return ConsoleColor.Yellow; // Both layers
if (e.InPatch && !e.InRoot) return ConsoleColor.DarkYellow; // Only patch
if (e.InRoot && !e.InPatch) return ConsoleColor.Gray; // Only root
}
else if (e.Kind == EntryKind.Zip)
{
if (e.InRoot && e.InPatch) return ConsoleColor.Green;
if (e.InRoot && !e.InPatch) return ConsoleColor.DarkGreen;
if (e.InPatch && !e.InRoot) return ConsoleColor.Magenta;
if (e.InRoot && e.InPatch) return ConsoleColor.Green; // Both layers
if (e.InRoot && !e.InPatch) return ConsoleColor.DarkGreen; // Only root
if (e.InPatch && !e.InRoot) return ConsoleColor.Magenta; // Only patch
}
return ConsoleColor.DarkGray;
}
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
public class NavigationController
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
public enum EntryKind { Directory, Zip }
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher
{
class Program
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
using System;
using System.Runtime.InteropServices;
@@ -1,3 +1,6 @@
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
// Licensed under the MIT License. See LICENSE file in project root for details.
namespace MarcerGameDvdLauncher;
/// <summary>
-14
View File
@@ -1,14 +0,0 @@
@echo off
REM Starts MarcerGameDvdLauncher.exe from the correct folder
setlocal
set EXE_PATH=%~dp0MarcerGameDvdLauncher\bin\Release\net10.0\MarcerGameDvdLauncher.exe
if not exist "%EXE_PATH%" (
echo [ERROR] Application not built. Please run build.cmd first.
exit /b 1
)
pushd "MarcerGameDvdLauncher\bin\Release\net10.0"
"MarcerGameDvdLauncher.exe"
popd