documentation for level builders: docs index, step-by-step guide, file format reference (wad, geometry, objects, textures, audio, testlevel chapters, engine limits), strat vm and script language with the arena as worked example, tools overview, developer notes; readme rewritten as project front page with screenshots; review notes moved to notes/dev

This commit is contained in:
MineTec committed 2026-11-07 16:14:40 +01:00
1 parent 00b2880d67
commit 624718b893
41 files changed
+2175 -259

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
# Level editor – bug review (2026-11-07, agent "edbugs")
Scope: `editor.*`, `format.stpcmerge`, `levelbuild`, `mods.levels`, `mods.boottolevel`. Method: code review of
undo/history, project IO, build, play-test; property/fuzz tests; stress run of the editor on Xvfb.
## New tests
| Test | What it does |
|---|---|
| `editor/bugs/EditFuzzTest` | 12 seeds (env `LLAMAMOD_FUZZ_SEEDS`, 80 passed) × 150 random steps (move with/without merge key, place object, add brush, duplicate, delete, set random props, undo, redo) on edit/new projects of t1l1m001/t1l2m001 → save → load: same scene (every persistent prop, list order), save bytes identical, build identical and parseable (`Level.parse`); undo all → original WAD bytes; redo → same scene/project again |
| `editor/bugs/BrushFuzzTest` | 260 brushes of every kind with random/absurd parameters (as typed into the inspector): compile < 20 s, no exception, every generated model ≤ 8000 vertices, build OK or validator error |
| `editor/bugs/CorruptProjectTest` | every level.json value replaced by values of other types, truncated zips, flipped bytes: load gives IOException or a project that builds/validates and saves |
| `editor/bugs/CrossImportEdgeTest` | cross-level import into projects that already have own textures (used by a brush and a painted original face): indices shift correctly, save/load/build identical, edits after the import |
| `editor/bugs/EditorRegressionTest`, `editor/ui/DocumentRegressionTest`, `editor/tools/playtest/PlaytestSessionTest` | one regression test per fixed bug (all fail or run out of memory on the code before the fixes) |
| `editor/ui/EditorStress` (main, not a unit test) | on an X display: opens 9 damaged/missing project files, 400 rapid random edits with undo/redo/autosave/duplicate/delete, build + switch level / close level while building, undo all → original WAD, crash with autosave → recover. `java -cp all.jar:build/classes/java/test -Dllamamod.data=DIR -Dllamamod.editor.cd=CD com.eliasmueller.llamamod.editor.ui.EditorStress OUT [corrupt,village,spam,switch,undoall,crash,recover,nocd]` |
## Fixed bugs
| # | Severity | Bug / repro | Fix | Test |
|---|---|---|---|---|
| B1 | high | A box/platform/ramp/stairs/pillar/cylinder/tree/quad touching x=0 or z=0 (e.g. box 0..1) failed the **whole build** ("geometry outside the 96x96 grid: cell -1,-1") and made **all brushes vanish** from the live preview: wall collision is also registered in neighbour cells (margin 0.3), which do not exist outside the grid | `LevelBuilder.geometry`: skip margin-only collision cells outside the grid (real out-of-grid geometry still errors; goldens unchanged) | `brushAtGridEdgeBuilds` |
| B2 | low | The out-of-grid build error did not say which brush | `BrushCompiler.culprits`: error names the brushes near that cell ("– move brush:n3 into the grid") | `outsideGeometryNamesTheBrush` |
| B3 | high (data loss) | Recovering an autosave of a project that had a file: the document was **not dirty** (`dirty()` ignored `fresh` when a file was set) → closing/opening another level did not ask and `close()` deleted the autosave: recovered work lost | `Document.dirty()`: `history.isDirty() \|\| fresh \|\| file == null && canUndo()` | `recoveredDocumentWithFileIsDirty`, EditorStress crash/recover (dirty=true) |
| B4 | medium | After a cross-level import (`Importer` clears the history) the document looked saved after one Ctrl+Z (`History.clear()` marked the cleared state as saved; undoing the "Import … (permanent)" marker emptied the stack) → close without prompt lost the import | `History.clear()` keeps the dirty state (sentinel "saved state unreachable") | `clearKeepsDirtyState`, `CrossImportEdgeTest` |
| B5 | low | After more than 500 steps (undo limit), undoing everything showed the level as saved although the dropped steps were still applied | `History.push`: dropping the bottom step makes an empty-stack saved state unreachable | `undoLimitKeepsDirtyState` |
| B6 | high | Absurd brush values from the inspector froze or killed the editor (live preview compiles on the EDT): step ≤ 0 → endless loop in `LevelBuilder.cuts`; step 1e-9 / postsEvery 0 / steps 10⁶ / size 10⁶ / coordinates 10¹² → OutOfMemoryError; NaN, `rotation`/`tint` of the wrong length → exceptions that dropped **all** brushes from preview and build | `BrushCompiler.paramProblem` + `extentProblem` (non-numbers, step 0.125..64, ≤ 200 000 pieces, post spacing ≥ 0.1, steps 1..256, sizes 0..256, array lengths, > 64 cells outside the grid, \|y\| > 4096): brush skipped with a warning; `Validator` reports them as errors with the brush id (extent margin 8); any other runtime error of one brush only skips that brush | `absurdBrushParametersAreRejected`, `BrushFuzzTest` |
| B7 | low | Texture `#-1` (negative index) crashed the whole brush compile (IndexOutOfBounds outside the per-brush handling) | `LevelBuilder.tex`: negative index → IllegalArgumentException (brush skipped, warning) | `absurdBrushParametersAreRejected` |
| B8 | medium | A damaged project (wrong value types, unknown brushKind, broken references) threw ClassCastException/NPE/IAE instead of IOException; a huge brush value in a file ran the loader out of memory (B6) | `ProjectIO.read`/`info` wrap runtime errors: "the project file is damaged: …" | `damagedProjectGivesIoException`, `CorruptProjectTest` |
| B9 | medium (data loss) | Play-test of an unsaved level (or of a project saved outside the levels folder) wrote `<data>/levels/<slug>.llamalevel` and **silently overwrote a different project** with that name | `PlayTest.installTarget`: reuse the file only if it is the document's own file or the same project (kind, base, name); else `<stem>-2…` (used by both Play implementations) | `playTestDoesNotOverwriteOtherProjects` |
| B10 | medium | Boot-to-level play-test: the restore of the user's mod selection runs in a daemon thread of the editor; closing the (standalone) editor while the game runs left the boot mod (quitOnEscape, no menus) applied | `PlaytestRunner`: pending restore written to `<data>/editor/playtest-session.json`; deleted after the restore; on the next editor start (`onOpen` hook) a left-over session is restored (after the game has exited) | `PlaytestSessionTest` |
| B11 | medium | A build result arriving after the level was closed/replaced (Close or Open while building) was shown for the new level (problems linked to the wrong nodes by id) and – for Play – went on to install and **launch the closed level** | `PlayTest.build`: result ignored when `w.document() != d` (status "Build of … discarded") | EditorStress "switch" (status stays "Not built") |
| B13 | high (game crash) | Player starts around 34–35 / 58 in Village 1 crashed the **game** while loading (page fault reading 0xFACF0002 at 0x405527 in FUN_004054c0). Cause (decompile `FUN_00403020`): when the first floor test finds no polygon for a player-type strat (flags 0x40200) it sets the floor record flags \|= 5 ("floor on a map tile") **without** writing tile/polygon index (+0xc/+0xe); FUN_004054c0 then reads the tile→model table with the uninitialised index. The floor test only walks the tiles listed in the start's **own grid cell** – cell 35,58 has a tile (model 44) but no floor polygon under 35.5/58.5, cell 36,58 has no tile. In game (boot-to-level start override, :8): crash at 35.5/7.06/58.5, 35.5/9/58.5, 36.5/8/58.5, 5/8/5; fine at 34.2/7.35/58.2, 37.5/7.3/58.5 and the original start – exactly the positions `FloorFinder.below` (own cell, ≤ 0.6 above) finds no floor for. A death floor (30) does not crash (record reset at 0x40353e) | `StartOverride.unsafe/unsafeStart`; boot-to-level refuses such a start ("…no collision floor at or below it in grid cell 35, 58 (the game crashes while loading the level)", verified with apptest) and its version is 2 (old cached results are rebuilt); `LevelProjectMod.apply` refuses a level whose built WAD has such a start; the existing GeometryChecks error for the player start now says the game crashes (it already blocks Build/Play in the editor); "Play from camera position" already snaps to a floor of the own cell (`FloorFinder.near`) | `StartSafetyTest` (in-game positions, override refused, moved start reported + mod refused, no false alarms for 8 originals, testwelt and all templates), `OriginalStartsTest` |
| B12 | low | Autosave wrote the file in place; a crash during the write destroyed the only recovery copy | write `.tmp` + move | – (review) |
## Verified, no bug found
* Undo/redo consistency over random sequences incl. merged drags, grouped batches, deferred deletes of
waypoint chains, duplicates; undo all → byte-identical original WAD (unit fuzz + in the running editor
after 400 UI-level edits).
* Project round trip: diff storage incl. order, removed/added/changed, runs, unknown fields; save after load
byte-identical; cross-import with shifted user textures.
* Renderer sync: no stale selection after delete/undo spam (`selection.prune` per flush); no exceptions on
stderr during the stress run; build runs on a snapshot (no model access off the EDT).
* Opening damaged/empty/missing files and a missing base level shows one error dialog and keeps the current level.
## Not fixed (reasons)
* ~~Damaged-file dialogs show raw exception text~~ – fixed 2026-11-07 (edui): `ErrorDialog` (all `ctx.error` calls) shows one
plain sentence ("The project file is damaged.", "The file x does not exist.") and the exception + stack trace behind **Details**.
* ~~After a failed open, the status bar keeps "Loading <file>"~~ – fixed 2026-11-07 (edui): a failed background task sets
"<task> failed"; EditorStress `corrupt` now fails if the status still says "Loading…".
* ~~`GlRenderer.dispose()` does not delete `dynVao`/`dynVbo`~~ – fixed 2026-11-07 (edgeo), see editor-render.md.
* ~~`SceneRenderer.ids`/`nodes` keep pick ids of deleted nodes~~ – fixed 2026-11-07 (edgeo): dropped every 256 removals.
* ~~Tile-paint erase strokes are one undo step per tile~~ – fixed 2026-11-07 (edgeo): `cmd.Stroke`, one step per drag.
* `installTarget` treats a file with the same kind/base/level name as the previous play-test copy; two different
projects with the same level name and base still share one file (rare; renaming one avoids it).
* `LevelProjectSource`: when all TestLevel chapters 2..49 are taken, the 49th is used twice – practically unreachable.
* LlamaMod's own Play (outside the editor) does not finish a pending boot-to-level restore; it shows the
mod selection as "changes pending" (activeMods contains boot-to-level), so it is visible; fixing it would need a
framework change.
+94
View File
@@ -0,0 +1,94 @@
# Launcher and mod pipeline – bug review (2026-11-07, agent "lxbugs")
Scope: `Main` (CLI), `ui`, `platform` (Installer, IsoImage, GameSource, Integrity, GameLauncher, ExePatcher,
Config, FileOps, RegFile), `save`, `mods` (ModManager, ModRegistry, options, cache, world-9 fixup, ApplyLog),
`exe`, all `mods.*`. Method: code review, decompile checks (`re/groove_decomp.c`), programmatic combination
tests against the real groove.exe, ISO images built with genisoimage, in-game smoke tests with
`tools/apptest.py` (display :8).
## New tests
| Test | What it does |
|---|---|
| `mods/ExeModCombinationsTest` | all 31 combinations of the exe mods (intro-skip, cheats, practice, freecam, boot-to-level) × {defaults, every exe option on}: apply against the real groove.exe, re-parse the result (`ExeImage.parse`), sections sorted and non-overlapping (VA and raw), SizeOfImage covers the last section, `objdump -h` accepts the all-mods exe and lists `.llamac`/`.llamad`, restore gives the HKCU base exe, re-apply is 100 % cache hits and byte-identical (journal replay) |
| `platform/IsoVariantsTest` | ISOs built with genisoimage: Joliet (like the CD), Rock Ridge only, plain ISO 9660 → install + restore → lower-case `game/groove.exe` with the HKCU patch, `checkGameExe` OK, save game visible to the save editor, long names kept (Joliet/RR); truncated ISO, text file, all-zero file, ISO without game data → clear `IOException`s; setup again from a changed source / resume after cancel |
| `mods/ModManagerConcurrencyTest` | second instance applying/clearing the cache while the first applies → clean error, first finishes; clear cache refused while the game runs; exe written by another instance is not reported as "changed by antivirus" |
| `save/SaveStoreTest` | three saves within one second keep three backups; `(null)kingN.ksv` migration (empty slot, used slot, wrong size), corrupt slot, import of a wrong-size file, read-only slot file |
| `MainCliTest` | `--install` without/with two arguments, `--apply`/`--launch`/`--unlock` before setup, `--unlock` with missing/non-numeric/out-of-range slot, world, chapter (no file written), valid world-9 chapter 49 |
| `platform/GameLauncherPathsTest` | ANSI code page / length check of game paths; Wine data drive `L:` used for CDPath, exe and registry file |
| `platform/ConfigTest` | truncated config.json with an existing installation → still installed, copy kept as `config.json.broken`; wrong value types |
| `mods/levels/LevelProjectModTest.versionFollowsTheFileWhenItIsSavedAgain` | cache key changes when the project file is saved again |
## Fixed bugs
| # | Severity | Bug / repro | Fix | Test |
|---|---|---|---|---|
| L1 | high | **Intro skip + Boot to level could not be combined** (both default to "skip logos"): apply failed with "intro-skip (skip logo movies and images) (0x0041D56E, 3 bytes) overlaps "skip logo movies" of boot-to-level". Hit every editor play-test (it forces boot-to-level logos=true) for users with Intro skip enabled – 16 of 62 exe-mod combinations failed | `IntroSkipMod`: skip its logo patch when the identical patch is already there (boot-to-level runs first, order 90 < 100) | `ExeModCombinationsTest` |
| L2 | medium | ISO without Joliet (Rock Ridge only, e.g. `genisoimage -R`, or plain ISO 9660): names were the upper-case 8.3 names (`GROOVE.EXE`, `ARGOLOGO.DAT` instead of `argologompeg.dat`); on Linux `baseExe` did not find `original/hd/groove.exe` → no exe patch, exe mods failed ("NoSuchFile"), `game/groove.exe` missing → Play impossible; save games as `KING0.KSV` next to the editor's `king0.ksv` | `IsoImage`: Rock Ridge `NM` names when there is no Joliet; `Installer.setupGameDir`: all-upper-case HD names are copied lower-case; `ModManager.baseExe` finds the original exe case-insensitively | `IsoVariantsTest` |
| L3 | medium | **No lock between instances** (second window, CLI `--apply`, standalone editor play-test): both rebuild `cd-active.new`, `cache/<mod>/<key>.tmp` and `game/groove.exe` → deleted half-built folders, mixed results. The first window's config kept the old exe hash → after the other instance applied, Play showed "groove.exe was removed or changed – probably by antivirus software" | `ModManager.apply`/`clearCache` hold an exclusive `FileLock` on `<data>/.apply.lock` ("Another LlamaMod window or command is applying mods right now…"); `checkGameExe` accepts (and adopts) the exe hash stored on disk by another instance | `ModManagerConcurrencyTest` |
| L4 | medium (data loss) | Save backups are folders named by the second; a second save of the same slot within that second **overwrote the backup of the original state** (`REPLACE_EXISTING`) | `SaveStore.backupDir`: `<ts>-2`, `-3`, … when the file already exists | `SaveStoreTest` |
| L5 | medium | Level projects: `LevelProjectMod.version()` was the hash of the file **when the mod list was built**. Saving the project again (editor) while the launcher was open → Apply used the old cache key → the **old build** came back from the cache. The launcher also never rebuilt its mod list (`AppState.registry` was final): projects created in the editor appeared only after a restart | `LevelProjectMod.version()` re-hashes when size/mtime changed; `AppState.reloadMods()` on every `MainFrame.refreshAll()` (editor "mods changed", wizard, theme) | `versionFollowsTheFileWhenItIsSavedAgain` |
| L6 | medium | Setup again (Settings → Run setup again, `--install`) **skipped every file whose size matched** the old copy ("resume"), so a repaired or different ISO did not replace damaged same-size files; files not in the new source stayed in `original/` (and in cd-active); the mod cache (keyed by names + sizes) served outputs of the old data | `Installer`: resume only for an interrupted install of the same source (marker `original/.install-source`), otherwise copy everything, delete stale files, clear the cache | `IsoVariantsTest.setupAgainCopiesChangedDataAndDropsTheCache` |
| L7 | medium | Game paths: the game builds file names in a **260-byte buffer** (0x583748, FUN_00415690, no length check; the pointer at 0x58384C after it is passed to `free`) and uses ANSI file functions. A data folder path longer than ~230 characters crashes the game; characters outside the ANSI code page (Windows user "Łukasz" on a German system, Cyrillic names, …) become '?' → CD files not found. Linux: same through Wine (home folder with such characters) | Linux: the data folder is mapped as drive **L:** in our prefix (`wine/dosdevices/l:`), CDPath = `L:\cd-active\`, exe `L:\game\groove.exe` (short, ASCII); Windows (and the Linux fallback to Z:): `GameLauncher.checkPaths` refuses to start with a clear message (path > 200 chars or not encodable in `native.encoding`) | `GameLauncherPathsTest`; in game: all runs below use `L:` |
| L8 | low | CLI argument errors: `--install` without path → `NoSuchElementException` stack trace; `--unlock` without/with bad numbers → stack traces; `--unlock 4 1 1` wrote `king3.ksv`, `--unlock 0 …` wrote `king-1.ksv`, world 10 / chapter 7 of world 1 were written into the save (game crashes on the map); `--apply`/`--launch` before setup → "NoSuchFileException: …/original/cd"; `--install` while the game runs replaced game/groove.exe and then failed | `Main`: `UsageException` (exit 2, "Error: …"), range checks (slot 1–3, world 1–9, chapter 1–n), "not set up yet", running-game checks for `--install`/`--unlock`; `--editor-build/-import/-new` refused in a build without editor (`Edition`) | `MainCliTest` |
| L9 | low | "Clear cache" (Settings) while the game runs: on Linux with the symlink fallback the running game lost its CD files, on Windows the delete fails half-way on locked hard links, and the following re-apply is refused | `ModManager.clearCache` refuses while the game runs (and takes the apply lock) | `clearCacheIsRefusedWhileTheGameRuns` |
| L10 | low | Damaged `config.json` (cut off by a crash or power loss): defaults → "not installed" → setup wizard again (600 MB copy), and the next save overwrote the damaged file | `Config.load`: copy to `config.json.broken`; if `original/` and `game/groove.exe` exist, the installation stays usable (mod selection/options are lost) | `ConfigTest` |
| L11 | low | Apply wrote `game/groove.exe` **before** swapping `cd-active`; when the swap failed (Windows: cd-active open in Explorer or being scanned) the new exe (e.g. boot-to-level into world 9) ran against the old CD folder; `gameExeSha256` was set before the write, so a failed write produced a false "changed by antivirus" alarm | `ModManager.apply`: cd-active first, then the exe; the hash is recorded after a successful write | existing ModManager tests |
| L12 | low | Windows: replacing a read-only file (save game copied from a CD/old install) failed with AccessDenied | `FileOps.writeAtomic` clears the read-only flag of the target first | `SaveStoreTest` (no-op on Linux) |
## In-game smoke tests (apptest, display :8, session `lxbugs-mx`, all with CDPath `L:\cd-active\`)
Each run: apply via the CLI → (slot 3 unlocked at the level, menu → slot → map → level via `tolevel`) →
key sequence with screenshots → crash check (process, game.log, winedbg, window) → audio client present.
Exe mods were additionally checked in every combination by `ExeModCombinationsTest` (apply, re-parse,
objdump, replay, restore).
| Run | Mods (options) | Level | Result |
|---|---|---|---|
| orig | none | Village 1 | OK, walking |
| all-v | fog, randomizer, mirror, intro-skip, cheats, practice, freecam, debug-wireframe, testworld (defaults) | Village 1 | OK; wireframe + mirror visible, F7 free camera flies, F5/F8 |
| all-t | same | TestLevel 1 (Test World) | OK |
| introskip | intro-skip (story on) | Village 1 | OK |
| cheats | cheats (diagnostics on) | Village 1 | first run: harness timeout on the map (Enter not taken); re-runs `cheats-def` (defaults) and `cheats-diag2` (diagnostics) OK → harness flake |
| practice | practice (defaults, checkpoints) | Village 1 | OK, F5 / F8 |
| freecam | freecam | Village 1 | OK, F7 camera and F9 noclip |
| mrf-v / mrf-r | mirror, randomizer, fog | Village 1 / River 1 | OK / OK (raft level mirrored and randomized) |
| twdbg-t / twdbg-v | testworld, debug-wireframe | TestLevel 1 / Village 1 | OK / OK |
| boot | boot-to-level (world 1, chapter 2) | boots into Village 2 | OK |
| boot-exe | boot-to-level (world 3 ch 1) + intro-skip (story) + cheats + practice + freecam | boots into River 1 | OK (this combination failed to apply before L1); F7 works |
After rebasing onto the merged main tree (simp-all + launcher UX changes): orig, all-v, boot-exe, mrf-r
re-run with the final jar: all OK (CDPath `L:\cd-active\`).
![matrix](../img/launcher-matrix.jpg)
Screens: all mods in Village (wireframe, mirrored) and the free camera; River with mirror+randomizer+fog;
Test World with all mods; boot-to-level + all exe mods (River loading card); practice restore.
## Not fixed / recommendations
* ~~**Cache key and app updates**~~ – fixed in 0.2.0: the chain root contains the app version, the
build id (hash of the compiled classes) and `ExePatcher.REVISION`; entries of other builds (and of
0.1.0) are deleted after an apply.
* **Cross-mod hotkeys**: practice (save/restore) and freecam (camera/noclip) offer the same key list
and do not know each other – the same key in both triggers both. Needs a check across options
(e.g. ModsPanel or a `Mod.hotkeys()` hook).
* **Launcher edition** (`edition.properties` `editor=false`): only hides the editor button and the
`--editor*` commands; built by `./gradlew packageAll` (`fatJarLauncher`, `packageLinuxLauncher`,
`packageWindowsLauncher`) since 0.2.0. The editor packages **must stay in
the jar**: `mods.boottolevel.StartOverride` (editor.io/model, tools.playtest.FloorFinder) and
`mods.levels.*` (editor.io) need them at apply time; `ModRegistry` would only skip level projects
(LinkageError is caught), but boot-to-level with a player start override would fail with
NoClassDefFoundError.
* **OneDrive / redirected Documents** (decided: no warning) as data folder: hard links inside a synced folder and
on-demand files are fragile; the wizard could warn when the chosen folder is under `%OneDrive%`.
* ~~**Interrupted apply**~~ – fixed 2026-11-07: `cd-active.new.complete` marker +
`ModManager.recoverInterruptedApply()` (GUI start, Play, `--launch`), see llamamod-app.md.
Follow-up 2026-11-07: the GUI tells the user once what was repaired and retries when another
window held the apply lock (was silently skipped).
* ~~A `config.json` saved by a stale window overwrites activeMods/gameExeSha256~~ – fixed
2026-11-07: field-level merge-on-save under `.config.lock` (`Config.save`).
Follow-up: the window rebuilds its Mods/Settings tabs after a merge or when activated with a
changed config.json (`Config.refresh`), so its check boxes no longer show stale values.
* ~~Old Wine versions are not detected~~ – 2026-11-07: `wine --version` in Settings, warning below
Wine 5.0 (Settings in red, once before Play); missing Wine gives a clear message.
+95
View File
@@ -0,0 +1,95 @@
# Level editor – UX review (2026-11-07)
Usability pass by "edux": the editor used like a new user on Xvfb (1920×1080 screen; windows of
1500×920, 1920×1050 and 1280×720), mouse and keyboard only, following the manual's tutorials
(*Your first level*, *Make a parkour*, *Edit an original level*, *Import a Blender model*) plus
placing objects, the Objects/Environment/Textures panels, Shift+F5, dark theme, F1 help and
Preferences. Play-tests ran from the editor's own **F5 / Shift+F5** flow, with the game on its own
display and the editor started with the private PulseAudio server of an apptest session
(`PULSE_SERVER` and `-Dllamamod.gameDisplay` are inherited by `GameLauncher`; the game's audio went
into the private null sink). Both play-tests (new flat-floor level, Village from the camera
position) started straight in the level, and Escape in the game returned to the editor.
## Before / after
| Before | After |
|---|---|
| ![](../img/editor/ux_llama_before.jpg) | ![](../img/editor/ux_llama_after.jpg) |
| A 1920-wide window: the Inspector is 170 px wide and shows no values at all. Side docks have two rows of tabs. | The docks get their intended widths (290 / 340 px). Side-dock tabs are icons with the active one named. The engine fields of the object type are folded away. |
| ![](../img/editor/ux_brush_before.jpg) | ![](../img/editor/ux_brush_after.jpg) |
| The Brush tool's buttons read "Bl…", "Pl…", the texture names "gr…", the numbers show "0,5". | Full labels and a decimal point. |
| ![](../img/editor/ux_coin_before.jpg) | ![](../img/editor/ux_coin_after.jpg) |
| The Coin tool's options are cut off: only the top pixels of the fields are visible. | The options are on one line, or wrap to a second row in narrow windows. |
| ![](../img/editor/ux_menu_before.jpg) | ![](../img/editor/ux_menu_after.jpg) |
| *Help* comes before *Geometry*, and the shortcuts are German ("Strg", "Umschalt"). | The menus are File … Geometry, Tools, Help, and the keys are named "Ctrl", "Shift". |
| ![](../img/editor/ux_orig_before.jpg) | ![](../img/editor/ux_orig_after.jpg) |
| *Open original level*: the description is cut at the right edge, and the selected row's id is grey on orange. | The text wraps and the id is readable. |
| ![](../img/editor/ux_small_before.jpg) | ![](../img/editor/ux_small_after.jpg) |
| A 1280×720 window: the right dock is squeezed to 130 px. Also, Esc did not close the dialog, so it was still open and blocked every key. | The docks keep their sizes and the tool options wrap. Esc closes dialogs. |
![](../img/editor/ux_quit_game.jpg)
*New: Play while the game still runs offers to quit it.*
## Findings
Severity: **H** = blocks or seriously misleads; **M** = friction every session; **L** = polish.
| # | Sev | Where | Finding | Status |
|---|---|---|---|---|
| 1 | H | Docks (`DockLayout.applySizes`) | The right dock started at about 170 px instead of 340. The inner split's width was read before the outer divider moved. Effect: Inspector values and Brush buttons are invisible or truncated in every session that has no saved layout. | **Fixed**: lay out the outer split first |
| 2 | H | Window resize | When the window shrinks (1280×720, a laptop), the GL canvas's large minimum size squeezes the side docks to about 130 px. | **Fixed**: the viewport card has a small minimum size |
| 3 | H | Inspector sections | A section kept the height it had when created. Wrapped text and later rows (the Llama's waypoint buttons, the parameter descriptions) were clipped. | **Fixed**: the maximum height follows the preferred height |
| 4 | H | Inspector rows | Labels had a minimum height of 10 px. When the GridBag fell back to minimum sizes, every descender was cut ("Obiect tvpe"). | **Fixed** |
| 5 | H | Tool options bar | Options wider than the space left of Snap/View/Layers (the Coin, Place and Zones tools at 1500 px, the Brush tool at 1280 px) went to a second FlowLayout row that the bar did not grow for, so they were invisible. | **Fixed**: `WrapLayout`, the bar grows to two rows; all tools have the same bar height, so the viewport no longer jumps when you switch tools |
| 6 | H | Dialogs | Esc did not close the editor's own dialogs (*Open original level*, *New level*, *Level settings*, *Import mesh* …). A dialog half off-screen then silently swallowed all keys. | **Fixed**: Esc acts as the close button on every JDialog that has no Esc binding of its own |
| 7 | M | Side docks | Four tabs in 290 px wrapped into two rows that swap places when clicked (Swing's wrap layout). Clicks hit the wrong row, and typing into the filter field fired tool shortcuts. | **Fixed**: one scrolling row; in the side docks inactive tabs show only their icon (title as tooltip) and only the active tab has a ×. The asset, environment and texture sub-tabs also use one scrolling row |
| 8 | M | Locale (`Main`) | On a German system: menu keys read "Strg+Umschalt+D" and "Strg+Öffnende Klammer", spinners showed "0,75" next to "0.25", and Swing dialogs said "Ja"/"Nein" while the manual says Yes. Cause: `ImageIO` loads AWT before anything else. | **Fixed**: `Locale.setDefault(ENGLISH)` as the first line of `Main.main` (framework change, see below) |
| 9 | M | Menu bar | *Geometry* (a plugin menu) came after *Help*. | **Fixed**: plugin menus go before Tools, and Help is last |
| 10 | M | View menu | There were two **Panels** submenus (the Textures plugin's *View/Panels* entry and the shell's own), and a separator between every submenu. | **Fixed**: plugin entries are merged into the one Panels menu, and the submenus form one block |
| 11 | M | Toolbar | Tools were sorted by order number only, so the groups interleaved (Place objects, Brush, Coins, Tiles, Waypoints, Collision …) with separators in between. Two tools had the same icon (Zones and Zones & triggers). | **Fixed**: grouped in workflow order Select · Geometry · Texture paint · Objects · Environment. *Zones & triggers* uses the trigger icon |
| 12 | M | Start-up | The last tool was restored (for example Brush or Scale), so the first click in a freshly opened level drew a brush. | **Fixed**: the editor always starts with Select |
| 13 | M | Play / F5 | "The game is still running. Quit it first" was a dead end, because the game window is often hidden (another display, or full screen behind the editor). | **Fixed**: **Quit game and play** kills our own game (`GameLauncher.kill`), waits for the previous play-test to restore the mod selection, then starts. Tested: the old game was closed and the new one started in the village |
| 14 | M | Inspector, objects | Every object showed the engine fields of its type (code offset, variables, slots, type flags, extra memory) above Transform. | **Fixed**: they moved to a **Type details** section that is folded at first. *Object type* stays in **Type** |
| 15 | M | Inspector number fields | Long values showed their end ("6238" for 40.6238, "396, 4096, 8192"). | **Fixed**: the caret goes to the start and the full value is in the tooltip. Truncated labels ("Flags (2 = not s…") also get a tooltip |
| 16 | M | Outliner filter | Typing "Coin" showed a collapsed *Coin* group (depth 3), so the matches needed another click. | **Fixed**: groups are expanded down to depth 4 while filtering |
| 17 | M | Move / rotate / scale | No feedback during a drag: how far, which snap step. | **Fixed**: the status bar shows "Move 1 item Δ x 0.50 y 0.00 z 0.00 cells (snap 0.25, Ctrl = free)", the rotation angle and the scale factors |
| 18 | M | Assets panel | *Tile models* is empty in a new level ("0 of 0" and a white area). The *Imported* tab gave no hint either. | **Fixed**: grey hints ("no tile models yet … the Library tab has ready-made pieces", "File → Import mesh…", "Nothing matches the filter", "Open a level …") |
| 19 | M | Window size | The first-start size of 1500×920 is larger than a 1366×768 laptop screen, so the window hung over the edges. | **Fixed**: the size is clamped to the screen, and the window is maximized when it doesn't fit |
| 20 | L | Open original level | The description was clipped (the dialog was packed before the text was set), and the selected row's id was grey on orange. | **Fixed** |
| 21 | L | Status bar | The last-build label was cut ("Built 03:37:16 · 6158 KB · 2 w…"). | **Fixed**: "Built 03:37 · 6.0 MB · 2 warnings" |
| 22 | M | New level | A new level keeps the base level's 13 zones and 30 lights. Their outlines float far from the 16×14 floor (pink boxes in the sky), which is confusing in an empty level. | **Fixed (0.2.0)**: new projects keep only the start zone and the directional lights; teleport zones, point lights, waypoints and region masks of the base are removed (stored as edits, old projects unchanged). Directional light markers are drawn above the player start |
| 23 | M | Play-test | While the game runs, nothing in the editor says so: the status line falls back to "Ready" after 6 s. | Fixed (edui): `EditorContext.setGameRunning(what, stop)`; while the play-test game runs the status bar shows a green "Game running: <level>" badge and the toolbar Play button becomes **Stop game**; both reset when the watcher sees the game exit (`img/editor/ui_playtest_running.jpg`) |
| 24 | L | Shift+F5 | There is no mark in the viewport showing which point "the middle of the view" is. | Fixed (edui): crosshair in the middle of the viewport while Shift is held (300 ms delay, hidden while a mouse button is down) or while *Play from camera position* is hovered; green = valid start, red = not (status bar says why) – `tools.playtest.PlayHereMarker`, `Viewport.setHudOverlay` |
| 25 | L | Outliner | Rows show technical ids (`brush:n19`, `strat:6`). Several brushes are all called "Floor". | Fixed (edui): rows read "Coin 3", "Floor 2", "Waypoint 4", "Chain 1 – Llama", "Light 2", "Model 12 (…)", position/height in the grey column; the id is the row tooltip (filter still matches ids) |
| 26 | L | Inspector hints | Help texts in tool sections use fixed 215–230 px HTML widths, so they don't use a wider dock. | Fixed (edui): `Widgets.wrappingLabel(html)` wraps at the container/viewport width (Inspector hints, `GeoWidgets.hint`, environment `Ui.Form.note`, `ParamsSection.note`) |
| 27 | L | Problems panel | It shows "No problems." before any check has run. | Not fixed |
| 28 | L | Duplicates | Objects can be placed from both the Objects panel (left) and Assets → Objects (bottom). | Kept: both are documented, and drag & drop needs the grid |
| 29 | L | Menus | Key names come from AWT ("Ctrl+Open Bracket"), while the manual writes "Ctrl+[". | Not fixed (would need a custom accelerator renderer) |
| 30 | L | Toolbar | The tool buttons stay enabled while no level is open. | Not fixed (harmless) |
These worked well: the start page with recent projects, the *New level* dialog, crash recovery (a
killed editor offered **Recover** and restored the level), coin lines, Quick setups with
options-bar exit targets, *Death plane height…*, *Array duplicate*, the *Import mesh* dialog
(preview, size, warnings, click-to-place), context help (F1 opened the chapter of the panel
under the mouse), the Shift+F5 hint when aiming at the sky, and the dark theme (readable;
the selected **Snap** toggle has rather low contrast).
## Changes (files)
* `editor/ui`: `DockLayout` (sizes, scroll tabs, compact side tabs, minimum viewport size),
`ToolOptionsBar` + new `WrapLayout`, `EditorWindow` (menu order, merged Panels menu,
start tool, window size), `EditorExtensions.tools()` (grouped order), `EditorApp` (Esc closes
dialogs), `OpenOriginalDialog`, `PlayTest` (status text), `panels/InspectorPanel`,
`panels/OutlinerPanel`, `panels/AssetBrowserPanel`, `tools/TransformSupport`.
* Plugins (one-liners): `tools/objects/ZoneTool` (icon), `tools/environment/EnvironmentPanel` and
`tools/textures/TextureManagerPanel` (scroll tabs), `tools/playtest/PlaytestRunner`
(Quit game and play).
* **Framework**: `Main.java`: `Locale.setDefault(Locale.ENGLISH)` as the first line of `main`
(it affects the launcher UI too: English key names and Swing dialog buttons, decimal points).
* Manual: 03 (docks, toolbar groups, start tool), 05 (drag feedback, Type details), 09
(Objects tab icon), 15 and 20 (Quit game and play).
Verified: `./gradlew spotlessApply build` green (239 tests, ManualTest/ShortcutConflictTest/
EditorShellTest included), every fix checked in the running editor (screenshots above), two
play-tests in the game.
+98
View File
@@ -0,0 +1,98 @@
# LlamaMod launcher – UX review (2026-11-07)
Usability pass by "lxux" over the mod manager without the editor (the *launcher* edition that
Discord testers get, `edition.properties` `editor=false`; the full edition was checked too).
Used like a new, non-technical user on Xvfb (1920×1080 screen with a window manager; windows of
980×760, 1280×720 and 1920×1040), mouse only: the first-start wizard with
`~/Downloads/GROOVE_DK.iso` and its error cases (a text file named `.iso`, a folder without the
game, a truncated ISO, cancelling the copy), the Mods tab (all categories, every option editor,
badges, Apply/Restore, progress), Save games (unlock, edit, save, backups), Settings (resolution,
virtual desktop, Wine, theme, data folder, cache, set up again) and Play. The game ran on its own
display (`-Dllamamod.gameDisplay=:9`, supported by `GameLauncher.wineProcess`) with the private
PulseAudio of an apptest session (`PULSE_SERVER`); the game's audio went into the null sink. Game
messages were tested by killing the game, deleting `game/groove.exe`, and crashing it on purpose
(`ExeSmokeMod` with `crash=true`). Light and dark themes.
Not tested live: "not enough disk space" (no small file system without root; the check is
covered by code review), Windows (the antivirus notice is unchanged there).
## Before / after
| Before | After |
|---|---|
| ![](../img/app/ux_start_before.jpg) | ![](../img/app/ux_start_after.jpg) |
| The app opens with the mod list scrolled to the end (Visual, Cheats); Levels and Gameplay are out of sight. | The list opens at the top and keeps its place when it is rebuilt (Apply, theme). Short windows get a lower header. |
| ![](../img/app/ux_mods_before.jpg) | ![](../img/app/ux_mods_after.jpg) |
| Options have no explanation unless you hover (Worlds "all"? Checkpoint spacing "10" of what?). Descriptions talk about "far clip plane", "strafe", "rigid object models". | Every option is explained next to it. Mod descriptions say what happens in the game and which keys to press. |
| ![](../img/app/ux_wizard_before.jpg) | ![](../img/app/ux_wizard_after.jpg) |
| A text file is "shorter than expected (truncated?)", a wrong folder gets "expected Setup/…/Hd/groove.exe", a half-downloaded ISO is "Game found" and fails only after Set up. | Plain messages that say what to do; an incomplete ISO is caught at once, with both sizes. |
| ![](../img/app/ux_exe_before.jpg) | ![](../img/app/ux_exe_after.jpg) |
| On Linux a missing groove.exe is blamed on antivirus software and explained with Windows Defender menus. After Rebuild you have to click Play again. | Linux gets a short message; **Rebuild** writes the file and starts the game. |
| ![](../img/app/ux_saves_before.jpg) | ![](../img/app/ux_saves_after.jpg) |
| "lowest 4 bits of the coin count", "unknown item flag (bit 0)", "Cursor on", "Revert"; no hint how to restore a backup. | Plain wording, "Opens at", "Undo changes"; **Import…** starts in the backups folder and the hint says so. |
| ![](../img/app/ux_settings_before.jpg) | ![](../img/app/ux_settings_after.jpg) |
| "registry values ScreenW/ScreenH", "wine from PATH"; the mod cache shows 0.0 MB after applying 6 mods (stale). | "Automatic (1920x1052)" shows the real size, plain hints, a note on what *Set up again* keeps; the cache size is current. |
| ![](../img/app/ux_dark_before.jpg) | ![](../img/app/ux_dark_after.jpg) |
| Dark theme: the *modifies groove.exe* badge is dark orange on grey (barely readable). | A lighter badge text in the dark theme. |
| New: quitting a hidden or hung game | New: crash report |
|---|---|
| ![](../img/app/ux_quit_after.jpg) | ![](../img/app/ux_crash_after.jpg) |
| While the game runs, Play becomes **■ Quit game** (neutral colour) with a confirmation. | Shown as soon as Wine reports the crash (the game hides behind Wine's error window until that is closed), with Quit game / Open logs. |
![](../img/app/ux_av_linux_before.jpg)
*Before: on Linux every first Apply of an exe mod showed this Windows Defender notice. Now it is
shown on Windows only.*
## Findings
Severity: **H** = blocks or seriously misleads; **M** = friction every session; **L** = polish.
| # | Sev | Where | Finding | Status |
|---|---|---|---|---|
| 1 | H | Mods tab | Opens scrolled to the bottom: building the cards queues "scroll caret into view" for the last spinner/text field. Also after Apply and theme changes the list jumped. | **Fixed**: `reload()` restores the scroll position (top on the first build) after those events |
| 2 | H | Mod options | Option explanations existed only as tooltips. Non-technical users don't hover; "Worlds: all", "Checkpoint spacing 10", "Camera speed 3", "Fog: Scaled" are guesswork. | **Fixed**: grey explanation next to every option (wraps with the window) |
| 3 | H | Mod texts | Descriptions were written for developers ("far clip plane", "strafe", "rigid object models", "1/32 tile per tick", "Used by the level editor's Play button" in the launcher edition) and Free camera/Practice did not say which key switches them on. | **Fixed**: all 11 built-in mods rewritten: what changes in the game, keys (F7/F9, F5/F8, "can be changed below"), how to reach the Test World. Cheat codes kept as "(code ← → …)" |
| 4 | H | Wizard | A non-ISO file → "ISO image is shorter than expected (truncated?)"; a wrong folder → "expected Setup/…/Hd/groove.exe and Setup/…/Cd/wads". | **Fixed**: "This file is not a CD image (ISO)…" / "The game was not found here… (it contains a folder named Setup)" |
| 5 | H | Wizard | A truncated ISO (interrupted download) passed the check ("Game found: 187 files") and failed after Set up with the same cryptic message. | **Fixed**: `IsoImage` compares the file size with the end of the last file extent: "The ISO file is incomplete: it has 300 MB, but the CD image needs 582 MB. The download or copy was probably interrupted – copy the file again." |
| 6 | M | Wizard | Not enough space was only found after Set up ("Not enough disk space: … needed"). | **Fixed**: checked when the source is chosen (nearest existing parent of the data folder); the installer's message says to free space or choose another folder |
| 7 | M | Wizard | Cancel during the copy silently went back to the first page. Long messages were cut at the dialog edge. | **Fixed**: "Setup cancelled. Click Set up to continue – files already copied are kept." (resume works); messages wrap |
| 8 | H | Play | No way to stop the game from LlamaMod; under Wine a hung or hidden game blocked Apply and saves until the user found `wineserver -k`. | **Fixed**: the Play button reads **■ Quit game** while the game runs (neutral colour, confirmation "Progress since your last save is lost") |
| 9 | H | Play | A crash only produced "The game has ended." – and under Wine not even that while Wine's error window (often invisible behind/inside the virtual desktop) kept the process alive. | **Fixed**: `GameLauncher.crashReport()` reads game.log; the app shows "The game crashed" with advice (switch off the last mods / Restore original, report game.log), Quit game and Open logs – also while the error window is still open |
| 10 | M | Play / Linux | The antivirus notice (Windows Defender menus) appeared on Linux before every first exe-mod Apply; "groove.exe was removed or changed – probably by antivirus software" on Linux. | **Fixed**: notice on Windows only; Linux gets a plain "missing or changed – Rebuild" dialog |
| 11 | M | Play | After **Rebuild** in that dialog the user had to click Play again. | **Fixed**: Rebuild continues with the launch (also after Apply → Play) |
| 12 | M | Play | "Creating Wine prefix (once, takes a moment)", "Writing registry values". | **Fixed**: "Preparing Wine for the game (only the first time, may take a minute)", "Writing the game settings" |
| 13 | M | Settings | A wrong *Wine command* showed "Found: /wrong/path" and Play failed with "Cannot run program … error=2". | **Fixed**: "Not found: … – correct the command or leave the field empty", Play explains the same; the line updates when the field is left |
| 14 | M | Settings | The mod cache size was computed once at start (0.0 MB after applying 548 MB). | **Fixed**: the tab refreshes when opened |
| 15 | M | Settings | "Game resolution (registry values ScreenW/ScreenH)"; what "Automatic" means was only in a tooltip. | **Fixed**: the entry reads "Automatic (1920x1052)" (updates with *Run in a window*); plain hint mentions typing a custom size |
| 16 | M | Window | At 1280×720 (laptops) the 150 px header left room for 3½ mod cards. | **Fixed**: header is 104 px when the window is under 860 px tall (also the default 980×760 window) |
| 17 | M | Saves | "The game stores only the lowest 4 bits of the coin count (0–15). Collectible = unknown item flag (bit 0)." "Cursor on", "Revert". Status after saving printed the full backup path. Restoring a backup meant finding the folder by hand. | **Fixed**: plain hint, "Opens at", "Undo changes", "Save game 1 saved. The old version was backed up (Import… restores it)."; Import… opens in the backups folder |
| 18 | M | Status bar | "6 mod(s) active, 33 file(s) changed." / "Applying mods – done." – no next step, no duration. | **Fixed**: "Mods applied – 6 active (took 3 s). Click Play to start the game."; every task ends with "– done (took …)" |
| 19 | L | Mod cards | "v1 · testworld": ids mean nothing to players. | **Fixed**: "v1"; the id is in the tooltip (for the command line) |
| 20 | L | Dark theme | *modifies groove.exe* badge text nearly invisible. | **Fixed** |
| 21 | L | Hints | Grey hint text used `Label.disabledForeground` (about 3:1 contrast). | **Fixed**: `App.hintColor()` (blend of label and background, ~6:1), used for option, settings, wizard and save hints |
| 22 | L | Mods intro | Fixed 640 px wrap; long technical sentence. | **Fixed**: "Tick the mods you want, then click Apply and Play…" |
| 23 | L | Wizard done | "Choose mods on the Mods tab, edit save games…". | **Fixed**: "Next: tick the mods you like on the Mods tab, click Apply, then Play. The Save games tab can unlock worlds." |
| 24 | L | Launcher edition | No help/manual at all (the manual window lives in the editor plugin and needs an editor context). | Open – a Help button would need `ManualWindow` without `EditorContext` |
| 25 | L | Mirror mode | *Worlds* is free text ("all" / "1,3"); a list of checkboxes would be friendlier. | **Fixed** 2026-11-07: check boxes per world (new `ModOption.Type.MULTI`; stored "all"/"1,3" still load) |
| 26 | L | Wizard | The copy page shows the file name, not "123 of 568 MB" or a time. | **Fixed** 2026-11-07: "Copying 123 of 568 MB (file)" |
| 27 | L | Save games | The chapter table shows 4–5 rows at 720 px height. | **Fixed** 2026-11-07: lives moved into the world-map section (one header less), narrower number/check columns, centered coins – 6 rows at 1280×720 and 840×720 (was 4.5) |
## Changes (files)
* UI: `ui/MainFrame` (Quit game, crash dialog, Rebuild → continue, header height, task duration,
settings refresh), `ui/ModsPanel` (inline option hints, scroll position, card height, badge,
version label, status), `ui/SetupWizard` (free-space check, cancel message, wrapped messages),
`ui/SettingsPanel`, `ui/SavesPanel`, `ui/AntivirusDialogs` (Windows-only notice, Linux text,
`problem` returns the choice), `ui/App.hintColor()`, `messages.properties`.
* Platform: `IsoImage` (not-an-ISO and incomplete-ISO messages, extent check), `GameSource`
(game-not-found message), `Installer` (space and damage messages, `notEnoughSpace`),
`GameLauncher` (`crashReport()`, `isCommand()`, Wine-command check, progress texts).
* Mods (texts only, no behaviour or cache-key change): testworld, mirror, randomizer,
boottolevel, cheats, debug, fog, freecam, introskip, practice, levels.
* Test: `platform/LauncherMessagesTest`.
* Manual: chapters 02 (wizard errors, Quit game, inline explanations, Save games/Settings
labels), 18 (options explained, Windows-only antivirus), 20 (new "Setting up and playing"
table, Wine command, first Play), generated mod reference, `img/app_mods.jpg`,
`img/app_saves.jpg`.