documentation for level builders: docs index, step-by-step guide, file format reference (wad, geometry, objects, textures, audio, testlevel chapters, engine limits), strat vm and script language with the arena as worked example, tools overview, developer notes; readme rewritten as project front page with screenshots; review notes moved to notes/dev

This commit is contained in:
MineTec committed 2026-11-07 16:14:40 +01:00
1 parent 00b2880d67
commit 624718b893
41 files changed
+2175 -259

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
# Strats: how objects work
Every object in the game – a coin, a squirrel, a moving platform, a camera, the llama itself – is
driven by a small program called a **strat** (Argonaut's "strategy" scripts, inherited from Croc).
The programs are compiled bytecode for a virtual machine in `groove.exe` (`FUN_0054d180`). LlamaMod
can read every one of them, change their numbers, write new ones and move them between levels.
| Page | For |
|---|---|
| this page | how the VM works: units, instances, frames, variables, events, spawning, collision |
| [Reading scripts](reading-scripts.md) | the editor's Script view and the assembly listing, with a worked example |
| [The script language](script-language.md) | writing new behaviour in LlamaMod's structured language |
| [Customising strats](custom-types.md) | changing constants, cloning types, custom types in the editor and in mods |
| [Example: the Alligator Arena](arena.md) | a complete minigame written in the script language, explained step by step |
| [Example: the roller coaster](roller-coaster.md) | coming soon |
Lab notes and tables: [notes/strat-opcodes.md](../../notes/strat-opcodes.md) (all 434 opcodes with
semantics, confidence and Argonaut names), [notes/strat-language.md](../../notes/strat-language.md)
(complete syntax), [notes/editor-object-schemas.md](../../notes/editor-object-schemas.md)
(parameters of individual types). Machine-readable tables, used by the editor and the compiler:
[`ops.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/ops.tsv),
[`props.tsv`](../../src/main/resources/com/eliasmueller/llamamod/format/strat/props.tsv).
## Code units and types
The strat code of a level sits at the end of its STPC chunk as a list of **code units**:
`u32 nUnits`, then per unit `u32 nWords` and the words. A unit is one compiled source file and can
contain several strats plus their name strings and data (for example `Spider` + `RopeForSpider`).
The village has 53 units, Lab 1 has 65.
A **type** is just an entry point into this code: a MAP record or a `spawn` instruction names a
byte offset in STPC, and the code there starts with `debugName "Name"`. Code refers to models,
animation sets and other code by **absolute STPC offsets** – which is why STPC cannot be
rearranged without relocating every reference, and why each level carries its own copy of the
code of every type it uses.
LlamaMod's disassembler covers the code of all 34 level WADs (1461 units, 670 555 words) and
assembles every unit back **byte-identically**.
## One frame in the life of a strat
The VM is a **stack machine** that runs every live strat once per frame (30 frames per second):
1. The strat continues where it stopped last frame.
2. It executes instructions – push values, compute, read and write properties, play animations,
move – until it reaches a **yield** (`jmpYield`, `jzYield`, `callYield` …).
3. At the yield its turn ends; the next strat runs. Next frame it continues after the yield.
So a typical object is "set up, then `loop { …; yield }`", and anything that takes time is a loop
that counts frames. Speeds and turn rates in scripts are **per frame**.
`end` / `remove` deletes the strat; `return` returns from a `call`.
## Data a strat works with
| Kind | Syntax | What it is |
|---|---|---|
| **variables** | `v0`, `v1`, … (or an alias name) | the strat's own variables (count = the type's *nVars*). The first *n* hold the instance's **MAP parameters** |
| **properties** | `self.x`, `self.rotY`, `self.floorHeight`, `self.damageTaken` … | 194 named engine fields of the strat: position, rotation, floor under it, distance to the player, animation state, alpha, collision damage … |
| other strats' properties | `player.x`, `parent.v3`, `camera.…`, `target.…`, `boss.…`, `dialog.…` | the player, the strat that spawned this one (`parent`), the camera and a few registered special strats |
| **globals** | `g0` … | shared by all strats; the engine itself never reads most of them – their meaning comes from how scripts use them (e.g. g95 = event bits, g101–g105 = exit target and cell, g17–g20 = dive target) |
| **numbers** | `1.0`, `90deg`, `204r` | 20.12 fixed point (4096 = 1.0 = one cell); angles 0x1000000 = 360°; `r` marks a raw integer |
Binary operators pop *b* (top) then *a* and push `a op b`; comparisons push 0/1; `mul` = a·b >> 12.
## Events
A strat registers **event handlers** in its slots (`onEvent`, in the script language
`on touched { … }` / `on event(type, param) { … }`). When the event fires, the handler runs like a
subroutine. Known event types:
| Type | Fires when |
|---|---|
| 4 | **touched**: contact with another strat (pick-ups, enemies). Needs collision (`collisionOn` + `collRadius`) |
| 0x40 | timer |
| 0x80 / 0x100 | player distance |
## Spawning
`spawn<params, vars, slots, typeFlags, extra>(arguments…, unit:N+offset)` creates a new strat from
code: the five inline values are exactly the type fields of a MAP record
([Objects](../reference/objects.md#strat-record-58-bytes-certain-unless-noted)), the stack
arguments become the child's parameters, and the child's `parent` is the spawner. Facts found
the hard way (by the arena):
* The child appears at the spawner's **matrix position** of the last frame, not at its current
`x/y/z`: after `self.x = …` the spawner must **yield** before spawning.
* A type that uses collision needs collision type flags (e.g. 0x28000 like Coin) and enough
`extra` words for its collision bones – otherwise the game crashes in the heap allocator.
* The strat pool must have room for every spawned strat (`poolExtra`).
## Collision and damage
On contact, each side's `hitDamage` is copied into the other's `damageTaken`, and both get event 4.
The llama's attacks set bits in its `hitDamage`: **256** kick (Action on the ground), **512** dive
(Action in the air; it homes in on the target an enemy publishes in g17–g19), **1024** charge.
The llama counts a contact as *its* hit if the other's `hitDamage` shares a bit with its own,
otherwise `damageTaken & 127 == 1` costs it a heart and knocks it back. The original Alligator has
513 (= 512 | 1): only the dive hurts it, a kick gets you bitten.
## References to level data
Some operands are indices into the level's own data and differ between levels; the cross-level
import rewrites exactly these:
| Operand | Points to |
|---|---|
| `model:N`, `anim:N`, `unit:N[+off]` (`address`, opcode 0xb2) | a model, an animation set, code of a unit; negative = shared animations in `DEFANIM.WAD` |
| `sound:N` (opcode 0xd7) | a sample of the level's sound bank |
| `text:N` (opcodes 0xf0, 0xfa) | a dialogue line in LGPC |
| `tex:N` (pushed before texture opcodes) | a texture entry (shadows, sprites, particles) |
## How the names were found
All 434 opcodes (0x000–0x1b1) were named by decompiling every handler with Ghidra; 252 of them
match Argonaut's own names from the Croc 2 PlayStation demo's jump table and the Croc 1
decompilation (knowledge only, no code used – see the sources in
[notes/strat-opcodes.md](../../notes/strat-opcodes.md#sources)). Confidence: 353 high, 79 medium,
2 low; high-confidence opcodes cover 97 % of all instructions in the levels. The names of rarely
used opcodes and properties can still be wrong.
+240
View File
@@ -0,0 +1,240 @@
# Example: the Alligator Arena
The *Alligator Arena* mod is a complete minigame built from scratch: a walled pit on an island,
alligators coming in waves, coins popping out on every hit, an exit that opens after the last wave.
It uses every technique of this documentation – a scripted level, two new strat types written in
the [script language](script-language.md), and an object (the Alligator) imported from another
world. This page walks through the code.
![The arena: an alligator chasing the llama](../manual/img/mod_arena.jpg)
| File | Role |
|---|---|
| [`ArenaMod.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaMod.java) | the mod: options, writes the WAD and the loading picture |
| [`Arena.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/Arena.java) | the level script: geometry and object placement |
| [`ArenaScripts.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaScripts.java) | the strat scripts `ArenaMaster`, `ArenaGator` (and a test probe) |
| [`ArenaImport.java`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/ArenaImport.java) | compiles the scripts and merges them into the base level |
| [`ArenaTest.java`](../../src/test/java/com/eliasmueller/llamamod/mods/arena/ArenaTest.java) | unit tests (scripts compile for all option corners, the built level decodes) |
| [notes/mod-arena.md](../../notes/mod-arena.md) | lab notes: engine facts found, what was verified in the game |
Options: alligator **lives** (1–10, default 5), **waves** (1–5, default 3; wave *n* brings
min(*n*, 2) alligators), llama **energy** cap (1–5, default 4), **coins** per hit (0–5, default 1),
TestLevel **chapter** (32–49, default 49).
## 1. The plan
* The **base level** is Village 1 (`t1l1m001`), like every new level: its code provides the
Llama, Coin, CheckPointPost and textures.
* The Alligator lives in **Jungle by Day 2** (`t1l4m002`). Its model, animations and sounds are
needed, so the new scripts are **compiled against that level** and then **imported** into the
village base together with everything they reference.
* Two new types: `ArenaMaster`, an invisible referee standing on the exit pad, and `ArenaGator`,
a new alligator spawned by the master.
* The level goes into a TestLevel chapter ([why 32–49](../reference/testlevel.md)).
## 2. The level (`Arena.build`)
The level is a [`LevelScript`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelScript.java)
for the Java [`LevelBuilder`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/LevelBuilder.java),
the same builder the Test World uses. Coordinates are game units (one cell each):
```java
l.sea(0, 0, 40, 40, -1.5, new double[] {ix0 - 3, iz0 - 3, ix1 + 3, iz1 + 3}); // sea + death plane
l.floor(a.x0(), a.z0(), a.x1(), a.z1(), a.floor(), "sand"); // pit floor 16..24
l.box(a.x0() - t, a.z0() - t, a.x1() + t, a.z0(), 0, WALL,
o().top("stone").side("stone_moss")); // one of four walls
// exit pad: one cell with collision material 16 under a gate
l.box(ex - 0.5, ez - 0.5, ex + 0.5, ez + 0.5, a.floor(), a.floor() + 0.06,
o().top("pattern_purple").side("inca").rot(0).sideColl(false).mat(DOOR_MAT));
l.player(st[0], st[1], st[2], null, 1.6); // llama + start camera node 1.6 behind it
l.checkpoint(a.x0() + 0.8, a.z0() + 0.8);
l.strat(ArenaScripts.MASTER, ex, ez, a.floor(), 0.0, null, LevelBuilder.Waypoints.NONE, 0, false);
l.coins(List.of(new double[] {a.x0() + 0.6, 20}, new double[] {a.x1() - 0.6, 20}));
```
Points to note:
* The **exit pad** is a 1 × 1 box whose top has collision **material 16** – the exit rule from
[Objects](../reference/objects.md#the-exit-leveldoor--certain). There is no LevelDoor; the master
takes its role.
* The walls are 1.1 high – too high to jump (0.35), so the fight stays in the pit.
* Texture names (`sand`, `stone_moss`, `inca`) are resolved on the base level by picture.
## 3. Compiling and importing (`ArenaImport`)
`Arena.buildWad` runs before the geometry is built:
```java
merged = ArenaImport.merge(baseWad, src, "t1l4m002", units(probe, debug));
return LevelScript.super.buildWad(merged.wad(), wads, opts);
```
1. **`append`** compiles each unit with `StratCompiler` → `StratAssembler`, laid out as if it were
the last unit of **t1l4m002's** STPC, and appends it there. So `model:47` is the Alligator's model
and `unit:56+0xf0` is that level's `EnemyCoin`. A later unit can name an earlier one as
`unit:@ArenaGator`; `append` replaces that with `unit:N+offset`.
2. **`merge`** asks [`StpcMerge`](../../src/main/java/com/eliasmueller/llamamod/format/stpcmerge/StpcMerge.java)
– the editor's cross-world import engine – to import the two entry points into the village:
it follows every reference and copies the model, 7 animation sets, textures, 3 sound samples and
the spawned strats (`EnemyCoin` and `Sparkles` already exist in the village and are shared),
relocates every operand and verifies the result.
3. It adds one **unplaced prototype MAP record** per type with its type fields
(`nVars`, `slots`, `typeFlags`, `extra`), so the level builder can place the type by name.
The type fields are declared in `Arena.units`:
| Type | vars | slots | type flags | extra | why |
|---|---|---|---|---|---|
| ArenaGator | 16 | 5 | 0xC000 | 3 | collision like the Alligator; 3 collision bones |
| ArenaMaster | 12 | 0 | 0 | 0 | invisible logic, no collision, no handlers |
## 4. The referee: `ArenaMaster`
`ArenaScripts.master()` generates the script; the waves are unrolled by Java. With 2 waves it
reads like this (simplified):
```
strat ArenaMaster {
alias wave = v1
alias alive = v3
alias kills = v4
alias t = v5
g104 = -1 // no exit cell yet (the llama's own exit check reads g104/g105)
g105 = -1
t = 60 // 2 seconds
while (t > 0) { t -= 1; call cap; yield }
wave = 1
self.x = 20.5 // move to a spawn spot …
self.z = 22.2
yield // … and let the engine update the matrix before spawning
spawn<0, 16, 5, 50176, 3>(unit:@ArenaGator)
alive += 1
self.x = 20.5 // back onto the exit pad
self.z = 23.5
while (alive > 0) { call cap; yield } // gators do parent.v3 -= 1 when they die
t = 45
while (t > 0) { t -= 1; call cap; yield }
wave = 2
… // two spawns this time
g101 = 4096r // exit target (1, 9, chapter) as raw 20.12 values
g102 = 36864r
g103 = 200704r
g104 = int(self.x) // the exit cell
g105 = int(self.z)
… // 5 bonus EnemyCoins
loop {
call cap
if (player.floorMaterial == 16 && int(self.x) == int(player.x)
&& int(self.z) == int(player.z)) {
endLevel(4096r, 36864r, 200704r) // the LevelDoor's rule
g1 += 1
remove
}
… // a Sparkles effect every 8 frames
yield
}
}
sub cap { // energy option: keep the llama's energy at the cap
if (player.health2 > 4) { player.health2 = 4 }
}
```
What it shows:
* **Variables as state** – `alive` is `v3`, which the gators change from outside with
`parent.v3 -= 1`: the parent/child link is the communication channel.
* **Waiting = looping with yield**: every wait is a `while … yield` that also calls `cap`, so the
energy cap holds every frame (also after a respawn).
* **The spawn rule**: move, `yield`, then `spawn` – without the yield the alligator would appear at
the master's old position (it copies the matrix of the last frame).
* **The exit** is the LevelDoor logic written out: material 16 under the llama in the same cell →
`endLevel(tribe, world, chapter)`. Setting g101–g105 additionally arms the Llama's built-in check
for the same cell.
## 5. The alligator: `ArenaGator`
`ArenaScripts.gator()` reuses the original Alligator's assets but has its own behaviour. Its parts:
**Setup** – model, size, collision and how it takes hits:
```
setModel(model:47)
scale(1.5)
self.hitDamage = 1793 // 1 | 256 | 512 | 1024
collisionOn<16384>()
collRadius(409r)
collisionBone(0, 409r) // three collision spheres along the body
collisionBone(6.0, 409r)
collisionBone(10.0, 409r)
on touched {
if (self.damageTaken & 1792) { // kick, dive or charge of the llama
if (cool <= 0 && lives > 0) { hit = 1 }
} else {
bit = 1 // contact without an attack: it bit the llama
}
}
```
`hitDamage` 1793 shares a bit with all three llama attacks, so each of them counts as the llama's
hit; bit 1 makes a plain contact hurt the llama ([Collision and damage](README.md#collision-and-damage)).
The handler only sets flags – the main loop reacts to them, which keeps the handler short.
**Fade-in** – `setAmbient` from 0 to 63 over 32 frames, playing the idle animation.
**Main loop** – a state machine in a `switch`, once per frame:
| State | Behaviour |
|---|---|
| 0 watch | turn towards the player (2° per frame); after a timer, if the llama is within 5 cells and on the same floor height (±0.4), play the snap animation and sound |
| 1 snap | wait for the animation, then start chasing |
| 2 chase | accelerate up to `0.03 + 0.006 · wave` per frame towards the llama; lunge when closer than 1.2, give up after 75 frames |
| 4, 5 lunge | fast forward move, then slow down (numbers from the original Alligator) |
| 3 back off | move backwards for a few frames, then rest and watch again |
Before the `switch`, the flags from the handler are handled:
* `hit`: lose a life, sound, three green particle puffs, *coins* `EnemyCoin`s, blink for 45
frames (alpha 0.25 and a red tint every 4 frames, no further hits meanwhile), back off.
* `bit`: back off and rest 90 frames, so the llama gets a breather.
After the `switch` the gator is clamped to the pit and to the floor height.
**Death** – when `lives` reaches 0 the loop `break`s: collision off, the original's death (shrink
over a few frames with puffs, five `EnemyCoin`s), then `parent.v3 -= 1` (one alive less for the
master), `parent.v4 += 1` (kills) and `remove`.
**`sub aim`** – ported from the original Alligator: each frame it publishes its position in
g17–g19 (and the distance in g20) if it is the best target below a jumping llama. That is what makes
the llama's **dive** home in on it.
## 6. The mod (`ArenaMod`)
`apply` refuses a chapter another mod already wrote, builds the WAD, rewrites the tally total
(`LevelTally.counted`: 2 placed coins + every coin the alligators can drop + 5 bonus), writes it
to `wads/T1L9M0nn.wad` and draws a loading picture with Java2D (`CorEncoder`). That is all a level
mod needs: LlamaMod's mod pipeline puts the files into the game's CD folder and links the empty
TestLevel chapters.
## 7. How it was tested
* `ArenaTest` compiles the scripts for every corner of the options, checks wave sizes, builds the
level from the CD and decodes the merged code (the master spawns the gator unit, the gator
spawns EnemyCoin).
* In the game ([`tools/apptest.py`](../../tools/apptest.py)): a probe type (`ArenaProbe`, option
`probe=true`) proved that compiled `on touched` handlers fire; then fights with default options,
a quick win with `lives=1, waves=1`, and a defeat (energy 0 → *Continue* restarts the arena).
![Defeated alligator: puffs and coins](../img/strats/arena_defeat.jpg)
![Win: bonus coins on the exit pad, then the tally](../img/strats/arena_win.jpg)
## What to take from it
1. Borrow assets by **compiling against the level that has them** and importing with `StpcMerge`.
2. Keep event handlers short; set flags and react in the main loop.
3. Communicate through `parent.vN` and globals; spawn only after a `yield`.
4. Copy numbers (speeds, animation indices, type fields) from the original type's script.
5. Write the exit yourself if you need a condition – material 16 + same cell + `endLevel`.
+101
View File
@@ -0,0 +1,101 @@
# Customising strats
From the smallest change to a completely new object type. Steps 1–5 need only the level editor;
step 6 is for programmers writing a LlamaMod mod.
| Step | What changes | Where it is stored |
|---|---|---|
| 1. Object parameters | one placed object | the project's object list |
| 2. Constants | a number in a type's code – every object of that type | `stratEdits.constants` |
| 3. Import a type from another level | the level's code gets a new type with its models, sounds, texts | the project's import list |
| 4. Clone a type | a new type as a copy of an existing one | `stratEdits.types` (source text) |
| 5. New type from a script | a new type written from scratch | `stratEdits.types` |
| 6. Script types in a mod | types compiled and merged by Java code | the mod's source |
A project never stores game code: constants are stored as *old → new value at an offset*, custom
types as source text, imports as *type X from level Y*. Everything is rebuilt from the player's own
CD when the project is opened ([notes/editor-format.md](../../notes/editor-format.md)).
## 1. Object parameters
Select an object; the Inspector's *Object parameters* section shows its MAP parameters (= the
variables v0, v1, … of its strat). For many common types the editor knows names, ranges and meanings
(schemas in [`Schemas.java`](../../src/main/java/com/eliasmueller/llamamod/editor/tools/objects/Schemas.java));
the rest are raw values with the values used in the original levels as a hint.
User guide: [manual chapter 9](../manual/09-objects.md).
## 2. Constants
Double-click a line with a number in the Script panel (or *Edit value…*). The change applies to
every object of the type, is undoable and turns the line orange. On load the old value is checked
first, so a project made for another game version cannot corrupt the code silently. A
`pushShort` holds only −8.0 … 7.99; larger values need a source edit.
![Original and edited Coin constant in the game](../manual/img/strat_coin_constant_ab.jpg)
## 3. Importing types from other levels
*Assets → All worlds* lists every object of every level; dragging one into the viewport imports it.
The import copies the type's code **and everything it reaches**: spawned sub-strats (projectiles,
shards, heads), models, animation sets, textures, sound samples and dialogue lines, relocates all
references, shares what the level already has (Coin, EnemyCoin, Sparkles …) and verifies the
merged code. Limits: 16 MB level memory, 128 sounds, 127 texture pages.
Details: [notes/editor-cross-import.md](../../notes/editor-cross-import.md),
[manual chapter 9](../manual/09-objects.md#importing-objects-from-other-worlds).
## 4. Cloning a type
*Clone as new type…* in the Script panel copies a type's listing into the type editor, renamed.
Change what you need, *Check* (assemble and verify) and *Create type*. The new unit is appended at
the end of the level's code – nothing else moves – and the type appears in *Assets → Objects*.
* **Type fields / parameters from** decides which existing type's MAP fields (variables, slots,
collision flags, extra words) and parameter names new objects get. Pick the type you cloned,
unless you changed how many variables or collision bones the code uses.
* *Edit type…* and *Delete type…* work later on, with undo; placed objects follow their type.
* Custom types can refer to each other by name (`unit:OtherType`).
The manual's tutorial *Make the squirrel run in a circle* clones Bucky and inserts a circle loop
([manual chapter 22](../manual/22-strats.md#tutorial-make-the-squirrel-run-in-a-circle)).
![Two squirrels running in circles: waypoint chain and custom script](../manual/img/strat_bucky_circle.jpg)
## 5. A new type from a script
*Level → New object type from script…* opens the type editor with a template. Set *Language* to
*Script* and write the behaviour in the [script language](script-language.md). Models, animations
and sounds are those of the current level (`model:N`, `anim:N`, `sound:N` – look them up in the
Script panel of a type that uses them). Choose *Type fields from* a type with similar needs (a
pick-up: Coin; a walking enemy: one of the enemies).
## 6. Script types in a mod (Java)
A mod can do the same in code. The arena shows the pattern
([`mods/arena/`](../../src/main/java/com/eliasmueller/llamamod/mods/arena/)):
1. Write the types as script-language text (often with values filled in from options).
2. **Compile against a source level** whose models, animations and sounds you want to use – the
indices in the script are that level's:
`StratAssembler.assemble(StratCompiler.compile(src), StratListing.layout(stpcImage, base))`
and append the words as a new unit to the source level's STPC (`ArenaImport.append`).
3. **Merge into the base level** with `StpcMerge` (the same engine as the editor's import): it
copies the closure (models, animation sets, textures, sounds, spawned strats), relocates and
verifies. Add one unplaced prototype MAP record per type with its type fields.
4. **Build the level** with a `LevelScript` / `LevelBuilder`
([`levelbuild/`](../../src/main/java/com/eliasmueller/llamamod/levelbuild/)) and place the types by
name with `l.strat(...)`.
5. Write the WAD into a TestLevel chapter from the mod's `apply` and add a loading picture.
The [arena page](arena.md) walks through every part of this.
## Choosing type fields
| Field | What to put there |
|---|---|
| variables | at least the highest `vN` the code uses + 1 (parameters count as variables) |
| slots | number of event handler slots (`on …` blocks) – at least 1 if there is one |
| type flags | 0 for invisible logic; collision types need collision flags (Coin: 0x28000; Alligator: 0xC000 / 0xC400) |
| extra | collision-bone words: at least the number of `collisionBone` calls |
Wrong values do not always fail at once: too small collision areas corrupt the strat heap and
crash later (page fault in `FUN_0041ae20`).
+114
View File
@@ -0,0 +1,114 @@
# Reading scripts
LlamaMod shows every strat in two forms: the **Script view**, a readable reconstruction, and the
**Assembly** listing, the exact instructions. Both are in the level editor's *Script* panel
(**View → Panels → Script**, **Ctrl+Shift+K**); the user-level walkthrough is
[manual chapter 22](../manual/22-strats.md).
![The Script panel with Bucky's script](../manual/img/strat_script_panel.jpg)
Code: [`StratListing`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratListing.java)
(assembly), [`StratPseudo`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratPseudo.java)
(script view), [`OpTable`](../../src/main/java/com/eliasmueller/llamamod/format/strat/OpTable.java)
(names and help texts).
## Example: the Coin
Script view of the village coin:
```
strat Coin:
setModel(model:5)
collRadius(409r)
onEvent ev_0000, 0 // event type 4 → L_0084
if (v1) { collisionOn<32768>() ... }
if (v0) {
if (self.floorMaterial == -1.0) { skipArgs … } else {
self.y = self.floorHeight + 204r
self.rotY = rnd(4096.0)
}
}
loop { turnLeft(5.625deg); yield }
```
Line by line:
| Line | Meaning |
|---|---|
| `setModel(model:5)` | show model 5 of this level's STPC (the coin) |
| `collRadius(409r)` | collision radius 409/4096 ≈ 0.1 cell |
| `onEvent ev_0000, 0` | install the handler table entry `ev_0000` in slot 0: event type 4 (touched) jumps to `L_0084`, the pick-up code |
| `if (v1) …` | parameter 1 switches collision on |
| `if (v0) …` | parameter 0 (`snapToFloor` in the editor) puts the coin 204/4096 ≈ 0.05 above the floor under it and turns it randomly – unless there is no floor (`floorMaterial == -1`) |
| `loop { turnLeft(5.625deg); yield }` | spin 5.625° per frame, forever |
Changing `204r` to `1.5` makes every snapping coin float 1.5 cells high – that is a constant edit
([Customising strats](custom-types.md)).
## Script view conventions
| You see | It means |
|---|---|
| `self.x`, `player.y`, `parent.v3` | property or variable of this strat, the player, the spawner |
| `v0`, `snapToFloor` | variable 0; named when the editor knows the type's parameter |
| `g12` | global variable 12 |
| `1.0`, `5.625deg`, `204r` | fixed point (1.0 = one cell), an angle, a raw integer (204/4096) |
| `op<a, b>(x, y)` | instruction `op` with inline operands `a, b` and stack arguments `x, y` |
| `loop { … yield }` | runs once per frame |
| `while`, `do … while`, `switch`, `break`, `continue`, `break name` | rebuilt from the jumps |
| `a && b`, `a \|\| b` | short-circuit conditions (chains of conditional jumps) |
| `goto L_xxxx` | jumps that do not form nested blocks – 31 places in the whole game, all one pattern in the llama's script |
The script view is a **reading aid** and is not compiled back. Edits happen on the assembly (or
in the script language for new types).
## The assembly listing
One instruction per line, exactly as stored:
```
Coin:
debugName "Coin"
address model:5 ; model
setModel
onEvent ev_0000, 0 ; event type 4 → L_0084
pushVar snapToFloor ; v0 (alias from the object schema)
jz L_018c
...
L_0218:
push 5.625deg ; = 64.0
turnLeft
jmpYield L_0218
end
```
* Short opcodes (below 0x45) carry a signed 16-bit operand: `pushVar v3`, `pushShort 1.5`,
`jz label`. Long opcodes take inline words: `spawn 3, 6, 0, 0, 0`.
* Directives: `.unit`, `.alias`, `.word`, `.string`, `.event`, `.switch` / `.case`, `.at`.
* Disassemble → assemble is byte-identical for all 1461 units of the game; no instruction is
emitted as raw words.
Full syntax: [notes/strat-language.md](../../notes/strat-language.md). Every opcode with its stack
effect and meaning: [notes/strat-opcodes.md](../../notes/strat-opcodes.md).
## Finding things
* **Which script runs this object?** Select it with *Follow selection* on.
* **Who spawns this type?** The side panel lists the units that reference it.
* **What does this instruction do?** Select the line; the side panel shows the opcode's
semantics, stack effect, Argonaut name and confidence.
* **Where is this variable used?** *Uses* lists every line with the selected operand.
* **Outside the editor:** [`tools/strats.py`](../../tools/strats.py) `level.wad --all` lists every
named strat of a level with code offsets and type fields;
[notes/strat_types_table.md](../../notes/strat_types_table.md) shows which level has which type.
## Typical patterns
| Pattern | Meaning |
|---|---|
| `v1 * 30` | seconds → frames |
| `self.distToPlayerSqXZ < 25` | player within 5 cells (squared distance) |
| `turnTowardAngleY(self.angleToPlayer, 2deg)` | turn towards the player, 2° per frame |
| `if (!self.animPlaying) animPlay(anim:N)` | loop an animation |
| `wpFirst()`, `wpNext()`, `self.wpParam` | walk the waypoint chain; per-node parameter |
| `g95 = g95 \| (1 << (v - 1))` | set an event bit that guards wait for |
+20
View File
@@ -0,0 +1,20 @@
# Example: the roller coaster
> **Coming soon.** A roller-coaster mod is in development. This page will walk through it the way
> [the arena page](arena.md) does, once it is finished. Until then, its lab notes will appear as
> `notes/mod-coaster.md` and its code under `src/main/java/com/eliasmueller/llamamod/mods/coaster/`.
<!-- TODO (docs): fill in when mods/coaster is merged: overview + screenshots (docs/img/strats/),
files table, the track geometry, the cart/player strat scripts step by step, options, tests,
lessons learned. Link notes/mod-coaster.md and the 18-mods.md section. -->
## What is already known about the original rides
* Catacombs chapters 1, 3 and 5 (`t1l7m001`, `t1l7m003`, `t1l7m005`) are roller-coaster rides. In
these levels the player is not the `Llama` strat but a **`RollerCoaster`** strat
([Objects](../reference/objects.md#special-objects)).
* The rides have the longest views of the game: fog end up to 23 cells
([Engine facts](../reference/engine-limits.md#drawing)).
* The ride music is track 25, `ROLLER` ([Music](../reference/audio.md#music)).
* Objects of these levels can be imported into other levels, but they only work with matching
geometry ([manual chapter 9](../manual/09-objects.md#limits-and-things-to-know)).
+87
View File
@@ -0,0 +1,87 @@
# The script language
LlamaMod's **script language** (version 2) is a small structured language in the style of the
Script view. [`StratCompiler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratCompiler.java)
compiles it to strat assembly, and
[`StratAssembler`](../../src/main/java/com/eliasmueller/llamamod/format/strat/StratAssembler.java)
turns that into bytecode with the same checks as hand-written assembly. It is used by the editor's
type editor (*Language: Script*) and by mods such as the [arena](arena.md). Complete reference:
[notes/strat-language.md](../../notes/strat-language.md#script-language-stratcompiler--writing-new-behaviour);
tests: `StratCompilerTest`.
## A first strat
```
// bobs one cell up and down and turns
strat Bobber {
alias base = v0
alias phase = v1
setModel(model:5) // any opcode: name<inline operands>(stack arguments)
base = self.y + 1 // 1 = one cell
phase = 0
on touched { // event handler (type 4), runs as a subroutine
soundPlay(sound:6)
}
loop {
phase = phase + 4deg
self.y = base + sin(phase)
turnLeft(4deg)
yield // end of this frame
}
}
sub wobble { // called with "call wobble", ends with return
self.rotZ = sin(self.rotY) * 0.1
}
```
## Building blocks
| Construct | Notes |
|---|---|
| `strat Name { … }` | a type; its label `Name` is the entry point |
| `sub name { … }`, `call name`, `return` | subroutines |
| `alias name = v3` | a name for a variable; parameter names from the object schema are predefined |
| `x = e`, `x += e`, `-=`, `*=`, `/=` | assignment; `x += 1` compiles to `inc` |
| places | `vN`, alias, `gN`, `self.prop`, `player.prop`, `parent.prop`, `parent.vN`, `camera.` / `target.` / `boss.` / `dialog.prop` |
| `if (c) { } else if (c) { } else { }` | |
| `while (c) { }`, `loop { }`, `do { } while (c)` | a `yield` as the last statement of a loop becomes the loop's frame-ending back jump |
| `switch (e) { case 1: … break default: … }` | C rules (fall through without `break`); case values are constants |
| `break`, `continue`, `yield`, `end`, `remove` | |
| `on touched { … }`, `on event(type[, param]) [slot n] { … }` | event handlers; touched needs `collisionOn<…>()` and `collRadius(…)` |
| `op<a, b>(e1, e2)` | **any opcode** by name: inline operands in `<>` (raw words), stack arguments in `()`; the count must match the opcode |
### Numbers
* `10` and `10.0` are ten cells (fixed point); `1.5`, `90deg` as in assembly.
* **Raw** integers: `215r` (= 215/4096) or hex `0x10`. Values inside `<…>` are always raw.
* Operators over constants are folded at compile time with the VM's own fixed-point rules:
`2 * 3 + 0.5` → `6.5`.
* `use rawNumbers` switches to the version-1 rule (plain integers raw) – old scripts get it
automatically.
### Operators (low → high precedence)
`||` · `&&` · `|` · `&` · `==` `!=` · `<` `>` `<=` `>=` · `<<` `>>` · `+` `-` · `*` `/` · unary
`-` `!`. In conditions `&&` / `||` short-circuit; in a value both sides are evaluated.
### References
`model:N`, `anim:N`, `unit:N[+off]` or `unit:Name` (another custom type), `sound:N`, `tex:N` –
indices into the level the code is compiled against.
## What the compiler does not do (yet)
* no `for` loop, no typed variables (everything is a 32-bit word, fixed point by convention);
* `spawn` takes raw type fields (`spawn<0, 16, 5, 0xC400, 3>(unit:…)`) – you must know the
values the spawned type needs;
* the Script view does not show event handlers as `on … { }` blocks.
## Tips
* Look at an original type that does something similar first (Script panel) and copy its numbers
– speeds, animation indices, sounds and type fields are level-specific.
* Keep each frame's work small and always reach a `yield` in loops; a loop without a yield freezes
the game.
* Test in the game early: the compiler checks the code's form and references, not whether the
object behaves well.