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:
1 parent
00b2880d67
commit
624718b893
41 files changed
+2175
-259
No files matched your search
@@ -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.
|
||||
@@ -0,0 +1,240 @@
|
||||
# Example: the Alligator Arena
|
||||
|
||||
The *Alligator Arena* mod is a complete minigame built from scratch: a walled pit on an island,
|
||||
alligators coming in waves, coins popping out on every hit, an exit that opens after the last wave.
|
||||
It uses every technique of this documentation – a scripted level, two new strat types written in
|
||||
the [script language](script-language.md), and an object (the Alligator) imported from another
|
||||
world. This page walks through the code.
|
||||
|
||||

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

|
||||
|
||||

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

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

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

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