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
No files matched your search
@@ -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.
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
| **Launcher**: mods with options, save-game editor, one-click play | **Level editor**: brushes, tiles, meshes, objects, textures, play-test with F5 |
|
||||
|  |  |
|
||||
| **Test World**: all 253 figures and objects of the game in one level | **Alligator Arena**: a minigame with new, scripted enemies |
|
||||
|  |  |
|
||||
| **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
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
## 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
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
| The launcher: mods with options, save games, settings | The level editor with Village 1 |
|
||||
|  |  |
|
||||
| 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.
|
||||
@@ -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/).
|
||||
@@ -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.
|
||||
|
||||

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

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

|
||||
|
||||
## 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.
|
||||
@@ -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).
|
||||
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 301 KiB |
@@ -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) |
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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)).
|
||||
@@ -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).
|
||||
|
||||

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

|
||||
|
||||
## 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).
|
||||
@@ -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)) |
|
||||
@@ -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*".
|
||||
@@ -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)).
|
||||
|
||||

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

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

|
||||
|
||||

|
||||
|
||||
## 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`.
|
||||
@@ -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.
|
||||
|
||||

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

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

|
||||
|
||||
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 |
|
||||
@@ -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)).
|
||||
@@ -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.
|
||||
@@ -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/).
|
||||
|
||||

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

|
||||
|
||||
## 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 |
|
||||
@@ -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`).
|
||||
@@ -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\`).
|
||||
|
||||

|
||||

|
||||
|
||||
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 |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|  |  |
|
||||
| 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. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| The Brush tool's buttons read "Bl…", "Pl…", the texture names "gr…", the numbers show "0,5". | Full labels and a decimal point. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| 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. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| *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". |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| *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. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| 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. |
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||
*Before: on Linux every first Apply of an exe mod showed this Windows Defender notice. Now it is
|
||||
shown on Windows only.*
|
||||
@@ -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).
|
||||
|
||||

|
||||

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