150 lines
11 KiB
Markdown
150 lines
11 KiB
Markdown
# 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 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.
|
||

|
||
* 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`).
|
||
|
||

|
||
|
||
## 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).
|
||

|
||
* 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).
|
||

|
||
* **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. 
|
||
|
||
## 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.
|