Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,117 @@
|
||||
# Changelog
|
||||
|
||||
## 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.3'
|
||||
|
||||
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
|
||||
|
||||
@@ -115,6 +144,12 @@ Turns the hidden TestLevel (world 9, chapter 1) into a playground island: a gall
|
||||
|
||||
*Id* `testworld`
|
||||
|
||||
#### Village Chapter 1 (play-test)
|
||||
|
||||
Temporary play-test copy from the level editor – removed when the play-test ends. Changes the original level t1l1m001.
|
||||
|
||||
*Id* `playtest-village-chapter-1`
|
||||
|
||||
### Gameplay mods
|
||||
|
||||
#### Boot to level
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`).
|
||||
@@ -60,7 +60,7 @@ objdump, replay, restore).
|
||||
After rebasing onto the merged main tree (simp-all + launcher UX changes): orig, all-v, boot-exe, mrf-r
|
||||
re-run with the final jar: all OK (CDPath `L:\cd-active\`).
|
||||
|
||||

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
*Before: on Linux every first Apply of an exe mod showed this Windows Defender notice. Now it is
|
||||
shown on Windows only.*
|
||||
@@ -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
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 250 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: 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).
|
||||
@@ -57,7 +57,7 @@ diagnostics) → `jmp 0x41C050`. The cave writes only game globals, so it needs
|
||||
|
||||
| Feature | State | In the mod |
|
||||
|---|---|---|
|
||||
| `-display_diagnostics` (DAT_005834D0) | works: writes `<COMPUTER>.dispdiag` (DDraw/D3D enumeration) into the game folder | option `diagnostics` (tested: ELIASPC.dispdiag, 60 KB) |
|
||||
| `-display_diagnostics` (DAT_005834D0) | works: writes `<COMPUTER>.dispdiag` (DDraw/D3D enumeration) into the game folder; FUN_0040f4f0 opens **every display mode of every DirectDraw device × D3D device** (FUN_00410550/FUN_00410700 = mode switch, FUN_00411000), logs caps and texture formats, closes it again, then returns and the game starts normally. On Windows with two monitors both screens flashed black for 30+ s (tester, 2026-11); invisible under Wine | option `diagnostics`, label "Troubleshooting: write display diagnostics", off by default, warning in its description (tested: ELIASPC.dispdiag, 60 KB) |
|
||||
| `c:\kingdom_dbg.txt` | dead: FUN_0040d2c0 (called from FUN_0043c220) only creates the file with "Logfile %s started now/today (write this code!)"; the 43 debug prints (FUN_00414600) go to OutputDebugStringA (0x6DC3E8), never to the file | no |
|
||||
| FrameAdvanceDelay (FUN_0043cb40) | slide show `In\001.bmp…` + `In\In.wav` at start, `Out\…` at exit, delay from registry `FrameAdvanceDelay` (default 1.0 s); folders are empty | no (data, not a cheat) |
|
||||
| F4 in game | requests state 0x0C (world map) – the normal "back to map" | no |
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Llama Coaster (`coaster`, LEVELS)
|
||||
|
||||
Package `mods/coaster`: `CoasterMod` (options, chapter check, writes `wads/T1L9M0nn.wad` +
|
||||
loading picture `T19nn.COR`), `CoasterLevel` (level script: track geometry + objects),
|
||||
`CoasterTrack` (layouts → segments, corners, waypoint chain), `CoasterSpeed` (patches the
|
||||
RollerCoaster strat's speed constants), `CoasterSim` (frame model of the strat's speed/jump logic,
|
||||
used by the tests to check a layout). Tests: `CoasterTest`. Registered in `ModRegistry` (one line).
|
||||
Docs: [docs/strats/roller-coaster.md](../docs/strats/roller-coaster.md), manual chapter 18.
|
||||
|
||||
| Option | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `track` | `loop` | `loop` (~103 units/lap) or `grand` (~150 units, two jumps, two spin pads) |
|
||||
| `laps` | 2 (1–3) | laps before the ride ends at the station |
|
||||
| `speed` | `normal` | `slow` ×0.8, `normal` ×1, `fast` ×1.25 (cruise 614 raw and boost 20 raw/frame) |
|
||||
| `coins` | on | CoinRollerCoaster along the straights (loop 27, grand 44; placed once, not per lap) |
|
||||
| `chapter` | 48 (32–49) | TestLevel chapter; a chapter another mod already wrote → apply error |
|
||||
|
||||
## How the original rides work
|
||||
|
||||
**Roller coaster = Catacombs 1, 3, 5 (`t1l7m001/3/5`, world 7 – not world 8; Yzma's lab is
|
||||
world 8 and has no ride).** Strat units of t1l7m001: `RollerCoaster` (unit 0, 1775 words),
|
||||
`CameraFixedPointBox`, `RollerDoor`, `CoinRollerCoaster`, `WompyRollerCoaster`,
|
||||
`FireworksTrigger`, `Camera`, `SpawnableRing`, `Sparkles`, `SpawnableNeedleBall`.
|
||||
|
||||
* **The cart is the player.** There is no `Llama` strat: `RollerCoaster` calls `isPlayer()` and
|
||||
`lookAtMe()`, uses `model:0` (cart with Kuzco-llama and Pacha, scale 2) and its animations
|
||||
(lean, cheer, look back, fall). The llama is part of the cart model, not attached at runtime.
|
||||
* **Camera:** the strat spawns the shared `Camera` strat (`spawn<4,41,2,0x248000,0>(0.9, 1.0, …,
|
||||
unit:Camera)`), the same chase camera as the Llama's; it follows `g29` = the cart's heading,
|
||||
which the strat sets to the next waypoint's heading in corners (so the camera swings round
|
||||
before the cart). `CameraFixedPointBox` (one per level) is a fixed-point camera zone.
|
||||
* **Track following = waypoint chain + floor.** At start (`L_0200`) every waypoint x/z is rounded
|
||||
to halves and its heading to a quarter turn (param2). The cart drives straight along its heading
|
||||
at speed `v19` (min 0.15 = 614 raw), the floor height comes from the collision floor below it
|
||||
(`noFloorPhysics`, `self.y = floorHeight`; a climb steeper than ~0.1 per frame loses the
|
||||
floor). Within 0.75 in front of the current waypoint it turns by 90° on a small arc round the
|
||||
waypoint (funnel: lateral offset clamped to 1.2·distance) and switches to the next one – so the
|
||||
tracks are straight legs on the grid with 90° corners; only the geometry makes slopes.
|
||||
Left/right input shifts the cart sideways (`v18`, ±half the speed); the rails are collision walls.
|
||||
* **Floor materials** (collision material of the track bed): 5 boost (+20 raw speed and
|
||||
"excess" per frame, decays back to cruise), 11 jump (vertical speed 163 raw, gravity 14 raw per
|
||||
frame), 13 spin (small hop + half turn: the cart rides backwards until the next corner), 15 an
|
||||
animation/sound pad, 16 the end (only when the current waypoint is the last one), 30 the
|
||||
invisible death plane far below (fall → restart).
|
||||
* **Start:** the first two waypoints have param 1.0 (station): the cart crawls (0.025 + nudges)
|
||||
out of the station with letterbox borders and the start line, then picks up cruise speed.
|
||||
* **End:** on material 16 with the last waypoint active the strat plays a sound and after 30
|
||||
frames calls `endLevel(param1..3)` – the cart's own params 1–3 are the target (tribe, world,
|
||||
chapter) → tally screen. `RollerDoor` are gates the cart passes, `FireworksTrigger` fires rockets
|
||||
at its waypoint spots when the cart passes, `WompyRollerCoaster` is a spinning Wompy pick-up.
|
||||
|
||||
**River = Jungle river 1–4 (`t1l3m001…4`, world 3; world 2 is the jungle at night).** The player
|
||||
strat is `KuscoAndPachaTogetherOnATree` (both on a log, also `isPlayer()` + the shared `Camera`).
|
||||
It also rounds its waypoints to halves/quarter turns and turns at waypoints whose param is 1 or 2
|
||||
(left/right), but it floats on water (`floorMaterial 5` = water there: rumble, splash particles)
|
||||
and is pushed by current/force zones (`RiverCurrentBox`, many "kraft" zones); the player steers
|
||||
freely between the banks, `CameraLift`/`RiverWaterfallCamera` give the scripted shots, and the
|
||||
level ends at a normal `LevelDoor`. It is a steerable boat, not a rail ride, so the coaster mod
|
||||
uses the RollerCoaster strat.
|
||||
|
||||
## Design
|
||||
|
||||
* **Base level = t1l7m001** (not the village + import): its STPC already has every unit the ride
|
||||
needs and their textures, sounds and the ride music; importing `RollerCoaster` + `Camera` into
|
||||
the village would only duplicate them. The original geometry and objects are replaced by
|
||||
`LevelBuilder` output (`LevelScript.buildWad`), so `format.stpcmerge` is only used read-only
|
||||
(`StpcImage`/`StratCode`) by `CoasterSpeed`.
|
||||
* **Track:** straight legs along the axes, 2 wide (two 1-wide lanes, centre line on integers),
|
||||
joined by flat 2 × 2 corner squares; heights change only on straights. Pieces: TRACK, BOOST (mat
|
||||
5), JUMP (11), SPIN (13), END (16), STATION, GAP (no floor). Rails 0.35 high (inner face =
|
||||
collision), underside, pillars every 5 units and under corners, a boarding platform with posts
|
||||
left of the station, ground plane at y = 0 with material 30. Textures are t1l7m001 TEXT indices.
|
||||
* **Leg 0** of every layout: run-in, END pad, brake run, STATION (5), first drop with a boost at
|
||||
its top. The circuit must close (checked) and END must come before STATION on leg 0.
|
||||
* **Waypoint chain** (`CoasterTrack.nodes`): node 0 one unit behind the cart and node 1 at the
|
||||
station's end, both param 1.0 (crawl); then one node per corner, 0.5 before its centre, heading
|
||||
after the turn, repeated per lap; last node 9 units past the END pad on leg 0, so the END pad is
|
||||
live only in the last lap. RollerCoaster params = (0, 1, 9, chapter) ×4096 → the tally restarts
|
||||
the ride, like the arena.
|
||||
* **Speed** (`CoasterSpeed`): finds the six `pushShort 614` (cruise floor/cap) and two
|
||||
`pushShort 20` (boost, after `pushVar v19/v22`) in the RollerCoaster unit with `StratListing`
|
||||
and rewrites the 16-bit operands; nothing else changes.
|
||||
* **Checking a layout offline** (`CoasterSim`, test `everyJumpLandsAndNoClimbLosesTheFloor`):
|
||||
for every track × speed × laps every jump must clear its gap and land before the commit zone of
|
||||
the next corner, no climb > 0.1 per frame, speed stays 0.02–0.45.
|
||||
* Objects: `RollerCoaster` at the station start, a `RollerDoor` at the station exit, a
|
||||
`FireworksTrigger` before the END pad with six rocket spots, CoinRollerCoaster every 1.5 units
|
||||
on straights ≥ 4 long outside leg 0 (on a slight zig-zag). The tally total (LNFO) is the coin count.
|
||||
* Loading/tally picture: drawn in Java (dark purple, a rail curve, title "Llama Coaster" in the
|
||||
band y 265–335 that the game's tally text leaves free).
|
||||
|
||||
## Verified in game (apptest, :26, 2026-11-07)
|
||||
|
||||
* loop/normal 1 lap and 2 laps, loop/fast 1 lap, grand/fast 1 lap (chapter 40), grand/slow:
|
||||
station crawl with letterbox and start line, drops, climbs, boost arrows, start hint "Kusco, bleib in der
|
||||
Mitte der Kurve und wir werden uns automatisch umdrehen!", spin pad (cart turns backwards), jump over the gap and
|
||||
landing, corners, coins counted (e.g. 14/27, 19/44), END pad → fireworks → tally with the
|
||||
loading picture. No crash in any run.
|
||||
* After the tally: **Enter** restarts the ride (same chapter); **Esc** asks "Möchtest du das
|
||||
Spiel beenden?" (No → back to the tally). In the ride, Esc → *Beenden und zur Auswahlkarte
|
||||
wechseln* → Yes → world-9 map on "TestLevel Kapitel 48". Same behaviour as the arena.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Limits
|
||||
|
||||
* Only 90° corners on the grid and straight slopes (the strat cannot follow curves or banking);
|
||||
no loops/inversions (the cart has no roll).
|
||||
* The tally coin count is not kept on the world map (it showed 0 after the ride) – same as the
|
||||
other TestLevel mods; not investigated.
|
||||
* The original levels' sky/fog/lighting of t1l7m001 stays (dark catacombs, fog end 23 cells).
|
||||
@@ -35,6 +35,16 @@ properties). Language/syntax: `notes/strat-language.md`. Disassembler/assembler:
|
||||
getter FUN_00550e60 and setter FUN_005509f0 share the numbering; ids without a setter are read-only
|
||||
(a write would go to address 0).
|
||||
|
||||
## Spawning and collision (engine facts, found by the arena, 2026-11-07)
|
||||
|
||||
* `spawn<params, vars, slots, typeFlags, extra>` (0xe0, FUN_0054bfc0) takes the same fields as a MAP
|
||||
strat record: params = MAP parameter count, vars = local variables, slots, typeFlags = MAP type flags
|
||||
(collision setup), `extra` = words of the collision-bone area.
|
||||
* `spawn` copies the spawner's matrix/translation block (+0x20…), not its `x/y/z` properties: after
|
||||
`self.x = …` the child still appears at the old place. Set the position, **yield**, then spawn.
|
||||
* A MAP-placed type that uses `collisionOn` needs collision type flags in its MAP record (0x28000 like
|
||||
Coin); with typeFlags 0 the game crashes (page fault in the heap allocator FUN_0041ae20).
|
||||
|
||||
## Sources
|
||||
|
||||
* groove.exe handler tables, decompiled with Ghidra 12.2 (headless, all 0x1b2 + 0x45 entries and
|
||||
@@ -642,7 +652,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 143 | `camScale` | yes | yes | low | DAT_006D9D30, camera value initialised to 1.0 (zoom/fov factor?). |
|
||||
| 144 | `levelHeaderC` | yes | no | low | Level header (DAT_00584648)+0x0C ushort << 12. |
|
||||
| 145 | `slopeSteepness` | yes | no | medium | 1.0 - floor normal y (DAT_0057DDA4) << 12: 0 on flat ground (FUN_004054c0). |
|
||||
| 147 | `levelTableByte` | yes | no | low | Byte table 0x585024[map + (chapter + tribe*10)*4] << 12 (per-level constant). |
|
||||
| 147 | `levelTableByte` | yes | no | low | Byte table 0x585024[map + (world + tribe*10)*4] << 12 (FUN_0041af40(tribe, world, map)); bss, never written by the game (always 0) – but world-9 coin counters of TestLevel chapters 22–31 land in its tribe-1 part (see testlevel-chapters.md). |
|
||||
| 149 | `flagEC_1` | yes | no | low | Flag +0xEC & 0x1. |
|
||||
| 150 | `flagEC_8000000` | yes | no | low | Flag +0xEC & 0x8000000. |
|
||||
| 151 | `cheat5` | yes | no | medium | Cheat toggle bits DAT_00584710 & 0x20 (button-sequence cheats, FUN_0041a0xx). |
|
||||
@@ -660,10 +670,10 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 165 | `instanceData1` | yes | yes | low | Map-instance record (+0xF0)+0x28 -> word 1. |
|
||||
| 166 | `floorCollisionOn` | yes | no | medium | Flag +0xEC & 0x8000 (floor collision, ops 0xa5/0xa6). |
|
||||
| 169 | `lives` | yes | yes | low | DAT_00584E6C (fixed, starts at 5.0, saved per slot) - lives or max health. |
|
||||
| 171 | `saveProgressA` | yes | no | low | DAT_00585008 << 12 (saved progress byte). |
|
||||
| 172 | `progressWorld` | yes | no | low | DAT_0058500C << 12 (saved; world of the furthest/selected level, 8 = finale). |
|
||||
| 173 | `progressChapter` | yes | no | low | DAT_00585010 << 12 (saved; chapter, 6 with world 8 = last level). |
|
||||
| 174 | `saveProgressB` | yes | no | low | DAT_00585014 << 12 (saved progress byte). |
|
||||
| 171 | `saveProgressA` | yes | no | low | DAT_00585008 << 12: last played tribe (KSV +0x3E, set by endLevel). |
|
||||
| 172 | `progressWorld` | yes | no | low | DAT_0058500C << 12: last played world (KSV +0x3F, set by endLevel; 8 = finale). |
|
||||
| 173 | `progressChapter` | yes | no | low | DAT_00585010 << 12: last played chapter (KSV +0x40; 6 with world 8 = last level). |
|
||||
| 174 | `saveProgressB` | yes | no | low | DAT_00585014 << 12: last played map (KSV +0x41). |
|
||||
| 175 | `unk_6da308` | yes | no | low | DAT_006DA308, no other reference. |
|
||||
| 178 | `cheat4` | yes | no | medium | DAT_00584710 & 0x10 (cheat toggle). |
|
||||
| 179 | `hudFlags` | yes | yes | medium | DAT_006DA2E0 HUD element enable mask (default 0x3F000; 0x2000 timer, 0x8000/0x10000/0x20000/0x40000 counters). |
|
||||
@@ -676,7 +686,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 187 | `hitDamage` | yes | yes | medium | +0x140: damage dealt on contact (copied into the other strat's +0x144). |
|
||||
| 188 | `damageTaken` | yes | no | medium | +0x144: damage received from the last contact (player cleared to 0 each frame). |
|
||||
| 189 | `health2` | yes | yes | medium | DAT_00584E70 = health (same as 92, settable). |
|
||||
| 190 | `levelRecord` | yes | yes | low | Per-level saved word DAT_00584F1C[chapter + world*6] (best/collected value for the current level; HUD shows it). |
|
||||
| 190 | `levelRecord` | yes | yes | medium | Coin counter of the current level: u32 DAT_00584F1C[chapter + world*6] (coins << 12; HUD and tally show it, zeroed on level start, KSV keeps worlds 0–8 as u16). World 9 has no row: TestLevel chapter c hits 0x584FF4 + 4c (chapter 9 = furthest progress 0x585018) – only chapters 1 and 32–49 are free (testlevel-chapters.md). |
|
||||
| 191 | `counterC` | yes | yes | low | DAT_00584FFC: persistent HUD counter (icon 0x150), saved. |
|
||||
| 192 | `counterA` | yes | yes | low | DAT_00584FF4: persistent HUD counter (icon 0x184, displayed up to 999) - probably coins. |
|
||||
| 201 | `distToPlayerSq` | yes | no | high | +0x148: cached squared distance to the player ((d>>6)^2 sum), updated each frame when tracking (op 0x30). |
|
||||
|
||||
@@ -57,7 +57,8 @@ WAD path absolute!), `rs.sh NAME "SEQ"` (restart with installed WAD + `seq`, mon
|
||||
**Caution**: no `pgrep -f groove.exe` in wait loops – it matches your own shell
|
||||
(the command line contains "groove.exe") and lamatest takes it for a foreign instance.
|
||||
|
||||
State/PIDs/wine.log: `$LAMATEST_DIR` (default `/tmp/lamatest-$USER`).
|
||||
State/PIDs/wine.log: `$LAMATEST_DIR` (default `/tmp/lamatest-$USER`). Game: `$LAMA_WINEPREFIX` (default `~/.wine-lama`),
|
||||
`lama-mod`: `$LAMA_MOD` (see [docs/tools.md](../docs/tools.md#paths)).
|
||||
|
||||
`run` exit codes: 0 OK, 3 crash, 4 timeout/hang, 5 image frozen.
|
||||
Output in `<outdir>`: `report.txt`, `wine.log` (stderr, truncated to 2 MB),
|
||||
@@ -66,7 +67,7 @@ Output in `<outdir>`: `report.txt`, `wine.log` (stderr, truncated to 2 MB),
|
||||
|
||||
## Safety rules (implemented in the script)
|
||||
|
||||
- Aborts if a groove.exe with a different DISPLAY is running (Elias' instance).
|
||||
- Aborts if a groove.exe with a different DISPLAY is running (the user's own instance).
|
||||
- xdotool only with DISPLAY=:5.
|
||||
- Sound: own null sink, the app is never muted; `stop` unloads the sink.
|
||||
- Save games (`*.ksv` in the game folder) are backed up on `start` and restored
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# TestLevel chapters: which ones are safe (2026-11-07)
|
||||
|
||||
World 9 ("TestLevel") has 49 chapter slots (table 0x572420), but the game's per-chapter arrays only
|
||||
have rows for worlds 0–8. Indexing them with world 9 runs into other globals. Code:
|
||||
`save/TestLevelChapters` (usable = chapter **1** and **32–49**).
|
||||
|
||||
## The arrays (index world·6 + chapter)
|
||||
|
||||
| Array | Where it is used |
|
||||
|---|---|
|
||||
| u32 coins 0x584F1C (coins << 12) | `levelRecord` (strat var 0xBE) get/set, HUD (FUN_00426bf0), tally (FUN_0040dd60), endLevel (FUN_0054d620: "all coins" check), **zeroed on every level start** (FUN_0041cba0), KSV keeps 54 entries as u16 |
|
||||
| u8 flags 0x584EC8 (bit 0 collectible, bit 1 gold coin) | collectible picked up (HUD, strat var 0xFA rising) → \|=1; endLevel → \|=2 when coins == static total 0x5722F4[world·5 + chapter] (garbage for world 9, often 0) or cheat 0x584710&8 → \|=3; gold count FUN_0041cd70 (worlds 1–8 only) |
|
||||
|
||||
Coin total table 0x5722F4 and the selection map only know worlds 1–8 (map shows garbage targets
|
||||
for world 9 – cosmetic).
|
||||
|
||||
## What world-9 chapter c hits
|
||||
|
||||
Coin slot 0x584FF4 + 4c, flag byte 0x584EFE + c (Ghidra references 0x584EF0–0x585240):
|
||||
|
||||
| c | coin slot | flag byte |
|
||||
|---|---|---|
|
||||
| 0 | 0x584FF4 counter var 0xC0 (HUD) | free |
|
||||
| **1** | 0x584FF8 free (only cleared by new game) | free |
|
||||
| 2 | 0x584FFC counter var 0xBF (HUD icon 0x150) | free |
|
||||
| 3 | 0x585000 HUD flag (FUN_00427c40) | free |
|
||||
| 4 | 0x585004 var 0x80 | free |
|
||||
| 5–8 | 0x585008–14 last played tribe/world/chapter/map (strat vars 0xAB–0xAE, KSV +0x3E–41) | free |
|
||||
| 6–9 | | 0x584F04 **language index** (u32; \|=2 → text table out of range) |
|
||||
| 9 | 0x585018 **furthest progress** (world, chapter bytes; KSV +0x42/43) | |
|
||||
| 10–13 | 0x58501C/20 free | 0x584F08 option (KSV +0xD6) |
|
||||
| 12 | 0x585024 start of byte table (var 0x93, tribe 0) | |
|
||||
| 14–21 | 0x585028–48 free | 0x584F0C / 0x584F10 (var 0x5B) |
|
||||
| 22–25 | 0x58504C–58 table 0x585024, tribe 1 (read by var 0x93 in original levels) | free |
|
||||
| 26–29 | table, tribe 1 | 0x584F18 option (KSV +0xD8) |
|
||||
| 30–31 | table, tribe 1 (world 8/9) | coin slots of world 0 (never read) |
|
||||
| **32–49** | table, tribes 2/3 (never used: tribe is 1, 4 only in the attract demo) | coin slots of world 0 |
|
||||
|
||||
## Verified in game (apptest :24, 2026-11-07)
|
||||
|
||||
* **Chapter 9** (arena test mod, 1 alligator, 2 coins collected): pause → "Beenden und zur
|
||||
Auswahlkarte" → the selection map shows world "°p", "Kapitel 0", coin target 14384: furthest
|
||||
progress 0x585018 was replaced by the coin counter (2 << 12 → world 0, chapter 32).
|
||||

|
||||
* **Chapter 49** (same run): tally "Münzen gesammelt 2 / 12, Geheimnisse 0 / 0" (LNFO recounted),
|
||||
save written with progress (9, 50) – the map caps the cursor at chapter 49, no crash; map fine.
|
||||

|
||||
* Before (arena on chapter 8, arena agent): tally "0 / 55": the collected value is the coin slot
|
||||
after endLevel copied the level ids into 0x585008–14 (chapter 8 = last played map = 0), the total
|
||||
is LNFO of the Village base.
|
||||
|
||||
## Tally totals (LNFO)
|
||||
|
||||
Chunk LNFO = u32 coins, u32 secrets → 0x5FF070 / 0x5FF060 (FUN_0042AB10; the tally shows the values
|
||||
of the level just finished, kept in 0x5FF064 / 0x5FF06C). Originals: e.g. t1l3m004 40 = 20 Coin +
|
||||
4 Coin5·5; levels with CoinLine/CoinCircle/enemies count those too. `mods/levels/LevelTally` recounts
|
||||
Coin (1) + Coin5 (5) and Secret strats for new level projects and the Test World (20 coins, 0
|
||||
secrets); the arena adds its dynamic coins (alligator hit/death coins + 5 exit bonus).
|
||||
|
||||
## Migration
|
||||
|
||||
* Level projects: stored chapter 1 or 32–49 kept (first file wins), others get the first free of
|
||||
32–49; more than 18 projects share chapters (only one of them can be on – apply error otherwise).
|
||||
Play-tests/exports of an old project: `migrate` (2..19 → 32..49, 20..31 → 32..43).
|
||||
* The chapter options are choices (level: 1, 32–49; arena: 32–49, default 49); a stored old value is
|
||||
invalid and falls back to the default (ModOptions).
|
||||
* Editor: New level / Level settings spinners only offer usable chapters. The empty template
|
||||
(`Templates.makeEmpty`) defaults to 32; New level and CLI `--editor-new` take the first chapter
|
||||
from 32 no project in the levels folder uses (`LevelProjects.nextFreeChapter`).
|
||||
|
||||

|
||||
|
||||
## Fix: base exe patch relocates both arrays (ExePatcher.REVISION 2, 2026-11-07)
|
||||
|
||||
`ExePatcher.relocateLevelRecords` (part of the always-applied base patch: `ExePatcher.patch` for
|
||||
the installer, `ExePatcher.changes` replayed as owner "base" in `ModManager.baseExe`) moves both
|
||||
arrays to a 128-entry buffer (index ≤ 9·6 + 49 = 103) in the slack after .data's VirtualSize:
|
||||
.data ends at 0x6DB935, the page at 0x6DC000 (.idata follows), so coins u32[128] at **0x6DB940**,
|
||||
flags u8[128] at **0x6DBB40** (zero-filled and RW, mapped with .data's last page; no new section,
|
||||
no code). Every absolute address of the arrays in .text is rewritten (whole-file scan of
|
||||
0x584EC8–0x584EFE / 0x584F1C–0x584FF4: no other references, none in .data):
|
||||
|
||||
| Old | New | Sites (operand VA) |
|
||||
|---|---|---|
|
||||
| 0x584F1C coins | 0x6DB940 | tally 0x40DE67, KSV load 0x41C72A / save 0x41C8FB, new game 0x41CA6B, level start 0x41CBCC, HUD 0x426AC2 / 0x426D43 / 0x426E08, endLevel 0x54D6BA, levelRecord ptr 0x550BC5, 0x55223E |
|
||||
| 0x584FF4 coin loop end (`cmp eax, imm; jl`) | 0x6DBA18 | 0x41C74B, 0x41C91A (0x584FF4 itself is also counter var 0xC0 – those uses stay) |
|
||||
| 0x584EC8 flags | 0x6DBB40 | tally 0x40DEA1 / 0x40DEC9, KSV 0x41C65C / 0x41C832, new game 0x41CA64 (rep stosd 54 bytes), collectible 0x42706B, endLevel 0x54D6E3 / 0x54D708 / 0x54D70F |
|
||||
| 0x584ECE flags from world 1 | 0x6DBB46 | map icons 0x41B724 / 0x41B79C / 0x41B7EE / 0x54A75E, gold count 0x41CD7B |
|
||||
| 0x584EFE flag loop end | 0x6DBB76 | KSV 0x41C6D6 / 0x41C8AF, gold count 0x41CD9B |
|
||||
|
||||
Worlds 0–8 behave as before (KSV keeps the first 54 entries; the rest is never saved). World-9
|
||||
flags are not cleared by "new game" (only the 54 saved bytes are) – they only affect the map's
|
||||
collectible/gold icons of TestLevel chapters within one game session. If the counts of these
|
||||
references differ (other exe build) the relocation is skipped as a whole. Test: `ExePatcherTest`.
|
||||
|
||||
**Effect:** every world-9 chapter is harmless in LlamaMod's groove.exe, so the remaining holes are
|
||||
closed without restricting the map: picking an empty (linked) chapter like 9 on the world map,
|
||||
chapters 2–31 that unlock together with 32+, and Boot to level on chapters 2–31 (its option keeps
|
||||
1–49). Not linking empty chapters is no option: a missing WAD crashes the map ("Couldnt load wad",
|
||||
`ModManager.world9Fixup`). Mod levels still only use 1 and 32–49 (no migration churn; also safe
|
||||
with an exe that has not been rebuilt yet after the update).
|
||||
|
||||
Verified in game (apptest :24, 2026-11-07):
|
||||
|
||||
* A/B, Test World on, map cursor world 9 chapter 9 (the linked empty chapter), start, pause →
|
||||
quit to the map: old jar (base REVISION 1) → map "°p / Kapitel 0 / 14384" (progress zeroed by the
|
||||
level start alone); new → "TestLevel / Kapitel 9 / 237" (237 = garbage static coin total, cosmetic).
|
||||

|
||||
* Arena (test mod) on chapter 9, 2 coins collected (HUD 2 / 12), quit to the map → map fine.
|
||||
* Save with world-1 chapter 1 flags = 3 and gold count 2: slot menu "Goldmünzen 2", the map shows
|
||||
the collectible and gold icons of Village chapter 1 (KSV load loop + map reads relocated).
|
||||
* **KSV save loop after finishing a normal level, A/B byte comparison** (apptest :25, sessions
|
||||
`savetest-a` = fat jar of the commit before 00b2880, base REVISION 1; `savetest-b` = HEAD,
|
||||
REVISION 2; 2026-11-07). Slot 3 (`king2.ksv`) preloaded identically in both with a distinct
|
||||
pattern for worlds 1–8, chapters 1–5: flags `(w + c) % 4`, coins `((5w + c) % 15 + 1) << 12`
|
||||
(gold count 19/20 to match), so a save loop reading the old arrays in B would write zeros.
|
||||
Mods: boot-to-level (slot 3) + cheats `rewards` (flags |= 3 at the chapter end). Runs:
|
||||
1. **Jungle night 2** (world 2 ch. 2, exit pad reachable directly): start `29.45, 3.5, 60.3, 180
|
||||
@ 2/2`, Up ~5.5 s → 3 coins → tally "Münzen gesammelt 3 / 70" → Return.
|
||||
2. **Village 1** (world 1 ch. 1): t1l1m001 rebuilt by a throw-away tool (WadImporter → tile 420
|
||||
set to its swap-alt model 49 with the exit pad, RubyIdolDoor removed, the two coins near the
|
||||
door moved to x 42.5, `StartOverride` 42.5/3.5/51.8 yaw 180 → WadBuilder), written over the
|
||||
session's `cd-active/wads/t1l1m001.wad` after apply; Up 3.5 s → tally "Dorf Kapitel 1,
|
||||
Münzen gesammelt 2 / 55" → Return. (Village 4's direct exit is behind the Yzma boss fight.)
|
||||
3. **TestLevel chapter 1** (Test World, start `44.3, 3.6, 15.5, 90 @ 9/1`, 3 coins, rewards
|
||||
cheat) on top of the Village-1 save → tally → Return.
|
||||
|
||||
All three saved KSVs are **byte-identical in A and B** (`cmp`: 0 differences). Run 1: chapter
|
||||
(2,2) coins 9 → 3, flags 0 → 3, gold 20 → 21, progress (2,2) → (2,3), cursor (2,3), last played
|
||||
(2,2); every other preloaded coin/flag entry and bytes 0xB0–0xFF unchanged. Run 2: (1,1) coins
|
||||
→ 2, flags → 3, gold 19 → 20, progress (1,2). Run 3 (world-9 leak check): only cursor world/chapter
|
||||
(9,1) and last-played world 9 change; all 54 coin/flag entries, gold, progress and 0xB0–0xFF stay
|
||||
as before – the world-9 coins/flags (buffer index 55) are not written (54 entries only). B's save
|
||||
loaded again without mods: slot menu "Gespeichertes Spiel 3 / Dorf Kapitel 2 / Goldmünzen 20",
|
||||
world map on Village 2 with the collectible and gold icons (preloaded flags 3).
|
||||

|
||||
* **TestLevel tally** (question from the coaster test): Return does save (KSV rewritten, see run 3)
|
||||
– there is never a visible "saved" step, also not in normal levels (the next level loads at
|
||||
once). The Test World / coaster / arena exits lead back to the same chapter, so the restart zeroes
|
||||
that chapter's coin counter (level start, FUN_0041cba0) → the map then shows 0; world-9 coins are
|
||||
never in the KSV anyway. Expected and harmless. Esc on the tally asks "Möchtest du das Spiel
|
||||
beenden? Ja / Nein" (Return = Nein, back to the tally) – identical with the old exe (A), so not
|
||||
caused by the relocation. 
|
||||
|
||||
## Before the fix
|
||||
|
||||
* The map let the player pick any unlocked world-9 chapter; empty ones are linked to an installed
|
||||
world-9 WAD (ModManager), so playing e.g. chapter 9 from the map still clobbered progress. Unlocking
|
||||
chapter 32+ unlocks 2–31 on the map too. Fixed by the relocation above.
|
||||
@@ -6,6 +6,7 @@ import com.eliasmueller.llamamod.editor.model.Light;
|
||||
import com.eliasmueller.llamamod.editor.model.NodeKind;
|
||||
import com.eliasmueller.llamamod.editor.model.PathNode;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.save.TestLevelChapters;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -45,7 +46,7 @@ public final class Templates {
|
||||
LevelSettings st = s.settings();
|
||||
st.setRaw("kind", LevelSettings.NEW);
|
||||
st.setRaw("name", "New level");
|
||||
st.setRaw("targetChapter", 1);
|
||||
st.setRaw("targetChapter", TestLevelChapters.FIRST);
|
||||
st.setRaw("poolReserve", NEW_LEVEL_RESERVE);
|
||||
int[] table = s.regions().table().clone();
|
||||
table[0] = 0xffffffff;
|
||||
|
||||
@@ -26,7 +26,7 @@ import com.eliasmueller.llamamod.format.TextChunk;
|
||||
import com.eliasmueller.llamamod.format.Trak;
|
||||
import com.eliasmueller.llamamod.format.Vertex;
|
||||
import com.eliasmueller.llamamod.format.Wad;
|
||||
import com.eliasmueller.llamamod.levelbuild.LevelBuilder;
|
||||
import com.eliasmueller.llamamod.levelbuild.BaseTextures;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
@@ -340,7 +340,7 @@ public final class WadImporter {
|
||||
s.addQuiet(pg);
|
||||
}
|
||||
List<TextChunk.TextureEntry> te = TextChunk.textureEntries(d);
|
||||
Map<Integer, String> names = textureNames(s.settings().base());
|
||||
Map<Integer, String> names = textureNames(d);
|
||||
for (int i = 0; i < te.size(); i++) {
|
||||
TextureEntry t = textureEntry("tex:" + i, te.get(i));
|
||||
t.setRaw("name", names.getOrDefault(i, ""));
|
||||
@@ -383,11 +383,13 @@ public final class WadImporter {
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Names of village textures (levelbuild names) for the outliner; other levels have none. */
|
||||
private static Map<Integer, String> textureNames(String base) {
|
||||
/** Village texture names (levelbuild names) for the outliner, wherever the level has the picture. */
|
||||
private static Map<Integer, String> textureNames(byte[] text) {
|
||||
Map<Integer, String> m = new HashMap<>();
|
||||
if ("t1l1m001".equalsIgnoreCase(base)) {
|
||||
LevelBuilder.TEX.forEach((k, v) -> m.putIfAbsent(v, k));
|
||||
try {
|
||||
BaseTextures.resolve(text).forEach((k, v) -> m.putIfAbsent(v, k));
|
||||
} catch (RuntimeException e) {
|
||||
// unusual TEXT layout: no names
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,9 @@ public final class ViewerDemo {
|
||||
o.put(a, args[++i]);
|
||||
}
|
||||
}
|
||||
Path cd = Path.of(o.getOrDefault("--cd", System.getProperty("user.home") + "/.wine-lama/iso-dk/Setup/Eng/Cd"));
|
||||
String envCd = System.getenv("LLAMAMOD_CD");
|
||||
Path cd = Path.of(o.getOrDefault(
|
||||
"--cd", envCd != null ? envCd : System.getProperty("user.home") + "/.wine-lama/iso-dk/Setup/Eng/Cd"));
|
||||
byte[] wad;
|
||||
if (args[0].equals("testworld")) {
|
||||
byte[] base = Files.readAllBytes(cd.resolve(LevelBuilder.BASE_WAD));
|
||||
|
||||
@@ -7,6 +7,8 @@ import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.KeyEventDispatcher;
|
||||
import java.awt.KeyboardFocusManager;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.InputEvent;
|
||||
@@ -93,6 +95,38 @@ public final class Viewport extends JPanel {
|
||||
private boolean dirty = true;
|
||||
private long lastTick = System.nanoTime();
|
||||
private final Set<Integer> keys = new HashSet<>();
|
||||
|
||||
/**
|
||||
* While flying, WASD/QE/Shift reach the fly loop even if another component (a text field, a
|
||||
* dock) still has the keyboard focus: with some window managers the heavyweight GL canvas does
|
||||
* not get the focus from a right-click.
|
||||
*/
|
||||
private final KeyEventDispatcher flyKeys = e -> {
|
||||
if (!this.flying) {
|
||||
return false;
|
||||
}
|
||||
if (e.getID() == KeyEvent.KEY_PRESSED) {
|
||||
keys.add(e.getKeyCode());
|
||||
} else if (e.getID() == KeyEvent.KEY_RELEASED) {
|
||||
keys.remove(e.getKeyCode());
|
||||
}
|
||||
// modifiers pass on (others track Shift, e.g. the play-here crosshair); typed keys do not
|
||||
int k = e.getKeyCode();
|
||||
return k != KeyEvent.VK_SHIFT && k != KeyEvent.VK_CONTROL && k != KeyEvent.VK_ALT && k != KeyEvent.VK_META;
|
||||
};
|
||||
|
||||
@Override
|
||||
public void addNotify() {
|
||||
super.addNotify();
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(flyKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNotify() {
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(flyKeys);
|
||||
super.removeNotify();
|
||||
}
|
||||
|
||||
private boolean flying;
|
||||
private BufferedImage capture;
|
||||
private boolean captureNext;
|
||||
@@ -1131,6 +1165,7 @@ public final class Viewport extends JPanel {
|
||||
}
|
||||
if (drag == Drag.FLY) {
|
||||
flying = false;
|
||||
keys.clear(); // a key released after the button may have gone to another component
|
||||
canvas.setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
if (drag != Drag.NONE && buttonOf(drag, e)) {
|
||||
|
||||
@@ -191,8 +191,14 @@ public final class EnvMath {
|
||||
"ENDGAME"
|
||||
};
|
||||
|
||||
/** Ids whose {@code .ASF} file the (German) CD does not have: GAMEOVER, TALLY, PUZZLE2, PLATFORM. */
|
||||
private static final List<Integer> NO_FILE = List.of(10, 11, 36, 37);
|
||||
|
||||
public static String musicLabel(int id) {
|
||||
return id >= 0 && id < MUSIC.length ? id + " – " + MUSIC[id] : id + " – (unknown)";
|
||||
if (id < 0 || id >= MUSIC.length) {
|
||||
return id + " – (unknown)";
|
||||
}
|
||||
return id + " – " + MUSIC[id] + (NO_FILE.contains(id) ? " (no file on the CD)" : "");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ lights
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.eliasmueller.llamamod.editor.model.cmd.Command;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.SetProp;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorContext;
|
||||
import com.eliasmueller.llamamod.mods.levels.LevelProjects;
|
||||
import com.eliasmueller.llamamod.save.TestLevelChapters;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.GridLayout;
|
||||
@@ -38,6 +39,7 @@ import javax.swing.JScrollPane;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SpinnerListModel;
|
||||
|
||||
/** Level → Settings: name, chapter, grid size, death plane, WFPC flags, object pool (one undo step). */
|
||||
final class LevelSettingsDialog {
|
||||
@@ -80,9 +82,13 @@ final class LevelSettingsDialog {
|
||||
"Base level",
|
||||
new JLabel(st.base() + " (object code, textures, sounds)"),
|
||||
"Fixed when the project is created (the project stores only the differences to it)");
|
||||
JSpinner chapter = Ui.number(Math.max(1, st.targetChapter()), 1, 49, 1, "0", v -> {});
|
||||
JSpinner chapter = new JSpinner(new SpinnerListModel(TestLevelChapters.all()));
|
||||
chapter.setValue(TestLevelChapters.migrate(st.targetChapter()));
|
||||
JLabel used = new JLabel(usedChapters(ctx, st.targetChapter()));
|
||||
f.row("TestLevel chapter", chapter, "New levels replace this chapter of world 9 (TestLevel)");
|
||||
f.row(
|
||||
"TestLevel chapter",
|
||||
chapter,
|
||||
"New levels replace this chapter of world 9 (TestLevel): 32–49, or 1 (the Test World's)");
|
||||
f.full(used);
|
||||
chapter.setEnabled(st.isNewLevel());
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ final class GeoIcons {
|
||||
private static final Color RED = new Color(0xD93F3F);
|
||||
|
||||
static void register() {
|
||||
Icons.register("prim-box", (g, fg) -> block(g, 2, 5, 12, 8, TOP, SIDE));
|
||||
Icons.register("prim-platform", (g, fg) -> block(g, 1, 4, 14, 4, TOP, SIDE));
|
||||
Icons.register("prim-box", (g, fg) -> block(g, 2, 1, 12, 8, TOP, SIDE));
|
||||
Icons.register("prim-platform", (g, fg) -> block(g, 1, 2.5, 14, 4, TOP, SIDE));
|
||||
Icons.register("prim-floor", (g, fg) -> {
|
||||
Path2D p = poly(1, 10, 8, 6.5, 15, 10, 8, 13.5);
|
||||
g.setColor(TOP);
|
||||
@@ -47,7 +47,7 @@ final class GeoIcons {
|
||||
g.setColor(fg);
|
||||
g.draw(p);
|
||||
});
|
||||
Icons.register("prim-pillar", (g, fg) -> block(g, 5, 2, 6, 12, STONE, STONE.darker()));
|
||||
Icons.register("prim-pillar", (g, fg) -> block(g, 5, 0.5, 6, 12, STONE, STONE.darker()));
|
||||
Icons.register("prim-cylinder", (g, fg) -> {
|
||||
g.setColor(STONE.darker());
|
||||
g.fill(new java.awt.geom.Rectangle2D.Double(2, 6, 12, 5));
|
||||
@@ -122,7 +122,7 @@ final class GeoIcons {
|
||||
Icons.register("prim-invbox", (g, fg) -> {
|
||||
g.setStroke(
|
||||
new BasicStroke(1.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1, new float[] {2, 1.5f}, 0));
|
||||
outlineBlock(g, 2, 5, 12, 8, new Color(255, 60, 60, 60), RED);
|
||||
outlineBlock(g, 2, 1, 12, 8, new Color(255, 60, 60, 60), RED);
|
||||
});
|
||||
Icons.register("geo-collision", (g, fg) -> {
|
||||
Path2D p = poly(1, 10, 8, 6.5, 15, 10, 8, 13.5);
|
||||
@@ -221,6 +221,7 @@ final class GeoIcons {
|
||||
}
|
||||
|
||||
/** Isometric block: x, y of the top-left, width w, side height h. */
|
||||
/** An isometric block filling y .. y + w / 2 + h; keep that within the 16 × 16 icon box. */
|
||||
private static void block(Graphics2D g, double x, double y, double w, double h, Color top, Color side) {
|
||||
Color fg = g.getColor();
|
||||
double d = w / 4;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.eliasmueller.llamamod.editor.tools.geometry;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.model.NodeKind;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.model.SceneEvent;
|
||||
import com.eliasmueller.llamamod.editor.model.TextureEntry;
|
||||
import com.eliasmueller.llamamod.editor.model.TexturePage;
|
||||
import com.eliasmueller.llamamod.editor.render.Legend;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorContext;
|
||||
import com.eliasmueller.llamamod.editor.ui.Widgets;
|
||||
import com.eliasmueller.llamamod.format.TextChunk;
|
||||
import com.eliasmueller.llamamod.levelbuild.BaseTextures;
|
||||
import com.eliasmueller.llamamod.levelbuild.LevelBuilder;
|
||||
import com.eliasmueller.llamamod.util.SwingListeners;
|
||||
import java.awt.BorderLayout;
|
||||
@@ -19,11 +23,12 @@ import java.awt.Image;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.IntConsumer;
|
||||
@@ -145,12 +150,49 @@ final class GeoWidgets {
|
||||
|
||||
private static final Map<Scene, Map<String, BufferedImage>> CACHE = new WeakHashMap<>();
|
||||
|
||||
private static final Map<Scene, Map<String, Integer>> NAMES = new WeakHashMap<>();
|
||||
|
||||
/** Scenes whose texture/page edits clear their cache entries (listener added on first use). */
|
||||
private static final Set<Scene> WATCHED = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
|
||||
static void clearCache(Scene s) {
|
||||
CACHE.remove(s);
|
||||
NAMES.remove(s);
|
||||
}
|
||||
|
||||
private static void watch(Scene s) {
|
||||
if (WATCHED.add(s)) {
|
||||
s.addListener(e -> {
|
||||
if (e.type() == SceneEvent.Type.RELOADED
|
||||
|| e.node().kind() == NodeKind.TEXTURE
|
||||
|| e.node().kind() == NodeKind.TEXTURE_PAGE) {
|
||||
clearCache(s);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Village texture names the scene's textures show (name → index), see {@link BaseTextures}. */
|
||||
static Map<String, Integer> names(Scene s) {
|
||||
if (s == null) {
|
||||
return LevelBuilder.TEX;
|
||||
}
|
||||
watch(s);
|
||||
return NAMES.computeIfAbsent(s, k -> {
|
||||
List<TextChunk.TextureEntry> e = new ArrayList<>();
|
||||
for (TextureEntry t : k.textures()) {
|
||||
e.add(t.toFormat());
|
||||
}
|
||||
List<TextChunk.Page> p = new ArrayList<>();
|
||||
for (TexturePage pg : k.pages()) {
|
||||
p.add(new TextChunk.Page(pg.flags(), pg.width(), pg.height(), pg.pixels()));
|
||||
}
|
||||
return BaseTextures.resolve(e, p);
|
||||
});
|
||||
}
|
||||
|
||||
/** Texture index of a brush texture name (names, "#n", flats → -1), or -1. */
|
||||
static int index(String tex) {
|
||||
static int index(Scene s, String tex) {
|
||||
if (tex == null || tex.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
@@ -161,7 +203,7 @@ final class GeoWidgets {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
Integer i = LevelBuilder.TEX.get(tex);
|
||||
Integer i = names(s).get(tex);
|
||||
return i == null ? -1 : i;
|
||||
}
|
||||
|
||||
@@ -181,13 +223,14 @@ final class GeoWidgets {
|
||||
if (s == null || tex == null || tex.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
watch(s);
|
||||
Map<String, BufferedImage> m = CACHE.computeIfAbsent(s, k -> new LinkedHashMap<>());
|
||||
if (m.containsKey(tex)) {
|
||||
return m.get(tex);
|
||||
}
|
||||
BufferedImage img = null;
|
||||
Color flat = flatColor(tex);
|
||||
int i = index(tex);
|
||||
int i = index(s, tex);
|
||||
if (flat != null) {
|
||||
img = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = img.createGraphics();
|
||||
@@ -240,17 +283,17 @@ final class GeoWidgets {
|
||||
return new ImageIcon(out);
|
||||
}
|
||||
|
||||
/** All texture choices of the scene: named village textures, flat colours, then "#n". */
|
||||
/**
|
||||
* All texture choices of the scene: the village texture names its base has, flat colours, then
|
||||
* "#n".
|
||||
*/
|
||||
static List<String> choices(Scene s) {
|
||||
List<String> l = new ArrayList<>(LevelBuilder.TEX.keySet());
|
||||
Map<String, Integer> names = names(s);
|
||||
List<String> l = new ArrayList<>(names.keySet());
|
||||
l.addAll(new java.util.TreeSet<>(LevelBuilder.FLAT.keySet()));
|
||||
Map<Integer, Boolean> named = new IdentityHashMap<>();
|
||||
for (int v : LevelBuilder.TEX.values()) {
|
||||
named.put(v, true);
|
||||
}
|
||||
if (s != null) {
|
||||
for (int i = 0; i < s.textures().size(); i++) {
|
||||
if (!LevelBuilder.TEX.containsValue(i)) {
|
||||
if (!names.containsValue(i)) {
|
||||
l.add("#" + i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,21 +337,36 @@ public final class ManualContent {
|
||||
return switch (o.type()) {
|
||||
case BOOL -> "on / off";
|
||||
case INT -> o.min() + " – " + o.max();
|
||||
case CHOICE -> {
|
||||
List<String> c = new ArrayList<>();
|
||||
o.choices().forEach(ch -> c.add(ch.label()));
|
||||
yield String.join(", ", c);
|
||||
}
|
||||
case MULTI -> {
|
||||
List<String> c = new ArrayList<>();
|
||||
o.choices().forEach(ch -> c.add(ch.label()));
|
||||
yield "any of " + String.join(", ", c);
|
||||
}
|
||||
case CHOICE -> choiceList(o);
|
||||
case MULTI -> "any of " + choiceList(o);
|
||||
case SEED -> "any text";
|
||||
case STRING -> "text";
|
||||
};
|
||||
}
|
||||
|
||||
/** Choice labels, comma-separated; runs of three or more consecutive numbers as "a – b". */
|
||||
static String choiceList(ModOption o) {
|
||||
List<String> labels = o.choices().stream().map(ModOption.Choice::label).toList();
|
||||
List<String> out = new ArrayList<>();
|
||||
for (int i = 0; i < labels.size(); ) {
|
||||
int j = i;
|
||||
while (j + 1 < labels.size() && consecutive(labels.get(j), labels.get(j + 1))) {
|
||||
j++;
|
||||
}
|
||||
if (j - i >= 2) {
|
||||
out.add(labels.get(i) + " – " + labels.get(j));
|
||||
} else {
|
||||
out.addAll(labels.subList(i, j + 1));
|
||||
}
|
||||
i = j + 1;
|
||||
}
|
||||
return String.join(", ", out);
|
||||
}
|
||||
|
||||
private static boolean consecutive(String a, String b) {
|
||||
return a.matches("-?\\d{1,9}") && b.matches("-?\\d{1,9}") && Integer.parseInt(b) == Integer.parseInt(a) + 1;
|
||||
}
|
||||
|
||||
private static String defaultText(ModOption o) {
|
||||
return switch (o.type()) {
|
||||
case BOOL -> "true".equals(o.defaultValue()) ? "on" : "off";
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.eliasmueller.llamamod.platform.Config;
|
||||
import com.eliasmueller.llamamod.platform.GameLauncher;
|
||||
import com.eliasmueller.llamamod.platform.Json;
|
||||
import com.eliasmueller.llamamod.platform.Progress;
|
||||
import com.eliasmueller.llamamod.save.TestLevelChapters;
|
||||
import com.eliasmueller.llamamod.save.Worlds;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -50,7 +51,7 @@ public final class PlaytestRunner {
|
||||
/** World/chapter of a project: TestLevel chapter, or the original's place; null if not on the map. */
|
||||
public static int[] target(Document d) {
|
||||
if (d.isNewLevel()) {
|
||||
return new int[] {9, Math.max(1, d.scene().settings().targetChapter())};
|
||||
return new int[] {9, TestLevelChapters.migrate(d.scene().settings().targetChapter())};
|
||||
}
|
||||
Matcher m = Pattern.compile("t1l(\\d)m(\\d{3})").matcher(d.base().toLowerCase(Locale.ROOT));
|
||||
if (m.matches()) {
|
||||
|
||||
@@ -40,6 +40,11 @@ public final class Icons {
|
||||
|
||||
private Icons() {}
|
||||
|
||||
/** Names of all registered icons (sorted). */
|
||||
public static java.util.List<String> names() {
|
||||
return PAINTERS.keySet().stream().sorted().toList();
|
||||
}
|
||||
|
||||
public static void register(String name, Painter p) {
|
||||
PAINTERS.put(name, p);
|
||||
CACHE.keySet().removeIf(k -> k.startsWith(name + "@"));
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.eliasmueller.llamamod.editor.ui;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.io.ProjectIO;
|
||||
import com.eliasmueller.llamamod.editor.io.ProjectInfo;
|
||||
import com.eliasmueller.llamamod.editor.io.Templates;
|
||||
import com.eliasmueller.llamamod.mods.levels.LevelProjects;
|
||||
import com.eliasmueller.llamamod.save.TestLevelChapters;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
@@ -12,11 +11,8 @@ import java.awt.Font;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.ButtonGroup;
|
||||
@@ -28,7 +24,7 @@ import javax.swing.JPanel;
|
||||
import javax.swing.JRadioButton;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.SpinnerListModel;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
/** New level: name, base level (object types, textures, sky, music), template, TestLevel chapter. */
|
||||
@@ -51,7 +47,8 @@ final class NewLevelDialog extends JDialog {
|
||||
base.setSelectedItem(l);
|
||||
}
|
||||
}
|
||||
JSpinner chapter = new JSpinner(new SpinnerNumberModel(nextFreeChapter(w), 1, 49, 1));
|
||||
JSpinner chapter = new JSpinner(new SpinnerListModel(TestLevelChapters.all()));
|
||||
chapter.setValue(LevelProjects.nextFreeChapter(w.env().levelsDir()));
|
||||
PicturePanel pic = new PicturePanel(300, 170);
|
||||
Runnable updatePic = () -> {
|
||||
OriginalLevels.Level l = (OriginalLevels.Level) base.getSelectedItem();
|
||||
@@ -75,7 +72,8 @@ final class NewLevelDialog extends JDialog {
|
||||
row,
|
||||
"TestLevel chapter",
|
||||
chapter,
|
||||
"The level replaces this chapter of world 9 (TestLevel) when it is enabled");
|
||||
"The level replaces this chapter of world 9 (TestLevel) when it is enabled (32–49; 1 is the Test"
|
||||
+ " World's)");
|
||||
JPanel templates = new JPanel();
|
||||
templates.setLayout(new BoxLayout(templates, BoxLayout.Y_AXIS));
|
||||
ButtonGroup g = new ButtonGroup();
|
||||
@@ -161,26 +159,6 @@ final class NewLevelDialog extends JDialog {
|
||||
return row + 1;
|
||||
}
|
||||
|
||||
/** First TestLevel chapter from 2 that no project in the levels folder uses. */
|
||||
static int nextFreeChapter(EditorWindow w) {
|
||||
Set<Integer> used = new HashSet<>();
|
||||
for (Path p : LevelProjects.list(w.env().levelsDir())) {
|
||||
try {
|
||||
ProjectInfo i = ProjectIO.info(Files.readAllBytes(p));
|
||||
if (i.isNewLevel()) {
|
||||
used.add(i.targetChapter());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// skip broken files
|
||||
}
|
||||
}
|
||||
int c = 2;
|
||||
while (used.contains(c) && c < 49) {
|
||||
c++;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
Result result() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.eliasmueller.llamamod.editor.model.cmd.AddNode;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Batch;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Command;
|
||||
import com.eliasmueller.llamamod.platform.Json;
|
||||
import com.eliasmueller.llamamod.save.TestLevelChapters;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
@@ -592,7 +593,7 @@ public final class ProjectController {
|
||||
? String.format(
|
||||
Locale.ROOT,
|
||||
"T1L9M%03d",
|
||||
Math.max(1, d.scene().settings().targetChapter()))
|
||||
TestLevelChapters.migrate(d.scene().settings().targetChapter()))
|
||||
: d.base();
|
||||
fc.setSelectedFile(new File(fc.getCurrentDirectory(), stem + ".wad"));
|
||||
if (fc.showSaveDialog(w) != JFileChooser.APPROVE_OPTION) {
|
||||
|
||||
@@ -46,6 +46,10 @@ public final class ExeImage {
|
||||
return (characteristics & 0x20000000) != 0;
|
||||
}
|
||||
|
||||
public boolean writable() {
|
||||
return (characteristics & 0x80000000) != 0;
|
||||
}
|
||||
|
||||
/** Bytes of the section that come from the file. */
|
||||
public long fileBackedSize() {
|
||||
return virtualSize == 0 ? rawSize : Math.min(virtualSize, rawSize);
|
||||
@@ -70,6 +74,9 @@ public final class ExeImage {
|
||||
public record DynamicImport(String owner, String dll, String function, long slot) implements Op {}
|
||||
|
||||
private final byte[] data;
|
||||
/** The file as parsed, before any patch: {@link GameExe} resolves its sites here. */
|
||||
private final byte[] original;
|
||||
|
||||
private final int peOff;
|
||||
private final int optOff;
|
||||
private final long imageBase;
|
||||
@@ -81,9 +88,11 @@ public final class ExeImage {
|
||||
private final Region dataRegion;
|
||||
private final List<Op> journal = new ArrayList<>();
|
||||
private String owner = "base";
|
||||
private GameExe game;
|
||||
|
||||
private ExeImage(byte[] exe) throws ExePatchException {
|
||||
data = exe.clone();
|
||||
original = exe.clone();
|
||||
if (data.length < 0x40 || data[0] != 'M' || data[1] != 'Z') {
|
||||
throw new ExePatchException("Not a Windows executable (no MZ header)");
|
||||
}
|
||||
@@ -157,12 +166,18 @@ public final class ExeImage {
|
||||
return va >= dataRegion.va;
|
||||
}
|
||||
|
||||
/** File offset of a VA in the original file, or -1 (not file-backed, e.g. .bss or the cave). */
|
||||
/**
|
||||
* File offset of a VA in the original file, or -1 (not file-backed, e.g. .bss or the cave). The
|
||||
* PE headers are mapped at the image base, so they have VAs too (section flags can be patched).
|
||||
*/
|
||||
public int fileOffset(long va) {
|
||||
return fileOffset(va, 1);
|
||||
}
|
||||
|
||||
private int fileOffset(long va, int len) {
|
||||
if (va >= imageBase && va + len <= imageBase + headerSize()) {
|
||||
return (int) (va - imageBase);
|
||||
}
|
||||
for (Section s : sections) {
|
||||
if (s.containsFileBacked(va, len)) {
|
||||
return (int) (s.rawOffset() + (va - s.va()));
|
||||
@@ -171,8 +186,11 @@ public final class ExeImage {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** VA of a file offset inside a section, or -1. */
|
||||
/** VA of a file offset inside the headers or a section, or -1. */
|
||||
public long va(int fileOffset) {
|
||||
if (fileOffset >= 0 && fileOffset < headerSize()) {
|
||||
return imageBase + fileOffset;
|
||||
}
|
||||
for (Section s : sections) {
|
||||
if (fileOffset >= s.rawOffset() && fileOffset < s.rawOffset() + s.fileBackedSize()) {
|
||||
return s.va() + (fileOffset - s.rawOffset());
|
||||
@@ -181,6 +199,21 @@ public final class ExeImage {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int headerSize() {
|
||||
return i32(optOff + 60); // SizeOfHeaders
|
||||
}
|
||||
|
||||
/** File offset of the Characteristics field of section {@code name}'s header. */
|
||||
public int sectionFlagsOffset(String name) {
|
||||
int table = optOff + u16(peOff + 20);
|
||||
for (int k = 0; k < sections.size(); k++) {
|
||||
if (sections.get(k).name().equals(name)) {
|
||||
return table + 40 * k + 36;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No section " + name);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- reading
|
||||
|
||||
public byte[] read(long va, int len) {
|
||||
@@ -195,6 +228,26 @@ public final class ExeImage {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Bytes of the file as parsed (before any patch); file-backed section data only. */
|
||||
public byte[] readOriginal(long va, int len) {
|
||||
int off = fileOffsetChecked(va, len);
|
||||
return Arrays.copyOfRange(original, off, off + len);
|
||||
}
|
||||
|
||||
/** Build identification and version-independent sites of this executable. */
|
||||
public GameExe game() {
|
||||
if (game == null) {
|
||||
game = new GameExe(this, original);
|
||||
}
|
||||
return game;
|
||||
}
|
||||
|
||||
/** Like {@link #find(String, String)}, but on the file as parsed and stopping after {@code limit} hits. */
|
||||
public List<Long> findOriginal(String sectionName, String pattern, int limit) {
|
||||
Pattern p = Pattern.of(pattern);
|
||||
return find(original, List.of(section(sectionName)), p.bytes, p.mask, limit);
|
||||
}
|
||||
|
||||
public int readU8(long va) {
|
||||
return read(va, 1)[0] & 0xff;
|
||||
}
|
||||
@@ -237,8 +290,12 @@ public final class ExeImage {
|
||||
}
|
||||
|
||||
private List<Long> find(byte[] pat, boolean[] mask, int limit) {
|
||||
return find(data, sections, pat, mask, limit);
|
||||
}
|
||||
|
||||
private static List<Long> find(byte[] data, List<Section> in, byte[] pat, boolean[] mask, int limit) {
|
||||
List<Long> out = new ArrayList<>();
|
||||
for (Section s : sections) {
|
||||
for (Section s : in) {
|
||||
int start = s.rawOffset();
|
||||
int end = s.rawOffset() + (int) s.fileBackedSize();
|
||||
outer:
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.eliasmueller.llamamod.exe;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Which groove.exe this is, and where the {@link Site}s are in it. Sites are resolved on the file
|
||||
* as parsed (before any patch), so the order in which mods patch does not matter. A site that is
|
||||
* not found exactly once means an unknown program version: {@link #va} throws with a message that
|
||||
* names the build and the supported ones.
|
||||
*/
|
||||
public final class GameExe {
|
||||
/** The known builds. */
|
||||
public enum Build {
|
||||
GERMAN("German", "2001-02-12", 1_642_496, "7832eda4ee47a717e94441f21b75a9f30d11445c9f2408c6135a8fc01812e95b"),
|
||||
ENGLISH("English", "2000-10-17", 1_634_304, "bf0eb8829ed3e371c4e951ece0a7753acf2c28d07001b842e079b098576403b0");
|
||||
|
||||
public final String language;
|
||||
/** PE timestamp (UTC date). */
|
||||
public final String date;
|
||||
|
||||
public final int size;
|
||||
public final String sha256;
|
||||
|
||||
Build(String language, String date, int size, String sha256) {
|
||||
this.language = language;
|
||||
this.date = date;
|
||||
this.size = size;
|
||||
this.sha256 = sha256;
|
||||
}
|
||||
|
||||
public long va(Site s) {
|
||||
return this == GERMAN ? s.german : s.english;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return language + " version of " + date;
|
||||
}
|
||||
}
|
||||
|
||||
private final ExeImage img;
|
||||
private final byte[] original;
|
||||
private final Build build;
|
||||
private final String sha256;
|
||||
private final Map<Site, Long> sites = new EnumMap<>(Site.class);
|
||||
private final Map<Site, Long> matches = new EnumMap<>(Site.class);
|
||||
|
||||
GameExe(ExeImage img, byte[] original) {
|
||||
this.img = img;
|
||||
this.original = original;
|
||||
sha256 = sha256(original);
|
||||
Build b = null;
|
||||
for (Build k : Build.values()) {
|
||||
if (k.sha256.equals(sha256)) {
|
||||
b = k;
|
||||
}
|
||||
}
|
||||
build = b;
|
||||
}
|
||||
|
||||
/** SHA-256 of the file as parsed (hex). */
|
||||
public String sha256() {
|
||||
return sha256;
|
||||
}
|
||||
|
||||
/** The known build, or null for any other groove.exe. */
|
||||
public Build build() {
|
||||
return build;
|
||||
}
|
||||
|
||||
/** "German version of 2001-02-12" or "unknown version (PE date …, … bytes)". */
|
||||
public String describe() {
|
||||
if (build != null) {
|
||||
return build.toString();
|
||||
}
|
||||
int pe = ByteBuffer.wrap(original).order(ByteOrder.LITTLE_ENDIAN).getInt(0x3c);
|
||||
long ts = ByteBuffer.wrap(original).order(ByteOrder.LITTLE_ENDIAN).getInt(pe + 8) & 0xffffffffL;
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"unknown version (PE date %s, %d bytes)",
|
||||
Instant.ofEpochSecond(ts).atZone(ZoneOffset.UTC).toLocalDate(),
|
||||
original.length);
|
||||
}
|
||||
|
||||
/** Address of {@code s} in this executable. */
|
||||
public long va(Site s) throws ExePatchException {
|
||||
Long v = sites.get(s);
|
||||
if (v == null) {
|
||||
v = resolve(s);
|
||||
sites.put(s, v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/** Where the rule of {@code s} matched: its signature match, or the address in its base site. */
|
||||
public long anchor(Site s) throws ExePatchException {
|
||||
va(s);
|
||||
return matches.get(s);
|
||||
}
|
||||
|
||||
/** The original bytes at {@code s} as hex (expected bytes of a patch there). */
|
||||
public String original(Site s, int len) throws ExePatchException {
|
||||
return HexFormat.ofDelimiter(" ")
|
||||
.formatHex(img.readOriginal(va(s), len))
|
||||
.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private long resolve(Site s) throws ExePatchException {
|
||||
Site.Rule r = s.rule;
|
||||
long at;
|
||||
if (r.pattern() != null) {
|
||||
List<Long> hits = img.findOriginal(".text", r.pattern(), 2);
|
||||
if (hits.size() != 1) {
|
||||
throw unknown(s, hits.isEmpty() ? "not found" : "found more than once");
|
||||
}
|
||||
at = hits.getFirst() + r.offset();
|
||||
} else {
|
||||
at = va(r.base()) + r.offset();
|
||||
}
|
||||
matches.put(s, at);
|
||||
long v =
|
||||
switch (r.kind()) {
|
||||
case CODE -> at;
|
||||
case OPERAND -> {
|
||||
long a = u32(at);
|
||||
if (!inSection(a, ".data")) {
|
||||
throw unknown(s, String.format(Locale.ROOT, "address 0x%X is not in .data", a));
|
||||
}
|
||||
yield a;
|
||||
}
|
||||
case CALL_TARGET -> {
|
||||
long t = relTarget(at);
|
||||
if (t < 0) {
|
||||
throw unknown(s, "no call at " + ExeImage.hexVa(at));
|
||||
}
|
||||
yield t;
|
||||
}
|
||||
};
|
||||
if (r.calls() != null && relTarget(v) != va(r.calls())) {
|
||||
throw unknown(s, "does not call " + r.calls().name());
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/** Target of the call/jmp rel32 at {@code va}, or -1. */
|
||||
private long relTarget(long va) {
|
||||
int op = img.readOriginal(va, 1)[0] & 0xff;
|
||||
if (op != 0xE8 && op != 0xE9) {
|
||||
return -1;
|
||||
}
|
||||
return (va + 5 + (int) u32(va + 1)) & 0xffffffffL;
|
||||
}
|
||||
|
||||
private long u32(long va) {
|
||||
return ByteBuffer.wrap(img.readOriginal(va, 4))
|
||||
.order(ByteOrder.LITTLE_ENDIAN)
|
||||
.getInt()
|
||||
& 0xffffffffL;
|
||||
}
|
||||
|
||||
private boolean inSection(long va, String name) {
|
||||
try {
|
||||
ExeImage.Section sec = img.section(name);
|
||||
return va >= sec.va() && va < sec.va() + Math.max(sec.virtualSize(), sec.rawSize());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ExePatchException unknown(Site s, String why) {
|
||||
StringBuilder known = new StringBuilder();
|
||||
for (Build b : Build.values()) {
|
||||
known.append(known.isEmpty() ? "" : ", ").append(b);
|
||||
}
|
||||
return new ExePatchException(String.format(
|
||||
Locale.ROOT,
|
||||
"groove.exe (%s): %s (%s) %s – unknown program version. LlamaMod supports: %s.",
|
||||
describe(),
|
||||
s.name(),
|
||||
s.description,
|
||||
why,
|
||||
known));
|
||||
}
|
||||
|
||||
private static String sha256(byte[] b) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(b));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.eliasmueller.llamamod.exe;
|
||||
|
||||
/**
|
||||
* Named code sites and globals of groove.exe that LlamaMod patches or uses, with the rule that
|
||||
* finds each one in any build ({@link GameExe#va}) and its address in the two known builds
|
||||
* (German 2001-02-12 / English 2000-10-17; GameExeTest checks both).
|
||||
*
|
||||
* <p>Rules: {@code code} = unique match of a byte signature in .text ({@code ??} masks absolute
|
||||
* addresses and rel32 operands, which differ between builds); {@code operand} = the absolute
|
||||
* address stored at an offset of a signature match (or of another site); {@code callTarget} = the
|
||||
* target of the rel32 call/jmp at a site. {@code calling(s)} additionally requires the instruction
|
||||
* at the site to call {@code s}. Globals are never guessed: each is read from an instruction that
|
||||
* uses it. To support another build see notes/llamamod-app.md ("groove.exe versions").
|
||||
*/
|
||||
public enum Site {
|
||||
// boot to level
|
||||
MENU_FRAME(
|
||||
code("A1 ?? ?? ?? ?? 57 33 FF 3B C7 0F 85 ?? ?? ?? ??"),
|
||||
0x549C50,
|
||||
0x549990,
|
||||
"state 2 (main menu) frame handler, starts with mov eax,[dword]"),
|
||||
GAME_FRAME(
|
||||
code("A1 ?? ?? ?? ?? 85 C0 0F 85 ?? ?? ?? ?? 83 3D ?? ?? ?? ?? 01"),
|
||||
0x549F40,
|
||||
0x549C80,
|
||||
"state 3 (in game) frame handler, starts with mov eax,[dword]"),
|
||||
DEMO_SWITCH(
|
||||
code(
|
||||
"E8 ?? ?? ?? ?? 8B 15 ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 52 E8 ?? ?? ?? ?? 83 C4 04 E8 ?? ?? ?? ?? A1 ?? ?? ?? ?? 83 F8 04 75 58 8B 0D ?? ?? ?? ?? A3 ?? ?? ?? ?? 8B C1 C1 E0 04 41 8B 90 ?? ?? ?? ?? 83 F9 03 89 15 ?? ?? ?? ?? 8B 90 ?? ?? ?? ?? 89 15 ?? ?? ?? ?? 8B 90 ?? ?? ?? ?? 8B 80 ?? ?? ?? ?? 89 15 ?? ?? ?? ?? A3 ?? ?? ?? ?? 89 0D ?? ?? ?? ??"),
|
||||
0x549E47,
|
||||
0x549B87,
|
||||
"main menu: music shutdown and switch to the attract demo (state 2 -> 4)"),
|
||||
MUSIC_STOP(callTarget(DEMO_SWITCH), 0x546CC0, 0x546AF0, "music shutdown, part 1 (call at DEMO_SWITCH)"),
|
||||
MUSIC_TRACK(operandAt(DEMO_SWITCH, 7), 0x6D94B4, 0x6D8B44, "current music track (pushed for MUSIC_PLAY)"),
|
||||
MUSIC_ID(operandAt(DEMO_SWITCH, 13), 0x586430, 0x585AC0, "music id, zeroed before the switch"),
|
||||
MUSIC_PLAY(callTarget(DEMO_SWITCH, 18), 0x546990, 0x5467C0, "music shutdown, part 2 (cdecl, 1 argument)"),
|
||||
MUSIC_UPDATE(callTarget(DEMO_SWITCH, 26), 0x5464D0, 0x546350, "music shutdown, part 3"),
|
||||
NEXT_STATE(operandAt(DEMO_SWITCH, 32), 0x584700, 0x583D90, "requested next game state"),
|
||||
STATE(
|
||||
operandAt(DEMO_SWITCH, 48),
|
||||
0x5846E4,
|
||||
0x583D74,
|
||||
"game state (2 main menu, 3 in game, 6 save slots, 12 world map)"),
|
||||
TRIBE(operandAt(DEMO_SWITCH, 69), 0x584EB4, 0x584544, "current tribe"),
|
||||
WORLD(operandAt(DEMO_SWITCH, 81), 0x584EB8, 0x584548, "current world"),
|
||||
CHAPTER(operandAt(DEMO_SWITCH, 99), 0x584EBC, 0x58454C, "current chapter"),
|
||||
MAP(operandAt(DEMO_SWITCH, 104), 0x584EC0, 0x584550, "current map"),
|
||||
READ_SLOTS(
|
||||
code("81 EC 00 01 00 00 53 56 57 33 DB BF ?? ?? ?? ??"),
|
||||
0x41C0E0,
|
||||
0x41C050,
|
||||
"reads the three save slot files"),
|
||||
LOAD_SLOT(
|
||||
code("A1 ?? ?? ?? ?? C7 05 ?? ?? ?? ?? 00 00 00 00 C1 E0 08"),
|
||||
0x41CC70,
|
||||
0x41CB80,
|
||||
"loads save slot SLOT (or starts a new game in it)"),
|
||||
SUB_STATE(operand("C7 05 ?? ?? ?? ?? 04 00 00 00 EB 10", 2), 0x584638, 0x583CC8, "menu sub-state"),
|
||||
MAP_TRIBE(operand("A0 ?? ?? ?? ?? 8D BA ?? ?? ?? ?? C1 E9 0C", 1), 0x585588, 0x584C18, "world map: tribe"),
|
||||
MAP_WORLD(operand("89 15 ?? ?? ?? ?? A3 ?? ?? ?? ?? 5E", 2), 0x584C24, 0x5842B4, "world map: world"),
|
||||
MAP_CHAPTER(
|
||||
operand("8B 15 ?? ?? ?? ?? A3 ?? ?? ?? ?? A0 ?? ?? ?? ??", 2), 0x584C20, 0x5842B0, "world map: chapter"),
|
||||
MAP_MAP(operand("A3 ?? ?? ?? ?? 5E 83 C4 60 C3 90 90", 1), 0x58557C, 0x584C0C, "world map: map"),
|
||||
FROM_MAIN_MENU(
|
||||
operand("39 1D ?? ?? ?? ?? 74 6E A1 ?? ?? ?? ??", 2),
|
||||
0x5855B0,
|
||||
0x584C40,
|
||||
"set when the save slots are opened from the main menu"),
|
||||
SLOT(operand("A1 ?? ?? ?? ?? 83 F8 FF 75 31 33 C9", 1), 0x584C28, 0x5842B8, "selected save slot (0-2)"),
|
||||
PRESSED(
|
||||
operand("A1 ?? ?? ?? ?? 25 00 40 00 00 74 36", 1),
|
||||
0x5FCF08,
|
||||
0x5FC598,
|
||||
"control bits pressed this frame (0x8 = pause)"),
|
||||
// cheats
|
||||
STARTUP_CALL(
|
||||
code("E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 84 C0 75 15"),
|
||||
0x41D3EA,
|
||||
0x41D2BA,
|
||||
"WinMain: call right after the command line was parsed"),
|
||||
STARTUP_TARGET(callTarget(STARTUP_CALL), 0x41C050, 0x41BFC0, "function called at STARTUP_CALL"),
|
||||
CHEAT_FLAGS(
|
||||
operand("8B 0D ?? ?? ?? ?? 85 C8 75 3A 6A 08", 2),
|
||||
0x584710,
|
||||
0x583DA0,
|
||||
"cheat bits toggled by the main-menu codes"),
|
||||
DISPLAY_DIAGNOSTICS(
|
||||
operand("C6 05 ?? ?? ?? ?? 01 68 ?? ?? ?? ??", 2),
|
||||
0x5834D0,
|
||||
0x582B60,
|
||||
"-display_diagnostics switch (byte)"),
|
||||
// intro skip
|
||||
LOGOS(
|
||||
code("68 ?? ?? ?? ?? E8 ?? ?? ?? ?? A1 ?? ?? ?? ?? 83 C4 08"),
|
||||
0x41D56E,
|
||||
0x41D43E,
|
||||
"WinMain: push offset disneylogompeg.dat, first instruction of the logo block"),
|
||||
AFTER_LOGOS(
|
||||
code("E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 6A 2C"),
|
||||
0x41D5D3,
|
||||
0x41D4A3,
|
||||
"WinMain: first instruction after the logo block"),
|
||||
DEMO_IDLE_JBE(
|
||||
code("76 41 A1 ?? ?? ?? ?? 6A 01 A3 ?? ?? ?? ??"),
|
||||
0x549DC0,
|
||||
0x549B00,
|
||||
"main menu: jbe of the idle check (1800 frames -> attract demo)"),
|
||||
STORY_INTRO(
|
||||
code("E8 ?? ?? ?? ?? A1 ?? ?? ?? ?? 83 C4 08 83 F8 04"),
|
||||
0x54AEC9,
|
||||
0x54ABD9,
|
||||
"call of the movie player for %d%d%dINTRO.DAT"),
|
||||
MOVIE_PLAYER(callTarget(STORY_INTRO), 0x41F630, 0x41F4B0, "movie player (called at STORY_INTRO)"),
|
||||
STORY_EULOGY(
|
||||
code("E8 ?? ?? ?? ?? A1 ?? ?? ?? ?? 68 ?? ?? ?? ?? A3 ?? ?? ?? ??").calling(MOVIE_PLAYER),
|
||||
0x54B138,
|
||||
0x54AE48,
|
||||
"call of the movie player for EULOGY.DAT"),
|
||||
STORY_OUTRO(
|
||||
code("E8 ?? ?? ?? ?? 68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 6A 03").calling(MOVIE_PLAYER),
|
||||
0x54B26F,
|
||||
0x54AF7F,
|
||||
"call of the movie player for %d%d%dOUTRO.DAT"),
|
||||
// practice
|
||||
TICK_CALL(
|
||||
code("E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 8B 15 ?? ?? ?? ?? A1 ?? ?? ?? ?? 42"),
|
||||
0x54B77E,
|
||||
0x54B48E,
|
||||
"game loop: per-tick call of STRAT_UPDATE"),
|
||||
STRAT_UPDATE(callTarget(TICK_CALL), 0x54C4A0, 0x54C1B0, "strat update (one logic tick of all objects)"),
|
||||
PLAYER(operand("A1 ?? ?? ?? ?? 83 C4 0C 46 83 C7 10", 1), 0x6D9E1C, 0x6D94A4, "player (llama) strat pointer"),
|
||||
// free camera
|
||||
CAM_CALL_RUNNING(
|
||||
code("E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 5F 5E 5D C3 89 2D ?? ?? ?? ??"),
|
||||
0x54B79F,
|
||||
0x54B4AF,
|
||||
"frame: call of CAM_UPDATE while running"),
|
||||
CAM_UPDATE(callTarget(CAM_CALL_RUNNING), 0x54BAD0, 0x54B7E0, "copies the camera strat into the render camera"),
|
||||
CAM_CALL_PAUSED(
|
||||
code("E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 5F 5E 5D C3 90").calling(CAM_UPDATE),
|
||||
0x54B7E0,
|
||||
0x54B4F0,
|
||||
"frame: call of CAM_UPDATE while paused"),
|
||||
TICK_AFTER_STRATS(
|
||||
code("E8 ?? ?? ?? ?? 8B 15 ?? ?? ?? ?? A1 ?? ?? ?? ?? 42"),
|
||||
0x54B783,
|
||||
0x54B493,
|
||||
"tick loop: call of PARTICLES right after TICK_CALL"),
|
||||
PARTICLES(callTarget(TICK_AFTER_STRATS), 0x4287C0, 0x428630, "particle update"),
|
||||
AFTER_INPUT(
|
||||
code("E8 ?? ?? ?? ?? A0 ?? ?? ?? ?? 84 C0 74 09"),
|
||||
0x54B5FD,
|
||||
0x54B30D,
|
||||
"frame: call right after the frame's input read"),
|
||||
AFTER_INPUT_TARGET(callTarget(AFTER_INPUT), 0x427C40, 0x427AB0, "function called at AFTER_INPUT"),
|
||||
PAD_BITS(
|
||||
code("8B 44 24 04 83 EC 08 53 33 DB 3B C3"),
|
||||
0x4256C0,
|
||||
0x425540,
|
||||
"pad structs -> control bits (int argument)"),
|
||||
REGIONS_CALL_FRAME(
|
||||
code("E8 ?? ?? ?? ?? A1 ?? ?? ?? ?? BF 04 00 00 00"),
|
||||
0x54B614,
|
||||
0x54B324,
|
||||
"frame: call of REGIONS before the ticks"),
|
||||
REGIONS(callTarget(REGIONS_CALL_FRAME), 0x41E020, 0x41DEA0, "region mask (rooms to draw) from the camera cell"),
|
||||
REGIONS_CALL_RUNNING(
|
||||
code("E8 ?? ?? ?? ?? 5F 5E 5D C3 89 2D ?? ?? ?? ??").calling(REGIONS),
|
||||
0x54B7A4,
|
||||
0x54B4B4,
|
||||
"frame: call of REGIONS after CAM_CALL_RUNNING"),
|
||||
REGIONS_CALL_PAUSED(
|
||||
code("E8 ?? ?? ?? ?? 5F 5E 5D C3 90 90 E9 ?? ?? ?? ??").calling(REGIONS),
|
||||
0x54B7E5,
|
||||
0x54B4F5,
|
||||
"frame: call of REGIONS after CAM_CALL_PAUSED"),
|
||||
STRAT_RESET(
|
||||
code(
|
||||
"89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ?? 89 3D ?? ?? ?? ??"),
|
||||
0x54D0D5,
|
||||
0x54CDE5,
|
||||
"level start (strat system reset): mov [dword],edi"),
|
||||
TICKS(
|
||||
operand("89 35 ?? ?? ?? ?? 8B 0D ?? ?? ?? ??", 2),
|
||||
0x5724D4,
|
||||
0x5724D8,
|
||||
"logic ticks this frame (30 per second)"),
|
||||
PAUSED(operand("A1 ?? ?? ?? ?? 83 C4 10 85 C0 74 53", 1), 0x584640, 0x583CD0, "pause flag"),
|
||||
CONTROLS(operand("A3 ?? ?? ?? ?? 7E 0A B8 10 00 00 00", 1), 0x5FCF20, 0x5FC5B0, "held control bits"),
|
||||
STICK(operand("8B 3D ?? ?? ?? ?? 83 FF 2A 0F 83 ?? ?? ?? ??", 2), 0x5FCEFC, 0x5FC58C, "stick magnitude"),
|
||||
DEMO(operand("8B 0D ?? ?? ?? ?? 3B CB 89 1D ?? ?? ?? ??", 2), 0x5FCF3C, 0x5FC5CC, "demo playback active"),
|
||||
PAD_COUNT(operand("3B 2D ?? ?? ?? ?? 0F 83 ?? ?? ?? ??", 2), 0x5FCF6C, 0x5FC5FC, "number of pad structs"),
|
||||
PADS(operand("89 3D ?? ?? ?? ?? 75 04 32 C0 5F C3", 2), 0x5FCF70, 0x5FC600, "pad structs (12 bytes each)"),
|
||||
PITCH(
|
||||
operand("A3 ?? ?? ?? ?? A1 ?? ?? ?? ?? 89 0D ?? ?? ?? ??", 1),
|
||||
0x5790A0,
|
||||
0x5790A0,
|
||||
"render camera pitch (4096 = 360 degrees)"),
|
||||
YAW(operand("66 8B 0D ?? ?? ?? ?? 89 1D ?? ?? ?? ??", 3), 0x5790A4, 0x5790A4, "render camera yaw"),
|
||||
CAM_X(operand("8B 15 ?? ?? ?? ?? 8B 01 8B 49 08 2B C2", 2), 0x5790B0, 0x5790B0, "render camera position x, y, z"),
|
||||
TARGET_X(
|
||||
operand("68 ?? ?? ?? ?? E8 ?? ?? ?? ?? 8B 54 24 1C", 1),
|
||||
0x6D9D80,
|
||||
0x6D9408,
|
||||
"render camera target x, y, z"),
|
||||
SINE(operand("8B 1C 8D ?? ?? ?? ?? 8B C8 0F AF CA", 3), 0x574318, 0x574318, "s32[4096] sine table, 4096 = 1.0"),
|
||||
// base patch: level record relocation
|
||||
COINS(
|
||||
operand("8B 34 B5 ?? ?? ?? ?? C1 EE 0C 3B F2", 3),
|
||||
0x584F1C,
|
||||
0x5845AC,
|
||||
"u32 coin counters [world*6 + chapter], 54 rows"),
|
||||
FLAGS(
|
||||
operand("BE ?? ?? ?? ?? C6 82 ?? ?? ?? ?? 01", 1),
|
||||
0x584EC8,
|
||||
0x584558,
|
||||
"u8 chapter flags [world*6 + chapter], 54 rows");
|
||||
|
||||
final Rule rule;
|
||||
/** Address in the German build (2001-02-12). */
|
||||
public final long german;
|
||||
/** Address in the English build (2000-10-17). */
|
||||
public final long english;
|
||||
|
||||
public final String description;
|
||||
|
||||
Site(Rule rule, long german, long english, String description) {
|
||||
this.rule = rule;
|
||||
this.german = german;
|
||||
this.english = english;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
enum Kind {
|
||||
CODE,
|
||||
OPERAND,
|
||||
CALL_TARGET
|
||||
}
|
||||
|
||||
/** {@code pattern} null: relative to {@code base}. */
|
||||
record Rule(Kind kind, String pattern, Site base, int offset, Site calls) {
|
||||
Rule calling(Site s) {
|
||||
return new Rule(kind, pattern, base, offset, s);
|
||||
}
|
||||
}
|
||||
|
||||
private static Rule code(String pattern) {
|
||||
return new Rule(Kind.CODE, pattern, null, 0, null);
|
||||
}
|
||||
|
||||
private static Rule operand(String pattern, int offset) {
|
||||
return new Rule(Kind.OPERAND, pattern, null, offset, null);
|
||||
}
|
||||
|
||||
private static Rule operandAt(Site base, int offset) {
|
||||
return new Rule(Kind.OPERAND, null, base, offset, null);
|
||||
}
|
||||
|
||||
private static Rule callTarget(Site base) {
|
||||
return callTarget(base, 0);
|
||||
}
|
||||
|
||||
private static Rule callTarget(Site base, int offset) {
|
||||
return new Rule(Kind.CALL_TARGET, null, base, offset, null);
|
||||
}
|
||||
}
|
||||