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

+98 -237
View File
@@ -1,268 +1,129 @@
# levelbau – level modding kit for *The Emperor's New Groove* (PC, 2000)
# LlamaMod – mods and a level editor for *The Emperor's New Groove* (PC, 2000)
German title: *Ein Königreich für ein Lama*. PC port by Argonaut, built on the Croc 2 engine.
This kit came out of an AI-assisted reverse-engineering project. It contains:
**LlamaMod** is a one-click mod manager and a full 3D **level editor** for the PC game, for
**Windows and Linux**. It installs the game from your own CD or ISO into its own folder, applies
mods without ever touching the original files, and lets you build new levels, edit the original
ones and share them as small files. Everything is based on a complete reverse engineering of the
game's level formats and its object scripts ("strats").
* **Format documentation** for the level files (`*.wad`): geometry, collision, object
placement, textures, waypoints, visibility zones, and what makes the level exit work.
* **Python tools** that read, export and rebuild those files byte-exactly.
* **A level builder** (`tools/levelbuild.py`). You describe a level as a short Python script
(boxes, ramps, stairs, fences, trees, sea, objects) and it writes a complete, playable WAD.
* **An example level, "Testwelt"** (`levels/testwelt.py`). It goes into the hidden, empty
*TestLevel* slot (world 9, chapter 1) and has its own geometry, a gallery with every
object type on pedestals, an animal pen and a small parkour with a working exit.
* **A debug patch generator** (`tools/debugviz.py`). It makes collision visible in every
level, including invisible walls, along with invisible trigger and camera objects.
* **An automated in-game tester** (`tools/lamatest.py`). On Linux it runs the game under
Wine on a virtual display, loads the test level, takes screenshots and detects crashes.
| | |
|---|---|
| ![LlamaMod launcher](docs/img/launcher.jpg) | ![Level editor](docs/img/editor.jpg) |
| **Launcher**: mods with options, save-game editor, one-click play | **Level editor**: brushes, tiles, meshes, objects, textures, play-test with F5 |
| ![Test World gallery](docs/img/testworld-gallery.jpg) | ![Alligator Arena](docs/manual/img/mod_arena.jpg) |
| **Test World**: all 253 figures and objects of the game in one level | **Alligator Arena**: a minigame with new, scripted enemies |
| ![Debug view](docs/img/debug-view.jpg) | ![glTF in Blender](docs/img/gltf-blender.jpg) |
| **Debug view**: collision and invisible objects in the running game | **glTF/OBJ** export and import for Blender |
Nothing from the game itself is included: no game files, no executable, no decompiled
code, no built WADs. Every tool works on **your own copy** of the game.
## Features
> The notes in `notes/` are detailed and technical. Handing them to an AI assistant is the
> recommended way to use this kit (see the last section).
* **Mods** – tick, *Apply*, *Play*: Test World, Alligator Arena, your own levels, Boot to level,
Intro skip, Mirror mode, Practice tools (save/restore position, extra checkpoints), Randomizer,
Draw distance, Built-in cheats, Debug wireframe, Free camera & noclip.
* **Level editor** – new levels from templates or editing the original levels; brushes, the base
level's tiles, OBJ/glTF meshes; textures (import, paint, animate); objects from **every world**;
waypoints, cameras, triggers; fog, music, lights, visibility regions; strat scripts (read,
change numbers, write new object types); checks for problems; **F5** starts the game directly in
the level.
* **Sharing** – a level is a small `.llamalevel` file with only your changes and no game data; the
receiver's LlamaMod builds it from their own copy. New levels live in the hidden *TestLevel*
world.
* **Save games** – unlock worlds and chapters, coins, collectibles; backups before every change.
* **Safe** – the original CD/ISO and files are never changed; *Restore original* switches all mods
off.
---
## Get it
## Requirements
You need **your own copy of the game** (CD or ISO image). Development used the German CD; other
language versions should work but are untested.
* Your own copy of the game. Development used the **German CD version**. Other language
versions should use the same format, but this hasn't been tested.
* Python 3.8+ with `numpy` and `Pillow`. Optional: `capstone` (disassembler helper).
* For the automated in-game tests only: Linux, Wine, Xvfb, xdotool, ImageMagick, tesseract
with German language data, and PulseAudio (`pactl`).
* Optional: Blender (preview renders), and Ghidra (to regenerate the decompile, see below).
The builder and the export tools are plain Python, so they should also work on Windows.
The test harness is Linux/Wine-only.
### Paths – adjust these
The tools were written for one Linux/Wine setup and some paths are hard-coded at the top of
the scripts:
| Script | Variable | Meaning |
| System | Package | Start |
|---|---|---|
| `tools/levelbuild.py` | `WADS` | folder with the original level WADs (the `wads` folder of the game CD) |
| `tools/levelbuild.py` | `MODWAD` | where `--install` puts the built level (`T1L9M001.wad`) |
| `tools/debugviz.py` | `CD_WADS` | same as `WADS` |
| `tools/lamatest.py` | `PREFIX`, `LAMA_MOD` | Wine prefix and the overlay script |
| Windows | `LlamaMod-<version>-full-windows-x64.zip` | unzip, start `LlamaMod.exe` (no installation, no admin rights) |
| Linux | `LlamaMod-<version>-full-linux-x64.tar.gz` | unpack, start `./LlamaMod`; install **Wine** to run the game |
The game reads its level data from the CD drive, in the `wads` folder of the CD. To mod it,
make a copy of the CD data (or an overlay of symlinks, see `tools/lama-mod`), point the CD
drive or `CDPath` at that copy, and put modified WADs into its `wads` folder.
Both bring their own Java runtime; the editor needs OpenGL 3.3. A *launcher* edition without the
editor is also built. What changed: [CHANGELOG.md](CHANGELOG.md).
---
### Build from source
## Folder layout
Needs a JDK 25 (toolchain) and Gradle running on Java 21 (for Spotless):
```
README.md this file
notes/ format documentation and findings
geometry.md TRAK/MAP geometry, collision polygons, coordinate spaces, zones, limits
objects.md MAP chunk layout, strat (object) entries, waypoints, player start, exit
strat_types_table.md every object type in every original level, with counts
textures.md TEXT chunk: texture pages, RLE16, texture table, animations
levelbau.md how to use levelbuild.py, measured player physics, exit logic
testharness.md the automated Wine/Xvfb tester
debugviz.md debug patch: colour legend, what is shown, limits
img/ screenshots and preview renders
tools/
geomlib.py core library: WAD, TRAK, STPC models, MAP geometry, TEXT, collision
geom_export.py WAD -> OBJ/MTL (+ textures, vertex colours, collision.obj, objects.obj)
geom_import.py OBJ -> new geometry inside a WAD copy
mapchunk.py MAP chunk parser/writer (byte-exact on all original levels)
strats.py, strat_table.py, stpcwalk.py, map_dump.py object types and strat code helpers
textchunk.py, text_export.py, texsheet.py textures: export/import PNG, contact sheets
levelbuild.py the level builder (see notes/levelbau.md)
debugviz.py debug patch generator (collision/trigger visualisation)
lamatest.py automated in-game test on a virtual display (Linux/Wine)
lama-mod shell script: builds a CD overlay with modded WADs (Wine setup)
vorschau/ Blender preview render scripts
disx86.py small disassembler helper for groove.exe (needs capstone)
wadtool.py minimal WAD chunk lister/extractor/replacer (used by several tools)
cor2png.py, png2cor.py loading/concept pictures (*.COR, LZ-compressed 640x480) <-> PNG
asf2wav.py music (*.ASF, 4-bit ADPCM) -> WAV
levels/
testwelt.py the example test world
testwelt_parkour.seq scripted run through the parkour (used with lamatest.py)
sprunglabor.py "jump lab": test lanes used to measure jump height/width
re/ghidra_scripts/
DumpAll.java Ghidra headless script: decompiles every function to one C file
```bash
./gradlew spotlessApply build fatJar # tests + build/libs/llamamod-<ver>-all.jar
java -jar build/libs/llamamod-*-all.jar # LlamaMod
java -jar build/libs/llamamod-*-all.jar --editor # the level editor alone
./gradlew packageAll # Linux + Windows packages, full and launcher edition
```
---
Details: [For developers](docs/developers.md).
## Quick start
## Documentation
**[docs/](docs/README.md)** – start there.
* **[Build your own level](docs/guide/README.md)** – a step-by-step guide for newcomers.
* **[Level editor manual](docs/manual/index.md)** – every tool and dialog, with tutorials (also
built into the editor: **F1**).
* **[Reference](docs/reference/README.md)** – file formats (WAD, geometry, collision, objects,
textures, music), TestLevel, engine limits.
* **[Strats](docs/strats/README.md)** – how the game's object scripts work, the script language,
custom object types, and the [Alligator Arena walkthrough](docs/strats/arena.md).
* **[Tools](docs/tools.md)** – the app's command line, the Python kit, in-game test harnesses.
* **[Lab notes](notes/README.md)** – the detailed engineering notebook.
## The Python kit
The project started as a Python reverse-engineering kit, which is still in [`tools/`](tools/):
byte-exact parsers and writers for every level chunk, OBJ export/import, a script-driven level
builder ([`tools/levelbuild.py`](tools/levelbuild.py), example [`levels/testwelt.py`](levels/testwelt.py)),
texture and picture converters, a music decoder, the debug-view generator and an automated
in-game tester for Linux/Wine. Python 3.8+ with `numpy` and `Pillow`.
```bash
# export a level to OBJ (+ collision and object markers)
python3 tools/geom_export.py /path/to/cd/wads/t1l1m001.wad out_village --collision --objects
# -> level.obj/.mtl, texture pages, collision.obj, objects.obj, preview images
# build the example test world
python3 tools/levelbuild.py levels/testwelt.py testwelt.wad
# build the original test world from its script
python3 tools/levelbuild.py levels/testwelt.py testwelt.wad --base /path/to/cd/wads/t1l1m001.wad
python3 tools/levelbuild.py --types # object types you can place
python3 tools/levelbuild.py --textures tex.png # texture sheet with the names you can use
# build the debug patch for all levels
python3 tools/debugviz.py --help
# music to WAV, loading picture to PNG
python3 tools/asf2wav.py /path/to/cd/music/Village.asf village.wav
python3 tools/cor2png.py /path/to/cd/t111.cor t111.png
```
### Playing the test level
Some scripts have hard-coded paths of the original Wine setup at the top (`WADS`, `MODWAD` in
`levelbuild.py`, `CD_WADS` in `debugviz.py`, `PREFIX`/`LAMA_MOD` in `lamatest.py`) – adjust them or
pass `--base`/`--src`. All tools: [docs/tools.md](docs/tools.md).
The game has a hidden world 9, *TestLevel*, with 49 chapter slots, but the retail game ships
no data for it. Put your built WAD into the CD's `wads` folder as `T1L9M001.wad`. Also add a
loading picture `T191.COR`, for example a copy of another `T1xx.COR`.
### Key facts in one minute
To reach world 9, edit a save file (`king0.ksv`…`king2.ksv`, 256 bytes):
* **The world is made of tiles.** TRAK holds 100–650 small models (render triangles + collision
polygons); MAP places them on a W × H grid with height and rotation, plus baked vertex colours
and a 32-region visibility table. No BSP, no single world mesh.
* **Objects are "strats"**: compiled scripts in STPC. An object's type is its byte offset into
STPC, so a level can only use the types its own STPC contains (or types merged in from other
levels).
* **Scale**: y up, one unit per grid cell; the llama is 0.6 tall, walks 1.1 units/s, steps up
nothing and jumps ~1.3 far.
* **The exit** fires only when the llama stands in the LevelDoor's grid cell **on floor with
collision material 16**.
* **Textures** are PlayStation-style 15-bit pixels on 256 × 256 pages; there are no free UVs.
* `+0x04/+0x05` = map cursor (world, chapter)
* `+0x3f/+0x40` = current world/chapter
* `+0x42/+0x43` = highest unlocked world/chapter
More: [docs/reference](docs/reference/README.md).
Set them to world 9, chapter 1.
### Building on this with an AI assistant
When you leave through the exit, the game unlocks the next chapter. `levelbuild.py --install`
therefore links chapters 2–49 to the same file, so that picking one on the map doesn't crash.
This whole project was made by an AI coding agent (Claude Code). To continue it, give an assistant
the [lab notes](notes/README.md) first – they contain offsets, field meanings, confidence levels and
pitfalls – and let it regenerate the decompile of `groove.exe` with Ghidra for questions about game
logic (headless `DumpAll` script, see [Tools](docs/tools.md#reverse-engineering)). Always test in the
game (`tools/apptest.py`) and change one thing at a time: the engine crashes on inconsistent data.
### Debug patch: visible collision and invisible walls
## Legal
```bash
python3 tools/debugviz.py --all debug_wads --src /path/to/cd/wads # all 33 level WADs
python3 tools/debugviz.py t1l1m001.wad t1l1m001_debug.wad # a single level
python3 tools/debugviz_check.py debug_wads /path/to/cd/wads # offline sanity check
```
Put the resulting WADs into the CD's `wads` folder in place of the originals, keeping the
same file names. On the Wine setup, `lama-mod debug an|aus` toggles them. The patch only
adds render geometry. Collision, objects, waypoints and zones stay byte-identical, so
gameplay is unchanged.
Legend (full version in `notes/debugviz.md`):
| Colour | Meaning |
|---|---|
| green / yellow-green wireframe | walkable floor / slope |
| blue | wall |
| orange | ceiling / overhang |
| **red, semi-transparent fill** | **invisible collision**: invisible walls and floors |
| magenta lines | death plane ("no floor") |
| violet fill | level exit floor (material 16) |
| other colours | special collision materials |
| Marker | Meaning |
|---|---|
| white arrow | player start |
| violet cube | exit |
| turquoise cube | checkpoint |
| cyan cube | camera |
| yellow cube | trigger / cutscene |
| orange cube | force or special zone |
| gold cube | secret zone |
| grey cube | other invisible helper object |
| pink wire box | ambience / music / fog zone |
Cubes sit on a stick, and a horizontal line shows the object's facing. Wire boxes show
trigger areas.
Status: 19 of 33 levels were loaded in the game without problems. The rest
(catacombs/roller coaster world 7, lab world 8, two others) passed the offline check only.
Collision of moving objects (platforms, doors) is not shown.
---
## LlamaMod and the level editor
Besides the Python kit, this repository contains **LlamaMod** (Java 25, Swing; `src/`), a
one-click mod manager for the game on **Windows and Linux**, and a full **3D level editor**
built into it. Nothing from the game is included: the wizard installs the game from your own
CD/ISO into LlamaMod's data folder, and all mods are built from that copy.
* **Mods** – tick mods, click *Apply*/*Play*: debug wireframe, Test World, randomizer, fog,
mirror mode, intro skip, cheats, practice tools, free camera, *boot to level* … Mods may change
level files and `groove.exe` (code caves); the original files are never touched.
Developer notes: `notes/llamamod-app.md`, one `notes/mod-*.md` per mod.
* **Level editor** (*Level Editor* button, or `LlamaMod --editor`): create new levels from
templates, brushes and the base level's tile models and objects, or edit the original
levels; import OBJ/glTF meshes and PNG/JPG textures (texture manager, paint-on-faces,
animations); waypoints, zones, fog, lights, visibility regions; **Play (F5)** builds the
level and starts the game directly in it (Escape returns to the editor).
* **Sharing**: a level is a small `.llamalevel` file with only your changes. Others import it in
LlamaMod (*Mods → Levels → Import level*) and it is built from their own game copy; new levels
appear in the hidden *TestLevel* world (chapters 1–49).
User guide (English): **`notes/editor-guide.md`**. In the editor: *Help → Getting started*
and *Help → Shortcut sheet*. Technical notes: `notes/editor-design.md`, `editor-api.md`,
`editor-format.md`, `editor-render.md`, `editor-ui.md`, `editor-tools-*.md`,
`editor-playtest.md`.
```bash
./gradlew build fatJar # tests + build/libs/llamamod-<ver>-all.jar
./gradlew packageLinux # build/dist/LlamaMod-<ver>-full-linux-x64.tar.gz (own Java runtime)
./gradlew packageWindows # build/dist/LlamaMod-<ver>-full-windows-x64.zip (LlamaMod.exe + runtime)
./gradlew packageAll # both systems, full and launcher (no editor) edition
java -jar build/libs/llamamod-*-all.jar # LlamaMod
java -jar build/libs/llamamod-*-all.jar --editor # the level editor alone
```
![The level editor](notes/img/editor/ui_select_move.jpg)
---
## Key facts
* **The world is made of tiles.** TRAK holds about 100–650 small models: render triangles
plus collision polygons. MAP places them on a W×H grid (96×96 in the village) with height
and rotation. MAP also stores baked vertex colours and a 32-zone visibility table. There is
no BSP and no single world mesh.
* **Objects are "strats".** These are compiled scripts in the STPC chunk. In MAP, an object's
type is simply its **byte offset into STPC**. A level can therefore only use the object
types its own STPC contains. The builder uses the village (`t1l1m001`) as its base.
* **Coordinates and scale.** y points up. Fixed point 20.12 for objects, 4096 = 360° for
angles, and render space vs. game space have z mirrored. The llama is about 0.6 units
tall, runs about 1.1 units/s, can step up at most about 0.35 and jumps about 1.3 units
far with a run-up.
* **The level exit (LevelDoor)** is an invisible trigger. It fires only when the llama
stands in its grid cell **on floor with collision material 16**. Its parameters are
(tribe, world, chapter) of the next level.
* **Texture pixels** are PS1-style 15-bit BGR, RLE16-compressed, on 256×256 pages. The
value 0x0000 means transparent.
The parsers rebuild every original WAD byte-for-byte, so they can be trusted as format
specs.
---
## Building on this with an AI assistant
This whole kit was made by an AI coding agent (Claude Code) working through the steps
below. Any capable assistant that can run code should be able to continue.
1. **Give it the notes first.** Ask it to read `notes/*.md`. They contain offsets, field meanings, confidence levels and pitfalls.
2. **Regenerate the decompile** for questions about game logic. Install Ghidra and a full
JDK (version 25 for Ghidra 12.x), then run
`analyzeHeadless <projdir> groove -import groove.exe -scriptPath re/ghidra_scripts -postScript DumpAll.java groove_decomp.c`.
This writes all functions as C (about 2 MB) for the assistant to search with grep.
Loader functions worth knowing: MAP `0x42ac50`, TRAK `0x42aac0`, STPC `0x42ab50`, strat
interpreter `0x54d180`.
3. **Always test in the game.** Have the assistant load and screenshot every build. On
Linux, `tools/lamatest.py run <wad> <outdir>` does this unattended in about 75 s. On
Windows, test by hand or adapt the script.
4. **Change one thing at a time** and keep the original WADs untouched. The game crashes on
inconsistent data: tiles outside the grid, a vertex-colour block that doesn't match the
vertex counts, or missing chapters.
### Ideas for what to try next
* Use object types from other levels. You would have to merge and relocate their STPC
code and their textures.
* ~~Import custom textures~~ – done in the level editor (texture manager, import, painting).
* ~~Make a real visual editor~~ – done: the LlamaMod level editor (see above).
* Add custom music or sounds (SMPC/AMPC/SRPC are sound banks).
---
This is a non-commercial fan project, not affiliated with Disney or Argonaut. Please share
only tools and your own content, not the game's files.
This is a non-commercial fan project, not affiliated with Disney Interactive or Argonaut Games.
**No game files are included** – no executable, no level data, no music, no decompiled code. You
need your own copy of the game; every tool works on that copy. Please share only tools and your own
content (`.llamalevel` projects contain only your changes), not the game's files.
+77
View File
@@ -0,0 +1,77 @@
# LlamaMod documentation
Documentation for modding *The Emperor's New Groove* (PC, 2000; German: *Ein Königreich für ein
Lama*) with LlamaMod: building levels, the game's file formats, and how the game's object scripts
("strats") work.
![The Test World: every figure of the game in one level](img/testworld-gallery.jpg)
## Start here
| If you want to … | read |
|---|---|
| build your own level | **[Guide: Build your own level](guide/README.md)** |
| look something up in the editor | [Level editor manual](manual/index.md) (also in the editor: **F1**) |
| know the numbers and rules of the engine | [Design rules](guide/design-rules.md) |
| build levels from code | [Levels from scripts](guide/levels-in-code.md) |
| understand the game's data | [Reference](reference/README.md) |
| change or write object behaviour | [Strats](strats/README.md) |
| find a tool | [Tools](tools.md) |
| work on LlamaMod itself | [For developers](developers.md) |
## Contents
**[Guide](guide/README.md)** – for level builders
* [Build your own level](guide/README.md): setup, editor basics, ground, start and exit, objects,
look and feel, play-test, sharing
* [Design rules](guide/design-rules.md): sizes, jump distances, the rules that keep a level working
* [Levels from scripts](guide/levels-in-code.md): Python and Java level builders, the editor's
command line
**[Level editor manual](manual/index.md)** – every tool, panel and dialog of the editor, with
tutorials (first level, parkour, editing an original level, Blender import)
**[Reference](reference/README.md)** – the game's data
* [Game files](reference/game-files.md): folders, naming scheme, worlds, COR pictures, movies,
save games
* [WAD container](reference/wad.md): the chunks of a level file
* [Geometry and collision](reference/geometry.md): tiles, grid, coordinate spaces, collision
materials, visibility regions
* [Objects, waypoints, zones](reference/objects.md): strat records, parameters, the exit,
environment zones, lights
* [Textures](reference/textures.md): pages, pixels, texture rectangles, animations
* [Music and sound](reference/audio.md): ASF music, sound banks, speech – known and unknown
* [TestLevel (world 9)](reference/testlevel.md): where custom levels live, safe chapters
* [Engine facts and limits](reference/engine-limits.md): scale, draw distance, memory, crashes
**[Strats](strats/README.md)** – object behaviour
* [How the strat VM works](strats/README.md): units, frames, variables, events, spawning,
collision
* [Reading scripts](strats/reading-scripts.md): the Script view and the assembly
* [The script language](strats/script-language.md): writing new behaviour
* [Customising strats](strats/custom-types.md): constants, imports, clones, custom types, mods
* [Example: the Alligator Arena](strats/arena.md): a complete minigame explained step by step
* [Example: the roller coaster](strats/roller-coaster.md): coming soon
**[Tools](tools.md)** – LlamaMod's launcher, editor and command line, the Python kit, the in-game
test harnesses
**[For developers](developers.md)** – build, code map, writing a mod, maintaining the manual
**[Lab notes](../notes/README.md)** – the detailed engineering notebook behind all of this
## Pictures
| | |
|---|---|
| ![LlamaMod launcher](img/launcher.jpg) | ![Level editor](img/editor.jpg) |
| The launcher: mods with options, save games, settings | The level editor with Village 1 |
| ![Alligator Arena](manual/img/mod_arena.jpg) | ![Debug view](img/debug-view.jpg) |
| The Alligator Arena minigame | Debug view: collision and invisible objects in the game |
## Status and honesty
Everything here was found by reverse engineering and tested against the game; where something is
a guess, the page says so (*likely*, *unknown*). Development used the German CD; other language
versions are expected to work but were not tested. The project is not affiliated with Disney or
Argonaut and contains no game data.
+86
View File
@@ -0,0 +1,86 @@
# For developers
How the code is organised, how to build and test it, and where the engineering notes are.
## Build
Requirements: a **JDK 25** installed (the Gradle toolchain compiles with it; there is no
auto-download). Gradle itself and Spotless currently need to run on **Java 21**, e.g.
`JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew …` on Debian/Ubuntu.
```bash
./gradlew spotlessApply build # compile, format, run the tests
./gradlew fatJar # build/libs/llamamod-<ver>-all.jar
java -jar build/libs/llamamod-*-all.jar # launcher
java -jar build/libs/llamamod-*-all.jar --editor # the editor alone
./gradlew packageLinux # tar.gz with its own Java runtime
./gradlew packageWindows # zip with LlamaMod.exe + runtime (built on Linux)
./gradlew packageAll # both systems, full and launcher (no editor) edition
```
Details (jlink, launch4j, editions, version and build id): [notes/llamamod-app.md](../notes/llamamod-app.md#build).
**Tests that need the game.** Golden and in-game-data tests read the CD folder given by
`-Dllamamod.cd=<…/Setup/<lang>/Cd>` (default: the author's Wine setup); without it they are
skipped. Byte-exact round trips over all level WADs (WAD, TRAK, MAP, TEXT, strat code) are the
safety net for every format change.
## Code map
Package root [`src/main/java/com/eliasmueller/llamamod/`](../src/main/java/com/eliasmueller/llamamod/):
| Package | Content | Notes |
|---|---|---|
| `format` | WAD, TRAK, STPC, MAP, TEXT, collision, COR | [Reference](reference/README.md) |
| `format.strat` | strat disassembler, assembler, script view, compiler | [Strats](strats/README.md), [notes/strat-language.md](../notes/strat-language.md) |
| `format.stpcmerge` | importing object types between levels (code, models, textures, sounds, texts) | [notes/editor-cross-import.md](../notes/editor-cross-import.md) |
| `levelbuild` | Java level builder (`LevelBuilder`, `LevelScript`) | [Levels from scripts](guide/levels-in-code.md) |
| `mods` | mod framework (`Mod`, `ModContext`, `ModManager`, options) and the built-in mods | [notes/llamamod-app.md](../notes/llamamod-app.md#mods), `notes/mod-*.md` |
| `exe` | patching `groove.exe`: PE image, code caves, a tiny x86 emitter | [notes/llamamod-app.md](../notes/llamamod-app.md#grooveexe-patches-and-code-caves-exeexeimage-exex86) |
| `platform` | install from ISO/CD, data folder, config, registry, Wine/Windows launch | [notes/llamamod-app.md](../notes/llamamod-app.md) |
| `save` | KSV save games, TestLevel chapters | [Game files](reference/game-files.md#save-games-ksv) |
| `ui` | launcher window | |
| `editor.model`, `editor.io` | scene model, commands/undo, project format, WAD import/build | [notes/editor-api.md](../notes/editor-api.md), [notes/editor-format.md](../notes/editor-format.md) |
| `editor.render` | OpenGL viewport | [notes/editor-render.md](../notes/editor-render.md) |
| `editor.ui` | editor shell and plugin API | [notes/editor-ui.md](../notes/editor-ui.md) |
| `editor.tools.*` | editor plugins: geometry, objects, environment, textures, catalog (cross-world import), strats, playtest, gltf, help | `notes/editor-tools-*.md`, [notes/editor-playtest.md](../notes/editor-playtest.md) |
## Writing a mod
A mod implements [`Mod`](../src/main/java/com/eliasmueller/llamamod/mods/Mod.java): `id`, `name`,
`description`, `category` (LEVELS, GAMEPLAY, VISUAL, CHEATS), `options()` and `apply(ModContext)`.
`apply` reads files of the game as earlier mods left them (`ctx.read`) and writes replacements
(`ctx.write`); `ctx.exe()` gives access to `groove.exe` for patches. Mods are registered in
[`ModRegistry`](../src/main/java/com/eliasmueller/llamamod/mods/ModRegistry.java). LlamaMod caches
mod outputs and never touches the original files.
Good examples: [`mods/testworld/`](../src/main/java/com/eliasmueller/llamamod/mods/testworld/) (a
level built in code), [`mods/arena/`](../src/main/java/com/eliasmueller/llamamod/mods/arena/) (level
+ new strat types, walkthrough: [Arena](strats/arena.md)),
[`mods/fog/`](../src/main/java/com/eliasmueller/llamamod/mods/fog/) (data-only change of all levels),
[`mods/practice/`](../src/main/java/com/eliasmueller/llamamod/mods/practice/) (exe code caves with
hotkeys). Full API: [notes/llamamod-app.md](../notes/llamamod-app.md#writing-a-mod).
Every mod should be checked in the running game; [`tools/apptest.py`](../tools/apptest.py) does
this unattended on Linux ([Tools](tools.md#testing-in-the-game)).
## The editor manual
[`docs/manual/`](manual/index.md) is the editor's user manual **and** its built-in help: the build
bundles the folder into the jar (`processResources` in `build.gradle`), the Help window renders it
with its own small Markdown converter, and F1 maps editor topics to chapter anchors
(`editor/tools/help/context-help.txt`). Rules for that folder:
* Only the Markdown subset described at the top of `index.md`; every chapter must be linked from
`index.md`; links must stay **inside** `docs/manual` (the help window cannot open other files).
* Blocks between `generated:NAME` comments (shortcuts, menus, mod reference) are rewritten from the
live program by `ManualTest` – do not edit them by hand.
* `./gradlew test --tests '*ManualTest*'` checks links, anchors and images.
Maintenance notes: [notes/editor-manual.md](../notes/editor-manual.md).
## Engineering notes
[`notes/`](../notes/README.md) is the project's lab notebook: one file per format, editor area and
mod, with addresses in `groove.exe`, confidence levels, what was verified in the game and known
limits. Process material (agent briefs, review reports) is in [`notes/dev/`](../notes/dev/).
+126
View File
@@ -0,0 +1,126 @@
# Build your own level
A step-by-step path from "I have the game" to "my friends are playing my level". Each step is short
and links to the chapter of the [editor manual](../manual/index.md) that has the details.
The manual is also built into the editor: press **F1** over any panel, tool or dialog.
| Step | |
|---|---|
| 1 | [Set up LlamaMod](#1-set-up-llamamod) |
| 2 | [Learn the editor in ten minutes](#2-learn-the-editor-in-ten-minutes) |
| 3 | [Plan the level](#3-plan-the-level) |
| 4 | [Build the ground](#4-build-the-ground) |
| 5 | [Start, checkpoints and the exit](#5-start-checkpoints-and-the-exit) |
| 6 | [Objects, enemies and cameras](#6-objects-enemies-and-cameras) |
| 7 | [Look and feel: textures, fog, music, light](#7-look-and-feel-textures-fog-music-light) |
| 8 | [Play-test](#8-play-test) |
| 9 | [Share it](#9-share-it) |
| 10 | [Go further](#10-go-further) |
Before you start, skim [Design rules](design-rules.md): the numbers (jump distances, draw
distance) and the handful of rules that keep the game from crashing.
![The level editor](../img/editor.jpg)
## 1. Set up LlamaMod
1. Get **your own copy** of the game: the CD or an ISO image of it (*The Emperor's New Groove* PC,
2000; German: *Ein Königreich für ein Lama*). LlamaMod contains no game files.
2. Download the **full** package for Windows or Linux, unpack it and start `LlamaMod.exe` /
`./LlamaMod` (Linux needs Wine to run the game). Or build it from source – see the
[main README](../../README.md#build-from-source).
3. The first-start wizard copies the game into LlamaMod's data folder. The original CD or ISO is
never changed.
4. Click **Play** once to check that the unmodded game runs.
Manual: [Installing LlamaMod and the first start](../manual/02-installing.md).
## 2. Learn the editor in ten minutes
Click **Level Editor** in LlamaMod and follow
[Your first level in 10 minutes](../manual/tutorial-first-level.md): a new level from the
*Island* template, a ramp, a coin trail, play it. Then read
[Introduction and concepts](../manual/01-introduction.md) – tiles, brushes, objects, zones, base
levels, chapters – and [Navigating the viewport](../manual/04-navigation.md).
## 3. Plan the level
* **New or edited?** A *new level* starts empty on a **base level** (default: Village 1) that
provides the object types, textures, sky, fog and music. *Editing an original level* changes the
real Village, River … in place ([tutorial](../manual/tutorial-original-level.md)).
* **Choose the base by its objects.** Only the base's object types are available directly; more
can be imported from any world ([Importing objects](../manual/09-objects.md#importing-objects-from-other-worlds)).
* **Size.** The village grid is 96 × 96 cells; the llama is 0.6 tall and jumps about 1.3 cells.
A small, dense level plays better than a big empty one – the draw distance is only 8–16 cells.
* **Where it goes.** New levels live in the hidden world 9, *TestLevel*, on chapter 1 or 32–49
([why](../reference/testlevel.md)). The editor picks a free chapter.
## 4. Build the ground
* **Brushes** (B) draw boxes, ramps, stairs, cylinders and more; they are cut into tiles when the
level is built ([Building geometry](../manual/06-geometry.md)).
* **Tiles** (T) place the base level's own tile models for an original look.
* **Meshes** from Blender or any 3D program come in as OBJ or glTF
([Importing meshes](../manual/07-meshes.md), [Blender tutorial](../manual/tutorial-blender.md)).
* **Collision** (K) decides what is solid: walls, floors, invisible walls, death floors, the exit
floor. The llama cannot step up even small edges – use slopes, or walls without collision.
![A level exported as glTF and opened in Blender](../img/gltf-blender.jpg)
## 5. Start, checkpoints and the exit
Use the one-click setups in the Objects panel ([Objects](../manual/09-objects.md#one-click-setups)):
* **Player start** – the llama plus its start camera node behind it. Keep the space behind the
llama free, or the camera jumps aside.
* **Checkpoint** – where the llama respawns after a fall.
* **Exit** – a LevelDoor *and* a floor with collision material 16 in the same grid cell. Without
that floor the exit does nothing ([why](../reference/objects.md#the-exit-leveldoor--certain)).
Its target is the next level; in TestLevel never "world finished" (7, 7, 7).
* **Death plane** – a death floor below everything, so falling off respawns instead of falling
forever.
## 6. Objects, enemies and cameras
* Place coins with the **Coin tool** (O), anything else from **Assets → Objects**
([Objects](../manual/09-objects.md)).
* Enemies and moving things often need a **waypoint chain** – some guards even need a pair of
waypoint boxes and an event bit, or the game removes them. The Inspector and *Check for problems*
tell you ([Waypoints](../manual/10-waypoints.md)).
* **Cameras**: camera zones, cut scenes and the start camera
([Cameras and triggers](../manual/11-cameras-triggers.md)).
* Change how an object behaves – a number in its script or a whole new type: see
[Strats](../strats/README.md) and [manual chapter 22](../manual/22-strats.md).
## 7. Look and feel: textures, fog, music, light
* **Textures**: the base's textures, imported PNG/JPG pictures, painting onto faces, flip-book
animations ([Textures](../manual/08-textures.md)).
* **Environment zones** set fog (= draw distance), sky colour and **music**; teleport zones move
the player between areas ([Environment](../manual/12-environment.md),
[music ids](../reference/audio.md#music)).
* **Light**: tiles get baked vertex colours (*Bake lighting*), lights only light objects.
* **Visibility regions** hide areas the camera cannot see – needed for big levels
([Visibility](../manual/13-visibility.md)).
## 8. Play-test
**F5** builds the level and starts the game directly in it; **Shift+F5** starts at the camera
position. *Check for problems* (also run on every build) lists errors and warnings – double-click
one to select the culprit. **Escape** in the game quits straight back to the editor.
Manual: [Building, problems and play-testing](../manual/15-build-play.md).
## 9. Share it
**File → Save** writes a small `.llamalevel` project: only your changes, no game data. Send that
file; the receiver copies it into LlamaMod's `levels` folder (or opens it in the editor and uses
*Level → Add to LlamaMod…*), ticks it in the *Levels* category and clicks *Apply*. It is built from
their own game copy. Manual: [Sharing levels](../manual/17-sharing.md).
## 10. Go further
* Build levels from code instead of the mouse: [Levels from scripts](levels-in-code.md).
* Understand the data: [Reference](../reference/README.md).
* Write object behaviour: [Strats](../strats/README.md) and the
[Alligator Arena walkthrough](../strats/arena.md).
* All tools at a glance: [Tools](../tools.md).
+48
View File
@@ -0,0 +1,48 @@
# Design rules
The numbers and rules that matter when you design a level for this engine. Background and sources:
[Engine facts and limits](../reference/engine-limits.md).
## Sizes to design with
| | |
|---|---|
| one grid cell | 1 unit |
| llama | 0.6 tall, walks 1.1 units/s |
| jump up | ≤ 0.35 (0.5 fails) |
| jump across, with a run-up | ~1.3 (from standing much less) |
| easy parkour | gaps 0.3 with +0.2 height, platforms ≥ 1.5 long |
| steps without jumping | none – use slopes or walls without collision |
| draw distance | the fog end of the zone: 8–16 cells in the originals |
| objects visible | within ~12.5 units of the llama |
![The Test World's gallery: every figure of the game on a pedestal](../img/testworld-gallery.jpg)
## Rules that keep the level working
1. **One player start**, with nothing solid right behind the llama (start camera).
2. **Exit = LevelDoor + floor with collision material 16 in the same cell.** Target (1, 9,
chapter) in TestLevel – never (7, 7, 7).
3. **A death floor or death plane** under every place the llama can fall to.
4. **A checkpoint** before every hard part.
5. **TestLevel chapter 1 or 32–49** for new levels.
6. **Guards and box walkers need their waypoint boxes** (and guards an event bit someone sets),
or the game removes them.
7. **Every area must be visible from where the camera is**: with visibility regions, check the
masks (*Check for problems* reports regions nothing can see).
8. **Stay within the budget**: 16 MB level memory, 127 texture pages, 128 sounds – the import
preview and *Check for problems* show where you are.
The editor and both level builders enforce the data-level rules (tiles inside the grid, vertex
colours, type fields, collision cells) automatically. *Check for problems* reports the rest.
## Good habits
* **Play-test often** (F5). The engine is unforgiving and a small change can stop the exit or
hide an area.
* **Look at the originals.** Open an original level in the editor and see how it builds doors,
cameras and enemy paths; the [debug view mod](../tools.md#debug-view) shows collision and
invisible objects in the running game.
* **Keep levels compact.** Short sight lines hide the draw distance and keep the frame rate up.
* **Copy numbers** (speeds, parameters, waypoint fields) from original objects that already do
what you want.
+93
View File
@@ -0,0 +1,93 @@
# Levels from scripts
Besides the editor, a level can be described as a short script that a **level builder** turns
into a complete WAD: geometry made of boxes, ramps, stairs, trees and sea, objects placed by type
name, lighting baked automatically. There are two builders with the same ideas:
| Builder | Language | Used by |
|---|---|---|
| [`tools/levelbuild.py`](../../tools/levelbuild.py) | Python 3 (numpy, Pillow) | the original test world [`levels/testwelt.py`](../../levels/testwelt.py), the jump lab [`levels/sprunglabor.py`](../../levels/sprunglabor.py) |
| [`levelbuild/LevelBuilder.java`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java) | Java (`LevelScript`) | the Test World mod ([`mods/testworld/`](../../src/main/java/com/eliasmueller/llamamod/mods/testworld/)), the [Alligator Arena](../strats/arena.md), the editor's brushes |
Both start from a **base level** (default Village 1): its object code, textures and sound are
kept; geometry, tile grid, vertex colours, objects and waypoints are generated from scratch. Full
documentation of the Python builder, the measured physics and the test world:
[notes/levelbau.md](../../notes/levelbau.md).
## A Python level
```python
LEVEL_ARGS = dict(name='MyLevel', seed=7)
def build(L):
L.sea(0, 0, 60, 60, y=-2.0, kill=(6, 4, 54, 56)) # sea + death plane
L.box(14, 12, 26, 24, -0.4, 0, top='stein', side='stein') # a stone plaza
L.stairs(26, 18, 29, 22, 0, 1.5, 3, '+x') # stairs up to the east
L.box(29, 17, 33, 23, 0, 1.5, top='gras', side='erde')
L.player(20, 14.5, rot=0) # facing +z
L.checkpoint(17, 18.5)
L.coins([(27, 20), (28, 20)])
L.strat('Bunny', 31, 20)
L.box(31, 21, 32, 22, 1.5, 1.56, top='platte', side='stein', mat=16) # exit floor
L.exit(31.5, 21.5) # LevelDoor in the same cell
```
```bash
python3 tools/levelbuild.py my_level.py out/my_level.wad # build
python3 tools/levelbuild.py --types # object types of the base
python3 tools/levelbuild.py --textures tex.png # texture names and a contact sheet
```
Coordinates are game units: x east, y up, z = grid row ("south"); angles in degrees, 0° faces +z.
Building blocks: `floor`, `box`, `platform`, `pillar`, `ramp`, `stairs`, `fence`, `tree`, `sea`,
`quad`, `collider`, `obj` (an OBJ file), `height_at`. The exit needs a floor with collision
material 16 in its cell – the test world uses `box(..., mat=16)` under the door.
The Python tools have a few **hard-coded paths** at the top of the scripts (`WADS`, `MODWAD` in
`levelbuild.py`, `CD_WADS` in `debugviz.py`, `PREFIX` in `lamatest.py`) that point into the original
author's Wine setup; use `--base <cd>/wads/t1l1m001.wad` or adjust them. `--install` only works with
that setup – with LlamaMod, use a Java level or the editor instead.
## A Java level
Implement [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java):
```java
public final class MyLevel implements LevelScript {
@Override public String levelName() { return "My Level"; }
@Override public void build(LevelBuilder l) {
l.sea(0, 0, 40, 40, -1.5, new double[] {7, 7, 33, 33});
l.floor(10, 10, 30, 30, 0, "grass");
l.box(18, 24, 22, 25, 0, 0.06, o().top("pattern_purple").mat(16)); // exit floor
l.player(20, 12, 0);
l.checkpoint(12, 12);
l.exit(20.5, 24.5, null, null, new int[] {1, 9, 32});
l.coins(List.of(new double[] {20, 16}, new double[] {20, 18}));
}
}
```
`buildWad(baseWad, wads, opts)` returns the WAD. To ship it, write a small
[mod](../developers.md#writing-a-mod) whose `apply` writes `wads/T1L9M0nn.wad` (chapter 1 or
32–49) and a loading picture – [`ArenaMod`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java)
is a complete example. Texture names are English here (`grass`, `stone`, `sand`, `inca`, …) and are
matched by picture on the chosen base.
## The editor from the command line
The editor's project format can be scripted too:
```bash
llamamod --editor-new my.llamalevel t1l1m001 --name "My level" # new project on a base
llamamod --editor-import t1l2m001 jungle.llamalevel # an original level as a project
llamamod --editor-build my.llamalevel out.wad # build the WAD
llamamod --import-level my.llamalevel # install as a level mod
```
## Seeing the result without the game
* [`tools/geom_export.py`](../../tools/geom_export.py) `level.wad outdir --collision --objects`
writes OBJ files, texture pages and two preview images.
* [`tools/vorschau/`](../../tools/vorschau/) renders labelled overviews with Blender (Eevee).
* In the game: the test harness, see [Tools](../tools.md#testing-in-the-game).
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

+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).
+120
View File
@@ -0,0 +1,120 @@
# Strats: how objects work
Every object in the game – a coin, a squirrel, a moving platform, a camera, the llama itself – is
driven by a small program called a **strat** (Argonaut's "strategy" scripts, inherited from Croc).
The programs are compiled bytecode for a virtual machine in `groove.exe` (`FUN_0054d180`). LlamaMod
can read every one of them, change their numbers, write new ones and move them between levels.
| Page | For |
|---|---|
| this page | how the VM works: units, instances, frames, variables, events, spawning, collision |
| [Reading scripts](reading-scripts.md) | the editor's Script view and the assembly listing, with a worked example |
| [The script language](script-language.md) | writing new behaviour in LlamaMod's structured language |
| [Customising strats](custom-types.md) | changing constants, cloning types, custom types in the editor and in mods |
| [Example: the Alligator Arena](arena.md) | a complete minigame written in the script language, explained step by step |
| [Example: the roller coaster](roller-coaster.md) | coming soon |
Lab notes and tables: [notes/strat-opcodes.md](../../notes/strat-opcodes.md) (all 434 opcodes with
semantics, confidence and Argonaut names), [notes/strat-language.md](../../notes/strat-language.md)
(complete syntax), [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md)
(parameters of individual types). Machine-readable tables, used by the editor and the compiler:
[`ops.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/ops.tsv),
[`props.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/props.tsv).
## Code units and types
The strat code of a level sits at the end of its STPC chunk as a list of **code units**:
`u32 nUnits`, then per unit `u32 nWords` and the words. A unit is one compiled source file and can
contain several strats plus their name strings and data (for example `Spider` + `RopeForSpider`).
The village has 53 units, Lab 1 has 65.
A **type** is just an entry point into this code: a MAP record or a `spawn` instruction names a
byte offset in STPC, and the code there starts with `debugName "Name"`. Code refers to models,
animation sets and other code by **absolute STPC offsets** – which is why STPC cannot be
rearranged without relocating every reference, and why each level carries its own copy of the
code of every type it uses.
LlamaMod's disassembler covers the code of all 34 level WADs (1461 units, 670 555 words) and
assembles every unit back **byte-identically**.
## One frame in the life of a strat
The VM is a **stack machine** that runs every live strat once per frame (30 frames per second):
1. The strat continues where it stopped last frame.
2. It executes instructions – push values, compute, read and write properties, play animations,
move – until it reaches a **yield** (`jmpYield`, `jzYield`, `callYield` …).
3. At the yield its turn ends; the next strat runs. Next frame it continues after the yield.
So a typical object is "set up, then `loop { …; yield }`", and anything that takes time is a loop
that counts frames. Speeds and turn rates in scripts are **per frame**.
`end` / `remove` deletes the strat; `return` returns from a `call`.
## Data a strat works with
| Kind | Syntax | What it is |
|---|---|---|
| **variables** | `v0`, `v1`, … (or an alias name) | the strat's own variables (count = the type's *nVars*). The first *n* hold the instance's **MAP parameters** |
| **properties** | `self.x`, `self.rotY`, `self.floorHeight`, `self.damageTaken` … | 194 named engine fields of the strat: position, rotation, floor under it, distance to the player, animation state, alpha, collision damage … |
| other strats' properties | `player.x`, `parent.v3`, `camera.…`, `target.…`, `boss.…`, `dialog.…` | the player, the strat that spawned this one (`parent`), the camera and a few registered special strats |
| **globals** | `g0` … | shared by all strats; the engine itself never reads most of them – their meaning comes from how scripts use them (e.g. g95 = event bits, g101–g105 = exit target and cell, g17–g20 = dive target) |
| **numbers** | `1.0`, `90deg`, `204r` | 20.12 fixed point (4096 = 1.0 = one cell); angles 0x1000000 = 360°; `r` marks a raw integer |
Binary operators pop *b* (top) then *a* and push `a op b`; comparisons push 0/1; `mul` = a·b >> 12.
## Events
A strat registers **event handlers** in its slots (`onEvent`, in the script language
`on touched { … }` / `on event(type, param) { … }`). When the event fires, the handler runs like a
subroutine. Known event types:
| Type | Fires when |
|---|---|
| 4 | **touched**: contact with another strat (pick-ups, enemies). Needs collision (`collisionOn` + `collRadius`) |
| 0x40 | timer |
| 0x80 / 0x100 | player distance |
## Spawning
`spawn<params, vars, slots, typeFlags, extra>(arguments…, unit:N+offset)` creates a new strat from
code: the five inline values are exactly the type fields of a MAP record
([Objects](../reference/objects.md#strat-record-58-bytes-certain-unless-noted)), the stack
arguments become the child's parameters, and the child's `parent` is the spawner. Facts found
the hard way (by the arena):
* The child appears at the spawner's **matrix position** of the last frame, not at its current
`x/y/z`: after `self.x = …` the spawner must **yield** before spawning.
* A type that uses collision needs collision type flags (e.g. 0x28000 like Coin) and enough
`extra` words for its collision bones – otherwise the game crashes in the heap allocator.
* The strat pool must have room for every spawned strat (`poolExtra`).
## Collision and damage
On contact, each side's `hitDamage` is copied into the other's `damageTaken`, and both get event 4.
The llama's attacks set bits in its `hitDamage`: **256** kick (Action on the ground), **512** dive
(Action in the air; it homes in on the target an enemy publishes in g17–g19), **1024** charge.
The llama counts a contact as *its* hit if the other's `hitDamage` shares a bit with its own,
otherwise `damageTaken & 127 == 1` costs it a heart and knocks it back. The original Alligator has
513 (= 512 | 1): only the dive hurts it, a kick gets you bitten.
## References to level data
Some operands are indices into the level's own data and differ between levels; the cross-level
import rewrites exactly these:
| Operand | Points to |
|---|---|
| `model:N`, `anim:N`, `unit:N[+off]` (`address`, opcode 0xb2) | a model, an animation set, code of a unit; negative = shared animations in `DEFANIM.WAD` |
| `sound:N` (opcode 0xd7) | a sample of the level's sound bank |
| `text:N` (opcodes 0xf0, 0xfa) | a dialogue line in LGPC |
| `tex:N` (pushed before texture opcodes) | a texture entry (shadows, sprites, particles) |
## How the names were found
All 434 opcodes (0x000–0x1b1) were named by decompiling every handler with Ghidra; 252 of them
match Argonaut's own names from the Croc 2 PlayStation demo's jump table and the Croc 1
decompilation (knowledge only, no code used – see the sources in
[notes/strat-opcodes.md](../../notes/strat-opcodes.md#sources)). Confidence: 353 high, 79 medium,
2 low; high-confidence opcodes cover 97 % of all instructions in the levels. The names of rarely
used opcodes and properties can still be wrong.
+240
View File
@@ -0,0 +1,240 @@
# Example: the Alligator Arena
The *Alligator Arena* mod is a complete minigame built from scratch: a walled pit on an island,
alligators coming in waves, coins popping out on every hit, an exit that opens after the last wave.
It uses every technique of this documentation – a scripted level, two new strat types written in
the [script language](script-language.md), and an object (the Alligator) imported from another
world. This page walks through the code.
![The arena: an alligator chasing the llama](../manual/img/mod_arena.jpg)
| File | Role |
|---|---|
| [`ArenaMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java) | the mod: options, writes the WAD and the loading picture |
| [`Arena.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/Arena.java) | the level script: geometry and object placement |
| [`ArenaScripts.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaScripts.java) | the strat scripts `ArenaMaster`, `ArenaGator` (and a test probe) |
| [`ArenaImport.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaImport.java) | compiles the scripts and merges them into the base level |
| [`ArenaTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/arena/ArenaTest.java) | unit tests (scripts compile for all option corners, the built level decodes) |
| [notes/mod-arena.md](../../notes/mod-arena.md) | lab notes: engine facts found, what was verified in the game |
Options: alligator **lives** (1–10, default 5), **waves** (1–5, default 3; wave *n* brings
min(*n*, 2) alligators), llama **energy** cap (1–5, default 4), **coins** per hit (0–5, default 1),
TestLevel **chapter** (32–49, default 49).
## 1. The plan
* The **base level** is Village 1 (`t1l1m001`), like every new level: its code provides the
Llama, Coin, CheckPointPost and textures.
* The Alligator lives in **Jungle by Day 2** (`t1l4m002`). Its model, animations and sounds are
needed, so the new scripts are **compiled against that level** and then **imported** into the
village base together with everything they reference.
* Two new types: `ArenaMaster`, an invisible referee standing on the exit pad, and `ArenaGator`,
a new alligator spawned by the master.
* The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)).
## 2. The level (`Arena.build`)
The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java)
for the Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java),
the same builder the Test World uses. Coordinates are game units (one cell each):
```java
l.sea(0, 0, 40, 40, -1.5, new double[] {ix0 - 3, iz0 - 3, ix1 + 3, iz1 + 3}); // sea + death plane
l.floor(a.x0(), a.z0(), a.x1(), a.z1(), a.floor(), "sand"); // pit floor 16..24
l.box(a.x0() - t, a.z0() - t, a.x1() + t, a.z0(), 0, WALL,
o().top("stone").side("stone_moss")); // one of four walls
// exit pad: one cell with collision material 16 under a gate
l.box(ex - 0.5, ez - 0.5, ex + 0.5, ez + 0.5, a.floor(), a.floor() + 0.06,
o().top("pattern_purple").side("inca").rot(0).sideColl(false).mat(DOOR_MAT));
l.player(st[0], st[1], st[2], null, 1.6); // llama + start camera node 1.6 behind it
l.checkpoint(a.x0() + 0.8, a.z0() + 0.8);
l.strat(ArenaScripts.MASTER, ex, ez, a.floor(), 0.0, null, LevelBuilder.Waypoints.NONE, 0, false);
l.coins(List.of(new double[] {a.x0() + 0.6, 20}, new double[] {a.x1() - 0.6, 20}));
```
Points to note:
* The **exit pad** is a 1 × 1 box whose top has collision **material 16** – the exit rule from
[Objects](../reference/objects.md#the-exit-leveldoor--certain). There is no LevelDoor; the master
takes its role.
* The walls are 1.1 high – too high to jump (0.35), so the fight stays in the pit.
* Texture names (`sand`, `stone_moss`, `inca`) are resolved on the base level by picture.
## 3. Compiling and importing (`ArenaImport`)
`Arena.buildWad` runs before the geometry is built:
```java
merged = ArenaImport.merge(baseWad, src, "t1l4m002", units(probe, debug));
return LevelScript.super.buildWad(merged.wad(), wads, opts);
```
1. **`append`** compiles each unit with `StratCompiler` → `StratAssembler`, laid out as if it were
the last unit of **t1l4m002's** STPC, and appends it there. So `model:47` is the Alligator's model
and `unit:56+0xf0` is that level's `EnemyCoin`. A later unit can name an earlier one as
`unit:@ArenaGator`; `append` replaces that with `unit:N+offset`.
2. **`merge`** asks [`StpcMerge`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/StpcMerge.java)
– the editor's cross-world import engine – to import the two entry points into the village:
it follows every reference and copies the model, 7 animation sets, textures, 3 sound samples and
the spawned strats (`EnemyCoin` and `Sparkles` already exist in the village and are shared),
relocates every operand and verifies the result.
3. It adds one **unplaced prototype MAP record** per type with its type fields
(`nVars`, `slots`, `typeFlags`, `extra`), so the level builder can place the type by name.
The type fields are declared in `Arena.units`:
| Type | vars | slots | type flags | extra | why |
|---|---|---|---|---|---|
| ArenaGator | 16 | 5 | 0xC000 | 3 | collision like the Alligator; 3 collision bones |
| ArenaMaster | 12 | 0 | 0 | 0 | invisible logic, no collision, no handlers |
## 4. The referee: `ArenaMaster`
`ArenaScripts.master()` generates the script; the waves are unrolled by Java. With 2 waves it
reads like this (simplified):
```
strat ArenaMaster {
alias wave = v1
alias alive = v3
alias kills = v4
alias t = v5
g104 = -1 // no exit cell yet (the llama's own exit check reads g104/g105)
g105 = -1
t = 60 // 2 seconds
while (t > 0) { t -= 1; call cap; yield }
wave = 1
self.x = 20.5 // move to a spawn spot …
self.z = 22.2
yield // … and let the engine update the matrix before spawning
spawn<0, 16, 5, 50176, 3>(unit:@ArenaGator)
alive += 1
self.x = 20.5 // back onto the exit pad
self.z = 23.5
while (alive > 0) { call cap; yield } // gators do parent.v3 -= 1 when they die
t = 45
while (t > 0) { t -= 1; call cap; yield }
wave = 2
… // two spawns this time
g101 = 4096r // exit target (1, 9, chapter) as raw 20.12 values
g102 = 36864r
g103 = 200704r
g104 = int(self.x) // the exit cell
g105 = int(self.z)
… // 5 bonus EnemyCoins
loop {
call cap
if (player.floorMaterial == 16 && int(self.x) == int(player.x)
&& int(self.z) == int(player.z)) {
endLevel(4096r, 36864r, 200704r) // the LevelDoor's rule
g1 += 1
remove
}
… // a Sparkles effect every 8 frames
yield
}
}
sub cap { // energy option: keep the llama's energy at the cap
if (player.health2 > 4) { player.health2 = 4 }
}
```
What it shows:
* **Variables as state** – `alive` is `v3`, which the gators change from outside with
`parent.v3 -= 1`: the parent/child link is the communication channel.
* **Waiting = looping with yield**: every wait is a `while … yield` that also calls `cap`, so the
energy cap holds every frame (also after a respawn).
* **The spawn rule**: move, `yield`, then `spawn` – without the yield the alligator would appear at
the master's old position (it copies the matrix of the last frame).
* **The exit** is the LevelDoor logic written out: material 16 under the llama in the same cell →
`endLevel(tribe, world, chapter)`. Setting g101–g105 additionally arms the Llama's built-in check
for the same cell.
## 5. The alligator: `ArenaGator`
`ArenaScripts.gator()` reuses the original Alligator's assets but has its own behaviour. Its parts:
**Setup** – model, size, collision and how it takes hits:
```
setModel(model:47)
scale(1.5)
self.hitDamage = 1793 // 1 | 256 | 512 | 1024
collisionOn<16384>()
collRadius(409r)
collisionBone(0, 409r) // three collision spheres along the body
collisionBone(6.0, 409r)
collisionBone(10.0, 409r)
on touched {
if (self.damageTaken & 1792) { // kick, dive or charge of the llama
if (cool <= 0 && lives > 0) { hit = 1 }
} else {
bit = 1 // contact without an attack: it bit the llama
}
}
```
`hitDamage` 1793 shares a bit with all three llama attacks, so each of them counts as the llama's
hit; bit 1 makes a plain contact hurt the llama ([Collision and damage](README.md#collision-and-damage)).
The handler only sets flags – the main loop reacts to them, which keeps the handler short.
**Fade-in** – `setAmbient` from 0 to 63 over 32 frames, playing the idle animation.
**Main loop** – a state machine in a `switch`, once per frame:
| State | Behaviour |
|---|---|
| 0 watch | turn towards the player (2° per frame); after a timer, if the llama is within 5 cells and on the same floor height (±0.4), play the snap animation and sound |
| 1 snap | wait for the animation, then start chasing |
| 2 chase | accelerate up to `0.03 + 0.006 · wave` per frame towards the llama; lunge when closer than 1.2, give up after 75 frames |
| 4, 5 lunge | fast forward move, then slow down (numbers from the original Alligator) |
| 3 back off | move backwards for a few frames, then rest and watch again |
Before the `switch`, the flags from the handler are handled:
* `hit`: lose a life, sound, three green particle puffs, *coins* `EnemyCoin`s, blink for 45
frames (alpha 0.25 and a red tint every 4 frames, no further hits meanwhile), back off.
* `bit`: back off and rest 90 frames, so the llama gets a breather.
After the `switch` the gator is clamped to the pit and to the floor height.
**Death** – when `lives` reaches 0 the loop `break`s: collision off, the original's death (shrink
over a few frames with puffs, five `EnemyCoin`s), then `parent.v3 -= 1` (one alive less for the
master), `parent.v4 += 1` (kills) and `remove`.
**`sub aim`** – ported from the original Alligator: each frame it publishes its position in
g17–g19 (and the distance in g20) if it is the best target below a jumping llama. That is what makes
the llama's **dive** home in on it.
## 6. The mod (`ArenaMod`)
`apply` refuses a chapter another mod already wrote, builds the WAD, rewrites the tally total
(`LevelTally.counted`: 2 placed coins + every coin the alligators can drop + 5 bonus), writes it
to `wads/T1L9M0nn.wad` and draws a loading picture with Java2D (`CorEncoder`). That is all a level
mod needs: LlamaMod's mod pipeline puts the files into the game's CD folder and links the empty
TestLevel chapters.
## 7. How it was tested
* `ArenaTest` compiles the scripts for every corner of the options, checks wave sizes, builds the
level from the CD and decodes the merged code (the master spawns the gator unit, the gator
spawns EnemyCoin).
* In the game ([`tools/apptest.py`](../../tools/apptest.py)): a probe type (`ArenaProbe`, option
`probe=true`) proved that compiled `on touched` handlers fire; then fights with default options,
a quick win with `lives=1, waves=1`, and a defeat (energy 0 → *Continue* restarts the arena).
![Defeated alligator: puffs and coins](../img/strats/arena_defeat.jpg)
![Win: bonus coins on the exit pad, then the tally](../img/strats/arena_win.jpg)
## What to take from it
1. Borrow assets by **compiling against the level that has them** and importing with `StpcMerge`.
2. Keep event handlers short; set flags and react in the main loop.
3. Communicate through `parent.vN` and globals; spawn only after a `yield`.
4. Copy numbers (speeds, animation indices, type fields) from the original type's script.
5. Write the exit yourself if you need a condition – material 16 + same cell + `endLevel`.
+101
View File
@@ -0,0 +1,101 @@
# Customising strats
From the smallest change to a completely new object type. Steps 1–5 need only the level editor;
step 6 is for programmers writing a LlamaMod mod.
| Step | What changes | Where it is stored |
|---|---|---|
| 1. Object parameters | one placed object | the project's object list |
| 2. Constants | a number in a type's code – every object of that type | `stratEdits.constants` |
| 3. Import a type from another level | the level's code gets a new type with its models, sounds, texts | the project's import list |
| 4. Clone a type | a new type as a copy of an existing one | `stratEdits.types` (source text) |
| 5. New type from a script | a new type written from scratch | `stratEdits.types` |
| 6. Script types in a mod | types compiled and merged by Java code | the mod's source |
A project never stores game code: constants are stored as *old → new value at an offset*, custom
types as source text, imports as *type X from level Y*. Everything is rebuilt from the player's own
CD when the project is opened ([notes/editor-format.md](../../notes/editor-format.md)).
## 1. Object parameters
Select an object; the Inspector's *Object parameters* section shows its MAP parameters (= the
variables v0, v1, … of its strat). For many common types the editor knows names, ranges and meanings
(schemas in [`Schemas.java`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/objects/Schemas.java));
the rest are raw values with the values used in the original levels as a hint.
User guide: [manual chapter 9](../manual/09-objects.md).
## 2. Constants
Double-click a line with a number in the Script panel (or *Edit value…*). The change applies to
every object of the type, is undoable and turns the line orange. On load the old value is checked
first, so a project made for another game version cannot corrupt the code silently. A
`pushShort` holds only −8.0 … 7.99; larger values need a source edit.
![Original and edited Coin constant in the game](../manual/img/strat_coin_constant_ab.jpg)
## 3. Importing types from other levels
*Assets → All worlds* lists every object of every level; dragging one into the viewport imports it.
The import copies the type's code **and everything it reaches**: spawned sub-strats (projectiles,
shards, heads), models, animation sets, textures, sound samples and dialogue lines, relocates all
references, shares what the level already has (Coin, EnemyCoin, Sparkles …) and verifies the
merged code. Limits: 16 MB level memory, 128 sounds, 127 texture pages.
Details: [notes/editor-cross-import.md](../../notes/editor-cross-import.md),
[manual chapter 9](../manual/09-objects.md#importing-objects-from-other-worlds).
## 4. Cloning a type
*Clone as new type…* in the Script panel copies a type's listing into the type editor, renamed.
Change what you need, *Check* (assemble and verify) and *Create type*. The new unit is appended at
the end of the level's code – nothing else moves – and the type appears in *Assets → Objects*.
* **Type fields / parameters from** decides which existing type's MAP fields (variables, slots,
collision flags, extra words) and parameter names new objects get. Pick the type you cloned,
unless you changed how many variables or collision bones the code uses.
* *Edit type…* and *Delete type…* work later on, with undo; placed objects follow their type.
* Custom types can refer to each other by name (`unit:OtherType`).
The manual's tutorial *Make the squirrel run in a circle* clones Bucky and inserts a circle loop
([manual chapter 22](../manual/22-strats.md#tutorial-make-the-squirrel-run-in-a-circle)).
![Two squirrels running in circles: waypoint chain and custom script](../manual/img/strat_bucky_circle.jpg)
## 5. A new type from a script
*Level → New object type from script…* opens the type editor with a template. Set *Language* to
*Script* and write the behaviour in the [script language](script-language.md). Models, animations
and sounds are those of the current level (`model:N`, `anim:N`, `sound:N` – look them up in the
Script panel of a type that uses them). Choose *Type fields from* a type with similar needs (a
pick-up: Coin; a walking enemy: one of the enemies).
## 6. Script types in a mod (Java)
A mod can do the same in code. The arena shows the pattern
([`mods/arena/`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/)):
1. Write the types as script-language text (often with values filled in from options).
2. **Compile against a source level** whose models, animations and sounds you want to use – the
indices in the script are that level's:
`StratAssembler.assemble(StratCompiler.compile(src), StratListing.layout(stpcImage, base))`
and append the words as a new unit to the source level's STPC (`ArenaImport.append`).
3. **Merge into the base level** with `StpcMerge` (the same engine as the editor's import): it
copies the closure (models, animation sets, textures, sounds, spawned strats), relocates and
verifies. Add one unplaced prototype MAP record per type with its type fields.
4. **Build the level** with a `LevelScript` / `LevelBuilder`
([`levelbuild/`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/)) and place the types by
name with `l.strat(...)`.
5. Write the WAD into a TestLevel chapter from the mod's `apply` and add a loading picture.
The [arena page](arena.md) walks through every part of this.
## Choosing type fields
| Field | What to put there |
|---|---|
| variables | at least the highest `vN` the code uses + 1 (parameters count as variables) |
| slots | number of event handler slots (`on …` blocks) – at least 1 if there is one |
| type flags | 0 for invisible logic; collision types need collision flags (Coin: 0x28000; Alligator: 0xC000 / 0xC400) |
| extra | collision-bone words: at least the number of `collisionBone` calls |
Wrong values do not always fail at once: too small collision areas corrupt the strat heap and
crash later (page fault in `FUN_0041ae20`).
+114
View File
@@ -0,0 +1,114 @@
# Reading scripts
LlamaMod shows every strat in two forms: the **Script view**, a readable reconstruction, and the
**Assembly** listing, the exact instructions. Both are in the level editor's *Script* panel
(**View → Panels → Script**, **Ctrl+Shift+K**); the user-level walkthrough is
[manual chapter 22](../manual/22-strats.md).
![The Script panel with Bucky's script](../manual/img/strat_script_panel.jpg)
Code: [`StratListing`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratListing.java)
(assembly), [`StratPseudo`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratPseudo.java)
(script view), [`OpTable`](../../src/main/java/com/eliasmueller/llamamod/format/strat/OpTable.java)
(names and help texts).
## Example: the Coin
Script view of the village coin:
```
strat Coin:
setModel(model:5)
collRadius(409r)
onEvent ev_0000, 0 // event type 4 → L_0084
if (v1) { collisionOn<32768>() ... }
if (v0) {
if (self.floorMaterial == -1.0) { skipArgs … } else {
self.y = self.floorHeight + 204r
self.rotY = rnd(4096.0)
}
}
loop { turnLeft(5.625deg); yield }
```
Line by line:
| Line | Meaning |
|---|---|
| `setModel(model:5)` | show model 5 of this level's STPC (the coin) |
| `collRadius(409r)` | collision radius 409/4096 ≈ 0.1 cell |
| `onEvent ev_0000, 0` | install the handler table entry `ev_0000` in slot 0: event type 4 (touched) jumps to `L_0084`, the pick-up code |
| `if (v1) …` | parameter 1 switches collision on |
| `if (v0) …` | parameter 0 (`snapToFloor` in the editor) puts the coin 204/4096 ≈ 0.05 above the floor under it and turns it randomly – unless there is no floor (`floorMaterial == -1`) |
| `loop { turnLeft(5.625deg); yield }` | spin 5.625° per frame, forever |
Changing `204r` to `1.5` makes every snapping coin float 1.5 cells high – that is a constant edit
([Customising strats](custom-types.md)).
## Script view conventions
| You see | It means |
|---|---|
| `self.x`, `player.y`, `parent.v3` | property or variable of this strat, the player, the spawner |
| `v0`, `snapToFloor` | variable 0; named when the editor knows the type's parameter |
| `g12` | global variable 12 |
| `1.0`, `5.625deg`, `204r` | fixed point (1.0 = one cell), an angle, a raw integer (204/4096) |
| `op<a, b>(x, y)` | instruction `op` with inline operands `a, b` and stack arguments `x, y` |
| `loop { … yield }` | runs once per frame |
| `while`, `do … while`, `switch`, `break`, `continue`, `break name` | rebuilt from the jumps |
| `a && b`, `a \|\| b` | short-circuit conditions (chains of conditional jumps) |
| `goto L_xxxx` | jumps that do not form nested blocks – 31 places in the whole game, all one pattern in the llama's script |
The script view is a **reading aid** and is not compiled back. Edits happen on the assembly (or
in the script language for new types).
## The assembly listing
One instruction per line, exactly as stored:
```
Coin:
debugName "Coin"
address model:5 ; model
setModel
onEvent ev_0000, 0 ; event type 4 → L_0084
pushVar snapToFloor ; v0 (alias from the object schema)
jz L_018c
...
L_0218:
push 5.625deg ; = 64.0
turnLeft
jmpYield L_0218
end
```
* Short opcodes (below 0x45) carry a signed 16-bit operand: `pushVar v3`, `pushShort 1.5`,
`jz label`. Long opcodes take inline words: `spawn 3, 6, 0, 0, 0`.
* Directives: `.unit`, `.alias`, `.word`, `.string`, `.event`, `.switch` / `.case`, `.at`.
* Disassemble → assemble is byte-identical for all 1461 units of the game; no instruction is
emitted as raw words.
Full syntax: [notes/strat-language.md](../../notes/strat-language.md). Every opcode with its stack
effect and meaning: [notes/strat-opcodes.md](../../notes/strat-opcodes.md).
## Finding things
* **Which script runs this object?** Select it with *Follow selection* on.
* **Who spawns this type?** The side panel lists the units that reference it.
* **What does this instruction do?** Select the line; the side panel shows the opcode's
semantics, stack effect, Argonaut name and confidence.
* **Where is this variable used?** *Uses* lists every line with the selected operand.
* **Outside the editor:** [`tools/strats.py`](../../tools/strats.py) `level.wad --all` lists every
named strat of a level with code offsets and type fields;
[notes/strat_types_table.md](../../notes/strat_types_table.md) shows which level has which type.
## Typical patterns
| Pattern | Meaning |
|---|---|
| `v1 * 30` | seconds → frames |
| `self.distToPlayerSqXZ < 25` | player within 5 cells (squared distance) |
| `turnTowardAngleY(self.angleToPlayer, 2deg)` | turn towards the player, 2° per frame |
| `if (!self.animPlaying) animPlay(anim:N)` | loop an animation |
| `wpFirst()`, `wpNext()`, `self.wpParam` | walk the waypoint chain; per-node parameter |
| `g95 = g95 \| (1 << (v - 1))` | set an event bit that guards wait for |
+20
View File
@@ -0,0 +1,20 @@
# Example: the roller coaster
> **Coming soon.** A roller-coaster mod is in development. This page will walk through it the way
> [the arena page](arena.md) does, once it is finished. Until then, its lab notes will appear as
> `notes/mod-coaster.md` and its code under `src/main/java/com/eliasmueller/llamamod/mods/coaster/`.
<!-- TODO (docs): fill in when mods/coaster is merged: overview + screenshots (docs/img/strats/),
files table, the track geometry, the cart/player strat scripts step by step, options, tests,
lessons learned. Link notes/mod-coaster.md and the 18-mods.md section. -->
## What is already known about the original rides
* Catacombs chapters 1, 3 and 5 (`t1l7m001`, `t1l7m003`, `t1l7m005`) are roller-coaster rides. In
these levels the player is not the `Llama` strat but a **`RollerCoaster`** strat
([Objects](../reference/objects.md#special-objects)).
* The rides have the longest views of the game: fog end up to 23 cells
([Engine facts](../reference/engine-limits.md#drawing)).
* The ride music is track 25, `ROLLER` ([Music](../reference/audio.md#music)).
* Objects of these levels can be imported into other levels, but they only work with matching
geometry ([manual chapter 9](../manual/09-objects.md#limits-and-things-to-know)).
+87
View File
@@ -0,0 +1,87 @@
# The script language
LlamaMod's **script language** (version 2) is a small structured language in the style of the
Script view. [`StratCompiler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratCompiler.java)
compiles it to strat assembly, and
[`StratAssembler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratAssembler.java)
turns that into bytecode with the same checks as hand-written assembly. It is used by the editor's
type editor (*Language: Script*) and by mods such as the [arena](arena.md). Complete reference:
[notes/strat-language.md](../../notes/strat-language.md#script-language-stratcompiler--writing-new-behaviour);
tests: `StratCompilerTest`.
## A first strat
```
// bobs one cell up and down and turns
strat Bobber {
alias base = v0
alias phase = v1
setModel(model:5) // any opcode: name<inline operands>(stack arguments)
base = self.y + 1 // 1 = one cell
phase = 0
on touched { // event handler (type 4), runs as a subroutine
soundPlay(sound:6)
}
loop {
phase = phase + 4deg
self.y = base + sin(phase)
turnLeft(4deg)
yield // end of this frame
}
}
sub wobble { // called with "call wobble", ends with return
self.rotZ = sin(self.rotY) * 0.1
}
```
## Building blocks
| Construct | Notes |
|---|---|
| `strat Name { … }` | a type; its label `Name` is the entry point |
| `sub name { … }`, `call name`, `return` | subroutines |
| `alias name = v3` | a name for a variable; parameter names from the object schema are predefined |
| `x = e`, `x += e`, `-=`, `*=`, `/=` | assignment; `x += 1` compiles to `inc` |
| places | `vN`, alias, `gN`, `self.prop`, `player.prop`, `parent.prop`, `parent.vN`, `camera.` / `target.` / `boss.` / `dialog.prop` |
| `if (c) { } else if (c) { } else { }` | |
| `while (c) { }`, `loop { }`, `do { } while (c)` | a `yield` as the last statement of a loop becomes the loop's frame-ending back jump |
| `switch (e) { case 1: … break default: … }` | C rules (fall through without `break`); case values are constants |
| `break`, `continue`, `yield`, `end`, `remove` | |
| `on touched { … }`, `on event(type[, param]) [slot n] { … }` | event handlers; touched needs `collisionOn<…>()` and `collRadius(…)` |
| `op<a, b>(e1, e2)` | **any opcode** by name: inline operands in `<>` (raw words), stack arguments in `()`; the count must match the opcode |
### Numbers
* `10` and `10.0` are ten cells (fixed point); `1.5`, `90deg` as in assembly.
* **Raw** integers: `215r` (= 215/4096) or hex `0x10`. Values inside `<…>` are always raw.
* Operators over constants are folded at compile time with the VM's own fixed-point rules:
`2 * 3 + 0.5` → `6.5`.
* `use rawNumbers` switches to the version-1 rule (plain integers raw) – old scripts get it
automatically.
### Operators (low → high precedence)
`||` · `&&` · `|` · `&` · `==` `!=` · `<` `>` `<=` `>=` · `<<` `>>` · `+` `-` · `*` `/` · unary
`-` `!`. In conditions `&&` / `||` short-circuit; in a value both sides are evaluated.
### References
`model:N`, `anim:N`, `unit:N[+off]` or `unit:Name` (another custom type), `sound:N`, `tex:N` –
indices into the level the code is compiled against.
## What the compiler does not do (yet)
* no `for` loop, no typed variables (everything is a 32-bit word, fixed point by convention);
* `spawn` takes raw type fields (`spawn<0, 16, 5, 0xC400, 3>(unit:…)`) – you must know the
values the spawned type needs;
* the Script view does not show event handlers as `on … { }` blocks.
## Tips
* Look at an original type that does something similar first (Script panel) and copy its numbers
– speeds, animation indices, sounds and type fields are level-specific.
* Keep each frame's work small and always reach a `yield` in loops; a loop without a yield freezes
the game.
* Test in the game early: the compiler checks the code's form and references, not whether the
object behaves well.
+121
View File
@@ -0,0 +1,121 @@
# Tools
Everything in this repository that you can run, what it is for and where its code and notes are.
None of the tools contain game data; they all work on your own copy of the game.
## LlamaMod (Java app)
The main product: a mod manager with a launcher and a 3D level editor, for Windows and Linux.
Java 25 + Swing, sources in [`src/main/java/com/eliasmueller/llamamod/`](../src/main/java/com/eliasmueller/llamamod/).
![The LlamaMod launcher](img/launcher.jpg)
| Part | What it does | Docs |
|---|---|---|
| launcher | first-start wizard (copies the game from CD/ISO), mod list with options, save-game editor, starts the game (Wine on Linux) | [manual: Installing](manual/02-installing.md), [manual: Mods](manual/18-mods.md), [notes/llamamod-app.md](../notes/llamamod-app.md) |
| level editor | 3D editor for new and original levels: brushes, tiles, meshes, textures, objects, waypoints, cameras, zones, lights, visibility, strat scripts, play-test, glTF export | [editor manual](manual/index.md) |
| mods | Test World, Alligator Arena, level projects, Boot to level, Intro skip, Mirror mode, Practice tools, Randomizer, Draw distance, Built-in cheats, Debug wireframe, Free camera | [manual: LlamaMod mods](manual/18-mods.md), `notes/mod-*.md` |
### Command line
```text
llamamod start the GUI
llamamod --version show the version and build id
llamamod --install <iso|folder> set up the game data
llamamod --mods list available mods and their options
llamamod --apply [id,id,...] [--opt id.key=value ...]
apply mods (none = original); options are saved
llamamod --launch [--wait] start the game
llamamod --registry show the registry values
llamamod --saves show save games
llamamod --unlock <slot 1-3> <world> <chapter> [--cursor]
llamamod --editor [project.llamalevel | t1l1m001] level editor
llamamod --editor-build <project.llamalevel> <out.wad> [--cd <dir>]
llamamod --editor-import <level.wad|t1l1m001> <project.llamalevel> [--cd <dir>]
llamamod --editor-new <project.llamalevel> [base level] [--name <name>] [--cd <dir>]
llamamod --import-level <project.llamalevel> (into the level folder, shown as a mod)
llamamod --remove-level <id|file> move a level mod's file to backups/levels/
```
Data folder: `-Dllamamod.data=<folder>` or `LLAMAMOD_DATA` (default `~/.local/share/LlamaMod`,
Windows `%LOCALAPPDATA%\LlamaMod`). Source: [`Main.java`](../src/main/java/com/eliasmueller/llamamod/Main.java).
### Debug view
The *Debug wireframe* mod
([`mods/debug/`](../src/main/java/com/eliasmueller/llamamod/mods/debug/)) adds visible geometry to
every level: collision as coloured frames (invisible walls filled red, the exit floor violet, death
planes magenta) and markers for invisible objects (start, exit, checkpoints, cameras, triggers,
zones). Gameplay is unchanged. Its Python ancestor is `tools/debugviz.py`; colour legend and
details: [notes/debugviz.md](../notes/debugviz.md).
![Debug view in Village 3](img/debug-view.jpg)
## Python kit (`tools/`)
The reverse-engineering toolkit the project started with. Python 3.8+ with `numpy` and `Pillow`
(`capstone` for `disx86.py`). All parsers rebuild the original files byte for byte.
| Tool | Purpose |
|---|---|
| [`wadtool.py`](../tools/wadtool.py) | list, extract and replace WAD chunks |
| [`geomlib.py`](../tools/geomlib.py) | library: WAD, TRAK, STPC models, MAP tile part, TEXT, collision |
| [`geom_export.py`](../tools/geom_export.py) | level → OBJ/MTL with textures and vertex colours, `--collision` (collision.obj by material), `--objects`, preview renders |
| [`geom_import.py`](../tools/geom_import.py) | OBJ → new world geometry in a copy of a level |
| [`geom_demo.py`](../tools/geom_demo.py) | example geometry for `geom_import.py` |
| [`mapchunk.py`](../tools/mapchunk.py) | MAP parser/writer |
| [`map_dump.py`](../tools/map_dump.py) | print a level's tiles, strats, lights |
| [`strats.py`](../tools/strats.py) | object types of a level: names, instance counts, code offsets, type fields |
| [`strat_table.py`](../tools/strat_table.py) | type × level table → [notes/strat_types_table.md](../notes/strat_types_table.md) |
| [`stpcwalk.py`](../tools/stpcwalk.py) | rough walk through STPC (models, animations, code) |
| [`textchunk.py`](../tools/textchunk.py), [`text_export.py`](../tools/text_export.py) | TEXT chunk; export pages to PNG and import them back |
| [`texsheet.py`](../tools/texsheet.py) | contact sheet of a level's textures with indices |
| [`levelbuild.py`](../tools/levelbuild.py) | level builder from a Python script ([guide](guide/levels-in-code.md)) |
| [`make_testlevel.py`](../tools/make_testlevel.py) | early experiment: one object per type on a floor plate |
| [`debugviz.py`](../tools/debugviz.py), [`debugviz_check.py`](../tools/debugviz_check.py) | debug-view WADs for all levels and an offline check |
| [`cor2png.py`](../tools/cor2png.py), [`png2cor.py`](../tools/png2cor.py) | loading pictures (COR) ↔ PNG |
| [`asf2wav.py`](../tools/asf2wav.py) | music (ASF) → WAV, exact decoder |
| [`disx86.py`](../tools/disx86.py) | disassemble `groove.exe` at a virtual address |
| [`vorschau/`](../tools/vorschau/) | Blender (Eevee) preview renders of a level with labels |
| [`lama-mod`](../tools/lama-mod) | shell script: CD overlay of symlinks for mods in a hand-made Wine setup (pre-LlamaMod) |
Level scripts in [`levels/`](../levels/): `testwelt.py` (the original test world),
`sprunglabor.py` (jump lab used to measure the llama), `testwelt_parkour.seq` (scripted run).
Several scripts have **hard-coded paths** to the original author's Wine setup at the top (see
[Levels from scripts](guide/levels-in-code.md#a-python-level)).
## Testing in the game
| Tool | What it does | Notes |
|---|---|---|
| [`tools/apptest.py`](../tools/apptest.py) | runs the game **through LlamaMod** in an isolated session: own data folder, Wine prefix, invisible X display (Xvfb), muted private PulseAudio; applies mods, unlocks a level, navigates the menus, presses keys, takes screenshots, detects crashes | [notes/llamamod-app.md](../notes/llamamod-app.md#testing-mods-in-game-toolsapptestpy) |
| [`tools/lamatest.py`](../tools/lamatest.py) | the older harness for a hand-made Wine setup: loads a WAD as TestLevel and runs a key sequence | [notes/testharness.md](../notes/testharness.md) |
Linux only; needs Wine, Xvfb, xdotool, ImageMagick, tesseract (German data, for reading menu
texts) and PulseAudio. `apptest.py` defaults to a session root inside the author's job folder –
set `APPTEST_ROOT`.
```bash
./gradlew fatJar
tools/apptest.py run demo --display 7 --mods arena --level 9 49 \
--seq "Up:1.5 shot:walk z shot:kick" --out /tmp/demo
```
## Reverse engineering
* `re/ghidra_scripts/DumpAll.java` – headless Ghidra script that decompiles every function of
`groove.exe` into one C file. The exe, the decompile and (currently) the script itself are
excluded from the repository (`.gitignore`). Run
`analyzeHeadless <projdir> groove -import groove.exe -scriptPath re/ghidra_scripts -postScript DumpAll.java groove_decomp.c`
(Ghidra 12.x needs JDK 25).
* Key functions: MAP loader `0x42ac50`, TRAK `0x42aac0`, STPC `0x42ab50`, strat VM `0x54d180`,
chunk dispatcher `0x558960`.
## Other folders
| Folder | Content |
|---|---|
| [`examples/`](../examples/) | `testwelt.llamalevel`: the original test world as an editor project |
| [`packaging/`](../packaging/) | icons, Windows (launch4j) and Linux packaging files |
| [`deb-build/`](../deb-build/) | older script (German) that builds a Debian package of the game itself from your ISO, with the soundtrack converted to MP3 |
+64
View File
@@ -0,0 +1,64 @@
# Lab notes
The project's engineering notebook: what was found in `groove.exe` and the game data, how each part
of LlamaMod works, and what was verified in the game. The notes are detailed and technical (with
addresses, confidence levels and test logs). For a readable introduction start with the
[documentation](../docs/README.md) instead.
Images used by the notes are in [`img/`](img/).
## Formats and engine
| Note | Topic |
|---|---|
| [geometry.md](geometry.md) | TRAK / MAP / STPC models, coordinate spaces, collision, visibility, writing new geometry |
| [objects.md](objects.md) | MAP layout, strat records, waypoints, zones, special objects, the other chunks |
| [textures.md](textures.md) | TEXT chunk |
| [strat-opcodes.md](strat-opcodes.md) | all 434 strat VM opcodes, conventions, sources |
| [strat-language.md](strat-language.md) | LlamaMod's strat assembly, script view and script language |
| [strat_types_table.md](strat_types_table.md) | every object type in every level, with counts |
| [editor-object-schemas.md](editor-object-schemas.md) | parameters of object types, guards, event bits, start camera |
| [testlevel-chapters.md](testlevel-chapters.md) | world 9 chapters and the relocation patch |
| [levelbau.md](levelbau.md) | the Python level builder, measured physics, the exit, the test world |
| [debugviz.md](debugviz.md) | debug view: colour legend, how it is built |
| [testharness.md](testharness.md) | `tools/lamatest.py`, PC controls |
## LlamaMod app and mods
| Note | Topic |
|---|---|
| [llamamod-app.md](llamamod-app.md) | build, CLI, data folder, registry, mod framework, exe patches, save games, `tools/apptest.py` |
| [mod-arena.md](mod-arena.md) | Alligator Arena |
| [mod-testworld.md](mod-testworld.md) | Test World and its gallery |
| [mod-levels.md](mod-levels.md) | level projects as mods |
| [mod-boottolevel.md](mod-boottolevel.md) | Boot to level |
| [mod-introskip.md](mod-introskip.md) | Intro skip |
| [mod-mirror.md](mod-mirror.md) | Mirror mode |
| [mod-practice.md](mod-practice.md) | Practice tools |
| [mod-randomizer.md](mod-randomizer.md) | Randomizer |
| [mod-fog.md](mod-fog.md) | Draw distance |
| [mod-cheats.md](mod-cheats.md) | Built-in cheats |
| [mod-freecam.md](mod-freecam.md) | Free camera & noclip |
## Level editor internals
| Note | Topic |
|---|---|
| [editor-design.md](editor-design.md) | design brief |
| [editor-api.md](editor-api.md) | scene model, commands, IO |
| [editor-format.md](editor-format.md) | the `.llamalevel` project format |
| [editor-render.md](editor-render.md) | OpenGL viewport, coordinate conventions, culling |
| [editor-ui.md](editor-ui.md) | application shell and plugin API |
| [editor-tools-geometry.md](editor-tools-geometry.md) | brushes, tiles, collision, mesh import, library |
| [editor-tools-objects.md](editor-tools-objects.md) | objects, paths, cameras |
| [editor-tools-environment.md](editor-tools-environment.md) | zones, fog, music, lights, visibility, baking |
| [editor-tools-textures.md](editor-tools-textures.md) | texture manager, import, painting, animations |
| [editor-cross-import.md](editor-cross-import.md) | importing object types from other levels, engine limits |
| [editor-playtest.md](editor-playtest.md) | play-test straight into the level |
| [editor-manual.md](editor-manual.md) | the manual window and context help |
| [editor-guide.md](editor-guide.md) | early short user guide (superseded by [docs/manual](../docs/manual/index.md)) |
## Process
[`dev/`](dev/) holds the briefs for the AI agents that worked on the project and the review reports
of the overnight UX and bug reviews (`bug-review-*.md`, `ux-review-*.md`).
File renamed without changes.
@@ -60,7 +60,7 @@ objdump, replay, restore).
After rebasing onto the merged main tree (simp-all + launcher UX changes): orig, all-v, boot-exe, mrf-r
re-run with the final jar: all OK (CDPath `L:\cd-active\`).
![matrix](img/launcher-matrix.jpg)
![matrix](../img/launcher-matrix.jpg)
Screens: all mods in Village (wireframe, mirrored) and the free camera; River with mirror+randomizer+fog;
Test World with all mods; boot-to-level + all exe mods (River loading card); practice restore.
@@ -14,20 +14,20 @@ position) started straight in the level, and Escape in the game returned to the
| Before | After |
|---|---|
| ![](img/editor/ux_llama_before.jpg) | ![](img/editor/ux_llama_after.jpg) |
| ![](../img/editor/ux_llama_before.jpg) | ![](../img/editor/ux_llama_after.jpg) |
| A 1920-wide window: the Inspector is 170 px wide and shows no values at all. Side docks have two rows of tabs. | The docks get their intended widths (290 / 340 px). Side-dock tabs are icons with the active one named. The engine fields of the object type are folded away. |
| ![](img/editor/ux_brush_before.jpg) | ![](img/editor/ux_brush_after.jpg) |
| ![](../img/editor/ux_brush_before.jpg) | ![](../img/editor/ux_brush_after.jpg) |
| The Brush tool's buttons read "Bl…", "Pl…", the texture names "gr…", the numbers show "0,5". | Full labels and a decimal point. |
| ![](img/editor/ux_coin_before.jpg) | ![](img/editor/ux_coin_after.jpg) |
| ![](../img/editor/ux_coin_before.jpg) | ![](../img/editor/ux_coin_after.jpg) |
| The Coin tool's options are cut off: only the top pixels of the fields are visible. | The options are on one line, or wrap to a second row in narrow windows. |
| ![](img/editor/ux_menu_before.jpg) | ![](img/editor/ux_menu_after.jpg) |
| ![](../img/editor/ux_menu_before.jpg) | ![](../img/editor/ux_menu_after.jpg) |
| *Help* comes before *Geometry*, and the shortcuts are German ("Strg", "Umschalt"). | The menus are File … Geometry, Tools, Help, and the keys are named "Ctrl", "Shift". |
| ![](img/editor/ux_orig_before.jpg) | ![](img/editor/ux_orig_after.jpg) |
| ![](../img/editor/ux_orig_before.jpg) | ![](../img/editor/ux_orig_after.jpg) |
| *Open original level*: the description is cut at the right edge, and the selected row's id is grey on orange. | The text wraps and the id is readable. |
| ![](img/editor/ux_small_before.jpg) | ![](img/editor/ux_small_after.jpg) |
| ![](../img/editor/ux_small_before.jpg) | ![](../img/editor/ux_small_after.jpg) |
| A 1280×720 window: the right dock is squeezed to 130 px. Also, Esc did not close the dialog, so it was still open and blocked every key. | The docks keep their sizes and the tool options wrap. Esc closes dialogs. |
![](img/editor/ux_quit_game.jpg)
![](../img/editor/ux_quit_game.jpg)
*New: Play while the game still runs offers to quit it.*
@@ -20,27 +20,27 @@ covered by code review), Windows (the antivirus notice is unchanged there).
| Before | After |
|---|---|
| ![](img/app/ux_start_before.jpg) | ![](img/app/ux_start_after.jpg) |
| ![](../img/app/ux_start_before.jpg) | ![](../img/app/ux_start_after.jpg) |
| The app opens with the mod list scrolled to the end (Visual, Cheats); Levels and Gameplay are out of sight. | The list opens at the top and keeps its place when it is rebuilt (Apply, theme). Short windows get a lower header. |
| ![](img/app/ux_mods_before.jpg) | ![](img/app/ux_mods_after.jpg) |
| ![](../img/app/ux_mods_before.jpg) | ![](../img/app/ux_mods_after.jpg) |
| Options have no explanation unless you hover (Worlds "all"? Checkpoint spacing "10" of what?). Descriptions talk about "far clip plane", "strafe", "rigid object models". | Every option is explained next to it. Mod descriptions say what happens in the game and which keys to press. |
| ![](img/app/ux_wizard_before.jpg) | ![](img/app/ux_wizard_after.jpg) |
| ![](../img/app/ux_wizard_before.jpg) | ![](../img/app/ux_wizard_after.jpg) |
| A text file is "shorter than expected (truncated?)", a wrong folder gets "expected Setup/…/Hd/groove.exe", a half-downloaded ISO is "Game found" and fails only after Set up. | Plain messages that say what to do; an incomplete ISO is caught at once, with both sizes. |
| ![](img/app/ux_exe_before.jpg) | ![](img/app/ux_exe_after.jpg) |
| ![](../img/app/ux_exe_before.jpg) | ![](../img/app/ux_exe_after.jpg) |
| On Linux a missing groove.exe is blamed on antivirus software and explained with Windows Defender menus. After Rebuild you have to click Play again. | Linux gets a short message; **Rebuild** writes the file and starts the game. |
| ![](img/app/ux_saves_before.jpg) | ![](img/app/ux_saves_after.jpg) |
| ![](../img/app/ux_saves_before.jpg) | ![](../img/app/ux_saves_after.jpg) |
| "lowest 4 bits of the coin count", "unknown item flag (bit 0)", "Cursor on", "Revert"; no hint how to restore a backup. | Plain wording, "Opens at", "Undo changes"; **Import…** starts in the backups folder and the hint says so. |
| ![](img/app/ux_settings_before.jpg) | ![](img/app/ux_settings_after.jpg) |
| ![](../img/app/ux_settings_before.jpg) | ![](../img/app/ux_settings_after.jpg) |
| "registry values ScreenW/ScreenH", "wine from PATH"; the mod cache shows 0.0 MB after applying 6 mods (stale). | "Automatic (1920x1052)" shows the real size, plain hints, a note on what *Set up again* keeps; the cache size is current. |
| ![](img/app/ux_dark_before.jpg) | ![](img/app/ux_dark_after.jpg) |
| ![](../img/app/ux_dark_before.jpg) | ![](../img/app/ux_dark_after.jpg) |
| Dark theme: the *modifies groove.exe* badge is dark orange on grey (barely readable). | A lighter badge text in the dark theme. |
| New: quitting a hidden or hung game | New: crash report |
|---|---|
| ![](img/app/ux_quit_after.jpg) | ![](img/app/ux_crash_after.jpg) |
| ![](../img/app/ux_quit_after.jpg) | ![](../img/app/ux_crash_after.jpg) |
| While the game runs, Play becomes **■ Quit game** (neutral colour) with a confirmation. | Shown as soon as Wine reports the crash (the game hides behind Wine's error window until that is closed), with Quit game / Open logs. |
![](img/app/ux_av_linux_before.jpg)
![](../img/app/ux_av_linux_before.jpg)
*Before: on Linux every first Apply of an exe mod showed this Windows Defender notice. Now it is
shown on Windows only.*
+1 -1
View File
@@ -5,7 +5,7 @@ Code: `src/main/java/com/eliasmueller/llamamod/editor/tools/environment/`, tests
`src/test/.../editor/tools/environment/` (`EnvironmentToolsTest` needs `-Dllamamod.cd`; `EnvTour`
= scripted screenshots, `EnvScenarios` = the in-game test projects).
![zones](img/editor/env_zones.jpg)
![zones](img/editor/env_zone.jpg)
## What the user gets
@@ -28,7 +28,7 @@ import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
/** Regression tests for the bugs in notes/bug-review-editor.md (model, io, build). */
/** Regression tests for the bugs in notes/dev/bug-review-editor.md (model, io, build). */
class EditorRegressionTest {
private static Scene emptyScene() {
return new Scene();
@@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* B10 (notes/bug-review-editor.md): the restore of the user's mod selection after a boot-to-level
* B10 (notes/dev/bug-review-editor.md): the restore of the user's mod selection after a boot-to-level
* play-test is kept on disk, so an editor closed while the game runs restores it on its next start.
*/
class PlaytestSessionTest {
@@ -12,7 +12,7 @@ import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/** Regression tests for notes/bug-review-editor.md (document state, play-test install target). */
/** Regression tests for notes/dev/bug-review-editor.md (document state, play-test install target). */
class DocumentRegressionTest {
private static Project project(String name) {
Scene s = new Scene();
@@ -22,7 +22,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
/**
* B13 (notes/bug-review-editor.md): a player start whose grid cell has no collision floor at or
* B13 (notes/dev/bug-review-editor.md): a player start whose grid cell has no collision floor at or
* below it crashes the game while loading (page fault 0x405527). Positions verified in game
* (Village 1, boot-to-level start override, 2026-11-07): 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.