33 Commits
Author SHA1 Message Date
MineTec 165134c0d8 0.2.4
build / build (push) Failing after 1m9s
release / release (push) Failing after 51s
2026-11-08 18:28:02 +01:00
MineTec 526059c70a mountain 3 crash fix mod (keeps the boulder spawners alive, one word of t1l5m003), object palette test skips without a display 2026-11-08 18:28:02 +01:00
MineTec ec2876d9c8 developer mode mod: game's developer switch on (warp with a+s, river log cheat), keys for the hidden aux inputs (fly, heart and fruit, idols, coins) on both exe versions 2026-11-08 18:09:03 +01:00
MineTec e36ff2d2c2 mountain 3 crash: rollingbouldermountain spawners end while their boulders still write parent.v13 (use after free), seen in game, one-word level fix tested; diag tooling in notes/dev 2026-11-08 17:41:19 +01:00
MineTec 8b3c38fe56 debug wireframe: allowance areas option (off, floating spots, all edges) from an exact port of the engine floor test, capped by level memory 2026-11-08 17:41:19 +01:00
MineTec db08cf8bf3 developer switch (strat property 258): 101 reads, a+s dev warp points, aux bits for hearts, fruit, coins, idols and flying, river log invincible noclip, flipped and tested in game 2026-11-08 17:33:05 +01:00
MineTec b5b6e22957 frog moonjump: kuscofrog's mid-air code rises while padaux bit 0x800 is set, nothing on pc ever sets it (verified in game by poking the bit) 2026-11-08 16:44:54 +01:00
MineTec 15ad050005 floor explainer: no side view or header, pink labelled allowance, all polygon numbers in the zoom 2026-11-08 16:29:59 +01:00
MineTec 74f8fec341 nicer floor explainer: terrain overview with grid and polygon numbers, zoom on both crate needles with the accepted slivers, side view 2026-11-08 16:26:16 +01:00
MineTec 8baed2e87c floor test explainer image (needle triangle, accepted sliver, kuzco, heights), poly 11 is the same needle on the crate's other side 2026-11-08 16:21:37 +01:00
MineTec 9e8e7f9a10 floor test inclusion rule from the code (edge planes with a small tolerance, highest plane wins): explains kuzco floating next to the crate in jungle day 2 (needle triangle tip), tools/floortest.py re-implements the engine test 2026-11-08 16:15:28 +01:00
MineTec c39d58bdac boxedwine notes: real amd gpu shows a black window, software gl works 2026-11-07 23:01:02 +01:00
MineTec 0c2920dfe4 browser feasibility notes: boxedwine runs the game natively into the 3d level, its web build has no opengl/webgl bridge so direct3d fails 2026-11-07 22:44:04 +01:00
MineTec d32d197b54 0.2.3
build / build (push) Successful in 1m5s
release / release (push) Successful in 3m18s
2026-11-07 21:18:12 +01:00
MineTec 0568ae7f24 exit logic corrected from in-game debugging: the leveldoor only sets g101-g105, the llama's script ends the level (material 16, exit cell, near the floor); stale floor record after a position jump explains the hover exit skip in jungle day 2 2026-11-07 20:57:15 +01:00
MineTec 64259ea15b fly key capture lets shift, ctrl and alt through, so the play-here crosshair no longer sticks after flying fast 2026-11-07 19:32:45 +01:00
MineTec 322e36c0f3 toolbar block icons centred in their 16px box (test for all icons), tutorials say where the objects tab is 2026-11-07 19:09:39 +01:00
MineTec 52c70e817d editor: fly keys work while another component has the keyboard focus (window-wide key dispatcher during right-mouse fly), held keys cleared when the flight ends 2026-11-07 19:03:20 +01:00
MineTec 1cc59bfb78 0.2.2
build / build (push) Successful in 49s
release / release (push) Successful in 1m31s
2026-11-07 18:34:32 +01:00
MineTec 932ccf5fc8 warning dialog for unsupported groove.exe versions at start and before the first apply of a session (nothing blocked) 2026-11-07 18:32:42 +01:00
MineTec ff6673af66 readme and docs: german and english game versions supported, what happens with other versions 2026-11-07 18:17:31 +01:00
MineTec ca22c75fac english groove.exe (2000-10-17) supported next to the german one: named exe sites found by signatures per build (german patches byte-identical), base patch and every exe mod on both, unknown builds get a clear message; display diagnostics moved to troubleshooting with a warning (it flashes every monitor black) 2026-11-07 18:14:15 +01:00
MineTec f984019199 gitignore the english groove.exe used for version support 2026-11-07 17:31:02 +01:00
MineTec abe97b6977 changelog: alligator arena, documentation and known limits for 0.2.1; current launcher and editor screenshots everywhere, coaster in the readme pictures
build / build (push) Successful in 57s
release / release (push) Successful in 1m16s
2026-11-07 16:51:40 +01:00
MineTec bc8408d963 save path of the relocated coin and flag arrays verified in game: saves after finishing jungle 2 and village 1 byte-identical to the old exe, testlevel coins never reach the save 2026-11-07 16:51:16 +01:00
MineTec a64df88c77 release automation: gitea actions build every push and publish tagged releases (version from the tag, packages, sha256sums, changelog section as text), simple 5-step install guide in the readme 2026-11-07 16:47:42 +01:00
MineTec f5fd411592 llama coaster: ride the catacombs coaster cart on a new track (loop or grand circuit with drops, climbs, boosts, spin pads and jumps), 1-3 laps, three speeds, coins; offline cart simulation checks every jump and climb; roller coaster chapter in the strats docs 2026-11-07 16:45:54 +01:00
MineTec 6d449257a7 tool paths configurable by environment (LLAMAMOD_CD, LAMA_WINEPREFIX, LAMA_MOD, APPTEST_ROOT, APPTEST_ISO; tools/lamapaths.py), music tracks without a file on the cd marked in the editor and the tutorial uses kronk, levelbuild.py accepts the english texture names 2026-11-07 16:34:17 +01:00
MineTec a0d961357e readme: large current screenshots of the launcher and of the editor with an open level, outside the picture table 2026-11-07 16:19:26 +01:00
MineTec 624718b893 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 2026-11-07 16:14:40 +01:00
MineTec 00b2880d67 base exe patch moves the per-chapter coin and flag arrays into free .data space (128 entries), so every testlevel chapter is safe (revision 2, caches rebuilt once); new projects start on a free chapter from 32, manual shows number ranges, texture swatch caches follow texture edits, stray levelbau.md.orig removed 2026-11-07 15:52:00 +01:00
MineTec d6b852a1b6 testlevel chapters 2-31 overwrite game memory (coin counter and flag rows end after world 8: progress, language, options): level projects, play-tests and the arena use chapters 1 and 32-49, old settings migrate; level tally counts the level's own coins and secrets 2026-11-07 15:27:08 +01:00
MineTec 987b0104fa levelbuild texture names mean the same picture on every base level (matched by pixels, missing village textures copied over in the java builder, clear error otherwise), editor texture choices follow the base 2026-11-07 15:27:08 +01:00
209 changed files with 10298 additions and 802 deletions

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Builds and tests every push and pull request (tests that need the game CD skip themselves).
name: build
on:
push:
branches: ['**']
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
25
21
- name: Build and test
run: |
./gradlew --no-daemon \
-Porg.gradle.java.installations.fromEnv=JAVA_HOME_25_X64 \
-Porg.gradle.java.installations.auto-download=false \
build
+61
View File
@@ -0,0 +1,61 @@
# Builds and publishes a release when a version tag is pushed:
# git tag v0.2.1 && git push origin v0.2.1
# The release gets the four packages (full/launcher x Windows/Linux), SHA256SUMS.txt and the
# matching CHANGELOG.md section as its text. Tests that need the game CD skip themselves here.
#
# Needs: Gitea Actions enabled and a runner with the label "ubuntu-latest" (act_runner).
# The built-in token is used; if it may not create releases on your instance, add a repository
# secret RELEASE_TOKEN (a token with write access to the repository).
name: release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: JDK 25 (toolchain) and 21 (runs Gradle)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
25
21
- name: Version from the tag
run: |
V="${GITHUB_REF_NAME#v}"
echo "VERSION=$V" >> "$GITHUB_ENV"
python3 tools/release/changelog_section.py "$V" > release-notes.md
echo "Release $V"; head -5 release-notes.md
- name: Build, test and package
run: |
./gradlew --no-daemon \
-Porg.gradle.java.installations.fromEnv=JAVA_HOME_25_X64 \
-Porg.gradle.java.installations.auto-download=false \
-PreleaseVersion="$VERSION" \
build packageAll
- name: Checksums
run: |
cd build/dist
ls -la
sha256sum LlamaMod-"$VERSION"-*.zip LlamaMod-"$VERSION"-*.tar.gz > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Publish
env:
GITEA_URL: ${{ github.server_url }}
GITEA_REPO: ${{ github.repository }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
run: |
python3 tools/release/publish_gitea.py \
--tag "$GITHUB_REF_NAME" --title "LlamaMod $VERSION" --notes release-notes.md \
build/dist/LlamaMod-"$VERSION"-*.zip build/dist/LlamaMod-"$VERSION"-*.tar.gz \
build/dist/SHA256SUMS.txt
+1
View File
@@ -1,5 +1,6 @@
# game data (copyrighted) - never commit
re/groove.exe
re/groove_en.exe
re/groove_decomp.c
re/proj/
re/ghidra.log
+131
View File
@@ -0,0 +1,131 @@
# Changelog
## 0.2.4 – 2026-11-08
- **New mod "Mountain 3 crash fix"** (Gameplay): the rolling boulder spawners in Mountain chapter 3
deleted themselves too early and their boulders later wrote into the freed memory (use after free),
which can crash the game in `FreeLocalMem`. One word of level data keeps the spawners alive.
- **New mod "Developer mode"** (Cheats): turns on the game's hidden developer switch. Hold A+S to
warp to the developers' warp points, F1 to fly, F2 for a heart and 5 fruit, F3/F11 to count idols
and coins up; Space on the river log makes it invincible and fast.
- **Debug wireframe: new option "Allowance areas"** (off by default). The game counts the llama as
standing on a floor even a little outside its edges; at sharp corners this margin becomes a long
sliver where the llama can stand in the air. *Floating spots* marks those places in pink, *All edges*
draws the margin as a thin pink band along every floor edge.
## 0.2.3 – 2026-11-07
- **Editor:** flying with the right mouse button + W A S D / Q E now works even when another part of
the window (a text field, a dock) still had the keyboard focus.
- **Editor:** the block, pillar and invisible-box icons sat too low in the toolbars (drawn outside
their box); a test now keeps every icon inside its 16 × 16 box.
- **Tutorials** say where the Objects panel is (the ⊙ tab next to *Outliner*).
## 0.2.2 – 2026-11-07
- **English game version supported.** All exe mods (boot to level, cheats, intro skip, practice
tools, free camera) and the base patch now also work with the English groove.exe of
2000-10-17. With any other version LlamaMod warns at start and before the first Apply (nothing
is blocked); mods that change groove.exe then stop with a clear message.
- **Display diagnostics option renamed and explained.** The cheats option is now called
"Troubleshooting: write display diagnostics" and warns that the game switches through every
screen mode of every monitor at start (screens go black for a while). It stays off by default.
## 0.2.1 – changes since 0.1.0
The first test release was 0.1.0 (launcher edition). Everything below is new for testers.
There are two downloads: **full** (launcher + level editor) and **launcher** (mods only).
### Launcher
- **Clearer texts everywhere.** Mod descriptions and options are explained in plain words,
directly under each option instead of only in tooltips.
- **Setup wizard**
- Wrong files, truncated ISO downloads and too little disk space are caught when you
pick the source, not after you click Set up.
- Copy progress shows megabytes ("123 of 568 MB").
- Cancel tells you what happened.
- **Play**
- While the game runs, Play becomes **Quit game**, so a hung game can be stopped from LlamaMod.
- A crash now says "The game crashed" with advice, instead of only "The game has ended".
- Wine: a too-old Wine version gives a warning, and a wrong Wine command is explained.
- **Safer applying**
- Two LlamaMod windows (or the command line) can no longer break each other's apply.
- If LlamaMod is closed in the middle of applying mods, it finishes or rolls back the
apply on the next start and tells you.
- Settings changed in one window show up in the other.
- Old cached mod results are rebuilt after an update.
- **Mods**
- **Mirror mode:** pick the worlds with check boxes.
- **Practice and Free camera:** if two hotkeys use the same key, Apply refuses and says
which ones clash.
- **Boot to level:** a custom start position belongs to its level and no longer breaks
other levels.
- **Intro skip and Boot to level** can now be used together.
- **Test World:** a new gallery with all 253 characters, enemies, animals and objects
of the game, standing still on pedestals (the animal pen is gone).
- **New levels and the Alligator Arena only use TestLevel chapters 1 and 32–49.** On other
chapters the game stored the level's coins over your progress and other game data (chapter 9
broke the world map). Levels and settings on old chapters move to a free one automatically.
The game itself is fixed too: picking an empty TestLevel chapter on the world map (or in
Boot to level) no longer damages your progress.
- The level tally shows the real coin and secret totals of new levels, the Test World and the
arena instead of the Village's "55".
- **New: Alligator Arena** – a minigame on a TestLevel chapter (default 49): alligators come in
waves into a walled pit; every hit costs one of their lives and coins pop out, their bite
costs your energy; beat every wave and the exit opens. Lives, waves, energy and coins per hit
can be set.
- **New: Llama Coaster** – a roller-coaster ride on a TestLevel chapter (default 48), with the
cart from the Catacombs: drops, climbs, boost arrows, a jump and a spin pad. Two tracks, 1–3
laps, three speeds.
- **Save games**
- Backups can no longer overwrite each other.
- Import… opens in the backups folder.
- The table fits small (1280×720) screens.
- **Layout:** works on small and laptop screens; the dark theme is easier to read.
### Level editor (full edition)
- **Play-test**
- Shows "Game running" and a **Stop game** button.
- Shift+F5 shows a crosshair where the llama will start.
- Play-test copies are removed when the game ends, and your mod selection comes back.
- **Readable names.** "Coin 3", "Floor 2", "Waypoint 4" instead of internal ids, in the
outliner, inspector, status bar and messages; the outliner filter finds them.
- **Error messages.** Damaged files give one plain sentence with optional details.
- **Geometry**
- Painting or erasing tiles in one drag is one undo step.
- New tile option **No collision** (walk-through decoration).
- **Meshes and export**
- **Export level as glTF/GLB** (textures, vertex colours; optionally only the selection,
or collision as a separate mesh).
- Imported and exported models are **no longer mirrored** compared to Blender. Meshes in
older projects keep their look; switch them to "right" handedness in the inspector to
fix them.
- **Objects**
- **Guards** and **alligators** need a waypoint box around them, or the game removes
them. The editor now sets this up automatically and warns when it is wrong. It also
says which objects can trigger a guard's event bit.
- The start camera no longer starts inside the llama's head on some base levels. Moving
the player start moves its camera and intro fly-by too.
- Snap to floor follows the game's own rule.
- **Object scripts**
- Custom object types can be edited and deleted (with undo).
- **Script language with compiler:** if/else, loops, switch, events, named parameters.
- The script view is much easier to read.
- Opcodes have checked descriptions.
- Narrow docks, small windows and long labels wrap instead of being cut off.
### Documentation
- New guide for building your own levels, a reference of the game's file formats and engine
limits, and a chapter on object scripts (strats) with the arena and the coaster as worked
examples: start at [docs/README.md](docs/README.md).
### Known limits
- Tested on Linux (Wine) and in a Windows VM. Real Windows hardware is not tested yet.
- The game draws objects only within about 12 units of the llama.
- Four music tracks of the game have no file on the CD (GAMEOVER, TALLY, PUZZLE2, PLATFORM); the
editor marks them.
+136 -232
View File
@@ -1,268 +1,172 @@
# 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.
![The LlamaMod launcher: tick mods, set their options, Apply and Play](docs/img/readme-launcher.jpg)
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.
*The launcher: tick the mods you want, set their options, click Apply and Play.*
> 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).
![The LlamaMod level editor with Village chapter 1 open](docs/img/readme-editor.jpg)
---
*The level editor with Village chapter 1 open: outliner (left), 3D view, inspector (right), assets and scripts (bottom).*
## Requirements
More pictures:
* 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).
| | |
|---|---|
| ![Test World gallery](docs/img/testworld-gallery.jpg) | ![Alligator Arena](docs/manual/img/mod_arena.jpg) |
| **Test World**: all 253 figures and objects of the game in one level | **Alligator Arena**: a minigame with new, scripted enemies |
| ![Llama Coaster](docs/manual/img/mod_coaster.jpg) | ![Debug view](docs/img/debug-view.jpg) |
| **Llama Coaster**: a new roller coaster track for the catacombs cart | **Debug view**: collision and invisible objects in the running game |
| ![glTF in Blender](docs/img/gltf-blender.jpg) | |
| **glTF/OBJ** export and import for Blender | |
The builder and the export tools are plain Python, so they should also work on Windows.
The test harness is Linux/Wine-only.
## Features
### Paths – adjust these
* **Mods** – tick, *Apply*, *Play*: Test World, Alligator Arena, Llama Coaster, 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.
The tools were written for one Linux/Wine setup and some paths are hard-coded at the top of
the scripts:
## Get it
| Script | Variable | Meaning |
|---|---|---|
| `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 |
You need **your own copy of the game** (the CD or an ISO image of it). Nothing else – LlamaMod
brings everything it needs (also Java).
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.
**Supported game versions:** the **German CD** (*Ein Königreich für ein Lama*, `groove.exe` of
2001-02-12 – the version everything was developed and tested with) and the **English first release**
(*The Emperor's New Groove*, `groove.exe` of 2000-10-17). With any other version the level mods
(Test World, Arena, Coaster, your own levels, Mirror, Randomizer, Draw distance, Debug wireframe)
should still work; the mods marked *modifies groove.exe* stop with a clear message instead – then
please send us that `groove.exe` so we can add it.
---
### Install in 5 steps
## Folder layout
1. **Download** from the **Releases** page of this repository (top of the page):
* Windows: `LlamaMod-<version>-full-windows-x64.zip`
* Linux: `LlamaMod-<version>-full-linux-x64.tar.gz`
```
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
(*full* = with the level editor. The *launcher* packages are the same without the editor.)
2. **Unpack** it into any folder you like (for example *Documents* or your home folder).
There is no installer and you need no admin rights.
3. **Start** LlamaMod:
* Windows: double-click **`LlamaMod.exe`**. If Windows says *"Windows protected your PC"*,
click **More info → Run anyway** (the program is not signed, that's all).
* Linux: install Wine once (`sudo apt install wine` on Ubuntu/Mint/Debian), then double-click
**`LlamaMod`** or run `./LlamaMod`. `./install-desktop-entry.sh` adds it to your app menu.
4. **Set up the game** – on the first start a short wizard asks for your game: put the CD in or
pick the ISO file, click **Set up** and wait about a minute. Your CD/ISO is only read, never
changed.
5. **Play** – on the **Mods** tab tick the mods you want, click **Apply**, then **Play**.
*Restore original* switches all mods off again.
New levels (Test World, Alligator Arena, Llama Coaster, your own) are in the hidden world
**TestLevel**: open the **Save games** tab once and click **Unlock TestLevel**, then pick it on the
world map. If your antivirus complains about `groove.exe`: mods that change the game program are
marked *modifies groove.exe*; LlamaMod tells you what to do. More help:
[Troubleshooting](docs/manual/20-troubleshooting.md). What changed: [CHANGELOG.md](CHANGELOG.md).
### Build from source
Needs a JDK 25 (toolchain) and Gradle running on Java 21 (for Spotless):
```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
Where the tools look for the game files (or pass `--base`/`--src`):
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`.
| Variable | Meaning | Default |
|---|---|---|
| `LLAMAMOD_CD` | the CD folder (`wads/`, `music/`, …), also for the Gradle tests | `~/.wine-lama/iso-dk/Setup/Eng/Cd` |
| `LAMA_WINEPREFIX` | Wine prefix of a hand-made game setup (`lamatest.py`, `levelbuild.py --install`) | `~/.wine-lama` |
| `LAMA_MOD` | the `lama-mod` script used by `lamatest.py` | `~/.local/bin/lama-mod`, else `tools/lama-mod` |
| `APPTEST_ROOT`, `APPTEST_ISO` | session folders and ISO of `apptest.py` | `~/.cache/llamamod-apptest` (`$CLAUDE_JOB_DIR/tmp` if set), `~/Downloads/GROOVE_DK.iso` |
| `LLAMAMOD_DATA` | data folder of the app | `~/.local/share/LlamaMod` (Windows `%LOCALAPPDATA%\LlamaMod`) |
To reach world 9, edit a save file (`king0.ksv`…`king2.ksv`, 256 bytes):
All tools and variables: [docs/tools.md](docs/tools.md#paths).
* `+0x04/+0x05` = map cursor (world, chapter)
* `+0x3f/+0x40` = current world/chapter
* `+0x42/+0x43` = highest unlocked world/chapter
### Key facts in one minute
Set them to world 9, chapter 1.
* **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.
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.
More: [docs/reference](docs/reference/README.md).
### Debug patch: visible collision and invisible walls
### Building on this with an AI assistant
```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
```
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.
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.
## Legal
Legend (full version in `notes/debugviz.md`):
| Colour | Meaning |
|---|---|
| green / yellow-green wireframe | walkable floor / slope |
| blue | wall |
| orange | ceiling / overhang |
| **red, semi-transparent fill** | **invisible collision**: invisible walls and floors |
| magenta lines | death plane ("no floor") |
| violet fill | level exit floor (material 16) |
| other colours | special collision materials |
| Marker | Meaning |
|---|---|
| white arrow | player start |
| violet cube | exit |
| turquoise cube | checkpoint |
| cyan cube | camera |
| yellow cube | trigger / cutscene |
| orange cube | force or special zone |
| gold cube | secret zone |
| grey cube | other invisible helper object |
| pink wire box | ambience / music / fog zone |
Cubes sit on a stick, and a horizontal line shows the object's facing. Wire boxes show
trigger areas.
Status: 19 of 33 levels were loaded in the game without problems. The rest
(catacombs/roller coaster world 7, lab world 8, two others) passed the offline check only.
Collision of moving objects (platforms, doors) is not shown.
---
## LlamaMod and the level editor
Besides the Python kit, this repository contains **LlamaMod** (Java 25, Swing; `src/`), a
one-click mod manager for the game on **Windows and Linux**, and a full **3D level editor**
built into it. Nothing from the game is included: the wizard installs the game from your own
CD/ISO into LlamaMod's data folder, and all mods are built from that copy.
* **Mods** – tick mods, click *Apply*/*Play*: debug wireframe, Test World, randomizer, fog,
mirror mode, intro skip, cheats, practice tools, free camera, *boot to level* … Mods may change
level files and `groove.exe` (code caves); the original files are never touched.
Developer notes: `notes/llamamod-app.md`, one `notes/mod-*.md` per mod.
* **Level editor** (*Level Editor* button, or `LlamaMod --editor`): create new levels from
templates, brushes and the base level's tile models and objects, or edit the original
levels; import OBJ/glTF meshes and PNG/JPG textures (texture manager, paint-on-faces,
animations); waypoints, zones, fog, lights, visibility regions; **Play (F5)** builds the
level and starts the game directly in it (Escape returns to the editor).
* **Sharing**: a level is a small `.llamalevel` file with only your changes. Others import it in
LlamaMod (*Mods → Levels → Import level*) and it is built from their own game copy; new levels
appear in the hidden *TestLevel* world (chapters 1–49).
User guide (English): **`notes/editor-guide.md`**. In the editor: *Help → Getting started*
and *Help → Shortcut sheet*. Technical notes: `notes/editor-design.md`, `editor-api.md`,
`editor-format.md`, `editor-render.md`, `editor-ui.md`, `editor-tools-*.md`,
`editor-playtest.md`.
```bash
./gradlew build fatJar # tests + build/libs/llamamod-<ver>-all.jar
./gradlew packageLinux # build/dist/LlamaMod-<ver>-full-linux-x64.tar.gz (own Java runtime)
./gradlew packageWindows # build/dist/LlamaMod-<ver>-full-windows-x64.zip (LlamaMod.exe + runtime)
./gradlew packageAll # both systems, full and launcher (no editor) edition
java -jar build/libs/llamamod-*-all.jar # LlamaMod
java -jar build/libs/llamamod-*-all.jar --editor # the level editor alone
```
![The level editor](notes/img/editor/ui_select_move.jpg)
---
## Key facts
* **The world is made of tiles.** TRAK holds about 100–650 small models: render triangles
plus collision polygons. MAP places them on a W×H grid (96×96 in the village) with height
and rotation. MAP also stores baked vertex colours and a 32-zone visibility table. There is
no BSP and no single world mesh.
* **Objects are "strats".** These are compiled scripts in the STPC chunk. In MAP, an object's
type is simply its **byte offset into STPC**. A level can therefore only use the object
types its own STPC contains. The builder uses the village (`t1l1m001`) as its base.
* **Coordinates and scale.** y points up. Fixed point 20.12 for objects, 4096 = 360° for
angles, and render space vs. game space have z mirrored. The llama is about 0.6 units
tall, runs about 1.1 units/s, can step up at most about 0.35 and jumps about 1.3 units
far with a run-up.
* **The level exit (LevelDoor)** is an invisible trigger. It fires only when the llama
stands in its grid cell **on floor with collision material 16**. Its parameters are
(tribe, world, chapter) of the next level.
* **Texture pixels** are PS1-style 15-bit BGR, RLE16-compressed, on 256×256 pages. The
value 0x0000 means transparent.
The parsers rebuild every original WAD byte-for-byte, so they can be trusted as format
specs.
---
## Building on this with an AI assistant
This whole kit was made by an AI coding agent (Claude Code) working through the steps
below. Any capable assistant that can run code should be able to continue.
1. **Give it the notes first.** Ask it to read `notes/*.md`. They contain offsets, field meanings, confidence levels and pitfalls.
2. **Regenerate the decompile** for questions about game logic. Install Ghidra and a full
JDK (version 25 for Ghidra 12.x), then run
`analyzeHeadless <projdir> groove -import groove.exe -scriptPath re/ghidra_scripts -postScript DumpAll.java groove_decomp.c`.
This writes all functions as C (about 2 MB) for the assistant to search with grep.
Loader functions worth knowing: MAP `0x42ac50`, TRAK `0x42aac0`, STPC `0x42ab50`, strat
interpreter `0x54d180`.
3. **Always test in the game.** Have the assistant load and screenshot every build. On
Linux, `tools/lamatest.py run <wad> <outdir>` does this unattended in about 75 s. On
Windows, test by hand or adapt the script.
4. **Change one thing at a time** and keep the original WADs untouched. The game crashes on
inconsistent data: tiles outside the grid, a vertex-colour block that doesn't match the
vertex counts, or missing chapters.
### Ideas for what to try next
* Use object types from other levels. You would have to merge and relocate their STPC
code and their textures.
* ~~Import custom textures~~ – done in the level editor (texture manager, import, painting).
* ~~Make a real visual editor~~ – done: the LlamaMod level editor (see above).
* Add custom music or sounds (SMPC/AMPC/SRPC are sound banks).
---
This is a non-commercial fan project, not affiliated with Disney or Argonaut. Please share
only tools and your own content, not the game's files.
This is a non-commercial fan project, not affiliated with Disney Interactive or Argonaut Games.
**No game files are included** – no executable, no level data, no music, no decompiled code. You
need your own copy of the game; every tool works on that copy. Please share only tools and your own
content (`.llamalevel` projects contain only your changes), not the game's files.
+7 -3
View File
@@ -4,7 +4,8 @@ plugins {
}
group = 'com.eliasmueller.llamamod'
version = '0.2.0'
// Release builds pass -PreleaseVersion=<tag without v> (see .gitea/workflows/release.yml).
version = findProperty('releaseVersion') ?: '0.2.4'
repositories {
mavenCentral()
@@ -92,11 +93,14 @@ processResources {
test {
useJUnitPlatform()
// golden tests: -Dllamamod.cd=<cd dir> -Dllamamod.ref=<python reference dir>
systemProperty 'llamamod.cd', System.getProperty('llamamod.cd', "${System.getProperty('user.home')}/.wine-lama/iso-dk/Setup/Eng/Cd")
// golden tests: -Dllamamod.cd=<cd dir> (or env LLAMAMOD_CD) -Dllamamod.ref=<python reference dir>;
// tests that need the CD are skipped when the folder does not exist
systemProperty 'llamamod.cd', System.getProperty('llamamod.cd', System.getenv('LLAMAMOD_CD') ?: "${System.getProperty('user.home')}/.wine-lama/iso-dk/Setup/Eng/Cd")
systemProperty 'llamamod.ref', System.getProperty('llamamod.ref', '')
// level builder references: python3 tools/levelbuild.py levels/testwelt.py <lbref>/testwelt.wad etc.
systemProperty 'llamamod.lbref', System.getProperty('llamamod.lbref', '')
// English groove.exe (2000-10-17) for the version tests; never committed, skipped when absent
systemProperty 'llamamod.exeEn', System.getProperty('llamamod.exeEn', "${projectDir}/re/groove_en.exe")
maxHeapSize = '2g'
}
+77
View File
@@ -0,0 +1,77 @@
# LlamaMod documentation
Documentation for modding *The Emperor's New Groove* (PC, 2000; German: *Ein Königreich für ein
Lama*) with LlamaMod: building levels, the game's file formats, and how the game's object scripts
("strats") work.
![The Test World: every figure of the game in one level](img/testworld-gallery.jpg)
## Start here
| If you want to … | read |
|---|---|
| build your own level | **[Guide: Build your own level](guide/README.md)** |
| look something up in the editor | [Level editor manual](manual/index.md) (also in the editor: **F1**) |
| know the numbers and rules of the engine | [Design rules](guide/design-rules.md) |
| build levels from code | [Levels from scripts](guide/levels-in-code.md) |
| understand the game's data | [Reference](reference/README.md) |
| change or write object behaviour | [Strats](strats/README.md) |
| find a tool | [Tools](tools.md) |
| work on LlamaMod itself | [For developers](developers.md) |
## Contents
**[Guide](guide/README.md)** – for level builders
* [Build your own level](guide/README.md): setup, editor basics, ground, start and exit, objects,
look and feel, play-test, sharing
* [Design rules](guide/design-rules.md): sizes, jump distances, the rules that keep a level working
* [Levels from scripts](guide/levels-in-code.md): Python and Java level builders, the editor's
command line
**[Level editor manual](manual/index.md)** – every tool, panel and dialog of the editor, with
tutorials (first level, parkour, editing an original level, Blender import)
**[Reference](reference/README.md)** – the game's data
* [Game files](reference/game-files.md): folders, naming scheme, worlds, COR pictures, movies,
save games
* [WAD container](reference/wad.md): the chunks of a level file
* [Geometry and collision](reference/geometry.md): tiles, grid, coordinate spaces, collision
materials, visibility regions
* [Objects, waypoints, zones](reference/objects.md): strat records, parameters, the exit,
environment zones, lights
* [Textures](reference/textures.md): pages, pixels, texture rectangles, animations
* [Music and sound](reference/audio.md): ASF music, sound banks, speech – known and unknown
* [TestLevel (world 9)](reference/testlevel.md): where custom levels live, safe chapters
* [Engine facts and limits](reference/engine-limits.md): scale, draw distance, memory, crashes
**[Strats](strats/README.md)** – object behaviour
* [How the strat VM works](strats/README.md): units, frames, variables, events, spawning,
collision
* [Reading scripts](strats/reading-scripts.md): the Script view and the assembly
* [The script language](strats/script-language.md): writing new behaviour
* [Customising strats](strats/custom-types.md): constants, imports, clones, custom types, mods
* [Example: the Alligator Arena](strats/arena.md): a complete minigame explained step by step
* [Example: the roller coaster](strats/roller-coaster.md): a new ride for the game's own coaster cart – track data and waypoints, no new strat code
**[Tools](tools.md)** – LlamaMod's launcher, editor and command line, the Python kit, the in-game
test harnesses
**[For developers](developers.md)** – build, code map, writing a mod, maintaining the manual
**[Lab notes](../notes/README.md)** – the detailed engineering notebook behind all of this
## Pictures
| | |
|---|---|
| ![LlamaMod launcher](img/launcher.jpg) | ![Level editor](img/editor.jpg) |
| The launcher: mods with options, save games, settings | The level editor with Village 1 |
| ![Alligator Arena](manual/img/mod_arena.jpg) | ![Debug view](img/debug-view.jpg) |
| The Alligator Arena minigame | Debug view: collision and invisible objects in the game |
## Status and honesty
Everything here was found by reverse engineering and tested against the game; where something is
a guess, the page says so (*likely*, *unknown*). Development used the German CD; the English
first release is supported too (see [game versions](reference/game-files.md#grooveexe-versions)). The project is not affiliated with Disney or
Argonaut and contains no game data.
+108
View File
@@ -0,0 +1,108 @@
# 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>` or the environment variable `LLAMAMOD_CD` (default:
`~/.wine-lama/iso-dk/Setup/Eng/Cd`); if that folder does not exist 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/).
## Releasing
Releases are built by Gitea Actions ([.gitea/workflows/release.yml](../.gitea/workflows/release.yml)):
1. Add a `## <version> – …` section at the top of [CHANGELOG.md](../CHANGELOG.md) (it becomes the
release text; the workflow fails if it is missing).
2. `git tag v<version> && git push origin v<version>`
The workflow builds and tests (tests that need the game CD skip themselves on the runner), runs
`packageAll` with `-PreleaseVersion=<version>` (the tag decides the version; without it
`build.gradle`'s default is used), writes `SHA256SUMS.txt` and creates the release with the four
packages via [tools/release/publish_gitea.py](../tools/release/publish_gitea.py) (standard library
only; re-running replaces files of the same name). [.gitea/workflows/build.yml](../.gitea/workflows/build.yml)
builds every push.
Requirements on the Gitea side: Actions enabled (`[actions] ENABLED = true`), an `act_runner` with
the label `ubuntu-latest` (an image with `python3`, `curl`/network access – the build downloads the
Temurin JDK/jmods and launch4j), and an attachment size limit above ~60 MB (`[attachment] MAX_SIZE`
in `app.ini`). If the built-in token may not create releases, add a repository secret
`RELEASE_TOKEN` with write access. Locally the same packages come from
`./gradlew -PreleaseVersion=<version> packageAll` (output in `build/dist`).
+126
View File
@@ -0,0 +1,126 @@
# Build your own level
A step-by-step path from "I have the game" to "my friends are playing my level". Each step is short
and links to the chapter of the [editor manual](../manual/index.md) that has the details.
The manual is also built into the editor: press **F1** over any panel, tool or dialog.
| Step | |
|---|---|
| 1 | [Set up LlamaMod](#1-set-up-llamamod) |
| 2 | [Learn the editor in ten minutes](#2-learn-the-editor-in-ten-minutes) |
| 3 | [Plan the level](#3-plan-the-level) |
| 4 | [Build the ground](#4-build-the-ground) |
| 5 | [Start, checkpoints and the exit](#5-start-checkpoints-and-the-exit) |
| 6 | [Objects, enemies and cameras](#6-objects-enemies-and-cameras) |
| 7 | [Look and feel: textures, fog, music, light](#7-look-and-feel-textures-fog-music-light) |
| 8 | [Play-test](#8-play-test) |
| 9 | [Share it](#9-share-it) |
| 10 | [Go further](#10-go-further) |
Before you start, skim [Design rules](design-rules.md): the numbers (jump distances, draw
distance) and the handful of rules that keep the game from crashing.
![The level editor](../img/editor.jpg)
## 1. Set up LlamaMod
1. Get **your own copy** of the game: the CD or an ISO image of it (*The Emperor's New Groove* PC,
2000; German: *Ein Königreich für ein Lama*). LlamaMod contains no game files.
2. Download the **full** package for Windows or Linux, unpack it and start `LlamaMod.exe` /
`./LlamaMod` (Linux needs Wine to run the game). Or build it from source – see the
[main README](../../README.md#build-from-source).
3. The first-start wizard copies the game into LlamaMod's data folder. The original CD or ISO is
never changed.
4. Click **Play** once to check that the unmodded game runs.
Manual: [Installing LlamaMod and the first start](../manual/02-installing.md).
## 2. Learn the editor in ten minutes
Click **Level Editor** in LlamaMod and follow
[Your first level in 10 minutes](../manual/tutorial-first-level.md): a new level from the
*Island* template, a ramp, a coin trail, play it. Then read
[Introduction and concepts](../manual/01-introduction.md) – tiles, brushes, objects, zones, base
levels, chapters – and [Navigating the viewport](../manual/04-navigation.md).
## 3. Plan the level
* **New or edited?** A *new level* starts empty on a **base level** (default: Village 1) that
provides the object types, textures, sky, fog and music. *Editing an original level* changes the
real Village, River … in place ([tutorial](../manual/tutorial-original-level.md)).
* **Choose the base by its objects.** Only the base's object types are available directly; more
can be imported from any world ([Importing objects](../manual/09-objects.md#importing-objects-from-other-worlds)).
* **Size.** The village grid is 96 × 96 cells; the llama is 0.6 tall and jumps about 1.3 cells.
A small, dense level plays better than a big empty one – the draw distance is only 8–16 cells.
* **Where it goes.** New levels live in the hidden world 9, *TestLevel*, on chapter 1 or 32–49
([why](../reference/testlevel.md)). The editor picks a free chapter.
## 4. Build the ground
* **Brushes** (B) draw boxes, ramps, stairs, cylinders and more; they are cut into tiles when the
level is built ([Building geometry](../manual/06-geometry.md)).
* **Tiles** (T) place the base level's own tile models for an original look.
* **Meshes** from Blender or any 3D program come in as OBJ or glTF
([Importing meshes](../manual/07-meshes.md), [Blender tutorial](../manual/tutorial-blender.md)).
* **Collision** (K) decides what is solid: walls, floors, invisible walls, death floors, the exit
floor. The llama cannot step up even small edges – use slopes, or walls without collision.
![A level exported as glTF and opened in Blender](../img/gltf-blender.jpg)
## 5. Start, checkpoints and the exit
Use the one-click setups in the Objects panel ([Objects](../manual/09-objects.md#one-click-setups)):
* **Player start** – the llama plus its start camera node behind it. Keep the space behind the
llama free, or the camera jumps aside.
* **Checkpoint** – where the llama respawns after a fall.
* **Exit** – a LevelDoor *and* a floor with collision material 16 in the same grid cell. Without
that floor the exit does nothing ([why](../reference/objects.md#the-exit-leveldoor--certain)).
Its target is the next level; in TestLevel never "world finished" (7, 7, 7).
* **Death plane** – a death floor below everything, so falling off respawns instead of falling
forever.
## 6. Objects, enemies and cameras
* Place coins with the **Coin tool** (O), anything else from **Assets → Objects**
([Objects](../manual/09-objects.md)).
* Enemies and moving things often need a **waypoint chain** – some guards even need a pair of
waypoint boxes and an event bit, or the game removes them. The Inspector and *Check for problems*
tell you ([Waypoints](../manual/10-waypoints.md)).
* **Cameras**: camera zones, cut scenes and the start camera
([Cameras and triggers](../manual/11-cameras-triggers.md)).
* Change how an object behaves – a number in its script or a whole new type: see
[Strats](../strats/README.md) and [manual chapter 22](../manual/22-strats.md).
## 7. Look and feel: textures, fog, music, light
* **Textures**: the base's textures, imported PNG/JPG pictures, painting onto faces, flip-book
animations ([Textures](../manual/08-textures.md)).
* **Environment zones** set fog (= draw distance), sky colour and **music**; teleport zones move
the player between areas ([Environment](../manual/12-environment.md),
[music ids](../reference/audio.md#music)).
* **Light**: tiles get baked vertex colours (*Bake lighting*), lights only light objects.
* **Visibility regions** hide areas the camera cannot see – needed for big levels
([Visibility](../manual/13-visibility.md)).
## 8. Play-test
**F5** builds the level and starts the game directly in it; **Shift+F5** starts at the camera
position. *Check for problems* (also run on every build) lists errors and warnings – double-click
one to select the culprit. **Escape** in the game quits straight back to the editor.
Manual: [Building, problems and play-testing](../manual/15-build-play.md).
## 9. Share it
**File → Save** writes a small `.llamalevel` project: only your changes, no game data. Send that
file; the receiver copies it into LlamaMod's `levels` folder (or opens it in the editor and uses
*Level → Add to LlamaMod…*), ticks it in the *Levels* category and clicks *Apply*. It is built from
their own game copy. Manual: [Sharing levels](../manual/17-sharing.md).
## 10. Go further
* Build levels from code instead of the mouse: [Levels from scripts](levels-in-code.md).
* Understand the data: [Reference](../reference/README.md).
* Write object behaviour: [Strats](../strats/README.md) and the
[Alligator Arena walkthrough](../strats/arena.md).
* All tools at a glance: [Tools](../tools.md).
+48
View File
@@ -0,0 +1,48 @@
# Design rules
The numbers and rules that matter when you design a level for this engine. Background and sources:
[Engine facts and limits](../reference/engine-limits.md).
## Sizes to design with
| | |
|---|---|
| one grid cell | 1 unit |
| llama | 0.6 tall, walks 1.1 units/s |
| jump up | ≤ 0.35 (0.5 fails) |
| jump across, with a run-up | ~1.3 (from standing much less) |
| easy parkour | gaps 0.3 with +0.2 height, platforms ≥ 1.5 long |
| steps without jumping | none – use slopes or walls without collision |
| draw distance | the fog end of the zone: 8–16 cells in the originals |
| objects visible | within ~12.5 units of the llama |
![The Test World's gallery: every figure of the game on a pedestal](../img/testworld-gallery.jpg)
## Rules that keep the level working
1. **One player start**, with nothing solid right behind the llama (start camera).
2. **Exit = LevelDoor + floor with collision material 16 in the same cell.** Target (1, 9,
chapter) in TestLevel – never (7, 7, 7).
3. **A death floor or death plane** under every place the llama can fall to.
4. **A checkpoint** before every hard part.
5. **TestLevel chapter 1 or 32–49** for new levels.
6. **Guards and box walkers need their waypoint boxes** (and guards an event bit someone sets),
or the game removes them.
7. **Every area must be visible from where the camera is**: with visibility regions, check the
masks (*Check for problems* reports regions nothing can see).
8. **Stay within the budget**: 16 MB level memory, 127 texture pages, 128 sounds – the import
preview and *Check for problems* show where you are.
The editor and both level builders enforce the data-level rules (tiles inside the grid, vertex
colours, type fields, collision cells) automatically. *Check for problems* reports the rest.
## Good habits
* **Play-test often** (F5). The engine is unforgiving and a small change can stop the exit or
hide an area.
* **Look at the originals.** Open an original level in the editor and see how it builds doors,
cameras and enemy paths; the [debug view mod](../tools.md#debug-view) shows collision and
invisible objects in the running game.
* **Keep levels compact.** Short sight lines hide the draw distance and keep the frame rate up.
* **Copy numbers** (speeds, parameters, waypoint fields) from original objects that already do
what you want.
+97
View File
@@ -0,0 +1,97 @@
# 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.
**Texture names**: the Python builder's own names are German (`gras`, `stein`, `erde`, `platte`,
`meer` …, list: `--textures`); the English names of the Java builder (`grass`, `stone`, `earth`,
`slab`, `sea` …) work as well and give the same result.
The base level is `t1l1m001.wad` in the CD folder `$LLAMAMOD_CD/wads` (default
`~/.wine-lama/iso-dk/Setup/Eng/Cd`), or `--base <file>`. `--install` copies the WAD into the TestLevel
mod of a hand-made Wine setup (`$LAMA_WINEPREFIX/mods/testlevel`, default `~/.wine-lama`) – with
LlamaMod, use a Java level or the editor instead. All variables: [Tools: Paths](../tools.md#paths).
## A Java level
Implement [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java):
```java
public final class MyLevel implements LevelScript {
@Override public String levelName() { return "My Level"; }
@Override public void build(LevelBuilder l) {
l.sea(0, 0, 40, 40, -1.5, new double[] {7, 7, 33, 33});
l.floor(10, 10, 30, 30, 0, "grass");
l.box(18, 24, 22, 25, 0, 0.06, o().top("pattern_purple").mat(16)); // exit floor
l.player(20, 12, 0);
l.checkpoint(12, 12);
l.exit(20.5, 24.5, null, null, new int[] {1, 9, 32});
l.coins(List.of(new double[] {20, 16}, new double[] {20, 18}));
}
}
```
`buildWad(baseWad, wads, opts)` returns the WAD. To ship it, write a small
[mod](../developers.md#writing-a-mod) whose `apply` writes `wads/T1L9M0nn.wad` (chapter 1 or
32–49) and a loading picture – [`ArenaMod`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java)
is a complete example. Texture names are English here (`grass`, `stone`, `sand`, `inca`, …) and are
matched by picture on the chosen base.
## The editor from the command line
The editor's project format can be scripted too:
```bash
llamamod --editor-new my.llamalevel t1l1m001 --name "My level" # new project on a base
llamamod --editor-import t1l2m001 jungle.llamalevel # an original level as a project
llamamod --editor-build my.llamalevel out.wad # build the WAD
llamamod --import-level my.llamalevel # install as a level mod
```
## Seeing the result without the game
* [`tools/geom_export.py`](../../tools/geom_export.py) `level.wad outdir --collision --objects`
writes OBJ files, texture pages and two preview images.
* [`tools/vorschau/`](../../tools/vorschau/) renders labelled overviews with Blender (Eevee).
* In the game: the test harness, see [Tools](../tools.md#testing-in-the-game).
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

+2 -2
View File
@@ -76,7 +76,7 @@ created. Object types of other levels can be added later with **Assets → All w
The game has eight worlds – **Village, Jungle at Night, River, Jungle by Day, Mountain, City,
Catacombs, Lab** – each with several chapters. A ninth, hidden world called **TestLevel** has 49
empty chapter slots. New levels go into one of these TestLevel chapters, so they never replace an
empty chapter slots; 19 of them (1 and 32–49) are safe to use. New levels go into one of these TestLevel chapters, so they never replace an
original level: each project gets its own chapter (shown in the New level dialog and in
[Level settings](14-level-settings.md)). In the game they appear on the world-9 map as
"TestLevel – chapter *n*".
@@ -98,7 +98,7 @@ and press **Play** to play the level – or press **F5** in the editor. See
## What you need
* The PC game (CD or ISO image; developed with the German version).
* The PC game (CD or ISO image): the German or the English version (see [FAQ](20-troubleshooting.md#faq)).
* LlamaMod – see [Installing LlamaMod](02-installing.md).
* A graphics card with **OpenGL 3.3** for the editor's 3D view.
* On Linux, Wine to run the game (LlamaMod sets it up).
+1 -1
View File
@@ -1,7 +1,7 @@
# Installing LlamaMod and the first start
LlamaMod runs on **Windows** and **Linux**. It needs your own copy of the game – the CD or an ISO
image of it. The game itself does not have to be installed: LlamaMod copies what it needs into
image of it, German or English version. The game itself does not have to be installed: LlamaMod copies what it needs into
its own data folder and never changes the original.
## Download and start
+1 -1
View File
@@ -29,7 +29,7 @@ list of recent projects.
| **Level name** | The name LlamaMod and the loading picture show. |
| **Author** | Your name (remembered for the next level). |
| **Base level** | The original level that provides object types, textures, sky, fog and music, shown with its loading picture. Default: Village – Chapter 1. See [Base levels](01-introduction.md#base-levels). |
| **TestLevel chapter** | The chapter of world 9 (TestLevel) the level replaces when it is enabled. The next free chapter is suggested. |
| **TestLevel chapter** | The chapter of world 9 (TestLevel, 32–49 or 1) the level replaces when it is enabled. The next free chapter is suggested. |
| **Start with** | **Empty** (nothing but the base level's sky, fog and music), **Flat floor** (a 16×14 grass floor, player start and an exit) or **Island** (a grass island with trees in the sea, player start and an exit). |
Every new level starts clean: of the base level it keeps only the **start zone** (its fog, colours
+1 -1
View File
@@ -87,7 +87,7 @@ toggle snapping for this drag. **Shift+click** selects an existing brush without
The options section in the Inspector changes with the primitive. Textures are chosen with a
swatch button: click it for a searchable grid of all textures of the level (names are the
village texture names; `#n` is texture number *n* of the base level, see
village texture names the base level has; `#n` is texture number *n* of the base level, see
[Textures](08-textures.md)). **none** leaves that part out.
| Option | Primitives | Meaning |
+4 -2
View File
@@ -10,8 +10,10 @@ paint them onto faces.
* A level has up to **127 pages**. Each page has one *blend class* (opaque, cut-out,
semi-transparent …); every texture on it should have the same class.
* Textures are numbered: `#221` is texture 221 of the level. In brush texture fields you can type
`#221` or a name. The names (`grass`, `stone`, `bark` …) are those of the village textures;
other base levels mostly use numbers.
`#221` or a name. The names (`grass`, `stone`, `bark` …) are those of the village textures.
On another base level a name means the same picture wherever that level has it (Village 2 and 3
have most of them); names the base level does not have are left out of the texture grid, and a
brush that uses one is skipped with a warning.
* Brushes can also use flat colours (colour names, or `rgbRRGGBB`).
* The game shows textures in **15-bit colour** (5 bits per channel) with 1 bit of transparency,
so gradients get coarser and soft edges become hard.
+1 -1
View File
@@ -113,7 +113,7 @@ Colour pickers update the viewport live while you pick; the whole pick is one un
| Field | Meaning |
|---|---|
| **Music** | Track started when the zone becomes active (`music/<name>.ASF`): VILLAGE, JUNGLE1, RIVER, CITY, CATACOMB, LAB, BALLOON, KRONK, VICTORY … *0 – (none)* stops the music. |
| **Music** | Track started when the zone becomes active (`music/<name>.ASF`): VILLAGE, JUNGLE1, RIVER, CITY, CATACOMB, LAB, BALLOON, KRONK, VICTORY … *0 – (none)* stops the music. Tracks marked *(no file on the CD)* have no music file (on the German CD) – pick another one. |
| **Sky model** | The object model drawn as the sky dome (one of the sky models used in this level), or none. |
| **Sky yaw** | Turns the sky dome. |
+1 -1
View File
@@ -25,7 +25,7 @@ nothing.
| **Description** | A few sentences for the mod list. |
| **Project** | *new level* or *changes to an original level* (read only). |
| **Base level** | The original level the project is built on – it provides the object code, textures, sounds, sky and music (read only). |
| **TestLevel chapter** | New levels only: the chapter of world 9 (TestLevel, 1–49) the level replaces when its mod is on. |
| **TestLevel chapter** | New levels only: the chapter of world 9 (TestLevel: 32–49, or 1) the level replaces when its mod is on. Chapters 2–31 are not offered: the game would store this level's coins over other game data. |
Below the chapter field the dialog lists which chapters other level projects in your `levels`
folder already use, so you can pick a free one. Each level needs its own chapter; chapter 1 is
+7 -4
View File
@@ -39,14 +39,17 @@ Then tick the level in LlamaMod, click **Apply** and **▶ Play**. To get rid of
## Chapters and conflicts
Each new level occupies one of the 49 TestLevel chapters. The editor picks a free chapter for
Each new level occupies one of the TestLevel chapters **32–49** (or chapter 1). The editor picks a free chapter for
new projects, but levels from different people may use the same one. If two active level mods
use the same chapter – or two mods change the same original level – LlamaMod stops applying and
names the files involved. Fix it in the mod's options: every new-level mod has a
**TestLevel chapter** option (1–49).
**TestLevel chapter** option (1, 32–49).
> **Note:** Chapter 1 of the TestLevel is also used by the built-in **Test World** mod. Prefer
> chapters 2 and above for your own levels.
> **Note:** Chapter 1 of the TestLevel is also used by the built-in **Test World** mod. The game
> keeps coins and flags for chapters 2–31 of world 9 in memory that belongs to other things (your
> progress, the text language …), so LlamaMod does not use them: levels saved with such a chapter
> by an older version move to a free chapter from 32 on. With more than 18 new levels, some share a
> chapter – only one of them can be on at a time.
## Updating a shared level
+57 -4
View File
@@ -38,7 +38,7 @@ option:
| Option | Values | Meaning |
|---|---|---|
| TestLevel chapter | 1 – 49 | the chapter of world 9 the level occupies; each level needs its own (chapter 1 is also used by the Test World mod) |
| TestLevel chapter | 1, 32 – 49 | the chapter of world 9 the level occupies; each level needs its own (chapter 1 is also used by the Test World mod). Other chapters would overwrite game data such as your progress, so older levels on chapters 2–31 move to a free one of 32–49 |
Edited original levels have no options – they replace their chapter. See
[Sharing levels](17-sharing.md).
@@ -61,7 +61,7 @@ not want with **Remove…**.
## The Alligator Arena minigame
The **Alligator Arena** mod (Levels) puts a small minigame on a TestLevel chapter (default 8):
The **Alligator Arena** mod (Levels) puts a small minigame on a TestLevel chapter (32–49, default 49):
a walled sand pit where alligators come in waves (wave 1: one, then two at a time). Face an
alligator and **kick** (Action) or **jump and press Action in the air** (the llama dives onto
it). Every hit costs it one life: it blinks, backs off and coins pop out – they count on the
@@ -74,6 +74,21 @@ another mod on the same chapter stops the apply with a message – pick another
![Alligator Arena: two alligators, coins popping out](img/mod_arena.jpg)
## The Llama Coaster ride
The **Llama Coaster** mod (Levels) puts a roller-coaster ride on a TestLevel chapter (32–49,
default 48). You ride the cart from the Catacombs coaster levels: it leaves the station by itself,
races down drops and up climbs, speeds up on the boost arrows, jumps over a gap and turns round on
the spin pad. Press **Left** and **Right** to move the cart across the track and collect the
coins. After the last lap the cart stops at the station, fireworks go off and the tally appears;
**Enter** rides again, and *Beenden und zur Auswahlkarte wechseln* in the Esc menu goes back to the
map. The options pick the track (*Loop* or the longer *Grand circuit*), the laps (1–3), the speed
and whether there are coins. Unlock TestLevel on the **Save games** tab and pick the chapter on the
world-9 map. A level project or another mod on the same chapter stops the apply with a message –
pick another chapter then.
![Llama Coaster: boost arrows on the Grand circuit](img/mod_coaster.jpg)
## Command line
LlamaMod can be scripted:
@@ -107,7 +122,21 @@ A minigame on a TestLevel chapter: alligators come in waves into a walled pit. E
| Waves | 1 – 5 | 3 | Wave n brings n alligators (at most 2 at once). |
| Llama energy | 1 – 5 | 4 | Energy cap of the llama in the arena (5 = normal). |
| Coins per hit | 0 – 5 | 1 | Coins that pop out per hit (5 more at the end). |
| TestLevel chapter | 2 – 8 | 8 | Chapter of world 9 the arena replaces. |
| TestLevel chapter | 32 – 49 | 49 | Chapter of world 9 the arena replaces (32–49; chapter 1 is the Test World's). |
#### Llama Coaster
A roller coaster ride on a TestLevel chapter, with the cart from the Catacombs coaster levels: drops, climbs, boost arrows, a spin pad that turns the cart backwards and a jump over a gap. Steer left and right to stay on the track and collect coins; the ride ends at the station. Unlock TestLevel on the Save games tab.
*Id* `coaster`
| Option | Values | Default | Description |
|---|---|---|---|
| Track | Loop, Grand circuit | Loop | Short loop (about 100 units) or the grand circuit (about 150 units, two jumps, two spins). |
| Laps | 1 – 3 | 2 | Laps before the ride stops at the station. |
| Speed | Slow, Normal (as in the game), Fast | Normal (as in the game) | Cruise speed and boost strength of the cart. |
| Coins | on / off | on | Coins along the track. |
| TestLevel chapter | 32 – 49 | 48 | Chapter of world 9 the coaster replaces (32–49; chapter 1 is the Test World's, 49 the arena's). |
#### Test World
@@ -156,6 +185,12 @@ Plays every level mirrored left to right, like the mirror mode of racing games.
| Readable textures | on / off | off | Flips writing and pictures back so they read normally. Some textures (water edges, paths) then show seams. Off: a true mirror image. |
| Mirror object models | on / off | off | Also mirrors the shape of objects such as huts and crates (characters stay as they are). Off: objects are only placed mirrored. |
#### Mountain 3 crash fix
Fixes a bug in Mountain chapter 3 that can crash the game: the rolling boulder spawners delete themselves too early and their boulders later write into the freed memory. With the fix the spawners stay alive; the boulders roll as before.
*Id* `mountain3-fix`
#### Practice tools
Practise hard jumps: press F5 to remember where the llama stands and F8 to jump back there (in the same level; the keys can be changed below). Also adds extra checkpoints along the coin trail of levels that have checkpoints.
@@ -212,7 +247,7 @@ Switches on the game's secret cheat codes for you, so you don't have to type the
| Complete chapter rewards | on / off | off | Finishing a chapter also gives you its collectible and gold coin (code → → ↓ ↓ ↓ ↑ ↓ ↓). |
| Start with 500 fruit | on / off | off | The purple fruit counter starts at 500 in every chapter (code ↑ ↓ → ← ← ← ← ↓). |
| Full charge meter | on / off | off | The charge bar starts full in every chapter (code ↑ ← ↓ → ↓ ↓ ↑ ↑). |
| Write display diagnostics | on / off | off | For graphics problems: the game writes a report of your graphics devices (<computer name>.dispdiag) into the game folder. Same as its hidden -display_diagnostics switch. |
| Troubleshooting: write display diagnostics | on / off | off | Only for troubleshooting when asked to. At start the game switches through every screen mode of every monitor – all screens go black for a while (30 s or more) – and writes a report (<computer name>.dispdiag) into the game folder, then starts normally. Same as its hidden -display_diagnostics switch. |
#### Debug wireframe
@@ -220,6 +255,24 @@ Shows what is normally invisible: the walls and floors the llama collides with (
*Id* `debug-wireframe`
| Option | Values | Default | Description |
|---|---|---|---|
| Allowance areas | Off, Floating spots, All edges | Off | Pink marks where the game still counts the llama as standing on a floor although it is just beside it (the collision test accepts a small margin outside every floor edge). Off: nothing extra. Floating spots: only the places where that margin reaches noticeably far, past sharp corners, and the llama would stand in the air above the real floor. All edges: a thin pink band along every floor edge, also where it does not matter. Off by default because it adds geometry to every level. |
#### Developer mode
Turns on the game's hidden developer switch and puts its extra buttons on the keyboard. Hold A+S to warp the llama to the developers' warp points (22 levels, they go round in a circle). Hold F1 to fly, press F2 for a heart and 5 fruit, hold F3 for idols and F11 for coins (keys can be changed below). On the river log (world 3) hold Space: no damage, through the banks, very fast. In the finale, holding Space+Z ends the level. The developers' level-skip and music menu stays unavailable (it needs a second switch that is always off).
*Id* `devmode` · **modifies groove.exe** (see [antivirus warnings](20-troubleshooting.md))
| Option | Values | Default | Description |
|---|---|---|---|
| Fly key | F1, F2, F3, F4, F5, F7, F8, F9, F11, F12, Insert, Delete, Home, End | F1 | Hold to fly: the llama (also from standing), the bunny and the frog (after a jump) rise while the key is held. |
| Heart and fruit key | F1, F2, F3, F4, F5, F7, F8, F9, F11, F12, Insert, Delete, Home, End | F2 | Each press gives one heart and 5 purple fruit. |
| Idol key | F1, F2, F3, F4, F5, F7, F8, F9, F11, F12, Insert, Delete, Home, End | F3 | Hold to count the pink idols up (to 99). |
| Coin key | F1, F2, F3, F4, F5, F7, F8, F9, F11, F12, Insert, Delete, Home, End | F11 | Hold to count the coins up fast. |
| Fly key switches on and off | on / off | off | Press the fly key once to start flying and again to stop, instead of holding it. |
#### Free camera & noclip
Press F7 to fly the camera freely around the level: arrow keys move, A/S turn, PgUp/PgDn look up and down, X/Z rise and sink, Shift or Space go faster. Press F9 for noclip: the llama flies through walls (arrow keys, X/Z up and down). Press the key again to switch off (the keys can be changed below).
+8 -3
View File
@@ -11,6 +11,7 @@
| "Not enough free disk space" | The game copy needs about 900 MB. Free up space or choose another **Data folder**. |
| The game hangs or its window is gone | Click **■ Quit game** in LlamaMod (the Play button while the game runs). |
| "The game crashed" | Switch off the mods you turned on last, or click **Restore original**, and play again. If the original game crashes too, report it with `game.log` (**Settings → Logs**). |
| All monitors flash black for half a minute or more when the game starts | The **Built-in cheats** option *Troubleshooting: write display diagnostics* is on. It makes the game try every screen mode of every monitor before it starts. Switch it off; use it only when you are asked for a display report. |
| "groove.exe was removed or changed" | Click **Rebuild**; LlamaMod writes it again and then starts the game. On Windows, see [Antivirus warnings](#antivirus-warnings). |
## The editor
@@ -101,9 +102,13 @@ after an update, **Settings → Clear cache** throws it away; active mods are re
## FAQ
**Do I need the German version of the game?**
The tools were developed with the German CD. Other language versions use the same engine and
should work; if one does not, please report it.
**Which versions of the game work?**
The German CD (`groove.exe` of 2001-02-12) and the English first release (2000-10-17) are
supported, including every mod. With another version LlamaMod shows a warning at start and before
the first Apply (nothing is blocked); the level mods still work, but the mods
marked *modifies groove.exe* stop with "unknown program version" – the first lines of
`logs/apply.log` name the version LlamaMod found. Please report it together with that
`groove.exe`, then it can be added.
**Can I share levels with people who do not have the game?**
No – levels are built from the receiver's own game copy. That is also what makes sharing legal:
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+3 -2
View File
@@ -40,8 +40,9 @@ Every change can be undone with **Ctrl+Z**.
## Step 4: A coin trail
1. Open the **Objects** panel (left) and click **Coin line** under the quick setups – or press
**O** for the **Coin line / arc** tool.
1. Open the **Objects** tab in the left dock – the ⊙ icon right next to *Outliner* (inactive tabs show only
their icon) and click **Coin line** under **Quick setups** – or press **O** for the
**Coin line / arc** tool.
2. Click once at the start of the trail and once at its end. Coins appear every *spacing* cells
on the ground (also up the ramp).
+3 -2
View File
@@ -32,7 +32,8 @@ Every drag is one undo step – **Ctrl+Z** takes it back.
## Step 3: Add a checkpoint
1. Open the **Objects** panel (left dock) and click the quick setup **Checkpoint**.
1. Open the **Objects** tab in the left dock – the ⊙ icon right next to *Outliner* (inactive tabs show only
their icon) and click the quick setup **Checkpoint**.
2. The Place tool (**P**) is now active. Click on the ground where the checkpoint should stand.
The editor places a checkpoint post together with its respawn point a little in front of it.
@@ -50,7 +51,7 @@ The start of the level uses the **start zone**:
4. In the Inspector, click **Fog / sky colour** and choose a new colour – for example a soft
orange for an evening mood. The viewport follows while you pick.
5. Lower **End (Max)** a little for a denser fog.
6. Under **Music & sky**, choose another **Music** track, for example `PUZZLE2`.
6. Under **Music & sky**, choose another **Music** track, for example `22 – KRONK`.
The village houses have their own zones (one per door), so the inside of a house keeps its
original fog. See [Environment](12-environment.md) for every field.
+2 -1
View File
@@ -69,7 +69,8 @@ plane shows as material 30. Look for:
The llama should not have to start over after every fall.
1. Open the **Objects** panel (left dock) and click **Checkpoint** in **Quick setups**.
1. Open the **Objects** tab in the left dock – the ⊙ icon right next to *Outliner* (inactive tabs show only
their icon) and click **Checkpoint** in **Quick setups**.
2. Click on a platform in the middle of the course.
You get a checkpoint post with its respawn point just in front of it. Falling after passing it
+56
View File
@@ -0,0 +1,56 @@
# Reference: game files and engine
Everything we know about the data of *The Emperor's New Groove* (PC, 2000; German: *Ein Königreich
für ein Lama*), found by reverse engineering `groove.exe` and checking every claim against the
level files on the CD and in the running game. The game runs on Argonaut's **Croc 2 engine** (a PC
port of a PlayStation design), which explains most of its quirks.
Confidence is marked where it matters: **certain** = verified in code and data (byte-exact round
trip or seen in game), **likely** = consistent with the data, **unknown** = open question.
| Page | What it covers |
|---|---|
| [Game files](game-files.md) | CD and HD folders, level naming scheme, worlds and chapters, loading pictures (COR), movies, demos, save games (KSV) |
| [WAD container](wad.md) | the chunk list of a level file and which chunk does what |
| [Geometry and collision](geometry.md) | TRAK tile models, the MAP tile grid, coordinate spaces, collision polygons and materials, vertex colours, visibility regions |
| [Objects, waypoints, zones](objects.md) | MAP strat records, parameters, waypoints, environment zones (fog, music, teleports), lights, the player start, checkpoints and the exit |
| [Textures](textures.md) | TEXT chunk: pages, pixel format, texture rectangles, texture animations |
| [Music and sound](audio.md) | ASF music, the level sound bank, speech, what is still unknown |
| [TestLevel (world 9)](testlevel.md) | the hidden world all custom levels live in, and which chapters are safe |
| [Engine facts and limits](engine-limits.md) | scale, jump distances, draw distance, memory budget, things that crash the game |
How objects *behave* (the strat bytecode) has its own section: [Strats](../strats/README.md).
## Where the details live
These pages are the readable summary. The full lab notes – with every address in `groove.exe`,
open questions and the history of each finding – are in [`notes/`](../../notes/README.md):
| Topic | Lab notes |
|---|---|
| geometry, collision, coordinate spaces | [notes/geometry.md](../../notes/geometry.md) |
| MAP layout, strat records, other chunks | [notes/objects.md](../../notes/objects.md) |
| textures | [notes/textures.md](../../notes/textures.md) |
| every object type in every level | [notes/strat_types_table.md](../../notes/strat_types_table.md) |
| object parameters | [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md) |
| world 9 chapters | [notes/testlevel-chapters.md](../../notes/testlevel-chapters.md) |
| level building, physics measurements, the exit | [notes/levelbau.md](../../notes/levelbau.md) |
| fog, lights, regions | [notes/editor-tools-environment.md](../../notes/editor-tools-environment.md), [notes/mod-fog.md](../../notes/mod-fog.md) |
| importing objects across levels | [notes/editor-cross-import.md](../../notes/editor-cross-import.md) |
## Code that reads and writes these formats
Every parser rebuilds all original files byte for byte, so the code is a reliable executable
specification.
| Format | Java (LlamaMod) | Python (kit) |
|---|---|---|
| WAD container | [`format/Wad.java`](../../src/main/java/com/eliasmueller/llamamod/format/Wad.java) | [`tools/wadtool.py`](../../tools/wadtool.py) |
| TRAK / STPC models, collision | [`format/Trak.java`](../../src/main/java/com/eliasmueller/llamamod/format/Trak.java), [`Model.java`](../../src/main/java/com/eliasmueller/llamamod/format/Model.java), [`Collision.java`](../../src/main/java/com/eliasmueller/llamamod/format/Collision.java), [`Stpc.java`](../../src/main/java/com/eliasmueller/llamamod/format/Stpc.java) | [`tools/geomlib.py`](../../tools/geomlib.py) |
| MAP | [`format/MapChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/MapChunk.java) | [`tools/mapchunk.py`](../../tools/mapchunk.py) |
| TEXT | [`format/TextChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/TextChunk.java) | [`tools/textchunk.py`](../../tools/textchunk.py) |
| whole level | [`format/Level.java`](../../src/main/java/com/eliasmueller/llamamod/format/Level.java) | – |
| strat code | [`format/strat/`](../../src/main/java/com/eliasmueller/llamamod/format/strat/), [`format/stpcmerge/`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/) | [`tools/strats.py`](../../tools/strats.py), [`tools/stpcwalk.py`](../../tools/stpcwalk.py) |
| COR pictures | [`format/CorImage.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorImage.java), [`CorEncoder.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorEncoder.java) | [`tools/cor2png.py`](../../tools/cor2png.py), [`tools/png2cor.py`](../../tools/png2cor.py) |
| KSV saves | [`save/Ksv.java`](../../src/main/java/com/eliasmueller/llamamod/save/Ksv.java) | – |
| ASF music | – | [`tools/asf2wav.py`](../../tools/asf2wav.py) (decode only) |
+73
View File
@@ -0,0 +1,73 @@
# Music and sound
Sound is the least explored part of the game. This page separates what is verified from what is
only a guess. Custom music or sound effects are **not supported yet** by any LlamaMod tool.
## Music
| Fact | Status |
|---|---|
| Music tracks are files `music/<NAME>.asf` on the CD (33 files) | certain |
| A level does not reference music files directly: **environment zones** carry a music id, and the start zone's id plays when the level begins | certain |
| Music ids map to names through a table in `groove.exe` at 0x578960 (20 bytes per entry, name pointer at +8), played by `FUN_00546b70`; id 0 stops the music | certain |
| ASF here is Argonaut's own format (not Microsoft ASF): 4-bit ADPCM, 44.1 kHz stereo, 17-byte blocks, decoded by `asfcodec.adl` in the game folder | certain (decoder reimplemented) |
| A second zone field (u16 at +0x56) is probably the ambience setting | likely |
The id table, as used by the editor's zone inspector
([`EnvMath.MUSIC`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/environment/EnvMath.java)):
| Id | Track | Id | Track | Id | Track | Id | Track |
|---|---|---|---|---|---|---|---|
| 0 | (stop) | 10 | GAMEOVER | 20 | SNKJUNG | 30 | TUNE1 |
| 1 | VILLAGE | 11 | TALLY | 21 | FROG | 31 | CHACHA |
| 2 | JUNGLE1 | 12 | TURRACE | 22 | KRONK | 32 | JJUNGLE2 |
| 3 | RIVER | 13 | PACHASNK | 23 | KIDLLAMA | 33 | TUNE9 |
| 4 | JUNGLE2 | 14 | MOUNT2 | 24 | TUNE8 | 34 | TUNE6 |
| 5 | MOUNTAIN | 15 | BALLOON | 25 | ROLLER | 35 | VICTORY |
| 6 | CITY | 16 | JUNGSPID | 26 | BUNNY | 36 | PUZZLE2 |
| 7 | CATACOMB | 17 | JAGCHASE | 27 | BLOCK | 37 | PLATFORM |
| 8 | LAB | 18 | RIVER | 28 | TUNE5 | 38 | (none) |
| 9 | TITLE | 19 | RIVRACE | 29 | CITYPUZZ | 39 | ENDGAME |
Four names (GAMEOVER, TALLY, PUZZLE2, PLATFORM) have no `.asf` file on the German CD; what the
game plays for them (probably nothing) was not checked. The editor marks them *(no file on the CD)*.
**Changing the music of a level** works today: pick another id for the start zone (or any zone)
in the editor's *Environment* panel ([manual: Environment](../manual/12-environment.md),
[tutorial: Edit an original level](../manual/tutorial-original-level.md)).
**Listening to the tracks:** [`tools/asf2wav.py`](../../tools/asf2wav.py) `in.asf out.wav`
decodes a track exactly like the game's codec (filter 0: `s = s1 + (n << shift)`, filter 1:
`s = 2·s1 − s2 + (n << shift)`, 16-bit wrap-around instead of clipping – FFmpeg's decoder clips and
drifts).
**New music – open:** there is no encoder yet. Writing one (the inverse of `asf2wav.py`) and
replacing a track file through a mod would be the natural next step; the header fields after the
block count (`rate`, `flags` with bit 1 = stereo) are read by the decoder, the others are *unknown*.
## Sound effects
| Fact | Status |
|---|---|
| Each level has its own sample bank, chunk **SMPC**: `u32 n`, n × `{u32 "cvg " tag, u32 length, data}` | certain ([`SoundMerge.java`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/SoundMerge.java)) |
| Strats play samples by index (`soundPlay(sound:N)`, opcodes 0xd5 / 0x99 / 0x131 play, stop, is-playing); at most **128** samples per level, ids with bit 0x80 are the front-end sounds | certain |
| The samples are PlayStation-style VAG ADPCM, decoded by `Vagcodec.adl` | likely (tag and codec name; the sample header was not decoded) |
| **AMPC** holds ambient sound data (`ambi` / `VABp` blocks) | likely, not decoded |
| `FESOUND.WAD` is the front-end sound bank (not a chunk file) | likely |
Samples can be **moved between levels**: the editor's cross-world import and the arena copy the
samples an imported object needs into the target level and renumber the `sound:N` operands
([notes/editor-cross-import.md](../../notes/editor-cross-import.md)). Changing which sample an
object plays is a constant edit in its script ([Strats](../strats/custom-types.md)). Exporting or
replacing a sample's audio is not possible yet.
## Speech and dialogue
* **LGPC** holds the dialogue texts of a level in all languages plus a speech slot per line.
* A speech slot `#<hex>` is an index into **SRPC**, whose entries are offset/size pairs into the
global speech file `music/<language>.cvs` in the game folder (the German version ships
`english.cvs`).
* Strats show texts with opcodes 0xf0 / 0xfa (`topSay text:N`). Imported objects bring their
texts and speech references along.
The CVS file format itself was not analysed.
+67
View File
@@ -0,0 +1,67 @@
# Engine facts and limits
Numbers to design levels with, and the things that crash the game. Measured in the game with the
test harness unless noted.
## Scale and movement
One unit = one grid cell. Measured with the jump lab ([`levels/sprunglabor.py`](../../levels/sprunglabor.py))
and a coin measuring track ([notes/levelbau.md](../../notes/levelbau.md#findings-from-the-game-tests-lamatest-xvfb)):
| Quantity | Value |
|---|---|
| llama height | about 0.6 |
| walking speed | about 1.1 units/s (≈ 1 s to accelerate) |
| highest edge a jump clears | 0.35 (0.5 is too high) |
| long jump with a run-up | about 1.3 units (from standing: much shorter, a 0.5 gap fails) |
| comfortable parkour | gaps of 0.3 with +0.2 height, platforms ≥ 1.5 long |
| step-up without jumping | none – any edge with wall collision blocks; use slopes or walls without collision |
| frame rate of the logic | 30 frames per second (script speeds are per frame) |
Controls are **camera-relative**; the start camera sits behind the llama and moves aside if
something solid is right behind it – keep the area behind start points clear.
## Drawing
| Fact | Value |
|---|---|
| draw distance | = **fog end** of the active environment zone; originals 7–16 cells at detail *Maximum* (roller coaster up to 23), about half at *Minimum* |
| objects | drawn only within about **12.5 units of the llama** (they keep running); their activity radius also follows the fog end |
| hidden areas | visibility regions: a cell is drawn only if the camera region's mask contains its region |
| texture coordinates | none per vertex – every triangle shows half of a texture rectangle |
| lights | light only object models; tiles use baked vertex colours |
The *Draw distance* mod scales all fog values of all levels
([notes/mod-fog.md](../../notes/mod-fog.md)).
## Memory and counts
| Limit | Value | Source |
|---|---|---|
| level memory | **16 MB** for STPC, TEXT, TRAK, MAP, LGPC and the strat pool; originals use up to ~7 MB | `FUN_0041eea0` |
| strat heap | 65533 words (≈ 256 KB) for strat slots (0x154 bytes each), collision blocks and variables | `FUN_0041ae20` |
| strat slots | `poolExtra + 30` – must cover placed **and** spawned objects | MAP |
| vertices per tile model | 8000 (transform buffer) | |
| collision polygon | within ±8 units of its tile's origin | i16 fixed point |
| texture pages | 127 | signed page byte |
| texture entries | 65535 | u16 face field |
| sound samples | 128 per level | signed char id |
| file sizes that worked | TRAK up to 1 MB, 5800 tiles, WAD up to ~7 MB (originals); the test world: 628 models, 2474 tiles, 6.3 MB | measured |
## Things that crash the game
| Cause | Symptom |
|---|---|
| a tile outside the W × H grid | memory corruption, crash on load or later |
| vertex-colour block not exactly one entry per vertex | objects read shifted, crash |
| a strat type with collision but type flags 0 (or too few collision-bone words) | page fault in the heap allocator `FUN_0041ae20` |
| exit target (7, 7, 7) in world 9 | crash after saving (world 10 has no name) |
| a selectable chapter without a WAD | "Couldnt load wad" on the world map |
| a waypoint-dependent type without its boxes | not a crash: the game removes the object ("removing strat") |
| world-9 chapters 2–31 (unpatched exe) | progress, language or options overwritten ([TestLevel](testlevel.md)) |
## Coordinates, numbers, angles
* Game space: x east, y up, z = grid row; strats use 20.12 fixed point (4096 = 1.0).
* MAP angles: 4096 = 360°; strat angles: 0x1000000 = 360°.
* Objects on a flat original floor tile stand at `tile.y + 0.25`.
+110
View File
@@ -0,0 +1,110 @@
# 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**. The English first release is supported as well: all exe
patches find their places in both builds (table below). Its own CD data has not been tested yet –
the formats are expected to be the same.
### groove.exe versions
| Build | PE date | Size | Notes |
|---|---|---|---|
| German | 2001-02-12 | 1,642,496 bytes | reference for all addresses in these pages |
| English (first release, menu "Start Game") | 2000-10-17 | 1,634,304 bytes | same code, shifted (e.g. main menu frame handler 0x549990 instead of 0x549C50); .data globals laid out differently (game state 0x583D74 instead of 0x5846E4, player strat 0x6D94A4 instead of 0x6D9E1C); .data 0xD000 bytes in the file instead of 0xE000 |
LlamaMod supports both (`exe/Site` lists every address it uses in both builds). Addresses in the
reference pages are those of the German build.
## Level naming scheme
Levels are named **`t<tribe>l<world>m<chapter>`** (tribe is always 1 in the game):
| World | Name | Files | Notes |
|---|---|---|---|
| 1 | Village | t1l1m001–004 | `t1l1m001` is the default base level for new levels |
| 2 | Jungle at Night | t1l2m001–003 | t1l2m003 is the llama chase (player strat `LlamaChase`) |
| 3 | River | t1l3m001–004 | player strat `KuscoAndPachaTogetherOnATree` |
| 4 | Jungle by Day | t1l4m001–003 | t1l4m002 has the Alligator (used by the arena) |
| 5 | Mountain | t1l5m001–003 | |
| 6 | City | t1l6m001–003 | |
| 7 | Catacombs | t1l7m001–005 | m001/3/5 are roller-coaster rides (player strat `RollerCoaster`) |
| 8 | Lab | t1l8m001–006 | |
| 9 | TestLevel | none on the CD | 49 chapter slots, see [TestLevel](testlevel.md) |
Other WADs: `t0i0m000` (front end), `t0i0m998` / `t0i0m999` (outro and game-over backdrops),
`t6l6m001/002` (memory-card/options screens – *likely*), `FESOUND.WAD` (front-end sound bank, not
a chunk file). The chapter counts the world map uses are a table in `groove.exe` (0x572420):
4, 3, 4, 3, 3, 3, 5, 5, 49 – world 8 has one more file than its table entry (*unknown* why).
## Pictures (COR)
| File | Use |
|---|---|
| `T1<w><c>.COR` | loading picture of world *w*, chapter *c* (`T191.COR` … `T1949.COR` for TestLevel) |
| `<w><c><n>.cor` | concept art shown with a chapter (*likely*) |
| `tally<n>.cor`, `gameover.cor`, `loading.cor`, logos | screens; `gameovertest.cor` is unused |
Format (*certain*, [`tools/cor2png.py`](../../tools/cor2png.py)): 8-byte signature
`17 98 AF 89 FE 42 D1 12`, u32 version 1, u32 unpacked size, u32 packed size, then LZ data:
a byte `n < 0x80` = *n* literal bytes follow; `1LLLoooo oooooooo` = copy L+3 bytes from *o* bytes
back. The result is 640×480 RGB (a few greyscale ones). [`tools/png2cor.py`](../../tools/png2cor.py)
and [`CorEncoder.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorEncoder.java) write
it; LlamaMod uses that for the loading pictures of new levels.
## Movies and demos
* `movies/<w><c><n>intro.dat` / `…outro.dat` and the logo movies: MPEG-1 video 320×240, 29.97 fps,
MP2 audio, played through DirectShow. The Intro skip mod patches the calls that start them
([notes/mod-introskip.md](../../notes/mod-introskip.md)).
* `wads/*.DEM`: recorded attract-mode demos, u32 frame count + 8 bytes of input per frame
(*likely*; not used by LlamaMod).
## Save games (KSV)
`king0.ksv`…`king2.ksv` in the game folder, 256 bytes each. Decoded from the load/save functions
`FUN_0041c640` / `FUN_0041c820`; code: [`save/Ksv.java`](../../src/main/java/com/eliasmueller/llamamod/save/Ksv.java).
| Offset | Meaning |
|---|---|
| 0x00 | used flag (0 = empty) |
| 0x01 / 0x02 | lives / energy |
| 0x03–0x06 | world-map cursor: tribe, world, chapter, map |
| 0x07 | number of gold coins |
| 0x08–0x3D | u8 [9][6] chapter flags (bit 0 collectible found, bit 1 all coins = gold coin) |
| 0x3E–0x41 | last played tribe, world, chapter, map |
| 0x42 / 0x43 | highest unlocked world / chapter |
| 0x44–0xAF | u16 [9][6] coins per chapter |
| 0xB0–0xCF | unknown |
| 0xD0–0xD9 | options (effects and music volume, speaker mode, …) |
To start a TestLevel chapter without a mod, set the cursor, the last played and the highest
unlocked world/chapter to 9 and the chapter. LlamaMod does this with `llamamod --unlock <slot> 9 <chapter> --cursor`
and with the *Save games* tab.
## Hidden debug features
Strings and code in `groove.exe` point to leftovers of the developers' tools. They were seen in the
binary but **not** verified or used by LlamaMod: F6 writes a screenshot `capture<n>.tga`, the
command-line switch `-display_diagnostics` writes a `<PC name>.dispdiag` file, a log
`c:\kingdom_dbg.txt`, and an unused slide-show player that reads the empty `in`/`out` folders.
The *Built-in cheats* mod uses the game's own cheat codes
([notes/mod-cheats.md](../../notes/mod-cheats.md)).
+155
View File
@@ -0,0 +1,155 @@
# Geometry and collision
The world is **not** one big mesh and there is no BSP. It is a **grid of tiles**, inherited from
Croc: TRAK holds a set of small models (the level's construction kit), and MAP places them on a
W × H grid, one cell = one unit. Every byte described here is reproduced exactly by
[`format/Trak.java`](../../src/main/java/com/eliasmueller/llamamod/format/Trak.java),
[`format/MapChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/MapChunk.java) and
[`tools/geomlib.py`](../../tools/geomlib.py) for all original levels.
Full detail with all loader addresses: [notes/geometry.md](../../notes/geometry.md).
![The test world from above in the editor: every square is one grid cell](../img/testworld-top.jpg)
## Coordinate spaces
| Space | Used by | Axes |
|---|---|---|
| **Game space** | strats, collision, MAP fixed-point tile positions, lights | x east, **y up**, z = grid row (positive); 20.12 fixed point (4096 = 1.0); right-handed as the game shows it |
| **Export space** | TRAK/STPC vertices, MAP float tile positions | same, but **z negated** (grid at z ∈ (−H, 0]); drawn by a Direct3D (left-handed) camera |
| **File space** | OBJ/glTF of every LlamaMod and kit tool | = game space (y up, right-handed): Blender shows a level the same way round as the game |
Converting export ↔ file space negates z and reverses the triangle corner order. Angles:
4096 = 360° in MAP records; the strat VM uses 0x1000000 = 360°. A tile is turned only about y
(0, 1024, 2048, 3072 in practice). Rotation 0 faces +z, 90° faces +x.
## TRAK: tile models (*certain*)
`u32 n`, then **all n headers** (0x84 bytes each), then the data of every model in order.
Typical levels have 100–650 models with up to 422 vertices / 798 faces each.
| Header field | Meaning |
|---|---|
| 0x00 | bounding-box centre (3 × f32) |
| 0x0c | 8 box corners (used for frustum culling) |
| 0x6c / 0x6e | vertex count / face count |
| 0x78 / 0x7a / 0x7c | collision polygon counts in three groups (the engine always uses the sum; meaning of the groups *unknown*) |
| pointers | 0 in the file, set on load |
Data per model:
* **Vertex**, 24 bytes: `f32 x, y, z, nx, ny, nz` (lighting comes from the MAP vertex colours, not
from the normals).
* **Face**, 28 bytes, triangles only: `u16 flags, u16 v0, v1, v2, u16 texture, u16 0, f32 plane
nx, ny, nz, d`.
* **Collision polygon**, 32 bytes: `u8 type, u8 material`, then 5 planes `{i8 nx, ny, nz, u8 0,
i16 d}`. Plane 0 is the surface, planes 1–4 the edges (normals point inwards). Type bit 0 =
quad, otherwise triangle. Normals are ×127, d is 20.12 fixed point – so a collision polygon
must lie within **±8 units of its tile's origin**.
### Face flags
| Bit | Meaning |
|---|---|
| 0x0008 | start of a new render batch (texture page or blend mode changes) – must be set on the first face of each batch, at the latest every 256 faces |
| 0x0010, 0x0020, 0x0800 | which corners of the texture rectangle the triangle uses (below) |
| 0x0400 | double-sided (no backface culling) |
| 0x4000 | ignore vertex colours |
| 0x0001, 0x0002 | special sorting/transparency modes (*likely*) |
| others | present in the data, not read by the PC renderer (*unknown*, probably PlayStation leftovers) |
### No free texture coordinates
The engine has **no per-vertex UVs**. Every triangle shows one half of a texture rectangle from the
TEXT table: with flag 0x800 the face is the first or (with 0x10) second half of a quad, without it
a triangle covering the lower-left half; 0x20 mirrors horizontally. 96 % of world faces are quad
halves. This is why the level builder and editor split surfaces into 1 × 1 pieces and why a narrow
wall shows a squashed texture. See [Textures](textures.md).
## MAP: the tile grid (*certain*)
MAP starts with the grid and the tiles; objects follow (see [Objects](objects.md)). The blocks that
concern geometry:
| Block | Format |
|---|---|
| header | `u32 nTiles, u32 W, u32 H` |
| tiles (floats) | nTiles × `{f32 x, y, z, u32 rotY, u32 model, u32 hasAlt}` in export space; x, z = cell centre |
| regions | 32 × `{u32 visibility mask, i32 value}`, then W·H × u32 region number per cell |
| tiles (fixed point) | nTiles × `{rx, ry, rz, x, y, z}` 20.12 in game space – used for drawing and collision; must match the float copy exactly |
| model index | nTiles × u32 (= the float block's `model`) |
| tile swaps (WFPC 0x10000) | one `{cellX, y, cellZ, modelA, modelB}` per tile with `hasAlt ≠ 0`: doors and bridges that change their model (strat opcodes 0x11a / 0x11b) |
| vertex colours | per tile: one u32 RGBA per vertex of its model (0x80 = 1.0, the game doubles and clamps); more sets for animated colours; then the same for the swap model |
Rules that crash the game or corrupt the level when broken:
* every tile must lie inside the grid (`0 ≤ x < W`, `0 ≤ row < H`) – outside, the cell list
writes into foreign memory;
* the vertex-colour block must have **exactly** one entry per vertex of the tile's model – else
everything after it (the objects) is read shifted;
* the float and fixed-point copies and the model index must agree;
* tile swaps: one entry per tile with `hasAlt ≠ 0`, in tile order.
The cell of a tile is `floor(x) + floor(−z) · W` (export-space floats).
## Collision (*certain*)
* Only **the tiles listed in the cell the player stands in** are tested (`FUN_004036d0` floors and
ceilings, `FUN_00403ad0` walls). A wall polygon that sticks into the neighbouring cell has no
effect there – builders enter a polygon into every cell it covers, as a collision-only tile if
needed.
* Collision is in game space, relative to the tile's fixed-point position.
* There is **no step-up logic**: a 0.12-unit edge with a wall polygon already blocks the llama.
Low plinths need walls without collision; stairs need an invisible slope.
### Collision materials
The material byte ends up in the player's ground state and is read by strats (`floorMaterial`).
| Material | Meaning |
|---|---|
| 0 | normal ground |
| **16** | **exit floor**: the LevelDoor ends the level only when the llama stands on material 16 in the door's cell; also used under the village house doors (zone teleports) |
| 17, 18 | ignored when WFPC flag 0x8000000 is set (*likely*: special surfaces) |
| **30** (0x1e) | death plane / water (*likely*): falling onto it empties the energy and the llama respawns at the last checkpoint or the start (Model 0 of many levels is such an invisible plane at y = −2.75) |
| others | special surfaces, meaning *unknown* – the debug view colours them |
![Debug view of Village 3: collision frames, invisible walls in red, objects as markers](../img/debug-view.jpg)
## Visibility regions (no BSP)
Drawing (`FUN_0042bf40`) walks the grid cells within the draw distance around the camera and draws
a cell's tiles **only if the cell's region bit is set in the mask of the camera's region**. Up to 32
regions; mask 0 or a camera outside the grid means "everything". New geometry must therefore get a
region that the surrounding regions can see, or it stays invisible – the builders put everything
into region 0 with mask `0xffffffff`. Objects in hidden cells are skipped too (unless strat flag
0x800). The editor's *Visibility regions* tool edits this
([manual chapter 13](../manual/13-visibility.md)).
## STPC: object models and code
STPC starts with the **object models** (same model format as TRAK, header 0x8c bytes, optionally
skinned with a bone table), then the **animation sets** (per-frame bone matrices and compressed
vertex deltas – the compression is only partly understood), then the **strat code units**
([Strats](../strats/README.md)). The engine never parses the code; MAP records and bytecode refer
into STPC by absolute byte offsets, so STPC can only be extended at the end or merged with
relocation ([notes/editor-cross-import.md](../../notes/editor-cross-import.md)).
## WFPC flags
WFPC is a u32 of feature flags. Some change the data layout and must stay as they are: 0x10,
0x100, 0x10000 (tile swaps), 0x100000, 0x200000, 0x4000000, 0x10000000. Others: 0x800 renderer
fog flag, 0x80000 read by a strat opcode, 0x8000000 makes collision materials 17/18 ignored.
## Writing new geometry – checklist
1. TRAK: correct counts, bounding box and face planes recomputed, texture index below the
number of texture entries, batch bit 0x8, ≤ 8000 vertices per model.
2. Collision polygons within ±8 units of the tile origin, entered into every cell they cover.
3. MAP: tiles inside the grid, float/fixed/model blocks consistent, vertex colours exactly per
vertex, tile-swap block matching `hasAlt`, a visible region.
4. Rewrite the WAD's total length.
The editor and both level builders do all of this; the details of the builder's approach
(splitting into tiles per cell and height band, merging identical tiles, baking sun, sky and
ambient occlusion into vertex colours) are in [notes/levelbau.md](../../notes/levelbau.md).
+163
View File
@@ -0,0 +1,163 @@
# 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)
An exit is **two parts working together** (decompiled strat code; the details below were checked
in game in Jungle Day 2 with a debugger, the basic rule also in Village 1):
* The **LevelDoor** object (no model) writes its three parameters – the next level (tribe, world,
chapter) – to globals g101–g103 and its grid cell to g104/g105. It does **not** end the level
itself in the cases we tested (see the details below).
* **Kuzco's own script** ends the level when **all** of these hold:
```
floor material under Kuzco == 16 ; the remembered floor (see "floor test" below)
and floor(x), floor(z) == the cell in g104/g105 ; the exit's grid cell, any height
and Kuzco is at most ~0.6 above that floor's height
-> level change to g101..g103 (tally screen)
```
On a material-16 floor **outside** the exit cell the same check is a house door: fade-out and
the teleport of the environment zone of that cell.
So a normal exit needs **a floor with collision material 16 in the exit's grid cell**. In several
original levels that floor only exists after a tile swap opens the door (Village 1 and Jungle Day 2:
the RubyIdolDoor swaps the tile to a model that has the material-16 pad; checked in game in Village
1). The parameters of the originals are (1, world, chapter + 1); **(7, 7, 7)** in the last chapter of
a world means "world finished" – in TestLevel that crashes, so custom levels use (1, 9, chapter).
The tally shows coins x / LNFO total and secrets and unlocks the next chapter. The arena sets
g101–g105 itself to make its own exit ([Arena](../strats/arena.md)).
**Details checked in game (Jungle Day 2, t1l4m002, 2026-11-07):**
* The LevelDoor stores its parameters and cell in g101–g105, then checks global 0x21 **once**
(after 5 ticks); if it is not 3 the door removes itself. In Jungle Day 2 it was gone after the
start, so the Llama's check is what ends the level. That check needs floor material 16, the
door's cell, and the llama no more than 0.6 above the recorded floor height. Off the exit cell
the same material-16 check is the house door (fade-out and zone teleport).
* The floor test only looks at tiles in the player's cell and takes the **nearest upward floor
below the player, at any distance**. Material 17 is ignored for the llama. A llama hovering 3.7
units above a house-door pad records material 16.
* Each tick runs all strat scripts first and then the floor tests. A position change made after
the floor tests is seen by the next tick's scripts with the **old** floor record. So hovering
above any house-door pad and then moving the llama straight to floor height in the exit cell
ends the level, even though the exit cell has no material 16 yet (it appears only in the swap
model when the RubyIdolDoor opens). Moving there from normal ground does nothing.
## 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)) |
+53
View File
@@ -0,0 +1,53 @@
# 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.
Save games are unaffected: a save after finishing a normal or TestLevel chapter is byte-identical
with and without the relocation, and only the 54 entries of worlds 0–8 are saved.
## Making world 9 reachable
* **Empty chapters must exist.** Finishing chapter *n* unlocks chapter *n* + 1, and the world map
crashes ("Couldnt load wad") when a selectable chapter has no file. LlamaMod links every missing
`T1L9M002…049.wad` to an installed world-9 WAD and `T192…T1949.COR` to `T191.COR`.
* **Never exit to (7, 7, 7).** "World finished" in world 9 unlocks a world 10 that has no name;
the game crashes after saving. Exits in TestLevel point to (1, 9, chapter).
* **Get there:** set a save game's map cursor to world 9 (`llamamod --unlock <slot> 9 <chapter>
--cursor`, the launcher's *Save games* tab), or use the *Boot to level* mod / the editor's
play-test, which start the game directly in the level.
## Tally totals
The tally screen after the exit shows "coins collected *x* / *total*". *Total* is not counted by
the game – it is the **LNFO** chunk (`u32 coins, u32 secrets`). A new level built on the village
would show the village's 55. LlamaMod recounts it for new levels (Coin = 1, Coin5 = 5, Secret
objects; the arena adds its dynamic coins).
Cosmetic leftovers: the world map shows no background for world 9 and a garbage coin target, and
the loading screen says "TestLevel Kapitel *n*".
+53
View File
@@ -0,0 +1,53 @@
# Textures (TEXT chunk)
Each level carries its own textures. Parse and rebuild are byte-identical for all level WADs
(including the RLE encoder). Code: [`format/TextChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/TextChunk.java),
[`tools/textchunk.py`](../../tools/textchunk.py). Lab notes: [notes/textures.md](../../notes/textures.md),
editor side: [notes/editor-tools-textures.md](../../notes/editor-tools-textures.md).
## Layout
```
u32 nPal 0 in all WADs (8-bit palettes are unused)
u32 nPages 13–22 per level, all 256×256
nPages × { u32 flags, u32 w, u32 h, data }
flags & 0x80 : u32 length + RLE16 (all original pages)
other ≠ 0 : w·h raw u16
u32 nTex ~180–1260 texture entries
nTex × { u16 flags, u8 page, u8 clut (0xff), u8 x0, x1, y0, y1 } (edges inclusive)
u32 nAnim texture animations
nAnim × { u16 texIndex, u8 type, payload } type 8 = flip book: list of texture indices
```
RLE16: a sequence of s16 *n*; *n* > 0 → *n* literal pixels follow, *n* < 0 → the next pixel
repeated −*n* times.
## Pixels
16 bit in **PlayStation order**: bits 0–4 red, 5–9 green, 10–14 blue, bit 15 = STP
(semi-transparency). `0x0000` is fully transparent. Export to PNG uses alpha 0 = `0x0000`,
alpha 128 = STP 0, alpha 255 = STP 1. The page flags (low 7 bits) select the blend class:
1/6 opaque, 4/7 alpha blend, 5 another blend mode, 2/3 special converters (*likely* the
PlayStation semi-transparency modes; details open).
## Texture entries
A face never has its own UVs: it names a **texture entry**, a rectangle on a page, and shows half
of it ([Geometry](geometry.md#no-free-texture-coordinates)). Entry flag bit 0 = flat colour (the
rectangle bytes are R, G, B instead) – the level builder appends such entries for plain colours.
Other entry flag bits (blend mode, size class) are only partly understood.
Limits: at most **127 pages** per level (signed page byte) and 65535 entries (u16 face field).
## Changing textures
* **Replace pixels, keep the table.** Exporting pages to PNG and importing them back changes no
index anywhere: `tools/text_export.py export level.wad dir` / `import level.wad dir new.wad`.
* **Add pages and entries.** New pages and entries are appended; faces then use the new indices.
The editor's texture manager does this, including colour reduction, painting onto faces and
flip-book animations ([manual chapter 8](../manual/08-textures.md)).
* **Same name, same picture.** Texture *indices* differ between base levels. The builders resolve
texture names (`grass`, `stone`, …) by picture fingerprint on each base and copy missing
village pictures over ([notes/levelbau.md](../../notes/levelbau.md)).
![Contact sheet of the Village 1 textures with their indices, made by tools/texsheet.py](../img/village-textures.jpg)
+63
View File
@@ -0,0 +1,63 @@
# WAD container
A level is one file `wads/t1l<world>m<chapter>.wad`. It is a flat list of chunks:
```
u32 total length
repeat: u32 tag (four ASCII letters stored reversed: "OFNI" in the file = INFO)
u32 length
length bytes of chunk data
```
There are no checksums. When a chunk changes size, only the chunk length and the total length
must be rewritten ([`format/Wad.java`](../../src/main/java/com/eliasmueller/llamamod/format/Wad.java),
[`tools/wadtool.py`](../../tools/wadtool.py) – `wadtool.py list|extract|replace`).
## Chunks
In file order. "Loader" is the function in `groove.exe` that reads the chunk (dispatcher at
0x558960).
| Chunk | Contents | Documented in |
|---|---|---|
| INFO, VERS | u32 1 each, ignored | – |
| WFPC | u32 feature flags of the level; they switch optional blocks in MAP and STPC on and off – never change them | [Geometry](geometry.md#wfpc-flags) |
| TEXT | texture pages, texture rectangles, texture animations | [Textures](textures.md) |
| FONT | 256 × 4 u16 glyph rectangles | – |
| SPRT | sprite list (u32, more with WFPC 0x100000) | – |
| NAME | u32 + level name (not shown anywhere in the game) | – |
| SMPC | sound samples of the level (`cvg ` blocks) | [Music and sound](audio.md) |
| AMPC | ambient sound data (`ambi`/`VABp`) | [Music and sound](audio.md) |
| SRPC | references into the speech file `music/<lang>.cvs` | [Music and sound](audio.md) |
| **TRAK** | world geometry: the tile models (render triangles + collision polygons) | [Geometry](geometry.md) |
| **STPC** | object models, skeletal animations and the **strat code** (object behaviour) | [Geometry](geometry.md#stpc-object-models-and-code), [Strats](../strats/README.md) |
| **MAP** | the tile grid, vertex colours, visibility regions, objects, waypoints, environment zones | [Geometry](geometry.md), [Objects](objects.md) |
| LGHT | lights (directional and point lights for object models) | [Objects](objects.md#lights) |
| LNFO | u32 coins, u32 secrets: the totals shown on the tally screen | [TestLevel](testlevel.md#tally-totals) |
| LGPC | dialogue texts in all languages + speech slots | – |
| END | end marker | – |
Not every level has every chunk. The three chunks that matter for level building are **TRAK**,
**STPC** and **MAP**; a new level takes everything else (textures, sounds, sky, fog, music) from an
original **base level**.
## The base-level idea
An object type exists in a level only if its code is in that level's STPC. MAP refers to a type
by its **byte offset into STPC** – there is no global type id. So every custom level starts as a
copy of an original level (default: Village 1, `t1l1m001`) and replaces geometry and objects:
* Python [`tools/levelbuild.py`](../../tools/levelbuild.py) and Java
[`levelbuild/LevelBuilder.java`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java)
generate TRAK and MAP from a script and keep STPC, TEXT and sound of the base.
* The level editor stores only the difference to the base level and rebuilds the WAD from the
user's own CD ([notes/editor-format.md](../../notes/editor-format.md)).
* Object types of *other* levels can be merged into the base's STPC together with their models,
animations, textures and sounds – the editor's *All worlds* import and the arena use this
([notes/editor-cross-import.md](../../notes/editor-cross-import.md)).
## Level memory
The game loads STPC, TEXT (compressed pages + 20 bytes per texture entry), TRAK, MAP, LGPC and the
strat pool into one **16 MB** block (`FUN_0041eea0`); original levels use up to about 7 MB. See
[Engine facts and limits](engine-limits.md).
+120
View File
@@ -0,0 +1,120 @@
# Strats: how objects work
Every object in the game – a coin, a squirrel, a moving platform, a camera, the llama itself – is
driven by a small program called a **strat** (Argonaut's "strategy" scripts, inherited from Croc).
The programs are compiled bytecode for a virtual machine in `groove.exe` (`FUN_0054d180`). LlamaMod
can read every one of them, change their numbers, write new ones and move them between levels.
| Page | For |
|---|---|
| this page | how the VM works: units, instances, frames, variables, events, spawning, collision |
| [Reading scripts](reading-scripts.md) | the editor's Script view and the assembly listing, with a worked example |
| [The script language](script-language.md) | writing new behaviour in LlamaMod's structured language |
| [Customising strats](custom-types.md) | changing constants, cloning types, custom types in the editor and in mods |
| [Example: the Alligator Arena](arena.md) | a complete minigame written in the script language, explained step by step |
| [Example: the roller coaster](roller-coaster.md) | a new ride for the original coaster cart: how the ride works, track and waypoint data, speed patch |
Lab notes and tables: [notes/strat-opcodes.md](../../notes/strat-opcodes.md) (all 434 opcodes with
semantics, confidence and Argonaut names), [notes/strat-language.md](../../notes/strat-language.md)
(complete syntax), [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md)
(parameters of individual types). Machine-readable tables, used by the editor and the compiler:
[`ops.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/ops.tsv),
[`props.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/props.tsv).
## Code units and types
The strat code of a level sits at the end of its STPC chunk as a list of **code units**:
`u32 nUnits`, then per unit `u32 nWords` and the words. A unit is one compiled source file and can
contain several strats plus their name strings and data (for example `Spider` + `RopeForSpider`).
The village has 53 units, Lab 1 has 65.
A **type** is just an entry point into this code: a MAP record or a `spawn` instruction names a
byte offset in STPC, and the code there starts with `debugName "Name"`. Code refers to models,
animation sets and other code by **absolute STPC offsets** – which is why STPC cannot be
rearranged without relocating every reference, and why each level carries its own copy of the
code of every type it uses.
LlamaMod's disassembler covers the code of all 34 level WADs (1461 units, 670 555 words) and
assembles every unit back **byte-identically**.
## One frame in the life of a strat
The VM is a **stack machine** that runs every live strat once per frame (30 frames per second):
1. The strat continues where it stopped last frame.
2. It executes instructions – push values, compute, read and write properties, play animations,
move – until it reaches a **yield** (`jmpYield`, `jzYield`, `callYield` …).
3. At the yield its turn ends; the next strat runs. Next frame it continues after the yield.
So a typical object is "set up, then `loop { …; yield }`", and anything that takes time is a loop
that counts frames. Speeds and turn rates in scripts are **per frame**.
`end` / `remove` deletes the strat; `return` returns from a `call`.
## Data a strat works with
| Kind | Syntax | What it is |
|---|---|---|
| **variables** | `v0`, `v1`, … (or an alias name) | the strat's own variables (count = the type's *nVars*). The first *n* hold the instance's **MAP parameters** |
| **properties** | `self.x`, `self.rotY`, `self.floorHeight`, `self.damageTaken` … | 194 named engine fields of the strat: position, rotation, floor under it, distance to the player, animation state, alpha, collision damage … |
| other strats' properties | `player.x`, `parent.v3`, `camera.…`, `target.…`, `boss.…`, `dialog.…` | the player, the strat that spawned this one (`parent`), the camera and a few registered special strats |
| **globals** | `g0` … | shared by all strats; the engine itself never reads most of them – their meaning comes from how scripts use them (e.g. g95 = event bits, g101–g105 = exit target and cell, g17–g20 = dive target) |
| **numbers** | `1.0`, `90deg`, `204r` | 20.12 fixed point (4096 = 1.0 = one cell); angles 0x1000000 = 360°; `r` marks a raw integer |
Binary operators pop *b* (top) then *a* and push `a op b`; comparisons push 0/1; `mul` = a·b >> 12.
## Events
A strat registers **event handlers** in its slots (`onEvent`, in the script language
`on touched { … }` / `on event(type, param) { … }`). When the event fires, the handler runs like a
subroutine. Known event types:
| Type | Fires when |
|---|---|
| 4 | **touched**: contact with another strat (pick-ups, enemies). Needs collision (`collisionOn` + `collRadius`) |
| 0x40 | timer |
| 0x80 / 0x100 | player distance |
## Spawning
`spawn<params, vars, slots, typeFlags, extra>(arguments…, unit:N+offset)` creates a new strat from
code: the five inline values are exactly the type fields of a MAP record
([Objects](../reference/objects.md#strat-record-58-bytes-certain-unless-noted)), the stack
arguments become the child's parameters, and the child's `parent` is the spawner. Facts found
the hard way (by the arena):
* The child appears at the spawner's **matrix position** of the last frame, not at its current
`x/y/z`: after `self.x = …` the spawner must **yield** before spawning.
* A type that uses collision needs collision type flags (e.g. 0x28000 like Coin) and enough
`extra` words for its collision bones – otherwise the game crashes in the heap allocator.
* The strat pool must have room for every spawned strat (`poolExtra`).
## Collision and damage
On contact, each side's `hitDamage` is copied into the other's `damageTaken`, and both get event 4.
The llama's attacks set bits in its `hitDamage`: **256** kick (Action on the ground), **512** dive
(Action in the air; it homes in on the target an enemy publishes in g17–g19), **1024** charge.
The llama counts a contact as *its* hit if the other's `hitDamage` shares a bit with its own,
otherwise `damageTaken & 127 == 1` costs it a heart and knocks it back. The original Alligator has
513 (= 512 | 1): only the dive hurts it, a kick gets you bitten.
## References to level data
Some operands are indices into the level's own data and differ between levels; the cross-level
import rewrites exactly these:
| Operand | Points to |
|---|---|
| `model:N`, `anim:N`, `unit:N[+off]` (`address`, opcode 0xb2) | a model, an animation set, code of a unit; negative = shared animations in `DEFANIM.WAD` |
| `sound:N` (opcode 0xd7) | a sample of the level's sound bank |
| `text:N` (opcodes 0xf0, 0xfa) | a dialogue line in LGPC |
| `tex:N` (pushed before texture opcodes) | a texture entry (shadows, sprites, particles) |
## How the names were found
All 434 opcodes (0x000–0x1b1) were named by decompiling every handler with Ghidra; 252 of them
match Argonaut's own names from the Croc 2 PlayStation demo's jump table and the Croc 1
decompilation (knowledge only, no code used – see the sources in
[notes/strat-opcodes.md](../../notes/strat-opcodes.md#sources)). Confidence: 353 high, 79 medium,
2 low; high-confidence opcodes cover 97 % of all instructions in the levels. The names of rarely
used opcodes and properties can still be wrong.
+240
View File
@@ -0,0 +1,240 @@
# Example: the Alligator Arena
The *Alligator Arena* mod is a complete minigame built from scratch: a walled pit on an island,
alligators coming in waves, coins popping out on every hit, an exit that opens after the last wave.
It uses every technique of this documentation – a scripted level, two new strat types written in
the [script language](script-language.md), and an object (the Alligator) imported from another
world. This page walks through the code.
![The arena: an alligator chasing the llama](../manual/img/mod_arena.jpg)
| File | Role |
|---|---|
| [`ArenaMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java) | the mod: options, writes the WAD and the loading picture |
| [`Arena.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/Arena.java) | the level script: geometry and object placement |
| [`ArenaScripts.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaScripts.java) | the strat scripts `ArenaMaster`, `ArenaGator` (and a test probe) |
| [`ArenaImport.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaImport.java) | compiles the scripts and merges them into the base level |
| [`ArenaTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/arena/ArenaTest.java) | unit tests (scripts compile for all option corners, the built level decodes) |
| [notes/mod-arena.md](../../notes/mod-arena.md) | lab notes: engine facts found, what was verified in the game |
Options: alligator **lives** (1–10, default 5), **waves** (1–5, default 3; wave *n* brings
min(*n*, 2) alligators), llama **energy** cap (1–5, default 4), **coins** per hit (0–5, default 1),
TestLevel **chapter** (32–49, default 49).
## 1. The plan
* The **base level** is Village 1 (`t1l1m001`), like every new level: its code provides the
Llama, Coin, CheckPointPost and textures.
* The Alligator lives in **Jungle by Day 2** (`t1l4m002`). Its model, animations and sounds are
needed, so the new scripts are **compiled against that level** and then **imported** into the
village base together with everything they reference.
* Two new types: `ArenaMaster`, an invisible referee standing on the exit pad, and `ArenaGator`,
a new alligator spawned by the master.
* The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)).
## 2. The level (`Arena.build`)
The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java)
for the Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java),
the same builder the Test World uses. Coordinates are game units (one cell each):
```java
l.sea(0, 0, 40, 40, -1.5, new double[] {ix0 - 3, iz0 - 3, ix1 + 3, iz1 + 3}); // sea + death plane
l.floor(a.x0(), a.z0(), a.x1(), a.z1(), a.floor(), "sand"); // pit floor 16..24
l.box(a.x0() - t, a.z0() - t, a.x1() + t, a.z0(), 0, WALL,
o().top("stone").side("stone_moss")); // one of four walls
// exit pad: one cell with collision material 16 under a gate
l.box(ex - 0.5, ez - 0.5, ex + 0.5, ez + 0.5, a.floor(), a.floor() + 0.06,
o().top("pattern_purple").side("inca").rot(0).sideColl(false).mat(DOOR_MAT));
l.player(st[0], st[1], st[2], null, 1.6); // llama + start camera node 1.6 behind it
l.checkpoint(a.x0() + 0.8, a.z0() + 0.8);
l.strat(ArenaScripts.MASTER, ex, ez, a.floor(), 0.0, null, LevelBuilder.Waypoints.NONE, 0, false);
l.coins(List.of(new double[] {a.x0() + 0.6, 20}, new double[] {a.x1() - 0.6, 20}));
```
Points to note:
* The **exit pad** is a 1 × 1 box whose top has collision **material 16** – the exit rule from
[Objects](../reference/objects.md#the-exit-leveldoor--certain). There is no LevelDoor; the master
takes its role.
* The walls are 1.1 high – too high to jump (0.35), so the fight stays in the pit.
* Texture names (`sand`, `stone_moss`, `inca`) are resolved on the base level by picture.
## 3. Compiling and importing (`ArenaImport`)
`Arena.buildWad` runs before the geometry is built:
```java
merged = ArenaImport.merge(baseWad, src, "t1l4m002", units(probe, debug));
return LevelScript.super.buildWad(merged.wad(), wads, opts);
```
1. **`append`** compiles each unit with `StratCompiler` → `StratAssembler`, laid out as if it were
the last unit of **t1l4m002's** STPC, and appends it there. So `model:47` is the Alligator's model
and `unit:56+0xf0` is that level's `EnemyCoin`. A later unit can name an earlier one as
`unit:@ArenaGator`; `append` replaces that with `unit:N+offset`.
2. **`merge`** asks [`StpcMerge`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/StpcMerge.java)
– the editor's cross-world import engine – to import the two entry points into the village:
it follows every reference and copies the model, 7 animation sets, textures, 3 sound samples and
the spawned strats (`EnemyCoin` and `Sparkles` already exist in the village and are shared),
relocates every operand and verifies the result.
3. It adds one **unplaced prototype MAP record** per type with its type fields
(`nVars`, `slots`, `typeFlags`, `extra`), so the level builder can place the type by name.
The type fields are declared in `Arena.units`:
| Type | vars | slots | type flags | extra | why |
|---|---|---|---|---|---|
| ArenaGator | 16 | 5 | 0xC000 | 3 | collision like the Alligator; 3 collision bones |
| ArenaMaster | 12 | 0 | 0 | 0 | invisible logic, no collision, no handlers |
## 4. The referee: `ArenaMaster`
`ArenaScripts.master()` generates the script; the waves are unrolled by Java. With 2 waves it
reads like this (simplified):
```
strat ArenaMaster {
alias wave = v1
alias alive = v3
alias kills = v4
alias t = v5
g104 = -1 // no exit cell yet (the llama's own exit check reads g104/g105)
g105 = -1
t = 60 // 2 seconds
while (t > 0) { t -= 1; call cap; yield }
wave = 1
self.x = 20.5 // move to a spawn spot …
self.z = 22.2
yield // … and let the engine update the matrix before spawning
spawn<0, 16, 5, 50176, 3>(unit:@ArenaGator)
alive += 1
self.x = 20.5 // back onto the exit pad
self.z = 23.5
while (alive > 0) { call cap; yield } // gators do parent.v3 -= 1 when they die
t = 45
while (t > 0) { t -= 1; call cap; yield }
wave = 2
… // two spawns this time
g101 = 4096r // exit target (1, 9, chapter) as raw 20.12 values
g102 = 36864r
g103 = 200704r
g104 = int(self.x) // the exit cell
g105 = int(self.z)
… // 5 bonus EnemyCoins
loop {
call cap
if (player.floorMaterial == 16 && int(self.x) == int(player.x)
&& int(self.z) == int(player.z)) {
endLevel(4096r, 36864r, 200704r) // the LevelDoor's rule
g1 += 1
remove
}
… // a Sparkles effect every 8 frames
yield
}
}
sub cap { // energy option: keep the llama's energy at the cap
if (player.health2 > 4) { player.health2 = 4 }
}
```
What it shows:
* **Variables as state** – `alive` is `v3`, which the gators change from outside with
`parent.v3 -= 1`: the parent/child link is the communication channel.
* **Waiting = looping with yield**: every wait is a `while … yield` that also calls `cap`, so the
energy cap holds every frame (also after a respawn).
* **The spawn rule**: move, `yield`, then `spawn` – without the yield the alligator would appear at
the master's old position (it copies the matrix of the last frame).
* **The exit** is the LevelDoor logic written out: material 16 under the llama in the same cell →
`endLevel(tribe, world, chapter)`. Setting g101–g105 additionally arms the Llama's built-in check
for the same cell.
## 5. The alligator: `ArenaGator`
`ArenaScripts.gator()` reuses the original Alligator's assets but has its own behaviour. Its parts:
**Setup** – model, size, collision and how it takes hits:
```
setModel(model:47)
scale(1.5)
self.hitDamage = 1793 // 1 | 256 | 512 | 1024
collisionOn<16384>()
collRadius(409r)
collisionBone(0, 409r) // three collision spheres along the body
collisionBone(6.0, 409r)
collisionBone(10.0, 409r)
on touched {
if (self.damageTaken & 1792) { // kick, dive or charge of the llama
if (cool <= 0 && lives > 0) { hit = 1 }
} else {
bit = 1 // contact without an attack: it bit the llama
}
}
```
`hitDamage` 1793 shares a bit with all three llama attacks, so each of them counts as the llama's
hit; bit 1 makes a plain contact hurt the llama ([Collision and damage](README.md#collision-and-damage)).
The handler only sets flags – the main loop reacts to them, which keeps the handler short.
**Fade-in** – `setAmbient` from 0 to 63 over 32 frames, playing the idle animation.
**Main loop** – a state machine in a `switch`, once per frame:
| State | Behaviour |
|---|---|
| 0 watch | turn towards the player (2° per frame); after a timer, if the llama is within 5 cells and on the same floor height (±0.4), play the snap animation and sound |
| 1 snap | wait for the animation, then start chasing |
| 2 chase | accelerate up to `0.03 + 0.006 · wave` per frame towards the llama; lunge when closer than 1.2, give up after 75 frames |
| 4, 5 lunge | fast forward move, then slow down (numbers from the original Alligator) |
| 3 back off | move backwards for a few frames, then rest and watch again |
Before the `switch`, the flags from the handler are handled:
* `hit`: lose a life, sound, three green particle puffs, *coins* `EnemyCoin`s, blink for 45
frames (alpha 0.25 and a red tint every 4 frames, no further hits meanwhile), back off.
* `bit`: back off and rest 90 frames, so the llama gets a breather.
After the `switch` the gator is clamped to the pit and to the floor height.
**Death** – when `lives` reaches 0 the loop `break`s: collision off, the original's death (shrink
over a few frames with puffs, five `EnemyCoin`s), then `parent.v3 -= 1` (one alive less for the
master), `parent.v4 += 1` (kills) and `remove`.
**`sub aim`** – ported from the original Alligator: each frame it publishes its position in
g17–g19 (and the distance in g20) if it is the best target below a jumping llama. That is what makes
the llama's **dive** home in on it.
## 6. The mod (`ArenaMod`)
`apply` refuses a chapter another mod already wrote, builds the WAD, rewrites the tally total
(`LevelTally.counted`: 2 placed coins + every coin the alligators can drop + 5 bonus), writes it
to `wads/T1L9M0nn.wad` and draws a loading picture with Java2D (`CorEncoder`). That is all a level
mod needs: LlamaMod's mod pipeline puts the files into the game's CD folder and links the empty
TestLevel chapters.
## 7. How it was tested
* `ArenaTest` compiles the scripts for every corner of the options, checks wave sizes, builds the
level from the CD and decodes the merged code (the master spawns the gator unit, the gator
spawns EnemyCoin).
* In the game ([`tools/apptest.py`](../../tools/apptest.py)): a probe type (`ArenaProbe`, option
`probe=true`) proved that compiled `on touched` handlers fire; then fights with default options,
a quick win with `lives=1, waves=1`, and a defeat (energy 0 → *Continue* restarts the arena).
![Defeated alligator: puffs and coins](../img/strats/arena_defeat.jpg)
![Win: bonus coins on the exit pad, then the tally](../img/strats/arena_win.jpg)
## What to take from it
1. Borrow assets by **compiling against the level that has them** and importing with `StpcMerge`.
2. Keep event handlers short; set flags and react in the main loop.
3. Communicate through `parent.vN` and globals; spawn only after a `yield`.
4. Copy numbers (speeds, animation indices, type fields) from the original type's script.
5. Write the exit yourself if you need a condition – material 16 + same cell + `endLevel`.
+101
View File
@@ -0,0 +1,101 @@
# Customising strats
From the smallest change to a completely new object type. Steps 1–5 need only the level editor;
step 6 is for programmers writing a LlamaMod mod.
| Step | What changes | Where it is stored |
|---|---|---|
| 1. Object parameters | one placed object | the project's object list |
| 2. Constants | a number in a type's code – every object of that type | `stratEdits.constants` |
| 3. Import a type from another level | the level's code gets a new type with its models, sounds, texts | the project's import list |
| 4. Clone a type | a new type as a copy of an existing one | `stratEdits.types` (source text) |
| 5. New type from a script | a new type written from scratch | `stratEdits.types` |
| 6. Script types in a mod | types compiled and merged by Java code | the mod's source |
A project never stores game code: constants are stored as *old → new value at an offset*, custom
types as source text, imports as *type X from level Y*. Everything is rebuilt from the player's own
CD when the project is opened ([notes/editor-format.md](../../notes/editor-format.md)).
## 1. Object parameters
Select an object; the Inspector's *Object parameters* section shows its MAP parameters (= the
variables v0, v1, … of its strat). For many common types the editor knows names, ranges and meanings
(schemas in [`Schemas.java`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/objects/Schemas.java));
the rest are raw values with the values used in the original levels as a hint.
User guide: [manual chapter 9](../manual/09-objects.md).
## 2. Constants
Double-click a line with a number in the Script panel (or *Edit value…*). The change applies to
every object of the type, is undoable and turns the line orange. On load the old value is checked
first, so a project made for another game version cannot corrupt the code silently. A
`pushShort` holds only −8.0 … 7.99; larger values need a source edit.
![Original and edited Coin constant in the game](../manual/img/strat_coin_constant_ab.jpg)
## 3. Importing types from other levels
*Assets → All worlds* lists every object of every level; dragging one into the viewport imports it.
The import copies the type's code **and everything it reaches**: spawned sub-strats (projectiles,
shards, heads), models, animation sets, textures, sound samples and dialogue lines, relocates all
references, shares what the level already has (Coin, EnemyCoin, Sparkles …) and verifies the
merged code. Limits: 16 MB level memory, 128 sounds, 127 texture pages.
Details: [notes/editor-cross-import.md](../../notes/editor-cross-import.md),
[manual chapter 9](../manual/09-objects.md#importing-objects-from-other-worlds).
## 4. Cloning a type
*Clone as new type…* in the Script panel copies a type's listing into the type editor, renamed.
Change what you need, *Check* (assemble and verify) and *Create type*. The new unit is appended at
the end of the level's code – nothing else moves – and the type appears in *Assets → Objects*.
* **Type fields / parameters from** decides which existing type's MAP fields (variables, slots,
collision flags, extra words) and parameter names new objects get. Pick the type you cloned,
unless you changed how many variables or collision bones the code uses.
* *Edit type…* and *Delete type…* work later on, with undo; placed objects follow their type.
* Custom types can refer to each other by name (`unit:OtherType`).
The manual's tutorial *Make the squirrel run in a circle* clones Bucky and inserts a circle loop
([manual chapter 22](../manual/22-strats.md#tutorial-make-the-squirrel-run-in-a-circle)).
![Two squirrels running in circles: waypoint chain and custom script](../manual/img/strat_bucky_circle.jpg)
## 5. A new type from a script
*Level → New object type from script…* opens the type editor with a template. Set *Language* to
*Script* and write the behaviour in the [script language](script-language.md). Models, animations
and sounds are those of the current level (`model:N`, `anim:N`, `sound:N` – look them up in the
Script panel of a type that uses them). Choose *Type fields from* a type with similar needs (a
pick-up: Coin; a walking enemy: one of the enemies).
## 6. Script types in a mod (Java)
A mod can do the same in code. The arena shows the pattern
([`mods/arena/`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/)):
1. Write the types as script-language text (often with values filled in from options).
2. **Compile against a source level** whose models, animations and sounds you want to use – the
indices in the script are that level's:
`StratAssembler.assemble(StratCompiler.compile(src), StratListing.layout(stpcImage, base))`
and append the words as a new unit to the source level's STPC (`ArenaImport.append`).
3. **Merge into the base level** with `StpcMerge` (the same engine as the editor's import): it
copies the closure (models, animation sets, textures, sounds, spawned strats), relocates and
verifies. Add one unplaced prototype MAP record per type with its type fields.
4. **Build the level** with a `LevelScript` / `LevelBuilder`
([`levelbuild/`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/)) and place the types by
name with `l.strat(...)`.
5. Write the WAD into a TestLevel chapter from the mod's `apply` and add a loading picture.
The [arena page](arena.md) walks through every part of this.
## Choosing type fields
| Field | What to put there |
|---|---|
| variables | at least the highest `vN` the code uses + 1 (parameters count as variables) |
| slots | number of event handler slots (`on …` blocks) – at least 1 if there is one |
| type flags | 0 for invisible logic; collision types need collision flags (Coin: 0x28000; Alligator: 0xC000 / 0xC400) |
| extra | collision-bone words: at least the number of `collisionBone` calls |
Wrong values do not always fail at once: too small collision areas corrupt the strat heap and
crash later (page fault in `FUN_0041ae20`).
+114
View File
@@ -0,0 +1,114 @@
# Reading scripts
LlamaMod shows every strat in two forms: the **Script view**, a readable reconstruction, and the
**Assembly** listing, the exact instructions. Both are in the level editor's *Script* panel
(**View → Panels → Script**, **Ctrl+Shift+K**); the user-level walkthrough is
[manual chapter 22](../manual/22-strats.md).
![The Script panel with Bucky's script](../manual/img/strat_script_panel.jpg)
Code: [`StratListing`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratListing.java)
(assembly), [`StratPseudo`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratPseudo.java)
(script view), [`OpTable`](../../src/main/java/com/eliasmueller/llamamod/format/strat/OpTable.java)
(names and help texts).
## Example: the Coin
Script view of the village coin:
```
strat Coin:
setModel(model:5)
collRadius(409r)
onEvent ev_0000, 0 // event type 4 → L_0084
if (v1) { collisionOn<32768>() ... }
if (v0) {
if (self.floorMaterial == -1.0) { skipArgs … } else {
self.y = self.floorHeight + 204r
self.rotY = rnd(4096.0)
}
}
loop { turnLeft(5.625deg); yield }
```
Line by line:
| Line | Meaning |
|---|---|
| `setModel(model:5)` | show model 5 of this level's STPC (the coin) |
| `collRadius(409r)` | collision radius 409/4096 ≈ 0.1 cell |
| `onEvent ev_0000, 0` | install the handler table entry `ev_0000` in slot 0: event type 4 (touched) jumps to `L_0084`, the pick-up code |
| `if (v1) …` | parameter 1 switches collision on |
| `if (v0) …` | parameter 0 (`snapToFloor` in the editor) puts the coin 204/4096 ≈ 0.05 above the floor under it and turns it randomly – unless there is no floor (`floorMaterial == -1`) |
| `loop { turnLeft(5.625deg); yield }` | spin 5.625° per frame, forever |
Changing `204r` to `1.5` makes every snapping coin float 1.5 cells high – that is a constant edit
([Customising strats](custom-types.md)).
## Script view conventions
| You see | It means |
|---|---|
| `self.x`, `player.y`, `parent.v3` | property or variable of this strat, the player, the spawner |
| `v0`, `snapToFloor` | variable 0; named when the editor knows the type's parameter |
| `g12` | global variable 12 |
| `1.0`, `5.625deg`, `204r` | fixed point (1.0 = one cell), an angle, a raw integer (204/4096) |
| `op<a, b>(x, y)` | instruction `op` with inline operands `a, b` and stack arguments `x, y` |
| `loop { … yield }` | runs once per frame |
| `while`, `do … while`, `switch`, `break`, `continue`, `break name` | rebuilt from the jumps |
| `a && b`, `a \|\| b` | short-circuit conditions (chains of conditional jumps) |
| `goto L_xxxx` | jumps that do not form nested blocks – 31 places in the whole game, all one pattern in the llama's script |
The script view is a **reading aid** and is not compiled back. Edits happen on the assembly (or
in the script language for new types).
## The assembly listing
One instruction per line, exactly as stored:
```
Coin:
debugName "Coin"
address model:5 ; model
setModel
onEvent ev_0000, 0 ; event type 4 → L_0084
pushVar snapToFloor ; v0 (alias from the object schema)
jz L_018c
...
L_0218:
push 5.625deg ; = 64.0
turnLeft
jmpYield L_0218
end
```
* Short opcodes (below 0x45) carry a signed 16-bit operand: `pushVar v3`, `pushShort 1.5`,
`jz label`. Long opcodes take inline words: `spawn 3, 6, 0, 0, 0`.
* Directives: `.unit`, `.alias`, `.word`, `.string`, `.event`, `.switch` / `.case`, `.at`.
* Disassemble → assemble is byte-identical for all 1461 units of the game; no instruction is
emitted as raw words.
Full syntax: [notes/strat-language.md](../../notes/strat-language.md). Every opcode with its stack
effect and meaning: [notes/strat-opcodes.md](../../notes/strat-opcodes.md).
## Finding things
* **Which script runs this object?** Select it with *Follow selection* on.
* **Who spawns this type?** The side panel lists the units that reference it.
* **What does this instruction do?** Select the line; the side panel shows the opcode's
semantics, stack effect, Argonaut name and confidence.
* **Where is this variable used?** *Uses* lists every line with the selected operand.
* **Outside the editor:** [`tools/strats.py`](../../tools/strats.py) `level.wad --all` lists every
named strat of a level with code offsets and type fields;
[notes/strat_types_table.md](../../notes/strat_types_table.md) shows which level has which type.
## Typical patterns
| Pattern | Meaning |
|---|---|
| `v1 * 30` | seconds → frames |
| `self.distToPlayerSqXZ < 25` | player within 5 cells (squared distance) |
| `turnTowardAngleY(self.angleToPlayer, 2deg)` | turn towards the player, 2° per frame |
| `if (!self.animPlaying) animPlay(anim:N)` | loop an animation |
| `wpFirst()`, `wpNext()`, `self.wpParam` | walk the waypoint chain; per-node parameter |
| `g95 = g95 \| (1 << (v - 1))` | set an event bit that guards wait for |
+192
View File
@@ -0,0 +1,192 @@
# Example: the roller coaster
The *Llama Coaster* mod builds a new roller-coaster ride: a circuit with drops, climbs, boost
arrows, a spin pad, a jump over a gap, a station where the ride starts and ends, and fireworks at
the finish. Unlike the [arena](arena.md) it writes no new strat code: it **reuses the game's own
coaster cart** (`RollerCoaster`) and only gives it new track data – new geometry and a new
waypoint chain. This page explains how the original ride works and how the mod feeds it.
![The ride: first drop, boost, jump, spin pad, climb, station](../img/strats/coaster_ride.jpg)
| File | Role |
|---|---|
| [`CoasterMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterMod.java) | the mod: options, chapter check, writes the WAD and the loading picture |
| [`CoasterTrack.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterTrack.java) | the two layouts; turns legs and pieces into segments, corners and the waypoint chain |
| [`CoasterLevel.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterLevel.java) | the level script: track bed, rails, pillars, station, ground, objects |
| [`CoasterSpeed.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterSpeed.java) | patches the cart's speed constants for the Speed option |
| [`CoasterSim.java`](../../src/main/java/com/eliasmueller/llamamod/mods/coaster/CoasterSim.java) | frame-by-frame model of the cart, to check a layout without the game |
| [`CoasterTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/coaster/CoasterTest.java) | unit tests |
| [notes/mod-coaster.md](../../notes/mod-coaster.md) | lab notes: engine facts, what was verified in the game |
Options: **track** (Loop ~100 units, Grand circuit ~150 units), **laps** (1–3, default 2),
**speed** (slow / normal / fast), **coins** (on/off), TestLevel **chapter** (32–49, default 48).
See [manual chapter 18](../manual/18-mods.md#the-llama-coaster-ride) for players.
## 1. How the original ride works
Catacombs chapters 1, 3 and 5 (`t1l7m001`, `t1l7m003`, `t1l7m005`) are roller-coaster rides. The
ride music is track 25, `ROLLER` ([Music](../reference/audio.md#music)); the levels have the
longest views of the game (fog end up to 23 cells, [Engine facts](../reference/engine-limits.md#drawing)).
* **The cart is the player.** These levels have no `Llama` strat. The `RollerCoaster` strat calls
`isPlayer()` and draws `model:0` – the cart *with* Kuzco the llama and Pacha in it – so the llama
is not attached at run time, it is part of the cart model and its animations (lean, cheer, look
back). See [Objects](../reference/objects.md#special-objects).
* **The camera** is the ordinary `Camera` strat, spawned by the cart like the Llama spawns it. It
follows the global `g29`; in a corner the cart sets `g29` to the heading *after* the turn, so the
camera swings round first.
* **Track following**: the cart drives straight along its heading and turns by exactly 90° at a
waypoint. Its height comes from the floor under it. So a ride is a chain of straight legs on the
grid with 90° corners; slopes, jumps and gaps are pure geometry.
* **Floor materials** of the track bed trigger the effects: 5 boost arrows, 11 jump, 13 spin pad
(the cart hops and turns round, riding backwards to the next corner), 16 the end, 30 the
invisible death plane below (fall = restart).
* **Start and end**: the first waypoints are *station* nodes – the cart crawls out with letterbox
borders. On material 16, when the current waypoint is the last one, the strat waits 30 frames and
calls `endLevel` with its own parameters 1–3 (tribe, world, chapter) → the tally screen.
* Helpers: `RollerDoor` (gates), `FireworksTrigger` (rockets at its waypoint spots when the cart
passes), `CoinRollerCoaster` (coins hovering over the track), `WompyRollerCoaster`.
The river levels (`t1l3m001…4`) look similar but are not rail rides: their player
`KuscoAndPachaTogetherOnATree` floats on water, is pushed by current zones, steers freely between
the banks and leaves through a normal `LevelDoor`. That is why the mod uses the coaster.
### The cart script step by step
Excerpts of `RollerCoaster` as the editor's script view shows it (`v19` = speed, `v22` = boost
excess, `v18` = sideways offset, `v21` = 0 or 2048 when riding backwards):
```
call L_0200() // prepare the waypoints:
// self.wpX = int(self.wpX * 2 + 0.5) / 2 x/z rounded to halves
// setWaypointParam2((self.wpRotY + 512) & 3072) heading rounded to a quarter turn
spawn<4, 41, 2, 0x248000, 0>(0.9, 1.0, 0, 0, unit:Camera)
isPlayer()
self.y = self.floorHeight
showLevelStartInfo<1>()
loop {
call L_0074() // the end check (below)
call L_012c() // left/right input → v18 (at most half the speed)
moveRightQ(v18)
moveForwardQ(v19)
if (v22 > 2r) { v19 -= 2r; v22 -= 2r } // a boost wears off …
else if (v19 > 0.15) { v19 = max(v19 - 2r, 0.15) } // … down to cruise 0.15 (614 raw)
if (self.floorMaterial == 11) { … g22 = 163r } // jump: vertical speed
else if (self.floorMaterial == 5) { v19 += 20r; v22 += 20r } // boost arrows
else if (self.floorMaterial == 13) { … g22 = 61r; v20 = 128 } // spin: hop + half turn
if (self.y - 204r - v19 * 1228r < self.floorHeight && self.floorMaterial != -1) {
self.y = self.floorHeight // stay on the floor (a climb > ~0.1 per frame is lost)
} else { … fall … }
// corner: within 0.75 of the waypoint, steer round it and take the next one
v15 = (self.x - self.wpX) * v4 + (self.z - self.wpZ) * v5
if (v15 < 0.75 | (self.wpParam && v15 < 2.0)) { … g29 = self.wpRotY … }
…
}
L_0074: // the end
if (v13 > 1) { v13 -= 1; if (v13 <= 1) { endLevel(v1, v2, v3) } }
else if (self.floorMaterial == 16 && (self.wpEndFlags & 3r)) { v13 = 30 }
```
## 2. The plan
* **Base level = Catacombs 1 (`t1l7m001`)**, not the village: it already contains the cart, its
camera, coins, gates, fireworks, textures, sounds and the ride music. Importing them into the
village ([like the arena](arena.md#3-compiling-and-importing-arenaimport)) would only copy the
same things over. The mod keeps the base level's code and replaces geometry and objects.
* The **track** follows the cart's rules: straight legs along the axes, 90° corners, heights
changing only on straights.
* The **cart's waypoint chain** is generated from the same layout, so geometry and path cannot
disagree.
* The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)).
## 3. The track (`CoasterTrack`)
A layout is a start corner and a list of legs; each leg is a heading and a list of pieces
(kind, length in units, height change):
```java
new CoasterTrack("loop", new double[] {10, 10, 24}, List.of(
stationLeg(90, 4, 8, 4, 2), // run-in, END, brake, STATION, first drop
leg(0, p(TRACK, 2), p(TRACK, 8, 2), p(TRACK, 2)), // climb 2
leg(270, p(TRACK, 2), p(SPIN, 1), p(TRACK, 1), p(TRACK, 4, -2)),
leg(0, p(BOOST, 2), p(JUMP, 1), p(GAP, 2, -1), p(TRACK, 9)), // jump over a gap
leg(270, p(TRACK, 3), p(TRACK, 5, -2), p(TRACK, 2)),
leg(180, p(BOOST, 1), p(TRACK, 2), p(TRACK, 25, 7)))); // lift hill back up
```
The constructor lays the pieces out and refuses a layout that does not close, turns by anything
but 90° or lacks exactly one STATION and one END on leg 0. Each kind maps to a floor material
(TRACK 0, BOOST 5, JUMP 11, SPIN 13, END 16).
![The two layouts in the editor's top view: Loop and Grand circuit](../img/strats/coaster_tracks.jpg)
**Waypoints** (`nodes(laps)`): node 0 one unit behind the cart and node 1 at the end of the
station, both with parameter 1.0 (the crawl); then one node per corner, 0.5 before its centre with
the heading after the turn, repeated for every lap; and a last node 9 units past the END pad. So
the END pad only ends the ride in the last lap – in earlier laps the cart rolls over it.
## 4. The level (`CoasterLevel.build`)
The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java)
for the [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java):
* **Track bed**: per unit two 1 × 1 quads with the piece's texture and material (boost arrows,
jump stripes, spin target), an underside, and **rails** 0.35 high on both sides – their inner
faces are collision walls, so steering cannot leave the track. Gaps get end faces.
* **Corners**: flat 2 × 2 squares with rails on the two outer sides.
* **Pillars** every 5 units and under each corner, a **boarding platform** with posts left of the
station, a **ground plane** at height 0 with material 30 (the death plane).
* **Objects**:
```java
l.strat("RollerCoaster", st[0], st[1], st[2], st[3],
new int[] {0, tribe * FX, world * FX, chapter * FX}, // endLevel target = this chapter
LevelBuilder.Waypoints.of(wps), 0, false);
l.strat("RollerDoor", …); // gate at the station exit
l.strat("FireworksTrigger", …, Waypoints.of(fw), …); // six rocket spots around the END pad
l.strat("CoinRollerCoaster", …); // every 1.5 units on long straights
```
## 5. Speed and checking a layout (`CoasterSpeed`, `CoasterSim`)
The **Speed** option scales two constants of the cart: the cruise speed `pushShort 614` (six
places: the floor after a boost and the cap in corners) and the boost `pushShort 20` (two places).
`CoasterSpeed.find` locates them in the unit's listing (the instruction before and after must
match) and rewrites only those 16-bit operands.
A faster cart flies farther, and a jump that lands in a corner's turn zone misses the turn.
`CoasterSim` replays the cart's speed and jump rules frame by frame on a layout and reports jumps
that do not clear their gap or land too late, and climbs steeper than the floor probe allows.
The tests run it for every track × speed × laps.
## 6. The mod (`CoasterMod`)
`apply` refuses a chapter whose WAD another mod (or a level project) already changed, builds the
level on `t1l7m001`, sets the tally total to the number of coins (`LevelTally.counted`) and draws
a loading picture in Java (the title sits in the band the tally text leaves free).
![The tally after the ride](../img/strats/coaster_tally.jpg)
## 7. How it was tested
* **Unit tests** (`CoasterTest`): both layouts close, contain every feature and do not overlap;
bad layouts are refused; the waypoint chain follows the cart's rounding rules; every jump lands
for all speeds and laps; the speed patch changes exactly the 8 operands; the built level has one
RollerCoaster (and no Llama), the gate, the fireworks, the coins, the right exit target, the
floor materials 0/5/11/13/16/30 and the tally total; a chapter another mod wrote is refused.
* **In the game** (`tools/apptest.py`): Loop and Grand circuit at all three speeds, 1 and 2 laps –
full rides from the station to the tally with a screenshot every 1.5 s, no crash. After the tally
Enter restarts the ride; in the ride Esc → *Beenden und zur Auswahlkarte wechseln* goes back to
the world map.
## What to take from it
1. **Reuse a ride type with new data** when its rules fit: a new geometry plus a new waypoint chain
is a new ride, without writing strat code.
2. **Read the original strat first** – the 90° corners, the half-unit rounding and the 0.1 climb
limit all come from its script, and the layout code enforces them.
3. **Generate path and geometry from one description**, so they always match.
4. **Model the physics offline** (`CoasterSim`) to catch bad jumps before starting the game.
5. **Constants can be tuned in place**: find them in the listing by their neighbours and patch the
operand, instead of recompiling the strat.
+87
View File
@@ -0,0 +1,87 @@
# The script language
LlamaMod's **script language** (version 2) is a small structured language in the style of the
Script view. [`StratCompiler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratCompiler.java)
compiles it to strat assembly, and
[`StratAssembler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratAssembler.java)
turns that into bytecode with the same checks as hand-written assembly. It is used by the editor's
type editor (*Language: Script*) and by mods such as the [arena](arena.md). Complete reference:
[notes/strat-language.md](../../notes/strat-language.md#script-language-stratcompiler--writing-new-behaviour);
tests: `StratCompilerTest`.
## A first strat
```
// bobs one cell up and down and turns
strat Bobber {
alias base = v0
alias phase = v1
setModel(model:5) // any opcode: name<inline operands>(stack arguments)
base = self.y + 1 // 1 = one cell
phase = 0
on touched { // event handler (type 4), runs as a subroutine
soundPlay(sound:6)
}
loop {
phase = phase + 4deg
self.y = base + sin(phase)
turnLeft(4deg)
yield // end of this frame
}
}
sub wobble { // called with "call wobble", ends with return
self.rotZ = sin(self.rotY) * 0.1
}
```
## Building blocks
| Construct | Notes |
|---|---|
| `strat Name { … }` | a type; its label `Name` is the entry point |
| `sub name { … }`, `call name`, `return` | subroutines |
| `alias name = v3` | a name for a variable; parameter names from the object schema are predefined |
| `x = e`, `x += e`, `-=`, `*=`, `/=` | assignment; `x += 1` compiles to `inc` |
| places | `vN`, alias, `gN`, `self.prop`, `player.prop`, `parent.prop`, `parent.vN`, `camera.` / `target.` / `boss.` / `dialog.prop` |
| `if (c) { } else if (c) { } else { }` | |
| `while (c) { }`, `loop { }`, `do { } while (c)` | a `yield` as the last statement of a loop becomes the loop's frame-ending back jump |
| `switch (e) { case 1: … break default: … }` | C rules (fall through without `break`); case values are constants |
| `break`, `continue`, `yield`, `end`, `remove` | |
| `on touched { … }`, `on event(type[, param]) [slot n] { … }` | event handlers; touched needs `collisionOn<…>()` and `collRadius(…)` |
| `op<a, b>(e1, e2)` | **any opcode** by name: inline operands in `<>` (raw words), stack arguments in `()`; the count must match the opcode |
### Numbers
* `10` and `10.0` are ten cells (fixed point); `1.5`, `90deg` as in assembly.
* **Raw** integers: `215r` (= 215/4096) or hex `0x10`. Values inside `<…>` are always raw.
* Operators over constants are folded at compile time with the VM's own fixed-point rules:
`2 * 3 + 0.5` → `6.5`.
* `use rawNumbers` switches to the version-1 rule (plain integers raw) – old scripts get it
automatically.
### Operators (low → high precedence)
`||` · `&&` · `|` · `&` · `==` `!=` · `<` `>` `<=` `>=` · `<<` `>>` · `+` `-` · `*` `/` · unary
`-` `!`. In conditions `&&` / `||` short-circuit; in a value both sides are evaluated.
### References
`model:N`, `anim:N`, `unit:N[+off]` or `unit:Name` (another custom type), `sound:N`, `tex:N` –
indices into the level the code is compiled against.
## What the compiler does not do (yet)
* no `for` loop, no typed variables (everything is a 32-bit word, fixed point by convention);
* `spawn` takes raw type fields (`spawn<0, 16, 5, 0xC400, 3>(unit:…)`) – you must know the
values the spawned type needs;
* the Script view does not show event handlers as `on … { }` blocks.
## Tips
* Look at an original type that does something similar first (Script panel) and copy its numbers
– speeds, animation indices, sounds and type fields are level-specific.
* Keep each frame's work small and always reach a `yield` in loops; a loop without a yield freezes
the game.
* Test in the game early: the compiler checks the code's form and references, not whether the
object behaves well.
+135
View File
@@ -0,0 +1,135 @@
# Tools
Everything in this repository that you can run, what it is for and where its code and notes are.
None of the tools contain game data; they all work on your own copy of the game.
## LlamaMod (Java app)
The main product: a mod manager with a launcher and a 3D level editor, for Windows and Linux.
Java 25 + Swing, sources in [`src/main/java/com/eliasmueller/llamamod/`](../src/main/java/com/eliasmueller/llamamod/).
![The LlamaMod launcher](img/launcher.jpg)
| Part | What it does | Docs |
|---|---|---|
| launcher | first-start wizard (copies the game from CD/ISO), mod list with options, save-game editor, starts the game (Wine on Linux) | [manual: Installing](manual/02-installing.md), [manual: Mods](manual/18-mods.md), [notes/llamamod-app.md](../notes/llamamod-app.md) |
| level editor | 3D editor for new and original levels: brushes, tiles, meshes, textures, objects, waypoints, cameras, zones, lights, visibility, strat scripts, play-test, glTF export | [editor manual](manual/index.md) |
| mods | Test World, Alligator Arena, level projects, Boot to level, Intro skip, Mirror mode, Practice tools, Randomizer, Draw distance, Built-in cheats, Debug wireframe, Free camera | [manual: LlamaMod mods](manual/18-mods.md), `notes/mod-*.md` |
### Command line
```text
llamamod start the GUI
llamamod --version show the version and build id
llamamod --install <iso|folder> set up the game data
llamamod --mods list available mods and their options
llamamod --apply [id,id,...] [--opt id.key=value ...]
apply mods (none = original); options are saved
llamamod --launch [--wait] start the game
llamamod --registry show the registry values
llamamod --saves show save games
llamamod --unlock <slot 1-3> <world> <chapter> [--cursor]
llamamod --editor [project.llamalevel | t1l1m001] level editor
llamamod --editor-build <project.llamalevel> <out.wad> [--cd <dir>]
llamamod --editor-import <level.wad|t1l1m001> <project.llamalevel> [--cd <dir>]
llamamod --editor-new <project.llamalevel> [base level] [--name <name>] [--cd <dir>]
llamamod --import-level <project.llamalevel> (into the level folder, shown as a mod)
llamamod --remove-level <id|file> move a level mod's file to backups/levels/
```
Data folder: `-Dllamamod.data=<folder>` or `LLAMAMOD_DATA` (default `~/.local/share/LlamaMod`,
Windows `%LOCALAPPDATA%\LlamaMod`). Source: [`Main.java`](../src/main/java/com/eliasmueller/llamamod/Main.java).
### Debug view
The *Debug wireframe* mod
([`mods/debug/`](../src/main/java/com/eliasmueller/llamamod/mods/debug/)) adds visible geometry to
every level: collision as coloured frames (invisible walls filled red, the exit floor violet, death
planes magenta) and markers for invisible objects (start, exit, checkpoints, cameras, triggers,
zones). Gameplay is unchanged. Its Python ancestor is `tools/debugviz.py`; colour legend and
details: [notes/debugviz.md](../notes/debugviz.md).
![Debug view in Village 3](img/debug-view.jpg)
## Python kit (`tools/`)
The reverse-engineering toolkit the project started with. Python 3.8+ with `numpy` and `Pillow`
(`capstone` for `disx86.py`). All parsers rebuild the original files byte for byte.
| Tool | Purpose |
|---|---|
| [`wadtool.py`](../tools/wadtool.py) | list, extract and replace WAD chunks |
| [`geomlib.py`](../tools/geomlib.py) | library: WAD, TRAK, STPC models, MAP tile part, TEXT, collision |
| [`geom_export.py`](../tools/geom_export.py) | level → OBJ/MTL with textures and vertex colours, `--collision` (collision.obj by material), `--objects`, preview renders |
| [`geom_import.py`](../tools/geom_import.py) | OBJ → new world geometry in a copy of a level |
| [`geom_demo.py`](../tools/geom_demo.py) | example geometry for `geom_import.py` |
| [`mapchunk.py`](../tools/mapchunk.py) | MAP parser/writer |
| [`map_dump.py`](../tools/map_dump.py) | print a level's tiles, strats, lights |
| [`strats.py`](../tools/strats.py) | object types of a level: names, instance counts, code offsets, type fields |
| [`strat_table.py`](../tools/strat_table.py) | type × level table → [notes/strat_types_table.md](../notes/strat_types_table.md) |
| [`stpcwalk.py`](../tools/stpcwalk.py) | rough walk through STPC (models, animations, code) |
| [`textchunk.py`](../tools/textchunk.py), [`text_export.py`](../tools/text_export.py) | TEXT chunk; export pages to PNG and import them back |
| [`texsheet.py`](../tools/texsheet.py) | contact sheet of a level's textures with indices |
| [`levelbuild.py`](../tools/levelbuild.py) | level builder from a Python script ([guide](guide/levels-in-code.md)) |
| [`make_testlevel.py`](../tools/make_testlevel.py) | early experiment: one object per type on a floor plate |
| [`debugviz.py`](../tools/debugviz.py), [`debugviz_check.py`](../tools/debugviz_check.py) | debug-view WADs for all levels and an offline check |
| [`cor2png.py`](../tools/cor2png.py), [`png2cor.py`](../tools/png2cor.py) | loading pictures (COR) ↔ PNG |
| [`asf2wav.py`](../tools/asf2wav.py) | music (ASF) → WAV, exact decoder |
| [`disx86.py`](../tools/disx86.py) | disassemble `groove.exe` at a virtual address |
| [`vorschau/`](../tools/vorschau/) | Blender (Eevee) preview renders of a level with labels |
| [`lama-mod`](../tools/lama-mod) | shell script: CD overlay of symlinks for mods in a hand-made Wine setup (pre-LlamaMod) |
Level scripts in [`levels/`](../levels/): `testwelt.py` (the original test world),
`sprunglabor.py` (jump lab used to measure the llama), `testwelt_parkour.seq` (scripted run).
### Paths
The tools find the original game files through these environment variables
([`tools/lamapaths.py`](../tools/lamapaths.py)); the defaults match a hand-made Wine setup.
Command-line options (`--base`, `--src`, `--root`, `--iso`) win over them.
| Variable | Used by | Default |
|---|---|---|
| `LLAMAMOD_CD` | `levelbuild.py`, `debugviz.py`, `debugviz_check.py`, Gradle tests (`-Dllamamod.cd`) | `$LAMA_WINEPREFIX/iso-dk/Setup/Eng/Cd` – the CD folder (`wads/`, `music/`, `*.cor`) |
| `LAMA_WINEPREFIX` | `lamatest.py`, `levelbuild.py --install` | `~/.wine-lama` |
| `LAMA_MOD` | `lamatest.py` | `~/.local/bin/lama-mod`, else `tools/lama-mod` |
| `APPTEST_ROOT` | `apptest.py` (session folders) | `$CLAUDE_JOB_DIR/tmp` if set, else `~/.cache/llamamod-apptest` |
| `APPTEST_ISO` | `apptest.py` (first install) | `~/Downloads/GROOVE_DK.iso` |
| `LAMATEST_DIR` | `lamatest.py` (state, logs) | `/tmp/lamatest-$USER` |
A missing folder stops the tool with a message naming the variable to set.
## 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. Session folders and the ISO: `APPTEST_ROOT`, `APPTEST_ISO` (see
[Paths](#paths)).
```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 |
+65
View File
@@ -0,0 +1,65 @@
# 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-coaster.md](mod-coaster.md) | Llama Coaster (roller-coaster ride; how the original rides work) |
| [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`).
+34
View File
@@ -149,6 +149,40 @@ CutSceneTrigger camera moves only as points).
(≤ 2000 extra vertices per tile model, on average ~1–2× as many triangles as the original).
* Test level T1L9 (test world) has no debug version (the testlevel mod takes precedence).
## Allowance areas (LlamaMod option `debug-wireframe.allowance`, 2026-11-08)
Choice `off` (default) / `tips` ("Floating spots") / `all` ("All edges"). Shows the floor test's edge
allowance: the engine accepts a floor polygon up to `tol = −0x40 − ((0x1000 − ny·32) >> 6)` (≈0.016–0.023)
outside each edge plane; at sharp corners this becomes a sliver up to tol / sin(θ/2) long
(geometry.md "Floor test: inclusion rule …"). Colour **pink 255,60,200**.
* Code: `mods/debug/FloorTest` = bit-exact port of FUN_004036d0's floor branch (same as `tools/floortest.py`;
the sine table is `rint(sin(2πi/4096)·4096)`, identical to DAT_00574318), `mods/debug/AllowanceAreas`.
All geometry uses the engine's plane scale (n·p = d/32 with |n| = 127), not `Collision.decode`'s d/4096,
so the bands sit ~0.8 % further from the model origin than the green wireframe.
* **tips**: for every placed tile and every up-facing collision polygon (not material 17 = skipped for the
llama, not 30 = no floor) and every corner whose tolerance corner lies ≥ `MIN_REACH` (0.05) beyond the
true corner, the area beyond the corner (tolerance region ∩ half-plane past the corner) is sampled in up to
40 rows × 7 points. A sample counts when the exact engine test, with the feet on that polygon's plane,
picks this polygon, the point is outside the polygon without tolerance, and no other passing floor of the
cell is less than `MIN_GAP` (0.05) below (material-30 floors count as none). Samples outside the tile's
cell are dropped (the engine only tests the cell's tiles). Kept when the winning samples reach ≥ 0.05
outside the polygon: translucent strip (alpha 150) through the winning part of each row + an opaque
centre line (4 mm), at the plane height + 0.012, in the tile's marker cell model (no collision).
Strat object collision (crates as strats, platforms) is not considered.
* **all**: per TRAK model, every up-facing floor polygon (same material filter) gets an opaque ring from
its true corners to its tolerance corners (2n vertices, 2n triangles), appended to the model overlay like
the wireframe (tile transform, alt models free). Tolerance corners further than 1.5 out are clamped.
* Budget: if TEXT+TRAK+MAP+STPC+LGPC would exceed 12 MB (16 MB level arena) the level is rebuilt without
allowance geometry and the apply log says "allowance areas left out: level too large". Not reached in
any original level; a model whose overlay would pass 7900 vertices keeps its wireframe without bands.
* Numbers (all 33 levels): tips 0–1454 slivers per level (river t1l3m00x most, t1l4m002 394),
WAD +0–2.0 MB, ≤ 2 s per level; all: +0.02–2.9 MB, arena max 11.1 MB (t1l3m003).
t1l4m002 crate (tile 443): poly 15 corner 2 (reach 0.18, contains Kuzco's spot 57.204/43.733),
poly 11 corner 2 (0.175), polys 10/14 (0.11/0.13) on the other end, 12/13 barely (0.05).
* Tests: `AllowanceAreasTest` (exact port vs. 44 floortest.py points, sine table vs. groove.exe,
slivers of t1l4m002, no slivers on the flat start ground, band counts, process in each mode).
## Statistics per level (build of 2026-11-06)
Marker categories are the internal keys of `debugviz.py`: ausgang = exit, geheimnis = secret,
+119
View File
@@ -0,0 +1,119 @@
# Browser feasibility: Boxedwine (2026-11-07)
Question: can *The Emperor's New Groove* (groove.exe, D3D7/DirectDraw, DirectInput, DirectSound) run in a
web browser through [Boxedwine](https://github.com/danoon2/Boxedwine) (Wine + x86 emulator, also built to WebAssembly)?
## Verdict
**Not with today's Boxedwine web build: Direct3D is missing in the browser.**
- **Web (Emscripten) build 26R1:** the game starts and shows Wine dialogs, then stops with the game's own
message *"Direct Draw kann nicht gestartet werden."* ("Direct Draw cannot be started"). The web build has no
OpenGL bridge: the Emscripten makefile defines none of the `BOXEDWINE_OPENGL_*` switches, the stripped file
system has no `libGL.so.1` (Wine logs `Failed to load libGL … OpenGL support is disabled`), and the release
readme says *"OpenGL and Direct3D are not supported in the Web/Emscripten build because they can not currently be
converted to WebGL."* Without OpenGL, wined3d has no D3D device, so groove.exe finds no 3D device.
- **Native Linux build (same emulator, x86 JIT), Wine 11 file system:** **works.** After three small Boxedwine
fixes (see below) it reaches the intro, the main menu, the save slot screen, the map and in-game
Village chapter 1 at ~30 fps (llvmpipe on Xvfb, load average ~15). Keyboard input works. One visible problem:
in-game the camera seems to sit at Kuzco's position (the llama is not visible, the view pitches up/down
with Up/Down); in the attract-mode demo Kuzco is drawn normally. Not investigated.
With the Wine 6 file system it hangs on the loading screen (emulator livelock, see below).
- **Software renderer (plan B) does not help:** no own rasterizer found in groove.exe. `FUN_0040ffd0`
chooses a DirectDraw device + Direct3D device by name (`DisplayDevice`/`D3DDevice`). The "software" flag
(`SoftwareScreenBPP/W/H`, device record +0x10) is derived from the enumerated **D3D** device description
(`FUN_00412a10`: `(desc+0xf4 >> 3) & 1`), most likely D3D's own software device (RGB/MMX emulation) – in any case a device the game gets from Direct3D enumeration, not an own rasterizer (no other draw path was found by grep). In Wine that
device is wined3d too, which needs OpenGL (or Vulkan). Wine has no GL-free D3D rasterizer, so the software
path also needs a GL bridge in the browser.
- **Path in Boxedwine:** yes, D3D7 → Wine `ddraw` → `wined3d` → OpenGL. Natively, Boxedwine passes the
emulated GL calls to the host GL through SDL (`source/opengl/sdl`). In the browser that bridge would have
to become WebGL (GL ES 2/3), which Boxedwine does not do. The source has an unused ES path
(`source/opengl/es`: glshim/esopengl) that is not part of the Emscripten build.
### What it would take to ship
1. A GL → WebGL bridge for the Emscripten build (upstream work in Boxedwine: wined3d needs desktop GL 2.1+
features; a translation layer like gl4es/glshim or a WebGL2 backend for the marshalled GL calls). This is
the blocker, weeks of emulator work at least, not something LlamaMod can patch around.
2. Speed: the web build has no JIT (wasm interpreter, `TOTAL_MEMORY` 512 MB). Native JIT gives ~30 fps with
1.2 cores; the interpreter is typically several times slower, so even with WebGL the 3D game would likely
be far below playable on average hardware. Unmeasured.
3. Distribution page: a static page where the user drops their own ISO/folder; JS builds the app zip in
memory (game folder + CD data, ~400 MB without movies) plus a root zip with the registry values, never
uploaded anywhere. Never host game data. Needs COOP/COEP headers for the multi-threaded build.
4. Sound and movies: DirectSound opened 17 SDL audio streams natively (silent dummy driver here, audibility
not checked). Intro movies (`movies/*.dat`) do not play in Wine 11 under Boxedwine (quartz: `Failed to
create filter … hr 0x8007000e`); the game skips them.
Alternatives that do not need a GL bridge are out of scope here (e.g. a native port, or a WebGL renderer
re-implementation of the engine's D3D7 calls).
## Versions
| Item | Version / source |
|---|---|
| Boxedwine | 26R1.0 (2026-04-22). GitHub releases have Windows, Mac and Web builds, **no Linux build** → built from the source tarball |
| Linux build | GCC 11.4 (Mint 21.3). Official requirement is GCC 12; one fix needed (below). `libminizip-dev` missing → linked against the installed `libminizip.so.1` via a local symlink (no system packages installed) |
| File systems | from Boxedwine's own list `http://www.boxedwine.org/v2/26R1/filesV2.xml`: `TinyCore15Wine6.0.zip`, `TinyCore15Wine11.0.zip`; web: `boxedwine.zip` (stripped Wine 6) from `Boxedwine26R1Web.zip` |
| Browser | Chromium (`/usr/bin/chromium`, own profile) on Xvfb :26, page served by a local Python server with COOP/COEP headers on 127.0.0.1, MultiThreaded web build |
| Game | German CD via `tools/apptest.py setup boxedwine`; LlamaMod's patched `game/groove.exe` (HKCU registry) |
## Procedure (native)
```
Xvfb :26 -screen 0 1920x1080x24 -nolisten tcp +extension GLX
# prefix: copy user.reg out of the file-system zip into <root>/home/username/.wine/, append
# [Software\\Disney Interactive\\Emperors New Groove\\1.0] CDPath="Z:\\mnt\\lama\\cd-active\\" HDPath=".\\"
# ScreenW=dword:280 ScreenH=dword:1e0
# and write "disable" into .update-timestamp (otherwise wineboot updates the prefix: >15 min and it hung)
SDL_AUDIODRIVER=dummy DISPLAY=:26 boxedwine -root <root> -zip TinyCore15Wine11.0.zip \
-env WINEDLLOVERRIDES=mscoree,mshtml= -mount <apptest>/data /mnt/lama -w /mnt/lama/game /bin/wine groove.exe
```
Keys via `xdotool keydown/keyup` with the pointer over the window (X focus follows the pointer, no WM).
The game renders 640x480 into the top-left of the 800x600 Boxedwine window.
Boxedwine fixes needed (patch: `$CLAUDE_JOB_DIR/tmp/boxedwine/results/boxedwine-26R1-linux-xvfb.patch`, not upstreamed):
1. `readIcons.cpp`: `std::make_shared<U8[]>` → `std::shared_ptr<U8[]>(new U8[n]())` (GCC 11).
2. `platform/linux/platformOpenGL.cpp`: skip GLX fbconfigs with `GLX_SLOW_CONFIG` caveat and without
`GLX_WINDOW_BIT`. Under Xvfb/llvmpipe, Wine otherwise picks an accum-buffer config (SDL refuses it as
"accelerated") or a non-window config (`set_pixel_format … not compatible for window rendering`) and wined3d
reports `Failed to get a GL context`. Probably Xvfb-specific; real GPUs may not need it.
3. `KMemory::mapNativeMemory`: when no free pages are left above 0xD0000000, reserve in the user area
instead. Without it the first `glMapBufferRange` of wined3d (Wine 6) fails → `Failed to map bo` → page fault.
Not checked whether Wine 11 still needs it.
Wine 6 file system with these fixes: loading screen drawn through D3D7, ~10–25 presents/s for ~30 s, then
every emulated thread waits for the JIT memory mutex (`JitCodeGen::doJIT`) while one thread spins — the game
never leaves the loading screen. Not investigated further; Wine 11 does not show it.
## Procedure (web)
Web page = release `MultiThreaded/` folder plus `lamaroot.zip` (= `boxedwine.zip` with the user.reg above, CDPath
`C:\\files\\lama\\cd\\`) and `lama.zip` (`lama/` = game folder + `lama/cd/` = CD data without movies, stored,
415 MB). URL: `boxedwine.html?root=lamaroot&app=lama&p=groove.exe&sound=false`.
The `overlay=` parameter did **not** override `user.reg` of the root zip (game asked for the CD), a modified
root zip did. Loading 415 MB into the page took a few seconds locally.
## Screenshots
- `../img/dev/boxedwine-native-menu.jpg` – main menu (native Boxedwine, Wine 11)
- `../img/dev/boxedwine-native-demo.jpg` – attract-mode demo in 3D (the right/bottom strips are a stale frame
outside the 640x480 game area)
- `../img/dev/boxedwine-native-village.jpg` – Village chapter 1 in game (Kuzco not visible, see above)
- `../img/dev/boxedwine-web-ddraw-error.jpg` – web build in Chromium: "Direct Draw kann nicht gestartet werden."
## Running it locally
A durable native setup lives in `~/opt/boxedwine-26R1/` (rebuilt 2026-11-07 from the procedure above):
| Path | Content |
|---|---|
| `bin/boxedwine` | Boxedwine 26R1.0 built from `dl-src/` (GitHub tag tarball) with `boxedwine-26R1-linux-xvfb.patch`, GCC 11.4. The sources are CRLF: convert the five patched files to LF (`sed -i 's/\r$//'`) before `patch -p1`; `project/linux/linux_build/lib/libminizip.so` → system `libminizip.so.1` |
| `dl-fs/TinyCore15Wine11.0.zip` | Wine 11 file system (+ `filesV2.xml`) |
| `root/home/username/.wine/` | `user.reg` from the zip + the game's HKCU key, `.update-timestamp` = `disable` |
| `apptest/lama/data/` | `APPTEST_ROOT=~/opt/boxedwine-26R1/apptest tools/apptest.py setup lama`: `game/` (patched groove.exe), `cd-active/`; mounted as `/mnt/lama` |
Start: `lama-boxedwine` (`~/.local/bin`, bash) on the current `$DISPLAY`, normal sound
(`SDL_AUDIODRIVER=dummy lama-boxedwine` for silence). Log: `~/opt/boxedwine-26R1/last-run.log`;
PID file `~/opt/boxedwine-26R1/boxedwine.pid` blocks a second instance. Known issues: in-game camera at
Kuzco's position, no intro movies (see above).
**Real screen (AMD RX 480, Mesa radeonsi):** the window stayed black and the game hung after the movie attempts; with `LIBGL_ALWAYS_SOFTWARE=1` (llvmpipe) it shows the title and runs. `lama-boxedwine` therefore defaults to the software renderer (`LAMA_BW_GPU=1` to try the GPU).
File renamed without changes.
@@ -60,7 +60,7 @@ objdump, replay, restore).
After rebasing onto the merged main tree (simp-all + launcher UX changes): orig, all-v, boot-exe, mrf-r
re-run with the final jar: all OK (CDPath `L:\cd-active\`).
![matrix](img/launcher-matrix.jpg)
![matrix](../img/launcher-matrix.jpg)
Screens: all mods in Village (wireframe, mirrored) and the free camera; River with mirror+randomizer+fog;
Test World with all mods; boot-to-level + all exe mods (River loading card); practice restore.
+44
View File
@@ -0,0 +1,44 @@
package mtn3;
import com.eliasmueller.llamamod.exe.ExeImage;
import com.eliasmueller.llamamod.mods.Mod;
import com.eliasmueller.llamamod.mods.ModContext;
import com.eliasmueller.llamamod.mods.ModOption;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/** Throwaway diagnostics: stale parent-variable access log + local heap consistency check. */
public final class Mtn3DiagMod implements Mod {
static final Path DIR = Path.of("/home/elias/.claude/jobs/90c6eed7/tmp/m3/diag");
public String id() { return "mtn3-diag"; }
public String name() { return "Mountain 3 diagnostics"; }
public String description() { return "Logs stale parent writes and checks the local heap (test only)."; }
public boolean modifiesExecutable() { return true; }
public List<ModOption> options() {
return List.of(ModOption.bool("fix", "Fix", "Redirect stale parent writes to a dummy", false),
ModOption.string("rev", "Rev", "cache buster", "1"));
}
public void apply(ModContext ctx) throws IOException {
ExeImage exe = ctx.exe();
long data = exe.allocateData(8192, "mtn3 diag data");
long cave = exe.nextAllocation();
String fix = ctx.optionBool("fix") ? "1" : "0";
Path bin = DIR.resolve(String.format("cave_%s_%x_%x.bin", fix, cave, data));
Files.writeString(DIR.resolve("addresses.txt"), String.format("CAVE=0x%x DATA=0x%x FIX=%s%n", cave, data, fix));
if (!Files.exists(bin)) {
throw new IOException("assemble " + bin);
}
long at = exe.place(Files.readAllBytes(bin), "mtn3 diag code");
if (at != cave) {
throw new IOException("cave moved " + Long.toHexString(at));
}
exe.hookJmp(0x553770, "8B 44 24 04 8B", cave, "pushParentVarAddr");
exe.hookJmp(0x553a40, "56 8B 74 24 08", cave + 5, "popParentVar");
exe.hookJmp(0x41ada0, "8B 44 24 04 56", cave + 10, "FreeLocalMem");
exe.hookJmp(0x41ae20, "8B 0D 9C 49 58 00", cave + 15, "AllocLocalMem");
}
}
+285
View File
@@ -0,0 +1,285 @@
; Mountain 3 crash diagnostics (German groove.exe). nasm -f bin -DCAVE=.. -DDATA=.. -DFIX=0/1
bits 32
org CAVE
%define D(o) dword [DATA+o]
; DATA layout: 0 magic, 16 cnt_parent_ok, 20 cnt_stale_addr, 24 cnt_stale_pop, 28 cnt_free, 32 cnt_anom,
; 36 cnt_alloc, 40 stale ring idx, 44 all ring idx, 48 dummy, 52 last free ptr, 56 last alloc size, 60 tmp idx,
; 64 stale ring 32x32, 1088 anomaly record 64 bytes, 1152 dump 256 bytes (hdr-128..+128), 1408 all-ring 64x32 (2048)
; 3456 free history 64 x 16
entry_addr: jmp near push_addr
entry_pop: jmp near pop_var
entry_free: jmp near free_hook
entry_alloc: jmp near alloc_hook
push_addr:
mov eax,[esp+4]
mov ecx,[eax+0xc]
mov edx,[esp+8]
mov [DATA+60],edx
mov edx,0x11
call record_all
test ecx,ecx
jz .stale
cmp dword [ecx+0xec],0
jz .stale
inc D(16)
.ok:
mov edx,[ecx+0xf4]
mov ecx,[esp+8]
lea edx,[edx+ecx*4]
.push:
push edx
push eax
call 0x54bbd0
add esp,8
ret
.stale:
inc D(20)
mov edx,1
call record
call stale_info
call check_heap
%if FIX
mov edx,DATA+48
jmp .push
%else
test ecx,ecx
jnz .ok
mov edx,DATA+48
jmp .push
%endif
pop_var:
push esi
mov esi,[esp+8]
push esi
call 0x54bc00
add esp,4
mov ecx,[esi+0xc]
mov edx,[esp+0xc]
mov [DATA+60],edx
push eax
mov eax,esi
mov edx,0x12
call record_all
pop eax
test ecx,ecx
jz .stale
cmp dword [ecx+0xec],0
jz .stale
inc D(16)
.ok:
mov edx,[ecx+0xf4]
mov ecx,[esp+0xc]
lea edx,[edx+ecx*4]
.store:
pop esi
mov [edx],eax
ret
.stale:
inc D(24)
push eax
mov eax,esi
mov edx,2
call record
call stale_info
call check_heap
pop eax
%if FIX
mov edx,DATA+48
jmp .store
%else
test ecx,ecx
jnz .ok
mov edx,DATA+48
jmp .store
%endif
; record: eax strat, ecx parent, edx kind, [DATA+60] idx. Preserves eax, ecx.
record:
push esi
push edi
push ebx
mov esi,[DATA+40]
and esi,31
shl esi,5
lea edi,[DATA+64+esi]
jmp rec_common
record_all:
push esi
push edi
push ebx
mov esi,[DATA+44]
and esi,63
shl esi,5
lea edi,[DATA+1408+esi]
inc D(44)
dec D(40) ; compensated below
rec_common:
inc D(40)
mov [edi],eax
mov [edi+4],ecx
mov [edi+8],edx
mov ebx,[DATA+60]
mov [edi+12],ebx
mov ebx,[eax+0x18]
mov [edi+16],ebx
mov dword [edi+20],0
mov dword [edi+24],0
mov dword [edi+28],0
test ecx,ecx
jz .np
mov ebx,[ecx+0x18]
mov [edi+20],ebx
mov ebx,[ecx+0xf4]
mov [edi+24],ebx
mov ebx,[ecx+0xec]
mov [edi+28],ebx
.np:
pop ebx
pop edi
pop esi
ret
free_hook:
pushad
inc D(28)
mov eax,[esp+36]
mov [DATA+52],eax
mov esi,[DATA+28]
and esi,63
shl esi,4
mov [DATA+3456+esi],eax
mov ebx,[esp+32] ; caller
mov [DATA+3456+esi+4],ebx
mov ebx,[eax-4] ; size|used
mov [DATA+3456+esi+8],ebx
mov ebx,[eax-12] ; next
mov [DATA+3456+esi+12],ebx
call check_heap
popad
mov eax,[esp+4]
push esi
jmp 0x41ada5
alloc_hook:
pushad
inc D(36)
mov eax,[esp+36]
mov [DATA+56],eax
call check_heap
popad
mov ecx,[0x58499c]
jmp 0x41ae26
; walks the local heap; on the first inconsistency records it (code in anomaly+0)
check_heap:
pushad
mov esi,[0x58499c]
test esi,esi
jz .done
xor edi,edi
mov ecx,70000
.loop:
mov edx,1
cmp [esi+4],edi
jne .bad
mov edx,2
movzx eax,word [esi+10]
cmp eax,1
ja .bad
mov ebx,[esi]
test ebx,ebx
jz .done
mov edx,3
movzx eax,word [esi+8]
lea eax,[esi+eax*4+12]
cmp ebx,eax
jne .bad
mov edi,esi
mov esi,ebx
dec ecx
jnz .loop
.done:
popad
ret
.bad:
inc D(32)
cmp dword [DATA+32],1
jne .done
mov [DATA+1088],edx
mov [DATA+1092],esi
mov [DATA+1096],edi
mov eax,[esi]
mov [DATA+1100],eax
mov eax,[esi+4]
mov [DATA+1104],eax
mov eax,[esi+8]
mov [DATA+1108],eax
mov eax,[DATA+28]
mov [DATA+1112],eax
mov eax,[DATA+36]
mov [DATA+1116],eax
mov eax,[DATA+40]
mov [DATA+1120],eax
mov eax,[esp+32] ; return address of check_heap caller
mov [DATA+1124],eax
mov eax,[DATA+52]
mov [DATA+1128],eax
mov eax,[DATA+56]
mov [DATA+1132],eax
; dump 256 bytes from edi (previous header) if set, else esi-128
test edi,edi
jnz .src
lea edi,[esi-128]
.src:
mov esi,edi
lea edi,[DATA+1152]
mov ecx,64
rep movsd
jmp .done
; stale_info: ecx parent, [DATA+60] idx -> DATA+4480 + (n&31)*16: target, block hdr, size|used, value before
stale_info:
pushad
test ecx,ecx
jz .out
mov edi,[ecx+0xf4]
mov eax,[DATA+60]
lea edi,[edi+eax*4]
mov ebx,[DATA+40]
dec ebx
and ebx,31
shl ebx,4
lea ebx,[DATA+4480+ebx]
mov [ebx],edi
mov eax,[edi]
mov [ebx+12],eax
mov dword [ebx+4],0
mov dword [ebx+8],0
mov esi,[0x58499c]
mov ecx,70000
.l:
test esi,esi
jz .out
mov edx,[esi]
test edx,edx
jnz .h
movzx eax,word [esi+8]
lea edx,[esi+eax*4+12]
.h:
cmp edi,esi
jb .next
cmp edi,edx
jae .next
mov [ebx+4],esi
mov eax,[esi+8]
mov [ebx+8],eax
jmp .out
.next:
mov esi,[esi]
dec ecx
jnz .l
.out:
popad
ret
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""rd.py SESSION : attach briefly with winedbg, dump diag data, print summary"""
import os, re, subprocess, sys, struct
S = sys.argv[1]; DATA = 0x70f010; N = 4992
env = dict(os.environ, WINEPREFIX='/home/elias/.claude/jobs/90c6eed7/tmp/%s/data/wine' % S, WINEDEBUG='-all')
def dbg(cmds):
out = subprocess.run(['winedbg'], input='\n'.join(cmds) + '\ndetach\nquit\n', env=env, capture_output=True, text=True, timeout=60).stdout
return out
pl = dbg(['info proc'])
m = re.search(r'([0-9a-f]{8})\s+\d+\s+\\_ \'groove.exe\'', pl)
if not m: print('no groove.exe'); sys.exit(1)
pid = m.group(1)
out = dbg(['attach 0x' + pid, 'x/%dx 0x%x' % (N // 4, DATA), 'x/16x 0x58499c'])
words = {}
for l in out.splitlines():
mm = re.match(r'.*?0x([0-9a-f]{8}):\s+((?:[0-9a-f]{8}\s*)+)$', l)
if mm:
a = int(mm.group(1), 16)
for i, w in enumerate(mm.group(2).split()): words[a + 4 * i] = int(w, 16)
def W(o): return words.get(DATA + o, 0)
names = set()
for base, cnt in ((64, 32), (1408, 64)):
for i in range(cnt):
e = DATA + base + 32 * i
for k in (16, 20):
if words.get(e + k): names.add(words[e + k])
nm = {}
if names:
o2 = dbg(['attach 0x' + pid] + ['x/s 0x%x' % a for a in sorted(names)])
ss = [l for l in o2.splitlines() if l.startswith('Wine-dbg>') and '"' not in l or True]
segs = o2.split('Wine-dbg>')
for i, a in enumerate(sorted(names)):
nm[a] = segs[2 + i].strip() if 2 + i < len(segs) else hex(a)
print('parent_ok=%d stale_addr=%d stale_pop=%d free=%d alloc=%d anomalies=%d staleidx=%d allidx=%d' % (W(16), W(20), W(24), W(28), W(36), W(32), W(40), W(44)))
def ring(base, cnt, idx, label, last=12):
print('--', label)
n = min(idx, cnt)
for j in range(max(0, idx - last), idx):
e = base + 32 * (j % cnt)
s, p, k, vi, sn, pn, pv, pec = (W(e + 4 * q) for q in range(8))
print(' #%d kind=%x strat=%x(%s) parent=%x(%s) v%d parentvars=%x parent_ec=%x target=%x' % (j, k, s, nm.get(sn, '-'), p, nm.get(pn, '-'), vi, pv, pec, pv + 4 * vi))
ring(64, 32, W(40), 'stale', 32)
for j in range(max(0, W(40) - 32), W(40)):
e = 4480 + 16 * (j % 32); t, h, su, v = (W(e + 4 * q) for q in range(4))
print(' stale#%d target=%x block=%x off=+0x%x %s size=%d value_before=%x%s' % (j, t, h, t - h if h else 0, 'USED' if su >> 16 else 'FREE', su & 0xffff, v, ' <<< HEADER HIT' if h and t - h < 12 else ''))
ring(1408, 64, W(44), 'all parent accesses (last 16)', 16)
if W(32):
print('ANOMALY code=%d hdr=%x prevhdr=%x hdr.next=%x hdr.prev=%x sizeused=%x frees=%d allocs=%d staleidx=%d caller=%x lastfree=%x lastalloc=%d' % tuple(W(1088 + 4 * q) for q in range(12)))
d = [W(1152 + 4 * q) for q in range(64)]
for q in range(0, 64, 8): print(' ' + ' '.join('%08x' % x for x in d[q:q + 8]))
fh = [(W(3456 + 16 * q), W(3456 + 16 * q + 4), W(3456 + 16 * q + 8), W(3456 + 16 * q + 12)) for q in range(64)]
print('heapbase', hex(words.get(0x58499c, 0)))
if '-f' in sys.argv:
for q in range(64): print(' free ptr=%x caller=%x sizeused=%x next=%x' % fh[q])
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
"""wadfix.py IN OUT: RollingBoulderMountain never ends (its final 'wait until v13 == 0' compares with 0.5)."""
import struct, sys
d = bytearray(open(sys.argv[1], 'rb').read())
pat = struct.pack('<6I', 0x1002f, 0xd0004, 0x44, 0x4e, 0xfffc0012, 0xb4) # freezeOffCamera 1; v13; pushShort 0; eq; jzYield -4; end
i = d.find(pat)
assert i >= 0 and d.find(pat, i + 1) < 0, 'pattern not unique'
struct.pack_into('<I', d, i + 8, 0x08000044) # pushShort 0.5
open(sys.argv[2], 'wb').write(d)
print('patched at 0x%x' % (i + 8))
@@ -14,20 +14,20 @@ position) started straight in the level, and Escape in the game returned to the
| Before | After |
|---|---|
| ![](img/editor/ux_llama_before.jpg) | ![](img/editor/ux_llama_after.jpg) |
| ![](../img/editor/ux_llama_before.jpg) | ![](../img/editor/ux_llama_after.jpg) |
| A 1920-wide window: the Inspector is 170 px wide and shows no values at all. Side docks have two rows of tabs. | The docks get their intended widths (290 / 340 px). Side-dock tabs are icons with the active one named. The engine fields of the object type are folded away. |
| ![](img/editor/ux_brush_before.jpg) | ![](img/editor/ux_brush_after.jpg) |
| ![](../img/editor/ux_brush_before.jpg) | ![](../img/editor/ux_brush_after.jpg) |
| The Brush tool's buttons read "Bl…", "Pl…", the texture names "gr…", the numbers show "0,5". | Full labels and a decimal point. |
| ![](img/editor/ux_coin_before.jpg) | ![](img/editor/ux_coin_after.jpg) |
| ![](../img/editor/ux_coin_before.jpg) | ![](../img/editor/ux_coin_after.jpg) |
| The Coin tool's options are cut off: only the top pixels of the fields are visible. | The options are on one line, or wrap to a second row in narrow windows. |
| ![](img/editor/ux_menu_before.jpg) | ![](img/editor/ux_menu_after.jpg) |
| ![](../img/editor/ux_menu_before.jpg) | ![](../img/editor/ux_menu_after.jpg) |
| *Help* comes before *Geometry*, and the shortcuts are German ("Strg", "Umschalt"). | The menus are File … Geometry, Tools, Help, and the keys are named "Ctrl", "Shift". |
| ![](img/editor/ux_orig_before.jpg) | ![](img/editor/ux_orig_after.jpg) |
| ![](../img/editor/ux_orig_before.jpg) | ![](../img/editor/ux_orig_after.jpg) |
| *Open original level*: the description is cut at the right edge, and the selected row's id is grey on orange. | The text wraps and the id is readable. |
| ![](img/editor/ux_small_before.jpg) | ![](img/editor/ux_small_after.jpg) |
| ![](../img/editor/ux_small_before.jpg) | ![](../img/editor/ux_small_after.jpg) |
| A 1280×720 window: the right dock is squeezed to 130 px. Also, Esc did not close the dialog, so it was still open and blocked every key. | The docks keep their sizes and the tool options wrap. Esc closes dialogs. |
![](img/editor/ux_quit_game.jpg)
![](../img/editor/ux_quit_game.jpg)
*New: Play while the game still runs offers to quit it.*
@@ -20,27 +20,27 @@ covered by code review), Windows (the antivirus notice is unchanged there).
| Before | After |
|---|---|
| ![](img/app/ux_start_before.jpg) | ![](img/app/ux_start_after.jpg) |
| ![](../img/app/ux_start_before.jpg) | ![](../img/app/ux_start_after.jpg) |
| The app opens with the mod list scrolled to the end (Visual, Cheats); Levels and Gameplay are out of sight. | The list opens at the top and keeps its place when it is rebuilt (Apply, theme). Short windows get a lower header. |
| ![](img/app/ux_mods_before.jpg) | ![](img/app/ux_mods_after.jpg) |
| ![](../img/app/ux_mods_before.jpg) | ![](../img/app/ux_mods_after.jpg) |
| Options have no explanation unless you hover (Worlds "all"? Checkpoint spacing "10" of what?). Descriptions talk about "far clip plane", "strafe", "rigid object models". | Every option is explained next to it. Mod descriptions say what happens in the game and which keys to press. |
| ![](img/app/ux_wizard_before.jpg) | ![](img/app/ux_wizard_after.jpg) |
| ![](../img/app/ux_wizard_before.jpg) | ![](../img/app/ux_wizard_after.jpg) |
| A text file is "shorter than expected (truncated?)", a wrong folder gets "expected Setup/…/Hd/groove.exe", a half-downloaded ISO is "Game found" and fails only after Set up. | Plain messages that say what to do; an incomplete ISO is caught at once, with both sizes. |
| ![](img/app/ux_exe_before.jpg) | ![](img/app/ux_exe_after.jpg) |
| ![](../img/app/ux_exe_before.jpg) | ![](../img/app/ux_exe_after.jpg) |
| On Linux a missing groove.exe is blamed on antivirus software and explained with Windows Defender menus. After Rebuild you have to click Play again. | Linux gets a short message; **Rebuild** writes the file and starts the game. |
| ![](img/app/ux_saves_before.jpg) | ![](img/app/ux_saves_after.jpg) |
| ![](../img/app/ux_saves_before.jpg) | ![](../img/app/ux_saves_after.jpg) |
| "lowest 4 bits of the coin count", "unknown item flag (bit 0)", "Cursor on", "Revert"; no hint how to restore a backup. | Plain wording, "Opens at", "Undo changes"; **Import…** starts in the backups folder and the hint says so. |
| ![](img/app/ux_settings_before.jpg) | ![](img/app/ux_settings_after.jpg) |
| ![](../img/app/ux_settings_before.jpg) | ![](../img/app/ux_settings_after.jpg) |
| "registry values ScreenW/ScreenH", "wine from PATH"; the mod cache shows 0.0 MB after applying 6 mods (stale). | "Automatic (1920x1052)" shows the real size, plain hints, a note on what *Set up again* keeps; the cache size is current. |
| ![](img/app/ux_dark_before.jpg) | ![](img/app/ux_dark_after.jpg) |
| ![](../img/app/ux_dark_before.jpg) | ![](../img/app/ux_dark_after.jpg) |
| Dark theme: the *modifies groove.exe* badge is dark orange on grey (barely readable). | A lighter badge text in the dark theme. |
| New: quitting a hidden or hung game | New: crash report |
|---|---|
| ![](img/app/ux_quit_after.jpg) | ![](img/app/ux_crash_after.jpg) |
| ![](../img/app/ux_quit_after.jpg) | ![](../img/app/ux_crash_after.jpg) |
| While the game runs, Play becomes **■ Quit game** (neutral colour) with a confirmation. | Shown as soon as Wine reports the crash (the game hides behind Wine's error window until that is closed), with Quit game / Open logs. |
![](img/app/ux_av_linux_before.jpg)
![](../img/app/ux_av_linux_before.jpg)
*Before: on Linux every first Apply of an exe mod showed this Windows Defender notice. Now it is
shown on Windows only.*
+4 -2
View File
@@ -114,7 +114,7 @@ Kinds FLOOR, BOX, PLATFORM, PILLAR, RAMP, STAIRS, FENCE, TREE, SEA, KILL_PLANE,
– the `levelbuild.LevelBuilder` primitives; parameters = props named as in `BrushKind.params()`
(`Param(name, label, type, default, role, choices)`; role X/Y/Z/XZ_LIST/XYZ_LIST/XZ_RECT marks
coordinates). `brush.bounds()` gives an approximate box. Texture parameters: village names
(`LevelBuilder.TEX`), `"#index"`, flat colour names (`LevelBuilder.FLAT`) or `"rgbRRGGBB"`.
(`LevelBuilder.TEX`, resolved per base by `BaseTextures`), `"#index"`, flat colour names (`LevelBuilder.FLAT`) or `"rgbRRGGBB"`.
For a preview of the generated geometry: `BrushCompiler.compile(scene, warnings)` → generated
`TileModel`s/`Tile`s/textures (same as the build).
@@ -151,7 +151,9 @@ vertex colours tint the lighting.
* Meaning of many raw fields is unknown (`unk*` props of zones/waypoints, texture entry flags,
collision groups B/C, region value) – they are editable but not interpreted.
* Tile swaps (WFPC 0x10000) can only exist in bases with that flag.
* Texture names exist only for the village (levelbuild names); other levels use `#index`.
* Texture names are the village's levelbuild names; on other bases they resolve to the same picture
(`levelbuild.BaseTextures`, pixel fingerprint); names the base lacks are not offered (GeoWidgets) and a
brush using one is skipped with a warning (the editor has no CD access to copy them over).
* Lights: types other than 1/2/4 carry no data and are ignored by the engine.
* Removing original tiles shifts tile indices used by strat scripts (see editor-format.md).
* The engine limits: ≤ 8000 vertices per tile model, collision within ±8 of the tile origin
+1 -1
View File
@@ -5,7 +5,7 @@ Code: `src/main/java/com/eliasmueller/llamamod/editor/tools/environment/`, tests
`src/test/.../editor/tools/environment/` (`EnvironmentToolsTest` needs `-Dllamamod.cd`; `EnvTour`
= scripted screenshots, `EnvScenarios` = the in-game test projects).
![zones](img/editor/env_zones.jpg)
![zones](img/editor/env_zone.jpg)
## What the user gets
+94 -1
View File
@@ -88,7 +88,97 @@ and are set on load.
* normals ×127 (code multiplies by 32 → 4096 scale), d ×4096 (fixed point) → local coordinates only ±8 units!
* type bit0 = quad (4 edges), otherwise triangle (3 edges, 5th plane = 0). Bit2 ≈ normal points down (ny<0; ceilings/overhangs) [likely].
* material: ground type (0 = normal; 0x11/0x12 are ignored depending on WFPC 0x8000000, 0x1e = water/death plane [likely]; the value ends up in the player's ground state).
* Collision queries: `FUN_004036d0` (floor/ceiling), `FUN_00403ad0` (walls), `FUN_004042f0`, `FUN_004046e0`. Only **the tiles of the grid cell the player stands in** are tested (cell list from MAP), point relative to the tile's B position, rotated by −rot.
* Collision queries: `FUN_004036d0` (floor/ceiling against the map tiles), `FUN_00403ad0` (the same floor test against strat objects with a collision model, not walls – see the floor test section below), `FUN_004042f0`, `FUN_004046e0`. Only **the tiles of the grid cell the player stands in** are tested (cell list from MAP), point relative to the tile's B position, rotated by −rot.
### Floor test: inclusion rule and the floating-next-to-the-log case
![top and side view of the needle triangle next to the crate](img/floor_explained_t1l4m002.png)
The crate has two such needles, one per long side: poly 15 (z 43.68–43.73, measured live) and poly 11 (z 43.29–43.38, same shape, where a user got stuck). Drawn with `tools/floorre/explain_t1l4m002.py` (builds on the floor-test agent's plot).
(2026-11-08, from the decompile + objdump of 0x4036d0–0x403ad0, a bit-exact Python re-implementation
`tools/floortest.py`, and live reads in game. [certain] unless marked.)
**Who calls what.** Per strat and frame `FUN_00403020(strat, models=DAT_005846ec, world=DAT_00584648)`:
`FUN_004036d0` = floor test against the **map tiles of the strat's cell**; `FUN_00403ad0` = the same test
against **strat objects with a collision model** (strat+0xec bit 0x20, within the box radius strat+0x13c;
result index = (strat − DAT_006d9dc0)/0x154), skipped when strat+0xe8 bit 0x400. Neither is a wall test.
The map result wins unless the object floor is higher. Result → floor record *(strat+0xc0):
+0 height, +4 material (record byte 1), +6 flags (bit0 map floor, bit1 object floor), +0xc tile, +0xe polygon.
Material 0x1e → treated as "no floor" (height 0xffff0000).
**FUN_004036d0, per tile of the cell, per collision record** (all ints, 20.12):
1. Probe point, tile-local: `lx,lz` = (strat x/z − tile B x/z) rotated by −ry with the static sine table
`DAT_00574318` (4096 entries, cos = sin[i+0x400]), `ly = strat y − tile y + DAT_0057dd98`;
DAT_0057dd98 = **0x400 (0.25)**, or floor-record+0x18 / 2 for strats with flag 0x200000.
2. Skip material 0x11 (0x12 instead when strat+0xe8 bit 0x8000000). Skip ny = 0; skip ny < 0 unless flag 0x200000.
3. Height of the probe above the surface plane, measured **vertically**:
`i6 = (d0·4096 − nz0·32·lz − nx0·32·lx − ny0·32·ly) / (ny0·32)` (C division), `dist = −i6`.
The plane point under/over the probe is `Y = ly + i6` (plane equation solved for y at (lx,lz)).
4. `dist < −0x80` (plane more than 1/32 above the probe) → ceiling/"surface above" branch (below).
Else, if `dist < best` (strictly; initial 1000000) and ny > 0 → edge test.
5. **Edge test** (3 edges, 4 if type bit0), on the point (lx, Y, lz) *on the plane*:
`e_k = ((nyk·32·Y + nzk·32·lz + nxk·32·lx) >> 12) − dk`; **fail if e_k < tol** with
`tol = −0x40 − ((0x1000 − ny0·32) >> 6)` — i.e. 64 + (4096−4064·ny)/64 units of 1/4064:
**≈ 0.016 for flat floors, 0.023 at ny 0.5** (perpendicular distance outside the edge plane).
No radius, no vertex test, no other use of the record (bytes 5, 11, … are padding; for triangles the
5th plane is never read). The point must be inside (within tol) **every** edge plane.
6. All edges pass → `best = dist`, height = tile y + Y, tile, polygon index (counted over groups A+B+C).
**Selection rule: the passing plane with the smallest dist, i.e. the highest surface not more than
0.25 + 1/32 above the feet.** Consequence: a passing surface up to 0.28 above the feet wins over the
ground under the llama, and the llama is put onto it (verified, step 3 below).
7. Ceiling branch (dist < −0x80): passing up-facing planes are kept as "lowest surface above"
(local_70/local_5c). `FUN_00403020` uses that as the floor when no floor below was found
(unless strat+0xe8 & 0x40200) – from the decompile only, not observed.
**Why polygon edges reach out: the tolerance turns sharp corners into long spikes.**
Every edge is moved outwards by `tol`. At a corner with interior angle θ the accepted area therefore
sticks out by ≈ tol / sin(θ/2): 0.023 at a right angle (every normal log/crate edge, all quads of the
path), but ~0.19 at a 13.5° corner.
**The case (t1l4m002, world 4 chapter 2).** Cell 57,43 contains only tile 443 = model 129 (the
wooden crate/log with its piece of path) at B (57.5, 2.0, 43.5), ry 3072. The transform was checked
three ways: render faces (with z mirrored, see §2) and collision of model 129 line up (crate box on the
crate box); the bit-exact engine re-implementation gives exactly the live floor record; the debug-wireframe
screenshot shows the crate's cut end right next to the llama. Collision polygon 15 (type 0 = triangle, mat 0,
plane (108,65,10)/127 → ny 0.51, a steep bevel strip along the crate's side) is a **needle triangle**:
corners (world x/y/z) (57.725, 2.553, 43.734), (57.750, 2.651, 43.677), (57.315, 2.497, 43.729); the corner at
57.315 has **13.5°** and points away from the crate, exactly along the crate's side face (x ≈ 57.31).
Its tolerance zone is a 0.01–0.02 wide sliver that continues ~0.19 beyond the tip, over the path
(pink in `img/floor_needle_t1l4m002.png`). Kuzco's position 57.204 / 43.733:
* local (0.2329, ·, 0.2959); poly 15 edges e = (−37, 2005, −72) ≥ tol −95 → **passes**; Y = 2.4744 (the
steep plane extended 0.11 beyond its tip), dist 0.2498.
* poly 2 (path, ny 0.99) also passes, e = (525, 304, 850, 1101), height 2.270, dist 0.454 → loses
because 2.474 is higher and still within 0.28 of the feet.
* The sliver at x = 57.204 is only z 43.7300–43.7402 wide; x reaches down to ≈57.12.
* Why exactly x = 57.204 [verified, mechanism not decoded]: putting the llama into the crate
(x 57.25, z 43.60 / 43.70 / 43.733) the game pushes it back to x = 57.202–57.204 (wall push-out from the
crate side at ~0.11 distance). So walking along/against the crate's side face puts the llama exactly on
the line the needle's tolerance sliver extends to; only the ~1 cm band at the corner's z catches it.
**In game** (apptest :25, boot-to-level start "57.204, 2.474, 43.733, 0 @ 4/2", winedbg --gdb read/write of
strat +0x30/+0x50 and the floor record):
| written pos (x y z) | game afterwards: pos / floor h / poly | re-implementation |
|---|---|---|
| boot start 57.204 2.474 43.733 | 57.204/2.474/43.733, h 2.474, mat 0, tile 443 **poly 15** | poly 15, 2.4744 |
| 57.204 **2.27** 43.733 (on the path) | snapped **up** to y 2.474, poly 15 | poly 15, 2.4744 |
| 57.204 2.27 43.736 | y 2.469, poly 15 | poly 15, 2.4690 |
| 57.204 2.27 43.745 | y 2.270, poly 2 | poly 2, 2.2700 |
| 57.15 2.27 43.733 | y 2.266, poly 2 | poly 2, 2.2661 |
| 57.25 2.3 43.70 | pushed to x 57.204, y 2.270, poly 2 | – |
Screenshots: `img/floor_float_t1l4m002.jpg` (left: boot at the floating spot, poly 15; right: 0.012 further
in z on the path, poly 2; debug wireframe on), `img/floor_float_t1l4m002_full.jpg`.
After teleporting onto poly 15 with the debugger the llama sometimes slid off within seconds (steep
plane, ny 0.51); started there by boot-to-level it stays [observed, not analysed].
**How common.** Scan of all 31 t1 level WADs (sample points 0.3/0.6/0.9 of the way from each corner to its
tolerance tip, exact engine test over all tiles of that cell, counted when this polygon wins, the point
is ≥ 0.08 outside the real polygon and the next lower floor is > 0.05 below): 2–526 polygons per level,
t1l4m002: 110 (of 6468 floor-polygon instances), poly 443/15 among them (predicted spot 57.199/2.469/43.735).
Most are sharp triangle tips of terrain/rock meshes at ledges; normal right-angled log/crate edges
reach only 0.023 and never show it. [Counts are a heuristic scan; reachability of the spots not checked.]
Scripts: `tools/floortest.py` (single position); scan code: `$CLAUDE_JOB_DIR/tmp/floorre/exposed.py` + `reach.py` (scratch, not in the repo).
### Face flags (u16)
| Bit | Meaning | Confidence |
@@ -217,3 +307,6 @@ Implemented in `tools/geom_import.py` (OBJ → TRAK + MAP tile part, rest of the
* `geom_import.py BASE.wad MESH.obj NEW.wad [--coll-obj C.obj] [--keep-tiles] [--zones flat|keep] [--default-tex N] [--export-space]`
* `geom_demo.py BASE.wad demo.obj [--plaza]` – example geometry made of texture-tile quads.
* `geomlib.py` – parser/builder (TRAK, STPC models, MAP tile part, TEXT table/pages, collision).
* `floortest.py WAD X Y Z` – bit-exact re-implementation of the engine's map floor test (FUN_004036d0) at a world
position: every candidate polygon of the cell, its edge values/tolerance, and the chosen floor. Note: the engine's
plane scale is n/127 with d/4064 (normal×32 vs d×4096), `geomlib.coll_decode` uses d/4096 (0.8 % off).
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+73
View File
@@ -68,6 +68,31 @@ stein, stein_kante, stein_moos, platte, holz_dunkel, holz_maser, holzbalken, rin
inka, muster_lila, …`), a direct index (int) or a flat colour (`'meer'`, `'rot'`, … or
`(r,g,b)`; appended to the texture table as a flat-colour entry (flags 0x105, page −1)).
Contact sheet with indices: `notes/img/texturen_dorf.png`.
**Texture names on other bases** (2026-11-07). The `TEX` indices are village (t1l1m001) indices; on another
base the same index is a different picture (village 2: index 1 = dark red floor). Names are therefore
resolved per base by picture: `levelbuild.BaseTextures` holds a fingerprint (FNV-1a over page format,
size and pixels of the entry's rectangle) of every named village texture; a name maps to the village index
if the base shows that picture there (village 1: always → outputs unchanged), else to the first base entry
with the same picture (flags 0x104 preferred). Coverage of the 47 names: t1l1m002 38 (missing: the three
flower textures, `wood_grain*`, `wood_brown*`, `wood_rings`, `wood_edge`), t1l1m003 29, t1l1m004 28,
t1l2m001 9, t1l3m001/t1l5m001 0–2.
* Java builder (`LevelBuilder`, test world/arena/jump lab, `fromCd`): a missing picture is **copied from the
village** (CD file loader `wads/t1l1m001.wad`): packed onto a new 256×256 page (same page flags, 1 px
clamped border) and appended as an entry after the base table; flat colours and copies share the
appended range in order of first use. Without a loader (editor brushes: `BrushCompiler` passes none)
the name is rejected: "texture 'flowers' is a village texture this base level does not have …"
(the editor shows that as a brush warning and only offers the names the scene's base has).
`LevelBuilder.baseTextureNames()` lists the names a base has.
* Python `tools/levelbuild.py --base`: same matching (md5 of RGBA rectangle); missing names raise a
KeyError (no copying).
* In game (apptest, display :22, 2026-11-07): Java-built level on t1l1m002 (grass box, stone box, sand,
trees, plus copied `flowers`, `wood_rings`, `wood_grain`), `--editor-import` → level mod, Village
chapter 2: grass green, stone grey, trees bark + leaves, copied textures correct
(`notes/img/editor/village2_levelbuild_textures.jpg`, `…_textures2.jpg`). Before: grass = dark red
(`village2_levelbuild_start_camera.jpg`).
* Editor (display :22): on a t1l1m002 scene the brush swatches and the texture grid show the base's own
pictures for the names, without the names it lacks (`notes/img/editor/village2_texture_picker.jpg`).
**Every triangle shows a whole texture tile** (the engine has no free UVs) – so
everything is split into 1×1 pieces; narrow/flat walls show the texture squashed.
@@ -148,6 +173,11 @@ stairs, platforms register as shadow casters (`solids`). 1.0 = texture colour
## Exit (LevelDoor) – how it works
> **Corrected 2026-11-07 (in game, Jungle Day 2):** the listing below was the first reading. The
> LevelDoor only sets g101–g105 and checks global 0x21 *once*; the level is ended by the **Llama's
> script** (material 16 + exit cell + at most ~0.6 above the floor). See "Exit via stale floor
> material" further down and docs/reference/objects.md.
Strat bytecode (village, STPC 0x430332, 3 parameters, invisible, no model), decoded via the
VM `FUN_0054d180` (opcode < 0x45: table 0x5796bc with 16-bit operand, otherwise 0x578ff4+op·4):
```
@@ -174,6 +204,46 @@ loop: if g21 == 3 ; game state "running"
(zone teleport). t1l3m003: 2 of the 4 doors (cells 71,48/71,49) have no pad at all.
* Second path (strat code at 0x4252aa, player logic [likely]): floor material 16 and cell =
g104/g105 (set by the LevelDoor at start, Llama sets them to −1) → level change(g101..g103).
### Exit via stale floor material (hover + position jump, t1l4m002) – checked 2026-11-07
Speedrun video (position/hover hack): Kuzco hovers above a house-door stump, is moved to the exit and
the level ends. Reproduced in game (apptest, gdb through `winedbg --gdb`, position written by gdb):
* **Jungle Day chapter 2 = t1l4m002** (LNFO 95 coins / 2 secrets; tally "Dschungeltag Kapitel 2,
0/95, 0/2"). LevelDoor at 44.5/25.5 (cell 44,25), params (1,4,3); RubyIdolDoor in the same
cell. Cell 44,25 (tile 255) has material 16 **only in swap model 95** (pad x 44.67–45.0,
z 25.25–25.75, y 2.25); main model 94 has none → with the door closed the exit cannot fire.
House-door pads (material 16, main models) in cells 38,20 / 51,0 / 52,5 / 41,24 / 44,27 /
50,27 / 41,29 / 57,55 / 41,38 / 41,44 plus swap pads in 22,21 / 26,21 / 23,23 / 50,1 / 43,21 /
46,30 / 58,49 / 31,50; the zones pair them (e.g. 44,27 → 50.5/27.5, 50,27 → 44.5/27.5).
* **Floor test (FUN_004036d0):** only tiles of the player's own cell; per upward polygon the plane
height at (x,z) is compared with a probe point (y + 0.25, or y + (floor record +0x18)/2 with
+0xe8 flag 0x200000 as for the llama);
the **nearest floor below the probe wins, with no distance limit**. Material 17 is skipped for
the llama (+0xe8 = 0x24882a, no 0x8000000). If nothing is below: for the llama (+0xe8 & 0x40000)
material/height are **not** updated (stale) and x/z go back to the previous position.
In game: llama held at 44.9/**6.0**/27.5 (3.7 above the pad of the stump at 44,27) → floor
record h 2.30, **material 16**, tile 282 poly 15, every frame.
* **Order per tick (FUN_0054c4a0):** all strat scripts (VM FUN_0054d180) first, then the
collision pass (FUN_00403020 for every strat with +0xe8 & 0x8000). A position written after the
collision pass is seen by the scripts of the next tick with the **old floor record**.
In game: after hovering above the 44,27 pad, llama set to 44.5/2.3/25.5 right after its floor
test → next event endLevel `FUN_0054d620` (called from the VM, floor record still 16), before
any new floor test → tally (twice with screenshot, once traced). Control: same jump from normal
ground (material 0) → no exit; jump written just *before* the floor test → record becomes 0, no exit.
* **Who ends the level here: the Llama, not the LevelDoor.** LevelDoor bytecode: wait 5 ticks,
g101..g105 = params + cell, then **once** `if global 0x21 != 3 → end` (so "g21 == 3" above is a
one-time check, and global 0x21 is not the game state 0x5846e4), then the per-tick loop. In
t1l4m002 the LevelDoor was gone from the strat list (scan of 0x6d9e38) while g101..g105 =
1,4,3,44,25 and global 0x21 = 0. The Llama block at STPC 0x39ff4c–0x3a0064: if floorMaterial
== 16 and health > 0 and (y < floorHeight + 0.6 and global 0x21 != 2, or y < floorHeight + 0.025
and (g1 == 0 or global 0x23)): cell == (g104, g105) → level change(g101..g103), else fade-out +
`getDoorPos` (op 0x100, zone teleport = house door) + `forceCollision`. The 0.6 limit is why
hovering high above a house pad does not trigger the house teleport (bytecode; not separately
tested), while landing at floor height in the exit cell with the stale record does.
* Not explained: in two sessions with a moved player start (boot-to-level `start` 43.3/25.5) the
same jump did not end the level (Llama script ran with record 16 in cell 44,25, no endLevel).
* Images: `img/debug/exit-hover-trick.jpg` (top: video; bottom: our pool/door platform, tally).
* Parameters in the originals: (1, world, chapter+1) = next chapter; in the last chapter of a
world **(7,7,7)** = "world completed" → back to the selection map, unlock the next world.
* `FUN_0054d620`: remembers the current level (0x585008..14), sets the next (0x584eb4/b8/bc), shows the
@@ -190,6 +260,9 @@ loop: if g21 == 3 ; game state "running"
49 chapter slots, table 0x572420) and calls `lama-mod an`. For this, `lama-mod` now also takes
symlinks from mod folders (`find … -type f -o -type l`). Checked: chapter 2 on the map is
selectable and loads the test world.
* **Only chapters 1 and 32–49 are safe** for world 9: the per-chapter coin counter and flag byte of
the other chapters overwrite game globals (chapter 9 destroys the furthest progress), see
[testlevel-chapters.md](testlevel-chapters.md).
* The selection map shows no background for world 9 (only text) and 772 as the coin target
(coin table 0x5722f4 only has worlds 1–8) – cosmetic.
+67 -9
View File
@@ -121,9 +121,10 @@ Each contains `68 90 E0 56 00` (push offset subkey string, VA 0x56E090) followed
byte `02` of each `push 80000002h` becomes `01` (HKEY_CURRENT_USER), so no admin rights are needed
on Windows. German CD (`groove.exe` MD5 697d67a31c20079838c662d8187dbd86, 1,642,496 bytes),
file offsets of the changed bytes: **0x579B, 0x57D0, 0x5810, 0x5859, 0x5880** (original 02,
new 01; patched MD5 6199a42eb7582b2a70b1ac6e29f00f43). The patcher searches the pattern
(push of the subkey string VA + push HKLM) instead of fixed offsets, so other language builds work
if their code is the same.
new 01; MD5 with only this part 6199a42eb7582b2a70b1ac6e29f00f43; the base patch also relocates the
world-9 level records, see "groove.exe patches" below). The patcher searches the pattern
(push of the subkey string VA + push HKLM) instead of fixed offsets; the English build has the
changed bytes at the same file offsets. Supported builds: see "groove.exe versions" below.
File lookup (FUN_00415690): tries `HDPath\dir\file`, then `CDPath\dir\file` (checked with
`_access`), and only then loops over drive letters with `GetDriveTypeA == DRIVE_CDROM`. A plain
@@ -222,8 +223,11 @@ reads a CD folder, collects writes in `ctx.written`, resolves options (defaults
`game/groove.exe` is rebuilt on every apply: `original/hd/groove.exe` → HKCU base patch
(`platform/ExePatcher`, owner "base", always first) → exe changes of the enabled mods in apply
order. Without exe mods the result is byte-identical to the old ExePatcher output; "Restore
original" goes back to it. A mod gets the shared image with `ctx.exe()`; all changes are
order. Without exe mods the result is byte-identical to `ExePatcher.patch` (installer output);
"Restore original" goes back to it. The base patch (REVISION 2) = HKCU + `relocateLevelRecords`
(world-9 coin/flag arrays moved into .data's page slack – English build: .rdata's page slack, and
.rdata's section flags get the write bit, because its .data slack is only 43 bytes; 4-byte address
operands only, see testlevel-chapters.md); `ExePatcher.changes` lists both as byte runs. A mod gets the shared image with `ctx.exe()`; all changes are
attributed to the mod id, journaled into the mod's cache manifest (`"exe": [...]`) and replayed
(with full verification) on a cache hit. Every patch/allocation is logged in apply.log
(`exe <mod>: patch 0x… n bytes – label`).
@@ -267,6 +271,58 @@ in cave code are fine). Tests: `exe/ExeImageTest` (real exe, re-parse + `objdump
`mods/ModOptionsAndExeTest`; in-game check: `mods/ExeSmokeMod` (test classes, see below; its call
counter is an `allocateData` block).
### groove.exe versions (`exe/GameExe`, `exe/Site`)
Two builds are supported; their code is the same but shifted, and the .data globals are laid
out differently, so **no patch uses a fixed address**:
| Build | PE date | Size | SHA-256 |
|---|---|---|---|
| German (reference, all RE notes) | 2001-02-12 | 1,642,496 | `7832eda4ee47a717e94441f21b75a9f30d11445c9f2408c6135a8fc01812e95b` |
| English first release ("Start Game", "Chapter") | 2000-10-17 | 1,634,304 | `bf0eb8829ed3e371c4e951ece0a7753acf2c28d07001b842e079b098576403b0` |
`exe.game()` (a `GameExe`, cached per `ExeImage`) identifies the build by SHA-256 (`build()`,
`describe()` – logged in apply.log as `groove.exe: English version of 2000-10-17`) and resolves
each named `Site` on the file **as parsed** (before any patch): `va(site)`, `original(site, n)` =
the original bytes there (expected bytes for `patch`/`hookCall`, and the stolen bytes a detour
re-emits, e.g. the frame handlers' `mov eax,[global]`). Rules in `Site`: `code(sig)` = unique
match in .text, `??` masking absolute addresses and rel32 operands; `operand(sig, off)` /
`operandAt(site, off)` = the absolute address an instruction uses (must lie in .data – globals
are always read from code, never guessed); `callTarget(site)` = target of a call; `.calling(s)`
also checks that the call at the site goes to `s`. Each site lists its German and English VA:
`exe/GameExeTest` checks that both builds resolve to them and that every site disassembles
(objdump) to the expected instruction; `mods/ExeVersionsTest` checks that the German output of
every exe mod and option set is **byte-identical** to the output before the version layer (SHA-256
golden values) and that every mod makes the same patches in the English exe;
`ExeModCombinationsTest` runs all combinations on both. The English exe is read from
`-Dllamamod.exeEn` (default `re/groove_en.exe`, gitignored – never commit it or add it to test
resources); tests skip without it.
An unknown build fails on the first site that does not match exactly once, e.g.
`groove.exe (unknown version (PE date …, … bytes)): MENU_FRAME (state 2 (main menu) frame handler …)
not found – unknown program version. LlamaMod supports: German version of 2001-02-12, English
version of 2000-10-17.` The base patch stays lenient (HKCU by pattern, relocation skipped).
The mod cache key includes the exe hash for every build except the German one (whose keys did
not change).
**In game** (apptest :25, 2026-11-07, English exe in `original/hd` with the **German CD data** –
the English exe runs with it; menus are English because the strings are in the exe): main menu
without logos after 17 s and no attract demo after 100 s (intro skip); boot to level (Village 2,
TestLevel 9); practice F5/F8 restores the position; free camera F7 flies away from the llama;
cheats: winedbg showed cheat flags 0x583DA0 = the chosen bits, energy 0x584500 stayed 0x5000
after falls into water with `health`, dropped to 0x4000 without it; TestLevel chapter 9 → "Quit to
Map Select" gives a sane map (Village / Chapter 1 / 55) instead of the broken progress.
![English exe: menu, practice restore, free camera, map after TestLevel 9](img/mods/english_exe.jpg)
**Adding a build:** copy it to `re/` (gitignored), add a `GameExe.Build` (language, PE date, size,
SHA-256), run `GameExeTest`: every failing site needs a longer/other signature (keep it unique in
all builds; mask only addresses and rel32). For the new column, read each address off the
resolved sites (or with a script that pairs relocation entries of matched instructions, as done
for the English build: an operand is accepted only if most references to the German global agree),
then add the column to `Site` and a case to `Build.va`. Check the base patch's free space
(`ExePatcher.SLACK_SECTIONS`) and run `ExeVersionsTest` / `ExeModCombinationsTest`; then test in
game.
### Antivirus false positives
A changed groove.exe with extra code may be flagged by antivirus programs (Windows Defender).
@@ -313,7 +369,9 @@ Decoded from FUN_0041c640 (load) / FUN_0041c820 (save); details in `save/Ksv.jav
| 0xD0–0xD9 | options: effects volume, music volume, 0x80, speaker mode, 6 more option bytes |
Chapters per world (table 0x572420): 4, 3, 4, 3, 3, 3, 5, 5, 49. World 9 has no row in the
per-chapter arrays. The KSV files shipped on the CD are empty (0x00 then 0xCC). Selecting an empty
per-chapter arrays. Its coin counter/flag byte for chapters 2–31 land on other globals (chapter 9 =
furthest progress): mods use only chapters 1 and 32–49 (`save/TestLevelChapters`,
[testlevel-chapters.md](testlevel-chapters.md)). The KSV files shipped on the CD are empty (0x00 then 0xCC). Selecting an empty
slot and "new game" immediately writes a zeroed file.
## Tests done (2026-11-06)
@@ -354,9 +412,9 @@ On real hardware no wrapper should be needed (unverified).
Runs the game through the LlamaMod app (fat jar CLI), invisible and muted, one isolated *session*
per name: own data folder (`<root>/<name>/data`, = LLAMAMOD_DATA, own Wine prefix and save games),
own Xvfb display, own PulseAudio daemon with only a null sink (PulseEffects on the desktop steals
streams from null sinks on the main server). Root: `$APPTEST_ROOT`, default
`/home/elias/.claude/jobs/90c6eed7/tmp`. The original game data is installed once from
`~/Downloads/GROOVE_DK.iso` into `<root>/_llamamod-template` (file lock, so parallel agents wait)
streams from null sinks on the main server). Root: `$APPTEST_ROOT`, else `$CLAUDE_JOB_DIR/tmp`
(AI agent sessions), else `~/.cache/llamamod-apptest`. The original game data is installed once from
the ISO (`--iso`, `$APPTEST_ISO`, default `~/Downloads/GROOVE_DK.iso`) into `<root>/_llamamod-template` (file lock, so parallel agents wait)
and hard-linked into each session (~30 MB per session + ~200 MB Wine prefix on first start).
`~/.wine-lama` and the user's own game are never touched.
+10 -8
View File
@@ -11,11 +11,12 @@ Registered in `ModRegistry` (one line). Test-only: `ArenaTestMod` (src/test, scr
| `waves` | 3 (1–5) | wave n has min(n, 2) alligators (3 waves = 5 alligators) |
| `energy` | 4 (1–5) | llama energy cap (5 = game normal) |
| `coins` | 1 (0–5) | EnemyCoins per hit (+5 on defeat, +5 bonus at the exit) |
| `chapter` | 8 (2–8) | TestLevel chapter; a chapter already written by another mod → apply error |
| `chapter` | 49 (32–49) | TestLevel chapter; a chapter already written by another mod → apply error; a stored 2–8 falls back to 49 |
Chapter ≤ 8 on purpose: `levelRecord` (coin counter) lives at `0x584F1C[world·6 + chapter]`; for
world 9 chapter 9 that is 0x585018 = furthest progress (unverified risk for level projects on
chapters ≥ 9, see "Issues").
Only chapters 32–49: the coin counter `0x584F1C[world·6 + chapter]` and the flag byte
`0x584EC8[…]` of world-9 chapters 2–31 overwrite other globals (chapter 8 = last played map,
chapter 9 = furthest progress – verified, see [testlevel-chapters.md](testlevel-chapters.md)).
The tally total (LNFO) is 2 placed coins + alligators · ((lives − 1) · coins + 5) + 5 bonus.
Script options (`Arena`, tests/`ArenaTestMod`): `probe=true` (event probe 1.6 ahead of the llama),
`master=false` (no alligators), `start=x,z,rot`, `debug=true` (HUD coin counter shows
@@ -72,7 +73,9 @@ per type (nVars/slots/typeFlags/extra), then `LevelBuilder` builds the level.
* `spawn` copies the strat's matrix/translation block, not `x/y/z`: after `self.x = …` the child
appears at the old place unless the spawner yields first.
* Energy 0: the game's "Weiter / Beenden" screen; Weiter restarts the level (all strats fresh).
* The level tally after the exit shows "Münzen gesammelt 0 / 55" (not our coin counter).
* The level tally after the exit showed "Münzen gesammelt 0 / 55" on chapter 8: 0 = the coin slot
after endLevel wrote the last played map into it, 55 = LNFO of the Village base. Fixed: chapter
32–49 and recounted LNFO ("2 / 12" for 1 alligator with 1 life).
## Verified in game (apptest :24, Wine 6, load 3–9, 2026-11-07)
@@ -101,7 +104,6 @@ mod's chapter → IOException; probe option.
## Issues / limits
* Loading picture and tally use the TestLevel texts ("TestLevel Kapitel 8").
* Loading picture and tally use the TestLevel texts ("TestLevel Kapitel 49").
* The exit target is the arena itself (1, 9, chapter): it restarts after the tally.
* Level projects on TestLevel chapters ≥ 9 may write the coin counter over furthest progress
(0x585018) – not verified.
* Exiting chapter 49 unlocks "chapter 50" (progress 9/50); the map caps the cursor at 49 (checked).
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff. Show more