launcher: interrupted apply recovers cd-active.new (completion marker) or cd-active.old, config.json merged field by field on save across windows, wine version check with warning below 5.0, mirror worlds as check boxes (new multi option type, old values still load), copy progress in mb in the wizard, saves table fits 1280x720

This commit is contained in:
MineTec committed 2026-11-07 11:57:22 +01:00
1 parent 526f6072f6
commit a6708e5fff
28 files changed
+843 -112

No files matched your search

+24 -3
View File
@@ -56,6 +56,7 @@ different folder chosen in the wizard is remembered via `java.util.prefs`.
| Path | Content |
|---|---|
| `config.json` | settings, enabled/active mods, mod option values (`modOptions`), CD fingerprint |
| `.config.lock`, `.apply.lock` | file locks: config merge-on-save, apply/restore |
| `original/hd`, `original/cd` | pristine copy of `Setup/<lang>/Hd` and `…/Cd` from the ISO |
| `game/` | copy of the HD part the game runs from: **patched** `groove.exe`, DLLs, music, save games |
| `cd-active/` | what the game sees as its CD: hard links to originals + mod outputs |
@@ -66,6 +67,13 @@ different folder chosen in the wizard is remembered via `java.util.prefs`.
| `wine/` | own Wine prefix (Linux) |
| `logs/` | game.log, wineboot.log, registry.log |
**Config merge-on-save**: `Config.save` takes a file lock on `.config.lock` (plus a JVM-wide
monitor), re-reads config.json and three-way merges field by field against the values this
instance last loaded/saved: fields it did not change keep what another window/CLI/editor wrote
meanwhile (mod options merge per mod and key; unknown keys of newer versions are kept); a field
changed on both sides = last writer wins. The object then holds the merged values. A `new
Config()` that never touched a file overwrites everything (installer, tests).
## First start
The wizard takes an ISO file, an extracted folder or a mounted CD. ISO 9660 is read in pure Java
@@ -125,7 +133,10 @@ Windows the launcher refuses other paths with a message (choose a folder like `C
Wine cannot create win32 prefixes; system.reg only appears after `wineserver -w`).
`WINEDLLOVERRIDES=winemenubuilder.exe=d;mscoree=d;mshtml=d` (no menu entries, no Mono/Gecko
prompts). Optional virtual desktop: `wine explorer /desktop=LlamaMod,WxH Z:\…\groove.exe`.
Wine missing → clear error message. Running instances are detected via `/proc/*/environ`
Wine missing → clear error message. `wine --version` (cached per command) is shown in Settings;
below `GameLauncher.MIN_WINE` = 5.0 the line turns red and Play warns once per session ("Play
anyway"); unparsable output never warns or blocks. Only Wine 6.0.3 is actually tested; 5.0 is a
guess at the oldest usable version. Running instances are detected via `/proc/*/environ`
(`WINEPREFIX=<our prefix>`), so two games never share the prefix.
## Mods
@@ -140,7 +151,13 @@ SHA-256(CD fingerprint (names and sizes) | app version+build id | `ExePatcher.RE
skips the mod, and an app update re-runs every mod once (a changed mod without a `version()` bump
never serves stale output). Manifests store `"app"`; after each apply, cache entries of other app
builds (including all of 0.1.0, which have no `"app"`) are deleted. Then `cd-active` is rebuilt in
`cd-active.new` and swapped in (hard link → symlink (not Windows) → copy). Applying (and restoring)
`cd-active.new` and swapped in (hard link → symlink (not Windows) → copy). Once `cd-active.new` is
complete, `cd-active.new.complete` (content: the activeKey of that build) is written; it is removed
after the swap. If the app is killed between the two renames, `ModManager.recoverInterruptedApply()`
(GUI start, Play, CLI `--launch`) moves `cd-active.new` in when its marker matches config.json's
activeKey (exe and config are only written after the swap, so they still describe the previous
selection), otherwise moves `cd-active.old` back; without either nothing is touched (Play then
restores the original as before). Applying (and restoring)
is refused while the game runs (`GameLauncher.isRunning()`) and holds an exclusive lock on
`<data>/.apply.lock` (a second window/CLI/editor gets "Another LlamaMod window or command is applying
mods"); every run is logged to `logs/apply.log`.
@@ -163,7 +180,11 @@ above, collapsible headers with "n of m on") and sorts them by name inside a gro
**Options** (`mods/ModOption`, record `(key, label, description, type, defaultValue, min, max,
choices)`): factories `ModOption.bool(key, label, desc, def)`, `integer(key, label, desc, def, min,
max)`, `string(…, def)`, `choice(key, label, desc, def, ModOption.of(value, label)…)`, `seed(key,
label, desc, def)` (empty default = a random seed is chosen on first use and saved). In `apply`:
label, desc, def)` (empty default = a random seed is chosen on first use and saved), `multi(key, label,
desc, def, ModOption.of(value, label)…)` (any subset; value `all`, `none` or the chosen values
comma-separated in choice order, e.g. `1,3`; empty reads as `all`, so a former STRING option holding
"all"/"1,3" keeps its value and cache key; `o.selected(value)` → list of chosen values; UI: a grid of
check boxes; first user: mirror `worlds`). In `apply`:
`ctx.option(key)` (normalized string), `ctx.optionBool`, `ctx.optionInt`, `ctx.optionSeed` (long:
numeric text as is, other text hashed). Values live in config.json (`modOptions.<id>.<key>`, only
user-set values and generated seeds); invalid stored values fall back to the default. The UI shows