Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
165134c0d8 | ||
|
|
526059c70a | ||
|
|
ec2876d9c8 | ||
|
|
e36ff2d2c2 | ||
|
|
8b3c38fe56 | ||
|
|
db08cf8bf3 | ||
|
|
b5b6e22957 | ||
|
|
15ad050005 | ||
|
|
74f8fec341 | ||
|
|
8baed2e87c | ||
|
|
9e8e7f9a10 | ||
|
|
c39d58bdac | ||
|
|
0c2920dfe4 | ||
|
|
d32d197b54 | ||
|
|
0568ae7f24 | ||
|
|
64259ea15b | ||
|
|
322e36c0f3 | ||
|
|
52c70e817d | ||
|
|
1cc59bfb78 | ||
|
|
932ccf5fc8 | ||
|
|
ff6673af66 | ||
|
|
ca22c75fac | ||
|
|
f984019199 | ||
|
|
abe97b6977 | ||
|
|
bc8408d963 | ||
|
|
a64df88c77 | ||
|
|
f5fd411592 | ||
|
|
6d449257a7 | ||
|
|
a0d961357e | ||
|
|
624718b893 | ||
|
|
00b2880d67 | ||
|
|
d6b852a1b6 | ||
|
|
987b0104fa |
No files matched your search
@@ -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
|
||||
@@ -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,5 +1,6 @@
|
||||
# game data (copyrighted) - never commit
|
||||
re/groove.exe
|
||||
re/groove_en.exe
|
||||
re/groove_decomp.c
|
||||
re/proj/
|
||||
re/ghidra.log
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||

|
||||
|
||||
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 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**: all 253 figures and objects of the game in one level | **Alligator Arena**: a minigame with new, scripted enemies |
|
||||
|  |  |
|
||||
| **Llama Coaster**: a new roller coaster track for the catacombs cart | **Debug view**: collision and invisible objects in the running game |
|
||||
|  | |
|
||||
| **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
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# LlamaMod documentation
|
||||
|
||||
Documentation for modding *The Emperor's New Groove* (PC, 2000; German: *Ein Königreich für ein
|
||||
Lama*) with LlamaMod: building levels, the game's file formats, and how the game's object scripts
|
||||
("strats") work.
|
||||
|
||||

|
||||
|
||||
## Start here
|
||||
|
||||
| If you want to … | read |
|
||||
|---|---|
|
||||
| build your own level | **[Guide: Build your own level](guide/README.md)** |
|
||||
| look something up in the editor | [Level editor manual](manual/index.md) (also in the editor: **F1**) |
|
||||
| know the numbers and rules of the engine | [Design rules](guide/design-rules.md) |
|
||||
| build levels from code | [Levels from scripts](guide/levels-in-code.md) |
|
||||
| understand the game's data | [Reference](reference/README.md) |
|
||||
| change or write object behaviour | [Strats](strats/README.md) |
|
||||
| find a tool | [Tools](tools.md) |
|
||||
| work on LlamaMod itself | [For developers](developers.md) |
|
||||
|
||||
## Contents
|
||||
|
||||
**[Guide](guide/README.md)** – for level builders
|
||||
* [Build your own level](guide/README.md): setup, editor basics, ground, start and exit, objects,
|
||||
look and feel, play-test, sharing
|
||||
* [Design rules](guide/design-rules.md): sizes, jump distances, the rules that keep a level working
|
||||
* [Levels from scripts](guide/levels-in-code.md): Python and Java level builders, the editor's
|
||||
command line
|
||||
|
||||
**[Level editor manual](manual/index.md)** – every tool, panel and dialog of the editor, with
|
||||
tutorials (first level, parkour, editing an original level, Blender import)
|
||||
|
||||
**[Reference](reference/README.md)** – the game's data
|
||||
* [Game files](reference/game-files.md): folders, naming scheme, worlds, COR pictures, movies,
|
||||
save games
|
||||
* [WAD container](reference/wad.md): the chunks of a level file
|
||||
* [Geometry and collision](reference/geometry.md): tiles, grid, coordinate spaces, collision
|
||||
materials, visibility regions
|
||||
* [Objects, waypoints, zones](reference/objects.md): strat records, parameters, the exit,
|
||||
environment zones, lights
|
||||
* [Textures](reference/textures.md): pages, pixels, texture rectangles, animations
|
||||
* [Music and sound](reference/audio.md): ASF music, sound banks, speech – known and unknown
|
||||
* [TestLevel (world 9)](reference/testlevel.md): where custom levels live, safe chapters
|
||||
* [Engine facts and limits](reference/engine-limits.md): scale, draw distance, memory, crashes
|
||||
|
||||
**[Strats](strats/README.md)** – object behaviour
|
||||
* [How the strat VM works](strats/README.md): units, frames, variables, events, spawning,
|
||||
collision
|
||||
* [Reading scripts](strats/reading-scripts.md): the Script view and the assembly
|
||||
* [The script language](strats/script-language.md): writing new behaviour
|
||||
* [Customising strats](strats/custom-types.md): constants, imports, clones, custom types, mods
|
||||
* [Example: the Alligator Arena](strats/arena.md): a complete minigame explained step by step
|
||||
* [Example: the roller coaster](strats/roller-coaster.md): 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
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|  |  |
|
||||
| The launcher: mods with options, save games, settings | The level editor with Village 1 |
|
||||
|  |  |
|
||||
| The Alligator Arena minigame | Debug view: collision and invisible objects in the game |
|
||||
|
||||
## Status and honesty
|
||||
|
||||
Everything here was found by reverse engineering and tested against the game; where something is
|
||||
a guess, the page says so (*likely*, *unknown*). Development used the German CD; 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.
|
||||
@@ -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`).
|
||||
@@ -0,0 +1,126 @@
|
||||
# Build your own level
|
||||
|
||||
A step-by-step path from "I have the game" to "my friends are playing my level". Each step is short
|
||||
and links to the chapter of the [editor manual](../manual/index.md) that has the details.
|
||||
The manual is also built into the editor: press **F1** over any panel, tool or dialog.
|
||||
|
||||
| Step | |
|
||||
|---|---|
|
||||
| 1 | [Set up LlamaMod](#1-set-up-llamamod) |
|
||||
| 2 | [Learn the editor in ten minutes](#2-learn-the-editor-in-ten-minutes) |
|
||||
| 3 | [Plan the level](#3-plan-the-level) |
|
||||
| 4 | [Build the ground](#4-build-the-ground) |
|
||||
| 5 | [Start, checkpoints and the exit](#5-start-checkpoints-and-the-exit) |
|
||||
| 6 | [Objects, enemies and cameras](#6-objects-enemies-and-cameras) |
|
||||
| 7 | [Look and feel: textures, fog, music, light](#7-look-and-feel-textures-fog-music-light) |
|
||||
| 8 | [Play-test](#8-play-test) |
|
||||
| 9 | [Share it](#9-share-it) |
|
||||
| 10 | [Go further](#10-go-further) |
|
||||
|
||||
Before you start, skim [Design rules](design-rules.md): the numbers (jump distances, draw
|
||||
distance) and the handful of rules that keep the game from crashing.
|
||||
|
||||

|
||||
|
||||
## 1. Set up LlamaMod
|
||||
|
||||
1. Get **your own copy** of the game: the CD or an ISO image of it (*The Emperor's New Groove* PC,
|
||||
2000; German: *Ein Königreich für ein Lama*). LlamaMod contains no game files.
|
||||
2. Download the **full** package for Windows or Linux, unpack it and start `LlamaMod.exe` /
|
||||
`./LlamaMod` (Linux needs Wine to run the game). Or build it from source – see the
|
||||
[main README](../../README.md#build-from-source).
|
||||
3. The first-start wizard copies the game into LlamaMod's data folder. The original CD or ISO is
|
||||
never changed.
|
||||
4. Click **Play** once to check that the unmodded game runs.
|
||||
|
||||
Manual: [Installing LlamaMod and the first start](../manual/02-installing.md).
|
||||
|
||||
## 2. Learn the editor in ten minutes
|
||||
|
||||
Click **Level Editor** in LlamaMod and follow
|
||||
[Your first level in 10 minutes](../manual/tutorial-first-level.md): a new level from the
|
||||
*Island* template, a ramp, a coin trail, play it. Then read
|
||||
[Introduction and concepts](../manual/01-introduction.md) – tiles, brushes, objects, zones, base
|
||||
levels, chapters – and [Navigating the viewport](../manual/04-navigation.md).
|
||||
|
||||
## 3. Plan the level
|
||||
|
||||
* **New or edited?** A *new level* starts empty on a **base level** (default: Village 1) that
|
||||
provides the object types, textures, sky, fog and music. *Editing an original level* changes the
|
||||
real Village, River … in place ([tutorial](../manual/tutorial-original-level.md)).
|
||||
* **Choose the base by its objects.** Only the base's object types are available directly; more
|
||||
can be imported from any world ([Importing objects](../manual/09-objects.md#importing-objects-from-other-worlds)).
|
||||
* **Size.** The village grid is 96 × 96 cells; the llama is 0.6 tall and jumps about 1.3 cells.
|
||||
A small, dense level plays better than a big empty one – the draw distance is only 8–16 cells.
|
||||
* **Where it goes.** New levels live in the hidden world 9, *TestLevel*, on chapter 1 or 32–49
|
||||
([why](../reference/testlevel.md)). The editor picks a free chapter.
|
||||
|
||||
## 4. Build the ground
|
||||
|
||||
* **Brushes** (B) draw boxes, ramps, stairs, cylinders and more; they are cut into tiles when the
|
||||
level is built ([Building geometry](../manual/06-geometry.md)).
|
||||
* **Tiles** (T) place the base level's own tile models for an original look.
|
||||
* **Meshes** from Blender or any 3D program come in as OBJ or glTF
|
||||
([Importing meshes](../manual/07-meshes.md), [Blender tutorial](../manual/tutorial-blender.md)).
|
||||
* **Collision** (K) decides what is solid: walls, floors, invisible walls, death floors, the exit
|
||||
floor. The llama cannot step up even small edges – use slopes, or walls without collision.
|
||||
|
||||

|
||||
|
||||
## 5. Start, checkpoints and the exit
|
||||
|
||||
Use the one-click setups in the Objects panel ([Objects](../manual/09-objects.md#one-click-setups)):
|
||||
|
||||
* **Player start** – the llama plus its start camera node behind it. Keep the space behind the
|
||||
llama free, or the camera jumps aside.
|
||||
* **Checkpoint** – where the llama respawns after a fall.
|
||||
* **Exit** – a LevelDoor *and* a floor with collision material 16 in the same grid cell. Without
|
||||
that floor the exit does nothing ([why](../reference/objects.md#the-exit-leveldoor--certain)).
|
||||
Its target is the next level; in TestLevel never "world finished" (7, 7, 7).
|
||||
* **Death plane** – a death floor below everything, so falling off respawns instead of falling
|
||||
forever.
|
||||
|
||||
## 6. Objects, enemies and cameras
|
||||
|
||||
* Place coins with the **Coin tool** (O), anything else from **Assets → Objects**
|
||||
([Objects](../manual/09-objects.md)).
|
||||
* Enemies and moving things often need a **waypoint chain** – some guards even need a pair of
|
||||
waypoint boxes and an event bit, or the game removes them. The Inspector and *Check for problems*
|
||||
tell you ([Waypoints](../manual/10-waypoints.md)).
|
||||
* **Cameras**: camera zones, cut scenes and the start camera
|
||||
([Cameras and triggers](../manual/11-cameras-triggers.md)).
|
||||
* Change how an object behaves – a number in its script or a whole new type: see
|
||||
[Strats](../strats/README.md) and [manual chapter 22](../manual/22-strats.md).
|
||||
|
||||
## 7. Look and feel: textures, fog, music, light
|
||||
|
||||
* **Textures**: the base's textures, imported PNG/JPG pictures, painting onto faces, flip-book
|
||||
animations ([Textures](../manual/08-textures.md)).
|
||||
* **Environment zones** set fog (= draw distance), sky colour and **music**; teleport zones move
|
||||
the player between areas ([Environment](../manual/12-environment.md),
|
||||
[music ids](../reference/audio.md#music)).
|
||||
* **Light**: tiles get baked vertex colours (*Bake lighting*), lights only light objects.
|
||||
* **Visibility regions** hide areas the camera cannot see – needed for big levels
|
||||
([Visibility](../manual/13-visibility.md)).
|
||||
|
||||
## 8. Play-test
|
||||
|
||||
**F5** builds the level and starts the game directly in it; **Shift+F5** starts at the camera
|
||||
position. *Check for problems* (also run on every build) lists errors and warnings – double-click
|
||||
one to select the culprit. **Escape** in the game quits straight back to the editor.
|
||||
Manual: [Building, problems and play-testing](../manual/15-build-play.md).
|
||||
|
||||
## 9. Share it
|
||||
|
||||
**File → Save** writes a small `.llamalevel` project: only your changes, no game data. Send that
|
||||
file; the receiver copies it into LlamaMod's `levels` folder (or opens it in the editor and uses
|
||||
*Level → Add to LlamaMod…*), ticks it in the *Levels* category and clicks *Apply*. It is built from
|
||||
their own game copy. Manual: [Sharing levels](../manual/17-sharing.md).
|
||||
|
||||
## 10. Go further
|
||||
|
||||
* Build levels from code instead of the mouse: [Levels from scripts](levels-in-code.md).
|
||||
* Understand the data: [Reference](../reference/README.md).
|
||||
* Write object behaviour: [Strats](../strats/README.md) and the
|
||||
[Alligator Arena walkthrough](../strats/arena.md).
|
||||
* All tools at a glance: [Tools](../tools.md).
|
||||
@@ -0,0 +1,48 @@
|
||||
# Design rules
|
||||
|
||||
The numbers and rules that matter when you design a level for this engine. Background and sources:
|
||||
[Engine facts and limits](../reference/engine-limits.md).
|
||||
|
||||
## Sizes to design with
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| one grid cell | 1 unit |
|
||||
| llama | 0.6 tall, walks 1.1 units/s |
|
||||
| jump up | ≤ 0.35 (0.5 fails) |
|
||||
| jump across, with a run-up | ~1.3 (from standing much less) |
|
||||
| easy parkour | gaps 0.3 with +0.2 height, platforms ≥ 1.5 long |
|
||||
| steps without jumping | none – use slopes or walls without collision |
|
||||
| draw distance | the fog end of the zone: 8–16 cells in the originals |
|
||||
| objects visible | within ~12.5 units of the llama |
|
||||
|
||||

|
||||
|
||||
## Rules that keep the level working
|
||||
|
||||
1. **One player start**, with nothing solid right behind the llama (start camera).
|
||||
2. **Exit = LevelDoor + floor with collision material 16 in the same cell.** Target (1, 9,
|
||||
chapter) in TestLevel – never (7, 7, 7).
|
||||
3. **A death floor or death plane** under every place the llama can fall to.
|
||||
4. **A checkpoint** before every hard part.
|
||||
5. **TestLevel chapter 1 or 32–49** for new levels.
|
||||
6. **Guards and box walkers need their waypoint boxes** (and guards an event bit someone sets),
|
||||
or the game removes them.
|
||||
7. **Every area must be visible from where the camera is**: with visibility regions, check the
|
||||
masks (*Check for problems* reports regions nothing can see).
|
||||
8. **Stay within the budget**: 16 MB level memory, 127 texture pages, 128 sounds – the import
|
||||
preview and *Check for problems* show where you are.
|
||||
|
||||
The editor and both level builders enforce the data-level rules (tiles inside the grid, vertex
|
||||
colours, type fields, collision cells) automatically. *Check for problems* reports the rest.
|
||||
|
||||
## Good habits
|
||||
|
||||
* **Play-test often** (F5). The engine is unforgiving and a small change can stop the exit or
|
||||
hide an area.
|
||||
* **Look at the originals.** Open an original level in the editor and see how it builds doors,
|
||||
cameras and enemy paths; the [debug view mod](../tools.md#debug-view) shows collision and
|
||||
invisible objects in the running game.
|
||||
* **Keep levels compact.** Short sight lines hide the draw distance and keep the frame rate up.
|
||||
* **Copy numbers** (speeds, parameters, waypoint fields) from original objects that already do
|
||||
what you want.
|
||||
@@ -0,0 +1,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).
|
||||
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 301 KiB |
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||

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

|
||||
|
||||
## 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).
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
After Width: | Height: | Size: 43 KiB |
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Reference: game files and engine
|
||||
|
||||
Everything we know about the data of *The Emperor's New Groove* (PC, 2000; German: *Ein Königreich
|
||||
für ein Lama*), found by reverse engineering `groove.exe` and checking every claim against the
|
||||
level files on the CD and in the running game. The game runs on Argonaut's **Croc 2 engine** (a PC
|
||||
port of a PlayStation design), which explains most of its quirks.
|
||||
|
||||
Confidence is marked where it matters: **certain** = verified in code and data (byte-exact round
|
||||
trip or seen in game), **likely** = consistent with the data, **unknown** = open question.
|
||||
|
||||
| Page | What it covers |
|
||||
|---|---|
|
||||
| [Game files](game-files.md) | CD and HD folders, level naming scheme, worlds and chapters, loading pictures (COR), movies, demos, save games (KSV) |
|
||||
| [WAD container](wad.md) | the chunk list of a level file and which chunk does what |
|
||||
| [Geometry and collision](geometry.md) | TRAK tile models, the MAP tile grid, coordinate spaces, collision polygons and materials, vertex colours, visibility regions |
|
||||
| [Objects, waypoints, zones](objects.md) | MAP strat records, parameters, waypoints, environment zones (fog, music, teleports), lights, the player start, checkpoints and the exit |
|
||||
| [Textures](textures.md) | TEXT chunk: pages, pixel format, texture rectangles, texture animations |
|
||||
| [Music and sound](audio.md) | ASF music, the level sound bank, speech, what is still unknown |
|
||||
| [TestLevel (world 9)](testlevel.md) | the hidden world all custom levels live in, and which chapters are safe |
|
||||
| [Engine facts and limits](engine-limits.md) | scale, jump distances, draw distance, memory budget, things that crash the game |
|
||||
|
||||
How objects *behave* (the strat bytecode) has its own section: [Strats](../strats/README.md).
|
||||
|
||||
## Where the details live
|
||||
|
||||
These pages are the readable summary. The full lab notes – with every address in `groove.exe`,
|
||||
open questions and the history of each finding – are in [`notes/`](../../notes/README.md):
|
||||
|
||||
| Topic | Lab notes |
|
||||
|---|---|
|
||||
| geometry, collision, coordinate spaces | [notes/geometry.md](../../notes/geometry.md) |
|
||||
| MAP layout, strat records, other chunks | [notes/objects.md](../../notes/objects.md) |
|
||||
| textures | [notes/textures.md](../../notes/textures.md) |
|
||||
| every object type in every level | [notes/strat_types_table.md](../../notes/strat_types_table.md) |
|
||||
| object parameters | [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md) |
|
||||
| world 9 chapters | [notes/testlevel-chapters.md](../../notes/testlevel-chapters.md) |
|
||||
| level building, physics measurements, the exit | [notes/levelbau.md](../../notes/levelbau.md) |
|
||||
| fog, lights, regions | [notes/editor-tools-environment.md](../../notes/editor-tools-environment.md), [notes/mod-fog.md](../../notes/mod-fog.md) |
|
||||
| importing objects across levels | [notes/editor-cross-import.md](../../notes/editor-cross-import.md) |
|
||||
|
||||
## Code that reads and writes these formats
|
||||
|
||||
Every parser rebuilds all original files byte for byte, so the code is a reliable executable
|
||||
specification.
|
||||
|
||||
| Format | Java (LlamaMod) | Python (kit) |
|
||||
|---|---|---|
|
||||
| WAD container | [`format/Wad.java`](../../src/main/java/com/eliasmueller/llamamod/format/Wad.java) | [`tools/wadtool.py`](../../tools/wadtool.py) |
|
||||
| TRAK / STPC models, collision | [`format/Trak.java`](../../src/main/java/com/eliasmueller/llamamod/format/Trak.java), [`Model.java`](../../src/main/java/com/eliasmueller/llamamod/format/Model.java), [`Collision.java`](../../src/main/java/com/eliasmueller/llamamod/format/Collision.java), [`Stpc.java`](../../src/main/java/com/eliasmueller/llamamod/format/Stpc.java) | [`tools/geomlib.py`](../../tools/geomlib.py) |
|
||||
| MAP | [`format/MapChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/MapChunk.java) | [`tools/mapchunk.py`](../../tools/mapchunk.py) |
|
||||
| TEXT | [`format/TextChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/TextChunk.java) | [`tools/textchunk.py`](../../tools/textchunk.py) |
|
||||
| whole level | [`format/Level.java`](../../src/main/java/com/eliasmueller/llamamod/format/Level.java) | – |
|
||||
| strat code | [`format/strat/`](../../src/main/java/com/eliasmueller/llamamod/format/strat/), [`format/stpcmerge/`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/) | [`tools/strats.py`](../../tools/strats.py), [`tools/stpcwalk.py`](../../tools/stpcwalk.py) |
|
||||
| COR pictures | [`format/CorImage.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorImage.java), [`CorEncoder.java`](../../src/main/java/com/eliasmueller/llamamod/format/CorEncoder.java) | [`tools/cor2png.py`](../../tools/cor2png.py), [`tools/png2cor.py`](../../tools/png2cor.py) |
|
||||
| KSV saves | [`save/Ksv.java`](../../src/main/java/com/eliasmueller/llamamod/save/Ksv.java) | – |
|
||||
| ASF music | – | [`tools/asf2wav.py`](../../tools/asf2wav.py) (decode only) |
|
||||
@@ -0,0 +1,73 @@
|
||||
# Music and sound
|
||||
|
||||
Sound is the least explored part of the game. This page separates what is verified from what is
|
||||
only a guess. Custom music or sound effects are **not supported yet** by any LlamaMod tool.
|
||||
|
||||
## Music
|
||||
|
||||
| Fact | Status |
|
||||
|---|---|
|
||||
| Music tracks are files `music/<NAME>.asf` on the CD (33 files) | certain |
|
||||
| A level does not reference music files directly: **environment zones** carry a music id, and the start zone's id plays when the level begins | certain |
|
||||
| Music ids map to names through a table in `groove.exe` at 0x578960 (20 bytes per entry, name pointer at +8), played by `FUN_00546b70`; id 0 stops the music | certain |
|
||||
| ASF here is Argonaut's own format (not Microsoft ASF): 4-bit ADPCM, 44.1 kHz stereo, 17-byte blocks, decoded by `asfcodec.adl` in the game folder | certain (decoder reimplemented) |
|
||||
| A second zone field (u16 at +0x56) is probably the ambience setting | likely |
|
||||
|
||||
The id table, as used by the editor's zone inspector
|
||||
([`EnvMath.MUSIC`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/environment/EnvMath.java)):
|
||||
|
||||
| Id | Track | Id | Track | Id | Track | Id | Track |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 0 | (stop) | 10 | GAMEOVER | 20 | SNKJUNG | 30 | TUNE1 |
|
||||
| 1 | VILLAGE | 11 | TALLY | 21 | FROG | 31 | CHACHA |
|
||||
| 2 | JUNGLE1 | 12 | TURRACE | 22 | KRONK | 32 | JJUNGLE2 |
|
||||
| 3 | RIVER | 13 | PACHASNK | 23 | KIDLLAMA | 33 | TUNE9 |
|
||||
| 4 | JUNGLE2 | 14 | MOUNT2 | 24 | TUNE8 | 34 | TUNE6 |
|
||||
| 5 | MOUNTAIN | 15 | BALLOON | 25 | ROLLER | 35 | VICTORY |
|
||||
| 6 | CITY | 16 | JUNGSPID | 26 | BUNNY | 36 | PUZZLE2 |
|
||||
| 7 | CATACOMB | 17 | JAGCHASE | 27 | BLOCK | 37 | PLATFORM |
|
||||
| 8 | LAB | 18 | RIVER | 28 | TUNE5 | 38 | (none) |
|
||||
| 9 | TITLE | 19 | RIVRACE | 29 | CITYPUZZ | 39 | ENDGAME |
|
||||
|
||||
Four names (GAMEOVER, TALLY, PUZZLE2, PLATFORM) have no `.asf` file on the German CD; what the
|
||||
game plays for them (probably nothing) was not checked. 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.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Engine facts and limits
|
||||
|
||||
Numbers to design levels with, and the things that crash the game. Measured in the game with the
|
||||
test harness unless noted.
|
||||
|
||||
## Scale and movement
|
||||
|
||||
One unit = one grid cell. Measured with the jump lab ([`levels/sprunglabor.py`](../../levels/sprunglabor.py))
|
||||
and a coin measuring track ([notes/levelbau.md](../../notes/levelbau.md#findings-from-the-game-tests-lamatest-xvfb)):
|
||||
|
||||
| Quantity | Value |
|
||||
|---|---|
|
||||
| llama height | about 0.6 |
|
||||
| walking speed | about 1.1 units/s (≈ 1 s to accelerate) |
|
||||
| highest edge a jump clears | 0.35 (0.5 is too high) |
|
||||
| long jump with a run-up | about 1.3 units (from standing: much shorter, a 0.5 gap fails) |
|
||||
| comfortable parkour | gaps of 0.3 with +0.2 height, platforms ≥ 1.5 long |
|
||||
| step-up without jumping | none – any edge with wall collision blocks; use slopes or walls without collision |
|
||||
| frame rate of the logic | 30 frames per second (script speeds are per frame) |
|
||||
|
||||
Controls are **camera-relative**; the start camera sits behind the llama and moves aside if
|
||||
something solid is right behind it – keep the area behind start points clear.
|
||||
|
||||
## Drawing
|
||||
|
||||
| Fact | Value |
|
||||
|---|---|
|
||||
| draw distance | = **fog end** of the active environment zone; originals 7–16 cells at detail *Maximum* (roller coaster up to 23), about half at *Minimum* |
|
||||
| objects | drawn only within about **12.5 units of the llama** (they keep running); their activity radius also follows the fog end |
|
||||
| hidden areas | visibility regions: a cell is drawn only if the camera region's mask contains its region |
|
||||
| texture coordinates | none per vertex – every triangle shows half of a texture rectangle |
|
||||
| lights | light only object models; tiles use baked vertex colours |
|
||||
|
||||
The *Draw distance* mod scales all fog values of all levels
|
||||
([notes/mod-fog.md](../../notes/mod-fog.md)).
|
||||
|
||||
## Memory and counts
|
||||
|
||||
| Limit | Value | Source |
|
||||
|---|---|---|
|
||||
| level memory | **16 MB** for STPC, TEXT, TRAK, MAP, LGPC and the strat pool; originals use up to ~7 MB | `FUN_0041eea0` |
|
||||
| strat heap | 65533 words (≈ 256 KB) for strat slots (0x154 bytes each), collision blocks and variables | `FUN_0041ae20` |
|
||||
| strat slots | `poolExtra + 30` – must cover placed **and** spawned objects | MAP |
|
||||
| vertices per tile model | 8000 (transform buffer) | |
|
||||
| collision polygon | within ±8 units of its tile's origin | i16 fixed point |
|
||||
| texture pages | 127 | signed page byte |
|
||||
| texture entries | 65535 | u16 face field |
|
||||
| sound samples | 128 per level | signed char id |
|
||||
| file sizes that worked | TRAK up to 1 MB, 5800 tiles, WAD up to ~7 MB (originals); the test world: 628 models, 2474 tiles, 6.3 MB | measured |
|
||||
|
||||
## Things that crash the game
|
||||
|
||||
| Cause | Symptom |
|
||||
|---|---|
|
||||
| a tile outside the W × H grid | memory corruption, crash on load or later |
|
||||
| vertex-colour block not exactly one entry per vertex | objects read shifted, crash |
|
||||
| a strat type with collision but type flags 0 (or too few collision-bone words) | page fault in the heap allocator `FUN_0041ae20` |
|
||||
| exit target (7, 7, 7) in world 9 | crash after saving (world 10 has no name) |
|
||||
| a selectable chapter without a WAD | "Couldnt load wad" on the world map |
|
||||
| a waypoint-dependent type without its boxes | not a crash: the game removes the object ("removing strat") |
|
||||
| world-9 chapters 2–31 (unpatched exe) | progress, language or options overwritten ([TestLevel](testlevel.md)) |
|
||||
|
||||
## Coordinates, numbers, angles
|
||||
|
||||
* Game space: x east, y up, z = grid row; strats use 20.12 fixed point (4096 = 1.0).
|
||||
* MAP angles: 4096 = 360°; strat angles: 0x1000000 = 360°.
|
||||
* Objects on a flat original floor tile stand at `tile.y + 0.25`.
|
||||
@@ -0,0 +1,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)).
|
||||
@@ -0,0 +1,155 @@
|
||||
# Geometry and collision
|
||||
|
||||
The world is **not** one big mesh and there is no BSP. It is a **grid of tiles**, inherited from
|
||||
Croc: TRAK holds a set of small models (the level's construction kit), and MAP places them on a
|
||||
W × H grid, one cell = one unit. Every byte described here is reproduced exactly by
|
||||
[`format/Trak.java`](../../src/main/java/com/eliasmueller/llamamod/format/Trak.java),
|
||||
[`format/MapChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/MapChunk.java) and
|
||||
[`tools/geomlib.py`](../../tools/geomlib.py) for all original levels.
|
||||
|
||||
Full detail with all loader addresses: [notes/geometry.md](../../notes/geometry.md).
|
||||
|
||||

|
||||
|
||||
## Coordinate spaces
|
||||
|
||||
| Space | Used by | Axes |
|
||||
|---|---|---|
|
||||
| **Game space** | strats, collision, MAP fixed-point tile positions, lights | x east, **y up**, z = grid row (positive); 20.12 fixed point (4096 = 1.0); right-handed as the game shows it |
|
||||
| **Export space** | TRAK/STPC vertices, MAP float tile positions | same, but **z negated** (grid at z ∈ (−H, 0]); drawn by a Direct3D (left-handed) camera |
|
||||
| **File space** | OBJ/glTF of every LlamaMod and kit tool | = game space (y up, right-handed): Blender shows a level the same way round as the game |
|
||||
|
||||
Converting export ↔ file space negates z and reverses the triangle corner order. Angles:
|
||||
4096 = 360° in MAP records; the strat VM uses 0x1000000 = 360°. A tile is turned only about y
|
||||
(0, 1024, 2048, 3072 in practice). Rotation 0 faces +z, 90° faces +x.
|
||||
|
||||
## TRAK: tile models (*certain*)
|
||||
|
||||
`u32 n`, then **all n headers** (0x84 bytes each), then the data of every model in order.
|
||||
Typical levels have 100–650 models with up to 422 vertices / 798 faces each.
|
||||
|
||||
| Header field | Meaning |
|
||||
|---|---|
|
||||
| 0x00 | bounding-box centre (3 × f32) |
|
||||
| 0x0c | 8 box corners (used for frustum culling) |
|
||||
| 0x6c / 0x6e | vertex count / face count |
|
||||
| 0x78 / 0x7a / 0x7c | collision polygon counts in three groups (the engine always uses the sum; meaning of the groups *unknown*) |
|
||||
| pointers | 0 in the file, set on load |
|
||||
|
||||
Data per model:
|
||||
|
||||
* **Vertex**, 24 bytes: `f32 x, y, z, nx, ny, nz` (lighting comes from the MAP vertex colours, not
|
||||
from the normals).
|
||||
* **Face**, 28 bytes, triangles only: `u16 flags, u16 v0, v1, v2, u16 texture, u16 0, f32 plane
|
||||
nx, ny, nz, d`.
|
||||
* **Collision polygon**, 32 bytes: `u8 type, u8 material`, then 5 planes `{i8 nx, ny, nz, u8 0,
|
||||
i16 d}`. Plane 0 is the surface, planes 1–4 the edges (normals point inwards). Type bit 0 =
|
||||
quad, otherwise triangle. Normals are ×127, d is 20.12 fixed point – so a collision polygon
|
||||
must lie within **±8 units of its tile's origin**.
|
||||
|
||||
### Face flags
|
||||
|
||||
| Bit | Meaning |
|
||||
|---|---|
|
||||
| 0x0008 | start of a new render batch (texture page or blend mode changes) – must be set on the first face of each batch, at the latest every 256 faces |
|
||||
| 0x0010, 0x0020, 0x0800 | which corners of the texture rectangle the triangle uses (below) |
|
||||
| 0x0400 | double-sided (no backface culling) |
|
||||
| 0x4000 | ignore vertex colours |
|
||||
| 0x0001, 0x0002 | special sorting/transparency modes (*likely*) |
|
||||
| others | present in the data, not read by the PC renderer (*unknown*, probably PlayStation leftovers) |
|
||||
|
||||
### No free texture coordinates
|
||||
|
||||
The engine has **no per-vertex UVs**. Every triangle shows one half of a texture rectangle from the
|
||||
TEXT table: with flag 0x800 the face is the first or (with 0x10) second half of a quad, without it
|
||||
a triangle covering the lower-left half; 0x20 mirrors horizontally. 96 % of world faces are quad
|
||||
halves. This is why the level builder and editor split surfaces into 1 × 1 pieces and why a narrow
|
||||
wall shows a squashed texture. See [Textures](textures.md).
|
||||
|
||||
## MAP: the tile grid (*certain*)
|
||||
|
||||
MAP starts with the grid and the tiles; objects follow (see [Objects](objects.md)). The blocks that
|
||||
concern geometry:
|
||||
|
||||
| Block | Format |
|
||||
|---|---|
|
||||
| header | `u32 nTiles, u32 W, u32 H` |
|
||||
| tiles (floats) | nTiles × `{f32 x, y, z, u32 rotY, u32 model, u32 hasAlt}` in export space; x, z = cell centre |
|
||||
| regions | 32 × `{u32 visibility mask, i32 value}`, then W·H × u32 region number per cell |
|
||||
| tiles (fixed point) | nTiles × `{rx, ry, rz, x, y, z}` 20.12 in game space – used for drawing and collision; must match the float copy exactly |
|
||||
| model index | nTiles × u32 (= the float block's `model`) |
|
||||
| tile swaps (WFPC 0x10000) | one `{cellX, y, cellZ, modelA, modelB}` per tile with `hasAlt ≠ 0`: doors and bridges that change their model (strat opcodes 0x11a / 0x11b) |
|
||||
| vertex colours | per tile: one u32 RGBA per vertex of its model (0x80 = 1.0, the game doubles and clamps); more sets for animated colours; then the same for the swap model |
|
||||
|
||||
Rules that crash the game or corrupt the level when broken:
|
||||
|
||||
* every tile must lie inside the grid (`0 ≤ x < W`, `0 ≤ row < H`) – outside, the cell list
|
||||
writes into foreign memory;
|
||||
* the vertex-colour block must have **exactly** one entry per vertex of the tile's model – else
|
||||
everything after it (the objects) is read shifted;
|
||||
* the float and fixed-point copies and the model index must agree;
|
||||
* tile swaps: one entry per tile with `hasAlt ≠ 0`, in tile order.
|
||||
|
||||
The cell of a tile is `floor(x) + floor(−z) · W` (export-space floats).
|
||||
|
||||
## Collision (*certain*)
|
||||
|
||||
* Only **the tiles listed in the cell the player stands in** are tested (`FUN_004036d0` floors and
|
||||
ceilings, `FUN_00403ad0` walls). A wall polygon that sticks into the neighbouring cell has no
|
||||
effect there – builders enter a polygon into every cell it covers, as a collision-only tile if
|
||||
needed.
|
||||
* Collision is in game space, relative to the tile's fixed-point position.
|
||||
* There is **no step-up logic**: a 0.12-unit edge with a wall polygon already blocks the llama.
|
||||
Low plinths need walls without collision; stairs need an invisible slope.
|
||||
|
||||
### Collision materials
|
||||
|
||||
The material byte ends up in the player's ground state and is read by strats (`floorMaterial`).
|
||||
|
||||
| Material | Meaning |
|
||||
|---|---|
|
||||
| 0 | normal ground |
|
||||
| **16** | **exit floor**: the LevelDoor ends the level only when the llama stands on material 16 in the door's cell; also used under the village house doors (zone teleports) |
|
||||
| 17, 18 | ignored when WFPC flag 0x8000000 is set (*likely*: special surfaces) |
|
||||
| **30** (0x1e) | death plane / water (*likely*): falling onto it empties the energy and the llama respawns at the last checkpoint or the start (Model 0 of many levels is such an invisible plane at y = −2.75) |
|
||||
| others | special surfaces, meaning *unknown* – the debug view colours them |
|
||||
|
||||

|
||||
|
||||
## Visibility regions (no BSP)
|
||||
|
||||
Drawing (`FUN_0042bf40`) walks the grid cells within the draw distance around the camera and draws
|
||||
a cell's tiles **only if the cell's region bit is set in the mask of the camera's region**. Up to 32
|
||||
regions; mask 0 or a camera outside the grid means "everything". New geometry must therefore get a
|
||||
region that the surrounding regions can see, or it stays invisible – the builders put everything
|
||||
into region 0 with mask `0xffffffff`. Objects in hidden cells are skipped too (unless strat flag
|
||||
0x800). The editor's *Visibility regions* tool edits this
|
||||
([manual chapter 13](../manual/13-visibility.md)).
|
||||
|
||||
## STPC: object models and code
|
||||
|
||||
STPC starts with the **object models** (same model format as TRAK, header 0x8c bytes, optionally
|
||||
skinned with a bone table), then the **animation sets** (per-frame bone matrices and compressed
|
||||
vertex deltas – the compression is only partly understood), then the **strat code units**
|
||||
([Strats](../strats/README.md)). The engine never parses the code; MAP records and bytecode refer
|
||||
into STPC by absolute byte offsets, so STPC can only be extended at the end or merged with
|
||||
relocation ([notes/editor-cross-import.md](../../notes/editor-cross-import.md)).
|
||||
|
||||
## WFPC flags
|
||||
|
||||
WFPC is a u32 of feature flags. Some change the data layout and must stay as they are: 0x10,
|
||||
0x100, 0x10000 (tile swaps), 0x100000, 0x200000, 0x4000000, 0x10000000. Others: 0x800 renderer
|
||||
fog flag, 0x80000 read by a strat opcode, 0x8000000 makes collision materials 17/18 ignored.
|
||||
|
||||
## Writing new geometry – checklist
|
||||
|
||||
1. TRAK: correct counts, bounding box and face planes recomputed, texture index below the
|
||||
number of texture entries, batch bit 0x8, ≤ 8000 vertices per model.
|
||||
2. Collision polygons within ±8 units of the tile origin, entered into every cell they cover.
|
||||
3. MAP: tiles inside the grid, float/fixed/model blocks consistent, vertex colours exactly per
|
||||
vertex, tile-swap block matching `hasAlt`, a visible region.
|
||||
4. Rewrite the WAD's total length.
|
||||
|
||||
The editor and both level builders do all of this; the details of the builder's approach
|
||||
(splitting into tiles per cell and height band, merging identical tiles, baking sun, sky and
|
||||
ambient occlusion into vertex colours) are in [notes/levelbau.md](../../notes/levelbau.md).
|
||||
@@ -0,0 +1,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)) |
|
||||
@@ -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*".
|
||||
@@ -0,0 +1,53 @@
|
||||
# Textures (TEXT chunk)
|
||||
|
||||
Each level carries its own textures. Parse and rebuild are byte-identical for all level WADs
|
||||
(including the RLE encoder). Code: [`format/TextChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/TextChunk.java),
|
||||
[`tools/textchunk.py`](../../tools/textchunk.py). Lab notes: [notes/textures.md](../../notes/textures.md),
|
||||
editor side: [notes/editor-tools-textures.md](../../notes/editor-tools-textures.md).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
u32 nPal 0 in all WADs (8-bit palettes are unused)
|
||||
u32 nPages 13–22 per level, all 256×256
|
||||
nPages × { u32 flags, u32 w, u32 h, data }
|
||||
flags & 0x80 : u32 length + RLE16 (all original pages)
|
||||
other ≠ 0 : w·h raw u16
|
||||
u32 nTex ~180–1260 texture entries
|
||||
nTex × { u16 flags, u8 page, u8 clut (0xff), u8 x0, x1, y0, y1 } (edges inclusive)
|
||||
u32 nAnim texture animations
|
||||
nAnim × { u16 texIndex, u8 type, payload } type 8 = flip book: list of texture indices
|
||||
```
|
||||
|
||||
RLE16: a sequence of s16 *n*; *n* > 0 → *n* literal pixels follow, *n* < 0 → the next pixel
|
||||
repeated −*n* times.
|
||||
|
||||
## Pixels
|
||||
|
||||
16 bit in **PlayStation order**: bits 0–4 red, 5–9 green, 10–14 blue, bit 15 = STP
|
||||
(semi-transparency). `0x0000` is fully transparent. Export to PNG uses alpha 0 = `0x0000`,
|
||||
alpha 128 = STP 0, alpha 255 = STP 1. The page flags (low 7 bits) select the blend class:
|
||||
1/6 opaque, 4/7 alpha blend, 5 another blend mode, 2/3 special converters (*likely* the
|
||||
PlayStation semi-transparency modes; details open).
|
||||
|
||||
## Texture entries
|
||||
|
||||
A face never has its own UVs: it names a **texture entry**, a rectangle on a page, and shows half
|
||||
of it ([Geometry](geometry.md#no-free-texture-coordinates)). Entry flag bit 0 = flat colour (the
|
||||
rectangle bytes are R, G, B instead) – the level builder appends such entries for plain colours.
|
||||
Other entry flag bits (blend mode, size class) are only partly understood.
|
||||
|
||||
Limits: at most **127 pages** per level (signed page byte) and 65535 entries (u16 face field).
|
||||
|
||||
## Changing textures
|
||||
|
||||
* **Replace pixels, keep the table.** Exporting pages to PNG and importing them back changes no
|
||||
index anywhere: `tools/text_export.py export level.wad dir` / `import level.wad dir new.wad`.
|
||||
* **Add pages and entries.** New pages and entries are appended; faces then use the new indices.
|
||||
The editor's texture manager does this, including colour reduction, painting onto faces and
|
||||
flip-book animations ([manual chapter 8](../manual/08-textures.md)).
|
||||
* **Same name, same picture.** Texture *indices* differ between base levels. The builders resolve
|
||||
texture names (`grass`, `stone`, …) by picture fingerprint on each base and copy missing
|
||||
village pictures over ([notes/levelbau.md](../../notes/levelbau.md)).
|
||||
|
||||

|
||||
@@ -0,0 +1,63 @@
|
||||
# WAD container
|
||||
|
||||
A level is one file `wads/t1l<world>m<chapter>.wad`. It is a flat list of chunks:
|
||||
|
||||
```
|
||||
u32 total length
|
||||
repeat: u32 tag (four ASCII letters stored reversed: "OFNI" in the file = INFO)
|
||||
u32 length
|
||||
length bytes of chunk data
|
||||
```
|
||||
|
||||
There are no checksums. When a chunk changes size, only the chunk length and the total length
|
||||
must be rewritten ([`format/Wad.java`](../../src/main/java/com/eliasmueller/llamamod/format/Wad.java),
|
||||
[`tools/wadtool.py`](../../tools/wadtool.py) – `wadtool.py list|extract|replace`).
|
||||
|
||||
## Chunks
|
||||
|
||||
In file order. "Loader" is the function in `groove.exe` that reads the chunk (dispatcher at
|
||||
0x558960).
|
||||
|
||||
| Chunk | Contents | Documented in |
|
||||
|---|---|---|
|
||||
| INFO, VERS | u32 1 each, ignored | – |
|
||||
| WFPC | u32 feature flags of the level; they switch optional blocks in MAP and STPC on and off – never change them | [Geometry](geometry.md#wfpc-flags) |
|
||||
| TEXT | texture pages, texture rectangles, texture animations | [Textures](textures.md) |
|
||||
| FONT | 256 × 4 u16 glyph rectangles | – |
|
||||
| SPRT | sprite list (u32, more with WFPC 0x100000) | – |
|
||||
| NAME | u32 + level name (not shown anywhere in the game) | – |
|
||||
| SMPC | sound samples of the level (`cvg ` blocks) | [Music and sound](audio.md) |
|
||||
| AMPC | ambient sound data (`ambi`/`VABp`) | [Music and sound](audio.md) |
|
||||
| SRPC | references into the speech file `music/<lang>.cvs` | [Music and sound](audio.md) |
|
||||
| **TRAK** | world geometry: the tile models (render triangles + collision polygons) | [Geometry](geometry.md) |
|
||||
| **STPC** | object models, skeletal animations and the **strat code** (object behaviour) | [Geometry](geometry.md#stpc-object-models-and-code), [Strats](../strats/README.md) |
|
||||
| **MAP** | the tile grid, vertex colours, visibility regions, objects, waypoints, environment zones | [Geometry](geometry.md), [Objects](objects.md) |
|
||||
| LGHT | lights (directional and point lights for object models) | [Objects](objects.md#lights) |
|
||||
| LNFO | u32 coins, u32 secrets: the totals shown on the tally screen | [TestLevel](testlevel.md#tally-totals) |
|
||||
| LGPC | dialogue texts in all languages + speech slots | – |
|
||||
| END | end marker | – |
|
||||
|
||||
Not every level has every chunk. The three chunks that matter for level building are **TRAK**,
|
||||
**STPC** and **MAP**; a new level takes everything else (textures, sounds, sky, fog, music) from an
|
||||
original **base level**.
|
||||
|
||||
## The base-level idea
|
||||
|
||||
An object type exists in a level only if its code is in that level's STPC. MAP refers to a type
|
||||
by its **byte offset into STPC** – there is no global type id. So every custom level starts as a
|
||||
copy of an original level (default: Village 1, `t1l1m001`) and replaces geometry and objects:
|
||||
|
||||
* Python [`tools/levelbuild.py`](../../tools/levelbuild.py) and Java
|
||||
[`levelbuild/LevelBuilder.java`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java)
|
||||
generate TRAK and MAP from a script and keep STPC, TEXT and sound of the base.
|
||||
* The level editor stores only the difference to the base level and rebuilds the WAD from the
|
||||
user's own CD ([notes/editor-format.md](../../notes/editor-format.md)).
|
||||
* Object types of *other* levels can be merged into the base's STPC together with their models,
|
||||
animations, textures and sounds – the editor's *All worlds* import and the arena use this
|
||||
([notes/editor-cross-import.md](../../notes/editor-cross-import.md)).
|
||||
|
||||
## Level memory
|
||||
|
||||
The game loads STPC, TEXT (compressed pages + 20 bytes per texture entry), TRAK, MAP, LGPC and the
|
||||
strat pool into one **16 MB** block (`FUN_0041eea0`); original levels use up to about 7 MB. See
|
||||
[Engine facts and limits](engine-limits.md).
|
||||
@@ -0,0 +1,120 @@
|
||||
# Strats: how objects work
|
||||
|
||||
Every object in the game – a coin, a squirrel, a moving platform, a camera, the llama itself – is
|
||||
driven by a small program called a **strat** (Argonaut's "strategy" scripts, inherited from Croc).
|
||||
The programs are compiled bytecode for a virtual machine in `groove.exe` (`FUN_0054d180`). LlamaMod
|
||||
can read every one of them, change their numbers, write new ones and move them between levels.
|
||||
|
||||
| Page | For |
|
||||
|---|---|
|
||||
| this page | how the VM works: units, instances, frames, variables, events, spawning, collision |
|
||||
| [Reading scripts](reading-scripts.md) | the editor's Script view and the assembly listing, with a worked example |
|
||||
| [The script language](script-language.md) | writing new behaviour in LlamaMod's structured language |
|
||||
| [Customising strats](custom-types.md) | changing constants, cloning types, custom types in the editor and in mods |
|
||||
| [Example: the Alligator Arena](arena.md) | a complete minigame written in the script language, explained step by step |
|
||||
| [Example: the roller coaster](roller-coaster.md) | 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.
|
||||
@@ -0,0 +1,240 @@
|
||||
# Example: the Alligator Arena
|
||||
|
||||
The *Alligator Arena* mod is a complete minigame built from scratch: a walled pit on an island,
|
||||
alligators coming in waves, coins popping out on every hit, an exit that opens after the last wave.
|
||||
It uses every technique of this documentation – a scripted level, two new strat types written in
|
||||
the [script language](script-language.md), and an object (the Alligator) imported from another
|
||||
world. This page walks through the code.
|
||||
|
||||

|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| [`ArenaMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java) | the mod: options, writes the WAD and the loading picture |
|
||||
| [`Arena.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/Arena.java) | the level script: geometry and object placement |
|
||||
| [`ArenaScripts.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaScripts.java) | the strat scripts `ArenaMaster`, `ArenaGator` (and a test probe) |
|
||||
| [`ArenaImport.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaImport.java) | compiles the scripts and merges them into the base level |
|
||||
| [`ArenaTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/arena/ArenaTest.java) | unit tests (scripts compile for all option corners, the built level decodes) |
|
||||
| [notes/mod-arena.md](../../notes/mod-arena.md) | lab notes: engine facts found, what was verified in the game |
|
||||
|
||||
Options: alligator **lives** (1–10, default 5), **waves** (1–5, default 3; wave *n* brings
|
||||
min(*n*, 2) alligators), llama **energy** cap (1–5, default 4), **coins** per hit (0–5, default 1),
|
||||
TestLevel **chapter** (32–49, default 49).
|
||||
|
||||
## 1. The plan
|
||||
|
||||
* The **base level** is Village 1 (`t1l1m001`), like every new level: its code provides the
|
||||
Llama, Coin, CheckPointPost and textures.
|
||||
* The Alligator lives in **Jungle by Day 2** (`t1l4m002`). Its model, animations and sounds are
|
||||
needed, so the new scripts are **compiled against that level** and then **imported** into the
|
||||
village base together with everything they reference.
|
||||
* Two new types: `ArenaMaster`, an invisible referee standing on the exit pad, and `ArenaGator`,
|
||||
a new alligator spawned by the master.
|
||||
* The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)).
|
||||
|
||||
## 2. The level (`Arena.build`)
|
||||
|
||||
The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java)
|
||||
for the Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java),
|
||||
the same builder the Test World uses. Coordinates are game units (one cell each):
|
||||
|
||||
```java
|
||||
l.sea(0, 0, 40, 40, -1.5, new double[] {ix0 - 3, iz0 - 3, ix1 + 3, iz1 + 3}); // sea + death plane
|
||||
l.floor(a.x0(), a.z0(), a.x1(), a.z1(), a.floor(), "sand"); // pit floor 16..24
|
||||
l.box(a.x0() - t, a.z0() - t, a.x1() + t, a.z0(), 0, WALL,
|
||||
o().top("stone").side("stone_moss")); // one of four walls
|
||||
// exit pad: one cell with collision material 16 under a gate
|
||||
l.box(ex - 0.5, ez - 0.5, ex + 0.5, ez + 0.5, a.floor(), a.floor() + 0.06,
|
||||
o().top("pattern_purple").side("inca").rot(0).sideColl(false).mat(DOOR_MAT));
|
||||
l.player(st[0], st[1], st[2], null, 1.6); // llama + start camera node 1.6 behind it
|
||||
l.checkpoint(a.x0() + 0.8, a.z0() + 0.8);
|
||||
l.strat(ArenaScripts.MASTER, ex, ez, a.floor(), 0.0, null, LevelBuilder.Waypoints.NONE, 0, false);
|
||||
l.coins(List.of(new double[] {a.x0() + 0.6, 20}, new double[] {a.x1() - 0.6, 20}));
|
||||
```
|
||||
|
||||
Points to note:
|
||||
|
||||
* The **exit pad** is a 1 × 1 box whose top has collision **material 16** – the exit rule from
|
||||
[Objects](../reference/objects.md#the-exit-leveldoor--certain). There is no LevelDoor; the master
|
||||
takes its role.
|
||||
* The walls are 1.1 high – too high to jump (0.35), so the fight stays in the pit.
|
||||
* Texture names (`sand`, `stone_moss`, `inca`) are resolved on the base level by picture.
|
||||
|
||||
## 3. Compiling and importing (`ArenaImport`)
|
||||
|
||||
`Arena.buildWad` runs before the geometry is built:
|
||||
|
||||
```java
|
||||
merged = ArenaImport.merge(baseWad, src, "t1l4m002", units(probe, debug));
|
||||
return LevelScript.super.buildWad(merged.wad(), wads, opts);
|
||||
```
|
||||
|
||||
1. **`append`** compiles each unit with `StratCompiler` → `StratAssembler`, laid out as if it were
|
||||
the last unit of **t1l4m002's** STPC, and appends it there. So `model:47` is the Alligator's model
|
||||
and `unit:56+0xf0` is that level's `EnemyCoin`. A later unit can name an earlier one as
|
||||
`unit:@ArenaGator`; `append` replaces that with `unit:N+offset`.
|
||||
2. **`merge`** asks [`StpcMerge`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/StpcMerge.java)
|
||||
– the editor's cross-world import engine – to import the two entry points into the village:
|
||||
it follows every reference and copies the model, 7 animation sets, textures, 3 sound samples and
|
||||
the spawned strats (`EnemyCoin` and `Sparkles` already exist in the village and are shared),
|
||||
relocates every operand and verifies the result.
|
||||
3. It adds one **unplaced prototype MAP record** per type with its type fields
|
||||
(`nVars`, `slots`, `typeFlags`, `extra`), so the level builder can place the type by name.
|
||||
|
||||
The type fields are declared in `Arena.units`:
|
||||
|
||||
| Type | vars | slots | type flags | extra | why |
|
||||
|---|---|---|---|---|---|
|
||||
| ArenaGator | 16 | 5 | 0xC000 | 3 | collision like the Alligator; 3 collision bones |
|
||||
| ArenaMaster | 12 | 0 | 0 | 0 | invisible logic, no collision, no handlers |
|
||||
|
||||
## 4. The referee: `ArenaMaster`
|
||||
|
||||
`ArenaScripts.master()` generates the script; the waves are unrolled by Java. With 2 waves it
|
||||
reads like this (simplified):
|
||||
|
||||
```
|
||||
strat ArenaMaster {
|
||||
alias wave = v1
|
||||
alias alive = v3
|
||||
alias kills = v4
|
||||
alias t = v5
|
||||
g104 = -1 // no exit cell yet (the llama's own exit check reads g104/g105)
|
||||
g105 = -1
|
||||
t = 60 // 2 seconds
|
||||
while (t > 0) { t -= 1; call cap; yield }
|
||||
|
||||
wave = 1
|
||||
self.x = 20.5 // move to a spawn spot …
|
||||
self.z = 22.2
|
||||
yield // … and let the engine update the matrix before spawning
|
||||
spawn<0, 16, 5, 50176, 3>(unit:@ArenaGator)
|
||||
alive += 1
|
||||
self.x = 20.5 // back onto the exit pad
|
||||
self.z = 23.5
|
||||
while (alive > 0) { call cap; yield } // gators do parent.v3 -= 1 when they die
|
||||
t = 45
|
||||
while (t > 0) { t -= 1; call cap; yield }
|
||||
wave = 2
|
||||
… // two spawns this time
|
||||
|
||||
g101 = 4096r // exit target (1, 9, chapter) as raw 20.12 values
|
||||
g102 = 36864r
|
||||
g103 = 200704r
|
||||
g104 = int(self.x) // the exit cell
|
||||
g105 = int(self.z)
|
||||
… // 5 bonus EnemyCoins
|
||||
loop {
|
||||
call cap
|
||||
if (player.floorMaterial == 16 && int(self.x) == int(player.x)
|
||||
&& int(self.z) == int(player.z)) {
|
||||
endLevel(4096r, 36864r, 200704r) // the LevelDoor's rule
|
||||
g1 += 1
|
||||
remove
|
||||
}
|
||||
… // a Sparkles effect every 8 frames
|
||||
yield
|
||||
}
|
||||
}
|
||||
|
||||
sub cap { // energy option: keep the llama's energy at the cap
|
||||
if (player.health2 > 4) { player.health2 = 4 }
|
||||
}
|
||||
```
|
||||
|
||||
What it shows:
|
||||
|
||||
* **Variables as state** – `alive` is `v3`, which the gators change from outside with
|
||||
`parent.v3 -= 1`: the parent/child link is the communication channel.
|
||||
* **Waiting = looping with yield**: every wait is a `while … yield` that also calls `cap`, so the
|
||||
energy cap holds every frame (also after a respawn).
|
||||
* **The spawn rule**: move, `yield`, then `spawn` – without the yield the alligator would appear at
|
||||
the master's old position (it copies the matrix of the last frame).
|
||||
* **The exit** is the LevelDoor logic written out: material 16 under the llama in the same cell →
|
||||
`endLevel(tribe, world, chapter)`. Setting g101–g105 additionally arms the Llama's built-in check
|
||||
for the same cell.
|
||||
|
||||
## 5. The alligator: `ArenaGator`
|
||||
|
||||
`ArenaScripts.gator()` reuses the original Alligator's assets but has its own behaviour. Its parts:
|
||||
|
||||
**Setup** – model, size, collision and how it takes hits:
|
||||
|
||||
```
|
||||
setModel(model:47)
|
||||
scale(1.5)
|
||||
self.hitDamage = 1793 // 1 | 256 | 512 | 1024
|
||||
collisionOn<16384>()
|
||||
collRadius(409r)
|
||||
collisionBone(0, 409r) // three collision spheres along the body
|
||||
collisionBone(6.0, 409r)
|
||||
collisionBone(10.0, 409r)
|
||||
on touched {
|
||||
if (self.damageTaken & 1792) { // kick, dive or charge of the llama
|
||||
if (cool <= 0 && lives > 0) { hit = 1 }
|
||||
} else {
|
||||
bit = 1 // contact without an attack: it bit the llama
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`hitDamage` 1793 shares a bit with all three llama attacks, so each of them counts as the llama's
|
||||
hit; bit 1 makes a plain contact hurt the llama ([Collision and damage](README.md#collision-and-damage)).
|
||||
The handler only sets flags – the main loop reacts to them, which keeps the handler short.
|
||||
|
||||
**Fade-in** – `setAmbient` from 0 to 63 over 32 frames, playing the idle animation.
|
||||
|
||||
**Main loop** – a state machine in a `switch`, once per frame:
|
||||
|
||||
| State | Behaviour |
|
||||
|---|---|
|
||||
| 0 watch | turn towards the player (2° per frame); after a timer, if the llama is within 5 cells and on the same floor height (±0.4), play the snap animation and sound |
|
||||
| 1 snap | wait for the animation, then start chasing |
|
||||
| 2 chase | accelerate up to `0.03 + 0.006 · wave` per frame towards the llama; lunge when closer than 1.2, give up after 75 frames |
|
||||
| 4, 5 lunge | fast forward move, then slow down (numbers from the original Alligator) |
|
||||
| 3 back off | move backwards for a few frames, then rest and watch again |
|
||||
|
||||
Before the `switch`, the flags from the handler are handled:
|
||||
|
||||
* `hit`: lose a life, sound, three green particle puffs, *coins* `EnemyCoin`s, blink for 45
|
||||
frames (alpha 0.25 and a red tint every 4 frames, no further hits meanwhile), back off.
|
||||
* `bit`: back off and rest 90 frames, so the llama gets a breather.
|
||||
|
||||
After the `switch` the gator is clamped to the pit and to the floor height.
|
||||
|
||||
**Death** – when `lives` reaches 0 the loop `break`s: collision off, the original's death (shrink
|
||||
over a few frames with puffs, five `EnemyCoin`s), then `parent.v3 -= 1` (one alive less for the
|
||||
master), `parent.v4 += 1` (kills) and `remove`.
|
||||
|
||||
**`sub aim`** – ported from the original Alligator: each frame it publishes its position in
|
||||
g17–g19 (and the distance in g20) if it is the best target below a jumping llama. That is what makes
|
||||
the llama's **dive** home in on it.
|
||||
|
||||
## 6. The mod (`ArenaMod`)
|
||||
|
||||
`apply` refuses a chapter another mod already wrote, builds the WAD, rewrites the tally total
|
||||
(`LevelTally.counted`: 2 placed coins + every coin the alligators can drop + 5 bonus), writes it
|
||||
to `wads/T1L9M0nn.wad` and draws a loading picture with Java2D (`CorEncoder`). That is all a level
|
||||
mod needs: LlamaMod's mod pipeline puts the files into the game's CD folder and links the empty
|
||||
TestLevel chapters.
|
||||
|
||||
## 7. How it was tested
|
||||
|
||||
* `ArenaTest` compiles the scripts for every corner of the options, checks wave sizes, builds the
|
||||
level from the CD and decodes the merged code (the master spawns the gator unit, the gator
|
||||
spawns EnemyCoin).
|
||||
* In the game ([`tools/apptest.py`](../../tools/apptest.py)): a probe type (`ArenaProbe`, option
|
||||
`probe=true`) proved that compiled `on touched` handlers fire; then fights with default options,
|
||||
a quick win with `lives=1, waves=1`, and a defeat (energy 0 → *Continue* restarts the arena).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## What to take from it
|
||||
|
||||
1. Borrow assets by **compiling against the level that has them** and importing with `StpcMerge`.
|
||||
2. Keep event handlers short; set flags and react in the main loop.
|
||||
3. Communicate through `parent.vN` and globals; spawn only after a `yield`.
|
||||
4. Copy numbers (speeds, animation indices, type fields) from the original type's script.
|
||||
5. Write the exit yourself if you need a condition – material 16 + same cell + `endLevel`.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Customising strats
|
||||
|
||||
From the smallest change to a completely new object type. Steps 1–5 need only the level editor;
|
||||
step 6 is for programmers writing a LlamaMod mod.
|
||||
|
||||
| Step | What changes | Where it is stored |
|
||||
|---|---|---|
|
||||
| 1. Object parameters | one placed object | the project's object list |
|
||||
| 2. Constants | a number in a type's code – every object of that type | `stratEdits.constants` |
|
||||
| 3. Import a type from another level | the level's code gets a new type with its models, sounds, texts | the project's import list |
|
||||
| 4. Clone a type | a new type as a copy of an existing one | `stratEdits.types` (source text) |
|
||||
| 5. New type from a script | a new type written from scratch | `stratEdits.types` |
|
||||
| 6. Script types in a mod | types compiled and merged by Java code | the mod's source |
|
||||
|
||||
A project never stores game code: constants are stored as *old → new value at an offset*, custom
|
||||
types as source text, imports as *type X from level Y*. Everything is rebuilt from the player's own
|
||||
CD when the project is opened ([notes/editor-format.md](../../notes/editor-format.md)).
|
||||
|
||||
## 1. Object parameters
|
||||
|
||||
Select an object; the Inspector's *Object parameters* section shows its MAP parameters (= the
|
||||
variables v0, v1, … of its strat). For many common types the editor knows names, ranges and meanings
|
||||
(schemas in [`Schemas.java`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/objects/Schemas.java));
|
||||
the rest are raw values with the values used in the original levels as a hint.
|
||||
User guide: [manual chapter 9](../manual/09-objects.md).
|
||||
|
||||
## 2. Constants
|
||||
|
||||
Double-click a line with a number in the Script panel (or *Edit value…*). The change applies to
|
||||
every object of the type, is undoable and turns the line orange. On load the old value is checked
|
||||
first, so a project made for another game version cannot corrupt the code silently. A
|
||||
`pushShort` holds only −8.0 … 7.99; larger values need a source edit.
|
||||
|
||||

|
||||
|
||||
## 3. Importing types from other levels
|
||||
|
||||
*Assets → All worlds* lists every object of every level; dragging one into the viewport imports it.
|
||||
The import copies the type's code **and everything it reaches**: spawned sub-strats (projectiles,
|
||||
shards, heads), models, animation sets, textures, sound samples and dialogue lines, relocates all
|
||||
references, shares what the level already has (Coin, EnemyCoin, Sparkles …) and verifies the
|
||||
merged code. Limits: 16 MB level memory, 128 sounds, 127 texture pages.
|
||||
Details: [notes/editor-cross-import.md](../../notes/editor-cross-import.md),
|
||||
[manual chapter 9](../manual/09-objects.md#importing-objects-from-other-worlds).
|
||||
|
||||
## 4. Cloning a type
|
||||
|
||||
*Clone as new type…* in the Script panel copies a type's listing into the type editor, renamed.
|
||||
Change what you need, *Check* (assemble and verify) and *Create type*. The new unit is appended at
|
||||
the end of the level's code – nothing else moves – and the type appears in *Assets → Objects*.
|
||||
|
||||
* **Type fields / parameters from** decides which existing type's MAP fields (variables, slots,
|
||||
collision flags, extra words) and parameter names new objects get. Pick the type you cloned,
|
||||
unless you changed how many variables or collision bones the code uses.
|
||||
* *Edit type…* and *Delete type…* work later on, with undo; placed objects follow their type.
|
||||
* Custom types can refer to each other by name (`unit:OtherType`).
|
||||
|
||||
The manual's tutorial *Make the squirrel run in a circle* clones Bucky and inserts a circle loop
|
||||
([manual chapter 22](../manual/22-strats.md#tutorial-make-the-squirrel-run-in-a-circle)).
|
||||
|
||||

|
||||
|
||||
## 5. A new type from a script
|
||||
|
||||
*Level → New object type from script…* opens the type editor with a template. Set *Language* to
|
||||
*Script* and write the behaviour in the [script language](script-language.md). Models, animations
|
||||
and sounds are those of the current level (`model:N`, `anim:N`, `sound:N` – look them up in the
|
||||
Script panel of a type that uses them). Choose *Type fields from* a type with similar needs (a
|
||||
pick-up: Coin; a walking enemy: one of the enemies).
|
||||
|
||||
## 6. Script types in a mod (Java)
|
||||
|
||||
A mod can do the same in code. The arena shows the pattern
|
||||
([`mods/arena/`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/)):
|
||||
|
||||
1. Write the types as script-language text (often with values filled in from options).
|
||||
2. **Compile against a source level** whose models, animations and sounds you want to use – the
|
||||
indices in the script are that level's:
|
||||
`StratAssembler.assemble(StratCompiler.compile(src), StratListing.layout(stpcImage, base))`
|
||||
and append the words as a new unit to the source level's STPC (`ArenaImport.append`).
|
||||
3. **Merge into the base level** with `StpcMerge` (the same engine as the editor's import): it
|
||||
copies the closure (models, animation sets, textures, sounds, spawned strats), relocates and
|
||||
verifies. Add one unplaced prototype MAP record per type with its type fields.
|
||||
4. **Build the level** with a `LevelScript` / `LevelBuilder`
|
||||
([`levelbuild/`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/)) and place the types by
|
||||
name with `l.strat(...)`.
|
||||
5. Write the WAD into a TestLevel chapter from the mod's `apply` and add a loading picture.
|
||||
|
||||
The [arena page](arena.md) walks through every part of this.
|
||||
|
||||
## Choosing type fields
|
||||
|
||||
| Field | What to put there |
|
||||
|---|---|
|
||||
| variables | at least the highest `vN` the code uses + 1 (parameters count as variables) |
|
||||
| slots | number of event handler slots (`on …` blocks) – at least 1 if there is one |
|
||||
| type flags | 0 for invisible logic; collision types need collision flags (Coin: 0x28000; Alligator: 0xC000 / 0xC400) |
|
||||
| extra | collision-bone words: at least the number of `collisionBone` calls |
|
||||
|
||||
Wrong values do not always fail at once: too small collision areas corrupt the strat heap and
|
||||
crash later (page fault in `FUN_0041ae20`).
|
||||
@@ -0,0 +1,114 @@
|
||||
# Reading scripts
|
||||
|
||||
LlamaMod shows every strat in two forms: the **Script view**, a readable reconstruction, and the
|
||||
**Assembly** listing, the exact instructions. Both are in the level editor's *Script* panel
|
||||
(**View → Panels → Script**, **Ctrl+Shift+K**); the user-level walkthrough is
|
||||
[manual chapter 22](../manual/22-strats.md).
|
||||
|
||||

|
||||
|
||||
Code: [`StratListing`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratListing.java)
|
||||
(assembly), [`StratPseudo`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratPseudo.java)
|
||||
(script view), [`OpTable`](../../src/main/java/com/eliasmueller/llamamod/format/strat/OpTable.java)
|
||||
(names and help texts).
|
||||
|
||||
## Example: the Coin
|
||||
|
||||
Script view of the village coin:
|
||||
|
||||
```
|
||||
strat Coin:
|
||||
setModel(model:5)
|
||||
collRadius(409r)
|
||||
onEvent ev_0000, 0 // event type 4 → L_0084
|
||||
if (v1) { collisionOn<32768>() ... }
|
||||
if (v0) {
|
||||
if (self.floorMaterial == -1.0) { skipArgs … } else {
|
||||
self.y = self.floorHeight + 204r
|
||||
self.rotY = rnd(4096.0)
|
||||
}
|
||||
}
|
||||
loop { turnLeft(5.625deg); yield }
|
||||
```
|
||||
|
||||
Line by line:
|
||||
|
||||
| Line | Meaning |
|
||||
|---|---|
|
||||
| `setModel(model:5)` | show model 5 of this level's STPC (the coin) |
|
||||
| `collRadius(409r)` | collision radius 409/4096 ≈ 0.1 cell |
|
||||
| `onEvent ev_0000, 0` | install the handler table entry `ev_0000` in slot 0: event type 4 (touched) jumps to `L_0084`, the pick-up code |
|
||||
| `if (v1) …` | parameter 1 switches collision on |
|
||||
| `if (v0) …` | parameter 0 (`snapToFloor` in the editor) puts the coin 204/4096 ≈ 0.05 above the floor under it and turns it randomly – unless there is no floor (`floorMaterial == -1`) |
|
||||
| `loop { turnLeft(5.625deg); yield }` | spin 5.625° per frame, forever |
|
||||
|
||||
Changing `204r` to `1.5` makes every snapping coin float 1.5 cells high – that is a constant edit
|
||||
([Customising strats](custom-types.md)).
|
||||
|
||||
## Script view conventions
|
||||
|
||||
| You see | It means |
|
||||
|---|---|
|
||||
| `self.x`, `player.y`, `parent.v3` | property or variable of this strat, the player, the spawner |
|
||||
| `v0`, `snapToFloor` | variable 0; named when the editor knows the type's parameter |
|
||||
| `g12` | global variable 12 |
|
||||
| `1.0`, `5.625deg`, `204r` | fixed point (1.0 = one cell), an angle, a raw integer (204/4096) |
|
||||
| `op<a, b>(x, y)` | instruction `op` with inline operands `a, b` and stack arguments `x, y` |
|
||||
| `loop { … yield }` | runs once per frame |
|
||||
| `while`, `do … while`, `switch`, `break`, `continue`, `break name` | rebuilt from the jumps |
|
||||
| `a && b`, `a \|\| b` | short-circuit conditions (chains of conditional jumps) |
|
||||
| `goto L_xxxx` | jumps that do not form nested blocks – 31 places in the whole game, all one pattern in the llama's script |
|
||||
|
||||
The script view is a **reading aid** and is not compiled back. Edits happen on the assembly (or
|
||||
in the script language for new types).
|
||||
|
||||
## The assembly listing
|
||||
|
||||
One instruction per line, exactly as stored:
|
||||
|
||||
```
|
||||
Coin:
|
||||
debugName "Coin"
|
||||
address model:5 ; model
|
||||
setModel
|
||||
onEvent ev_0000, 0 ; event type 4 → L_0084
|
||||
pushVar snapToFloor ; v0 (alias from the object schema)
|
||||
jz L_018c
|
||||
...
|
||||
L_0218:
|
||||
push 5.625deg ; = 64.0
|
||||
turnLeft
|
||||
jmpYield L_0218
|
||||
end
|
||||
```
|
||||
|
||||
* Short opcodes (below 0x45) carry a signed 16-bit operand: `pushVar v3`, `pushShort 1.5`,
|
||||
`jz label`. Long opcodes take inline words: `spawn 3, 6, 0, 0, 0`.
|
||||
* Directives: `.unit`, `.alias`, `.word`, `.string`, `.event`, `.switch` / `.case`, `.at`.
|
||||
* Disassemble → assemble is byte-identical for all 1461 units of the game; no instruction is
|
||||
emitted as raw words.
|
||||
|
||||
Full syntax: [notes/strat-language.md](../../notes/strat-language.md). Every opcode with its stack
|
||||
effect and meaning: [notes/strat-opcodes.md](../../notes/strat-opcodes.md).
|
||||
|
||||
## Finding things
|
||||
|
||||
* **Which script runs this object?** Select it with *Follow selection* on.
|
||||
* **Who spawns this type?** The side panel lists the units that reference it.
|
||||
* **What does this instruction do?** Select the line; the side panel shows the opcode's
|
||||
semantics, stack effect, Argonaut name and confidence.
|
||||
* **Where is this variable used?** *Uses* lists every line with the selected operand.
|
||||
* **Outside the editor:** [`tools/strats.py`](../../tools/strats.py) `level.wad --all` lists every
|
||||
named strat of a level with code offsets and type fields;
|
||||
[notes/strat_types_table.md](../../notes/strat_types_table.md) shows which level has which type.
|
||||
|
||||
## Typical patterns
|
||||
|
||||
| Pattern | Meaning |
|
||||
|---|---|
|
||||
| `v1 * 30` | seconds → frames |
|
||||
| `self.distToPlayerSqXZ < 25` | player within 5 cells (squared distance) |
|
||||
| `turnTowardAngleY(self.angleToPlayer, 2deg)` | turn towards the player, 2° per frame |
|
||||
| `if (!self.animPlaying) animPlay(anim:N)` | loop an animation |
|
||||
| `wpFirst()`, `wpNext()`, `self.wpParam` | walk the waypoint chain; per-node parameter |
|
||||
| `g95 = g95 \| (1 << (v - 1))` | set an event bit that guards wait for |
|
||||
@@ -0,0 +1,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.
|
||||
|
||||

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

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

|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,87 @@
|
||||
# The script language
|
||||
|
||||
LlamaMod's **script language** (version 2) is a small structured language in the style of the
|
||||
Script view. [`StratCompiler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratCompiler.java)
|
||||
compiles it to strat assembly, and
|
||||
[`StratAssembler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratAssembler.java)
|
||||
turns that into bytecode with the same checks as hand-written assembly. It is used by the editor's
|
||||
type editor (*Language: Script*) and by mods such as the [arena](arena.md). Complete reference:
|
||||
[notes/strat-language.md](../../notes/strat-language.md#script-language-stratcompiler--writing-new-behaviour);
|
||||
tests: `StratCompilerTest`.
|
||||
|
||||
## A first strat
|
||||
|
||||
```
|
||||
// bobs one cell up and down and turns
|
||||
strat Bobber {
|
||||
alias base = v0
|
||||
alias phase = v1
|
||||
setModel(model:5) // any opcode: name<inline operands>(stack arguments)
|
||||
base = self.y + 1 // 1 = one cell
|
||||
phase = 0
|
||||
on touched { // event handler (type 4), runs as a subroutine
|
||||
soundPlay(sound:6)
|
||||
}
|
||||
loop {
|
||||
phase = phase + 4deg
|
||||
self.y = base + sin(phase)
|
||||
turnLeft(4deg)
|
||||
yield // end of this frame
|
||||
}
|
||||
}
|
||||
|
||||
sub wobble { // called with "call wobble", ends with return
|
||||
self.rotZ = sin(self.rotY) * 0.1
|
||||
}
|
||||
```
|
||||
|
||||
## Building blocks
|
||||
|
||||
| Construct | Notes |
|
||||
|---|---|
|
||||
| `strat Name { … }` | a type; its label `Name` is the entry point |
|
||||
| `sub name { … }`, `call name`, `return` | subroutines |
|
||||
| `alias name = v3` | a name for a variable; parameter names from the object schema are predefined |
|
||||
| `x = e`, `x += e`, `-=`, `*=`, `/=` | assignment; `x += 1` compiles to `inc` |
|
||||
| places | `vN`, alias, `gN`, `self.prop`, `player.prop`, `parent.prop`, `parent.vN`, `camera.` / `target.` / `boss.` / `dialog.prop` |
|
||||
| `if (c) { } else if (c) { } else { }` | |
|
||||
| `while (c) { }`, `loop { }`, `do { } while (c)` | a `yield` as the last statement of a loop becomes the loop's frame-ending back jump |
|
||||
| `switch (e) { case 1: … break default: … }` | C rules (fall through without `break`); case values are constants |
|
||||
| `break`, `continue`, `yield`, `end`, `remove` | |
|
||||
| `on touched { … }`, `on event(type[, param]) [slot n] { … }` | event handlers; touched needs `collisionOn<…>()` and `collRadius(…)` |
|
||||
| `op<a, b>(e1, e2)` | **any opcode** by name: inline operands in `<>` (raw words), stack arguments in `()`; the count must match the opcode |
|
||||
|
||||
### Numbers
|
||||
|
||||
* `10` and `10.0` are ten cells (fixed point); `1.5`, `90deg` as in assembly.
|
||||
* **Raw** integers: `215r` (= 215/4096) or hex `0x10`. Values inside `<…>` are always raw.
|
||||
* Operators over constants are folded at compile time with the VM's own fixed-point rules:
|
||||
`2 * 3 + 0.5` → `6.5`.
|
||||
* `use rawNumbers` switches to the version-1 rule (plain integers raw) – old scripts get it
|
||||
automatically.
|
||||
|
||||
### Operators (low → high precedence)
|
||||
|
||||
`||` · `&&` · `|` · `&` · `==` `!=` · `<` `>` `<=` `>=` · `<<` `>>` · `+` `-` · `*` `/` · unary
|
||||
`-` `!`. In conditions `&&` / `||` short-circuit; in a value both sides are evaluated.
|
||||
|
||||
### References
|
||||
|
||||
`model:N`, `anim:N`, `unit:N[+off]` or `unit:Name` (another custom type), `sound:N`, `tex:N` –
|
||||
indices into the level the code is compiled against.
|
||||
|
||||
## What the compiler does not do (yet)
|
||||
|
||||
* no `for` loop, no typed variables (everything is a 32-bit word, fixed point by convention);
|
||||
* `spawn` takes raw type fields (`spawn<0, 16, 5, 0xC400, 3>(unit:…)`) – you must know the
|
||||
values the spawned type needs;
|
||||
* the Script view does not show event handlers as `on … { }` blocks.
|
||||
|
||||
## Tips
|
||||
|
||||
* Look at an original type that does something similar first (Script panel) and copy its numbers
|
||||
– speeds, animation indices, sounds and type fields are level-specific.
|
||||
* Keep each frame's work small and always reach a `yield` in loops; a loop without a yield freezes
|
||||
the game.
|
||||
* Test in the game early: the compiler checks the code's form and references, not whether the
|
||||
object behaves well.
|
||||
@@ -0,0 +1,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/).
|
||||
|
||||

|
||||
|
||||
| Part | What it does | Docs |
|
||||
|---|---|---|
|
||||
| launcher | first-start wizard (copies the game from CD/ISO), mod list with options, save-game editor, starts the game (Wine on Linux) | [manual: Installing](manual/02-installing.md), [manual: Mods](manual/18-mods.md), [notes/llamamod-app.md](../notes/llamamod-app.md) |
|
||||
| level editor | 3D editor for new and original levels: brushes, tiles, meshes, textures, objects, waypoints, cameras, zones, lights, visibility, strat scripts, play-test, glTF export | [editor manual](manual/index.md) |
|
||||
| mods | Test World, Alligator Arena, level projects, Boot to level, Intro skip, Mirror mode, Practice tools, Randomizer, Draw distance, Built-in cheats, Debug wireframe, Free camera | [manual: LlamaMod mods](manual/18-mods.md), `notes/mod-*.md` |
|
||||
|
||||
### Command line
|
||||
|
||||
```text
|
||||
llamamod start the GUI
|
||||
llamamod --version show the version and build id
|
||||
llamamod --install <iso|folder> set up the game data
|
||||
llamamod --mods list available mods and their options
|
||||
llamamod --apply [id,id,...] [--opt id.key=value ...]
|
||||
apply mods (none = original); options are saved
|
||||
llamamod --launch [--wait] start the game
|
||||
llamamod --registry show the registry values
|
||||
llamamod --saves show save games
|
||||
llamamod --unlock <slot 1-3> <world> <chapter> [--cursor]
|
||||
llamamod --editor [project.llamalevel | t1l1m001] level editor
|
||||
llamamod --editor-build <project.llamalevel> <out.wad> [--cd <dir>]
|
||||
llamamod --editor-import <level.wad|t1l1m001> <project.llamalevel> [--cd <dir>]
|
||||
llamamod --editor-new <project.llamalevel> [base level] [--name <name>] [--cd <dir>]
|
||||
llamamod --import-level <project.llamalevel> (into the level folder, shown as a mod)
|
||||
llamamod --remove-level <id|file> move a level mod's file to backups/levels/
|
||||
```
|
||||
|
||||
Data folder: `-Dllamamod.data=<folder>` or `LLAMAMOD_DATA` (default `~/.local/share/LlamaMod`,
|
||||
Windows `%LOCALAPPDATA%\LlamaMod`). Source: [`Main.java`](../src/main/java/com/eliasmueller/llamamod/Main.java).
|
||||
|
||||
### Debug view
|
||||
|
||||
The *Debug wireframe* mod
|
||||
([`mods/debug/`](../src/main/java/com/eliasmueller/llamamod/mods/debug/)) adds visible geometry to
|
||||
every level: collision as coloured frames (invisible walls filled red, the exit floor violet, death
|
||||
planes magenta) and markers for invisible objects (start, exit, checkpoints, cameras, triggers,
|
||||
zones). Gameplay is unchanged. Its Python ancestor is `tools/debugviz.py`; colour legend and
|
||||
details: [notes/debugviz.md](../notes/debugviz.md).
|
||||
|
||||

|
||||
|
||||
## Python kit (`tools/`)
|
||||
|
||||
The reverse-engineering toolkit the project started with. Python 3.8+ with `numpy` and `Pillow`
|
||||
(`capstone` for `disx86.py`). All parsers rebuild the original files byte for byte.
|
||||
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| [`wadtool.py`](../tools/wadtool.py) | list, extract and replace WAD chunks |
|
||||
| [`geomlib.py`](../tools/geomlib.py) | library: WAD, TRAK, STPC models, MAP tile part, TEXT, collision |
|
||||
| [`geom_export.py`](../tools/geom_export.py) | level → OBJ/MTL with textures and vertex colours, `--collision` (collision.obj by material), `--objects`, preview renders |
|
||||
| [`geom_import.py`](../tools/geom_import.py) | OBJ → new world geometry in a copy of a level |
|
||||
| [`geom_demo.py`](../tools/geom_demo.py) | example geometry for `geom_import.py` |
|
||||
| [`mapchunk.py`](../tools/mapchunk.py) | MAP parser/writer |
|
||||
| [`map_dump.py`](../tools/map_dump.py) | print a level's tiles, strats, lights |
|
||||
| [`strats.py`](../tools/strats.py) | object types of a level: names, instance counts, code offsets, type fields |
|
||||
| [`strat_table.py`](../tools/strat_table.py) | type × level table → [notes/strat_types_table.md](../notes/strat_types_table.md) |
|
||||
| [`stpcwalk.py`](../tools/stpcwalk.py) | rough walk through STPC (models, animations, code) |
|
||||
| [`textchunk.py`](../tools/textchunk.py), [`text_export.py`](../tools/text_export.py) | TEXT chunk; export pages to PNG and import them back |
|
||||
| [`texsheet.py`](../tools/texsheet.py) | contact sheet of a level's textures with indices |
|
||||
| [`levelbuild.py`](../tools/levelbuild.py) | level builder from a Python script ([guide](guide/levels-in-code.md)) |
|
||||
| [`make_testlevel.py`](../tools/make_testlevel.py) | early experiment: one object per type on a floor plate |
|
||||
| [`debugviz.py`](../tools/debugviz.py), [`debugviz_check.py`](../tools/debugviz_check.py) | debug-view WADs for all levels and an offline check |
|
||||
| [`cor2png.py`](../tools/cor2png.py), [`png2cor.py`](../tools/png2cor.py) | loading pictures (COR) ↔ PNG |
|
||||
| [`asf2wav.py`](../tools/asf2wav.py) | music (ASF) → WAV, exact decoder |
|
||||
| [`disx86.py`](../tools/disx86.py) | disassemble `groove.exe` at a virtual address |
|
||||
| [`vorschau/`](../tools/vorschau/) | Blender (Eevee) preview renders of a level with labels |
|
||||
| [`lama-mod`](../tools/lama-mod) | shell script: CD overlay of symlinks for mods in a hand-made Wine setup (pre-LlamaMod) |
|
||||
|
||||
Level scripts in [`levels/`](../levels/): `testwelt.py` (the original test world),
|
||||
`sprunglabor.py` (jump lab used to measure the llama), `testwelt_parkour.seq` (scripted run).
|
||||
|
||||
### 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 |
|
||||
@@ -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`).
|
||||
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
@@ -60,7 +60,7 @@ objdump, replay, restore).
|
||||
After rebasing onto the merged main tree (simp-all + launcher UX changes): orig, all-v, boot-exe, mrf-r
|
||||
re-run with the final jar: all OK (CDPath `L:\cd-active\`).
|
||||
|
||||

|
||||

|
||||
|
||||
Screens: all mods in Village (wireframe, mirrored) and the free camera; River with mirror+randomizer+fog;
|
||||
Test World with all mods; boot-to-level + all exe mods (River loading card); practice restore.
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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 |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|  |  |
|
||||
| A 1920-wide window: the Inspector is 170 px wide and shows no values at all. Side docks have two rows of tabs. | The docks get their intended widths (290 / 340 px). Side-dock tabs are icons with the active one named. The engine fields of the object type are folded away. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| The Brush tool's buttons read "Bl…", "Pl…", the texture names "gr…", the numbers show "0,5". | Full labels and a decimal point. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| The Coin tool's options are cut off: only the top pixels of the fields are visible. | The options are on one line, or wrap to a second row in narrow windows. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| *Help* comes before *Geometry*, and the shortcuts are German ("Strg", "Umschalt"). | The menus are File … Geometry, Tools, Help, and the keys are named "Ctrl", "Shift". |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| *Open original level*: the description is cut at the right edge, and the selected row's id is grey on orange. | The text wraps and the id is readable. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| A 1280×720 window: the right dock is squeezed to 130 px. Also, Esc did not close the dialog, so it was still open and blocked every key. | The docks keep their sizes and the tool options wrap. Esc closes dialogs. |
|
||||
|
||||

|
||||

|
||||
|
||||
*New: Play while the game still runs offers to quit it.*
|
||||
|
||||
@@ -20,27 +20,27 @@ covered by code review), Windows (the antivirus notice is unchanged there).
|
||||
|
||||
| Before | After |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|  |  |
|
||||
| The app opens with the mod list scrolled to the end (Visual, Cheats); Levels and Gameplay are out of sight. | The list opens at the top and keeps its place when it is rebuilt (Apply, theme). Short windows get a lower header. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| Options have no explanation unless you hover (Worlds "all"? Checkpoint spacing "10" of what?). Descriptions talk about "far clip plane", "strafe", "rigid object models". | Every option is explained next to it. Mod descriptions say what happens in the game and which keys to press. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| A text file is "shorter than expected (truncated?)", a wrong folder gets "expected Setup/…/Hd/groove.exe", a half-downloaded ISO is "Game found" and fails only after Set up. | Plain messages that say what to do; an incomplete ISO is caught at once, with both sizes. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| On Linux a missing groove.exe is blamed on antivirus software and explained with Windows Defender menus. After Rebuild you have to click Play again. | Linux gets a short message; **Rebuild** writes the file and starts the game. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| "lowest 4 bits of the coin count", "unknown item flag (bit 0)", "Cursor on", "Revert"; no hint how to restore a backup. | Plain wording, "Opens at", "Undo changes"; **Import…** starts in the backups folder and the hint says so. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| "registry values ScreenW/ScreenH", "wine from PATH"; the mod cache shows 0.0 MB after applying 6 mods (stale). | "Automatic (1920x1052)" shows the real size, plain hints, a note on what *Set up again* keeps; the cache size is current. |
|
||||
|  |  |
|
||||
|  |  |
|
||||
| Dark theme: the *modifies groove.exe* badge is dark orange on grey (barely readable). | A lighter badge text in the dark theme. |
|
||||
|
||||
| New: quitting a hidden or hung game | New: crash report |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|  |  |
|
||||
| While the game runs, Play becomes **■ Quit game** (neutral colour) with a confirmation. | Shown as soon as Wine reports the crash (the game hides behind Wine's error window until that is closed), with Quit game / Open logs. |
|
||||
|
||||

|
||||

|
||||
|
||||
*Before: on Linux every first Apply of an exe mod showed this Windows Defender notice. Now it is
|
||||
shown on Windows only.*
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ Code: `src/main/java/com/eliasmueller/llamamod/editor/tools/environment/`, tests
|
||||
`src/test/.../editor/tools/environment/` (`EnvironmentToolsTest` needs `-Dllamamod.cd`; `EnvTour`
|
||||
= scripted screenshots, `EnvScenarios` = the in-game test projects).
|
||||
|
||||

|
||||

|
||||
|
||||
## What the user gets
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||

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