Files
llamamod/notes/dev/bug-review-editor.md

10 KiB
Raw Permalink Blame History

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 " – fixed 2026-11-07 (edui): a failed background task sets " 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.