Files
llamamod/notes/testlevel-chapters.md
T

150 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TestLevel chapters: which ones are safe (2026-11-07)
World 9 ("TestLevel") has 49 chapter slots (table 0x572420), but the game's per-chapter arrays only
have rows for worlds 0–8. Indexing them with world 9 runs into other globals. Code:
`save/TestLevelChapters` (usable = chapter **1** and **32–49**).
## The arrays (index world·6 + chapter)
| Array | Where it is used |
|---|---|
| u32 coins 0x584F1C (coins << 12) | `levelRecord` (strat var 0xBE) get/set, HUD (FUN_00426bf0), tally (FUN_0040dd60), endLevel (FUN_0054d620: "all coins" check), **zeroed on every level start** (FUN_0041cba0), KSV keeps 54 entries as u16 |
| u8 flags 0x584EC8 (bit 0 collectible, bit 1 gold coin) | collectible picked up (HUD, strat var 0xFA rising) → \|=1; endLevel → \|=2 when coins == static total 0x5722F4[world·5 + chapter] (garbage for world 9, often 0) or cheat 0x584710&8 → \|=3; gold count FUN_0041cd70 (worlds 1–8 only) |
Coin total table 0x5722F4 and the selection map only know worlds 1–8 (map shows garbage targets
for world 9 – cosmetic).
## What world-9 chapter c hits
Coin slot 0x584FF4 + 4c, flag byte 0x584EFE + c (Ghidra references 0x584EF0–0x585240):
| c | coin slot | flag byte |
|---|---|---|
| 0 | 0x584FF4 counter var 0xC0 (HUD) | free |
| **1** | 0x584FF8 free (only cleared by new game) | free |
| 2 | 0x584FFC counter var 0xBF (HUD icon 0x150) | free |
| 3 | 0x585000 HUD flag (FUN_00427c40) | free |
| 4 | 0x585004 var 0x80 | free |
| 5–8 | 0x585008–14 last played tribe/world/chapter/map (strat vars 0xAB–0xAE, KSV +0x3E–41) | free |
| 6–9 | | 0x584F04 **language index** (u32; \|=2 → text table out of range) |
| 9 | 0x585018 **furthest progress** (world, chapter bytes; KSV +0x42/43) | |
| 10–13 | 0x58501C/20 free | 0x584F08 option (KSV +0xD6) |
| 12 | 0x585024 start of byte table (var 0x93, tribe 0) | |
| 14–21 | 0x585028–48 free | 0x584F0C / 0x584F10 (var 0x5B) |
| 22–25 | 0x58504C–58 table 0x585024, tribe 1 (read by var 0x93 in original levels) | free |
| 26–29 | table, tribe 1 | 0x584F18 option (KSV +0xD8) |
| 30–31 | table, tribe 1 (world 8/9) | coin slots of world 0 (never read) |
| **32–49** | table, tribes 2/3 (never used: tribe is 1, 4 only in the attract demo) | coin slots of world 0 |
## Verified in game (apptest :24, 2026-11-07)
* **Chapter 9** (arena test mod, 1 alligator, 2 coins collected): pause → "Beenden und zur
Auswahlkarte" → the selection map shows world "°p", "Kapitel 0", coin target 14384: furthest
progress 0x585018 was replaced by the coin counter (2 << 12 → world 0, chapter 32).
![chapter 9: broken map](img/mods/testlevel_ch9_map_broken.jpg)
* **Chapter 49** (same run): tally "Münzen gesammelt 2 / 12, Geheimnisse 0 / 0" (LNFO recounted),
save written with progress (9, 50) – the map caps the cursor at chapter 49, no crash; map fine.
![chapter 49 tally](img/mods/testlevel_ch49_tally.jpg)
* Before (arena on chapter 8, arena agent): tally "0 / 55": the collected value is the coin slot
after endLevel copied the level ids into 0x585008–14 (chapter 8 = last played map = 0), the total
is LNFO of the Village base.
## Tally totals (LNFO)
Chunk LNFO = u32 coins, u32 secrets → 0x5FF070 / 0x5FF060 (FUN_0042AB10; the tally shows the values
of the level just finished, kept in 0x5FF064 / 0x5FF06C). Originals: e.g. t1l3m004 40 = 20 Coin +
4 Coin5·5; levels with CoinLine/CoinCircle/enemies count those too. `mods/levels/LevelTally` recounts
Coin (1) + Coin5 (5) and Secret strats for new level projects and the Test World (20 coins, 0
secrets); the arena adds its dynamic coins (alligator hit/death coins + 5 exit bonus).
## Migration
* Level projects: stored chapter 1 or 32–49 kept (first file wins), others get the first free of
32–49; more than 18 projects share chapters (only one of them can be on – apply error otherwise).
Play-tests/exports of an old project: `migrate` (2..19 → 32..49, 20..31 → 32..43).
* The chapter options are choices (level: 1, 32–49; arena: 32–49, default 49); a stored old value is
invalid and falls back to the default (ModOptions).
* Editor: New level / Level settings spinners only offer usable chapters. The empty template
(`Templates.makeEmpty`) defaults to 32; New level and CLI `--editor-new` take the first chapter
from 32 no project in the levels folder uses (`LevelProjects.nextFreeChapter`).
![chapter option](img/mods/testlevel_chapter_option.jpg)
## Fix: base exe patch relocates both arrays (ExePatcher.REVISION 2, 2026-11-07)
`ExePatcher.relocateLevelRecords` (part of the always-applied base patch: `ExePatcher.patch` for
the installer, `ExePatcher.changes` replayed as owner "base" in `ModManager.baseExe`) moves both
arrays to a 128-entry buffer (index ≤ 9·6 + 49 = 103) in the slack after .data's VirtualSize:
.data ends at 0x6DB935, the page at 0x6DC000 (.idata follows), so coins u32[128] at **0x6DB940**,
flags u8[128] at **0x6DBB40** (zero-filled and RW, mapped with .data's last page; no new section,
no code). Every absolute address of the arrays in .text is rewritten (whole-file scan of
0x584EC8–0x584EFE / 0x584F1C–0x584FF4: no other references, none in .data):
| Old | New | Sites (operand VA) |
|---|---|---|
| 0x584F1C coins | 0x6DB940 | tally 0x40DE67, KSV load 0x41C72A / save 0x41C8FB, new game 0x41CA6B, level start 0x41CBCC, HUD 0x426AC2 / 0x426D43 / 0x426E08, endLevel 0x54D6BA, levelRecord ptr 0x550BC5, 0x55223E |
| 0x584FF4 coin loop end (`cmp eax, imm; jl`) | 0x6DBA18 | 0x41C74B, 0x41C91A (0x584FF4 itself is also counter var 0xC0 – those uses stay) |
| 0x584EC8 flags | 0x6DBB40 | tally 0x40DEA1 / 0x40DEC9, KSV 0x41C65C / 0x41C832, new game 0x41CA64 (rep stosd 54 bytes), collectible 0x42706B, endLevel 0x54D6E3 / 0x54D708 / 0x54D70F |
| 0x584ECE flags from world 1 | 0x6DBB46 | map icons 0x41B724 / 0x41B79C / 0x41B7EE / 0x54A75E, gold count 0x41CD7B |
| 0x584EFE flag loop end | 0x6DBB76 | KSV 0x41C6D6 / 0x41C8AF, gold count 0x41CD9B |
Worlds 0–8 behave as before (KSV keeps the first 54 entries; the rest is never saved). World-9
flags are not cleared by "new game" (only the 54 saved bytes are) – they only affect the map's
collectible/gold icons of TestLevel chapters within one game session. If the counts of these
references differ (other exe build) the relocation is skipped as a whole. Test: `ExePatcherTest`.
**Effect:** every world-9 chapter is harmless in LlamaMod's groove.exe, so the remaining holes are
closed without restricting the map: picking an empty (linked) chapter like 9 on the world map,
chapters 2–31 that unlock together with 32+, and Boot to level on chapters 2–31 (its option keeps
1–49). Not linking empty chapters is no option: a missing WAD crashes the map ("Couldnt load wad",
`ModManager.world9Fixup`). Mod levels still only use 1 and 32–49 (no migration churn; also safe
with an exe that has not been rebuilt yet after the update).
Verified in game (apptest :24, 2026-11-07):
* A/B, Test World on, map cursor world 9 chapter 9 (the linked empty chapter), start, pause →
quit to the map: old jar (base REVISION 1) → map "°p / Kapitel 0 / 14384" (progress zeroed by the
level start alone); new → "TestLevel / Kapitel 9 / 237" (237 = garbage static coin total, cosmetic).
![chapter 9 before/after](img/mods/testlevel_ch9_fixed.jpg)
* Arena (test mod) on chapter 9, 2 coins collected (HUD 2 / 12), quit to the map → map fine.
* Save with world-1 chapter 1 flags = 3 and gold count 2: slot menu "Goldmünzen 2", the map shows
the collectible and gold icons of Village chapter 1 (KSV load loop + map reads relocated).
* **KSV save loop after finishing a normal level, A/B byte comparison** (apptest :25, sessions
`savetest-a` = fat jar of the commit before 00b2880, base REVISION 1; `savetest-b` = HEAD,
REVISION 2; 2026-11-07). Slot 3 (`king2.ksv`) preloaded identically in both with a distinct
pattern for worlds 1–8, chapters 1–5: flags `(w + c) % 4`, coins `((5w + c) % 15 + 1) << 12`
(gold count 19/20 to match), so a save loop reading the old arrays in B would write zeros.
Mods: boot-to-level (slot 3) + cheats `rewards` (flags |= 3 at the chapter end). Runs:
1. **Jungle night 2** (world 2 ch. 2, exit pad reachable directly): start `29.45, 3.5, 60.3, 180
@ 2/2`, Up ~5.5 s → 3 coins → tally "Münzen gesammelt 3 / 70" → Return.
2. **Village 1** (world 1 ch. 1): t1l1m001 rebuilt by a throw-away tool (WadImporter → tile 420
set to its swap-alt model 49 with the exit pad, RubyIdolDoor removed, the two coins near the
door moved to x 42.5, `StartOverride` 42.5/3.5/51.8 yaw 180 → WadBuilder), written over the
session's `cd-active/wads/t1l1m001.wad` after apply; Up 3.5 s → tally "Dorf Kapitel 1,
Münzen gesammelt 2 / 55" → Return. (Village 4's direct exit is behind the Yzma boss fight.)
3. **TestLevel chapter 1** (Test World, start `44.3, 3.6, 15.5, 90 @ 9/1`, 3 coins, rewards
cheat) on top of the Village-1 save → tally → Return.
All three saved KSVs are **byte-identical in A and B** (`cmp`: 0 differences). Run 1: chapter
(2,2) coins 9 → 3, flags 0 → 3, gold 20 → 21, progress (2,2) → (2,3), cursor (2,3), last played
(2,2); every other preloaded coin/flag entry and bytes 0xB0–0xFF unchanged. Run 2: (1,1) coins
→ 2, flags → 3, gold 19 → 20, progress (1,2). Run 3 (world-9 leak check): only cursor world/chapter
(9,1) and last-played world 9 change; all 54 coin/flag entries, gold, progress and 0xB0–0xFF stay
as before – the world-9 coins/flags (buffer index 55) are not written (54 entries only). B's save
loaded again without mods: slot menu "Gespeichertes Spiel 3 / Dorf Kapitel 2 / Goldmünzen 20",
world map on Village 2 with the collectible and gold icons (preloaded flags 3).
![tallies, slot menu, map (B)](img/mods/testlevel_save_ab.jpg)
* **TestLevel tally** (question from the coaster test): Return does save (KSV rewritten, see run 3)
– there is never a visible "saved" step, also not in normal levels (the next level loads at
once). The Test World / coaster / arena exits lead back to the same chapter, so the restart zeroes
that chapter's coin counter (level start, FUN_0041cba0) → the map then shows 0; world-9 coins are
never in the KSV anyway. Expected and harmless. Esc on the tally asks "Möchtest du das Spiel
beenden? Ja / Nein" (Return = Nein, back to the tally) – identical with the old exe (A), so not
caused by the relocation. ![Esc on the tally](img/mods/testlevel_tally_esc.jpg)
## Before the fix
* The map let the player pick any unlocked world-9 chapter; empty ones are linked to an installed
world-9 WAD (ModManager), so playing e.g. chapter 9 from the map still clobbered progress. Unlocking
chapter 32+ unlocks 2–31 on the map too. Fixed by the relocation above.