10 KiB
10 KiB
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.pruneper 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 ("ClassCastException …"); cosmetic, left to the UX review.
- After a failed open, the status bar keeps "Loading " (
StatusBar.progressDonekeeps the last message) – cosmetic (UX). GlRenderer.dispose()does not deletedynVao/dynVbo(only at window close) – negligible.SceneRenderer.ids/nodeskeep pick ids of deleted nodes until the next level (small, bounded by edits).- Tile-paint erase strokes are one undo step per tile (
DeferredRemovehas no merge) – UX, not a defect. installTargettreats 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.