documentation for level builders: docs index, step-by-step guide, file format reference (wad, geometry, objects, textures, audio, testlevel chapters, engine limits), strat vm and script language with the arena as worked example, tools overview, developer notes; readme rewritten as project front page with screenshots; review notes moved to notes/dev

This commit is contained in:
MineTec committed 2026-11-07 16:14:40 +01:00
1 parent 00b2880d67
commit 624718b893
41 files changed
+2175 -259

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
# Reference: game files and engine
Everything we know about the data of *The Emperor's New Groove* (PC, 2000; German: *Ein Königreich
für ein Lama*), found by reverse engineering `groove.exe` and checking every claim against the
level files on the CD and in the running game. The game runs on Argonaut's **Croc 2 engine** (a PC
port of a PlayStation design), which explains most of its quirks.
Confidence is marked where it matters: **certain** = verified in code and data (byte-exact round
trip or seen in game), **likely** = consistent with the data, **unknown** = open question.
| Page | What it covers |
|---|---|
| [Game files](game-files.md) | CD and HD folders, level naming scheme, worlds and chapters, loading pictures (COR), movies, demos, save games (KSV) |
| [WAD container](wad.md) | the chunk list of a level file and which chunk does what |
| [Geometry and collision](geometry.md) | TRAK tile models, the MAP tile grid, coordinate spaces, collision polygons and materials, vertex colours, visibility regions |
| [Objects, waypoints, zones](objects.md) | MAP strat records, parameters, waypoints, environment zones (fog, music, teleports), lights, the player start, checkpoints and the exit |
| [Textures](textures.md) | TEXT chunk: pages, pixel format, texture rectangles, texture animations |
| [Music and sound](audio.md) | ASF music, the level sound bank, speech, what is still unknown |
| [TestLevel (world 9)](testlevel.md) | the hidden world all custom levels live in, and which chapters are safe |
| [Engine facts and limits](engine-limits.md) | scale, jump distances, draw distance, memory budget, things that crash the game |
How objects *behave* (the strat bytecode) has its own section: [Strats](../strats/README.md).
## Where the details live
These pages are the readable summary. The full lab notes – with every address in `groove.exe`,
open questions and the history of each finding – are in [`notes/`](../../notes/README.md):
| Topic | Lab notes |
|---|---|
| geometry, collision, coordinate spaces | [notes/geometry.md](../../notes/geometry.md) |
| MAP layout, strat records, other chunks | [notes/objects.md](../../notes/objects.md) |
| textures | [notes/textures.md](../../notes/textures.md) |
| every object type in every level | [notes/strat_types_table.md](../../notes/strat_types_table.md) |
| object parameters | [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md) |
| world 9 chapters | [notes/testlevel-chapters.md](../../notes/testlevel-chapters.md) |
| level building, physics measurements, the exit | [notes/levelbau.md](../../notes/levelbau.md) |
| fog, lights, regions | [notes/editor-tools-environment.md](../../notes/editor-tools-environment.md), [notes/mod-fog.md](../../notes/mod-fog.md) |
| importing objects across levels | [notes/editor-cross-import.md](../../notes/editor-cross-import.md) |
## Code that reads and writes these formats
Every parser rebuilds all original files byte for byte, so the code is a reliable executable
specification.
| Format | Java (LlamaMod) | Python (kit) |
|---|---|---|
| WAD container | [`format/Wad.java`](../../src/main/java/com/eliasmueller/llamamod/format/Wad.java) | [`tools/wadtool.py`](../../tools/wadtool.py) |
| TRAK / STPC models, collision | [`format/Trak.java`](../../src/main/java/com/eliasmueller/llamamod/format/Trak.java), [`Model.java`](../../src/main/java/com/eliasmueller/llamamod/format/Model.java), [`Collision.java`](../../src/main/java/com/eliasmueller/llamamod/format/Collision.java), [`Stpc.java`](../../src/main/java/com/eliasmueller/llamamod/format/Stpc.java) | [`tools/geomlib.py`](../../tools/geomlib.py) |
| MAP | [`format/MapChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/MapChunk.java) | [`tools/mapchunk.py`](../../tools/mapchunk.py) |
| TEXT | [`format/TextChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/TextChunk.java) | [`tools/textchunk.py`](../../tools/textchunk.py) |
| whole level | [`format/Level.java`](../../src/main/java/com/eliasmueller/llamamod/format/Level.java) | – |
| strat code | [`format/strat/`](../../src/main/java/com/eliasmueller/llamamod/format/strat/), [`format/stpcmerge/`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/) | [`tools/strats.py`](../../tools/strats.py), [`tools/stpcwalk.py`](../../tools/stpcwalk.py) |
| COR pictures | [`format/CorImage.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorImage.java), [`CorEncoder.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorEncoder.java) | [`tools/cor2png.py`](../../tools/cor2png.py), [`tools/png2cor.py`](../../tools/png2cor.py) |
| KSV saves | [`save/Ksv.java`](../../src/main/java/com/eliasmueller/llamamod/save/Ksv.java) | – |
| ASF music | – | [`tools/asf2wav.py`](../../tools/asf2wav.py) (decode only) |
+73
View File
@@ -0,0 +1,73 @@
# Music and sound
Sound is the least explored part of the game. This page separates what is verified from what is
only a guess. Custom music or sound effects are **not supported yet** by any LlamaMod tool.
## Music
| Fact | Status |
|---|---|
| Music tracks are files `music/<NAME>.asf` on the CD (33 files) | certain |
| A level does not reference music files directly: **environment zones** carry a music id, and the start zone's id plays when the level begins | certain |
| Music ids map to names through a table in `groove.exe` at 0x578960 (20 bytes per entry, name pointer at +8), played by `FUN_00546b70`; id 0 stops the music | certain |
| ASF here is Argonaut's own format (not Microsoft ASF): 4-bit ADPCM, 44.1 kHz stereo, 17-byte blocks, decoded by `asfcodec.adl` in the game folder | certain (decoder reimplemented) |
| A second zone field (u16 at +0x56) is probably the ambience setting | likely |
The id table, as used by the editor's zone inspector
([`EnvMath.MUSIC`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/environment/EnvMath.java)):
| Id | Track | Id | Track | Id | Track | Id | Track |
|---|---|---|---|---|---|---|---|
| 0 | (stop) | 10 | GAMEOVER | 20 | SNKJUNG | 30 | TUNE1 |
| 1 | VILLAGE | 11 | TALLY | 21 | FROG | 31 | CHACHA |
| 2 | JUNGLE1 | 12 | TURRACE | 22 | KRONK | 32 | JJUNGLE2 |
| 3 | RIVER | 13 | PACHASNK | 23 | KIDLLAMA | 33 | TUNE9 |
| 4 | JUNGLE2 | 14 | MOUNT2 | 24 | TUNE8 | 34 | TUNE6 |
| 5 | MOUNTAIN | 15 | BALLOON | 25 | ROLLER | 35 | VICTORY |
| 6 | CITY | 16 | JUNGSPID | 26 | BUNNY | 36 | PUZZLE2 |
| 7 | CATACOMB | 17 | JAGCHASE | 27 | BLOCK | 37 | PLATFORM |
| 8 | LAB | 18 | RIVER | 28 | TUNE5 | 38 | (none) |
| 9 | TITLE | 19 | RIVRACE | 29 | CITYPUZZ | 39 | ENDGAME |
Four names (GAMEOVER, TALLY, PUZZLE2, PLATFORM) have no `.asf` file on the German CD; what the
game plays for them (probably nothing) was not checked.
**Changing the music of a level** works today: pick another id for the start zone (or any zone)
in the editor's *Environment* panel ([manual: Environment](../manual/12-environment.md),
[tutorial: Edit an original level](../manual/tutorial-original-level.md)).
**Listening to the tracks:** [`tools/asf2wav.py`](../../tools/asf2wav.py) `in.asf out.wav`
decodes a track exactly like the game's codec (filter 0: `s = s1 + (n << shift)`, filter 1:
`s = 2·s1 − s2 + (n << shift)`, 16-bit wrap-around instead of clipping – FFmpeg's decoder clips and
drifts).
**New music – open:** there is no encoder yet. Writing one (the inverse of `asf2wav.py`) and
replacing a track file through a mod would be the natural next step; the header fields after the
block count (`rate`, `flags` with bit 1 = stereo) are read by the decoder, the others are *unknown*.
## Sound effects
| Fact | Status |
|---|---|
| Each level has its own sample bank, chunk **SMPC**: `u32 n`, n × `{u32 "cvg " tag, u32 length, data}` | certain ([`SoundMerge.java`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/SoundMerge.java)) |
| Strats play samples by index (`soundPlay(sound:N)`, opcodes 0xd5 / 0x99 / 0x131 play, stop, is-playing); at most **128** samples per level, ids with bit 0x80 are the front-end sounds | certain |
| The samples are PlayStation-style VAG ADPCM, decoded by `Vagcodec.adl` | likely (tag and codec name; the sample header was not decoded) |
| **AMPC** holds ambient sound data (`ambi` / `VABp` blocks) | likely, not decoded |
| `FESOUND.WAD` is the front-end sound bank (not a chunk file) | likely |
Samples can be **moved between levels**: the editor's cross-world import and the arena copy the
samples an imported object needs into the target level and renumber the `sound:N` operands
([notes/editor-cross-import.md](../../notes/editor-cross-import.md)). Changing which sample an
object plays is a constant edit in its script ([Strats](../strats/custom-types.md)). Exporting or
replacing a sample's audio is not possible yet.
## Speech and dialogue
* **LGPC** holds the dialogue texts of a level in all languages plus a speech slot per line.
* A speech slot `#<hex>` is an index into **SRPC**, whose entries are offset/size pairs into the
global speech file `music/<language>.cvs` in the game folder (the German version ships
`english.cvs`).
* Strats show texts with opcodes 0xf0 / 0xfa (`topSay text:N`). Imported objects bring their
texts and speech references along.
The CVS file format itself was not analysed.
+67
View File
@@ -0,0 +1,67 @@
# Engine facts and limits
Numbers to design levels with, and the things that crash the game. Measured in the game with the
test harness unless noted.
## Scale and movement
One unit = one grid cell. Measured with the jump lab ([`levels/sprunglabor.py`](../../levels/sprunglabor.py))
and a coin measuring track ([notes/levelbau.md](../../notes/levelbau.md#findings-from-the-game-tests-lamatest-xvfb)):
| Quantity | Value |
|---|---|
| llama height | about 0.6 |
| walking speed | about 1.1 units/s (≈ 1 s to accelerate) |
| highest edge a jump clears | 0.35 (0.5 is too high) |
| long jump with a run-up | about 1.3 units (from standing: much shorter, a 0.5 gap fails) |
| comfortable parkour | gaps of 0.3 with +0.2 height, platforms ≥ 1.5 long |
| step-up without jumping | none – any edge with wall collision blocks; use slopes or walls without collision |
| frame rate of the logic | 30 frames per second (script speeds are per frame) |
Controls are **camera-relative**; the start camera sits behind the llama and moves aside if
something solid is right behind it – keep the area behind start points clear.
## Drawing
| Fact | Value |
|---|---|
| draw distance | = **fog end** of the active environment zone; originals 7–16 cells at detail *Maximum* (roller coaster up to 23), about half at *Minimum* |
| objects | drawn only within about **12.5 units of the llama** (they keep running); their activity radius also follows the fog end |
| hidden areas | visibility regions: a cell is drawn only if the camera region's mask contains its region |
| texture coordinates | none per vertex – every triangle shows half of a texture rectangle |
| lights | light only object models; tiles use baked vertex colours |
The *Draw distance* mod scales all fog values of all levels
([notes/mod-fog.md](../../notes/mod-fog.md)).
## Memory and counts
| Limit | Value | Source |
|---|---|---|
| level memory | **16 MB** for STPC, TEXT, TRAK, MAP, LGPC and the strat pool; originals use up to ~7 MB | `FUN_0041eea0` |
| strat heap | 65533 words (≈ 256 KB) for strat slots (0x154 bytes each), collision blocks and variables | `FUN_0041ae20` |
| strat slots | `poolExtra + 30` – must cover placed **and** spawned objects | MAP |
| vertices per tile model | 8000 (transform buffer) | |
| collision polygon | within ±8 units of its tile's origin | i16 fixed point |
| texture pages | 127 | signed page byte |
| texture entries | 65535 | u16 face field |
| sound samples | 128 per level | signed char id |
| file sizes that worked | TRAK up to 1 MB, 5800 tiles, WAD up to ~7 MB (originals); the test world: 628 models, 2474 tiles, 6.3 MB | measured |
## Things that crash the game
| Cause | Symptom |
|---|---|
| a tile outside the W × H grid | memory corruption, crash on load or later |
| vertex-colour block not exactly one entry per vertex | objects read shifted, crash |
| a strat type with collision but type flags 0 (or too few collision-bone words) | page fault in the heap allocator `FUN_0041ae20` |
| exit target (7, 7, 7) in world 9 | crash after saving (world 10 has no name) |
| a selectable chapter without a WAD | "Couldnt load wad" on the world map |
| a waypoint-dependent type without its boxes | not a crash: the game removes the object ("removing strat") |
| world-9 chapters 2–31 (unpatched exe) | progress, language or options overwritten ([TestLevel](testlevel.md)) |
## Coordinates, numbers, angles
* Game space: x east, y up, z = grid row; strats use 20.12 fixed point (4096 = 1.0).
* MAP angles: 4096 = 360°; strat angles: 0x1000000 = 360°.
* Objects on a flat original floor tile stand at `tile.y + 0.25`.
+99
View File
@@ -0,0 +1,99 @@
# Game files
What is on the CD and on the hard disk, how files are named, and the small formats around the
levels. The level files themselves are described in [WAD container](wad.md).
## Two folders: HD and CD
The installer copies a small **HD** part to the hard disk; everything big stays on the **CD**. The
game looks up each file in `HDPath\dir\file`, then in `CDPath\dir\file`, then on every CD drive
(`FUN_00415690`). A plain folder in `CDPath` is enough – no real CD drive is needed. LlamaMod relies
on this: it points `CDPath` at its own `cd-active` folder, where mod outputs replace original files
([notes/llamamod-app.md](../../notes/llamamod-app.md#registry-cd-path-and-the-exe-patch)).
| Folder | Contents |
|---|---|
| HD (`Setup/<lang>/Hd` on the CD) | `groove.exe`, `aal.dll`, `aleax2.3dl`, the audio codecs `asfcodec.adl` and `Vagcodec.adl`, `music/english.cvs` (speech), save games `king0.ksv`…`king2.ksv` |
| CD (`Setup/<lang>/Cd`) | `wads/` level files, `music/*.asf` music, `movies/*.dat` videos, `*.cor` pictures, `test9.tle`, empty folders `gallery/`, `in/`, `out/` |
Registry key (all values): `HKEY_LOCAL_MACHINE\Software\Disney Interactive\Emperors New Groove\1.0`
with `CDPath`, `HDPath`, screen size, volumes and devices. LlamaMod's base patch of `groove.exe`
redirects it to `HKEY_CURRENT_USER`, so no admin rights are needed.
Development used the **German CD**. Other language versions are expected to use the same formats
but were not tested.
## Level naming scheme
Levels are named **`t<tribe>l<world>m<chapter>`** (tribe is always 1 in the game):
| World | Name | Files | Notes |
|---|---|---|---|
| 1 | Village | t1l1m001–004 | `t1l1m001` is the default base level for new levels |
| 2 | Jungle at Night | t1l2m001–003 | t1l2m003 is the llama chase (player strat `LlamaChase`) |
| 3 | River | t1l3m001–004 | player strat `KuscoAndPachaTogetherOnATree` |
| 4 | Jungle by Day | t1l4m001–003 | t1l4m002 has the Alligator (used by the arena) |
| 5 | Mountain | t1l5m001–003 | |
| 6 | City | t1l6m001–003 | |
| 7 | Catacombs | t1l7m001–005 | m001/3/5 are roller-coaster rides (player strat `RollerCoaster`) |
| 8 | Lab | t1l8m001–006 | |
| 9 | TestLevel | none on the CD | 49 chapter slots, see [TestLevel](testlevel.md) |
Other WADs: `t0i0m000` (front end), `t0i0m998` / `t0i0m999` (outro and game-over backdrops),
`t6l6m001/002` (memory-card/options screens – *likely*), `FESOUND.WAD` (front-end sound bank, not
a chunk file). The chapter counts the world map uses are a table in `groove.exe` (0x572420):
4, 3, 4, 3, 3, 3, 5, 5, 49 – world 8 has one more file than its table entry (*unknown* why).
## Pictures (COR)
| File | Use |
|---|---|
| `T1<w><c>.COR` | loading picture of world *w*, chapter *c* (`T191.COR` … `T1949.COR` for TestLevel) |
| `<w><c><n>.cor` | concept art shown with a chapter (*likely*) |
| `tally<n>.cor`, `gameover.cor`, `loading.cor`, logos | screens; `gameovertest.cor` is unused |
Format (*certain*, [`tools/cor2png.py`](../../tools/cor2png.py)): 8-byte signature
`17 98 AF 89 FE 42 D1 12`, u32 version 1, u32 unpacked size, u32 packed size, then LZ data:
a byte `n < 0x80` = *n* literal bytes follow; `1LLLoooo oooooooo` = copy L+3 bytes from *o* bytes
back. The result is 640×480 RGB (a few greyscale ones). [`tools/png2cor.py`](../../tools/png2cor.py)
and [`CorEncoder.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorEncoder.java) write
it; LlamaMod uses that for the loading pictures of new levels.
## Movies and demos
* `movies/<w><c><n>intro.dat` / `…outro.dat` and the logo movies: MPEG-1 video 320×240, 29.97 fps,
MP2 audio, played through DirectShow. The Intro skip mod patches the calls that start them
([notes/mod-introskip.md](../../notes/mod-introskip.md)).
* `wads/*.DEM`: recorded attract-mode demos, u32 frame count + 8 bytes of input per frame
(*likely*; not used by LlamaMod).
## Save games (KSV)
`king0.ksv`…`king2.ksv` in the game folder, 256 bytes each. Decoded from the load/save functions
`FUN_0041c640` / `FUN_0041c820`; code: [`save/Ksv.java`](../../src/main/java/com/eliasmueller/llamamod/save/Ksv.java).
| Offset | Meaning |
|---|---|
| 0x00 | used flag (0 = empty) |
| 0x01 / 0x02 | lives / energy |
| 0x03–0x06 | world-map cursor: tribe, world, chapter, map |
| 0x07 | number of gold coins |
| 0x08–0x3D | u8 [9][6] chapter flags (bit 0 collectible found, bit 1 all coins = gold coin) |
| 0x3E–0x41 | last played tribe, world, chapter, map |
| 0x42 / 0x43 | highest unlocked world / chapter |
| 0x44–0xAF | u16 [9][6] coins per chapter |
| 0xB0–0xCF | unknown |
| 0xD0–0xD9 | options (effects and music volume, speaker mode, …) |
To start a TestLevel chapter without a mod, set the cursor, the last played and the highest
unlocked world/chapter to 9 and the chapter. LlamaMod does this with `llamamod --unlock <slot> 9 <chapter> --cursor`
and with the *Save games* tab.
## Hidden debug features
Strings and code in `groove.exe` point to leftovers of the developers' tools. They were seen in the
binary but **not** verified or used by LlamaMod: F6 writes a screenshot `capture<n>.tga`, the
command-line switch `-display_diagnostics` writes a `<PC name>.dispdiag` file, a log
`c:\kingdom_dbg.txt`, and an unused slide-show player that reads the empty `in`/`out` folders.
The *Built-in cheats* mod uses the game's own cheat codes
([notes/mod-cheats.md](../../notes/mod-cheats.md)).
+155
View File
@@ -0,0 +1,155 @@
# Geometry and collision
The world is **not** one big mesh and there is no BSP. It is a **grid of tiles**, inherited from
Croc: TRAK holds a set of small models (the level's construction kit), and MAP places them on a
W × H grid, one cell = one unit. Every byte described here is reproduced exactly by
[`format/Trak.java`](../../src/main/java/com/eliasmueller/llamamod/format/Trak.java),
[`format/MapChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/MapChunk.java) and
[`tools/geomlib.py`](../../tools/geomlib.py) for all original levels.
Full detail with all loader addresses: [notes/geometry.md](../../notes/geometry.md).
![The test world from above in the editor: every square is one grid cell](../img/testworld-top.jpg)
## Coordinate spaces
| Space | Used by | Axes |
|---|---|---|
| **Game space** | strats, collision, MAP fixed-point tile positions, lights | x east, **y up**, z = grid row (positive); 20.12 fixed point (4096 = 1.0); right-handed as the game shows it |
| **Export space** | TRAK/STPC vertices, MAP float tile positions | same, but **z negated** (grid at z ∈ (−H, 0]); drawn by a Direct3D (left-handed) camera |
| **File space** | OBJ/glTF of every LlamaMod and kit tool | = game space (y up, right-handed): Blender shows a level the same way round as the game |
Converting export ↔ file space negates z and reverses the triangle corner order. Angles:
4096 = 360° in MAP records; the strat VM uses 0x1000000 = 360°. A tile is turned only about y
(0, 1024, 2048, 3072 in practice). Rotation 0 faces +z, 90° faces +x.
## TRAK: tile models (*certain*)
`u32 n`, then **all n headers** (0x84 bytes each), then the data of every model in order.
Typical levels have 100–650 models with up to 422 vertices / 798 faces each.
| Header field | Meaning |
|---|---|
| 0x00 | bounding-box centre (3 × f32) |
| 0x0c | 8 box corners (used for frustum culling) |
| 0x6c / 0x6e | vertex count / face count |
| 0x78 / 0x7a / 0x7c | collision polygon counts in three groups (the engine always uses the sum; meaning of the groups *unknown*) |
| pointers | 0 in the file, set on load |
Data per model:
* **Vertex**, 24 bytes: `f32 x, y, z, nx, ny, nz` (lighting comes from the MAP vertex colours, not
from the normals).
* **Face**, 28 bytes, triangles only: `u16 flags, u16 v0, v1, v2, u16 texture, u16 0, f32 plane
nx, ny, nz, d`.
* **Collision polygon**, 32 bytes: `u8 type, u8 material`, then 5 planes `{i8 nx, ny, nz, u8 0,
i16 d}`. Plane 0 is the surface, planes 1–4 the edges (normals point inwards). Type bit 0 =
quad, otherwise triangle. Normals are ×127, d is 20.12 fixed point – so a collision polygon
must lie within **±8 units of its tile's origin**.
### Face flags
| Bit | Meaning |
|---|---|
| 0x0008 | start of a new render batch (texture page or blend mode changes) – must be set on the first face of each batch, at the latest every 256 faces |
| 0x0010, 0x0020, 0x0800 | which corners of the texture rectangle the triangle uses (below) |
| 0x0400 | double-sided (no backface culling) |
| 0x4000 | ignore vertex colours |
| 0x0001, 0x0002 | special sorting/transparency modes (*likely*) |
| others | present in the data, not read by the PC renderer (*unknown*, probably PlayStation leftovers) |
### No free texture coordinates
The engine has **no per-vertex UVs**. Every triangle shows one half of a texture rectangle from the
TEXT table: with flag 0x800 the face is the first or (with 0x10) second half of a quad, without it
a triangle covering the lower-left half; 0x20 mirrors horizontally. 96 % of world faces are quad
halves. This is why the level builder and editor split surfaces into 1 × 1 pieces and why a narrow
wall shows a squashed texture. See [Textures](textures.md).
## MAP: the tile grid (*certain*)
MAP starts with the grid and the tiles; objects follow (see [Objects](objects.md)). The blocks that
concern geometry:
| Block | Format |
|---|---|
| header | `u32 nTiles, u32 W, u32 H` |
| tiles (floats) | nTiles × `{f32 x, y, z, u32 rotY, u32 model, u32 hasAlt}` in export space; x, z = cell centre |
| regions | 32 × `{u32 visibility mask, i32 value}`, then W·H × u32 region number per cell |
| tiles (fixed point) | nTiles × `{rx, ry, rz, x, y, z}` 20.12 in game space – used for drawing and collision; must match the float copy exactly |
| model index | nTiles × u32 (= the float block's `model`) |
| tile swaps (WFPC 0x10000) | one `{cellX, y, cellZ, modelA, modelB}` per tile with `hasAlt ≠ 0`: doors and bridges that change their model (strat opcodes 0x11a / 0x11b) |
| vertex colours | per tile: one u32 RGBA per vertex of its model (0x80 = 1.0, the game doubles and clamps); more sets for animated colours; then the same for the swap model |
Rules that crash the game or corrupt the level when broken:
* every tile must lie inside the grid (`0 ≤ x < W`, `0 ≤ row < H`) – outside, the cell list
writes into foreign memory;
* the vertex-colour block must have **exactly** one entry per vertex of the tile's model – else
everything after it (the objects) is read shifted;
* the float and fixed-point copies and the model index must agree;
* tile swaps: one entry per tile with `hasAlt ≠ 0`, in tile order.
The cell of a tile is `floor(x) + floor(−z) · W` (export-space floats).
## Collision (*certain*)
* Only **the tiles listed in the cell the player stands in** are tested (`FUN_004036d0` floors and
ceilings, `FUN_00403ad0` walls). A wall polygon that sticks into the neighbouring cell has no
effect there – builders enter a polygon into every cell it covers, as a collision-only tile if
needed.
* Collision is in game space, relative to the tile's fixed-point position.
* There is **no step-up logic**: a 0.12-unit edge with a wall polygon already blocks the llama.
Low plinths need walls without collision; stairs need an invisible slope.
### Collision materials
The material byte ends up in the player's ground state and is read by strats (`floorMaterial`).
| Material | Meaning |
|---|---|
| 0 | normal ground |
| **16** | **exit floor**: the LevelDoor ends the level only when the llama stands on material 16 in the door's cell; also used under the village house doors (zone teleports) |
| 17, 18 | ignored when WFPC flag 0x8000000 is set (*likely*: special surfaces) |
| **30** (0x1e) | death plane / water (*likely*): falling onto it empties the energy and the llama respawns at the last checkpoint or the start (Model 0 of many levels is such an invisible plane at y = −2.75) |
| others | special surfaces, meaning *unknown* – the debug view colours them |
![Debug view of Village 3: collision frames, invisible walls in red, objects as markers](../img/debug-view.jpg)
## Visibility regions (no BSP)
Drawing (`FUN_0042bf40`) walks the grid cells within the draw distance around the camera and draws
a cell's tiles **only if the cell's region bit is set in the mask of the camera's region**. Up to 32
regions; mask 0 or a camera outside the grid means "everything". New geometry must therefore get a
region that the surrounding regions can see, or it stays invisible – the builders put everything
into region 0 with mask `0xffffffff`. Objects in hidden cells are skipped too (unless strat flag
0x800). The editor's *Visibility regions* tool edits this
([manual chapter 13](../manual/13-visibility.md)).
## STPC: object models and code
STPC starts with the **object models** (same model format as TRAK, header 0x8c bytes, optionally
skinned with a bone table), then the **animation sets** (per-frame bone matrices and compressed
vertex deltas – the compression is only partly understood), then the **strat code units**
([Strats](../strats/README.md)). The engine never parses the code; MAP records and bytecode refer
into STPC by absolute byte offsets, so STPC can only be extended at the end or merged with
relocation ([notes/editor-cross-import.md](../../notes/editor-cross-import.md)).
## WFPC flags
WFPC is a u32 of feature flags. Some change the data layout and must stay as they are: 0x10,
0x100, 0x10000 (tile swaps), 0x100000, 0x200000, 0x4000000, 0x10000000. Others: 0x800 renderer
fog flag, 0x80000 read by a strat opcode, 0x8000000 makes collision materials 17/18 ignored.
## Writing new geometry – checklist
1. TRAK: correct counts, bounding box and face planes recomputed, texture index below the
number of texture entries, batch bit 0x8, ≤ 8000 vertices per model.
2. Collision polygons within ±8 units of the tile origin, entered into every cell they cover.
3. MAP: tiles inside the grid, float/fixed/model blocks consistent, vertex colours exactly per
vertex, tile-swap block matching `hasAlt`, a visible region.
4. Rewrite the WAD's total length.
The editor and both level builders do all of this; the details of the builder's approach
(splitting into tiles per cell and height band, merging identical tiles, baking sun, sky and
ambient occlusion into vertex colours) are in [notes/levelbau.md](../../notes/levelbau.md).
+141
View File
@@ -0,0 +1,141 @@
# Objects, waypoints, zones and lights
Everything that is not static geometry lives in the second half of the MAP chunk: object
instances ("strats"), their parameters, waypoint chains and environment zones. Lights are in their
own chunk, LGHT. Full byte layouts: [notes/objects.md](../../notes/objects.md).
## Strats: objects are programs
An object – coin, enemy, door, camera, the player – is an instance of a **strat**: a script in the
level's STPC code. A MAP record says *where* and *which code*; the code does the rest. There is no
type id: **the type is the code's byte offset in STPC**. Type names come from the code itself
(a `debugName "Coin"` instruction at the entry point). How the code works: [Strats](../strats/README.md).
About 300 types are placed in the original levels, about 200 more are only spawned by other
strats (splinters, effects, heads). The full list of which type appears in which level, with
instance counts, is [notes/strat_types_table.md](../../notes/strat_types_table.md)
(`tools/strat_table.py`).
## The MAP object blocks
After the geometry blocks ([Geometry](geometry.md#map-the-tile-grid-certain)):
| Block | Format |
|---|---|
| parameter pool | `u32 n, n × u32` – all object parameters of the level, referenced by index |
| environment zones | `u32 n, n × 90 bytes` (below) |
| waypoints | `u32 n, n × 34 bytes {u32 next, u32, u16 rx, ry, rz, s32 x, y, z, u32, u32}`; `next ≠ 0` = the following entry belongs to the same chain |
| strats | `u32 nStrats, u32 poolExtra`, nStrats × 58-byte records |
| optional tail | paths (WFPC 0x200000, unused by the CD levels), a buffer size (WFPC 0x10), a u16 |
### Strat record (58 bytes, *certain* unless noted)
| Offset | Field |
|---|---|
| 0 | rotation x, y, z (u16 each, 4096 = 360°; x/z almost always 0) |
| 6 | position x, y, z (s32, 20.12 fixed point, game space) |
| 18 | **code offset in STPC = the type** |
| 22 / 26 | parameter count / index into the parameter pool (−1 if none) |
| 30 | number of strat variables (fixed per type) |
| 34 | 16-byte slots for event handlers (fixed per type, *likely*) |
| 38 | type flags: collision and physics setup (fixed per type) |
| 42 | extra memory words (collision bones; fixed per type) |
| 46 | first waypoint index or −1 |
| 54 | flags; bit 1 = not created at level start |
`poolExtra` is the number of strat slots beyond 30: the engine allocates `poolExtra + 30` slots,
which must cover all placed objects **plus everything spawned at run time**.
The type fields (variables, slots, flags, extra) come from the compiler and must be copied from an
original instance of the same type; a type that uses collision but has type flags 0 crashes the
game. Builders and the editor do this automatically.
### Parameters
The *n* parameters of an instance are pushed onto the new strat's stack and become its
**variables v0 … v(n−1)**. Values are 20.12 fixed point by convention (0x1000 = 1.0), booleans
0 / 0x1000, counts and world/chapter numbers `n << 12`. The editor knows the meaning of the
parameters of many types (schemas in
[`editor/tools/objects/Schemas.java`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/objects/Schemas.java),
findings in [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md)); the others are
shown raw with the values used in the original levels.
### Waypoints
A waypoint chain is a list of positions used for patrol paths, moving platforms, camera paths –
and for some types (guards, box walkers) as a **pair of boxes** the object must stand in, or the
game removes it ("removing strat"). Guards also wait for an **event bit** (global variable 95) that
another object sets ([notes/editor-object-schemas.md](../../notes/editor-object-schemas.md#event-bits-global-95)).
The **player's waypoint chain is the start camera**: the camera takes the first node within 2.5
cells of the llama. A good start has one node about 2 cells behind the llama and 0.5 above it, and
nothing solid right behind the llama (the camera moves aside).
## Special objects
| Role | Type | Notes |
|---|---|---|
| player start | `Llama` | exactly one per level (`RollerCoaster`, `KuscoAndPachaTogetherOnATree`, `LlamaChase` in special levels) |
| checkpoint | `CheckPointPost` (spawns `CheckPointHead`), `CheckPointBox` | the respawn point after a fall |
| exit | `LevelDoor` | invisible trigger, see below; variants `VillageEndDoor`, `EndGameDoor` |
| cameras | `CameraSide`, `CutSceneCameraDirectional`, `CutSceneTrigger` | camera zones and cut scenes |
| always present | `CheatTeleport`, `Wompy`, `CutSceneTrigger` | |
### The exit (LevelDoor) – *certain*
The LevelDoor has no model. Every frame it checks:
```
game state is "running"
and the floor polygon under the llama has collision material 16
and floor(llama.x) == floor(door.x) and floor(llama.z) == floor(door.z) ; same grid cell
-> endLevel(tribe, world, chapter) ; its three parameters
```
So an exit needs **a floor with collision material 16 in the door's grid cell** – height does not
matter. In several original levels the pad only appears when a tile swap opens the door (Village 1:
the RubyIdolDoor swaps the tile to a model that has the material-16 pad). The parameters are the
next level; the originals use (1, world, chapter + 1), and **(7, 7, 7)** in the last chapter of a
world means "world finished" – in TestLevel that crashes, so custom levels use (1, 9, chapter).
`endLevel` shows the tally (coins x / LNFO total, secrets) and unlocks the next chapter.
A second path in the Llama's own code ends the level when it stands on material 16 in the cell
stored in globals g104/g105 with the target in g101–g103 – the arena uses this
([Arena](../strats/arena.md)).
## Environment zones (90 bytes, *certain* for the fields below)
A zone sets fog, colours, music and sky, and optionally teleports the player.
| Field | Meaning |
|---|---|
| cell x, y, z | the trigger cell |
| target x, y, z, angle | teleport target and facing |
| flags (+0x20) | flag 8 = **start zone** (exactly one per level: the level begins with its settings); flag 4 = do not move the player |
| sky model | STPC offset of a sky model (0 = none) |
| fog mode | 0 none, 1 darkness with a light around the player, 2 table fog, 3 vertex fog |
| fog start / end | two pairs (detail Minimum and Maximum); **fog end = far clip = draw distance** |
| colours | ambient colour for object lighting, fog/backdrop colour |
| music id | 1–39 (see [Music](audio.md#music)), 0 stops the music |
Teleports are triggered from strat code (opcode 0x100 looks up the zone of the calling object's
cell). In the original levels the trigger cells hold exit floors (material 16) or door objects –
this is how the village house doors work. The editor edits zones in the
*Environment* panel ([manual chapter 12](../manual/12-environment.md)).
## Lights
LGHT: `u32 n`, then per light a type, a colour (×2/255) and direction or position (game space).
Lights only light **object models** (ambient + all directional lights + the nearest point lights,
at most 6). Tiles use baked vertex colours, so changing a light does not change the ground – bake
the vertex colours again. Point lights are full up to radius *a* and fade to 0 at *b*; one byte
makes them flicker.
## Tools
| Task | Tool |
|---|---|
| list types, instances, parameters of a level | [`tools/strats.py`](../../tools/strats.py) `level.wad [--all]`, [`tools/map_dump.py`](../../tools/map_dump.py) `--strats --tiles --lights` |
| type × level table | [`tools/strat_table.py`](../../tools/strat_table.py) |
| place objects from a script | [`tools/levelbuild.py`](../../tools/levelbuild.py) `L.strat(...)`, Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java) |
| place, edit, check objects visually | the level editor ([manual chapter 9](../manual/09-objects.md), [10](../manual/10-waypoints.md), [11](../manual/11-cameras-triggers.md)) |
+51
View File
@@ -0,0 +1,51 @@
# TestLevel (world 9)
The game has a hidden ninth world, **TestLevel**, with 49 chapter slots – but the retail CD ships
no data for it. It is the natural home for custom levels: originals stay untouched, and the world
map can reach it once a save game unlocks it. Every LlamaMod level mod, the Test World and the
Alligator Arena are installed as `wads/T1L9M0nn.wad` with a loading picture `T19n.COR`.
Code: [`save/TestLevelChapters.java`](../../src/main/java/com/eliasmueller/llamamod/save/TestLevelChapters.java),
lab notes: [notes/testlevel-chapters.md](../../notes/testlevel-chapters.md).
## Use chapters 1 and 32–49
The game keeps per-chapter coin counters and flag bytes in arrays that only have rows for worlds
0–8 (index `world · 6 + chapter`). For world 9, chapters 2–31 land on **other game variables**:
| Chapter | What its coin counter / flag byte overwrites |
|---|---|
| 1 | nothing (free) |
| 5–8 | the last played tribe/world/chapter/map |
| 6–9 (flags) | the **language index** |
| 9 | the **furthest progress** – the world map breaks |
| 10–31 | options, HUD state and tables used by the original levels |
| **32–49** | tables of tribes 2 and 3, which the game never uses – free |
So custom levels use **chapter 1 or 32–49**. The editor only offers these, new projects take the
first free chapter from 32, and old projects on other chapters are moved automatically.
LlamaMod additionally relocates both arrays to a 128-entry buffer in unused space of `groove.exe`
(base exe patch, revision 2), so even picking an empty chapter on the world map is harmless with
LlamaMod's executable. Mods still keep to 1 and 32–49 so they also work with an unpatched exe.
## Making world 9 reachable
* **Empty chapters must exist.** Finishing chapter *n* unlocks chapter *n* + 1, and the world map
crashes ("Couldnt load wad") when a selectable chapter has no file. LlamaMod links every missing
`T1L9M002…049.wad` to an installed world-9 WAD and `T192…T1949.COR` to `T191.COR`.
* **Never exit to (7, 7, 7).** "World finished" in world 9 unlocks a world 10 that has no name;
the game crashes after saving. Exits in TestLevel point to (1, 9, chapter).
* **Get there:** set a save game's map cursor to world 9 (`llamamod --unlock <slot> 9 <chapter>
--cursor`, the launcher's *Save games* tab), or use the *Boot to level* mod / the editor's
play-test, which start the game directly in the level.
## Tally totals
The tally screen after the exit shows "coins collected *x* / *total*". *Total* is not counted by
the game – it is the **LNFO** chunk (`u32 coins, u32 secrets`). A new level built on the village
would show the village's 55. LlamaMod recounts it for new levels (Coin = 1, Coin5 = 5, Secret
objects; the arena adds its dynamic coins).
Cosmetic leftovers: the world map shows no background for world 9 and a garbage coin target, and
the loading screen says "TestLevel Kapitel *n*".
+53
View File
@@ -0,0 +1,53 @@
# Textures (TEXT chunk)
Each level carries its own textures. Parse and rebuild are byte-identical for all level WADs
(including the RLE encoder). Code: [`format/TextChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/TextChunk.java),
[`tools/textchunk.py`](../../tools/textchunk.py). Lab notes: [notes/textures.md](../../notes/textures.md),
editor side: [notes/editor-tools-textures.md](../../notes/editor-tools-textures.md).
## Layout
```
u32 nPal 0 in all WADs (8-bit palettes are unused)
u32 nPages 13–22 per level, all 256×256
nPages × { u32 flags, u32 w, u32 h, data }
flags & 0x80 : u32 length + RLE16 (all original pages)
other ≠ 0 : w·h raw u16
u32 nTex ~180–1260 texture entries
nTex × { u16 flags, u8 page, u8 clut (0xff), u8 x0, x1, y0, y1 } (edges inclusive)
u32 nAnim texture animations
nAnim × { u16 texIndex, u8 type, payload } type 8 = flip book: list of texture indices
```
RLE16: a sequence of s16 *n*; *n* > 0 → *n* literal pixels follow, *n* < 0 → the next pixel
repeated −*n* times.
## Pixels
16 bit in **PlayStation order**: bits 0–4 red, 5–9 green, 10–14 blue, bit 15 = STP
(semi-transparency). `0x0000` is fully transparent. Export to PNG uses alpha 0 = `0x0000`,
alpha 128 = STP 0, alpha 255 = STP 1. The page flags (low 7 bits) select the blend class:
1/6 opaque, 4/7 alpha blend, 5 another blend mode, 2/3 special converters (*likely* the
PlayStation semi-transparency modes; details open).
## Texture entries
A face never has its own UVs: it names a **texture entry**, a rectangle on a page, and shows half
of it ([Geometry](geometry.md#no-free-texture-coordinates)). Entry flag bit 0 = flat colour (the
rectangle bytes are R, G, B instead) – the level builder appends such entries for plain colours.
Other entry flag bits (blend mode, size class) are only partly understood.
Limits: at most **127 pages** per level (signed page byte) and 65535 entries (u16 face field).
## Changing textures
* **Replace pixels, keep the table.** Exporting pages to PNG and importing them back changes no
index anywhere: `tools/text_export.py export level.wad dir` / `import level.wad dir new.wad`.
* **Add pages and entries.** New pages and entries are appended; faces then use the new indices.
The editor's texture manager does this, including colour reduction, painting onto faces and
flip-book animations ([manual chapter 8](../manual/08-textures.md)).
* **Same name, same picture.** Texture *indices* differ between base levels. The builders resolve
texture names (`grass`, `stone`, …) by picture fingerprint on each base and copy missing
village pictures over ([notes/levelbau.md](../../notes/levelbau.md)).
![Contact sheet of the Village 1 textures with their indices, made by tools/texsheet.py](../img/village-textures.jpg)
+63
View File
@@ -0,0 +1,63 @@
# WAD container
A level is one file `wads/t1l<world>m<chapter>.wad`. It is a flat list of chunks:
```
u32 total length
repeat: u32 tag (four ASCII letters stored reversed: "OFNI" in the file = INFO)
u32 length
length bytes of chunk data
```
There are no checksums. When a chunk changes size, only the chunk length and the total length
must be rewritten ([`format/Wad.java`](../../src/main/java/com/eliasmueller/llamamod/format/Wad.java),
[`tools/wadtool.py`](../../tools/wadtool.py) – `wadtool.py list|extract|replace`).
## Chunks
In file order. "Loader" is the function in `groove.exe` that reads the chunk (dispatcher at
0x558960).
| Chunk | Contents | Documented in |
|---|---|---|
| INFO, VERS | u32 1 each, ignored | – |
| WFPC | u32 feature flags of the level; they switch optional blocks in MAP and STPC on and off – never change them | [Geometry](geometry.md#wfpc-flags) |
| TEXT | texture pages, texture rectangles, texture animations | [Textures](textures.md) |
| FONT | 256 × 4 u16 glyph rectangles | – |
| SPRT | sprite list (u32, more with WFPC 0x100000) | – |
| NAME | u32 + level name (not shown anywhere in the game) | – |
| SMPC | sound samples of the level (`cvg ` blocks) | [Music and sound](audio.md) |
| AMPC | ambient sound data (`ambi`/`VABp`) | [Music and sound](audio.md) |
| SRPC | references into the speech file `music/<lang>.cvs` | [Music and sound](audio.md) |
| **TRAK** | world geometry: the tile models (render triangles + collision polygons) | [Geometry](geometry.md) |
| **STPC** | object models, skeletal animations and the **strat code** (object behaviour) | [Geometry](geometry.md#stpc-object-models-and-code), [Strats](../strats/README.md) |
| **MAP** | the tile grid, vertex colours, visibility regions, objects, waypoints, environment zones | [Geometry](geometry.md), [Objects](objects.md) |
| LGHT | lights (directional and point lights for object models) | [Objects](objects.md#lights) |
| LNFO | u32 coins, u32 secrets: the totals shown on the tally screen | [TestLevel](testlevel.md#tally-totals) |
| LGPC | dialogue texts in all languages + speech slots | – |
| END | end marker | – |
Not every level has every chunk. The three chunks that matter for level building are **TRAK**,
**STPC** and **MAP**; a new level takes everything else (textures, sounds, sky, fog, music) from an
original **base level**.
## The base-level idea
An object type exists in a level only if its code is in that level's STPC. MAP refers to a type
by its **byte offset into STPC** – there is no global type id. So every custom level starts as a
copy of an original level (default: Village 1, `t1l1m001`) and replaces geometry and objects:
* Python [`tools/levelbuild.py`](../../tools/levelbuild.py) and Java
[`levelbuild/LevelBuilder.java`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java)
generate TRAK and MAP from a script and keep STPC, TEXT and sound of the base.
* The level editor stores only the difference to the base level and rebuilds the WAD from the
user's own CD ([notes/editor-format.md](../../notes/editor-format.md)).
* Object types of *other* levels can be merged into the base's STPC together with their models,
animations, textures and sounds – the editor's *All worlds* import and the arena use this
([notes/editor-cross-import.md](../../notes/editor-cross-import.md)).
## Level memory
The game loads STPC, TEXT (compressed pages + 20 bytes per texture entry), TRAK, MAP, LGPC and the
strat pool into one **16 MB** block (`FUN_0041eea0`); original levels use up to about 7 MB. See
[Engine facts and limits](engine-limits.md).