130 lines
9.4 KiB
Markdown
130 lines
9.4 KiB
Markdown
# Level editor – play-test straight into the level (`editor.tools.playtest`, `mods.boottolevel`)
|
||
|
||
Before: Play installed the level and started the game at its main menu; the player had to
|
||
choose *Spiel starten* → slot 3 → Enter on the map. Now **Play (F5)** lands in the level a few
|
||
seconds after the game window opens, and **Escape** quits back to the editor.
|
||
|
||

|
||
|
||
## Editor side (`PlaytestEditorPlugin`, `PlaytestRunner`, `FloorFinder`)
|
||
|
||
| Action | Key | |
|
||
|---|---|---|
|
||
| Play (replaces the built-in `level.play`) | F5 | build → install level mod → apply user mods + boot mod → save slot → start |
|
||
| Play from camera position | Shift+F5 (toolbar) | the llama starts on the ground in the middle of the view, facing the camera direction |
|
||
| Play boots straight into the level | Level menu (check) | off = old behaviour (main menu, map cursor) |
|
||
| Escape quits the play-test | Level menu (check) | boot mod option `quitOnEscape` |
|
||
|
||
* Run: `PlayTest.build` (built-in, background) → the project snapshot is written to the
|
||
**temporary** `<data>/levels/.playtest/<stem>.llamalevel` (mod id `playtest-<stem>`; with
|
||
"Keep play-tested level in LlamaMod" on: `<data>/levels/<stem>.llamalevel` as before) → the user's **active** mods (others on the same WAD and Test
|
||
World on chapter 1 turned off) + the level mod become `enabledMods`; `boot-to-level` is applied
|
||
**in addition** (not stored in `enabledMods`) with world/chapter of the project, slot
|
||
(Preferences, default 3), `logos=true`, `quitOnEscape`, `start` → save slot unlocked up to the
|
||
level → `GameLauncher.launch`.
|
||
* **Restore**: a daemon thread polls `GameLauncher.isRunning()` every 2 s; when the game has
|
||
ended it re-applies the user's **previous** active mods and enabled list (stored in the
|
||
session: `mods`, `enabled`, `bootOptions`), restores the boot mod's own options and deletes
|
||
`levels/.playtest/` plus `playtest-*` entries in `enabledMods`/`modOptions`
|
||
(`LevelProjects.clearPlaytest`). With "keep" the level stays enabled/active (old behaviour).
|
||
The session file `<data>/editor/playtest-session.json` lets the next editor start finish it;
|
||
without a session, the next editor start deletes leftover copies if no game runs
|
||
(`clearStale`). The non-boot Play (menu, map cursor) uses the same install/restore path now.
|
||
* **Level → Add to LlamaMod…**: writes the snapshot to `<data>/levels/` (`PlayTest.installTarget`:
|
||
the same project's file is reused, file names compared case-insensitively) and sets the mod's
|
||
`chapter` option to the project's TestLevel chapter.
|
||
* The launcher hides `playtest-*` mods unless enabled/active (then: "(play-test)" + blue badge
|
||
"temporary play-test", no Remove button).
|
||
* **Play from camera position**: ground point = depth-buffer hit at the viewport centre
|
||
(export space → game x, y, −z), heading = camera yaw (`atan2(f.x, −f.z)`, 0 = +z).
|
||
`FloorFinder` then looks for **collision**: highest floor polygon (|n.y| ≥ 0.5) at or below
|
||
the hit + 0.6 among scene tiles and brush-generated tiles, searching up to 0.75 cells around
|
||
(rings of 8); material 30 (death floor) is rejected. Needed because render geometry often has
|
||
no collision (village: the hill edge east of the start renders, but only a death floor at
|
||
y 2.25 lies below – the llama fell to a game over). Since 2026-11-07 `FloorFinder` is a thin layer
|
||
over the objects tools' `Ground` (`Ground.ofCells(scene, …)` built from the query cell(s) only + `cellBelow`: the cell's own tiles only, death
|
||
floors included so a death floor is still reported/used as last resort, no swap models or neighbours).
|
||
* `-Dllamamod.editor.noLaunch=true` skips the start (tests start the game with apptest).
|
||
|
||
## Exe mod `boot-to-level` (GAMEPLAY, `modifiesExecutable`)
|
||
|
||
Options: `world` 1–9, `chapter` 1–49, `slot` 1–3, `logos` (skip logo movies/title, same patch
|
||
as intro-skip – only applied if the bytes are still original, so both mods can be on),
|
||
`quitOnEscape`, `start` ("x, y, z[, yaw]" in cells; empty = the level's own start). Order 90
|
||
(after level mods). Registered through `META-INF/services/com.eliasmueller.llamamod.mods.Mod`
|
||
(ServiceLoader; `ModRegistry` was not edited).
|
||
|
||
### Engine facts used
|
||
|
||
| | |
|
||
|---|---|
|
||
| 0x5846E4 | game state: 1 ?, 2 main menu, 3 in game, 4 attract demo, 5 options?, 6 save slots, 12 world map, 0x11 quit |
|
||
| 0x578F28 | frame handler per state (returns 1 = state changed → main loop runs the enter handler) |
|
||
| 0x578F7C | enter handler per state; state 3 = FUN_0054ae20: intro movie, loading screen `T1%d%d.COR`, FUN_00419510(tribe, world, chapter, 0) loads the level |
|
||
| 0x579050 | per-state update/render (3 = FUN_0054b590, the game frame) |
|
||
| 0x584EB4/B8/BC/C0 | tribe, world, chapter, map of the level to load; 0x585588/0x584C24/0x584C20/0x58557C = map cursor |
|
||
| 0x584700 | next state for fade transitions; 0x584638 = sub state (7 = enter level) |
|
||
| FUN_0041c0e0 | reads `king0..2.ksv` into 0x585274 + slot·0x100 |
|
||
| FUN_0041cc70 | loads slot 0x584C28 into the game variables (new game in it if empty) |
|
||
| 0x546CC0, 0x546990([0x6D94B4]), 0x5464D0 | music/sound shutdown done before every state switch (copied from the demo switch at 0x549E5D) |
|
||
| 0x5FCF08 bit 0x8 | "pause" pressed this frame (Escape) |
|
||
| main loop (WinMain) | exits through its normal cleanup (FUN_00545030, DestroyWindow, CoUninitialize) when the state is 0x11 – the main menu's *Beenden* |
|
||
|
||
### Patches
|
||
|
||
* **Boot**: `jmp` at 0x549C50 (state-2 frame handler, `A1 2C 37 58 00` = `mov eax,[0x58372c]`).
|
||
The cave runs once (flag in an `allocateData` block): pushad, read slots, `[0x584C28]=slot−1`,
|
||
load slot, set tribe 1/world/chapter/map 0 (+ map cursor), `[0x5855B0]=0`, sub state 7, music
|
||
shutdown, state 3, popad, `mov eax,1; ret` → the main loop enters state 3 exactly like the
|
||
map's Enter would. Later main-menu frames (after quitting to the menu) run the original code.
|
||
* **Escape quits** (`quitOnEscape`): `jmp` at 0x549F40 (state-3 frame handler, same 5 bytes):
|
||
`test byte [0x5FCF08],8; jz original; mov [0x5846E4],0x11; mov eax,1; ret`.
|
||
* **Logos**: 0x41D56E `68 28 25` → `53 EB 62` (see mod-introskip.md).
|
||
* **Start override** (data, not exe): the level's WAD (as written by earlier mods) is imported,
|
||
the Llama moved/turned and rebuilt (`StartOverride`; an unchanged rebuild is byte-identical).
|
||
Its waypoint chain moves and turns along only if it is an editor/levelbuild camera chain
|
||
(all nodes but the first on the llama); original levels use the llama's chain as a
|
||
level-wide list, which stays.
|
||
|
||
## Verified in game (apptest, display :9, Wine 6, 2026-11-06)
|
||
|
||
| Test | Result |
|
||
|---|---|
|
||
| `boot-to-level` world 1 chapter 2, original data | Village 2 fly-by 5 s after the window, gameplay at ~20 s, walking works |
|
||
| Editor Play on `examples/testwelt.llamalevel` (TestLevel 2) | game shows the Testwelt plaza with HUD 4 s after the window opens (`play_boot.jpg`) |
|
||
| Editor Play from camera position, Village 1, start 2.3 cells east | llama stands at the new spot (`play_from_here.jpg`); the original village fly-by is skipped (its cut-scene camera is tied to the original start) |
|
||
| Same with the view on the hill edge (no collision) | before FloorFinder: llama fell to a game over; now the editor finds the nearest floor or refuses |
|
||
| Escape in gameplay (village and TestLevel) | groove.exe gone after 1.3 s, no crash, no Wine error dialog, the next start works |
|
||
| Restore after the game | active mods back to the user's selection, exe has no mod patches |
|
||
|
||
Images: `img/editor/play_boot.jpg`, `play_from_here.jpg`, `play_editor.jpg`,
|
||
`play_windows_editor.jpg`.
|
||
|
||
## Windows package under Wine
|
||
|
||
`./gradlew packageWindows` → `LlamaMod-windows-x64.zip` (45 MB). Unpacked into an own win64
|
||
prefix (Wine 6.0.3), started as `JDK_JAVA_OPTIONS="-XX:ActiveProcessorCount=4
|
||
-Dllamamod.data=Z:\…\data" wine runtime/bin/javaw.exe -jar LlamaMod.exe --editor t1l1m001`
|
||
(the launch4j exe itself hangs in its `javaw -version` check under Wine 6, see llamamod-app.md):
|
||
the editor window opens (within about two minutes on the first start, software GL) and renders the village with all phase-3 plugins (Play from
|
||
camera position, Textures tab, paint tool) – `img/editor/play_windows_editor.jpg`. The game was
|
||
not started from the Windows build (the private Wine prefix has no game install); real Windows
|
||
hardware is untested.
|
||
|
||
## Tests
|
||
|
||
`PlaytestLifecycleTest` (temporary copy listed as `playtest-…` → restore removes it and puts
|
||
the enabled list back; keep → stays; stale copies cleared; Remove → backups, refused while the
|
||
game runs; regression: `My-level`/`my-level` get distinct ids), `BootToLevelModTest` (hooks at both frame handlers, logo patch, exe re-parses; Escape hook only
|
||
when asked; start parsing; original village: only the llama moves, file size unchanged;
|
||
editor-style chain moves and turns with the llama), `RestoreCheck` (manual), `EdTour` +
|
||
`PlayTourSteps` (scripted editor runs on an X display: camera placement, play-from-here).
|
||
|
||
## Temporary play-test copies – verified in game (apptest lvlrm, game on :5, editor on :31, 2026-11-07)
|
||
|
||
EdTour `run:level.play` on `/tmp/…/mytest.llamalevel` (real launch, private pulse): during the
|
||
game `levels/.playtest/mytest.llamalevel`, enabled `[playtest-mytest]`, active
|
||
`[playtest-mytest, boot-to-level]`, Testwelt plaza in game (`img/mods/levels_playtest_temporary.jpg`);
|
||
Escape → game gone → `.playtest` deleted, enabled `[]` and active `[level-old-village-test]` as
|
||
before the test, session file removed. Launcher badge: `img/mods/levels_playtest_badge.jpg`.
|