diff --git a/README.md b/README.md index 56ee863..6c97470 100644 --- a/README.md +++ b/README.md @@ -1,268 +1,129 @@ -# levelbau – level modding kit for *The Emperor's New Groove* (PC, 2000) +# LlamaMod – mods and a level editor for *The Emperor's New Groove* (PC, 2000) German title: *Ein Königreich für ein Lama*. PC port by Argonaut, built on the Croc 2 engine. -This kit came out of an AI-assisted reverse-engineering project. It contains: +**LlamaMod** is a one-click mod manager and a full 3D **level editor** for the PC game, for +**Windows and Linux**. It installs the game from your own CD or ISO into its own folder, applies +mods without ever touching the original files, and lets you build new levels, edit the original +ones and share them as small files. Everything is based on a complete reverse engineering of the +game's level formats and its object scripts ("strats"). -* **Format documentation** for the level files (`*.wad`): geometry, collision, object - placement, textures, waypoints, visibility zones, and what makes the level exit work. -* **Python tools** that read, export and rebuild those files byte-exactly. -* **A level builder** (`tools/levelbuild.py`). You describe a level as a short Python script - (boxes, ramps, stairs, fences, trees, sea, objects) and it writes a complete, playable WAD. -* **An example level, "Testwelt"** (`levels/testwelt.py`). It goes into the hidden, empty - *TestLevel* slot (world 9, chapter 1) and has its own geometry, a gallery with every - object type on pedestals, an animal pen and a small parkour with a working exit. -* **A debug patch generator** (`tools/debugviz.py`). It makes collision visible in every - level, including invisible walls, along with invisible trigger and camera objects. -* **An automated in-game tester** (`tools/lamatest.py`). On Linux it runs the game under - Wine on a virtual display, loads the test level, takes screenshots and detects crashes. +| | | +|---|---| +| ![LlamaMod launcher](docs/img/launcher.jpg) | ![Level editor](docs/img/editor.jpg) | +| **Launcher**: mods with options, save-game editor, one-click play | **Level editor**: brushes, tiles, meshes, objects, textures, play-test with F5 | +| ![Test World gallery](docs/img/testworld-gallery.jpg) | ![Alligator Arena](docs/manual/img/mod_arena.jpg) | +| **Test World**: all 253 figures and objects of the game in one level | **Alligator Arena**: a minigame with new, scripted enemies | +| ![Debug view](docs/img/debug-view.jpg) | ![glTF in Blender](docs/img/gltf-blender.jpg) | +| **Debug view**: collision and invisible objects in the running game | **glTF/OBJ** export and import for Blender | -Nothing from the game itself is included: no game files, no executable, no decompiled -code, no built WADs. Every tool works on **your own copy** of the game. +## Features -> The notes in `notes/` are detailed and technical. Handing them to an AI assistant is the -> recommended way to use this kit (see the last section). +* **Mods** – tick, *Apply*, *Play*: Test World, Alligator Arena, your own levels, Boot to level, + Intro skip, Mirror mode, Practice tools (save/restore position, extra checkpoints), Randomizer, + Draw distance, Built-in cheats, Debug wireframe, Free camera & noclip. +* **Level editor** – new levels from templates or editing the original levels; brushes, the base + level's tiles, OBJ/glTF meshes; textures (import, paint, animate); objects from **every world**; + waypoints, cameras, triggers; fog, music, lights, visibility regions; strat scripts (read, + change numbers, write new object types); checks for problems; **F5** starts the game directly in + the level. +* **Sharing** – a level is a small `.llamalevel` file with only your changes and no game data; the + receiver's LlamaMod builds it from their own copy. New levels live in the hidden *TestLevel* + world. +* **Save games** – unlock worlds and chapters, coins, collectibles; backups before every change. +* **Safe** – the original CD/ISO and files are never changed; *Restore original* switches all mods + off. ---- +## Get it -## Requirements +You need **your own copy of the game** (CD or ISO image). Development used the German CD; other +language versions should work but are untested. -* Your own copy of the game. Development used the **German CD version**. Other language - versions should use the same format, but this hasn't been tested. -* Python 3.8+ with `numpy` and `Pillow`. Optional: `capstone` (disassembler helper). -* For the automated in-game tests only: Linux, Wine, Xvfb, xdotool, ImageMagick, tesseract - with German language data, and PulseAudio (`pactl`). -* Optional: Blender (preview renders), and Ghidra (to regenerate the decompile, see below). - -The builder and the export tools are plain Python, so they should also work on Windows. -The test harness is Linux/Wine-only. - -### Paths – adjust these - -The tools were written for one Linux/Wine setup and some paths are hard-coded at the top of -the scripts: - -| Script | Variable | Meaning | +| System | Package | Start | |---|---|---| -| `tools/levelbuild.py` | `WADS` | folder with the original level WADs (the `wads` folder of the game CD) | -| `tools/levelbuild.py` | `MODWAD` | where `--install` puts the built level (`T1L9M001.wad`) | -| `tools/debugviz.py` | `CD_WADS` | same as `WADS` | -| `tools/lamatest.py` | `PREFIX`, `LAMA_MOD` | Wine prefix and the overlay script | +| Windows | `LlamaMod--full-windows-x64.zip` | unzip, start `LlamaMod.exe` (no installation, no admin rights) | +| Linux | `LlamaMod--full-linux-x64.tar.gz` | unpack, start `./LlamaMod`; install **Wine** to run the game | -The game reads its level data from the CD drive, in the `wads` folder of the CD. To mod it, -make a copy of the CD data (or an overlay of symlinks, see `tools/lama-mod`), point the CD -drive or `CDPath` at that copy, and put modified WADs into its `wads` folder. +Both bring their own Java runtime; the editor needs OpenGL 3.3. A *launcher* edition without the +editor is also built. What changed: [CHANGELOG.md](CHANGELOG.md). ---- +### Build from source -## Folder layout +Needs a JDK 25 (toolchain) and Gradle running on Java 21 (for Spotless): -``` -README.md this file -notes/ format documentation and findings - geometry.md TRAK/MAP geometry, collision polygons, coordinate spaces, zones, limits - objects.md MAP chunk layout, strat (object) entries, waypoints, player start, exit - strat_types_table.md every object type in every original level, with counts - textures.md TEXT chunk: texture pages, RLE16, texture table, animations - levelbau.md how to use levelbuild.py, measured player physics, exit logic - testharness.md the automated Wine/Xvfb tester - debugviz.md debug patch: colour legend, what is shown, limits - img/ screenshots and preview renders -tools/ - geomlib.py core library: WAD, TRAK, STPC models, MAP geometry, TEXT, collision - geom_export.py WAD -> OBJ/MTL (+ textures, vertex colours, collision.obj, objects.obj) - geom_import.py OBJ -> new geometry inside a WAD copy - mapchunk.py MAP chunk parser/writer (byte-exact on all original levels) - strats.py, strat_table.py, stpcwalk.py, map_dump.py object types and strat code helpers - textchunk.py, text_export.py, texsheet.py textures: export/import PNG, contact sheets - levelbuild.py the level builder (see notes/levelbau.md) - debugviz.py debug patch generator (collision/trigger visualisation) - lamatest.py automated in-game test on a virtual display (Linux/Wine) - lama-mod shell script: builds a CD overlay with modded WADs (Wine setup) - vorschau/ Blender preview render scripts - disx86.py small disassembler helper for groove.exe (needs capstone) - wadtool.py minimal WAD chunk lister/extractor/replacer (used by several tools) - cor2png.py, png2cor.py loading/concept pictures (*.COR, LZ-compressed 640x480) <-> PNG - asf2wav.py music (*.ASF, 4-bit ADPCM) -> WAV -levels/ - testwelt.py the example test world - testwelt_parkour.seq scripted run through the parkour (used with lamatest.py) - sprunglabor.py "jump lab": test lanes used to measure jump height/width -re/ghidra_scripts/ - DumpAll.java Ghidra headless script: decompiles every function to one C file +```bash +./gradlew spotlessApply build fatJar # tests + build/libs/llamamod--all.jar +java -jar build/libs/llamamod-*-all.jar # LlamaMod +java -jar build/libs/llamamod-*-all.jar --editor # the level editor alone +./gradlew packageAll # Linux + Windows packages, full and launcher edition ``` ---- +Details: [For developers](docs/developers.md). -## Quick start +## Documentation + +**[docs/](docs/README.md)** – start there. + +* **[Build your own level](docs/guide/README.md)** – a step-by-step guide for newcomers. +* **[Level editor manual](docs/manual/index.md)** – every tool and dialog, with tutorials (also + built into the editor: **F1**). +* **[Reference](docs/reference/README.md)** – file formats (WAD, geometry, collision, objects, + textures, music), TestLevel, engine limits. +* **[Strats](docs/strats/README.md)** – how the game's object scripts work, the script language, + custom object types, and the [Alligator Arena walkthrough](docs/strats/arena.md). +* **[Tools](docs/tools.md)** – the app's command line, the Python kit, in-game test harnesses. +* **[Lab notes](notes/README.md)** – the detailed engineering notebook. + +## The Python kit + +The project started as a Python reverse-engineering kit, which is still in [`tools/`](tools/): +byte-exact parsers and writers for every level chunk, OBJ export/import, a script-driven level +builder ([`tools/levelbuild.py`](tools/levelbuild.py), example [`levels/testwelt.py`](levels/testwelt.py)), +texture and picture converters, a music decoder, the debug-view generator and an automated +in-game tester for Linux/Wine. Python 3.8+ with `numpy` and `Pillow`. ```bash # export a level to OBJ (+ collision and object markers) python3 tools/geom_export.py /path/to/cd/wads/t1l1m001.wad out_village --collision --objects -# -> level.obj/.mtl, texture pages, collision.obj, objects.obj, preview images - -# build the example test world -python3 tools/levelbuild.py levels/testwelt.py testwelt.wad +# build the original test world from its script +python3 tools/levelbuild.py levels/testwelt.py testwelt.wad --base /path/to/cd/wads/t1l1m001.wad python3 tools/levelbuild.py --types # object types you can place -python3 tools/levelbuild.py --textures tex.png # texture sheet with the names you can use - -# build the debug patch for all levels -python3 tools/debugviz.py --help +# music to WAV, loading picture to PNG +python3 tools/asf2wav.py /path/to/cd/music/Village.asf village.wav +python3 tools/cor2png.py /path/to/cd/t111.cor t111.png ``` -### Playing the test level +Some scripts have hard-coded paths of the original Wine setup at the top (`WADS`, `MODWAD` in +`levelbuild.py`, `CD_WADS` in `debugviz.py`, `PREFIX`/`LAMA_MOD` in `lamatest.py`) – adjust them or +pass `--base`/`--src`. All tools: [docs/tools.md](docs/tools.md). -The game has a hidden world 9, *TestLevel*, with 49 chapter slots, but the retail game ships -no data for it. Put your built WAD into the CD's `wads` folder as `T1L9M001.wad`. Also add a -loading picture `T191.COR`, for example a copy of another `T1xx.COR`. +### Key facts in one minute -To reach world 9, edit a save file (`king0.ksv`…`king2.ksv`, 256 bytes): +* **The world is made of tiles.** TRAK holds 100–650 small models (render triangles + collision + polygons); MAP places them on a W × H grid with height and rotation, plus baked vertex colours + and a 32-region visibility table. No BSP, no single world mesh. +* **Objects are "strats"**: compiled scripts in STPC. An object's type is its byte offset into + STPC, so a level can only use the types its own STPC contains (or types merged in from other + levels). +* **Scale**: y up, one unit per grid cell; the llama is 0.6 tall, walks 1.1 units/s, steps up + nothing and jumps ~1.3 far. +* **The exit** fires only when the llama stands in the LevelDoor's grid cell **on floor with + collision material 16**. +* **Textures** are PlayStation-style 15-bit pixels on 256 × 256 pages; there are no free UVs. -* `+0x04/+0x05` = map cursor (world, chapter) -* `+0x3f/+0x40` = current world/chapter -* `+0x42/+0x43` = highest unlocked world/chapter +More: [docs/reference](docs/reference/README.md). -Set them to world 9, chapter 1. +### Building on this with an AI assistant -When you leave through the exit, the game unlocks the next chapter. `levelbuild.py --install` -therefore links chapters 2–49 to the same file, so that picking one on the map doesn't crash. +This whole project was made by an AI coding agent (Claude Code). To continue it, give an assistant +the [lab notes](notes/README.md) first – they contain offsets, field meanings, confidence levels and +pitfalls – and let it regenerate the decompile of `groove.exe` with Ghidra for questions about game +logic (headless `DumpAll` script, see [Tools](docs/tools.md#reverse-engineering)). Always test in the +game (`tools/apptest.py`) and change one thing at a time: the engine crashes on inconsistent data. -### Debug patch: visible collision and invisible walls +## Legal -```bash -python3 tools/debugviz.py --all debug_wads --src /path/to/cd/wads # all 33 level WADs -python3 tools/debugviz.py t1l1m001.wad t1l1m001_debug.wad # a single level -python3 tools/debugviz_check.py debug_wads /path/to/cd/wads # offline sanity check -``` - -Put the resulting WADs into the CD's `wads` folder in place of the originals, keeping the -same file names. On the Wine setup, `lama-mod debug an|aus` toggles them. The patch only -adds render geometry. Collision, objects, waypoints and zones stay byte-identical, so -gameplay is unchanged. - -Legend (full version in `notes/debugviz.md`): - -| Colour | Meaning | -|---|---| -| green / yellow-green wireframe | walkable floor / slope | -| blue | wall | -| orange | ceiling / overhang | -| **red, semi-transparent fill** | **invisible collision**: invisible walls and floors | -| magenta lines | death plane ("no floor") | -| violet fill | level exit floor (material 16) | -| other colours | special collision materials | - -| Marker | Meaning | -|---|---| -| white arrow | player start | -| violet cube | exit | -| turquoise cube | checkpoint | -| cyan cube | camera | -| yellow cube | trigger / cutscene | -| orange cube | force or special zone | -| gold cube | secret zone | -| grey cube | other invisible helper object | -| pink wire box | ambience / music / fog zone | - -Cubes sit on a stick, and a horizontal line shows the object's facing. Wire boxes show -trigger areas. - -Status: 19 of 33 levels were loaded in the game without problems. The rest -(catacombs/roller coaster world 7, lab world 8, two others) passed the offline check only. -Collision of moving objects (platforms, doors) is not shown. - ---- - -## LlamaMod and the level editor - -Besides the Python kit, this repository contains **LlamaMod** (Java 25, Swing; `src/`), a -one-click mod manager for the game on **Windows and Linux**, and a full **3D level editor** -built into it. Nothing from the game is included: the wizard installs the game from your own -CD/ISO into LlamaMod's data folder, and all mods are built from that copy. - -* **Mods** – tick mods, click *Apply*/*Play*: debug wireframe, Test World, randomizer, fog, - mirror mode, intro skip, cheats, practice tools, free camera, *boot to level* … Mods may change - level files and `groove.exe` (code caves); the original files are never touched. - Developer notes: `notes/llamamod-app.md`, one `notes/mod-*.md` per mod. -* **Level editor** (*Level Editor* button, or `LlamaMod --editor`): create new levels from - templates, brushes and the base level's tile models and objects, or edit the original - levels; import OBJ/glTF meshes and PNG/JPG textures (texture manager, paint-on-faces, - animations); waypoints, zones, fog, lights, visibility regions; **Play (F5)** builds the - level and starts the game directly in it (Escape returns to the editor). -* **Sharing**: a level is a small `.llamalevel` file with only your changes. Others import it in - LlamaMod (*Mods → Levels → Import level*) and it is built from their own game copy; new levels - appear in the hidden *TestLevel* world (chapters 1–49). - -User guide (English): **`notes/editor-guide.md`**. In the editor: *Help → Getting started* -and *Help → Shortcut sheet*. Technical notes: `notes/editor-design.md`, `editor-api.md`, -`editor-format.md`, `editor-render.md`, `editor-ui.md`, `editor-tools-*.md`, -`editor-playtest.md`. - -```bash -./gradlew build fatJar # tests + build/libs/llamamod--all.jar -./gradlew packageLinux # build/dist/LlamaMod--full-linux-x64.tar.gz (own Java runtime) -./gradlew packageWindows # build/dist/LlamaMod--full-windows-x64.zip (LlamaMod.exe + runtime) -./gradlew packageAll # both systems, full and launcher (no editor) edition -java -jar build/libs/llamamod-*-all.jar # LlamaMod -java -jar build/libs/llamamod-*-all.jar --editor # the level editor alone -``` - -![The level editor](notes/img/editor/ui_select_move.jpg) - ---- - -## Key facts - -* **The world is made of tiles.** TRAK holds about 100–650 small models: render triangles - plus collision polygons. MAP places them on a W×H grid (96×96 in the village) with height - and rotation. MAP also stores baked vertex colours and a 32-zone visibility table. There is - no BSP and no single world mesh. -* **Objects are "strats".** These are compiled scripts in the STPC chunk. In MAP, an object's - type is simply its **byte offset into STPC**. A level can therefore only use the object - types its own STPC contains. The builder uses the village (`t1l1m001`) as its base. -* **Coordinates and scale.** y points up. Fixed point 20.12 for objects, 4096 = 360° for - angles, and render space vs. game space have z mirrored. The llama is about 0.6 units - tall, runs about 1.1 units/s, can step up at most about 0.35 and jumps about 1.3 units - far with a run-up. -* **The level exit (LevelDoor)** is an invisible trigger. It fires only when the llama - stands in its grid cell **on floor with collision material 16**. Its parameters are - (tribe, world, chapter) of the next level. -* **Texture pixels** are PS1-style 15-bit BGR, RLE16-compressed, on 256×256 pages. The - value 0x0000 means transparent. - -The parsers rebuild every original WAD byte-for-byte, so they can be trusted as format -specs. - ---- - -## Building on this with an AI assistant - -This whole kit was made by an AI coding agent (Claude Code) working through the steps -below. Any capable assistant that can run code should be able to continue. - -1. **Give it the notes first.** Ask it to read `notes/*.md`. They contain offsets, field meanings, confidence levels and pitfalls. -2. **Regenerate the decompile** for questions about game logic. Install Ghidra and a full - JDK (version 25 for Ghidra 12.x), then run - `analyzeHeadless 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 ` 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. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e20653a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,77 @@ +# LlamaMod documentation + +Documentation for modding *The Emperor's New Groove* (PC, 2000; German: *Ein Königreich für ein +Lama*) with LlamaMod: building levels, the game's file formats, and how the game's object scripts +("strats") work. + +![The Test World: every figure of the game in one level](img/testworld-gallery.jpg) + +## Start here + +| If you want to … | read | +|---|---| +| build your own level | **[Guide: Build your own level](guide/README.md)** | +| look something up in the editor | [Level editor manual](manual/index.md) (also in the editor: **F1**) | +| know the numbers and rules of the engine | [Design rules](guide/design-rules.md) | +| build levels from code | [Levels from scripts](guide/levels-in-code.md) | +| understand the game's data | [Reference](reference/README.md) | +| change or write object behaviour | [Strats](strats/README.md) | +| find a tool | [Tools](tools.md) | +| work on LlamaMod itself | [For developers](developers.md) | + +## Contents + +**[Guide](guide/README.md)** – for level builders +* [Build your own level](guide/README.md): setup, editor basics, ground, start and exit, objects, + look and feel, play-test, sharing +* [Design rules](guide/design-rules.md): sizes, jump distances, the rules that keep a level working +* [Levels from scripts](guide/levels-in-code.md): Python and Java level builders, the editor's + command line + +**[Level editor manual](manual/index.md)** – every tool, panel and dialog of the editor, with +tutorials (first level, parkour, editing an original level, Blender import) + +**[Reference](reference/README.md)** – the game's data +* [Game files](reference/game-files.md): folders, naming scheme, worlds, COR pictures, movies, + save games +* [WAD container](reference/wad.md): the chunks of a level file +* [Geometry and collision](reference/geometry.md): tiles, grid, coordinate spaces, collision + materials, visibility regions +* [Objects, waypoints, zones](reference/objects.md): strat records, parameters, the exit, + environment zones, lights +* [Textures](reference/textures.md): pages, pixels, texture rectangles, animations +* [Music and sound](reference/audio.md): ASF music, sound banks, speech – known and unknown +* [TestLevel (world 9)](reference/testlevel.md): where custom levels live, safe chapters +* [Engine facts and limits](reference/engine-limits.md): scale, draw distance, memory, crashes + +**[Strats](strats/README.md)** – object behaviour +* [How the strat VM works](strats/README.md): units, frames, variables, events, spawning, + collision +* [Reading scripts](strats/reading-scripts.md): the Script view and the assembly +* [The script language](strats/script-language.md): writing new behaviour +* [Customising strats](strats/custom-types.md): constants, imports, clones, custom types, mods +* [Example: the Alligator Arena](strats/arena.md): a complete minigame explained step by step +* [Example: the roller coaster](strats/roller-coaster.md): coming soon + +**[Tools](tools.md)** – LlamaMod's launcher, editor and command line, the Python kit, the in-game +test harnesses + +**[For developers](developers.md)** – build, code map, writing a mod, maintaining the manual + +**[Lab notes](../notes/README.md)** – the detailed engineering notebook behind all of this + +## Pictures + +| | | +|---|---| +| ![LlamaMod launcher](img/launcher.jpg) | ![Level editor](img/editor.jpg) | +| The launcher: mods with options, save games, settings | The level editor with Village 1 | +| ![Alligator Arena](manual/img/mod_arena.jpg) | ![Debug view](img/debug-view.jpg) | +| The Alligator Arena minigame | Debug view: collision and invisible objects in the game | + +## Status and honesty + +Everything here was found by reverse engineering and tested against the game; where something is +a guess, the page says so (*likely*, *unknown*). Development used the German CD; other language +versions are expected to work but were not tested. The project is not affiliated with Disney or +Argonaut and contains no game data. diff --git a/docs/developers.md b/docs/developers.md new file mode 100644 index 0000000..eb86ee1 --- /dev/null +++ b/docs/developers.md @@ -0,0 +1,86 @@ +# For developers + +How the code is organised, how to build and test it, and where the engineering notes are. + +## Build + +Requirements: a **JDK 25** installed (the Gradle toolchain compiles with it; there is no +auto-download). Gradle itself and Spotless currently need to run on **Java 21**, e.g. +`JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew …` on Debian/Ubuntu. + +```bash +./gradlew spotlessApply build # compile, format, run the tests +./gradlew fatJar # build/libs/llamamod--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//Cd>` (default: the author's Wine setup); without it they are +skipped. Byte-exact round trips over all level WADs (WAD, TRAK, MAP, TEXT, strat code) are the +safety net for every format change. + +## Code map + +Package root [`src/main/java/com/eliasmueller/llamamod/`](../src/main/java/com/eliasmueller/llamamod/): + +| Package | Content | Notes | +|---|---|---| +| `format` | WAD, TRAK, STPC, MAP, TEXT, collision, COR | [Reference](reference/README.md) | +| `format.strat` | strat disassembler, assembler, script view, compiler | [Strats](strats/README.md), [notes/strat-language.md](../notes/strat-language.md) | +| `format.stpcmerge` | importing object types between levels (code, models, textures, sounds, texts) | [notes/editor-cross-import.md](../notes/editor-cross-import.md) | +| `levelbuild` | Java level builder (`LevelBuilder`, `LevelScript`) | [Levels from scripts](guide/levels-in-code.md) | +| `mods` | mod framework (`Mod`, `ModContext`, `ModManager`, options) and the built-in mods | [notes/llamamod-app.md](../notes/llamamod-app.md#mods), `notes/mod-*.md` | +| `exe` | patching `groove.exe`: PE image, code caves, a tiny x86 emitter | [notes/llamamod-app.md](../notes/llamamod-app.md#grooveexe-patches-and-code-caves-exeexeimage-exex86) | +| `platform` | install from ISO/CD, data folder, config, registry, Wine/Windows launch | [notes/llamamod-app.md](../notes/llamamod-app.md) | +| `save` | KSV save games, TestLevel chapters | [Game files](reference/game-files.md#save-games-ksv) | +| `ui` | launcher window | | +| `editor.model`, `editor.io` | scene model, commands/undo, project format, WAD import/build | [notes/editor-api.md](../notes/editor-api.md), [notes/editor-format.md](../notes/editor-format.md) | +| `editor.render` | OpenGL viewport | [notes/editor-render.md](../notes/editor-render.md) | +| `editor.ui` | editor shell and plugin API | [notes/editor-ui.md](../notes/editor-ui.md) | +| `editor.tools.*` | editor plugins: geometry, objects, environment, textures, catalog (cross-world import), strats, playtest, gltf, help | `notes/editor-tools-*.md`, [notes/editor-playtest.md](../notes/editor-playtest.md) | + +## Writing a mod + +A mod implements [`Mod`](../src/main/java/com/eliasmueller/llamamod/mods/Mod.java): `id`, `name`, +`description`, `category` (LEVELS, GAMEPLAY, VISUAL, CHEATS), `options()` and `apply(ModContext)`. +`apply` reads files of the game as earlier mods left them (`ctx.read`) and writes replacements +(`ctx.write`); `ctx.exe()` gives access to `groove.exe` for patches. Mods are registered in +[`ModRegistry`](../src/main/java/com/eliasmueller/llamamod/mods/ModRegistry.java). LlamaMod caches +mod outputs and never touches the original files. + +Good examples: [`mods/testworld/`](../src/main/java/com/eliasmueller/llamamod/mods/testworld/) (a +level built in code), [`mods/arena/`](../src/main/java/com/eliasmueller/llamamod/mods/arena/) (level ++ new strat types, walkthrough: [Arena](strats/arena.md)), +[`mods/fog/`](../src/main/java/com/eliasmueller/llamamod/mods/fog/) (data-only change of all levels), +[`mods/practice/`](../src/main/java/com/eliasmueller/llamamod/mods/practice/) (exe code caves with +hotkeys). Full API: [notes/llamamod-app.md](../notes/llamamod-app.md#writing-a-mod). + +Every mod should be checked in the running game; [`tools/apptest.py`](../tools/apptest.py) does +this unattended on Linux ([Tools](tools.md#testing-in-the-game)). + +## The editor manual + +[`docs/manual/`](manual/index.md) is the editor's user manual **and** its built-in help: the build +bundles the folder into the jar (`processResources` in `build.gradle`), the Help window renders it +with its own small Markdown converter, and F1 maps editor topics to chapter anchors +(`editor/tools/help/context-help.txt`). Rules for that folder: + +* Only the Markdown subset described at the top of `index.md`; every chapter must be linked from + `index.md`; links must stay **inside** `docs/manual` (the help window cannot open other files). +* Blocks between `generated:NAME` comments (shortcuts, menus, mod reference) are rewritten from the + live program by `ManualTest` – do not edit them by hand. +* `./gradlew test --tests '*ManualTest*'` checks links, anchors and images. + +Maintenance notes: [notes/editor-manual.md](../notes/editor-manual.md). + +## Engineering notes + +[`notes/`](../notes/README.md) is the project's lab notebook: one file per format, editor area and +mod, with addresses in `groove.exe`, confidence levels, what was verified in the game and known +limits. Process material (agent briefs, review reports) is in [`notes/dev/`](../notes/dev/). diff --git a/docs/guide/README.md b/docs/guide/README.md new file mode 100644 index 0000000..e530471 --- /dev/null +++ b/docs/guide/README.md @@ -0,0 +1,126 @@ +# Build your own level + +A step-by-step path from "I have the game" to "my friends are playing my level". Each step is short +and links to the chapter of the [editor manual](../manual/index.md) that has the details. +The manual is also built into the editor: press **F1** over any panel, tool or dialog. + +| Step | | +|---|---| +| 1 | [Set up LlamaMod](#1-set-up-llamamod) | +| 2 | [Learn the editor in ten minutes](#2-learn-the-editor-in-ten-minutes) | +| 3 | [Plan the level](#3-plan-the-level) | +| 4 | [Build the ground](#4-build-the-ground) | +| 5 | [Start, checkpoints and the exit](#5-start-checkpoints-and-the-exit) | +| 6 | [Objects, enemies and cameras](#6-objects-enemies-and-cameras) | +| 7 | [Look and feel: textures, fog, music, light](#7-look-and-feel-textures-fog-music-light) | +| 8 | [Play-test](#8-play-test) | +| 9 | [Share it](#9-share-it) | +| 10 | [Go further](#10-go-further) | + +Before you start, skim [Design rules](design-rules.md): the numbers (jump distances, draw +distance) and the handful of rules that keep the game from crashing. + +![The level editor](../img/editor.jpg) + +## 1. Set up LlamaMod + +1. Get **your own copy** of the game: the CD or an ISO image of it (*The Emperor's New Groove* PC, + 2000; German: *Ein Königreich für ein Lama*). LlamaMod contains no game files. +2. Download the **full** package for Windows or Linux, unpack it and start `LlamaMod.exe` / + `./LlamaMod` (Linux needs Wine to run the game). Or build it from source – see the + [main README](../../README.md#build-from-source). +3. The first-start wizard copies the game into LlamaMod's data folder. The original CD or ISO is + never changed. +4. Click **Play** once to check that the unmodded game runs. + +Manual: [Installing LlamaMod and the first start](../manual/02-installing.md). + +## 2. Learn the editor in ten minutes + +Click **Level Editor** in LlamaMod and follow +[Your first level in 10 minutes](../manual/tutorial-first-level.md): a new level from the +*Island* template, a ramp, a coin trail, play it. Then read +[Introduction and concepts](../manual/01-introduction.md) – tiles, brushes, objects, zones, base +levels, chapters – and [Navigating the viewport](../manual/04-navigation.md). + +## 3. Plan the level + +* **New or edited?** A *new level* starts empty on a **base level** (default: Village 1) that + provides the object types, textures, sky, fog and music. *Editing an original level* changes the + real Village, River … in place ([tutorial](../manual/tutorial-original-level.md)). +* **Choose the base by its objects.** Only the base's object types are available directly; more + can be imported from any world ([Importing objects](../manual/09-objects.md#importing-objects-from-other-worlds)). +* **Size.** The village grid is 96 × 96 cells; the llama is 0.6 tall and jumps about 1.3 cells. + A small, dense level plays better than a big empty one – the draw distance is only 8–16 cells. +* **Where it goes.** New levels live in the hidden world 9, *TestLevel*, on chapter 1 or 32–49 + ([why](../reference/testlevel.md)). The editor picks a free chapter. + +## 4. Build the ground + +* **Brushes** (B) draw boxes, ramps, stairs, cylinders and more; they are cut into tiles when the + level is built ([Building geometry](../manual/06-geometry.md)). +* **Tiles** (T) place the base level's own tile models for an original look. +* **Meshes** from Blender or any 3D program come in as OBJ or glTF + ([Importing meshes](../manual/07-meshes.md), [Blender tutorial](../manual/tutorial-blender.md)). +* **Collision** (K) decides what is solid: walls, floors, invisible walls, death floors, the exit + floor. The llama cannot step up even small edges – use slopes, or walls without collision. + +![A level exported as glTF and opened in Blender](../img/gltf-blender.jpg) + +## 5. Start, checkpoints and the exit + +Use the one-click setups in the Objects panel ([Objects](../manual/09-objects.md#one-click-setups)): + +* **Player start** – the llama plus its start camera node behind it. Keep the space behind the + llama free, or the camera jumps aside. +* **Checkpoint** – where the llama respawns after a fall. +* **Exit** – a LevelDoor *and* a floor with collision material 16 in the same grid cell. Without + that floor the exit does nothing ([why](../reference/objects.md#the-exit-leveldoor--certain)). + Its target is the next level; in TestLevel never "world finished" (7, 7, 7). +* **Death plane** – a death floor below everything, so falling off respawns instead of falling + forever. + +## 6. Objects, enemies and cameras + +* Place coins with the **Coin tool** (O), anything else from **Assets → Objects** + ([Objects](../manual/09-objects.md)). +* Enemies and moving things often need a **waypoint chain** – some guards even need a pair of + waypoint boxes and an event bit, or the game removes them. The Inspector and *Check for problems* + tell you ([Waypoints](../manual/10-waypoints.md)). +* **Cameras**: camera zones, cut scenes and the start camera + ([Cameras and triggers](../manual/11-cameras-triggers.md)). +* Change how an object behaves – a number in its script or a whole new type: see + [Strats](../strats/README.md) and [manual chapter 22](../manual/22-strats.md). + +## 7. Look and feel: textures, fog, music, light + +* **Textures**: the base's textures, imported PNG/JPG pictures, painting onto faces, flip-book + animations ([Textures](../manual/08-textures.md)). +* **Environment zones** set fog (= draw distance), sky colour and **music**; teleport zones move + the player between areas ([Environment](../manual/12-environment.md), + [music ids](../reference/audio.md#music)). +* **Light**: tiles get baked vertex colours (*Bake lighting*), lights only light objects. +* **Visibility regions** hide areas the camera cannot see – needed for big levels + ([Visibility](../manual/13-visibility.md)). + +## 8. Play-test + +**F5** builds the level and starts the game directly in it; **Shift+F5** starts at the camera +position. *Check for problems* (also run on every build) lists errors and warnings – double-click +one to select the culprit. **Escape** in the game quits straight back to the editor. +Manual: [Building, problems and play-testing](../manual/15-build-play.md). + +## 9. Share it + +**File → Save** writes a small `.llamalevel` project: only your changes, no game data. Send that +file; the receiver copies it into LlamaMod's `levels` folder (or opens it in the editor and uses +*Level → Add to LlamaMod…*), ticks it in the *Levels* category and clicks *Apply*. It is built from +their own game copy. Manual: [Sharing levels](../manual/17-sharing.md). + +## 10. Go further + +* Build levels from code instead of the mouse: [Levels from scripts](levels-in-code.md). +* Understand the data: [Reference](../reference/README.md). +* Write object behaviour: [Strats](../strats/README.md) and the + [Alligator Arena walkthrough](../strats/arena.md). +* All tools at a glance: [Tools](../tools.md). diff --git a/docs/guide/design-rules.md b/docs/guide/design-rules.md new file mode 100644 index 0000000..8329364 --- /dev/null +++ b/docs/guide/design-rules.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 | + +![The Test World's gallery: every figure of the game on a pedestal](../img/testworld-gallery.jpg) + +## Rules that keep the level working + +1. **One player start**, with nothing solid right behind the llama (start camera). +2. **Exit = LevelDoor + floor with collision material 16 in the same cell.** Target (1, 9, + chapter) in TestLevel – never (7, 7, 7). +3. **A death floor or death plane** under every place the llama can fall to. +4. **A checkpoint** before every hard part. +5. **TestLevel chapter 1 or 32–49** for new levels. +6. **Guards and box walkers need their waypoint boxes** (and guards an event bit someone sets), + or the game removes them. +7. **Every area must be visible from where the camera is**: with visibility regions, check the + masks (*Check for problems* reports regions nothing can see). +8. **Stay within the budget**: 16 MB level memory, 127 texture pages, 128 sounds – the import + preview and *Check for problems* show where you are. + +The editor and both level builders enforce the data-level rules (tiles inside the grid, vertex +colours, type fields, collision cells) automatically. *Check for problems* reports the rest. + +## Good habits + +* **Play-test often** (F5). The engine is unforgiving and a small change can stop the exit or + hide an area. +* **Look at the originals.** Open an original level in the editor and see how it builds doors, + cameras and enemy paths; the [debug view mod](../tools.md#debug-view) shows collision and + invisible objects in the running game. +* **Keep levels compact.** Short sight lines hide the draw distance and keep the frame rate up. +* **Copy numbers** (speeds, parameters, waypoint fields) from original objects that already do + what you want. diff --git a/docs/guide/levels-in-code.md b/docs/guide/levels-in-code.md new file mode 100644 index 0000000..6f90aeb --- /dev/null +++ b/docs/guide/levels-in-code.md @@ -0,0 +1,93 @@ +# Levels from scripts + +Besides the editor, a level can be described as a short script that a **level builder** turns +into a complete WAD: geometry made of boxes, ramps, stairs, trees and sea, objects placed by type +name, lighting baked automatically. There are two builders with the same ideas: + +| Builder | Language | Used by | +|---|---|---| +| [`tools/levelbuild.py`](../../tools/levelbuild.py) | Python 3 (numpy, Pillow) | the original test world [`levels/testwelt.py`](../../levels/testwelt.py), the jump lab [`levels/sprunglabor.py`](../../levels/sprunglabor.py) | +| [`levelbuild/LevelBuilder.java`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java) | Java (`LevelScript`) | the Test World mod ([`mods/testworld/`](../../src/main/java/com/eliasmueller/llamamod/mods/testworld/)), the [Alligator Arena](../strats/arena.md), the editor's brushes | + +Both start from a **base level** (default Village 1): its object code, textures and sound are +kept; geometry, tile grid, vertex colours, objects and waypoints are generated from scratch. Full +documentation of the Python builder, the measured physics and the test world: +[notes/levelbau.md](../../notes/levelbau.md). + +## A Python level + +```python +LEVEL_ARGS = dict(name='MyLevel', seed=7) + +def build(L): + L.sea(0, 0, 60, 60, y=-2.0, kill=(6, 4, 54, 56)) # sea + death plane + L.box(14, 12, 26, 24, -0.4, 0, top='stein', side='stein') # a stone plaza + L.stairs(26, 18, 29, 22, 0, 1.5, 3, '+x') # stairs up to the east + L.box(29, 17, 33, 23, 0, 1.5, top='gras', side='erde') + L.player(20, 14.5, rot=0) # facing +z + L.checkpoint(17, 18.5) + L.coins([(27, 20), (28, 20)]) + L.strat('Bunny', 31, 20) + L.box(31, 21, 32, 22, 1.5, 1.56, top='platte', side='stein', mat=16) # exit floor + L.exit(31.5, 21.5) # LevelDoor in the same cell +``` + +```bash +python3 tools/levelbuild.py my_level.py out/my_level.wad # build +python3 tools/levelbuild.py --types # object types of the base +python3 tools/levelbuild.py --textures tex.png # texture names and a contact sheet +``` + +Coordinates are game units: x east, y up, z = grid row ("south"); angles in degrees, 0° faces +z. +Building blocks: `floor`, `box`, `platform`, `pillar`, `ramp`, `stairs`, `fence`, `tree`, `sea`, +`quad`, `collider`, `obj` (an OBJ file), `height_at`. The exit needs a floor with collision +material 16 in its cell – the test world uses `box(..., mat=16)` under the door. + +The Python tools have a few **hard-coded paths** at the top of the scripts (`WADS`, `MODWAD` in +`levelbuild.py`, `CD_WADS` in `debugviz.py`, `PREFIX` in `lamatest.py`) that point into the original +author's Wine setup; use `--base /wads/t1l1m001.wad` or adjust them. `--install` only works with +that setup – with LlamaMod, use a Java level or the editor instead. + +## A Java level + +Implement [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java): + +```java +public final class MyLevel implements LevelScript { + @Override public String levelName() { return "My Level"; } + + @Override public void build(LevelBuilder l) { + l.sea(0, 0, 40, 40, -1.5, new double[] {7, 7, 33, 33}); + l.floor(10, 10, 30, 30, 0, "grass"); + l.box(18, 24, 22, 25, 0, 0.06, o().top("pattern_purple").mat(16)); // exit floor + l.player(20, 12, 0); + l.checkpoint(12, 12); + l.exit(20.5, 24.5, null, null, new int[] {1, 9, 32}); + l.coins(List.of(new double[] {20, 16}, new double[] {20, 18})); + } +} +``` + +`buildWad(baseWad, wads, opts)` returns the WAD. To ship it, write a small +[mod](../developers.md#writing-a-mod) whose `apply` writes `wads/T1L9M0nn.wad` (chapter 1 or +32–49) and a loading picture – [`ArenaMod`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java) +is a complete example. Texture names are English here (`grass`, `stone`, `sand`, `inca`, …) and are +matched by picture on the chosen base. + +## The editor from the command line + +The editor's project format can be scripted too: + +```bash +llamamod --editor-new my.llamalevel t1l1m001 --name "My level" # new project on a base +llamamod --editor-import t1l2m001 jungle.llamalevel # an original level as a project +llamamod --editor-build my.llamalevel out.wad # build the WAD +llamamod --import-level my.llamalevel # install as a level mod +``` + +## Seeing the result without the game + +* [`tools/geom_export.py`](../../tools/geom_export.py) `level.wad outdir --collision --objects` + writes OBJ files, texture pages and two preview images. +* [`tools/vorschau/`](../../tools/vorschau/) renders labelled overviews with Blender (Eevee). +* In the game: the test harness, see [Tools](../tools.md#testing-in-the-game). diff --git a/docs/img/debug-view.jpg b/docs/img/debug-view.jpg new file mode 100644 index 0000000..099e517 Binary files /dev/null and b/docs/img/debug-view.jpg differ diff --git a/docs/img/editor.jpg b/docs/img/editor.jpg new file mode 100644 index 0000000..65842ff Binary files /dev/null and b/docs/img/editor.jpg differ diff --git a/docs/img/gltf-blender.jpg b/docs/img/gltf-blender.jpg new file mode 100644 index 0000000..f21eda1 Binary files /dev/null and b/docs/img/gltf-blender.jpg differ diff --git a/docs/img/launcher.jpg b/docs/img/launcher.jpg new file mode 100644 index 0000000..17f17ae Binary files /dev/null and b/docs/img/launcher.jpg differ diff --git a/docs/img/strats/arena_defeat.jpg b/docs/img/strats/arena_defeat.jpg new file mode 100644 index 0000000..a132d55 Binary files /dev/null and b/docs/img/strats/arena_defeat.jpg differ diff --git a/docs/img/strats/arena_win.jpg b/docs/img/strats/arena_win.jpg new file mode 100644 index 0000000..2088031 Binary files /dev/null and b/docs/img/strats/arena_win.jpg differ diff --git a/docs/img/testworld-gallery.jpg b/docs/img/testworld-gallery.jpg new file mode 100644 index 0000000..3a930df Binary files /dev/null and b/docs/img/testworld-gallery.jpg differ diff --git a/docs/img/testworld-top.jpg b/docs/img/testworld-top.jpg new file mode 100644 index 0000000..ea0fc1c Binary files /dev/null and b/docs/img/testworld-top.jpg differ diff --git a/docs/img/village-textures.jpg b/docs/img/village-textures.jpg new file mode 100644 index 0000000..02493fe Binary files /dev/null and b/docs/img/village-textures.jpg differ diff --git a/docs/reference/README.md b/docs/reference/README.md new file mode 100644 index 0000000..35c9f27 --- /dev/null +++ b/docs/reference/README.md @@ -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) | diff --git a/docs/reference/audio.md b/docs/reference/audio.md new file mode 100644 index 0000000..c3861e7 --- /dev/null +++ b/docs/reference/audio.md @@ -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/.asf` on the CD (33 files) | certain | +| A level does not reference music files directly: **environment zones** carry a music id, and the start zone's id plays when the level begins | certain | +| Music ids map to names through a table in `groove.exe` at 0x578960 (20 bytes per entry, name pointer at +8), played by `FUN_00546b70`; id 0 stops the music | certain | +| ASF here is Argonaut's own format (not Microsoft ASF): 4-bit ADPCM, 44.1 kHz stereo, 17-byte blocks, decoded by `asfcodec.adl` in the game folder | certain (decoder reimplemented) | +| A second zone field (u16 at +0x56) is probably the ambience setting | likely | + +The id table, as used by the editor's zone inspector +([`EnvMath.MUSIC`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/environment/EnvMath.java)): + +| Id | Track | Id | Track | Id | Track | Id | Track | +|---|---|---|---|---|---|---|---| +| 0 | (stop) | 10 | GAMEOVER | 20 | SNKJUNG | 30 | TUNE1 | +| 1 | VILLAGE | 11 | TALLY | 21 | FROG | 31 | CHACHA | +| 2 | JUNGLE1 | 12 | TURRACE | 22 | KRONK | 32 | JJUNGLE2 | +| 3 | RIVER | 13 | PACHASNK | 23 | KIDLLAMA | 33 | TUNE9 | +| 4 | JUNGLE2 | 14 | MOUNT2 | 24 | TUNE8 | 34 | TUNE6 | +| 5 | MOUNTAIN | 15 | BALLOON | 25 | ROLLER | 35 | VICTORY | +| 6 | CITY | 16 | JUNGSPID | 26 | BUNNY | 36 | PUZZLE2 | +| 7 | CATACOMB | 17 | JAGCHASE | 27 | BLOCK | 37 | PLATFORM | +| 8 | LAB | 18 | RIVER | 28 | TUNE5 | 38 | (none) | +| 9 | TITLE | 19 | RIVRACE | 29 | CITYPUZZ | 39 | ENDGAME | + +Four names (GAMEOVER, TALLY, PUZZLE2, PLATFORM) have no `.asf` file on the German CD; what the +game plays for them (probably nothing) was not checked. + +**Changing the music of a level** works today: pick another id for the start zone (or any zone) +in the editor's *Environment* panel ([manual: Environment](../manual/12-environment.md), +[tutorial: Edit an original level](../manual/tutorial-original-level.md)). + +**Listening to the tracks:** [`tools/asf2wav.py`](../../tools/asf2wav.py) `in.asf out.wav` +decodes a track exactly like the game's codec (filter 0: `s = s1 + (n << shift)`, filter 1: +`s = 2·s1 − s2 + (n << shift)`, 16-bit wrap-around instead of clipping – FFmpeg's decoder clips and +drifts). + +**New music – open:** there is no encoder yet. Writing one (the inverse of `asf2wav.py`) and +replacing a track file through a mod would be the natural next step; the header fields after the +block count (`rate`, `flags` with bit 1 = stereo) are read by the decoder, the others are *unknown*. + +## Sound effects + +| Fact | Status | +|---|---| +| Each level has its own sample bank, chunk **SMPC**: `u32 n`, n × `{u32 "cvg " tag, u32 length, data}` | certain ([`SoundMerge.java`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/SoundMerge.java)) | +| Strats play samples by index (`soundPlay(sound:N)`, opcodes 0xd5 / 0x99 / 0x131 play, stop, is-playing); at most **128** samples per level, ids with bit 0x80 are the front-end sounds | certain | +| The samples are PlayStation-style VAG ADPCM, decoded by `Vagcodec.adl` | likely (tag and codec name; the sample header was not decoded) | +| **AMPC** holds ambient sound data (`ambi` / `VABp` blocks) | likely, not decoded | +| `FESOUND.WAD` is the front-end sound bank (not a chunk file) | likely | + +Samples can be **moved between levels**: the editor's cross-world import and the arena copy the +samples an imported object needs into the target level and renumber the `sound:N` operands +([notes/editor-cross-import.md](../../notes/editor-cross-import.md)). Changing which sample an +object plays is a constant edit in its script ([Strats](../strats/custom-types.md)). Exporting or +replacing a sample's audio is not possible yet. + +## Speech and dialogue + +* **LGPC** holds the dialogue texts of a level in all languages plus a speech slot per line. +* A speech slot `#` is an index into **SRPC**, whose entries are offset/size pairs into the + global speech file `music/.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. diff --git a/docs/reference/engine-limits.md b/docs/reference/engine-limits.md new file mode 100644 index 0000000..65e3a5c --- /dev/null +++ b/docs/reference/engine-limits.md @@ -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`. diff --git a/docs/reference/game-files.md b/docs/reference/game-files.md new file mode 100644 index 0000000..7c17f90 --- /dev/null +++ b/docs/reference/game-files.md @@ -0,0 +1,99 @@ +# Game files + +What is on the CD and on the hard disk, how files are named, and the small formats around the +levels. The level files themselves are described in [WAD container](wad.md). + +## Two folders: HD and CD + +The installer copies a small **HD** part to the hard disk; everything big stays on the **CD**. The +game looks up each file in `HDPath\dir\file`, then in `CDPath\dir\file`, then on every CD drive +(`FUN_00415690`). A plain folder in `CDPath` is enough – no real CD drive is needed. LlamaMod relies +on this: it points `CDPath` at its own `cd-active` folder, where mod outputs replace original files +([notes/llamamod-app.md](../../notes/llamamod-app.md#registry-cd-path-and-the-exe-patch)). + +| Folder | Contents | +|---|---| +| HD (`Setup//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//Cd`) | `wads/` level files, `music/*.asf` music, `movies/*.dat` videos, `*.cor` pictures, `test9.tle`, empty folders `gallery/`, `in/`, `out/` | + +Registry key (all values): `HKEY_LOCAL_MACHINE\Software\Disney Interactive\Emperors New Groove\1.0` +with `CDPath`, `HDPath`, screen size, volumes and devices. LlamaMod's base patch of `groove.exe` +redirects it to `HKEY_CURRENT_USER`, so no admin rights are needed. + +Development used the **German CD**. Other language versions are expected to use the same formats +but were not tested. + +## Level naming scheme + +Levels are named **`tlm`** (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.COR` | loading picture of world *w*, chapter *c* (`T191.COR` … `T1949.COR` for TestLevel) | +| `.cor` | concept art shown with a chapter (*likely*) | +| `tally.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/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 9 --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.tga`, the +command-line switch `-display_diagnostics` writes a `.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)). diff --git a/docs/reference/geometry.md b/docs/reference/geometry.md new file mode 100644 index 0000000..481b9e7 --- /dev/null +++ b/docs/reference/geometry.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). + +![The test world from above in the editor: every square is one grid cell](../img/testworld-top.jpg) + +## Coordinate spaces + +| Space | Used by | Axes | +|---|---|---| +| **Game space** | strats, collision, MAP fixed-point tile positions, lights | x east, **y up**, z = grid row (positive); 20.12 fixed point (4096 = 1.0); right-handed as the game shows it | +| **Export space** | TRAK/STPC vertices, MAP float tile positions | same, but **z negated** (grid at z ∈ (−H, 0]); drawn by a Direct3D (left-handed) camera | +| **File space** | OBJ/glTF of every LlamaMod and kit tool | = game space (y up, right-handed): Blender shows a level the same way round as the game | + +Converting export ↔ file space negates z and reverses the triangle corner order. Angles: +4096 = 360° in MAP records; the strat VM uses 0x1000000 = 360°. A tile is turned only about y +(0, 1024, 2048, 3072 in practice). Rotation 0 faces +z, 90° faces +x. + +## TRAK: tile models (*certain*) + +`u32 n`, then **all n headers** (0x84 bytes each), then the data of every model in order. +Typical levels have 100–650 models with up to 422 vertices / 798 faces each. + +| Header field | Meaning | +|---|---| +| 0x00 | bounding-box centre (3 × f32) | +| 0x0c | 8 box corners (used for frustum culling) | +| 0x6c / 0x6e | vertex count / face count | +| 0x78 / 0x7a / 0x7c | collision polygon counts in three groups (the engine always uses the sum; meaning of the groups *unknown*) | +| pointers | 0 in the file, set on load | + +Data per model: + +* **Vertex**, 24 bytes: `f32 x, y, z, nx, ny, nz` (lighting comes from the MAP vertex colours, not + from the normals). +* **Face**, 28 bytes, triangles only: `u16 flags, u16 v0, v1, v2, u16 texture, u16 0, f32 plane + nx, ny, nz, d`. +* **Collision polygon**, 32 bytes: `u8 type, u8 material`, then 5 planes `{i8 nx, ny, nz, u8 0, + i16 d}`. Plane 0 is the surface, planes 1–4 the edges (normals point inwards). Type bit 0 = + quad, otherwise triangle. Normals are ×127, d is 20.12 fixed point – so a collision polygon + must lie within **±8 units of its tile's origin**. + +### Face flags + +| Bit | Meaning | +|---|---| +| 0x0008 | start of a new render batch (texture page or blend mode changes) – must be set on the first face of each batch, at the latest every 256 faces | +| 0x0010, 0x0020, 0x0800 | which corners of the texture rectangle the triangle uses (below) | +| 0x0400 | double-sided (no backface culling) | +| 0x4000 | ignore vertex colours | +| 0x0001, 0x0002 | special sorting/transparency modes (*likely*) | +| others | present in the data, not read by the PC renderer (*unknown*, probably PlayStation leftovers) | + +### No free texture coordinates + +The engine has **no per-vertex UVs**. Every triangle shows one half of a texture rectangle from the +TEXT table: with flag 0x800 the face is the first or (with 0x10) second half of a quad, without it +a triangle covering the lower-left half; 0x20 mirrors horizontally. 96 % of world faces are quad +halves. This is why the level builder and editor split surfaces into 1 × 1 pieces and why a narrow +wall shows a squashed texture. See [Textures](textures.md). + +## MAP: the tile grid (*certain*) + +MAP starts with the grid and the tiles; objects follow (see [Objects](objects.md)). The blocks that +concern geometry: + +| Block | Format | +|---|---| +| header | `u32 nTiles, u32 W, u32 H` | +| tiles (floats) | nTiles × `{f32 x, y, z, u32 rotY, u32 model, u32 hasAlt}` in export space; x, z = cell centre | +| regions | 32 × `{u32 visibility mask, i32 value}`, then W·H × u32 region number per cell | +| tiles (fixed point) | nTiles × `{rx, ry, rz, x, y, z}` 20.12 in game space – used for drawing and collision; must match the float copy exactly | +| model index | nTiles × u32 (= the float block's `model`) | +| tile swaps (WFPC 0x10000) | one `{cellX, y, cellZ, modelA, modelB}` per tile with `hasAlt ≠ 0`: doors and bridges that change their model (strat opcodes 0x11a / 0x11b) | +| vertex colours | per tile: one u32 RGBA per vertex of its model (0x80 = 1.0, the game doubles and clamps); more sets for animated colours; then the same for the swap model | + +Rules that crash the game or corrupt the level when broken: + +* every tile must lie inside the grid (`0 ≤ x < W`, `0 ≤ row < H`) – outside, the cell list + writes into foreign memory; +* the vertex-colour block must have **exactly** one entry per vertex of the tile's model – else + everything after it (the objects) is read shifted; +* the float and fixed-point copies and the model index must agree; +* tile swaps: one entry per tile with `hasAlt ≠ 0`, in tile order. + +The cell of a tile is `floor(x) + floor(−z) · W` (export-space floats). + +## Collision (*certain*) + +* Only **the tiles listed in the cell the player stands in** are tested (`FUN_004036d0` floors and + ceilings, `FUN_00403ad0` walls). A wall polygon that sticks into the neighbouring cell has no + effect there – builders enter a polygon into every cell it covers, as a collision-only tile if + needed. +* Collision is in game space, relative to the tile's fixed-point position. +* There is **no step-up logic**: a 0.12-unit edge with a wall polygon already blocks the llama. + Low plinths need walls without collision; stairs need an invisible slope. + +### Collision materials + +The material byte ends up in the player's ground state and is read by strats (`floorMaterial`). + +| Material | Meaning | +|---|---| +| 0 | normal ground | +| **16** | **exit floor**: the LevelDoor ends the level only when the llama stands on material 16 in the door's cell; also used under the village house doors (zone teleports) | +| 17, 18 | ignored when WFPC flag 0x8000000 is set (*likely*: special surfaces) | +| **30** (0x1e) | death plane / water (*likely*): falling onto it empties the energy and the llama respawns at the last checkpoint or the start (Model 0 of many levels is such an invisible plane at y = −2.75) | +| others | special surfaces, meaning *unknown* – the debug view colours them | + +![Debug view of Village 3: collision frames, invisible walls in red, objects as markers](../img/debug-view.jpg) + +## Visibility regions (no BSP) + +Drawing (`FUN_0042bf40`) walks the grid cells within the draw distance around the camera and draws +a cell's tiles **only if the cell's region bit is set in the mask of the camera's region**. Up to 32 +regions; mask 0 or a camera outside the grid means "everything". New geometry must therefore get a +region that the surrounding regions can see, or it stays invisible – the builders put everything +into region 0 with mask `0xffffffff`. Objects in hidden cells are skipped too (unless strat flag +0x800). The editor's *Visibility regions* tool edits this +([manual chapter 13](../manual/13-visibility.md)). + +## STPC: object models and code + +STPC starts with the **object models** (same model format as TRAK, header 0x8c bytes, optionally +skinned with a bone table), then the **animation sets** (per-frame bone matrices and compressed +vertex deltas – the compression is only partly understood), then the **strat code units** +([Strats](../strats/README.md)). The engine never parses the code; MAP records and bytecode refer +into STPC by absolute byte offsets, so STPC can only be extended at the end or merged with +relocation ([notes/editor-cross-import.md](../../notes/editor-cross-import.md)). + +## WFPC flags + +WFPC is a u32 of feature flags. Some change the data layout and must stay as they are: 0x10, +0x100, 0x10000 (tile swaps), 0x100000, 0x200000, 0x4000000, 0x10000000. Others: 0x800 renderer +fog flag, 0x80000 read by a strat opcode, 0x8000000 makes collision materials 17/18 ignored. + +## Writing new geometry – checklist + +1. TRAK: correct counts, bounding box and face planes recomputed, texture index below the + number of texture entries, batch bit 0x8, ≤ 8000 vertices per model. +2. Collision polygons within ±8 units of the tile origin, entered into every cell they cover. +3. MAP: tiles inside the grid, float/fixed/model blocks consistent, vertex colours exactly per + vertex, tile-swap block matching `hasAlt`, a visible region. +4. Rewrite the WAD's total length. + +The editor and both level builders do all of this; the details of the builder's approach +(splitting into tiles per cell and height band, merging identical tiles, baking sun, sky and +ambient occlusion into vertex colours) are in [notes/levelbau.md](../../notes/levelbau.md). diff --git a/docs/reference/objects.md b/docs/reference/objects.md new file mode 100644 index 0000000..2c70c6f --- /dev/null +++ b/docs/reference/objects.md @@ -0,0 +1,141 @@ +# Objects, waypoints, zones and lights + +Everything that is not static geometry lives in the second half of the MAP chunk: object +instances ("strats"), their parameters, waypoint chains and environment zones. Lights are in their +own chunk, LGHT. Full byte layouts: [notes/objects.md](../../notes/objects.md). + +## Strats: objects are programs + +An object – coin, enemy, door, camera, the player – is an instance of a **strat**: a script in the +level's STPC code. A MAP record says *where* and *which code*; the code does the rest. There is no +type id: **the type is the code's byte offset in STPC**. Type names come from the code itself +(a `debugName "Coin"` instruction at the entry point). How the code works: [Strats](../strats/README.md). + +About 300 types are placed in the original levels, about 200 more are only spawned by other +strats (splinters, effects, heads). The full list of which type appears in which level, with +instance counts, is [notes/strat_types_table.md](../../notes/strat_types_table.md) +(`tools/strat_table.py`). + +## The MAP object blocks + +After the geometry blocks ([Geometry](geometry.md#map-the-tile-grid-certain)): + +| Block | Format | +|---|---| +| parameter pool | `u32 n, n × u32` – all object parameters of the level, referenced by index | +| environment zones | `u32 n, n × 90 bytes` (below) | +| waypoints | `u32 n, n × 34 bytes {u32 next, u32, u16 rx, ry, rz, s32 x, y, z, u32, u32}`; `next ≠ 0` = the following entry belongs to the same chain | +| strats | `u32 nStrats, u32 poolExtra`, nStrats × 58-byte records | +| optional tail | paths (WFPC 0x200000, unused by the CD levels), a buffer size (WFPC 0x10), a u16 | + +### Strat record (58 bytes, *certain* unless noted) + +| Offset | Field | +|---|---| +| 0 | rotation x, y, z (u16 each, 4096 = 360°; x/z almost always 0) | +| 6 | position x, y, z (s32, 20.12 fixed point, game space) | +| 18 | **code offset in STPC = the type** | +| 22 / 26 | parameter count / index into the parameter pool (−1 if none) | +| 30 | number of strat variables (fixed per type) | +| 34 | 16-byte slots for event handlers (fixed per type, *likely*) | +| 38 | type flags: collision and physics setup (fixed per type) | +| 42 | extra memory words (collision bones; fixed per type) | +| 46 | first waypoint index or −1 | +| 54 | flags; bit 1 = not created at level start | + +`poolExtra` is the number of strat slots beyond 30: the engine allocates `poolExtra + 30` slots, +which must cover all placed objects **plus everything spawned at run time**. + +The type fields (variables, slots, flags, extra) come from the compiler and must be copied from an +original instance of the same type; a type that uses collision but has type flags 0 crashes the +game. Builders and the editor do this automatically. + +### Parameters + +The *n* parameters of an instance are pushed onto the new strat's stack and become its +**variables v0 … v(n−1)**. Values are 20.12 fixed point by convention (0x1000 = 1.0), booleans +0 / 0x1000, counts and world/chapter numbers `n << 12`. The editor knows the meaning of the +parameters of many types (schemas in +[`editor/tools/objects/Schemas.java`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/objects/Schemas.java), +findings in [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md)); the others are +shown raw with the values used in the original levels. + +### Waypoints + +A waypoint chain is a list of positions used for patrol paths, moving platforms, camera paths – +and for some types (guards, box walkers) as a **pair of boxes** the object must stand in, or the +game removes it ("removing strat"). Guards also wait for an **event bit** (global variable 95) that +another object sets ([notes/editor-object-schemas.md](../../notes/editor-object-schemas.md#event-bits-global-95)). + +The **player's waypoint chain is the start camera**: the camera takes the first node within 2.5 +cells of the llama. A good start has one node about 2 cells behind the llama and 0.5 above it, and +nothing solid right behind the llama (the camera moves aside). + +## Special objects + +| Role | Type | Notes | +|---|---|---| +| player start | `Llama` | exactly one per level (`RollerCoaster`, `KuscoAndPachaTogetherOnATree`, `LlamaChase` in special levels) | +| checkpoint | `CheckPointPost` (spawns `CheckPointHead`), `CheckPointBox` | the respawn point after a fall | +| exit | `LevelDoor` | invisible trigger, see below; variants `VillageEndDoor`, `EndGameDoor` | +| cameras | `CameraSide`, `CutSceneCameraDirectional`, `CutSceneTrigger` | camera zones and cut scenes | +| always present | `CheatTeleport`, `Wompy`, `CutSceneTrigger` | | + +### The exit (LevelDoor) – *certain* + +The LevelDoor has no model. Every frame it checks: + +``` +game state is "running" +and the floor polygon under the llama has collision material 16 +and floor(llama.x) == floor(door.x) and floor(llama.z) == floor(door.z) ; same grid cell +-> endLevel(tribe, world, chapter) ; its three parameters +``` + +So an exit needs **a floor with collision material 16 in the door's grid cell** – height does not +matter. In several original levels the pad only appears when a tile swap opens the door (Village 1: +the RubyIdolDoor swaps the tile to a model that has the material-16 pad). The parameters are the +next level; the originals use (1, world, chapter + 1), and **(7, 7, 7)** in the last chapter of a +world means "world finished" – in TestLevel that crashes, so custom levels use (1, 9, chapter). +`endLevel` shows the tally (coins x / LNFO total, secrets) and unlocks the next chapter. + +A second path in the Llama's own code ends the level when it stands on material 16 in the cell +stored in globals g104/g105 with the target in g101–g103 – the arena uses this +([Arena](../strats/arena.md)). + +## Environment zones (90 bytes, *certain* for the fields below) + +A zone sets fog, colours, music and sky, and optionally teleports the player. + +| Field | Meaning | +|---|---| +| cell x, y, z | the trigger cell | +| target x, y, z, angle | teleport target and facing | +| flags (+0x20) | flag 8 = **start zone** (exactly one per level: the level begins with its settings); flag 4 = do not move the player | +| sky model | STPC offset of a sky model (0 = none) | +| fog mode | 0 none, 1 darkness with a light around the player, 2 table fog, 3 vertex fog | +| fog start / end | two pairs (detail Minimum and Maximum); **fog end = far clip = draw distance** | +| colours | ambient colour for object lighting, fog/backdrop colour | +| music id | 1–39 (see [Music](audio.md#music)), 0 stops the music | + +Teleports are triggered from strat code (opcode 0x100 looks up the zone of the calling object's +cell). In the original levels the trigger cells hold exit floors (material 16) or door objects – +this is how the village house doors work. The editor edits zones in the +*Environment* panel ([manual chapter 12](../manual/12-environment.md)). + +## Lights + +LGHT: `u32 n`, then per light a type, a colour (×2/255) and direction or position (game space). +Lights only light **object models** (ambient + all directional lights + the nearest point lights, +at most 6). Tiles use baked vertex colours, so changing a light does not change the ground – bake +the vertex colours again. Point lights are full up to radius *a* and fade to 0 at *b*; one byte +makes them flicker. + +## Tools + +| Task | Tool | +|---|---| +| list types, instances, parameters of a level | [`tools/strats.py`](../../tools/strats.py) `level.wad [--all]`, [`tools/map_dump.py`](../../tools/map_dump.py) `--strats --tiles --lights` | +| type × level table | [`tools/strat_table.py`](../../tools/strat_table.py) | +| place objects from a script | [`tools/levelbuild.py`](../../tools/levelbuild.py) `L.strat(...)`, Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java) | +| place, edit, check objects visually | the level editor ([manual chapter 9](../manual/09-objects.md), [10](../manual/10-waypoints.md), [11](../manual/11-cameras-triggers.md)) | diff --git a/docs/reference/testlevel.md b/docs/reference/testlevel.md new file mode 100644 index 0000000..e6d8704 --- /dev/null +++ b/docs/reference/testlevel.md @@ -0,0 +1,51 @@ +# TestLevel (world 9) + +The game has a hidden ninth world, **TestLevel**, with 49 chapter slots – but the retail CD ships +no data for it. It is the natural home for custom levels: originals stay untouched, and the world +map can reach it once a save game unlocks it. Every LlamaMod level mod, the Test World and the +Alligator Arena are installed as `wads/T1L9M0nn.wad` with a loading picture `T19n.COR`. + +Code: [`save/TestLevelChapters.java`](../../src/main/java/com/eliasmueller/llamamod/save/TestLevelChapters.java), +lab notes: [notes/testlevel-chapters.md](../../notes/testlevel-chapters.md). + +## Use chapters 1 and 32–49 + +The game keeps per-chapter coin counters and flag bytes in arrays that only have rows for worlds +0–8 (index `world · 6 + chapter`). For world 9, chapters 2–31 land on **other game variables**: + +| Chapter | What its coin counter / flag byte overwrites | +|---|---| +| 1 | nothing (free) | +| 5–8 | the last played tribe/world/chapter/map | +| 6–9 (flags) | the **language index** | +| 9 | the **furthest progress** – the world map breaks | +| 10–31 | options, HUD state and tables used by the original levels | +| **32–49** | tables of tribes 2 and 3, which the game never uses – free | + +So custom levels use **chapter 1 or 32–49**. The editor only offers these, new projects take the +first free chapter from 32, and old projects on other chapters are moved automatically. + +LlamaMod additionally relocates both arrays to a 128-entry buffer in unused space of `groove.exe` +(base exe patch, revision 2), so even picking an empty chapter on the world map is harmless with +LlamaMod's executable. Mods still keep to 1 and 32–49 so they also work with an unpatched exe. + +## Making world 9 reachable + +* **Empty chapters must exist.** Finishing chapter *n* unlocks chapter *n* + 1, and the world map + crashes ("Couldnt load wad") when a selectable chapter has no file. LlamaMod links every missing + `T1L9M002…049.wad` to an installed world-9 WAD and `T192…T1949.COR` to `T191.COR`. +* **Never exit to (7, 7, 7).** "World finished" in world 9 unlocks a world 10 that has no name; + the game crashes after saving. Exits in TestLevel point to (1, 9, chapter). +* **Get there:** set a save game's map cursor to world 9 (`llamamod --unlock 9 + --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*". diff --git a/docs/reference/textures.md b/docs/reference/textures.md new file mode 100644 index 0000000..bf82b8a --- /dev/null +++ b/docs/reference/textures.md @@ -0,0 +1,53 @@ +# Textures (TEXT chunk) + +Each level carries its own textures. Parse and rebuild are byte-identical for all level WADs +(including the RLE encoder). Code: [`format/TextChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/TextChunk.java), +[`tools/textchunk.py`](../../tools/textchunk.py). Lab notes: [notes/textures.md](../../notes/textures.md), +editor side: [notes/editor-tools-textures.md](../../notes/editor-tools-textures.md). + +## Layout + +``` +u32 nPal 0 in all WADs (8-bit palettes are unused) +u32 nPages 13–22 per level, all 256×256 + nPages × { u32 flags, u32 w, u32 h, data } + flags & 0x80 : u32 length + RLE16 (all original pages) + other ≠ 0 : w·h raw u16 +u32 nTex ~180–1260 texture entries + nTex × { u16 flags, u8 page, u8 clut (0xff), u8 x0, x1, y0, y1 } (edges inclusive) +u32 nAnim texture animations + nAnim × { u16 texIndex, u8 type, payload } type 8 = flip book: list of texture indices +``` + +RLE16: a sequence of s16 *n*; *n* > 0 → *n* literal pixels follow, *n* < 0 → the next pixel +repeated −*n* times. + +## Pixels + +16 bit in **PlayStation order**: bits 0–4 red, 5–9 green, 10–14 blue, bit 15 = STP +(semi-transparency). `0x0000` is fully transparent. Export to PNG uses alpha 0 = `0x0000`, +alpha 128 = STP 0, alpha 255 = STP 1. The page flags (low 7 bits) select the blend class: +1/6 opaque, 4/7 alpha blend, 5 another blend mode, 2/3 special converters (*likely* the +PlayStation semi-transparency modes; details open). + +## Texture entries + +A face never has its own UVs: it names a **texture entry**, a rectangle on a page, and shows half +of it ([Geometry](geometry.md#no-free-texture-coordinates)). Entry flag bit 0 = flat colour (the +rectangle bytes are R, G, B instead) – the level builder appends such entries for plain colours. +Other entry flag bits (blend mode, size class) are only partly understood. + +Limits: at most **127 pages** per level (signed page byte) and 65535 entries (u16 face field). + +## Changing textures + +* **Replace pixels, keep the table.** Exporting pages to PNG and importing them back changes no + index anywhere: `tools/text_export.py export level.wad dir` / `import level.wad dir new.wad`. +* **Add pages and entries.** New pages and entries are appended; faces then use the new indices. + The editor's texture manager does this, including colour reduction, painting onto faces and + flip-book animations ([manual chapter 8](../manual/08-textures.md)). +* **Same name, same picture.** Texture *indices* differ between base levels. The builders resolve + texture names (`grass`, `stone`, …) by picture fingerprint on each base and copy missing + village pictures over ([notes/levelbau.md](../../notes/levelbau.md)). + +![Contact sheet of the Village 1 textures with their indices, made by tools/texsheet.py](../img/village-textures.jpg) diff --git a/docs/reference/wad.md b/docs/reference/wad.md new file mode 100644 index 0000000..f3ef5c3 --- /dev/null +++ b/docs/reference/wad.md @@ -0,0 +1,63 @@ +# WAD container + +A level is one file `wads/t1lm.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/.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). diff --git a/docs/strats/README.md b/docs/strats/README.md new file mode 100644 index 0000000..d4a804a --- /dev/null +++ b/docs/strats/README.md @@ -0,0 +1,120 @@ +# Strats: how objects work + +Every object in the game – a coin, a squirrel, a moving platform, a camera, the llama itself – is +driven by a small program called a **strat** (Argonaut's "strategy" scripts, inherited from Croc). +The programs are compiled bytecode for a virtual machine in `groove.exe` (`FUN_0054d180`). LlamaMod +can read every one of them, change their numbers, write new ones and move them between levels. + +| Page | For | +|---|---| +| this page | how the VM works: units, instances, frames, variables, events, spawning, collision | +| [Reading scripts](reading-scripts.md) | the editor's Script view and the assembly listing, with a worked example | +| [The script language](script-language.md) | writing new behaviour in LlamaMod's structured language | +| [Customising strats](custom-types.md) | changing constants, cloning types, custom types in the editor and in mods | +| [Example: the Alligator Arena](arena.md) | a complete minigame written in the script language, explained step by step | +| [Example: the roller coaster](roller-coaster.md) | coming soon | + +Lab notes and tables: [notes/strat-opcodes.md](../../notes/strat-opcodes.md) (all 434 opcodes with +semantics, confidence and Argonaut names), [notes/strat-language.md](../../notes/strat-language.md) +(complete syntax), [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md) +(parameters of individual types). Machine-readable tables, used by the editor and the compiler: +[`ops.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/ops.tsv), +[`props.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/props.tsv). + +## Code units and types + +The strat code of a level sits at the end of its STPC chunk as a list of **code units**: +`u32 nUnits`, then per unit `u32 nWords` and the words. A unit is one compiled source file and can +contain several strats plus their name strings and data (for example `Spider` + `RopeForSpider`). +The village has 53 units, Lab 1 has 65. + +A **type** is just an entry point into this code: a MAP record or a `spawn` instruction names a +byte offset in STPC, and the code there starts with `debugName "Name"`. Code refers to models, +animation sets and other code by **absolute STPC offsets** – which is why STPC cannot be +rearranged without relocating every reference, and why each level carries its own copy of the +code of every type it uses. + +LlamaMod's disassembler covers the code of all 34 level WADs (1461 units, 670 555 words) and +assembles every unit back **byte-identically**. + +## One frame in the life of a strat + +The VM is a **stack machine** that runs every live strat once per frame (30 frames per second): + +1. The strat continues where it stopped last frame. +2. It executes instructions – push values, compute, read and write properties, play animations, + move – until it reaches a **yield** (`jmpYield`, `jzYield`, `callYield` …). +3. At the yield its turn ends; the next strat runs. Next frame it continues after the yield. + +So a typical object is "set up, then `loop { …; yield }`", and anything that takes time is a loop +that counts frames. Speeds and turn rates in scripts are **per frame**. + +`end` / `remove` deletes the strat; `return` returns from a `call`. + +## Data a strat works with + +| Kind | Syntax | What it is | +|---|---|---| +| **variables** | `v0`, `v1`, … (or an alias name) | the strat's own variables (count = the type's *nVars*). The first *n* hold the instance's **MAP parameters** | +| **properties** | `self.x`, `self.rotY`, `self.floorHeight`, `self.damageTaken` … | 194 named engine fields of the strat: position, rotation, floor under it, distance to the player, animation state, alpha, collision damage … | +| other strats' properties | `player.x`, `parent.v3`, `camera.…`, `target.…`, `boss.…`, `dialog.…` | the player, the strat that spawned this one (`parent`), the camera and a few registered special strats | +| **globals** | `g0` … | shared by all strats; the engine itself never reads most of them – their meaning comes from how scripts use them (e.g. g95 = event bits, g101–g105 = exit target and cell, g17–g20 = dive target) | +| **numbers** | `1.0`, `90deg`, `204r` | 20.12 fixed point (4096 = 1.0 = one cell); angles 0x1000000 = 360°; `r` marks a raw integer | + +Binary operators pop *b* (top) then *a* and push `a op b`; comparisons push 0/1; `mul` = a·b >> 12. + +## Events + +A strat registers **event handlers** in its slots (`onEvent`, in the script language +`on touched { … }` / `on event(type, param) { … }`). When the event fires, the handler runs like a +subroutine. Known event types: + +| Type | Fires when | +|---|---| +| 4 | **touched**: contact with another strat (pick-ups, enemies). Needs collision (`collisionOn` + `collRadius`) | +| 0x40 | timer | +| 0x80 / 0x100 | player distance | + +## Spawning + +`spawn(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. diff --git a/docs/strats/arena.md b/docs/strats/arena.md new file mode 100644 index 0000000..3f1f2e6 --- /dev/null +++ b/docs/strats/arena.md @@ -0,0 +1,240 @@ +# Example: the Alligator Arena + +The *Alligator Arena* mod is a complete minigame built from scratch: a walled pit on an island, +alligators coming in waves, coins popping out on every hit, an exit that opens after the last wave. +It uses every technique of this documentation – a scripted level, two new strat types written in +the [script language](script-language.md), and an object (the Alligator) imported from another +world. This page walks through the code. + +![The arena: an alligator chasing the llama](../manual/img/mod_arena.jpg) + +| File | Role | +|---|---| +| [`ArenaMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java) | the mod: options, writes the WAD and the loading picture | +| [`Arena.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/Arena.java) | the level script: geometry and object placement | +| [`ArenaScripts.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaScripts.java) | the strat scripts `ArenaMaster`, `ArenaGator` (and a test probe) | +| [`ArenaImport.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaImport.java) | compiles the scripts and merges them into the base level | +| [`ArenaTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/arena/ArenaTest.java) | unit tests (scripts compile for all option corners, the built level decodes) | +| [notes/mod-arena.md](../../notes/mod-arena.md) | lab notes: engine facts found, what was verified in the game | + +Options: alligator **lives** (1–10, default 5), **waves** (1–5, default 3; wave *n* brings +min(*n*, 2) alligators), llama **energy** cap (1–5, default 4), **coins** per hit (0–5, default 1), +TestLevel **chapter** (32–49, default 49). + +## 1. The plan + +* The **base level** is Village 1 (`t1l1m001`), like every new level: its code provides the + Llama, Coin, CheckPointPost and textures. +* The Alligator lives in **Jungle by Day 2** (`t1l4m002`). Its model, animations and sounds are + needed, so the new scripts are **compiled against that level** and then **imported** into the + village base together with everything they reference. +* Two new types: `ArenaMaster`, an invisible referee standing on the exit pad, and `ArenaGator`, + a new alligator spawned by the master. +* The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)). + +## 2. The level (`Arena.build`) + +The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java) +for the Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java), +the same builder the Test World uses. Coordinates are game units (one cell each): + +```java +l.sea(0, 0, 40, 40, -1.5, new double[] {ix0 - 3, iz0 - 3, ix1 + 3, iz1 + 3}); // sea + death plane +l.floor(a.x0(), a.z0(), a.x1(), a.z1(), a.floor(), "sand"); // pit floor 16..24 +l.box(a.x0() - t, a.z0() - t, a.x1() + t, a.z0(), 0, WALL, + o().top("stone").side("stone_moss")); // one of four walls +// exit pad: one cell with collision material 16 under a gate +l.box(ex - 0.5, ez - 0.5, ex + 0.5, ez + 0.5, a.floor(), a.floor() + 0.06, + o().top("pattern_purple").side("inca").rot(0).sideColl(false).mat(DOOR_MAT)); +l.player(st[0], st[1], st[2], null, 1.6); // llama + start camera node 1.6 behind it +l.checkpoint(a.x0() + 0.8, a.z0() + 0.8); +l.strat(ArenaScripts.MASTER, ex, ez, a.floor(), 0.0, null, LevelBuilder.Waypoints.NONE, 0, false); +l.coins(List.of(new double[] {a.x0() + 0.6, 20}, new double[] {a.x1() - 0.6, 20})); +``` + +Points to note: + +* The **exit pad** is a 1 × 1 box whose top has collision **material 16** – the exit rule from + [Objects](../reference/objects.md#the-exit-leveldoor--certain). There is no LevelDoor; the master + takes its role. +* The walls are 1.1 high – too high to jump (0.35), so the fight stays in the pit. +* Texture names (`sand`, `stone_moss`, `inca`) are resolved on the base level by picture. + +## 3. Compiling and importing (`ArenaImport`) + +`Arena.buildWad` runs before the geometry is built: + +```java +merged = ArenaImport.merge(baseWad, src, "t1l4m002", units(probe, debug)); +return LevelScript.super.buildWad(merged.wad(), wads, opts); +``` + +1. **`append`** compiles each unit with `StratCompiler` → `StratAssembler`, laid out as if it were + the last unit of **t1l4m002's** STPC, and appends it there. So `model:47` is the Alligator's model + and `unit:56+0xf0` is that level's `EnemyCoin`. A later unit can name an earlier one as + `unit:@ArenaGator`; `append` replaces that with `unit:N+offset`. +2. **`merge`** asks [`StpcMerge`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/StpcMerge.java) + – the editor's cross-world import engine – to import the two entry points into the village: + it follows every reference and copies the model, 7 animation sets, textures, 3 sound samples and + the spawned strats (`EnemyCoin` and `Sparkles` already exist in the village and are shared), + relocates every operand and verifies the result. +3. It adds one **unplaced prototype MAP record** per type with its type fields + (`nVars`, `slots`, `typeFlags`, `extra`), so the level builder can place the type by name. + +The type fields are declared in `Arena.units`: + +| Type | vars | slots | type flags | extra | why | +|---|---|---|---|---|---| +| ArenaGator | 16 | 5 | 0xC000 | 3 | collision like the Alligator; 3 collision bones | +| ArenaMaster | 12 | 0 | 0 | 0 | invisible logic, no collision, no handlers | + +## 4. The referee: `ArenaMaster` + +`ArenaScripts.master()` generates the script; the waves are unrolled by Java. With 2 waves it +reads like this (simplified): + +``` +strat ArenaMaster { + alias wave = v1 + alias alive = v3 + alias kills = v4 + alias t = v5 + g104 = -1 // no exit cell yet (the llama's own exit check reads g104/g105) + g105 = -1 + t = 60 // 2 seconds + while (t > 0) { t -= 1; call cap; yield } + + wave = 1 + self.x = 20.5 // move to a spawn spot … + self.z = 22.2 + yield // … and let the engine update the matrix before spawning + spawn<0, 16, 5, 50176, 3>(unit:@ArenaGator) + alive += 1 + self.x = 20.5 // back onto the exit pad + self.z = 23.5 + while (alive > 0) { call cap; yield } // gators do parent.v3 -= 1 when they die + t = 45 + while (t > 0) { t -= 1; call cap; yield } + wave = 2 + … // two spawns this time + + g101 = 4096r // exit target (1, 9, chapter) as raw 20.12 values + g102 = 36864r + g103 = 200704r + g104 = int(self.x) // the exit cell + g105 = int(self.z) + … // 5 bonus EnemyCoins + loop { + call cap + if (player.floorMaterial == 16 && int(self.x) == int(player.x) + && int(self.z) == int(player.z)) { + endLevel(4096r, 36864r, 200704r) // the LevelDoor's rule + g1 += 1 + remove + } + … // a Sparkles effect every 8 frames + yield + } +} + +sub cap { // energy option: keep the llama's energy at the cap + if (player.health2 > 4) { player.health2 = 4 } +} +``` + +What it shows: + +* **Variables as state** – `alive` is `v3`, which the gators change from outside with + `parent.v3 -= 1`: the parent/child link is the communication channel. +* **Waiting = looping with yield**: every wait is a `while … yield` that also calls `cap`, so the + energy cap holds every frame (also after a respawn). +* **The spawn rule**: move, `yield`, then `spawn` – without the yield the alligator would appear at + the master's old position (it copies the matrix of the last frame). +* **The exit** is the LevelDoor logic written out: material 16 under the llama in the same cell → + `endLevel(tribe, world, chapter)`. Setting g101–g105 additionally arms the Llama's built-in check + for the same cell. + +## 5. The alligator: `ArenaGator` + +`ArenaScripts.gator()` reuses the original Alligator's assets but has its own behaviour. Its parts: + +**Setup** – model, size, collision and how it takes hits: + +``` +setModel(model:47) +scale(1.5) +self.hitDamage = 1793 // 1 | 256 | 512 | 1024 +collisionOn<16384>() +collRadius(409r) +collisionBone(0, 409r) // three collision spheres along the body +collisionBone(6.0, 409r) +collisionBone(10.0, 409r) +on touched { + if (self.damageTaken & 1792) { // kick, dive or charge of the llama + if (cool <= 0 && lives > 0) { hit = 1 } + } else { + bit = 1 // contact without an attack: it bit the llama + } +} +``` + +`hitDamage` 1793 shares a bit with all three llama attacks, so each of them counts as the llama's +hit; bit 1 makes a plain contact hurt the llama ([Collision and damage](README.md#collision-and-damage)). +The handler only sets flags – the main loop reacts to them, which keeps the handler short. + +**Fade-in** – `setAmbient` from 0 to 63 over 32 frames, playing the idle animation. + +**Main loop** – a state machine in a `switch`, once per frame: + +| State | Behaviour | +|---|---| +| 0 watch | turn towards the player (2° per frame); after a timer, if the llama is within 5 cells and on the same floor height (±0.4), play the snap animation and sound | +| 1 snap | wait for the animation, then start chasing | +| 2 chase | accelerate up to `0.03 + 0.006 · wave` per frame towards the llama; lunge when closer than 1.2, give up after 75 frames | +| 4, 5 lunge | fast forward move, then slow down (numbers from the original Alligator) | +| 3 back off | move backwards for a few frames, then rest and watch again | + +Before the `switch`, the flags from the handler are handled: + +* `hit`: lose a life, sound, three green particle puffs, *coins* `EnemyCoin`s, blink for 45 + frames (alpha 0.25 and a red tint every 4 frames, no further hits meanwhile), back off. +* `bit`: back off and rest 90 frames, so the llama gets a breather. + +After the `switch` the gator is clamped to the pit and to the floor height. + +**Death** – when `lives` reaches 0 the loop `break`s: collision off, the original's death (shrink +over a few frames with puffs, five `EnemyCoin`s), then `parent.v3 -= 1` (one alive less for the +master), `parent.v4 += 1` (kills) and `remove`. + +**`sub aim`** – ported from the original Alligator: each frame it publishes its position in +g17–g19 (and the distance in g20) if it is the best target below a jumping llama. That is what makes +the llama's **dive** home in on it. + +## 6. The mod (`ArenaMod`) + +`apply` refuses a chapter another mod already wrote, builds the WAD, rewrites the tally total +(`LevelTally.counted`: 2 placed coins + every coin the alligators can drop + 5 bonus), writes it +to `wads/T1L9M0nn.wad` and draws a loading picture with Java2D (`CorEncoder`). That is all a level +mod needs: LlamaMod's mod pipeline puts the files into the game's CD folder and links the empty +TestLevel chapters. + +## 7. How it was tested + +* `ArenaTest` compiles the scripts for every corner of the options, checks wave sizes, builds the + level from the CD and decodes the merged code (the master spawns the gator unit, the gator + spawns EnemyCoin). +* In the game ([`tools/apptest.py`](../../tools/apptest.py)): a probe type (`ArenaProbe`, option + `probe=true`) proved that compiled `on touched` handlers fire; then fights with default options, + a quick win with `lives=1, waves=1`, and a defeat (energy 0 → *Continue* restarts the arena). + +![Defeated alligator: puffs and coins](../img/strats/arena_defeat.jpg) + +![Win: bonus coins on the exit pad, then the tally](../img/strats/arena_win.jpg) + +## What to take from it + +1. Borrow assets by **compiling against the level that has them** and importing with `StpcMerge`. +2. Keep event handlers short; set flags and react in the main loop. +3. Communicate through `parent.vN` and globals; spawn only after a `yield`. +4. Copy numbers (speeds, animation indices, type fields) from the original type's script. +5. Write the exit yourself if you need a condition – material 16 + same cell + `endLevel`. diff --git a/docs/strats/custom-types.md b/docs/strats/custom-types.md new file mode 100644 index 0000000..008e586 --- /dev/null +++ b/docs/strats/custom-types.md @@ -0,0 +1,101 @@ +# Customising strats + +From the smallest change to a completely new object type. Steps 1–5 need only the level editor; +step 6 is for programmers writing a LlamaMod mod. + +| Step | What changes | Where it is stored | +|---|---|---| +| 1. Object parameters | one placed object | the project's object list | +| 2. Constants | a number in a type's code – every object of that type | `stratEdits.constants` | +| 3. Import a type from another level | the level's code gets a new type with its models, sounds, texts | the project's import list | +| 4. Clone a type | a new type as a copy of an existing one | `stratEdits.types` (source text) | +| 5. New type from a script | a new type written from scratch | `stratEdits.types` | +| 6. Script types in a mod | types compiled and merged by Java code | the mod's source | + +A project never stores game code: constants are stored as *old → new value at an offset*, custom +types as source text, imports as *type X from level Y*. Everything is rebuilt from the player's own +CD when the project is opened ([notes/editor-format.md](../../notes/editor-format.md)). + +## 1. Object parameters + +Select an object; the Inspector's *Object parameters* section shows its MAP parameters (= the +variables v0, v1, … of its strat). For many common types the editor knows names, ranges and meanings +(schemas in [`Schemas.java`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/objects/Schemas.java)); +the rest are raw values with the values used in the original levels as a hint. +User guide: [manual chapter 9](../manual/09-objects.md). + +## 2. Constants + +Double-click a line with a number in the Script panel (or *Edit value…*). The change applies to +every object of the type, is undoable and turns the line orange. On load the old value is checked +first, so a project made for another game version cannot corrupt the code silently. A +`pushShort` holds only −8.0 … 7.99; larger values need a source edit. + +![Original and edited Coin constant in the game](../manual/img/strat_coin_constant_ab.jpg) + +## 3. Importing types from other levels + +*Assets → All worlds* lists every object of every level; dragging one into the viewport imports it. +The import copies the type's code **and everything it reaches**: spawned sub-strats (projectiles, +shards, heads), models, animation sets, textures, sound samples and dialogue lines, relocates all +references, shares what the level already has (Coin, EnemyCoin, Sparkles …) and verifies the +merged code. Limits: 16 MB level memory, 128 sounds, 127 texture pages. +Details: [notes/editor-cross-import.md](../../notes/editor-cross-import.md), +[manual chapter 9](../manual/09-objects.md#importing-objects-from-other-worlds). + +## 4. Cloning a type + +*Clone as new type…* in the Script panel copies a type's listing into the type editor, renamed. +Change what you need, *Check* (assemble and verify) and *Create type*. The new unit is appended at +the end of the level's code – nothing else moves – and the type appears in *Assets → Objects*. + +* **Type fields / parameters from** decides which existing type's MAP fields (variables, slots, + collision flags, extra words) and parameter names new objects get. Pick the type you cloned, + unless you changed how many variables or collision bones the code uses. +* *Edit type…* and *Delete type…* work later on, with undo; placed objects follow their type. +* Custom types can refer to each other by name (`unit:OtherType`). + +The manual's tutorial *Make the squirrel run in a circle* clones Bucky and inserts a circle loop +([manual chapter 22](../manual/22-strats.md#tutorial-make-the-squirrel-run-in-a-circle)). + +![Two squirrels running in circles: waypoint chain and custom script](../manual/img/strat_bucky_circle.jpg) + +## 5. A new type from a script + +*Level → New object type from script…* opens the type editor with a template. Set *Language* to +*Script* and write the behaviour in the [script language](script-language.md). Models, animations +and sounds are those of the current level (`model:N`, `anim:N`, `sound:N` – look them up in the +Script panel of a type that uses them). Choose *Type fields from* a type with similar needs (a +pick-up: Coin; a walking enemy: one of the enemies). + +## 6. Script types in a mod (Java) + +A mod can do the same in code. The arena shows the pattern +([`mods/arena/`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/)): + +1. Write the types as script-language text (often with values filled in from options). +2. **Compile against a source level** whose models, animations and sounds you want to use – the + indices in the script are that level's: + `StratAssembler.assemble(StratCompiler.compile(src), StratListing.layout(stpcImage, base))` + and append the words as a new unit to the source level's STPC (`ArenaImport.append`). +3. **Merge into the base level** with `StpcMerge` (the same engine as the editor's import): it + copies the closure (models, animation sets, textures, sounds, spawned strats), relocates and + verifies. Add one unplaced prototype MAP record per type with its type fields. +4. **Build the level** with a `LevelScript` / `LevelBuilder` + ([`levelbuild/`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/)) and place the types by + name with `l.strat(...)`. +5. Write the WAD into a TestLevel chapter from the mod's `apply` and add a loading picture. + +The [arena page](arena.md) walks through every part of this. + +## Choosing type fields + +| Field | What to put there | +|---|---| +| variables | at least the highest `vN` the code uses + 1 (parameters count as variables) | +| slots | number of event handler slots (`on …` blocks) – at least 1 if there is one | +| type flags | 0 for invisible logic; collision types need collision flags (Coin: 0x28000; Alligator: 0xC000 / 0xC400) | +| extra | collision-bone words: at least the number of `collisionBone` calls | + +Wrong values do not always fail at once: too small collision areas corrupt the strat heap and +crash later (page fault in `FUN_0041ae20`). diff --git a/docs/strats/reading-scripts.md b/docs/strats/reading-scripts.md new file mode 100644 index 0000000..817a489 --- /dev/null +++ b/docs/strats/reading-scripts.md @@ -0,0 +1,114 @@ +# Reading scripts + +LlamaMod shows every strat in two forms: the **Script view**, a readable reconstruction, and the +**Assembly** listing, the exact instructions. Both are in the level editor's *Script* panel +(**View → Panels → Script**, **Ctrl+Shift+K**); the user-level walkthrough is +[manual chapter 22](../manual/22-strats.md). + +![The Script panel with Bucky's script](../manual/img/strat_script_panel.jpg) + +Code: [`StratListing`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratListing.java) +(assembly), [`StratPseudo`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratPseudo.java) +(script view), [`OpTable`](../../src/main/java/com/eliasmueller/llamamod/format/strat/OpTable.java) +(names and help texts). + +## Example: the Coin + +Script view of the village coin: + +``` +strat Coin: +setModel(model:5) +collRadius(409r) +onEvent ev_0000, 0 // event type 4 → L_0084 +if (v1) { collisionOn<32768>() ... } +if (v0) { + if (self.floorMaterial == -1.0) { skipArgs … } else { + self.y = self.floorHeight + 204r + self.rotY = rnd(4096.0) + } +} +loop { turnLeft(5.625deg); yield } +``` + +Line by line: + +| Line | Meaning | +|---|---| +| `setModel(model:5)` | show model 5 of this level's STPC (the coin) | +| `collRadius(409r)` | collision radius 409/4096 ≈ 0.1 cell | +| `onEvent ev_0000, 0` | install the handler table entry `ev_0000` in slot 0: event type 4 (touched) jumps to `L_0084`, the pick-up code | +| `if (v1) …` | parameter 1 switches collision on | +| `if (v0) …` | parameter 0 (`snapToFloor` in the editor) puts the coin 204/4096 ≈ 0.05 above the floor under it and turns it randomly – unless there is no floor (`floorMaterial == -1`) | +| `loop { turnLeft(5.625deg); yield }` | spin 5.625° per frame, forever | + +Changing `204r` to `1.5` makes every snapping coin float 1.5 cells high – that is a constant edit +([Customising strats](custom-types.md)). + +## Script view conventions + +| You see | It means | +|---|---| +| `self.x`, `player.y`, `parent.v3` | property or variable of this strat, the player, the spawner | +| `v0`, `snapToFloor` | variable 0; named when the editor knows the type's parameter | +| `g12` | global variable 12 | +| `1.0`, `5.625deg`, `204r` | fixed point (1.0 = one cell), an angle, a raw integer (204/4096) | +| `op(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 | diff --git a/docs/strats/roller-coaster.md b/docs/strats/roller-coaster.md new file mode 100644 index 0000000..5d647db --- /dev/null +++ b/docs/strats/roller-coaster.md @@ -0,0 +1,20 @@ +# Example: the roller coaster + +> **Coming soon.** A roller-coaster mod is in development. This page will walk through it the way +> [the arena page](arena.md) does, once it is finished. Until then, its lab notes will appear as +> `notes/mod-coaster.md` and its code under `src/main/java/com/eliasmueller/llamamod/mods/coaster/`. + + + +## What is already known about the original rides + +* Catacombs chapters 1, 3 and 5 (`t1l7m001`, `t1l7m003`, `t1l7m005`) are roller-coaster rides. In + these levels the player is not the `Llama` strat but a **`RollerCoaster`** strat + ([Objects](../reference/objects.md#special-objects)). +* The rides have the longest views of the game: fog end up to 23 cells + ([Engine facts](../reference/engine-limits.md#drawing)). +* The ride music is track 25, `ROLLER` ([Music](../reference/audio.md#music)). +* Objects of these levels can be imported into other levels, but they only work with matching + geometry ([manual chapter 9](../manual/09-objects.md#limits-and-things-to-know)). diff --git a/docs/strats/script-language.md b/docs/strats/script-language.md new file mode 100644 index 0000000..db101b6 --- /dev/null +++ b/docs/strats/script-language.md @@ -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(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(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. diff --git a/docs/tools.md b/docs/tools.md new file mode 100644 index 0000000..95988a0 --- /dev/null +++ b/docs/tools.md @@ -0,0 +1,121 @@ +# Tools + +Everything in this repository that you can run, what it is for and where its code and notes are. +None of the tools contain game data; they all work on your own copy of the game. + +## LlamaMod (Java app) + +The main product: a mod manager with a launcher and a 3D level editor, for Windows and Linux. +Java 25 + Swing, sources in [`src/main/java/com/eliasmueller/llamamod/`](../src/main/java/com/eliasmueller/llamamod/). + +![The LlamaMod launcher](img/launcher.jpg) + +| Part | What it does | Docs | +|---|---|---| +| launcher | first-start wizard (copies the game from CD/ISO), mod list with options, save-game editor, starts the game (Wine on Linux) | [manual: Installing](manual/02-installing.md), [manual: Mods](manual/18-mods.md), [notes/llamamod-app.md](../notes/llamamod-app.md) | +| level editor | 3D editor for new and original levels: brushes, tiles, meshes, textures, objects, waypoints, cameras, zones, lights, visibility, strat scripts, play-test, glTF export | [editor manual](manual/index.md) | +| mods | Test World, Alligator Arena, level projects, Boot to level, Intro skip, Mirror mode, Practice tools, Randomizer, Draw distance, Built-in cheats, Debug wireframe, Free camera | [manual: LlamaMod mods](manual/18-mods.md), `notes/mod-*.md` | + +### Command line + +```text +llamamod start the GUI +llamamod --version show the version and build id +llamamod --install 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 [--cursor] +llamamod --editor [project.llamalevel | t1l1m001] level editor +llamamod --editor-build [--cd ] +llamamod --editor-import [--cd ] +llamamod --editor-new [base level] [--name ] [--cd ] +llamamod --import-level (into the level folder, shown as a mod) +llamamod --remove-level move a level mod's file to backups/levels/ +``` + +Data folder: `-Dllamamod.data=` or `LLAMAMOD_DATA` (default `~/.local/share/LlamaMod`, +Windows `%LOCALAPPDATA%\LlamaMod`). Source: [`Main.java`](../src/main/java/com/eliasmueller/llamamod/Main.java). + +### Debug view + +The *Debug wireframe* mod +([`mods/debug/`](../src/main/java/com/eliasmueller/llamamod/mods/debug/)) adds visible geometry to +every level: collision as coloured frames (invisible walls filled red, the exit floor violet, death +planes magenta) and markers for invisible objects (start, exit, checkpoints, cameras, triggers, +zones). Gameplay is unchanged. Its Python ancestor is `tools/debugviz.py`; colour legend and +details: [notes/debugviz.md](../notes/debugviz.md). + +![Debug view in Village 3](img/debug-view.jpg) + +## Python kit (`tools/`) + +The reverse-engineering toolkit the project started with. Python 3.8+ with `numpy` and `Pillow` +(`capstone` for `disx86.py`). All parsers rebuild the original files byte for byte. + +| Tool | Purpose | +|---|---| +| [`wadtool.py`](../tools/wadtool.py) | list, extract and replace WAD chunks | +| [`geomlib.py`](../tools/geomlib.py) | library: WAD, TRAK, STPC models, MAP tile part, TEXT, collision | +| [`geom_export.py`](../tools/geom_export.py) | level → OBJ/MTL with textures and vertex colours, `--collision` (collision.obj by material), `--objects`, preview renders | +| [`geom_import.py`](../tools/geom_import.py) | OBJ → new world geometry in a copy of a level | +| [`geom_demo.py`](../tools/geom_demo.py) | example geometry for `geom_import.py` | +| [`mapchunk.py`](../tools/mapchunk.py) | MAP parser/writer | +| [`map_dump.py`](../tools/map_dump.py) | print a level's tiles, strats, lights | +| [`strats.py`](../tools/strats.py) | object types of a level: names, instance counts, code offsets, type fields | +| [`strat_table.py`](../tools/strat_table.py) | type × level table → [notes/strat_types_table.md](../notes/strat_types_table.md) | +| [`stpcwalk.py`](../tools/stpcwalk.py) | rough walk through STPC (models, animations, code) | +| [`textchunk.py`](../tools/textchunk.py), [`text_export.py`](../tools/text_export.py) | TEXT chunk; export pages to PNG and import them back | +| [`texsheet.py`](../tools/texsheet.py) | contact sheet of a level's textures with indices | +| [`levelbuild.py`](../tools/levelbuild.py) | level builder from a Python script ([guide](guide/levels-in-code.md)) | +| [`make_testlevel.py`](../tools/make_testlevel.py) | early experiment: one object per type on a floor plate | +| [`debugviz.py`](../tools/debugviz.py), [`debugviz_check.py`](../tools/debugviz_check.py) | debug-view WADs for all levels and an offline check | +| [`cor2png.py`](../tools/cor2png.py), [`png2cor.py`](../tools/png2cor.py) | loading pictures (COR) ↔ PNG | +| [`asf2wav.py`](../tools/asf2wav.py) | music (ASF) → WAV, exact decoder | +| [`disx86.py`](../tools/disx86.py) | disassemble `groove.exe` at a virtual address | +| [`vorschau/`](../tools/vorschau/) | Blender (Eevee) preview renders of a level with labels | +| [`lama-mod`](../tools/lama-mod) | shell script: CD overlay of symlinks for mods in a hand-made Wine setup (pre-LlamaMod) | + +Level scripts in [`levels/`](../levels/): `testwelt.py` (the original test world), +`sprunglabor.py` (jump lab used to measure the llama), `testwelt_parkour.seq` (scripted run). + +Several scripts have **hard-coded paths** to the original author's Wine setup at the top (see +[Levels from scripts](guide/levels-in-code.md#a-python-level)). + +## Testing in the game + +| Tool | What it does | Notes | +|---|---|---| +| [`tools/apptest.py`](../tools/apptest.py) | runs the game **through LlamaMod** in an isolated session: own data folder, Wine prefix, invisible X display (Xvfb), muted private PulseAudio; applies mods, unlocks a level, navigates the menus, presses keys, takes screenshots, detects crashes | [notes/llamamod-app.md](../notes/llamamod-app.md#testing-mods-in-game-toolsapptestpy) | +| [`tools/lamatest.py`](../tools/lamatest.py) | the older harness for a hand-made Wine setup: loads a WAD as TestLevel and runs a key sequence | [notes/testharness.md](../notes/testharness.md) | + +Linux only; needs Wine, Xvfb, xdotool, ImageMagick, tesseract (German data, for reading menu +texts) and PulseAudio. `apptest.py` defaults to a session root inside the author's job folder – +set `APPTEST_ROOT`. + +```bash +./gradlew fatJar +tools/apptest.py run demo --display 7 --mods arena --level 9 49 \ + --seq "Up:1.5 shot:walk z shot:kick" --out /tmp/demo +``` + +## Reverse engineering + +* `re/ghidra_scripts/DumpAll.java` – headless Ghidra script that decompiles every function of + `groove.exe` into one C file. The exe, the decompile and (currently) the script itself are + excluded from the repository (`.gitignore`). Run + `analyzeHeadless 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 | diff --git a/notes/README.md b/notes/README.md new file mode 100644 index 0000000..a91fc4b --- /dev/null +++ b/notes/README.md @@ -0,0 +1,64 @@ +# Lab notes + +The project's engineering notebook: what was found in `groove.exe` and the game data, how each part +of LlamaMod works, and what was verified in the game. The notes are detailed and technical (with +addresses, confidence levels and test logs). For a readable introduction start with the +[documentation](../docs/README.md) instead. + +Images used by the notes are in [`img/`](img/). + +## Formats and engine + +| Note | Topic | +|---|---| +| [geometry.md](geometry.md) | TRAK / MAP / STPC models, coordinate spaces, collision, visibility, writing new geometry | +| [objects.md](objects.md) | MAP layout, strat records, waypoints, zones, special objects, the other chunks | +| [textures.md](textures.md) | TEXT chunk | +| [strat-opcodes.md](strat-opcodes.md) | all 434 strat VM opcodes, conventions, sources | +| [strat-language.md](strat-language.md) | LlamaMod's strat assembly, script view and script language | +| [strat_types_table.md](strat_types_table.md) | every object type in every level, with counts | +| [editor-object-schemas.md](editor-object-schemas.md) | parameters of object types, guards, event bits, start camera | +| [testlevel-chapters.md](testlevel-chapters.md) | world 9 chapters and the relocation patch | +| [levelbau.md](levelbau.md) | the Python level builder, measured physics, the exit, the test world | +| [debugviz.md](debugviz.md) | debug view: colour legend, how it is built | +| [testharness.md](testharness.md) | `tools/lamatest.py`, PC controls | + +## LlamaMod app and mods + +| Note | Topic | +|---|---| +| [llamamod-app.md](llamamod-app.md) | build, CLI, data folder, registry, mod framework, exe patches, save games, `tools/apptest.py` | +| [mod-arena.md](mod-arena.md) | Alligator Arena | +| [mod-testworld.md](mod-testworld.md) | Test World and its gallery | +| [mod-levels.md](mod-levels.md) | level projects as mods | +| [mod-boottolevel.md](mod-boottolevel.md) | Boot to level | +| [mod-introskip.md](mod-introskip.md) | Intro skip | +| [mod-mirror.md](mod-mirror.md) | Mirror mode | +| [mod-practice.md](mod-practice.md) | Practice tools | +| [mod-randomizer.md](mod-randomizer.md) | Randomizer | +| [mod-fog.md](mod-fog.md) | Draw distance | +| [mod-cheats.md](mod-cheats.md) | Built-in cheats | +| [mod-freecam.md](mod-freecam.md) | Free camera & noclip | + +## Level editor internals + +| Note | Topic | +|---|---| +| [editor-design.md](editor-design.md) | design brief | +| [editor-api.md](editor-api.md) | scene model, commands, IO | +| [editor-format.md](editor-format.md) | the `.llamalevel` project format | +| [editor-render.md](editor-render.md) | OpenGL viewport, coordinate conventions, culling | +| [editor-ui.md](editor-ui.md) | application shell and plugin API | +| [editor-tools-geometry.md](editor-tools-geometry.md) | brushes, tiles, collision, mesh import, library | +| [editor-tools-objects.md](editor-tools-objects.md) | objects, paths, cameras | +| [editor-tools-environment.md](editor-tools-environment.md) | zones, fog, music, lights, visibility, baking | +| [editor-tools-textures.md](editor-tools-textures.md) | texture manager, import, painting, animations | +| [editor-cross-import.md](editor-cross-import.md) | importing object types from other levels, engine limits | +| [editor-playtest.md](editor-playtest.md) | play-test straight into the level | +| [editor-manual.md](editor-manual.md) | the manual window and context help | +| [editor-guide.md](editor-guide.md) | early short user guide (superseded by [docs/manual](../docs/manual/index.md)) | + +## Process + +[`dev/`](dev/) holds the briefs for the AI agents that worked on the project and the review reports +of the overnight UX and bug reviews (`bug-review-*.md`, `ux-review-*.md`). diff --git a/notes/bug-review-editor.md b/notes/dev/bug-review-editor.md similarity index 100% rename from notes/bug-review-editor.md rename to notes/dev/bug-review-editor.md diff --git a/notes/bug-review-launcher.md b/notes/dev/bug-review-launcher.md similarity index 99% rename from notes/bug-review-launcher.md rename to notes/dev/bug-review-launcher.md index d06b4df..7142157 100644 --- a/notes/bug-review-launcher.md +++ b/notes/dev/bug-review-launcher.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\`). -![matrix](img/launcher-matrix.jpg) +![matrix](../img/launcher-matrix.jpg) Screens: all mods in Village (wireframe, mirrored) and the free camera; River with mirror+randomizer+fog; Test World with all mods; boot-to-level + all exe mods (River loading card); practice restore. diff --git a/notes/ux-review-editor.md b/notes/dev/ux-review-editor.md similarity index 95% rename from notes/ux-review-editor.md rename to notes/dev/ux-review-editor.md index ba01064..45bd106 100644 --- a/notes/ux-review-editor.md +++ b/notes/dev/ux-review-editor.md @@ -14,20 +14,20 @@ position) started straight in the level, and Escape in the game returned to the | Before | After | |---|---| -| ![](img/editor/ux_llama_before.jpg) | ![](img/editor/ux_llama_after.jpg) | +| ![](../img/editor/ux_llama_before.jpg) | ![](../img/editor/ux_llama_after.jpg) | | A 1920-wide window: the Inspector is 170 px wide and shows no values at all. Side docks have two rows of tabs. | The docks get their intended widths (290 / 340 px). Side-dock tabs are icons with the active one named. The engine fields of the object type are folded away. | -| ![](img/editor/ux_brush_before.jpg) | ![](img/editor/ux_brush_after.jpg) | +| ![](../img/editor/ux_brush_before.jpg) | ![](../img/editor/ux_brush_after.jpg) | | The Brush tool's buttons read "Bl…", "Pl…", the texture names "gr…", the numbers show "0,5". | Full labels and a decimal point. | -| ![](img/editor/ux_coin_before.jpg) | ![](img/editor/ux_coin_after.jpg) | +| ![](../img/editor/ux_coin_before.jpg) | ![](../img/editor/ux_coin_after.jpg) | | The Coin tool's options are cut off: only the top pixels of the fields are visible. | The options are on one line, or wrap to a second row in narrow windows. | -| ![](img/editor/ux_menu_before.jpg) | ![](img/editor/ux_menu_after.jpg) | +| ![](../img/editor/ux_menu_before.jpg) | ![](../img/editor/ux_menu_after.jpg) | | *Help* comes before *Geometry*, and the shortcuts are German ("Strg", "Umschalt"). | The menus are File … Geometry, Tools, Help, and the keys are named "Ctrl", "Shift". | -| ![](img/editor/ux_orig_before.jpg) | ![](img/editor/ux_orig_after.jpg) | +| ![](../img/editor/ux_orig_before.jpg) | ![](../img/editor/ux_orig_after.jpg) | | *Open original level*: the description is cut at the right edge, and the selected row's id is grey on orange. | The text wraps and the id is readable. | -| ![](img/editor/ux_small_before.jpg) | ![](img/editor/ux_small_after.jpg) | +| ![](../img/editor/ux_small_before.jpg) | ![](../img/editor/ux_small_after.jpg) | | A 1280×720 window: the right dock is squeezed to 130 px. Also, Esc did not close the dialog, so it was still open and blocked every key. | The docks keep their sizes and the tool options wrap. Esc closes dialogs. | -![](img/editor/ux_quit_game.jpg) +![](../img/editor/ux_quit_game.jpg) *New: Play while the game still runs offers to quit it.* diff --git a/notes/ux-review-launcher.md b/notes/dev/ux-review-launcher.md similarity index 94% rename from notes/ux-review-launcher.md rename to notes/dev/ux-review-launcher.md index 09b7f28..80acd41 100644 --- a/notes/ux-review-launcher.md +++ b/notes/dev/ux-review-launcher.md @@ -20,27 +20,27 @@ covered by code review), Windows (the antivirus notice is unchanged there). | Before | After | |---|---| -| ![](img/app/ux_start_before.jpg) | ![](img/app/ux_start_after.jpg) | +| ![](../img/app/ux_start_before.jpg) | ![](../img/app/ux_start_after.jpg) | | The app opens with the mod list scrolled to the end (Visual, Cheats); Levels and Gameplay are out of sight. | The list opens at the top and keeps its place when it is rebuilt (Apply, theme). Short windows get a lower header. | -| ![](img/app/ux_mods_before.jpg) | ![](img/app/ux_mods_after.jpg) | +| ![](../img/app/ux_mods_before.jpg) | ![](../img/app/ux_mods_after.jpg) | | Options have no explanation unless you hover (Worlds "all"? Checkpoint spacing "10" of what?). Descriptions talk about "far clip plane", "strafe", "rigid object models". | Every option is explained next to it. Mod descriptions say what happens in the game and which keys to press. | -| ![](img/app/ux_wizard_before.jpg) | ![](img/app/ux_wizard_after.jpg) | +| ![](../img/app/ux_wizard_before.jpg) | ![](../img/app/ux_wizard_after.jpg) | | A text file is "shorter than expected (truncated?)", a wrong folder gets "expected Setup/…/Hd/groove.exe", a half-downloaded ISO is "Game found" and fails only after Set up. | Plain messages that say what to do; an incomplete ISO is caught at once, with both sizes. | -| ![](img/app/ux_exe_before.jpg) | ![](img/app/ux_exe_after.jpg) | +| ![](../img/app/ux_exe_before.jpg) | ![](../img/app/ux_exe_after.jpg) | | On Linux a missing groove.exe is blamed on antivirus software and explained with Windows Defender menus. After Rebuild you have to click Play again. | Linux gets a short message; **Rebuild** writes the file and starts the game. | -| ![](img/app/ux_saves_before.jpg) | ![](img/app/ux_saves_after.jpg) | +| ![](../img/app/ux_saves_before.jpg) | ![](../img/app/ux_saves_after.jpg) | | "lowest 4 bits of the coin count", "unknown item flag (bit 0)", "Cursor on", "Revert"; no hint how to restore a backup. | Plain wording, "Opens at", "Undo changes"; **Import…** starts in the backups folder and the hint says so. | -| ![](img/app/ux_settings_before.jpg) | ![](img/app/ux_settings_after.jpg) | +| ![](../img/app/ux_settings_before.jpg) | ![](../img/app/ux_settings_after.jpg) | | "registry values ScreenW/ScreenH", "wine from PATH"; the mod cache shows 0.0 MB after applying 6 mods (stale). | "Automatic (1920x1052)" shows the real size, plain hints, a note on what *Set up again* keeps; the cache size is current. | -| ![](img/app/ux_dark_before.jpg) | ![](img/app/ux_dark_after.jpg) | +| ![](../img/app/ux_dark_before.jpg) | ![](../img/app/ux_dark_after.jpg) | | Dark theme: the *modifies groove.exe* badge is dark orange on grey (barely readable). | A lighter badge text in the dark theme. | | New: quitting a hidden or hung game | New: crash report | |---|---| -| ![](img/app/ux_quit_after.jpg) | ![](img/app/ux_crash_after.jpg) | +| ![](../img/app/ux_quit_after.jpg) | ![](../img/app/ux_crash_after.jpg) | | While the game runs, Play becomes **■ Quit game** (neutral colour) with a confirmation. | Shown as soon as Wine reports the crash (the game hides behind Wine's error window until that is closed), with Quit game / Open logs. | -![](img/app/ux_av_linux_before.jpg) +![](../img/app/ux_av_linux_before.jpg) *Before: on Linux every first Apply of an exe mod showed this Windows Defender notice. Now it is shown on Windows only.* diff --git a/notes/editor-tools-environment.md b/notes/editor-tools-environment.md index 3c35f33..feed0de 100644 --- a/notes/editor-tools-environment.md +++ b/notes/editor-tools-environment.md @@ -5,7 +5,7 @@ Code: `src/main/java/com/eliasmueller/llamamod/editor/tools/environment/`, tests `src/test/.../editor/tools/environment/` (`EnvironmentToolsTest` needs `-Dllamamod.cd`; `EnvTour` = scripted screenshots, `EnvScenarios` = the in-game test projects). -![zones](img/editor/env_zones.jpg) +![zones](img/editor/env_zone.jpg) ## What the user gets diff --git a/src/test/java/com/eliasmueller/llamamod/editor/bugs/EditorRegressionTest.java b/src/test/java/com/eliasmueller/llamamod/editor/bugs/EditorRegressionTest.java index 4f53716..a3b3d23 100644 --- a/src/test/java/com/eliasmueller/llamamod/editor/bugs/EditorRegressionTest.java +++ b/src/test/java/com/eliasmueller/llamamod/editor/bugs/EditorRegressionTest.java @@ -28,7 +28,7 @@ import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; -/** Regression tests for the bugs in notes/bug-review-editor.md (model, io, build). */ +/** Regression tests for the bugs in notes/dev/bug-review-editor.md (model, io, build). */ class EditorRegressionTest { private static Scene emptyScene() { return new Scene(); diff --git a/src/test/java/com/eliasmueller/llamamod/editor/tools/playtest/PlaytestSessionTest.java b/src/test/java/com/eliasmueller/llamamod/editor/tools/playtest/PlaytestSessionTest.java index 784eeb1..d568194 100644 --- a/src/test/java/com/eliasmueller/llamamod/editor/tools/playtest/PlaytestSessionTest.java +++ b/src/test/java/com/eliasmueller/llamamod/editor/tools/playtest/PlaytestSessionTest.java @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; /** - * B10 (notes/bug-review-editor.md): the restore of the user's mod selection after a boot-to-level + * B10 (notes/dev/bug-review-editor.md): the restore of the user's mod selection after a boot-to-level * play-test is kept on disk, so an editor closed while the game runs restores it on its next start. */ class PlaytestSessionTest { diff --git a/src/test/java/com/eliasmueller/llamamod/editor/ui/DocumentRegressionTest.java b/src/test/java/com/eliasmueller/llamamod/editor/ui/DocumentRegressionTest.java index 78d3b83..e4e4125 100644 --- a/src/test/java/com/eliasmueller/llamamod/editor/ui/DocumentRegressionTest.java +++ b/src/test/java/com/eliasmueller/llamamod/editor/ui/DocumentRegressionTest.java @@ -12,7 +12,7 @@ import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -/** Regression tests for notes/bug-review-editor.md (document state, play-test install target). */ +/** Regression tests for notes/dev/bug-review-editor.md (document state, play-test install target). */ class DocumentRegressionTest { private static Project project(String name) { Scene s = new Scene(); diff --git a/src/test/java/com/eliasmueller/llamamod/editor/ui/StartSafetyTest.java b/src/test/java/com/eliasmueller/llamamod/editor/ui/StartSafetyTest.java index c7fc194..b8f196f 100644 --- a/src/test/java/com/eliasmueller/llamamod/editor/ui/StartSafetyTest.java +++ b/src/test/java/com/eliasmueller/llamamod/editor/ui/StartSafetyTest.java @@ -22,7 +22,7 @@ import java.util.List; import org.junit.jupiter.api.Test; /** - * B13 (notes/bug-review-editor.md): a player start whose grid cell has no collision floor at or + * B13 (notes/dev/bug-review-editor.md): a player start whose grid cell has no collision floor at or * below it crashes the game while loading (page fault 0x405527). Positions verified in game * (Village 1, boot-to-level start override, 2026-11-07): crash at 35.5/7.06/58.5, 35.5/9/58.5, * 36.5/8/58.5, 5/8/5; fine at 34.2/7.35/58.2, 37.5/7.3/58.5 and the original start.