strat scripts: all 434 vm opcodes named from groove.exe handlers (argonaut names via croc 2 lineage), own assembly syntax with byte-exact round trip over all 1461 code units, script view and script panel in the editor, editing values with undo, custom object types from script or clone, manual chapter with the bucky circle tutorial

This commit is contained in:
MineTec committed 2026-11-07 09:25:15 +01:00
1 parent b36a366240
commit 2386443fac
32 files changed
+5983 -5

No files matched your search

+1
View File
@@ -57,6 +57,7 @@ a saved `.llamalevel` file.
| **Problems** | bottom | yes |
| **Environment** | left | yes |
| **Visibility** | right | yes |
| **Script** | bottom | yes |
<!-- /generated:panels -->
### Outliner
+3
View File
@@ -106,6 +106,7 @@ built-in tools and every plugin):
| **C** | Show collision | View |
| **Ctrl+Shift+T** | Texture manager | View → Panels |
| **Shift+V** | Vertex colours only | View |
| **Ctrl+Shift+K** | Script | View → Panels |
| **Ctrl+B** | Build | Level |
| **F5** | Play | Level |
| **Shift+F5** | Play from camera position | Level |
@@ -209,6 +210,7 @@ Every menu command with its shortcut and a short description.
| Light objects (LGHT preview) (on/off) | | |
| Vertex colours only (on/off) | **Shift+V** | Show the baked/painted vertex colours without textures |
| Panels → Texture manager | **Ctrl+Shift+T** | Pages, textures, usage and animations |
| Panels → Script | **Ctrl+Shift+K** | Strat script of the selected object type |
### Level menu
@@ -226,6 +228,7 @@ Every menu command with its shortcut and a short description.
| Visibility regions | | |
| Import objects of a world… | **Ctrl+Shift+I** | Copy object types (enemies, pickups, platforms …) of another world into this level |
| Rebuild object catalogue | | Scan all levels of the CD again (thumbnails, sizes) |
| New object type from script… | | Create a custom object type from strat source text |
### Geometry menu
+150
View File
@@ -0,0 +1,150 @@
# Strat scripts: reading and changing object behaviour
Every object in the game – a coin, a squirrel, a moving platform, a camera – is driven by a small
program, its **strat** (Argonaut's "strategy" scripts). The level editor shows these programs in
the **Script** panel, lets you change their numbers (speeds, heights, timers, damage) and lets you
create **your own object types** by copying a script and editing it.
![The Script panel showing Bucky's script](img/strat_script_panel.jpg)
## The Script panel
Open it with **View → Panels → Script** (**Ctrl+Shift+K**), the **Open script** button in the
Inspector's *Script* section, or click its tab in the bottom dock.
| Part | What it does |
|---|---|
| **Type** | the code unit to show. A unit is one compiled script file and can hold several strats (Bucky + ThrowAcorn). Custom types are marked *[custom]* |
| **Script / Assembly** | *Script* is a readable view with expressions, `if`/`else` and `loop`; *Assembly* is the exact instruction list (the language you write new types in) |
| **Follow selection** | shows the script of the object you select in the viewport or the Outliner |
| **Find**, **Next** | search the text (Enter = next match) |
| **Uses** | lists every line that uses the selected line's variable, property or label |
| **Edit value…** | changes the numbers of the selected line (also: double-click) |
| **Clone as new type…** | copies this type into a new, editable object type |
The side panel explains the selected line: what the instruction does, how many values it takes
from the stack, its Argonaut name where known, and what properties like `floorHeight` mean. With
nothing selected it lists the unit's parameters, which other scripts **spawn or reference** this
one, and how many objects of it are placed.
### Reading a script
* `self.x`, `self.rotY`, `self.floorHeight` are properties of the object itself; `player.…`,
`parent.…` (the object that spawned it) and `camera.…` those of others.
* `v0`, `v1`, … are the object's variables. The first ones are the **parameters** you set in the
Inspector (*Object parameters*): parameter 0 = `v0`. Where the editor knows a parameter's
meaning, the script uses its name (`snapToFloor`).
* `g12` is a global variable shared by all scripts.
* Numbers are in game units: `1.0` = one cell (or "true"), angles are written like `5.625deg`.
Speeds and turn rates are **per frame** (the game runs at 30 frames per second).
* `yield` ends the object's turn for this frame; a `loop { … yield }` runs once per frame.
* `onEvent` installs a handler, for example "when the player touches me" (event type 4).
* `spawn<…>(…)` creates another object, `address model:5` names a 3D model of the level.
## Changing numbers
Double-click a line with a number (or select it and press **Edit value…**). Lines whose numbers
can be edited show a tooltip; edited lines turn **orange**.
![Editing a constant](img/strat_edit_constant.jpg)
* Type the new value as `1.5` (fixed point), `90deg` (angle), `42` or `0x2a`. The dialog shows
the value in all three readings and refuses impossible ones (a `pushShort` holds only
−8.0 … 7.99).
* The change can be undone (**Ctrl+Z**) like every other edit.
* It applies to **every object of that type** in the level, also to objects other scripts spawn.
* It is saved in the project and written into the level when you build or play.
Example: in the Coin's script, `self.y = self.floorHeight + 204` puts snapping coins just above
the floor. Changing `204` to `1.5` makes them float one and a half cells high:
![Original and edited Coin script in the game](img/strat_coin_constant_ab.jpg)
## Your own object types
**Clone as new type…** in the Script panel (or **Level → New object type from script…** for an
empty template) opens the type editor:
![Creating a custom type](img/strat_clone_dialog.jpg)
1. **Name** – the name of the new type (letters, digits, `_`).
2. **Entry label** – where the script starts (for clones: the renamed strat).
3. **Type fields / parameters from** – which existing type's settings (parameters, waypoints,
flags) new objects of this type start with.
4. Edit the source. **Check** assembles it and verifies the result; errors name the line.
5. **Create type**. The type appears in **Assets → Objects** and can be placed like any other.
The new script is added to the level's code; the original type is not changed. Custom types are
stored in the project (as text) and rebuilt every time it is opened.
Language reference: every instruction is one line, labels end with `:`, comments start with `;`.
`push 4deg` / `turnLeft` pushes a number and lets an instruction use it; `jmpYield label` waits
for the next frame and jumps. The full syntax and the list of all 434 instructions are in the
LlamaMod notes (`notes/strat-language.md`, `notes/strat-opcodes.md`).
## Tutorial: make the squirrel run in a circle
Bucky, the nut-throwing squirrel of *Jungle at Night*, can run in a circle in two ways. Both are
in one test level below: on the left the waypoint way, on the right the script way.
![Two squirrels running in circles, eight frames](img/strat_bucky_circle.jpg)
### Find out how Bucky moves
1. Import Bucky (**Assets → All worlds**, search "Bucky", drag it into the level).
2. Select it and open the **Script** panel. The script shows `wpFirst()`, `wpNext()` and
`wpTurnToY(v1)` / `moveForwardQ(215)`: Bucky **runs along its waypoint chain**, turning
with the rate in parameter 1 and moving 215/4096 cells per frame.
3. At every node it reads `self.wpParam` (the node's field **+26**): the low part chooses run
(`0`) or jump (`1.0`, `2.0`), the high part `128.0` means "run on", `64.0` "wait until the
player comes close". At the end: `if (!(!v0 && (self.wpEndFlags & 2))) goto …` – with
**parameter 0** set the chain repeats instead of ending.
### Way 1: a circular waypoint chain (no script change)
1. With Bucky selected, delete its imported path (**Delete path** in the Inspector) and draw a
new one with the waypoint tool (**N**): 12 nodes on a circle (radius 1.5–3 cells).
2. Select all nodes of the chain and set **Field +26** to `524288` (= 128.0, "run on without
waiting").
3. Set Bucky's **Parameter 0** to `1.0` (repeat the chain). Parameter 1 is the turn rate
(`64.0` = 5.6° per frame; larger turns faster).
4. Play. After the last node Bucky continues with the first one – forever.
### Way 2: a scripted circle (custom type)
1. Select Bucky, **Clone as new type…**, name it `BuckyCircle`.
2. In the source, find the label `L_05b0:` (the start of the main loop) and insert before it:
```
push 3.0
popVar v9 ; animation state: running
push 3.0
popVar v22
circle:
push 4deg ; turn 4° per frame
turnLeft
pushShort 215 ; forward speed per frame
moveForwardQ
forceCollision
pushProp floorHeight ; stay on the ground
popProp y
call L_0180 ; Bucky's animation routine
jmpYield circle ; next frame
```
3. **Check**, **Create type**, place a BuckyCircle from **Assets → Objects** and play. The
circle's size is speed ÷ turn rate: 215/4096 cells × 90 frames per turn ≈ 4.7 cells around.
## Limits
* Custom types cannot be deleted or edited in place yet – clone the custom type again with the
changes and replace the placed objects.
* Big structural changes are up to you: the editor checks that the code is well-formed and that
every model/animation/code reference points somewhere valid, not that the object behaves well.
* Names of rarely used instructions are educated guesses (the opcode list shows a confidence for
each).
* Small constants stored as short numbers only accept −8.0 … 7.99 (see *Changing numbers*).
* The Script view shows `goto` where the original jumps don't form nested blocks; the Assembly
view is always exact.
* In the clone dialog, renaming the type doesn't yet rename the entry label or the name inside
the script text – edit those by hand if you want them to match.
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

+1
View File
@@ -46,6 +46,7 @@ open the chapter about the panel, tool or dialog under the mouse pointer.
* [Environment: zones, fog, music and light](12-environment.md)
* [Visibility regions](13-visibility.md)
* [Level settings](14-level-settings.md)
* [Strat scripts: reading and changing object behaviour](22-strats.md)
## Testing and sharing
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

+151
View File
@@ -0,0 +1,151 @@
# Strat language (LlamaMod's assembly for the strat VM)
Object behaviour ("strats") is bytecode for the VM at groove.exe FUN_0054d180, stored per level in
the STPC chunk as **code units** (one per compiled source file; notes/editor-cross-import.md).
`format.strat` turns a unit into text and back:
| Class | Role |
|---|---|
| `OpTable` | opcode/property names and docs (resources `ops.tsv`, `props.tsv`; notes/strat-opcodes.md) |
| `StratListing` | unit → assembly text (+ per-line metadata: offsets, editable constants) |
| `StratAssembler` | text → unit words; layout = where the unit goes + model/anim/unit offsets |
| `StratPseudo` | read-only structured view (expressions, if/else, loops) |
| `Num` | literals: `42`, `0x2a`, `1.5` (20.12), `90deg` |
**Round trip (golden test `StratRoundTripTest`)**: disassemble → assemble is byte-identical for
**all 1461 units of all 34 level WADs** (670 555 words), with **no instruction emitted as raw
words**. Own syntax; the style is loosely inspired by Stratigise (knowledge only, no code used).
## Syntax
One statement per line; `;` starts a comment; `name:` defines a label (several per line allowed).
```
; unit 4 (139 words at STPC 0x422fce): Coin
.unit Coin
ev_0000:
.event 4, 0, L_0084 ; handler entry
L_000c:
.string "Coin", "WARNING: Coin "
.word 0x6f200020
L_0084:
pushProp damageTaken
push 2048.0 ; = 8388608
notEqual
jz L_013c
...
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
```
### Directives
| Directive | Words | Meaning |
|---|---|---|
| `.unit NAME` | 0 | unit name (informational) |
| `.alias name = vN` | 0 | variable alias; the listing declares one per schema parameter |
| `.word a, b, …` | n | raw words (numbers or `@label` = absolute address of a label) |
| `.string "a", "b"` | ⌈(bytes)/4⌉ | packed NUL-terminated strings, zero padded to the next word (`\"`, `\\` escapes) |
| `.event type, param, target` | 3 | event table entry; `rel` is computed against the `onEvent` that names this entry's label (exactly one must) |
| `.switch n` + n × `.case v, target` / `.case imm v, target` | 1 + 2/3 each | switch table (case as 0x44 short or 0x45 immediate); targets relative to the `switch` naming the table's label |
| `.at name, k` | 0 | defines `name` = next statement + k bytes (labels inside an instruction; not needed by the CD levels) |
### Instructions
`mnemonic operand, operand…` – mnemonics from notes/strat-opcodes.md (case-insensitive) or
`op_XX` for any opcode.
| Opcode kind | Operand syntax |
|---|---|
| short ops (< 0x45) | one operand, encoded in the high 16 bits (−32768…32767) |
| – variables (pushVar, popVar, pushVarAddr, parent vars) | `v3` or an alias |
| – globals (pushGlobal, popGlobal, …) | `g12` |
| – properties (pushProp, popProp, pushPlayerProp, …) | name from props.tsv (`x`, `rotY`, `floorHeight`) or `prop17` |
| – branches/calls (jz jnz jzYield jnzYield jmp jmpYield call callYield pushCodeWord pushCodeAddr popCodeWord) | label (or a number = raw word offset) |
| – `switch` | label of the `.switch` table |
| – `onEvent entry, slot[, hi=N]` | label of the `.event` entry, slot number |
| – `skipArgs`/`print` (0x02/0x03) | raw words: count, items… (`hi=N` first if the opcode word has high bits) |
| `push v` (0x45) | number; `tex:N` marks a texture index (before shadowTexture/spawnParticle/…) |
| `address ref` (0xb2) | `model:N[+off]`, `anim:N[+off]`, `unit:N[+off]`, `@label[+off]` (own unit), or a number (negative = DEFANIM.WAD animation) |
| `soundAddress sound:N` (0xd7), `topSay text:N` … (0xf0/0xfa) | level sound / text index |
| `debugName "Name"[, k]` (0xd4 0xea rel) | name string: the k-th (default first) occurrence of the string + NUL in the unit that starts a word or follows a NUL; or a label |
| other long ops | as many numbers as inline words (`spawn 3, 6, 0, 0, 0`) |
Numbers: decimal, `0x…`/`-0x…`, fixed point with a dot (`1.5` → 6144), angles `90deg` (360deg =
0x1000000). The listing prints fixed point when the value reads naturally as one (≥ 64 raw, ≤ 3
decimals), degrees for constants consumed by turn/tilt/sin/cos/`popProp rot*` (only when exact).
### Assembler rules
Two passes (sizes are fixed per mnemonic/directive, then encoding). Errors carry line numbers
(unknown mnemonic/label, operand count, 16-bit overflow, missing `onEvent`/`switch` for a table,
unknown model/anim/unit index). Layout: `base` = STPC offset of the unit's first word (`@label` and
own-unit addresses), plus the model/animation/unit offsets of the target STPC for `model:N` etc.
## Script view (`StratPseudo`)
Read-only. Stack code folded into expressions (`self.y = self.floorHeight + 204`), properties as
`self.x`, `player.y`, `parent.v3`, `camera.…`; forward `jz/jnz` → `if {…}` (with `else` when the
block ends in a forward `jmp`), backward `jmp`/`jmpYield` → `loop {… yield}`; blocks are only
built when they nest, everything else stays `goto label`. Inline operands in `<>`:
`spawn<3, 6, 0, 0, 0>(10.0, 204, 450, unit:35+0x28)`. Example (Coin):
```
strat Coin:
setModel(model:5)
collRadius(409)
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 + 204
self.rotY = rnd(4096.0)
}
}
loop { turnLeft(5.625deg); yield }
```
## Editing in the level editor (`editor.tools.strats`)
* **Script panel** (bottom dock, View → Panels → Script, Ctrl+Shift+K): unit chooser (all code units
with their type names, custom ones marked), *Follow selection*, Script/Assembly view, Find,
*Uses* (all lines with the selected operand), help for the selected line (opcode semantics,
property docs, constant values), side panel with parameters (schema aliases + use counts),
"referenced from" (units whose `address` operands point into this unit = who spawns it) and
instance count. Inspector section "Script" with *Open script*.
* **Constants** (double-click / *Edit value…*): `push`, `pushShort`, sound, texture and text
operands. Undoable (`ConstantEdit`), applied to reference + scene, stored in level.json
`stratEdits.constants` as `{unit, name, at (byte offset in the unit), old, new, short}`; on load
the old value is checked first (mismatch → warning, skipped). Edited lines are orange.
* **New types** (*Clone as new type…* in the panel, Level → *New object type from script…*):
source text (the cloned unit's listing with the strat renamed, or a template), entry label,
type fields/parameters taken from an existing type. *Check* assembles and verifies; *Create*
appends the unit at the end of the STPC code region (nothing else moves), adds the type to the
catalogue (Assets → Objects), stores `stratEdits.types` `{name, basedOn, entry, source, proto}`.
Verification: the merged STPC is disassembled again (no decode errors, entry decoded, every
`address` operand of the new unit hits a model, animation set or code, and the new unit's
listing re-assembles to the same words).
* **Load order** (`ProjectIO.LoadHook.order()`): constants of base units (−10) → imported types
(CrossImport, 0) → constants of imported units and custom types (+10). Custom units are always
last on load; if types were imported after a custom type in the session, the stored strat codes
are remapped with the saved custom block position (`stratEdits.session`).
## Limits
* Custom types cannot be removed or re-edited in place (clone again; the old unit stays as dead
code until the project is recreated). Constants of custom types are changed through their source.
* `pushShort` holds only −8.0…7.99; bigger values need a source edit (`push`).
* The script view is a reading aid (gotos where blocks do not nest; expressions assume the
documented pop/push counts; ops with variable stack use are shown raw).
* Names/semantics of low-confidence opcodes and properties are guesses (see confidence column).
+697
View File
@@ -0,0 +1,697 @@
# Strat VM opcodes (groove.exe, "Argonaut Strategy Language")
Machine-readable tables (the source of the editor's names and help):
`src/main/resources/com/eliasmueller/llamamod/format/strat/ops.tsv` (opcodes) and `props.tsv` (strat
properties). Language/syntax: `notes/strat-language.md`. Disassembler/assembler: `format.strat`.
## Summary
* **434 opcodes** (0x000–0x1b1; 0x00–0x44 "short" with a signed 16-bit argument in the high half
of the word, 0x45+ "long" with inline operand words). All of them have a name and a one-line
semantics from reading every handler (headless Ghidra decompile of the two handler tables
0x5796bc / 0x578ff4, plus the helpers they call).
* Confidence: **329 high, 71 medium, 34 low**. Of the **222 opcodes the 34 level WADs use**: 170 high,
37 medium, 15 low; high-confidence opcodes cover **96.8 %** of the 556314 instructions in the levels.
* **252 opcodes matched to Argonaut names** (column *ref*: Croc 2 PSX demo jump table / Croc 1
handler names), by handler behaviour and table position. 99 slots are no-ops, error stubs or
pop-only stubs (dropped Croc features).
* Strat properties (short ops 0x07/0x40 own, 0x1c/0x37 player, …): **194 ids** named (`props.tsv`),
getter FUN_00550e60 and setter FUN_005509f0 share the numbering; ids without a setter are read-only
(a write would go to address 0).
## Sources
* groove.exe handler tables, decompiled with Ghidra 12.2 (headless, all 0x1b2 + 0x45 entries and
their callees). Usage counts: our disassembler over all 34 level WADs.
* Knowledge only (no code copied – Stratigise is GPL-2.0): the opcode name lists of
[Stratigise](https://github.com/Argonaut-PS1-Reverse/Stratigise) (fork:
[knot126/Stratigise](https://github.com/knot126/Stratigise)) – especially
`doc/Croc2/Croc 2 psx demo strat_jump_table.txt` (319 handler names of the Croc 2 PSX demo) and
`doc/Croc1/Opcodes.md` – and the Croc 1 decompilation
[Xeeynamo/croc](https://github.com/Xeeynamo/croc) (`src/croc/3038.c`, `st*` handler names).
Croc 1 uses a different, expression-tree encoding; Croc 2 is the direct ancestor: our long
opcodes are the Croc 2 table index + 0x3b at the start (0x0a stNumber = our 0x45 push,
0x0e stAdd = 0x49 add), the offset shrinks stepwise where Croc 2 entries were dropped and grows
where Groove added opcodes (0x50 shl, 0x51 shr, 0x52 animToFrame, 0x9b stream play, most of
0x140+: particles, trails, tint, update order, level end).
## Conventions
* Stack: `PUSH` = FUN_0054bbd0, `POP` = FUN_0054bc00 (strat+0x104 = stack pointer). Binary ops
pop **b** (top) then **a** and push `a op b`. Comparisons push raw 0/1.
* Numbers are 20.12 fixed point (0x1000 = 1.0); `mul` = a·b>>12, `div` = (a<<8)/b<<4.
* Angles: a full turn is **0x1000000** (4096.0 in 20.12, i.e. 4096 sin-table units << 12);
getters mask angles to 24 bits. The language writes them as `90deg`.
* Variables: strat+0xF4 array; the first n are the MAP parameters of the instance.
Event handlers (0x0c): 16-byte slots at strat+0xF8; entry {type, param, rel}; type 4 = touched /
collision (pickups), 0x40 = timer, 0x80/0x100 = player distance.
* Special strats: player 0x6D9E1C (set by 0xb5 isPlayer), camera 0x6D9E28, camera targets
0x6D9E30/34, boss 0x6D9E24, dialog strat 0x6D9E2C, "parent" = strat+0xC (the spawner).
Short-op families: push value / push address / pop for own vars (0x04/0x08/0x41), own props
(0x07/0x0b/0x40), parent vars (0x18/0x19/0x3e) and props (0x1a/0x1b/0x3f), player
(0x1c/0x1d/0x37), camera (0x1e/0x1f/0x38), target (0x20/0x21/0x39), target2 (0x24/0x25/0x3a),
boss (0x26/0x27/0x3d), dialog (0x28/0x29/0x3b), globals 0x06/0x0a/0x43 (array 0x6D9E8C; the exe
never indexes it – meanings only from script use).
* Calls: 0x11 `call` pushes the return address (0x10 is the same with a yield, unused);
`return` (0xb3) pops it. Yield = flag 2 in strat+0xE8 ends the strat's frame.
## Opcodes
ops = operands (inline words for long ops, the 16-bit argument for short ops); pops/pushes
(? = variable); uses = instructions in the 34 level WADs; conf = confidence.
| op | name | operands | pops | pushes | uses | conf | ref | semantics |
|---|---|---|---|---|---|---|---|---|
| 0x00 | `nop_0x00` | - | 0 | 0 | | high | stCommandError | No-op (shared empty handler 00426500); unused in levels. <sub>426500</sub> |
| 0x01 | `hide` | imm | 0 | 0 | 294 | medium | stHide | arg!=0 sets, arg==0 clears flag 0x1000000 in +0xE8; renderer and shadow pass skip strats with this bit, so it hides the strat. <sub>54fa50</sub> |
| 0x02 | `skipArgs` | count | 0 | 0 | 1523 | medium | - | Inline block: count word, then count push instructions (0x44 one word, 0x45/0xea two words) that are skipped without executing; data block read by 0x0c event descriptors (same use count as 0x0c). <sub>553850</sub> |
| 0x03 | `print` | count | 0 | 0 | 26 | medium | stPrint | Debug print with output stripped: skips count inline push instructions like 0x02 and advances debug text line y DAT_006d9db8 by 12 (reset to 0x20/0x48 each frame). <sub>5537f0</sub> |
| 0x04 | `pushVar` | var | 0 | 1 | 50710 | high | stLocal | Pushes own variable n (+0xF4 array). <sub>553710</sub> |
| 0x05 | `pushCodeWord` | rel | 0 | 1 | 438 | high | - | Pushes the 32-bit word at pc+arg*4 (inline data in the code stream, pc = next instruction). <sub>5537b0</sub> |
| 0x06 | `pushGlobal` | global | 0 | 1 | 27756 | high | stGlobal | Pushes global n (0x6D9E8C[n]). <sub>5536b0</sub> |
| 0x07 | `pushProp` | prop | 0 | 1 | 38636 | high | stAlienVar | Pushes own property n via FUN_00550E60(self,self,n). <sub>552ab0</sub> |
| 0x08 | `pushVarAddr` | var | 0 | 1 | 2074 | high | stLocalAddress | Pushes address of own variable n (for 0x47/0x48/0x4d). <sub>5536f0</sub> |
| 0x09 | `pushCodeAddr` | rel | 0 | 1 | 28 | high | - | Pushes address pc+arg*4 (address of inline code-stream data word). <sub>553790</sub> |
| 0x0a | `pushGlobalAddr` | global | 0 | 1 | 1087 | high | stGlobalAddress | Pushes address of global n. <sub>553690</sub> |
| 0x0b | `pushPropAddr` | prop | 0 | 1 | 248 | high | stAlienVarAddress | Pushes address of own property n (FUN_005509F0(self,self,n)). <sub>552ad0</sub> |
| 0x0c | `onEvent` | rel,imm | 0 | 0 | 1523 | high | - | Installs event handler slot (2nd word) in table +0xF8 (16-byte slots): descriptor at word pc+1+off holds type, param, rel code address of the handler; type 0x40 = timer (param converted to frames), types 0x80/0x100 enable player-distance tracking (+0xEC bit 0x4000). <sub>54fc40</sub> |
| 0x0d | `clearEvent` | imm | 0 | 0 | 35 | high | - | Clears event handler slot arg (type word := 0). <sub>54dbe0</sub> |
| 0x0e | `disableEvent` | imm | 0 | 0 | | high | - | Disables event handler slot arg (sets bit 31 of its type word). <sub>54dc20</sub> |
| 0x0f | `enableEvent` | imm | 0 | 0 | | high | - | Re-enables event handler slot arg (clears bit 31 of its type word). <sub>54dc00</sub> |
| 0x10 | `callYield` | rel | 0 | 1 | | high | stJsr | Pushes return address (next pc), jumps to next+arg*4 and yields (+0xE8 bit 2); unused in levels. <sub>553d40</sub> |
| 0x11 | `call` | rel | 0 | 1 | 8471 | high | stJsrImm | Pushes return address (next pc) and jumps to next+arg*4 without yielding; the call form actually used by the compiler. <sub>553d10</sub> |
| 0x12 | `jzYield` | rel | 1 | 0 | 3012 | high | stBeq | Pops cond; if 0 jumps to next+arg*4; always yields this frame. <sub>553e10</sub> |
| 0x13 | `jnzYield` | rel | 1 | 0 | 202 | high | stBne | Pops cond; if !=0 jumps to next+arg*4; always yields this frame. <sub>553eb0</sub> |
| 0x14 | `jz` | rel | 1 | 0 | 32026 | high | stBeqImm | Pops cond; if 0 jumps to next+arg*4. <sub>553e50</sub> |
| 0x15 | `jnz` | rel | 1 | 0 | | high | stBneImm | Pops cond; if !=0 jumps to next+arg*4; unused. <sub>553e80</sub> |
| 0x16 | `jmp` | rel | 0 | 0 | 15125 | high | stJumpImm | Jumps to next+arg*4. <sub>553d80</sub> |
| 0x17 | `switch` | rel | 1 | 0 | 597 | high | stIndex_Jump | Pops value; table at next+arg*4+4: count, then entries (case value as 0x44 short or 0x45 imm, then rel offset); on match jumps to next+offset*4, otherwise falls through. <sub>550060</sub> |
| 0x18 | `pushParentVar` | var | 0 | 1 | 711 | high | stExt_Local | Pushes variable n of the linked strat at +0xC (parent/creator). <sub>553750</sub> |
| 0x19 | `pushParentVarAddr` | var | 0 | 1 | 135 | high | stExt_LocalAddress | Pushes address of variable n of the +0xC strat. <sub>553770</sub> |
| 0x1a | `pushParentProp` | prop | 0 | 1 | 1362 | high | stExt_AlienVar | Pushes property n of the +0xC strat. <sub>552af0</sub> |
| 0x1b | `pushParentPropAddr` | prop | 0 | 1 | | high | stExt_AlienVarAddress | Pushes address of property n of the +0xC strat; unused. <sub>553730</sub> |
| 0x1c | `pushPlayerProp` | playerProp | 0 | 1 | 4320 | high | stPlayer_AlienVar | Pushes property n of the player strat (DAT_006D9E1C, set by 0xb5), 0 if none. <sub>552d20</sub> |
| 0x1d | `pushPlayerPropAddr` | playerProp | 0 | 1 | | high | stPlayer_AlienVarAddress | Pushes address of player property n (0 if no player); unused. <sub>552d50</sub> |
| 0x1e | `pushCameraProp` | prop | 0 | 1 | 855 | high | stCamera_AlienVar | Pushes property n of the camera strat (DAT_006D9E28, set by 0xc8), 0 if none. <sub>552b60</sub> |
| 0x1f | `pushCameraPropAddr` | prop | 0 | 1 | | high | stCamera_AlienVarAddress | Pushes address of camera property n (0 if none); unused. <sub>552b90</sub> |
| 0x20 | `pushTargetProp` | prop | 0 | 1 | 1484 | medium | stTarget_AlienVar | Pushes property n of the camera target strat (DAT_006D9E30, set by 0xc9 from the player character strats), 0 if none. <sub>552c40</sub> |
| 0x21 | `pushTargetPropAddr` | prop | 0 | 1 | | medium | stTarget_AlienVarAddress | Pushes address of target property n (0 if none); unused. <sub>552c70</sub> |
| 0x22 | `error_0x22` | - | 0 | 0 | | high | stCollide_AlienVar | Prints "stCollide_AlienVar" not-implemented error; unused. <sub>54d200</sub> |
| 0x23 | `error_0x23` | - | 0 | 0 | | high | stCollide_AlienVarAddress | Prints "stCollide_AlienVarAddress" not-implemented error; unused. <sub>54d220</sub> |
| 0x24 | `pushTarget2Prop` | prop | 0 | 1 | | medium | stTarget_AlienVar | Pushes property n of the second target strat (DAT_006D9E34, set by 0xca), 0 if none; unused. <sub>552bd0</sub> |
| 0x25 | `pushTarget2PropAddr` | prop | 0 | 1 | | medium | stTarget_AlienVarAddress | Pushes address of target2 property n (0 if none); unused. <sub>552c00</sub> |
| 0x26 | `pushBossProp` | prop | 0 | 1 | | low | stBoss_AlienVar | Pushes property n of the strat in DAT_006D9E24 (set by 0xef), no null check; unused. <sub>552b10</sub> |
| 0x27 | `pushBossPropAddr` | prop | 0 | 1 | | low | stBoss_AlienVarAddress | Pushes address of property n of the DAT_006D9E24 strat; unused. <sub>552b30</sub> |
| 0x28 | `pushDialogProp` | prop | 0 | 1 | 1678 | low | stDialog_AlienVar | Pushes property n of the strat in DAT_006D9E2C (set by 0x113, used by NPC/puzzle strats), 0 if none. <sub>552cb0</sub> |
| 0x29 | `pushDialogPropAddr` | prop | 0 | 1 | | low | stDialog_AlienVarAddress | Pushes address of property n of the DAT_006D9E2C strat (0 if none); unused. <sub>552ce0</sub> |
| 0x2a | `shadow` | imm | 0 | 0 | 887 | medium | stShadow | arg!=0 sets, 0 clears +0xE8 bit 0x20 (shadow on; shadow pass requires it plus size from 0x59 and texture from 0x5a). <sub>54e8e0</sub> |
| 0x2b | `jmpYield` | rel | 0 | 0 | 2958 | high | - | Jumps to next+arg*4 and yields this frame (loop back-edge / wait one frame). <sub>553da0</sub> |
| 0x2c | `nop_0x2c` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
| 0x2d | `nop_0x2d` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
| 0x2e | `nop_0x2e` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
| 0x2f | `flagEC10000` | imm | 0 | 0 | 1888 | low | - | arg!=0 sets, 0 clears +0xEC bit 0x10000 (with 0x8000 both set the strat skips player-distance updates; property 0xf2 reads it); meaning unclear. <sub>54f720</sub> |
| 0x30 | `trackPlayer` | imm | 0 | 0 | | medium | - | If no player yet: re-executes itself next frame (pc-=4, yield); else arg sets/clears +0xEC bit 0x4000 = update distance to player each frame (+0x148). <sub>54fa80</sub> |
| 0x31 | `flagEC2` | imm | 0 | 0 | 410 | low | - | arg sets/clears +0xEC bit 2 (checked by collision/floor code via +0xC0, readable as property 0x1f). <sub>54e090</sub> |
| 0x32 | `flagE8_80000000` | imm | 0 | 0 | | low | - | arg sets/clears +0xE8 bit 31; unused. <sub>54dd90</sub> |
| 0x33 | `flagEC100` | imm | 0 | 0 | 60 | low | - | arg sets/clears +0xEC bit 0x100 (tested in the collision response, skips floor handling; used by drops/projectiles/rollercoaster). <sub>54dd30</sub> |
| 0x34 | `flagEC200000` | imm | 0 | 0 | 50 | low | - | arg sets/clears +0xEC bit 0x200000 (used by NPC helpers/canoe/llama). <sub>54dd60</sub> |
| 0x35 | `nop_0x35` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
| 0x36 | `nop_0x36` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
| 0x37 | `popPlayerProp` | playerProp | 1 | 0 | 1131 | high | - | Pops value and stores it into player property n (discarded if no player). <sub>553920</sub> |
| 0x38 | `popCameraProp` | prop | 1 | 0 | 564 | high | - | Pops value into camera strat property n (discarded if none). <sub>553950</sub> |
| 0x39 | `popTargetProp` | prop | 1 | 0 | 186 | medium | - | Pops value into camera target (DAT_006D9E30) property n (discarded if none). <sub>553980</sub> |
| 0x3a | `popTarget2Prop` | prop | 1 | 0 | | medium | - | Pops value into DAT_006D9E34 strat property n (discarded if none); unused. <sub>5539b0</sub> |
| 0x3b | `popDialogProp` | prop | 1 | 0 | | low | - | Pops value into DAT_006D9E2C strat property n (discarded if none); unused. <sub>5539e0</sub> |
| 0x3c | `error_0x3c` | - | 0 | 0 | | high | - | Prints "stCollide_AlienVar_Equals" not-implemented error; unused. <sub>54d240</sub> |
| 0x3d | `popBossProp` | prop | 1 | 0 | | low | - | Pops value into DAT_006D9E24 strat property n (no null check); unused. <sub>553a10</sub> |
| 0x3e | `popParentVar` | var | 1 | 0 | 204 | high | - | Pops value into variable n of the +0xC strat. <sub>553a40</sub> |
| 0x3f | `popParentProp` | prop | 1 | 0 | | high | - | Pops value into property n of the +0xC strat; unused. <sub>553a60</sub> |
| 0x40 | `popProp` | prop | 1 | 0 | 15730 | high | - | Pops value into own property n (via FUN_005509F0 address). <sub>553a90</sub> |
| 0x41 | `popVar` | var | 1 | 0 | 30379 | high | - | Pops value into own variable n. <sub>553ac0</sub> |
| 0x42 | `popCodeWord` | rel | 1 | 0 | 372 | high | - | Pops value into the code-stream word at pc+arg*4 (self-modifying inline data, pairs with 0x05). <sub>553ae0</sub> |
| 0x43 | `popGlobal` | global | 1 | 0 | 20214 | high | - | Pops value into global n. <sub>553b00</sub> |
| 0x44 | `pushShort` | imm | 0 | 1 | 82749 | high | stNumber | Pushes the sign-extended 16-bit arg (raw, not shifted to 20.12). <sub>553610</sub> |
| 0x45 | `push` | imm | 0 | 1 | 25870 | high | stNumber | Pushes inline 32-bit word (handler shared with 0xd7). <sub>5535f0</sub> |
| 0x46 | `neg` | - | 1 | 1 | 923 | high | stUMinus | Pops a, pushes -a. <sub>5508b0</sub> |
| 0x47 | `inc` | - | 1 | 0 | 1648 | high | stIncrease | Pops address, adds 1.0 (0x1000) to the word there. <sub>553890</sub> |
| 0x48 | `dec` | - | 1 | 0 | 1924 | high | stDecrease | Pops address, subtracts 1.0 (0x1000) from the word there. <sub>5538b0</sub> |
| 0x49 | `add` | - | 2 | 1 | 15901 | high | stAdd | Pops b, a; pushes a+b. <sub>5538d0</sub> |
| 0x4a | `sub` | - | 2 | 1 | 10042 | high | stSub | Pops b, a; pushes a-b. <sub>552d90</sub> |
| 0x4b | `mul` | - | 2 | 1 | 11877 | high | stMul | Pops b, a; pushes fixed-point a*b>>12. <sub>552dc0</sub> |
| 0x4c | `div` | - | 2 | 1 | 468 | high | stDiv | Pops b, a; pushes fixed-point (a<<8)/b<<4. <sub>552e00</sub> |
| 0x4d | `store` | - | 2 | 0 | | high | stEquals | Pops value, then address; stores value at address; unused (compiler uses pop ops). <sub>553900</sub> |
| 0x4e | `eq` | - | 2 | 1 | 10075 | high | stCompare | Pops b, a; pushes a==b (0/1, raw). <sub>553be0</sub> |
| 0x4f | `lt` | - | 2 | 1 | 5556 | high | stLessThan | Pops b, a; pushes a<b. <sub>553bb0</sub> |
| 0x50 | `shl` | - | 2 | 1 | 1162 | high | stShiftLeft | Pops b, a; pushes a << (b>>12) (b fixed shift count). <sub>54fb70</sub> |
| 0x51 | `shr` | - | 2 | 1 | 2496 | high | stShiftRight | Pops b, a; pushes a >> (b>>12) arithmetic (also stashes a in 0x6DA2D8). <sub>54fba0</sub> |
| 0x52 | `animToFrame` | - | 1 | 0 | 91 | medium | - | Pops frame (fixed); steps current anim frame +0x110 forward (wrapping) until it reaches frame (clamped to last frame of anim +0x14), applying each frame to the model. <sub>54fad0</sub> |
| 0x53 | `gt` | - | 2 | 1 | 3662 | high | stGreaterThan | Pops b, a; pushes a>b. <sub>553b20</sub> |
| 0x54 | `setModel` | - | 1 | 0 | 1439 | high | stSetModel | Pops model address; sets +0x10 and updates +0xEC bit 0x20 / +0xE8 bit 0x80 from model header fields. <sub>553c10</sub> |
| 0x55 | `scale` | - | 1 | 0 | 376 | high | stScale | Pops scale (fixed); sets x/y/z scale +0x60/+0x64/+0x68. <sub>54ffe0</sub> |
| 0x56 | `scaleX` | - | 1 | 0 | 102 | high | stScaleX | Pops scale (fixed) into +0x60. <sub>550000</sub> |
| 0x57 | `scaleY` | - | 1 | 0 | 242 | high | stScaleY | Pops scale (fixed) into +0x64. <sub>550020</sub> |
| 0x58 | `scaleZ` | - | 1 | 0 | 24 | high | stScalez | Pops scale (fixed) into +0x68. <sub>550040</sub> |
| 0x59 | `shadowSize` | - | 1 | 0 | 548 | high | stShadowSize | Pops shadow size (fixed) into +0x12C (shadow drawn only if >=0x80). <sub>54e840</sub> |
| 0x5a | `shadowTexture` | - | 1 | 0 | 548 | medium | stShadowType | Pops shadow texture index (TEXT texture) into +0x128 (short). <sub>54e860</sub> |
| 0x5b | `error_0x5b` | - | 0 | 0 | | high | stFlash | Prints "stFlash" not-implemented error; unused. <sub>54d260</sub> |
| 0x5c | `error_0x5c` | - | 0 | 0 | | high | stTrans | Prints "stTrans" not-implemented error; unused. <sub>54d280</sub> |
| 0x5d | `moveUp` | - | 1 | 0 | 50 | high | stMoveUp | Pops dist (fixed); moves strat by local (0,+d,0) via FUN_005501D0. <sub>550910</sub> |
| 0x5e | `moveDown` | - | 1 | 0 | 29 | high | stMoveDown | Pops dist; moves by local (0,-d,0). <sub>550960</sub> |
| 0x5f | `moveForward` | - | 1 | 0 | 95 | high | stMoveForward | Pops dist; moves by local (0,0,+d) along its facing. <sub>550590</sub> |
| 0x60 | `moveBackward` | - | 1 | 0 | 12 | high | stMoveBackward | Pops dist; moves by local (0,0,-d). <sub>5505c0</sub> |
| 0x61 | `moveLeft` | - | 1 | 0 | 20 | high | stMoveLeft | Pops dist; moves by local (+d,0,0). <sub>550720</sub> |
| 0x62 | `moveRight` | - | 1 | 0 | 4 | high | stMoveRight | Pops dist; moves by local (-d,0,0). <sub>550750</sub> |
| 0x63 | `turnRight` | - | 1 | 0 | 380 | high | stTurnRight | Pops angle; yaw +0x24 -= angle (24-bit wrap; 4096.0 = full turn). <sub>553c90</sub> |
| 0x64 | `turnLeft` | - | 1 | 0 | 775 | high | stTurnLeft | Pops angle; yaw +0x24 += angle. <sub>553cb0</sub> |
| 0x65 | `tiltLeft` | - | 1 | 0 | 4 | high | stTiltLeft | Pops angle; roll +0x28 -= angle. <sub>54e9e0</sub> |
| 0x66 | `tiltRight` | - | 1 | 0 | 6 | high | stTiltRight | Pops angle; roll +0x28 += angle. <sub>54ea00</sub> |
| 0x67 | `tiltForward` | - | 1 | 0 | 26 | high | stTiltForward | Pops angle; pitch +0x20 += angle. <sub>553cd0</sub> |
| 0x68 | `tiltBackward` | - | 1 | 0 | | high | stTiltBackward | Pops angle; pitch +0x20 -= angle; unused. <sub>553cf0</sub> |
| 0x69 | `turnToPlayerX` | - | 1 | 0 | | high | stTurnToPlayerX | Pops max step (angle); turns pitch +0x20 toward the player by at most step; unused. <sub>54ef50</sub> |
| 0x6a | `turnToPlayerY` | - | 1 | 0 | 345 | high | stTurnToPlayerY | Pops max step (angle); turns yaw +0x24 toward the player by at most step. <sub>54f0d0</sub> |
| 0x6b | `turnToPlayerXY` | - | 1 | 0 | 1 | high | stTurnToPlayerXY | Pops max step (angle); turns yaw and pitch toward the player. <sub>54eff0</sub> |
| 0x6c | `turnToX` | - | 4 | 0 | 2 | high | stTurnToX | Pops step, z, y, x; turns pitch toward point (x,y,z) by at most step. <sub>54eb10</sub> |
| 0x6d | `turnToY` | - | 4 | 0 | 209 | high | stTurnToY | Pops step, z, y, x; turns yaw toward point (x,y,z) by at most step. <sub>54ebc0</sub> |
| 0x6e | `turnToXY` | - | 4 | 0 | 28 | high | stTurnToXY | Pops step, z, y, x; turns yaw and pitch toward point (x,y,z). <sub>54ec60</sub> |
| 0x6f | `wobble` | - | 1 | 0 | | medium | stWobble | Pops and discards one value (unimplemented; shared pop handler); unused. <sub>554fb0</sub> |
| 0x70 | `resetPos` | - | 0 | 0 | 613 | high | stReSetPos | Restores 8 words (angles+position, +0x20..+0x3C) from the saved copy at +0xA0. <sub>54fa10</sub> |
| 0x71 | `savePos` | - | 0 | 0 | 566 | high | stSetPos | Saves 8 words (+0x20..+0x3C angles+position) to +0xA0 for 0x70. <sub>54f9f0</sub> |
| 0x72 | `nop_0x72` | - | 0 | 0 | | medium | stJump | No-op (shared empty handler); unused. <sub>426500</sub> |
| 0x73 | `wpFirst` | - | 0 | 0 | 833 | high | stWPFirst | Current waypoint +0x120 := first waypoint +0x118. <sub>54ea60</sub> |
| 0x74 | `wpLast` | - | 0 | 0 | 83 | high | stWPLast | Current waypoint +0x120 := last waypoint +0x11C. <sub>54f240</sub> |
| 0x75 | `wpNext` | - | 0 | 0 | 1129 | high | stWPNext | Current waypoint := its next link; wraps to first. <sub>54f260</sub> |
| 0x76 | `wpPrev` | - | 0 | 0 | 177 | high | stWPPrev | Current waypoint := its prev link; wraps to last. <sub>54f550</sub> |
| 0x77 | `error_0x77` | - | 0 | 0 | | high | stWPDel | Prints "stWpdel" not-implemented error; unused. <sub>54d2a0</sub> |
| 0x78 | `error_0x78` | - | 0 | 0 | | high | stWPNew | Prints "stWpnew" not-implemented error; unused. <sub>54d2c0</sub> |
| 0x79 | `wpNearest` | - | 0 | 0 | | high | stWPNearest | Sets current waypoint to the one nearest the strat position; unused. <sub>54f3f0</sub> |
| 0x7a | `wpFurthest` | - | 0 | 0 | | high | stWPFurthest | Sets current waypoint to the one furthest from the strat (excluding last); unused. <sub>54f500</sub> |
| 0x7b | `wpTurnToX` | - | 1 | 0 | 12 | high | stWPTurnToX | Pops max step (angle); turns pitch toward the current waypoint (pos at wp+0x18). <sub>54ee40</sub> |
| 0x7c | `wpTurnToY` | - | 1 | 0 | 230 | high | stWPTurnToY | Pops max step (angle); turns yaw toward the current waypoint (if any). <sub>54eed0</sub> |
| 0x7d | `wpTurnToXY` | - | 1 | 0 | 25 | high | stWPTurnToXY | Pops max step (angle); turns yaw and pitch toward the current waypoint. <sub>54ed60</sub> |
| 0x7e | `animPlay` | - | 1 | 0 | 3847 | high | stAnimPlay | Pops anim address; sets +0x14, frame +0x110 := 0, sets +0xE8 bit 8 (playing) and applies frame 0 to the model. <sub>550160</sub> |
| 0x7f | `animStop` | - | 0 | 0 | 458 | high | stAnimStop | Clears +0xE8 bit 8 (anim playing). <sub>54f7c0</sub> |
| 0x80 | `animClear` | - | 0 | 0 | | medium | stAnimClear | Clears the current anim pointer (+0x14) and flag 0x8 in +0xE8 (no anim playing). <sub>54f7d0</sub> |
| 0x81 | `animSetSpeed` | - | 0 | 0 | | high | stAnimSetSpeed | Unimplemented on PC: only logs "stAnimsetspeed" via the debug print; no stack effect. <sub>54d2e0</sub> |
| 0x82 | `collisionType` | - | 1 | 0 | 179 | high | stCollisionType | pops type (raw int); clears bits 0x42C of +0xEC then sets 1->0x8, 2->0x400, 3->0x20, 4->none, other->0x4 (collision shape/type flags). <sub>54fd60</sub> |
| 0x83 | `collRadius` | - | 1 | 0 | 821 | high | stCollRadius | pops radius; stores its low 16 bits raw into collision radius short +0xD2. <sub>54e910</sub> |
| 0x84 | `collHeight` | - | 0 | 0 | | high | stCollHeight | Unimplemented: logs "stCollheight" only. <sub>54d300</sub> |
| 0x85 | `collExtent` | - | 1 | 0 | | medium | stCollextent | pops extent; stores its low 16 bits raw into short +0xC4. <sub>54e930</sub> |
| 0x86 | `collView` | - | 0 | 0 | | high | stCollView | Unimplemented: logs "stCollview" only. <sub>54d320</sub> |
| 0x87 | `nop_0x87` | - | 0 | 0 | | low | stCollPoints | Shared no-op handler (0x426500); unused. Slot of Croc2 stCollPoints. <sub>426500</sub> |
| 0x88 | `nop_0x88` | - | 0 | 0 | | low | stCollSetPoint | Shared no-op handler (0x426500); unused. Slot of Croc2 stCollSetPoint. <sub>426500</sub> |
| 0x89 | `createTrigger` | - | 0 | 0 | 45 | medium | stCreateTrigger | Sets flag 0x40 in +0xE8 (strat becomes a trigger/touchable, used by pickups like Health, Carrot). <sub>54e4c0</sub> |
| 0x8a | `killTrigger` | - | 0 | 0 | | medium | stKillTrigger | Clears flags 0x40 and 0x4000000 in +0xE8 (undoes createTrigger). <sub>54e4b0</sub> |
| 0x8b | `wait` | - | 1 | 0 | 1671 | high | stWait | pops frames (fixed, >>12) into wait counter short +0xFE and sets flag 0x2 in +0xE8 (strat sleeps until the counter runs out). <sub>54fb40</sub> |
| 0x8c | `hold` | - | 0 | 0 | 110 | medium | stHold | Sets flags 0x12 (0x2 wait + 0x10 stopped) in +0xE8: suspends the strat indefinitely until released. <sub>54fce0</sub> |
| 0x8d | `release` | - | 0 | 0 | | high | stRelease | Unimplemented: logs "stRelease" only. <sub>54d340</sub> |
| 0x8e | `remove` | - | 0 | 0 | 985 | high | stRemove | Sets flags 0x6 in +0xE8 (yield + kill); same handler as 0xb4 end. <sub>5535e0</sub> |
| 0x8f | `nop_0x8f` | - | 0 | 0 | | low | stMapRemove | Shared no-op handler (0x426500); unused. Slot of Croc2 stMapRemove. <sub>426500</sub> |
| 0x90 | `mapAdd` | - | 0 | 0 | | high | stMapAdd | Unimplemented: logs "stMapadd" only. <sub>54d360</sub> |
| 0x91 | `mapReplace` | - | 0 | 0 | | high | stMapReplace | Unimplemented: logs "stMapreplace" only. <sub>54d380</sub> |
| 0x92 | `activated` | - | 0 | 0 | | medium | stActivated | Sets bit 0x8 in byte +0x44 of the strat's map-instance record (+0xF0): marks the map object as activated (persistent state). <sub>54db40</sub> |
| 0x93 | `collected` | - | 0 | 0 | | medium | stCollected | Sets bit 0x4 in byte +0x44 of the map-instance record (+0xF0): marks the map object as collected so it does not respawn. <sub>54db50</sub> |
| 0x94 | `spawnLegacy` | count,imm,imm,flags,imm | ? | 0 | | medium | stSpawn | pops code address of the new strat, then argCount (first inline word) argument values (popped inside FUN_0054bfc0); spawns a child strat at this strat's position (+0x20 block); inline words 3-5 are spawn parameters/flags (word 3 tested for 0x40000/0x80000/0x8000 collision-info allocation). Unused in shipped levels. <sub>5531d0</sub> |
| 0x95 | `spawnFrom` | count,imm,imm,flags,imm,imm | ? | 0 | | medium | stSpawnFrom | Like spawn (pops code address + args) but with an extra inline block giving a local offset (x/y/z) that is rotated by the strat's matrix (+0x70..) and added to its translation (+0x94..): spawn at an offset relative to this strat. Unused. <sub>5533f0</sub> |
| 0x96 | `link` | - | 0 | 0 | | high | stLink | Unimplemented: logs "stLink" only. <sub>54d3a0</sub> |
| 0x97 | `unlink` | - | 0 | 0 | | high | stUnlink | Unimplemented: logs "stUnlink" only. <sub>54d3c0</sub> |
| 0x98 | `soundShift` | - | 2 | 0 | 2 | medium | stSoundShift | pops pitch (fixed, >>12), then sound channel; changes pitch of a playing sound channel owned by this strat (FUN_00546400). <sub>54e0c0</sub> |
| 0x99 | `soundStop` | - | 1 | 0 | 209 | high | stSoundStop | pops sound channel; stops that channel if owned by this strat, or (channel <0/invalid) all sounds of this strat. <sub>54e1c0</sub> |
| 0x9a | `musicPlay` | - | 1 | 0 | 108 | high | stCdPlay | pops music track index (fixed, >>12); if different from the current track (0x586430) starts that ASF music stream (FUN_00546b70). <sub>54d4c0</sub> |
| 0x9b | `streamPlay` | - | 0 | 0 | | high | - | Unimplemented: logs "stStreamPlay" only. <sub>54d3e0</sub> |
| 0x9c | `speechSetPos` | - | 1 | 0 | 31 | low | - | pops a value (fixed, >>12) into global 0x6DA2B8 (meaning unknown, likely speech volume/range) and moves the speech/voice stream (handle 0x6D94B4, started by '#' text) to this strat's position. <sub>54d530</sub> |
| 0x9d | `discard_0x9d` | - | 1 | 0 | 28 | low | stMidiVolume | pops a value (>>12) and passes it to the no-op debug print; effectively drop. Croc2 slot midiVolume/cdFade. <sub>54d4f0</sub> |
| 0x9e | `speechStop` | - | 0 | 0 | 141 | high | stMidiStop | Stops the current speech/voice stream (handle 0x6D94B4) if one is playing and resets the handle to -1. <sub>54d510</sub> |
| 0x9f | `streamQueue` | - | 0 | 0 | | high | stMidiQueue | Unimplemented: logs "stStreamQueue" only. <sub>54d400</sub> |
| 0xa0 | `isLight` | - | 0 | 0 | | high | stIsLight | Unimplemented: logs "stIsLight" only. <sub>54d420</sub> |
| 0xa1 | `lightCol` | - | 0 | 0 | | high | stLightCol | Unimplemented: logs "stLightCol" only. <sub>54d440</sub> |
| 0xa2 | `lightFade` | - | 0 | 0 | | high | stLightFade | Unimplemented: logs "stLightFade" only. <sub>54d460</sub> |
| 0xa3 | `lightAtten` | - | 0 | 0 | | high | stLightAtten | Unimplemented: logs "stLightAtten" only. <sub>54d480</sub> |
| 0xa4 | `lightType` | - | 0 | 0 | | high | stLightType | Unimplemented: logs "stLightType" only. <sub>54d4a0</sub> |
| 0xa5 | `collisionOn` | flags | 0 | 0 | 1591 | high | stCollisionOn | ORs the inline flag word into +0xE8; if any of 0x34000 is set also sets 0x4000 in +0xEC. <sub>54fcf0</sub> |
| 0xa6 | `collisionOff` | flags | 0 | 0 | 1134 | high | stCollisionOff | Clears the inline flag word's bits in +0xE8. <sub>54fd40</sub> |
| 0xa7 | `collisionOffAll` | - | 0 | 0 | | high | stCollisionOffAll | Clears all collision flag bits 0x7E000 in +0xE8. <sub>54fd30</sub> |
| 0xa8 | `soundPlayEx` | - | 3 | 0 | 5 | high | stSoundPlay | pops pitch (fixed), volume (fixed), sound id; in game states 3/4 plays the sound on this strat and sets its volume and pitch (no channel returned). <sub>54e3a0</sub> |
| 0xa9 | `soundPlay4_0xa9` | - | 4 | 0 | | medium | stSoundPlay | pops 4 values and discards them (stubbed sound-play variant). <sub>54e400</sub> |
| 0xaa | `soundPlayAssEx` | - | 3 | 1 | | high | stSoundPlayAss | pops pitch, volume, sound id; plays it like 0xa8 and pushes the channel (raw int), or -1 if not in game state 3/4. <sub>54e320</sub> |
| 0xab | `soundPlayAss4_0xab` | - | 4 | 0 | | medium | stSoundPlayAss | pops 4 values and discards them (same stub as 0xa9; pushes nothing). <sub>54e400</sub> |
| 0xac | `int` | - | 1 | 1 | 1508 | high | stInt | pops x; pushes x & 0xFFFFF000 (fixed-point floor/truncate to integer). <sub>5501b0</sub> |
| 0xad | `sin` | - | 1 | 1 | 2994 | high | stSin | pops angle (fixed, 4096 table units = full turn); pushes sin from table 0x574318 (20.12). <sub>54e790</sub> |
| 0xae | `cos` | - | 1 | 1 | 3149 | high | stCos | pops angle; pushes cos (table 0x574318 offset by quarter turn 0x400). <sub>54e810</sub> |
| 0xaf | `not` | - | 1 | 1 | 4952 | high | stNot | pops x; pushes 1 if x==0 else 0. <sub>54fc20</sub> |
| 0xb0 | `pop` | - | 1 | 0 | | high | stPop | Drops the top stack value (stack pointer +0x104 -= 4). <sub>553dd0</sub> |
| 0xb1 | `stkCmp` | imm | 0 | 1 | | low | stStkCmp | Peeks (does not pop) the top stack entry, dereferences it as a pointer and pushes imm minus the pointed-to int (compare helper). <sub>553de0</sub> |
| 0xb2 | `address` | addr | 0 | 1 | 7890 | high | stAddress | Patches itself into 0x45 (push immediate) after resolving the operand to an absolute address (negative = import table 0x6DA324, else + STPC base 0x6D9DBC) and pushes it (code/model/anim address). <sub>553630</sub> |
| 0xb3 | `return` | - | 1 | 0 | 4974 | high | stReturn | pops code address and jumps there (pc = popped). <sub>5537d0</sub> |
| 0xb4 | `end` | - | 0 | 0 | 2006 | high | stEndStrat | Sets flags 0x6 in +0xE8 (yield + kill): ends the strat. Same handler as 0x8e. <sub>5535e0</sub> |
| 0xb5 | `isPlayer` | - | 0 | 0 | 62 | high | stIsPlayer | Registers this strat as the player (global 0x6D9E1C = strat) and sets flag 0x800 in +0xE8. <sub>550140</sub> |
| 0xb6 | `and` | - | 2 | 1 | 3004 | high | stAnd | pops b, a; pushes 1 if both are non-zero else 0 (logical and). <sub>552e60</sub> |
| 0xb7 | `or` | - | 2 | 1 | 5411 | high | stOr | pops b, a; pushes a \| b (bitwise, used as logical or). <sub>552ea0</sub> |
| 0xb8 | `bitwiseAnd` | - | 2 | 1 | 6814 | high | stBitwiseAnd | pops b, a; pushes a & b. <sub>552e30</sub> |
| 0xb9 | `discard_0xb9` | - | 1 | 0 | | medium | - | Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 ext-var slots). <sub>554fb0</sub> |
| 0xba | `discard_0xba` | - | 1 | 0 | | medium | - | Shared stub 0x554fb0: pops one value and discards it. Unused. <sub>554fb0</sub> |
| 0xbb | `nop_0xbb` | - | 0 | 0 | | low | - | Shared no-op handler (0x426500); unused. <sub>426500</sub> |
| 0xbc | `notEqual` | - | 2 | 1 | 2576 | high | stNotEqual | pops b, a; pushes 1 if a != b else 0. <sub>54fbd0</sub> |
| 0xbd | `greaterEqual` | - | 2 | 1 | 1169 | high | stGreaterEqual | pops b, a; pushes 1 if a >= b else 0. <sub>553b80</sub> |
| 0xbe | `lessEqual` | - | 2 | 1 | 2129 | high | stLessEqual | pops b, a; pushes 1 if a <= b else 0. <sub>553b50</sub> |
| 0xbf | `rnd` | - | 1 | 1 | 4990 | high | stRnd | pops n; pushes random value (two rand() halves) modulo n, 0 if n==0; result is in the same units as n (fixed in, fixed out). <sub>54f960</sub> |
| 0xc0 | `loseHeart` | - | 0 | 0 | 219 | high | stLoseHeart | In game state 3, unless player invulnerable (0x584710 bit 0): health 0x584E70 -= 1.0, or if already 0 sets 0x584638 = 2 (player dies); refreshes HUD (FUN_004268c0(1)). <sub>54f5c0</sub> |
| 0xc1 | `nop_0xc1` | - | 0 | 0 | | low | stResettoCheckPoint | Shared no-op handler (0x426500); unused. Croc2 slot stResettoCheckPoint. <sub>426500</sub> |
| 0xc2 | `forceCollision` | - | 0 | 0 | 1619 | medium | stForceCollision | Runs world collision for this strat (FUN_00403020 with level geometry): updates floor/ceiling/wall info in the collision block +0xC0 (ground height, surface type). <sub>54e9c0</sub> |
| 0xc3 | `turnFromPlayerY` | - | 1 | 0 | | medium | stTurnFromPlayerY | pops max turn step (angle, 0x1000000 = full turn); turns yaw +0x24 toward the direction from the player (0x6D9E1C) to this strat, i.e. away from the player, by at most step. <sub>54f1c0</sub> |
| 0xc4 | `rumble` | - | 2 | 0 | 93 | low | stRumble | Shared stub 0x555090: pops two values and discards them (PSX pad rumble/vibrate not implemented on PC). <sub>555090</sub> |
| 0xc5 | `suspendIfTooFar` | - | 1 | 0 | 232 | low | stSuspendIfTooFar | Shared stub 0x554fb0: pops one value (likely distance) and discards it. <sub>554fb0</sub> |
| 0xc6 | `collisionBone` | - | 2 | 0 | 90 | medium | stCollisionBone | pops value (short), then bone id (fixed, >>12); sets/adds entry {id, value} in the bone table at +0xC8 (count +0xC6). <sub>54e6c0</sub> |
| 0xc7 | `useBone` | - | 1 | 0 | 345 | low | stUseBone | pops one value (ignored); if a model/anim is set (+0x14) rebuilds the rotation matrix and stores a transformed point (matrix * local vector + translation +0x94) into +0xD8/+0xDC/+0xE0. <sub>54e4d0</sub> |
| 0xc8 | `isCamera` | - | 0 | 0 | 34 | high | stIsCamera | Registers this strat as the camera (global 0x6D9E28) and sets flag 0x8000000 in +0xE8. <sub>5500f0</sub> |
| 0xc9 | `lookAtMe` | - | 0 | 0 | 86 | high | stLookatme | Sets camera look-at target global 0x6D9E30 = this strat. <sub>5500e0</sub> |
| 0xca | `lookAtMe2` | - | 0 | 0 | | medium | stLookatme | Sets secondary look-at global 0x6D9E34 = this strat. Unused. <sub>5500d0</sub> |
| 0xcb | `pushCamera` | - | 0 | 0 | | high | stPushCamera | Pushes current camera (0x6D9E28) and look-at (0x6D9E30) pair onto the camera stack 0x6D9DCC (index 0x6D9E40). <sub>54dbb0</sub> |
| 0xcc | `popCamera` | - | 0 | 0 | | high | stPopCamera | If the camera stack is not empty, restores camera and look-at from it and increments camera-change counter 0x6D9D34. <sub>54db00</sub> |
| 0xcd | `nop_0xcd` | - | 0 | 0 | | low | stResetCamerapos | Shared no-op handler (0x426500); unused. Croc2 slot stResetCamerapos. <sub>426500</sub> |
| 0xce | `gainHeart` | - | 0 | 0 | 94 | high | stGainHeart | Health 0x584E70 += 1.0 if below max 0x584E6C; refreshes HUD (FUN_004268c0(1)). <sub>54f650</sub> |
| 0xcf | `nop_0xcf` | - | 0 | 0 | | low | stGainHeartPot | Shared no-op handler (0x426500); unused. Croc2 slot stGainHeartPot. <sub>426500</sub> |
| 0xd0 | `addInv` | imm | 0 | 0 | | medium | stAddInv | Adds inline item to inventory via FUN_00426860(item,1,1,0,0), which is a stub returning 0 on PC (no effect). <sub>54da00</sub> |
| 0xd1 | `nop_0xd1` | - | 0 | 0 | | low | stGainCrystal | Shared no-op handler (0x426500); unused. Croc2 slot stGainCrystal. <sub>426500</sub> |
| 0xd2 | `discard_0xd2` | - | 1 | 0 | | low | stCutscene | Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 slot stCutscene). <sub>554fb0</sub> |
| 0xd3 | `inventory` | imm | 0 | 1 | | medium | stInventory | Queries inline item via stub FUN_00426860 and pushes result<<12 (always 0 on PC). <sub>54d9d0</sub> |
| 0xd4 | `debugName` | imm,count | 0 | 0 | 1804 | high | stDebugname | Strat name: skips the first inline word, reads the byte length of the following name string, sets the name pointer (+0x18) to that string and jumps past it. <sub>553ef0</sub> |
| 0xd5 | `soundPlay` | - | 1 | 0 | 1835 | high | stSoundPlay | pops sound id; plays it on this strat (FUN_00546170); outside game states 3/4 first sets game state 0x5846E4 = 5. <sub>54e230</sub> |
| 0xd6 | `soundPlayAss` | - | 1 | 1 | 288 | high | stSoundPlayass | pops sound id; in game states 3/4 plays it and pushes the channel (raw int), else pushes -1. <sub>54e1e0</sub> |
| 0xd7 | `soundAddress` | sound | 0 | 1 | 2996 | high | stSoundAddress | Pushes the inline sound id (same handler as 0x45 push immediate). <sub>5535f0</sub> |
| 0xd8 | `nop_0xd8` | - | 0 | 0 | | low | stOnground | Shared no-op handler (0x426500); unused. Croc2 slot stOnground/stObjectFallslow. <sub>426500</sub> |
| 0xd9 | `collisionOffset` | - | 3 | 0 | 403 | high | stCollisionOffset | pops z, y, x; stores low 16 bits into shorts +0xD0, +0xCE, +0xCC (collision centre offset). <sub>54e050</sub> |
| 0xda | `abs` | - | 1 | 1 | 1847 | high | stAbs | pops x; pushes \|x\|. <sub>5508d0</sub> |
| 0xdb | `pickup` | imm | 0 | 0 | | medium | stPickup | inline flag: nonzero sets bit 0x200 in +0xEC, zero clears it (strat can be picked up/carried). <sub>54dd00</sub> |
| 0xdc | `min` | - | 2 | 1 | 1546 | high | stMin | pops b, a; pushes the smaller (signed). <sub>54dc80</sub> |
| 0xdd | `max` | - | 2 | 1 | 768 | high | stMax | pops b, a; pushes the larger (signed). <sub>54dc40</sub> |
| 0xde | `spawnParticle` | - | 6 | 0 | 1935 | medium | stSpawnparticle | pops param (texture or colour, type-dependent), vz, vy, vx (velocity, fixed), scale (fixed, 0x1000 = default), particle type (raw int); emits a particle at this strat's position (FUN_00428ea0). <sub>552ed0</sub> |
| 0xdf | `sgn` | - | 1 | 1 | | high | stSgn | pops x; pushes 1.0 (0x1000), -1.0 or 0 by sign. <sub>54e7c0</sub> |
| 0xe0 | `spawn` | count,count,imm,flags,imm | ? | 0 | 1788 | medium | stSpawn | Spawns a child strat via FUN_0054bfc0: pops code address of the new strat (top), then pops operand1 (param count) values from the parent stack as the child's MAP parameters; operand2 = local variable count of the child, operand4 = spawn flags (ORed with own +0x138; bits 0x40000/0x80000/0x8000 select model/collision setup), operands 3/5 extra sizes/flags; child copies parent orientation/position block (+0x20) and waypoint fields; fails silently (params still popped) if strat pool full <sub>553230</sub> |
| 0xe1 | `nop_0xe1` | - | 0 | 0 | | high | stDontLookatme? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0xe2 | `nop_0xe2` | - | 0 | 0 | | high | stRunat? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0xe3 | `moveForwardQ` | - | 1 | 0 | 854 | high | stMoveForwardq | pops distance (fixed); x(+0x30) += d*sin(yaw), z(+0x38) += d*cos(yaw), yaw = +0x24 (4096 units per turn, table 0x574318) <sub>550690</sub> |
| 0xe4 | `moveBackwardQ` | - | 1 | 0 | 436 | high | stMoveBackwardq | pops distance (fixed); moves the strat backwards along its yaw (x -= d*sin, z -= d*cos) <sub>550600</sub> |
| 0xe5 | `playSoundVol` | - | 2 | 0 | 1631 | high | stSoundPlay | pops volume (fixed, >>12), sound id; if game state DAT_005846e4 is 3 or 4 plays the sound for this strat (FUN_00546170) and sets the channel volume (+0x6a, FUN_00546360) <sub>54e2d0</sub> |
| 0xe6 | `playSoundChannel` | - | 2 | 1 | 47 | high | stSoundPlayass | pops volume (fixed), sound id; if game state DAT_005846e4==3 plays the sound with that volume and pushes the channel index (raw int, not fixed), else pushes -1 <sub>54e270</sub> |
| 0xe7 | `setWP` | - | 0 | 0 | 213 | high | stSetWP | saves the current waypoint: +0x124 = +0x120 <sub>54ea20</sub> |
| 0xe8 | `resetWP` | - | 0 | 0 | 193 | high | stResetWP | restores the saved waypoint: +0x120 = +0x124 <sub>54ea40</sub> |
| 0xe9 | `soundVolume` | - | 2 | 0 | 7 | high | stSoundVolume | pops volume (fixed), sound channel; sets the channel volume (+0x6a) if the channel belongs to this strat (FUN_00546360) <sub>54e190</sub> |
| 0xea | `nop_0xea` | - | 0 | 0 | | high | stString? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0xeb | `setBossHearts` | - | 1 | 0 | | high | stSetbossHearts | pops count (fixed); sets boss heart max DAT_00584f0c and current DAT_00584f10 to count <sub>54f580</sub> |
| 0xec | `loseBossHeart` | - | 0 | 0 | | high | stLosebossHeart | decrements boss hearts DAT_00584f10 (not below 0) and refreshes the HUD (FUN_004268c0(1)) <sub>54f5a0</sub> |
| 0xed | `soundShiftRelative` | - | 2 | 0 | 57 | medium | stSoundShiftrelative | pops shift/pitch (fixed, >>12), sound channel; sets channel +0x70 and updates a playing voice (FUN_00546460) if the channel belongs to this strat <sub>54e0f0</sub> |
| 0xee | `smin` | - | 2 | 1 | 42 | high | stSmin | pops limit (top), value; pushes value with its magnitude clamped to limit, keeping the sign (sign(v)*min(\|v\|,limit)) <sub>54dcc0</sub> |
| 0xef | `isBoss` | - | 0 | 0 | | high | stIsboss | registers this strat as the boss: DAT_006d9e24 = self <sub>550130</sub> |
| 0xf0 | `topSay` | text | 0 | 0 | 1308 | high | stTopsay | shows dialog text (inline text index) in the top text box: sets DAT_006d9858=1 and formats via FUN_0040d4c0 <sub>40d480</sub> |
| 0xf1 | `getParentPos` | - | 0 | 0 | 48 | high | stGetparentpos | copies parent's (+0xC) orientation+position block (+0x20..+0x3C, 8 words) into this strat <sub>54e030</sub> |
| 0xf2 | `setDrawMode` | - | 1 | 0 | | low | - | pops mode (raw int): 1 sets flag 0x80000 in +0xEC, 2 sets 0x100000, other clears both (exact effect of the bits unknown); unused <sub>54fe40</sub> |
| 0xf3 | `afterBoss` | - | 0 | 0 | | high | stAfterboss | if no boss (DAT_006d9e24) rewinds pc and yields (waits); else relinks this strat in the process list right after the boss <sub>553290</sub> |
| 0xf4 | `afterPlayer` | - | 0 | 0 | 144 | high | stAfterPlayer | if no player (DAT_006d9e1c) rewinds pc and yields (waits); else relinks this strat in the process list right after the player <sub>553380</sub> |
| 0xf5 | `beforePlayer` | - | 0 | 0 | 22 | high | stBeforePlayer | if no player rewinds pc and yields (waits); else relinks this strat in the process list right before the player <sub>553300</sub> |
| 0xf6 | `nop_0xf6` | - | 0 | 0 | | high | stBeforeboss?/stNohang? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0xf7 | `pushZero` | - | 0 | 1 | | high | stZero | pushes 0 (same handler as 0x1a7); unused <sub>54e880</sub> |
| 0xf8 | `nop_0xf8` | - | 0 | 0 | | high | stTophead? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0xf9 | `popDiscard_0xf9` | - | 1 | 0 | | high | stTopdialog? | pops and discards one value (stub FUN_00554fb0, shared with 0x6f etc.); unused <sub>554fb0</sub> |
| 0xfa | `bottomSay` | text | 0 | 0 | 891 | high | stBottomsay | shows dialog text (inline text index) in the bottom text box: sets DAT_006d94e8=2 and formats via FUN_0040d4c0 <sub>40d8d0</sub> |
| 0xfb | `nop_0xfb` | - | 0 | 0 | | high | stBottomhead? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0xfc | `popDiscard_0xfc` | - | 1 | 0 | | high | stBottomdialog? | pops and discards one value (stub FUN_00554fb0); unused <sub>554fb0</sub> |
| 0xfd | `getPlayerPos` | - | 0 | 0 | 193 | high | stGetPlayerpos | if a player exists copies its orientation+position block (+0x20..+0x3C) into this strat <sub>54dde0</sub> |
| 0xfe | `getWPPos` | - | 0 | 0 | 406 | high | stGetWPpos | sets rotation x/y/z (+0x20..+0x28) from the current waypoint's (+0x120) ints <<12 and position (+0x30..+0x3C) from waypoint +0x18..+0x24 <sub>54dfe0</sub> |
| 0xff | `getBossPos` | - | 0 | 0 | | high | stGetbosspos | copies the boss's (DAT_006d9e24) orientation+position block (+0x20..+0x3C) into this strat (no null check); unused <sub>54ddc0</sub> |
| 0x100 | `getDoorPos` | - | 0 | 0 | 31 | medium | stGetdoorpos | finds the level door record (DAT_00584648+0x24) in this strat's x/z cell nearest in y; if found sets current door DAT_00584644, places the strat at the door position/yaw (FUN_0054de00) and, unless door flag 2, moves it forward by the door's distance (+0x38) <sub>54df30</sub> |
| 0x101 | `fadeOut` | - | 1 | 0 | 181 | medium | stFadeout | pops speed selector (fixed, 1/2/3/other) and starts a screen fade-out (FUN_00415390 mode 1/5/0xb/3); sets DAT_00584638=6 unless it is 7 or 10 <sub>54d910</sub> |
| 0x102 | `fadeIn` | - | 1 | 0 | 150 | medium | stFadein | pops speed selector (fixed, 1/3/other) and starts a screen fade-in (FUN_00415390 mode 2/10/4); DAT_00584638=0 <sub>54d8a0</sub> |
| 0x103 | `moveUpQ` | - | 1 | 0 | 1018 | high | stMoveUpq | pops distance (fixed); y(+0x34) += d <sub>5508f0</sub> |
| 0x104 | `moveDownQ` | - | 1 | 0 | 314 | high | stMoveDownq | pops distance (fixed); y(+0x34) -= d <sub>550940</sub> |
| 0x105 | `forcePlayerDist` | - | 0 | 0 | | high | stForcePlayerdist | recomputes the cached squared player distance +0x148 (coords >>6) and clears +0x1C; 0x200000 if no player; unused <sub>54e950</sub> |
| 0x106 | `nop_0x106` | - | 0 | 0 | | high | stShadeType? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x107 | `setAnimSpeed` | - | 1 | 0 | 645 | low | stSetanimSpeed? | pops value and stores its low 16 bits (raw, not >>12) in halfword +0x114 (animation timing field, zeroed when the anim frame is set) <sub>54d880</sub> |
| 0x108 | `nop_0x108` | - | 0 | 0 | | high | stCheckleveldoor? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x109 | `nop_0x109` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x10a | `nop_0x10a` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x10b | `nop_0x10b` | - | 0 | 0 | | high | stGainjigsaw? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x10c | `nop_0x10c` | - | 0 | 0 | | high | stGaingoldengobbo? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x10d | `gainSecret` | - | 0 | 0 | 26 | high | stGainCrystal? | increments the collected-secrets counter DAT_00584e64 (copied to the HUD/stats at level reset); used only by strat Secret <sub>54f710</sub> |
| 0x10e | `nop_0x10e` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x10f | `nop_0x10f` | - | 0 | 0 | 1 | high | stCheckPoint? | no-op handler FUN_00426500 (does nothing, no inline operands); used once (LlamaChase) <sub>426500</sub> |
| 0x110 | `setMainCameraFlag` | - | 0 | 0 | 23 | medium | stIsmainCamera? | sets flag 0x8000000 in own flags +0xE8 (same flag the camera-register opcode FUN_005500f0 sets, without registering DAT_006d9e28); used by CameraCollisionInFront <sub>550120</sub> |
| 0x111 | `nop_0x111` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x112 | `nop_0x112` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x113 | `isDialog` | - | 0 | 0 | 157 | high | stIsdialog | registers this strat as the dialog partner: DAT_006d9e2c = self <sub>550110</sub> |
| 0x114 | `distance` | - | 3 | 1 | 20 | high | stDistance | pops z (top), y, x; pushes the 3D distance from the strat's position (+0x30) to (x,y,z) (FUN_00404bb0, float sqrt -> int) <sub>54f840</sub> |
| 0x115 | `showLevelStartInfo` | imm | 0 | 0 | 60 | medium | - | inline word: low byte stored in DAT_006da294 (level start info / title card index), logs 'show level start info %d' <sub>54e8b0</sub> |
| 0x116 | `topCloseDialog` | - | 0 | 0 | 32 | high | stTopclosedialog | closes the top dialog text box: DAT_006d9858 = 0 <sub>40d900</sub> |
| 0x117 | `bottomCloseDialog` | - | 0 | 0 | 4 | high | stBottomclosedialog | closes the bottom dialog text box: DAT_006d94e8 = 0 <sub>40d910</sub> |
| 0x118 | `nop_0x118` | - | 0 | 0 | | high | stNextinventory? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x119 | `nop_0x119` | - | 0 | 0 | | high | stPrevinventory? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x11a | `otherPiece` | - | 0 | 0 | 266 | medium | stOtherpiece | finds the switchable map piece at the strat's grid cell (FUN_00553f10, table DAT_00584648+0x50) and sets it to its alternate state (*piece[5] = piece[4]), swapping its texture/collision table entries if DAT_006da330 bit 0x10000000 <sub>553f70</sub> |
| 0x11b | `normalPiece` | - | 0 | 0 | 264 | medium | stNormalpiece | same as otherPiece but restores the piece's normal state (*piece[5] = piece[3]) <sub>554090</sub> |
| 0x11c | `delInv` | imm | 0 | 0 | | medium | stDelinv | inline word (inventory item id) passed to FUN_00426860(item,1,-1,0,0), which is a stub returning 0 in this build: effectively no-op; unused <sub>54da20</sub> |
| 0x11d | `nop_0x11d` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x11e | `nop_0x11e` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x11f | `nop_0x11f` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x120 | `nop_0x120` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x121 | `nop_0x121` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x122 | `nop_0x122` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x123 | `nop_0x123` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x124 | `nop_0x124` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x125 | `moveLeftQ` | - | 1 | 0 | 131 | high | stMoveLeftq | pops distance (fixed); strafes: x += d*cos(yaw), z += d*sin(yaw-half turn) (= -d*sin) <sub>550790</sub> |
| 0x126 | `moveRightQ` | - | 1 | 0 | 299 | high | stMoveRightq | pops distance (fixed); strafes opposite to moveLeft (x -= d*cos(yaw), z += d*sin(yaw)) <sub>550820</sub> |
| 0x127 | `bitwiseNot` | - | 1 | 1 | 1 | high | stBitwisenot | pops v; pushes ~v <sub>54fc00</sub> |
| 0x128 | `bordersOn` | - | 0 | 0 | 157 | high | stBorderson | enables cinematic letterbox borders: DAT_005fd090 = 1 <sub>54dac0</sub> |
| 0x129 | `bordersOff` | - | 0 | 0 | 176 | high | stBordersoff | disables letterbox borders: DAT_005fd090 = 0 <sub>54dad0</sub> |
| 0x12a | `soundAdsr` | - | 5 | 0 | | high | stSoundadsr | pops and discards 5 values (stubbed); unused <sub>54e120</sub> |
| 0x12b | `soundAdsrRelative` | - | 3 | 0 | 74 | medium | stSoundadsrrelative | pops a (top), b (fixed), sound channel; if the channel belongs to this strat sets channel +0x34 = b>>12, +0x38 = a>>12 (FUN_005463b0, envelope/range params) <sub>54e150</sub> |
| 0x12c | `nop_0x12c` | - | 0 | 0 | | high | stRotatepiece? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x12d | `setAmbient` | - | 3 | 0 | 273 | high | stSet_ambient | pops b, g, r (fixed, >>12, top = b) into bytes +0x135/+0x134/+0x133 and enables the own ambient light colour (+0x136 = 1) <sub>54fea0</sub> |
| 0x12e | `resetAmbient` | - | 0 | 0 | | high | stReset_ambient | disables the own ambient colour: +0x136 = 0; unused <sub>54fee0</sub> |
| 0x12f | `invActive` | - | 0 | 0 | | high | stInvactive | calls inventory stub FUN_00426860(0,1,0,1,0) (returns 0, no effect in this build); unused <sub>54da60</sub> |
| 0x130 | `invInactive` | - | 0 | 0 | | high | stInvinactive | calls inventory stub FUN_00426860(0,1,0,1,1) (no effect); unused <sub>54da40</sub> |
| 0x131 | `sampleStatus` | - | 1 | 1 | | high | stSamplestatus | pops sound id; pushes (fixed) the channel index on which this strat is playing that sound, or -1.0 if not playing (FUN_005461e0); unused <sub>54e480</sub> |
| 0x132 | `nop_0x132` | - | 0 | 0 | | high | stResettoCheckPointnlh? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x133 | `resetDoor` | - | 0 | 0 | 31 | high | stResetdoor | restores the stored door (DAT_006da28c -> DAT_00584644) and, if a player exists, places the player at that door (FUN_0054de00) and restores its saved orientation/position block from DAT_006d9e48 <sub>54def0</sub> |
| 0x134 | `storeDoor` | - | 0 | 0 | 62 | high | stStoredoor | stores the current door (DAT_00584644 -> DAT_006da28c) and the player's orientation/position block (+0x20..+0x3C) into DAT_006d9e48 <sub>54dec0</sub> |
| 0x135 | `cameraModified` | - | 0 | 0 | | medium | stCamera_modified | if a camera strat (DAT_006d9e28) exists copies this strat's orientation/position block into it and its position into globals DAT_006d9d20..2c; unused <sub>54d830</sub> |
| 0x136 | `pushPlayer` | - | 0 | 0 | | high | stPushPlayer | saves the current player pointer DAT_006d9e1c into DAT_006d9e20 (logs an error if no player); unused <sub>54db60</sub> |
| 0x137 | `popPlayer` | - | 0 | 0 | | high | stPopPlayer | restores the player pointer (DAT_006d9e1c and DAT_006d9e30) from DAT_006d9e20 and clears it; unused <sub>54db90</sub> |
| 0x138 | `resetToStart` | - | 0 | 0 | | high | stReSetPostrn | restores position +0x30..+0x38 from the saved (start) position +0xB0..+0xB8; unused <sub>54fa30</sub> |
| 0x139 | `gainItem` | - | 0 | 0 | | high | stGainitem | increments item counter DAT_005fd09c up to the maximum DAT_005fd0a4; unused <sub>54f680</sub> |
| 0x13a | `setItem` | - | 1 | 0 | | high | stSetitem | pops maximum (fixed); DAT_005fd0a4 = max, counter DAT_005fd09c = 0, enables item counter display (DAT_005fd0a0 = 1); unused <sub>54f6a0</sub> |
| 0x13b | `setTimer` | - | 1 | 0 | | high | stSettimer | pops time (fixed, >>12) into DAT_005fd0ac and enables the timer (DAT_005fd0a8 = 1); unused <sub>54f6d0</sub> |
| 0x13c | `timerOff` | - | 0 | 0 | | high | stTimeroff | disables the timer: DAT_005fd0a8 = 0; unused <sub>54f6f0</sub> |
| 0x13d | `distanceNoY` | - | 2 | 1 | 170 | high | stDistancenoy | pops z (top), x; pushes the horizontal (x/z) distance from the strat's position to (x,z) (FUN_00404c50) <sub>54f890</sub> |
| 0x13e | `nop_0x13e` | - | 0 | 0 | | high | stSwim? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x13f | `nop_0x13f` | - | 0 | 0 | | high | stLoseCrystal? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
| 0x140 | `nop_0x140` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x141 | `setDebugFlag5849a0On` | - | 0 | 0 | | medium | - | Writes short 1 to global 0x5849A0; that global is never read anywhere in groove.exe (dead PSX leftover). <sub>54dab0</sub> |
| 0x142 | `setDebugFlag5849a0Off` | - | 0 | 0 | | medium | - | Writes short 0xFFFF to global 0x5849A0 (never read; dead leftover). <sub>54daa0</sub> |
| 0x143 | `clearFlag5fd0b8` | - | 0 | 0 | | medium | - | Sets global 0x5FD0B8 = 0; global is never read elsewhere (dead leftover). <sub>54da80</sub> |
| 0x144 | `setFlag5fd0b8` | - | 0 | 0 | | medium | - | Sets global 0x5FD0B8 = 1; global is never read elsewhere (dead leftover). <sub>54da90</sub> |
| 0x145 | `nop_0x145` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x146 | `nop_0x146` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x147 | `nop_0x147` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x148 | `gotoLevel4_1` | - | 0 | 0 | | medium | - | Hardcoded level change: next level (0x584EB4/EB8/EBC/EC0) = 4/1/1/0, previous level (0x585008..14) = 4/1/4/0, game state 0x584638 = 7 (load level). <sub>54e430</sub> |
| 0x149 | `nop_0x149` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x14a | `hudReset2` | - | 0 | 0 | | low | - | Calls HUD/panel init FUN_004268c0(2): re-initialises the on-screen counter panels (coins, lives, etc.) for the current level. <sub>54f700</sub> |
| 0x14b | `setGlobal5fd086` | imm | 0 | 0 | | medium | - | Stores the low 16 bits of the inline word in global 0x5FD086 (never read elsewhere; dead leftover). <sub>54e890</sub> |
| 0x14c | `removeCollPoint` | - | 1 | 0 | 49 | medium | stSetCollPoint (inverse) | pops id (fixed, >>12); removes the collision point with that id from the strat's collision point list (+0xC8 array of 4-byte entries, count +0xC6). Counterpart of 0xc6 (add/set collision point). <sub>54e720</sub> |
| 0x14d | `setFlagEC_400000` | flags | 0 | 0 | | high | - | Inline word: nonzero sets, zero clears bit 0x400000 of strat flags2 (+0xEC); the bit selects which height constant own property 0xF2 returns. <sub>54f780</sub> |
| 0x14e | `atan2` | - | 2 | 1 | 457 | high | - | pops b (top), a; pushes angle atan2(a, b) as (0..0xFFF)<<12, i.e. full turn = 0x1000000 (4096.0); used with a=dx, b=dz like yaw-to-target. <sub>54f7f0</sub> |
| 0x14f | `nop_0x14f` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x150 | `nop_0x150` | - | 0 | 0 | 1 | high | - | Empty handler FUN_00426500 (shared no-op); does nothing (used once in HelpGirlSkip). <sub>426500</sub> |
| 0x151 | `nop_0x151` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x152 | `nop_0x152` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x153 | `findNearestLight` | - | 3 | 1 | 1 | medium | - | pops z (top), y, x; searches the level light table (0x586598, 0x1C-byte entries, count 0x5865D0) for the nearest light of type 2 (point light) to (x,y,z) and pushes its pointer (0 if none). <sub>554280</sub> |
| 0x154 | `setLightIntensity` | - | 2 | 0 | 3 | medium | - | pops intensity (fixed; top), light pointer (from 0x153); stores intensity in light+0x18, rebuilds the light (FUN_0042c4a0) and re-flags/relights the map polygons within its radius (0 = off, 0x1000 = full). <sub>554310</sub> |
| 0x155 | `nop_0x155` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x156 | `nop_0x156` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x157 | `nop_0x157` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x158 | `nop_0x158` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x159 | `nop_0x159` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x15a | `setPosToPlayer` | - | 0 | 0 | 27 | high | - | Copies the player's position x/y/z (+0x30..+0x38) and +0x3C into the strat (player strat = 0x6D9E1C). <sub>554480</sub> |
| 0x15b | `setPosToPlayerXZ` | - | 0 | 0 | | high | - | Copies the player's x and z (+0x30, +0x38) into the strat; y unchanged. <sub>5544b0</sub> |
| 0x15c | `setShadowHeight` | - | 1 | 0 | 1 | low | - | pops v; stores it in strat +0x14C, used by the shadow renderer as the shadow/ground height override (-0x1000 = use default 0x586424). <sub>5544d0</sub> |
| 0x15d | `packRgb` | - | 3 | 1 | 98 | high | - | pops c (top), b, a (fixed 0..255); pushes (c<<16)\|(b<<8)\|a as a raw packed colour int (push order r,g,b gives 0x00BBGGRR). <sub>5544f0</sub> |
| 0x15e | `trailCreate` | - | 2 | 1 | 6 | medium | - | pops vertB (top), vertA (fixed vertex indices of the strat's model/anim frame); allocates a free trail/ribbon slot (0x5FD180, 0x42C bytes each) owned by this strat and pushes its handle (0 if none free). <sub>554530</sub> |
| 0x15f | `trailAddPoint` | - | 3 | 0 | 6 | medium | - | pops colourB (top), colourA, trail handle; transforms the two trail vertices of the current anim frame into world space and appends them to the trail history (20 points) with the given colours; no-op if handle 0. <sub>5545c0</sub> |
| 0x160 | `trailFree` | - | 1 | 0 | 6 | high | - | pops trail handle; releases it (clears owner +0x414) if nonzero. <sub>554950</sub> |
| 0x161 | `setPauseState` | - | 1 | 0 | | medium | - | pops v (fixed); sets global pause state 0x584640 = v>>12 (0 = not paused, 1 = paused, 2 = pausing disabled). <sub>554970</sub> |
| 0x162 | `setFlagE8_1000` | flags | 0 | 0 | | high | - | Inline word: nonzero sets, zero clears bit 0x1000 of strat flags (+0xE8). <sub>54fdd0</sub> |
| 0x163 | `setCollOffset` | - | 3 | 0 | 189 | medium | - | pops z (top), y, x; stores them as 16-bit shorts into the strat's collision descriptor (+0xC0) at +0x32/+0x34/+0x36 = collision sphere centre offset. <sub>54fe00</sub> |
| 0x164 | `angleDiff` | - | 2 | 1 | 1507 | high | - | pops b (top), a (angles, full turn 0x1000000); pushes (a-b) wrapped to the signed half-turn range (-0x800000..0x7FF000). <sub>554990</sub> |
| 0x165 | `collideAsPlayer` | - | 0 | 0 | 27 | medium | - | Sets flag 0x800 on self (excludes self), temporarily makes self the player (0x6D9E1C) and runs the player-vs-strat collision pass FUN_004051d0, then restores the player; lets projectiles (LlamaSpit) hit strats like the player does. <sub>554bc0</sub> |
| 0x166 | `setGlobalTint` | - | 3 | 0 | | high | - | pops b (top), g, r (fixed, clamped 0..255); sets the global strat tint 0x6D9E14..16 and enables it, and applies it to every strat that has no own tint yet (+0x133..+0x136). <sub>554bf0</sub> |
| 0x167 | `screenOverlay` | flags | 0 | 0 | 18 | high | - | Inline word: nonzero enables the full-screen colour overlay (0x583740 = 1, colour reset to 0), zero disables it. <sub>54d970</sub> |
| 0x168 | `setScreenOverlayColour` | - | 4 | 0 | 18 | medium | - | pops d (top), c, b, a (fixed 0..255, >>12); sets the full-screen overlay colour to a<<24 \| d<<16 \| b<<8 \| c (a = alpha; d,b,c land in the R,G,B byte positions of the D3D colour). <sub>54d990</sub> |
| 0x169 | `nop_0x169` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x16a | `turnTowardStratY` | - | 2 | 0 | | high | stTurnTowardY | pops maxStep (top, angle), target strat pointer; turns own yaw (+0x24) toward the target's position by at most maxStep (full turn 0x1000000). <sub>554cd0</sub> |
| 0x16b | `pushSelf` | - | 0 | 1 | | high | - | Pushes the pointer of the executing strat (this). <sub>554cc0</sub> |
| 0x16c | `nop_0x16c` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x16d | `nop_0x16d` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x16e | `lerp` | - | 3 | 1 | 2167 | high | - | pops t (top), b, a; pushes b + (a-b)*t (fixed): t=0 gives b, t=1.0 gives a. <sub>5549c0</sub> |
| 0x16f | `clamp` | - | 3 | 1 | 459 | high | - | pops hi (top), x, lo; pushes max(lo, min(x, hi)) (push order lo, x, hi). <sub>554a10</sub> |
| 0x170 | `distSqToPos` | - | 3 | 1 | 166 | high | - | pops z (top), y, x; pushes squared 3D distance from own position, computed on coordinates >>6 (dx>>6)^2+(dy>>6)^2+(dz>>6)^2. <sub>54f8d0</sub> |
| 0x171 | `distSqToPosXZ` | - | 2 | 1 | 394 | high | - | pops z (top), x; pushes squared horizontal distance from own position, (dx>>6)^2+(dz>>6)^2. <sub>54f920</sub> |
| 0x172 | `approach` | - | 3 | 1 | 365 | high | - | pops step (top), target, value; pushes value moved toward target by step without overshooting. <sub>554a70</sub> |
| 0x173 | `distSqXZ` | - | 4 | 1 | 28 | high | - | pops z2 (top), x2, z1, x1; pushes (x2-x1)^2 + (z2-z1)^2 in fixed point. <sub>554ad0</sub> |
| 0x174 | `turnTowardAngleY` | - | 2 | 0 | 466 | high | stTurnTowardY | pops maxStep (top), targetYaw; turns own yaw (+0x24) toward targetYaw by at most maxStep, wrapping (full turn 0x1000000). <sub>54f150</sub> |
| 0x175 | `setFlagEC_80` | flags | 0 | 0 | | high | - | Inline word: nonzero sets, zero clears bit 0x80 of strat flags2 (+0xEC). <sub>54f750</sub> |
| 0x176 | `inRectXZ` | - | 6 | 1 | 7 | high | - | pops maxZ (top), maxX, minZ, minX, z, x; pushes integer 1 (not 0x1000) if minX<=x<=maxX and minZ<=z<=maxZ, else 0. <sub>554b40</sub> |
| 0x177 | `waypointStep` | - | 1 | 0 | 402 | high | stNextWaypoint/stPrevWaypoint | pops n (fixed); moves current waypoint (+0x120) n steps forward via next links (wraps to first +0x118) or -n steps back via prev links (wraps to last +0x11C). <sub>54f290</sub> |
| 0x178 | `inWaypointBoxXZ` | - | 2 | 1 | 319 | high | - | pops z (top), x; pushes integer 1 if (x,z) lies inside the axis-aligned XZ box spanned by the current waypoint and the next one (wrapping to first), else 0. <sub>54f2f0</sub> |
| 0x179 | `nearestWaypoint` | - | 0 | 0 | | high | stNearestWaypointNext | Sets current waypoint (+0x120) to the waypoint of the list (+0x118) nearest (XZ) to the strat. <sub>54f430</sub> |
| 0x17a | `nearestWaypointToPlayer` | - | 0 | 0 | | high | - | Sets current waypoint to the waypoint nearest (XZ) to the player. <sub>54f470</sub> |
| 0x17b | `nearestOtherWaypoint` | - | 0 | 0 | | high | - | Sets current waypoint to the waypoint nearest (XZ) to the strat, excluding the current one. <sub>54f4b0</sub> |
| 0x17c | `nop_0x17c` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x17d | `nop_0x17d` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x17e | `nop_0x17e` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x17f | `nop_0x17f` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x180 | `addTint` | - | 3 | 0 | | high | - | pops b (top), g, r (fixed deltas); adds them to the strat's tint (own +0x133..135, or the level ambient at 0x584644+0x4C if none), clamps 0..255, stores and enables own tint (+0x136). <sub>54fef0</sub> |
| 0x181 | `setAnimFrame` | - | 1 | 0 | | high | - | pops frame (fixed); if an anim is set (+0x14) sets current frame +0x110 (out of range/negative -> 0), clears +0x114 and sets flag 8 (anim changed). <sub>554d50</sub> |
| 0x182 | `nop_0x182` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x183 | `showSaveSlotInfo` | - | 1 | 0 | 1 | medium | - | pops slot (fixed 1..3); draws the save-game slot info text ('Gespeichertes Spiel n', chapter, etc.) for the main menu (FUN_0040db90). <sub>554da0</sub> |
| 0x184 | `pushTintR` | - | 0 | 1 | | high | - | Pushes own tint red byte (+0x133) as fixed. <sub>554dc0</sub> |
| 0x185 | `pushTintG` | - | 0 | 1 | | high | - | Pushes own tint green byte (+0x134) as fixed. <sub>554de0</sub> |
| 0x186 | `pushTintB` | - | 0 | 1 | | high | - | Pushes own tint blue byte (+0x135) as fixed. <sub>554e00</sub> |
| 0x187 | `emitterInit` | - | 7 | 0 | 113 | medium | stSpawnparticle | pops colour/texture (top, raw), z, y, x (vector/offset), type (fixed 1..0x2B preset, or >=0x1000 type flags), size/speed (fixed, 0x1000 default), slot (0..2, >=3 = extra slot +0x153); (re)initialises the strat's particle emitter in that slot (records at 0x5FE230, 0x34 bytes; texture/colour stored per type). <sub>552f40</sub> |
| 0x188 | `emitParticles` | - | 3 | 0 | | medium | - | pops yStep (top, fixed), count, slot; emits count particles from the slot's emitter at own position with random XZ velocity, each raised by yStep. <sub>552fb0</sub> |
| 0x189 | `randSigned` | - | 1 | 1 | 922 | high | - | pops n; pushes a random value in (-n, n) ((rand32 - rand32) % n, same fixed scale as n). <sub>54f9a0</sub> |
| 0x18a | `emitterSetParams` | - | 5 | 0 | 113 | low | - | pops e (top), d, c, b, slot; sets emitter slot params: +0x254 = b (rate/speed), +0x24A = c>>12 (life); if e==0 +0x25C = (d>>12)*32-32 else +0x246 = e, +0x23C = d. Allocates the slot if empty. <sub>5530b0</sub> |
| 0x18b | `emitterSetColour` | - | 5 | 0 | 9 | low | - | pops colour (top, raw packed, see 0x15d), b, g, r, slot; sets emitter colour (+0x250) and fade/alpha (+0x240..243): if r has no fraction it is an alpha value only, else r,g,b are per-channel fade values. <sub>553100</sub> |
| 0x18c | `emitParticlesStill` | - | 3 | 0 | 50 | medium | - | pops yStep (top, raw fixed), count, slot; emits count particles from the slot's emitter at own position without velocity, stacked by yStep. <sub>552ff0</sub> |
| 0x18d | `stubPop2_0x18d` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
| 0x18e | `stubPop2_0x18e` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
| 0x18f | `clampToWaypointBox` | - | 0 | 0 | 155 | high | - | Clamps own x/z (+0x30/+0x38) into the XZ box spanned by the current waypoint and the next one. <sub>54f390</sub> |
| 0x190 | `processBeforeCamera` | - | 0 | 0 | 107 | high | - | If no camera strat (0x6D9E28) yet: re-executes next frame (pc-=4, yield). Else unlinks self from the strat list (0x6D9E38) and reinserts it directly before the camera so it updates before the camera. <sub>554e60</sub> |
| 0x191 | `processBeforeCamera2` | - | 0 | 0 | 84 | high | - | Same as 0x190 (waits for camera, then moves self just before the camera in the update list); functionally identical alias. <sub>554f30</sub> |
| 0x192 | `endLevel` | - | 3 | 0 | 91 | medium | stEndlevel | pops sub (top), level, world (fixed); saves current level as previous, sets next level, sets 'all coins collected'/completion flags, updates furthest-progress 0x585018, state 0x584638 = 10 and starts the fade-out (world=level=sub=7 means return to the level map). <sub>54d620</sub> |
| 0x193 | `stubPop1Push0_0x193` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. <sub>5550b0</sub> |
| 0x194 | `stubPop1Push0_0x194` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. <sub>5550b0</sub> |
| 0x195 | `stubPop2_0x195` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
| 0x196 | `stubPop2_0x196` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
| 0x197 | `nop_0x197` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x198 | `nop_0x198` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x199 | `nop_0x199` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x19a | `nop_0x19a` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x19b | `nop_0x19b` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x19c | `nop_0x19c` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x19d | `sqrt` | - | 1 | 1 | 67 | high | - | pops v (fixed); pushes fixed sqrt(v) (fsqrt of v/4096, rescaled). <sub>54f610</sub> |
| 0x19e | `nop_0x19e` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x19f | `nop_0x19f` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x1a0 | `setWaypointParam1` | - | 1 | 0 | 167 | high | - | pops v; sets current waypoint +0x08 = (v>>12)&0xFFF (readable as own property 9). <sub>54ea80</sub> |
| 0x1a1 | `setWaypointParam2` | - | 1 | 0 | 245 | high | - | pops v; sets current waypoint +0x0C = (v>>12)&0xFFF (readable as own property 10). <sub>54eab0</sub> |
| 0x1a2 | `setWaypointParam3` | - | 1 | 0 | | high | - | pops v; sets current waypoint +0x10 = (v>>12)&0xFFF (readable as own property 11). <sub>54eae0</sub> |
| 0x1a3 | `stubPop1_0x1a3` | - | 1 | 0 | | high | - | Stub FUN_00554fb0: pops and discards 1 value. <sub>554fb0</sub> |
| 0x1a4 | `stubPop1_0x1a4` | - | 1 | 0 | | high | - | Stub FUN_00554fb0: pops and discards 1 value. <sub>554fb0</sub> |
| 0x1a5 | `nop_0x1a5` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
| 0x1a6 | `emitterKill` | - | 1 | 0 | | high | - | pops slot (fixed); frees the strat's particle emitter in that slot (+0x150+slot = 0xFF) and resets the record. <sub>553150</sub> |
| 0x1a7 | `stubPush0_0x1a7` | - | 0 | 1 | | high | - | Stub FUN_0054e880: pushes 0. <sub>54e880</sub> |
| 0x1a8 | `processFirst` | - | 0 | 0 | 1 | high | - | Moves self to the head of the strat update list (0x6D9E38) if not already there. <sub>554e20</sub> |
| 0x1a9 | `setRain` | - | 4 | 0 | 4 | low | - | pops colour (top, raw -> 0x6D9BE8), b (->0x578EF4), a2 (->0x6D9BF4), a1 (->0x578EF0) (all >>12); if the three counts/params are 0 rain is stopped and cleared, else rain is enabled (0x6D9BE4 = 1). <sub>554fc0</sub> |
| 0x1aa | `stubPop1Push0_0x1aa` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. <sub>5550b0</sub> |
| 0x1ab | `stubPop2_0x1ab` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
| 0x1ac | `stubPop1Push0_0x1ac` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. <sub>5550b0</sub> |
| 0x1ad | `stubPop2Push0_0x1ad` | - | 2 | 1 | | high | - | Stub FUN_005550d0: pops 2 values and pushes 0. <sub>5550d0</sub> |
| 0x1ae | `stubPop3_0x1ae` | - | 3 | 0 | | high | - | Stub FUN_005550f0: pops and discards 3 values. <sub>5550f0</sub> |
| 0x1af | `setWeatherRender` | - | 1 | 0 | | medium | - | pops mode (fixed): 0 installs the normal rain/weather update+render callbacks (0x5FF034/38), 1 installs no-op callbacks and clears 0x6D94D0 (weather off). <sub>555040</sub> |
| 0x1b0 | `emitParticlesVel` | - | 6 | 0 | 159 | medium | - | pops vz (top), vy, vx (velocity), yStep, count, slot (last three fixed >>12); emits count particles from the slot's emitter at own position with the given velocity, stacked by yStep. <sub>553030</sub> |
| 0x1b1 | `nop_0x1b1` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
## Properties (`pushProp`/`popProp` id; same ids for player/parent/camera/… variants)
| id | name | get | set | conf | semantics |
|---|---|---|---|---|---|
| 0 | `zero_0` | yes | no | high | Always pushes 0 (unused id; also 0x13, 0x15, 0x47, 0x75, 0x76, 0x77, 0x98, 0x9e, 0xc1, 0xd8, 0xed, 0xee, 0x102 push 0). |
| 1 | `unk_1` | yes | no | low | DAT_006d94d0 << 12 (int global, cleared in FUN_00426... init; meaning unknown). |
| 3 | `wpEndFlags` | yes | no | high | Current waypoint (+0x120) link bits: bit0 = no previous node (node[0]==0, first), bit1 = no next node (node[1]==0, last). Raw int. |
| 4 | `wpParam` | yes | yes | medium | Current waypoint field +0x28 (raw 32-bit, per-node parameter). |
| 5 | `distToWp` | yes | no | high | 3D distance from strat position (+0x30) to current waypoint position (wp+0x18), 20.12 (FUN_00404bb0, sqrt). |
| 6 | `wpX` | yes | yes | high | Current waypoint x (wp+0x18), 20.12. |
| 7 | `wpY` | yes | yes | high | Current waypoint y (wp+0x1C), 20.12. |
| 8 | `wpZ` | yes | yes | high | Current waypoint z (wp+0x20), 20.12. |
| 9 | `wpRotX` | yes | no | medium | Current waypoint int field +0x08 << 12 (node rotation x in 4096 table units, per notes). |
| 10 | `wpRotY` | yes | no | medium | Current waypoint int field +0x0C << 12 (node yaw in 4096 table units; e.g. checkpoint respawn yaw). |
| 11 | `wpRotZ` | yes | no | low | Current waypoint int field +0x10 << 12 (third node angle/param). |
| 12 | `playerDistToWp` | yes | no | high | 3D distance from the player (0x6D9E1C) to this strat's current waypoint, 20.12; 100.0 (0x64000) if no player. |
| 13 | `angleToWp` | yes | no | high | Yaw from the strat to its current waypoint: atan2(dx,dz) (FUN_00404d60), 24-bit angle (0x1000000 = full turn, 4096 table units <<12). |
| 14 | `animFrame` | yes | no | high | Current anim frame +0x110 << 12. |
| 15 | `animPlaying` | yes | no | high | Flag +0xE8 bit 3 (0x8, anim playing) as 0/1. |
| 16 | `floorHeight` | yes | yes | high | Floor height under the strat, collision record (+0xC0)+0x00, 20.12 (0xFFFF0000 = no floor). Get returns 0 if no record. |
| 17 | `floorMaterial` | yes | no | high | Floor surface/material id, short at (+0xC0)+0x04, << 12 (0xFFFF = none; 16 = level door trigger). 0 if no record. |
| 18 | `ceilingMaterial` | yes | no | medium | Short at (+0xC0)+0x30 << 12: material of the surface above (set together with ceiling height +0x28 in the collision code, 0xFFFF none). |
| 19 | `zero_19` | yes | no | high | Always 0. |
| 20 | `slideAngle` | yes | no | low | Short at (+0xC0)+0x2C << 12: direction (atan2 of old->new pos) written when the strat is pushed back off an edge/wall; 4096 units. |
| 21 | `zero_21` | yes | no | high | Always 0 (both branches push 0). |
| 22 | `collisionRadius` | yes | yes | high | Collision radius, short at +0xD2 (raw, not shifted). Set writes a full 32-bit word at +0xD2 (also clobbers the short at +0xD4). |
| 29 | `isActivated` | yes | no | high | Map-instance record (+0xF0) byte +0x44 bit 0x8 (persistent "activated" state, set by op 0x92). Returns 8 or 0. No null check. |
| 30 | `isCollected` | yes | no | high | Map-instance record (+0xF0) byte +0x44 bit 0x4 (persistent "collected", set by op 0x93). Returns 4 or 0. No null check. |
| 31 | `onGround` | yes | no | medium | Flag +0xEC & 0x2 (standing on a floor; slope/platform code FUN_004054c0 requires it). Returns 2 or 0. |
| 32 | `flagE8_400000` | yes | no | low | Flag +0xE8 bit 22 (0x400000) as 0/1. |
| 33 | `flagE8_800000` | yes | no | low | Flag +0xE8 bit 23 (0x800000) as 0/1. |
| 34 | `x` | yes | yes | high | Position x +0x30, 20.12. |
| 35 | `y` | yes | yes | high | Position y +0x34, 20.12. |
| 36 | `z` | yes | yes | high | Position z +0x38, 20.12. |
| 37 | `rotX` | yes | yes | high | Rotation x (pitch) +0x20; get masks to 24 bits (0x1000000 = full turn), set stores raw. |
| 38 | `rotY` | yes | yes | high | Rotation y (yaw) +0x24; 24-bit angle, 0x1000000 = full turn. |
| 39 | `rotZ` | yes | yes | high | Rotation z (roll) +0x28; 24-bit angle. |
| 41 | `angleToPlayer` | yes | no | high | Yaw from this strat to the player: atan2(px-x, pz-z) (FUN_00404d60(player+0x30, self+0x30)), 24-bit angle; 0 if no player. (Notes called 41 "distance to player" - code is an angle.) |
| 42 | `padHeld` | yes | no | high | Joypad buttons currently held, DAT_005FCF20 (bit mask: 0x10 up, 0x40 down, 0x80 left?, 0x20 right?, 0x1/0x8/0x400.. buttons; 0x10000-0x80000 = direction relative to player yaw). |
| 43 | `padReleased` | yes | no | high | Buttons released this frame, DAT_005FCF34 (= old & ~new). |
| 44 | `padPressed` | yes | no | high | Buttons newly pressed this frame, DAT_005FCF30 (= new & ~old). |
| 45 | `distToPlayer` | yes | no | high | 3D distance to the player, cached in +0x1C (computed lazily as sqrt of +0x148, the squared distance updated per frame), 20.12. |
| 47 | `scale` | yes | no | high | Scale y +0x64 (used as the uniform scale value), 20.12; see op 0x55. |
| 48 | `scaleX` | yes | no | high | Scale x +0x60, 20.12. |
| 49 | `scaleY` | yes | no | high | Scale y +0x64, 20.12. |
| 50 | `scaleZ` | yes | no | high | Scale z +0x68, 20.12. |
| 51 | `stratCount` | yes | no | medium | Number of strats in use DAT_006D9DC8 << 12. |
| 52 | `stratCapacity` | yes | no | medium | Level header (DAT_00584648)+0x08 << 12 (max strat count of the level; compared against stratCount on spawn). |
| 53 | `distToWpXZ` | yes | no | high | Horizontal (x/z) distance from strat to current waypoint, 20.12 (FUN_00404c50). |
| 54 | `boneX` | yes | no | medium | +0xD8: x of the bone/attachment point computed by op 0xc7 (useBone), 20.12 world. |
| 55 | `boneY` | yes | no | medium | +0xDC bone point y. |
| 56 | `boneZ` | yes | no | medium | +0xE0 bone point z. |
| 57 | `playerPtr` | yes | no | high | Pointer to the player strat (DAT_006D9E1C); 0 if none (usable as "player exists"). |
| 58 | `camLookAtX` | yes | yes | medium | Camera look-at point x DAT_006D9D80, 20.12 (default 0; cut-scene cameras write it). |
| 59 | `camLookAtY` | yes | yes | medium | Camera look-at point y DAT_006D9D84 (default 1.0). |
| 60 | `camLookAtZ` | yes | yes | medium | Camera look-at point z DAT_006D9D88. |
| 61 | `tribe` | yes | no | high | Current level tribe DAT_00584EB4 << 12 (level id part 1, %d_%d_%d file names). |
| 62 | `world` | yes | no | high | Current level world DAT_00584EB8 << 12. |
| 63 | `chapter` | yes | no | high | Current level chapter DAT_00584EBC << 12. |
| 64 | `map` | yes | no | high | Current map/sub-level DAT_00584EC0 << 12. |
| 65 | `hasCamTarget` | yes | no | medium | -1 if a camera target strat (DAT_006D9E30, op 0xc9) is set, else 0. |
| 66 | `hasCamTarget2` | yes | no | medium | -1 if the second camera target (DAT_006D9E34, op 0xca) is set, else 0. |
| 67 | `padType` | yes | no | low | DAT_005FCF18 << 12: 3 = digital pad, 2 = direction/analog input active this frame. |
| 68 | `camDistToTarget` | yes | no | high | 3D distance camera strat (0x6D9E28) -> camera target (0x6D9E30), 20.12; 0 if no camera. |
| 69 | `camDistToTarget2` | yes | no | high | 3D distance camera -> second target (0x6D9E34), 20.12; 0 if no camera. |
| 70 | `camTargetYawDiff` | yes | no | high | Signed yaw difference target.rotY - camera.rotY (wrapped to +-0x800000, FUN_0054dae0). No null checks. |
| 72 | `camDistToTargetXZ` | yes | no | high | Horizontal distance camera -> target (FUN_00404cc0), 20.12. No null check. |
| 73 | `camDistToTarget2XZ` | yes | no | high | Horizontal distance camera -> second target, 20.12. No null check. |
| 75 | `stickX` | yes | no | high | Analog/digital stick x DAT_005FCF1C (-0x2000..0x1FC0, i.e. about +-2.0 fixed). |
| 76 | `stickY` | yes | no | high | Stick y DAT_005FCF2C (same range). |
| 77 | `stickMagnitude` | yes | no | high | Stick deflection DAT_005FCEFC, raw int 0..0xB5 (thresholds: 42 deadzone, 84 direction bits, see ids 94-97/130-131). |
| 78 | `stickAngle` | yes | no | high | Stick direction DAT_005FCF4E (short, 4096 units) << 12. |
| 79 | `stickAngleWorld` | yes | no | high | (stick angle + camera yaw DAT_005FCF00) << 12, masked to 24 bits: stick direction in world yaw. |
| 80 | `stick2X` | yes | no | low | DAT_005FCF28, never written by the exe (second stick set, always 0). |
| 81 | `stick2Y` | yes | no | low | DAT_005FCF24, never written (always 0). |
| 82 | `stick2Magnitude` | yes | no | low | DAT_005FCEF8, never written (always 0). |
| 83 | `stick2Angle` | yes | no | low | Short DAT_005FCF4C << 12, never written. |
| 84 | `stick2AngleWorld` | yes | no | low | (camera yaw + DAT_005FCF4C) << 12 masked 24 bits. |
| 85 | `slopeForward` | yes | no | medium | Floor normal rotated into the strat's yaw frame, forward component DAT_0057DDA8 (FUN_004054c0; 0 unless onGround), 20.12. |
| 86 | `slopeSide` | yes | no | medium | Floor normal, sideways component in the strat's frame DAT_0057DD90, 20.12. |
| 87 | `slopeAngle` | yes | no | medium | Downhill direction relative to the strat's yaw DAT_0057DD94 (0..0xFFF) << 12. |
| 88 | `padAux` | yes | no | low | DAT_005FCF58 (extra input bits, cleared when >1 pads). |
| 90 | `padAuxPressed` | yes | no | low | DAT_005FCF60 = new & ~old of padAux. |
| 91 | `unk_5b` | yes | no | low | DAT_00584F10 << 12 (game state global, unknown). |
| 92 | `health` | yes | no | medium | Player health DAT_00584E70 (fixed, starts at 5.0, HUD shows it; reset to 5.0 on respawn). Same value as id 189 (settable there). |
| 93 | `unk_5d` | yes | no | low | DAT_0058464C (raw, game-state pointer/flag). |
| 94 | `stickWalkThreshold` | yes | no | medium | Constant 84 (0x54, raw int): stick magnitude above which direction bits are generated. |
| 95 | `stickRunThreshold` | yes | no | medium | Constant 123 (0x7B, raw int). |
| 96 | `stickWalkThreshold2` | yes | no | medium | Constant 84 (duplicate of 94). |
| 97 | `stickRunThreshold2` | yes | no | medium | Constant 123 (duplicate of 95). |
| 98 | `hasWaypoints` | yes | no | high | 1.0 if the EXECUTING strat (not the queried one) has a first waypoint (+0x118), else 0. For 0x1c (player prop) this still checks the caller. |
| 99 | `distToBoss` | yes | no | high | 3D distance from the boss strat (DAT_006D9E24, op 0xef) to this strat, 20.12; 100.0 if none. |
| 100 | `distToBossXZ` | yes | no | high | Horizontal distance boss -> this strat, 20.12; 100.0 if none. |
| 101 | `zoneX` | yes | no | low | Map zone/cell record containing the strat (FUN_0054d560, same x/z grid cell, nearest y) field +0x0C, integer part (& 0xFFFFF000); -1.0 if none. |
| 102 | `zoneY` | yes | no | low | Zone record field +0x10 integer part; -1.0 if none. |
| 103 | `zoneZ` | yes | no | low | Zone record field +0x14 integer part; -1.0 if none. |
| 104 | `distToWpSq` | yes | no | high | Squared 3D distance to current waypoint ((d>>6)^2 summed, i.e. 20.12 of world units squared). Same as 194. |
| 105 | `distToWpSqXZ` | yes | no | high | Squared horizontal distance to current waypoint. |
| 106 | `flag83730Clear` | yes | no | low | 1.0 if DAT_00583730 == 0 (FUN_00414fb0), else 0. |
| 107 | `distToPlayerXZ` | yes | no | high | Horizontal distance player -> this strat, 20.12; 100.0 if no player. |
| 108 | `saveByte5219` | yes | no | low | DAT_00585219 << 12 (per-save-slot byte, reset to 0 on new game). |
| 110 | `queueAEmpty` | yes | no | low | 1.0 if DAT_006D9858 < 1 (message/stream queue A empty), else 0. |
| 111 | `queueBEmpty` | yes | no | low | 1.0 if DAT_006D94E8 < 1 (queue B empty; also blocks HUD counters), else 0. |
| 120 | `camFloorHeight` | yes | yes | medium | Get: floor height of the map cell under the camera DAT_00586424 (-16.0 if outside the map). Set: writes the OVERRIDE DAT_0058641C (non-zero replaces the computed value). Get and set use different variables. |
| 121 | `camX` | yes | yes | high | Camera position x DAT_006D9D20 (copied from the camera strat each frame), 20.12. |
| 122 | `camY` | yes | yes | high | Camera position y DAT_006D9D24. |
| 123 | `camZ` | yes | yes | high | Camera position z DAT_006D9D28. |
| 124 | `flagEC_1000000` | yes | no | low | Flag +0xEC bit 24 as 0/1. |
| 125 | `flagEC_2000000` | yes | no | low | Flag +0xEC bit 25 as 0/1. |
| 126 | `hitDepth` | yes | no | medium | Short +0xD4 (raw): penetration depth of the last player-vs-strat sphere hit (FUN_004051d0), 0xFFFF none. |
| 127 | `hitAngle` | yes | no | medium | Short +0xD6 << 12: direction of the last sphere hit (4096 units; player gets the opposite of the strat). |
| 128 | `unk_80` | yes | no | low | DAT_00585004 << 12 (unknown game global). |
| 129 | `zoneFlags` | yes | no | low | Zone record (FUN_0054d560) +0x20 & 0xC00, << 12; -1.0 if none. |
| 130 | `stickDeadzone` | yes | no | medium | Constant 42 (0x2A, raw int). |
| 131 | `stickDeadzone2` | yes | no | medium | Constant 42 (duplicate). |
| 132 | `oldX` | yes | yes | medium | Previous-frame position x +0x50, 20.12 (collision code restores it on edge push-back). |
| 133 | `oldY` | yes | yes | medium | Previous position y +0x54. |
| 134 | `oldZ` | yes | yes | medium | Previous position z +0x58. |
| 135 | `unk_6da2c0` | yes | no | low | DAT_006DA2C0, no other reference (always 0). |
| 136 | `unk_6da2c4` | yes | no | low | DAT_006DA2C4, no other reference (always 0). |
| 137 | `queuesEmpty` | yes | no | low | 1.0 if both queue A and queue B are empty (ids 110 and 111), else 0. |
| 138 | `alpha` | yes | yes | medium | Transparency +0x10C (renderer: < 0x1000 = translucent, 1.0 = opaque), 20.12. |
| 139 | `oldRotX` | yes | yes | medium | Previous rotation x +0x40 (24-bit masked on get). |
| 140 | `oldRotY` | yes | yes | medium | Previous yaw +0x44 (compared with +0x24 by the collision code). |
| 141 | `oldRotZ` | yes | yes | medium | Previous rotation z +0x48. |
| 142 | `optionFlag80000` | yes | no | low | DAT_006DA330 & 0x80000 (global option/debug flag word). |
| 143 | `camScale` | yes | yes | low | DAT_006D9D30, camera value initialised to 1.0 (zoom/fov factor?). |
| 144 | `levelHeaderC` | yes | no | low | Level header (DAT_00584648)+0x0C ushort << 12. |
| 145 | `slopeSteepness` | yes | no | medium | 1.0 - floor normal y (DAT_0057DDA4) << 12: 0 on flat ground (FUN_004054c0). |
| 147 | `levelTableByte` | yes | no | low | Byte table 0x585024[map + (chapter + tribe*10)*4] << 12 (per-level constant). |
| 149 | `flagEC_1` | yes | no | low | Flag +0xEC & 0x1. |
| 150 | `flagEC_8000000` | yes | no | low | Flag +0xEC & 0x8000000. |
| 151 | `cheat5` | yes | no | medium | Cheat toggle bits DAT_00584710 & 0x20 (button-sequence cheats, FUN_0041a0xx). |
| 153 | `frameCounter` | yes | no | high | Strat-update frame counter DAT_006D9E10 (+1.0 per frame). |
| 154 | `hitSphere` | yes | no | medium | Byte +0x130 << 12: index of the collision sphere hit by the player (FUN_004051d0). |
| 155 | `unk_6d9d38` | yes | no | low | DAT_006D9D38, no other reference. |
| 156 | `unk_6d9cf0` | yes | no | low | DAT_006D9CF0, no other reference. |
| 157 | `isFloorCollisionSolid` | yes | no | low | 1 (raw) if +0xEC has both 0x8000 and 0x10000 set, else 0. |
| 159 | `scratch6d9e68` | yes | yes | low | DAT_006D9E68 (only accessed by strats). |
| 160 | `saveEnabled` | yes | yes | low | DAT_006D9E74 (1.0 enables the save-game prompt, reset to 0 on level start). |
| 161 | `hudValueA` | yes | yes | low | DAT_006DA300 (HUD element value, shown via >>12, reset per level). |
| 162 | `hudValueB` | yes | yes | low | DAT_006DA2F8 (HUD element value, reset per level). |
| 163 | `audioMode` | yes | yes | low | DAT_006DA2FC: 3.0/4.0/5.0 from the sound output menu (Stereo/Quatro/Surround); also written (slot+1) by save code. |
| 164 | `instanceData0` | yes | yes | low | Map-instance record (+0xF0)+0x28 -> word 0. Set returns NULL (crash) unless record+0x24 != 0. Get pushes nothing if no record. |
| 165 | `instanceData1` | yes | yes | low | Map-instance record (+0xF0)+0x28 -> word 1. |
| 166 | `floorCollisionOn` | yes | no | medium | Flag +0xEC & 0x8000 (floor collision, ops 0xa5/0xa6). |
| 169 | `lives` | yes | yes | low | DAT_00584E6C (fixed, starts at 5.0, saved per slot) - lives or max health. |
| 171 | `saveProgressA` | yes | no | low | DAT_00585008 << 12 (saved progress byte). |
| 172 | `progressWorld` | yes | no | low | DAT_0058500C << 12 (saved; world of the furthest/selected level, 8 = finale). |
| 173 | `progressChapter` | yes | no | low | DAT_00585010 << 12 (saved; chapter, 6 with world 8 = last level). |
| 174 | `saveProgressB` | yes | no | low | DAT_00585014 << 12 (saved progress byte). |
| 175 | `unk_6da308` | yes | no | low | DAT_006DA308, no other reference. |
| 178 | `cheat4` | yes | no | medium | DAT_00584710 & 0x10 (cheat toggle). |
| 179 | `hudFlags` | yes | yes | medium | DAT_006DA2E0 HUD element enable mask (default 0x3F000; 0x2000 timer, 0x8000/0x10000/0x20000/0x40000 counters). |
| 181 | `gameState1d74` | yes | yes | low | DAT_00581D74 (game/menu state, checked == 0 for normal play). |
| 182 | `camFlag800000` | yes | no | low | Camera strat +0xEC & 0x800000 (0 if no camera). |
| 183 | `slopeNormalX` | yes | no | medium | Floor normal x DAT_0057DD9C (world, 20.12; FUN_004054c0). |
| 184 | `slopeNormalZ` | yes | no | medium | Floor normal z DAT_0057DDA0. |
| 185 | `wpParam2` | yes | yes | medium | Current waypoint field +0x2C (raw); 0 / NULL if no waypoint. |
| 186 | `hitRadius` | yes | yes | medium | +0x13C: player interaction radius (player collides when squared player distance +0x148 <= r*r), 20.12. |
| 187 | `hitDamage` | yes | yes | medium | +0x140: damage dealt on contact (copied into the other strat's +0x144). |
| 188 | `damageTaken` | yes | no | medium | +0x144: damage received from the last contact (player cleared to 0 each frame). |
| 189 | `health2` | yes | yes | medium | DAT_00584E70 = health (same as 92, settable). |
| 190 | `levelRecord` | yes | yes | low | Per-level saved word DAT_00584F1C[chapter + world*6] (best/collected value for the current level; HUD shows it). |
| 191 | `counterC` | yes | yes | low | DAT_00584FFC: persistent HUD counter (icon 0x150), saved. |
| 192 | `counterA` | yes | yes | low | DAT_00584FF4: persistent HUD counter (icon 0x184, displayed up to 999) - probably coins. |
| 201 | `distToPlayerSq` | yes | no | high | +0x148: cached squared distance to the player ((d>>6)^2 sum), updated each frame when tracking (op 0x30). |
| 202 | `distToPlayerSqXZ` | yes | no | high | Squared horizontal distance player -> strat (FUN_00404c90); 100.0 if no player. |
| 203 | `soundVecX` | yes | yes | low | DAT_006DA2CC; on the next set of 203/204 the magnitude sqrt(x^2+y^2)*4 is passed to FUN_00546140 (sound/volume parameter). |
| 204 | `soundVecY` | yes | yes | low | DAT_006DA2D0, see 203. |
| 210 | `collisionHeight` | yes | yes | medium | Collision record (+0xC0)+0x18: strat height/size used for ceiling tests (ceiling hit if ceiling <= y + this), 20.12. Get 0 if no record. |
| 211 | `mapFloorHeight` | yes | no | low | (+0xC0)+0x1C: floor height from the landscape test (FUN_004036d0), 0xFFFF0000 none. No null check. |
| 212 | `mapFloorMaterial` | yes | no | low | Short (+0xC0)+0x24 << 12: material of that floor (0xFFFF none). |
| 213 | `objFloorHeight` | yes | no | low | (+0xC0)+0x20: floor height from the strat-object test (FUN_00403ad0, skipped with +0xE8 bit 0x400). |
| 214 | `objFloorMaterial` | yes | no | low | Short (+0xC0)+0x26 << 12: material of the object floor. |
| 215 | `wallMaterial` | yes | no | low | Short (+0xC0)+0x3A << 12 (material of the wall/side surface hit, set with object/face +0x3C/+0x3E). |
| 217 | `collisionField38` | yes | yes | low | Short (+0xC0)+0x38 raw on get; set writes 32 bits at +0x38 (also clobbers +0x3A). |
| 218 | `hudDigit` | yes | yes | low | DAT_006DA290: HUD single-digit counter (shown as '0'+value>>12). |
| 226 | `animFrameCount` | yes | no | high | Anim header (+0x14)+0x08 << 12 (number of frames). No null check. |
| 227 | `angleToCamLookAt` | yes | no | medium | Yaw from the strat to the camera look-at point (DAT_006D9D80), 24-bit angle. |
| 228 | `skyParamA` | yes | yes | low | DAT_0057D830 (render global, sky/background). |
| 229 | `skyParamB` | yes | yes | low | DAT_0057D83C. |
| 230 | `skyParamC` | yes | yes | low | DAT_0057D840. |
| 231 | `skyRotation` | yes | yes | low | DAT_0057D834: rotation added to a background model's yaw (>>12 & 0xFFF). |
| 232 | `hudTimer` | yes | yes | low | DAT_006DA304: HUD timer/counter value (>>12 displayed, element bit 0x2000). |
| 233 | `animPtr` | yes | no | high | Current anim address +0x14 (0 = none). |
| 234 | `cheat1` | yes | no | medium | DAT_00584710 & 0x2 (cheat toggle). |
| 236 | `hudRefresh` | yes | yes | low | DAT_006DA2DC (forces HUD counter redraw when non-zero). |
| 239 | `unk_57ddb0` | yes | no | low | DAT_0057DDB0, no other reference. |
| 240 | `unk_57ddb4` | yes | no | low | DAT_0057DDB4, no other reference. |
| 241 | `unk_57ddb8` | yes | no | low | DAT_0057DDB8, no other reference. |
| 242 | `drawDistance` | yes | no | low | 1000.0 unless +0xEC bit 0x10000; else (bit 0x400000 ? DAT_0058658C : DAT_00586590) + 0.5 (view/clip distance globals). |
| 243 | `ceilingHeight` | yes | no | medium | (+0xC0)+0x28: height of the surface above (1000.0 default when ceiling test enabled by +0xE8 0x200000). No null check. |
| 244 | `zoneParam` | yes | no | low | DAT_006DA298: short +0x38 of the zone record the strat entered (op FUN_0054df30, used as conveyor/offset). |
| 249 | `wpPtr` | yes | yes | high | Current waypoint pointer +0x120 (set allows jumping to a stored node). |
| 250 | `counterB` | yes | yes | low | DAT_00584E60: HUD counter (icon 0x11C), reset when entering a different level. |
| 253 | `musicBusy` | yes | no | low | 1 (raw) if FUN_00546600(DAT_006D94B4,0) == 2 (stream/voice state), else 0. |
| 265 | `musicTrack` | yes | no | medium | Current music track DAT_00586430 << 12 (level header +0x58, 0x23/0x27 for special modes). |
@@ -56,15 +56,24 @@ public final class ProjectIO {
public interface LoadHook {
void beforeChanges(Map<String, Object> json, Bases cd, Scene reference, Scene scene, List<String> warnings)
throws IOException;
/** Hooks run in ascending order (imports = 0). */
default int order() {
return 0;
}
}
private static final List<LoadHook> HOOKS = new CopyOnWriteArrayList<>();
static {
try {
Class.forName("com.eliasmueller.llamamod.editor.tools.catalog.CrossImport");
} catch (ClassNotFoundException | LinkageError e) {
// optional extension
for (String c : List.of(
"com.eliasmueller.llamamod.editor.tools.catalog.CrossImport",
"com.eliasmueller.llamamod.editor.tools.strats.StratEdits")) {
try {
Class.forName(c);
} catch (ClassNotFoundException | LinkageError e) {
// optional extension
}
}
}
@@ -114,7 +123,8 @@ public final class ProjectIO {
cur.assets().put(e.getKey(), e.getValue());
}
}
for (LoadHook h : HOOKS) {
for (LoadHook h :
HOOKS.stream().sorted(Comparator.comparingInt(LoadHook::order)).toList()) {
h.beforeChanges(json, cd, ref, cur, p.warnings());
}
cur.setNextIdCounter(intOr(json, "nextId", 1));
@@ -0,0 +1,123 @@
package com.eliasmueller.llamamod.editor.tools.strats;
import com.eliasmueller.llamamod.editor.ui.EditorContext;
import com.eliasmueller.llamamod.format.strat.Num;
import com.eliasmueller.llamamod.format.strat.StratListing;
import com.eliasmueller.llamamod.util.SwingListeners;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
/** Edits the constants of one script line: literals 1.5 (fixed), 90deg, 0x10, 42; validated. */
final class ConstantDialog {
private ConstantDialog() {}
static String describe(int v, StratListing.ConstKind k) {
if (k == StratListing.ConstKind.SOUND) {
return "sound sample " + v;
}
if (k == StratListing.ConstKind.TEXTURE) {
return "texture " + v;
}
if (k == StratListing.ConstKind.TEXT) {
return "text line " + v;
}
return Num.fixed(v) + " (20.12) = " + v + " raw = " + Num.degrees(v) + " as angle";
}
/** Problem with a new value, or null. */
static String validate(StratListing.Line l, Integer v) {
if (v == null) {
return "not a number (examples: 1.5, -0.25, 90deg, 0x2000, 42)";
}
if (l.constShort() && (v < Short.MIN_VALUE || v > Short.MAX_VALUE)) {
return "pushShort holds −32768…32767 raw (−8.0…7.99); for larger values the code would need a 32-bit push";
}
if (l.constKind() == StratListing.ConstKind.SOUND && (v < 0 || v > 255)) {
return "sound ids are 0…127 (bit 0x80 = front-end sounds)";
}
if ((l.constKind() == StratListing.ConstKind.TEXTURE || l.constKind() == StratListing.ConstKind.TEXT)
&& (v < 0 || v > 0xffff)) {
return "index out of range";
}
return null;
}
static String literal(StratListing.Line l) {
if (l.constKind() != StratListing.ConstKind.NUMBER) {
return Integer.toString(l.value());
}
String t = l.text();
String op = t.substring(t.indexOf(' ') + 1).trim();
return op.endsWith("deg") ? op : Num.literal(l.value());
}
/** New values by line (only changed ones), or null when cancelled. */
static Map<StratListing.Line, Integer> show(EditorContext ctx, List<StratListing.Line> consts) {
JPanel p = new JPanel(new GridBagLayout());
GridBagConstraints g = new GridBagConstraints();
g.insets = new Insets(3, 4, 3, 4);
g.anchor = GridBagConstraints.WEST;
List<JTextField> fields = new ArrayList<>();
List<JLabel> notes = new ArrayList<>();
int row = 0;
for (StratListing.Line l : consts) {
g.gridy = row++;
g.gridx = 0;
p.add(new JLabel(l.text()), g);
g.gridx = 1;
JTextField f = new JTextField(literal(l), 12);
p.add(f, g);
g.gridx = 2;
JLabel n = new JLabel(describe(l.value(), l.constKind()));
p.add(n, g);
fields.add(f);
notes.add(n);
SwingListeners.onTextChange(f, () -> {
Integer v = Num.parse(f.getText());
String e = validate(l, v);
n.setText(e != null ? "⚠ " + e : describe(v, l.constKind()));
});
}
g.gridy = row;
g.gridx = 0;
g.gridwidth = 3;
p.add(
new JLabel(
"<html><font color='gray'>Fixed point: 1.0 = 4096 · angles: 360deg = 4096.0 · speeds are per frame (30 fps)."
+ "<br>The change is stored in the project and applied to the level's code when it is built.</font></html>"),
g);
while (true) {
int r = JOptionPane.showConfirmDialog(
ctx.frame(), p, "Edit script constant", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (r != JOptionPane.OK_OPTION) {
return null;
}
Map<StratListing.Line, Integer> out = new LinkedHashMap<>();
String err = null;
for (int i = 0; i < consts.size(); i++) {
Integer v = Num.parse(fields.get(i).getText());
String e = validate(consts.get(i), v);
if (e != null) {
err = consts.get(i).text() + ": " + e;
break;
}
if (v != consts.get(i).value()) {
out.put(consts.get(i), v);
}
}
if (err == null) {
return out;
}
JOptionPane.showMessageDialog(ctx.frame(), err, "Edit script constant", JOptionPane.WARNING_MESSAGE);
}
}
}
@@ -0,0 +1,588 @@
package com.eliasmueller.llamamod.editor.tools.strats;
import com.eliasmueller.llamamod.editor.io.Project;
import com.eliasmueller.llamamod.editor.model.Node;
import com.eliasmueller.llamamod.editor.model.RawChunk;
import com.eliasmueller.llamamod.editor.model.Scene;
import com.eliasmueller.llamamod.editor.model.SceneEvent;
import com.eliasmueller.llamamod.editor.model.Strat;
import com.eliasmueller.llamamod.editor.model.cmd.Command;
import com.eliasmueller.llamamod.editor.ui.EditorContext;
import com.eliasmueller.llamamod.editor.ui.Widgets;
import com.eliasmueller.llamamod.format.stpcmerge.StratCode;
import com.eliasmueller.llamamod.format.strat.OpTable;
import com.eliasmueller.llamamod.format.strat.StratListing;
import com.eliasmueller.llamamod.format.strat.StratPseudo;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
/**
* Script panel: the strat code unit of the selected object type as structured script or as
* assembly, with search, cross-references, opcode help and editing of constants (undoable,
* stored in the project).
*/
final class ScriptPanel extends JPanel {
/** A list row: text, STPC offset of its first instruction (−1 = none), editable constants. */
record Row(String text, int offset, List<StratListing.Line> consts, StratListing.Line source) {}
private record UnitItem(int unit, String label) {
@Override
public String toString() {
return label;
}
}
private final EditorContext ctx;
private final JComboBox<UnitItem> units = new JComboBox<>();
private final JComboBox<String> mode = new JComboBox<>(new String[] {"Script", "Assembly"});
private final JCheckBox follow = new JCheckBox("Follow selection", true);
private final JTextField search = new JTextField(14);
private final DefaultListModel<Row> model = new DefaultListModel<>();
private final JList<Row> list = new JList<>(model);
private final JTextArea info = new JTextArea();
private final JLabel status = new JLabel(" ");
private StratListing listing;
private Set<Integer> editedAt = new HashSet<>();
private boolean updating;
private Scene shownScene;
private byte[] shownData;
ScriptPanel(EditorContext ctx) {
super(new BorderLayout());
this.ctx = ctx;
JPanel row1 = new JPanel(new FlowLayout(FlowLayout.LEADING, 4, 1));
row1.add(new JLabel("Type"));
units.setMaximumRowCount(30);
units.setPrototypeDisplayValue(new UnitItem(0, "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM"));
row1.add(units);
row1.add(mode);
row1.add(follow);
JPanel row2 = new JPanel(new FlowLayout(FlowLayout.LEADING, 4, 1));
row2.add(new JLabel("Find"));
row2.add(search);
row2.add(Widgets.button("Next", "search", "Find next (Enter)", () -> find(1)));
row2.add(Widgets.button("Uses", null, "Cross-references of the selected line's operand", this::references));
row2.add(Widgets.button("Edit value…", null, "Change the constants of the selected line", this::editSelected));
row2.add(Widgets.button(
"Clone as new type…",
"duplicate",
"Copy this type into a custom type and edit its script",
() -> TypeDialog.showClone(ctx, primaryType())));
JPanel top = new JPanel(new java.awt.GridLayout(2, 1));
top.add(row1);
top.add(row2);
add(top, BorderLayout.NORTH);
list.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setCellRenderer(new RowRenderer());
info.setEditable(false);
info.setLineWrap(true);
info.setWrapStyleWord(true);
info.setFont(UIManager.getFont("Label.font"));
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, new JScrollPane(list), new JScrollPane(info));
split.setResizeWeight(0.72);
add(split, BorderLayout.CENTER);
status.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6));
add(status, BorderLayout.SOUTH);
units.addActionListener(e -> {
if (!updating) {
show(selectedUnit(), -1);
}
});
mode.addActionListener(e -> show(selectedUnit(), -1));
search.addActionListener(e -> find(1));
list.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
describe(list.getSelectedValue());
}
});
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
editSelected();
}
}
});
ctx.addDocumentListener(d -> reload());
ctx.selection().addListener(ch -> {
if (follow.isSelected()) {
followSelection();
}
});
ctx.addSceneListener(e -> {
if (e.type() == SceneEvent.Type.RELOADED || e.node() instanceof RawChunk) {
SwingUtilities.invokeLater(this::refreshIfChanged);
}
});
reload();
}
private int selectedUnit() {
UnitItem u = (UnitItem) units.getSelectedItem();
return u == null ? -1 : u.unit;
}
private String primaryType() {
Scene s = ctx.scene();
if (s == null || listing == null) {
return null;
}
for (Map.Entry<Integer, String> e : s.catalog().names().entrySet()) {
if (StratEdits.analysis(s).img().unitOf(e.getKey()) == listing.unit
&& s.catalog().has(e.getValue())) {
return e.getValue();
}
}
return null;
}
private void refreshIfChanged() {
Scene s = ctx.scene();
RawChunk c = s == null ? null : StratEdits.chunk(s);
if (s != shownScene || c == null || c.data() != shownData) {
int keep = list.getSelectedIndex();
int u = selectedUnit();
reloadUnits();
if (u >= 0) {
selectUnit(u);
show(u, -1);
if (keep >= 0 && keep < model.size()) {
list.setSelectedIndex(keep);
list.ensureIndexIsVisible(keep);
}
}
}
}
private void reload() {
reloadUnits();
if (follow.isSelected()) {
followSelection();
}
if (listing == null && units.getItemCount() > 0) {
show(selectedUnit(), -1);
}
}
private void reloadUnits() {
updating = true;
try {
units.removeAllItems();
Scene s = ctx.scene();
StratEdits.Analysis a = s == null ? null : StratEdits.analysis(s);
shownScene = s;
shownData = s == null || StratEdits.chunk(s) == null
? null
: StratEdits.chunk(s).data();
if (a == null) {
model.clear();
listing = null;
status.setText("No level open.");
return;
}
Map<Integer, List<String>> names = new LinkedHashMap<>();
for (int u = 0; u < a.img().units.size(); u++) {
names.put(u, new ArrayList<>());
}
for (var e : s.catalog().names().entrySet()) {
int u = a.img().unitOf(e.getKey());
if (u >= 0 && !names.get(u).contains(e.getValue())) {
names.get(u).add(e.getValue());
}
}
List<UnitItem> items = new ArrayList<>();
for (var e : names.entrySet()) {
String n =
e.getValue().isEmpty() ? StratEdits.unitName(s, e.getKey()) : String.join(", ", e.getValue());
if (n.length() > 60) {
n = n.substring(0, 57) + "…";
}
items.add(new UnitItem(
e.getKey(),
n + (StratEdits.isCustomUnit(s, e.getKey()) ? " [custom]" : "") + " · unit " + e.getKey()));
}
items.sort((x, y) -> x.label.compareToIgnoreCase(y.label));
items.forEach(units::addItem);
} finally {
updating = false;
}
}
private void selectUnit(int u) {
updating = true;
for (int i = 0; i < units.getItemCount(); i++) {
if (units.getItemAt(i).unit == u) {
units.setSelectedIndex(i);
}
}
updating = false;
}
private void followSelection() {
Scene s = ctx.scene();
Node n = ctx.selection().primary();
if (s == null || !(n instanceof Strat st)) {
return;
}
StratEdits.Analysis a = StratEdits.analysis(s);
int u = a == null ? -1 : a.img().unitOf(st.code());
if (u >= 0) {
selectUnit(u);
show(u, st.code());
}
}
/** Shows a unit, scrolled to the row of STPC offset at (−1 = top). */
void show(int unit, int at) {
Scene s = ctx.scene();
model.clear();
if (s == null || unit < 0) {
listing = null;
return;
}
listing = StratEdits.listing(s, unit);
editedAt = new HashSet<>();
Project p = ctx.document().project();
int start = listing.start;
for (StratEdits.Constant c : StratEdits.records(p).constants()) {
if (c.unit() == unit) {
editedAt.add(start + c.at());
}
}
List<Row> rows = new ArrayList<>();
if ("Script".equals(mode.getSelectedItem())) {
StratPseudo ps = StratPseudo.of(listing);
List<List<StratListing.Line>> cs = ps.constants();
for (int i = 0; i < ps.lines.size(); i++) {
StratPseudo.Line l = ps.lines.get(i);
rows.add(new Row(" ".repeat(l.depth()) + l.text(), l.offset(), cs.get(i), null));
}
} else {
for (StratListing.Line l : listing.lines) {
rows.add(new Row(l.render(), l.offset(), l.constAt() >= 0 ? List.of(l) : List.of(), l));
}
}
rows.forEach(model::addElement);
int sel = -1;
if (at >= 0) {
for (int i = 0; i < rows.size(); i++) {
if (rows.get(i).offset == at) {
sel = i;
break;
}
}
}
if (sel >= 0) {
list.setSelectedIndex(sel);
int vis = Math.min(model.size() - 1, sel + list.getVisibleRowCount());
list.ensureIndexIsVisible(vis);
list.ensureIndexIsVisible(sel);
}
int nConst = (int) listing.lines.stream().filter(l -> l.constAt() >= 0).count();
status.setText(String.join(", ", listing.strats.values()) + " · unit " + unit + " · "
+ (listing.end - listing.start) / 4 + " words · " + nConst + " editable constants"
+ (editedAt.isEmpty() ? "" : " · " + editedAt.size() + " edited")
+ (StratEdits.isCustomUnit(s, unit) ? " · custom type" : "")
+ " · double-click a line to edit its value");
describeUnit();
}
private void describeUnit() {
Scene s = ctx.scene();
if (listing == null || s == null) {
info.setText("");
return;
}
StringBuilder b = new StringBuilder();
b.append("Strats in this unit: ")
.append(String.join(", ", listing.strats.values()))
.append('\n');
if (!listing.aliases.isEmpty()) {
b.append("\nParameters (from the object schema):\n");
listing.aliases.forEach((i, n) -> b.append(" v")
.append(i)
.append(" = ")
.append(n)
.append(" (")
.append(uses("\\b" + Pattern.quote(n) + "\\b"))
.append(" uses)\n"));
}
b.append("\nReferenced from other code (spawned / addressed by):\n");
StratEdits.Analysis a = StratEdits.analysis(s);
Map<String, Integer> by = new LinkedHashMap<>();
for (StratCode.Site site : a.code().addrSites) {
int from = a.img().unitOf(site.offset());
if (a.img().unitOf(site.value()) == listing.unit && from != listing.unit && from >= 0) {
by.merge(StratEdits.unitName(s, from) + " (unit " + from + ")", 1, Integer::sum);
}
}
if (by.isEmpty()) {
b.append(" – (only placed in the level)\n");
}
by.forEach(
(k, v) -> b.append(" ").append(k).append(v > 1 ? " ×" + v : "").append('\n'));
b.append("\nPlaced instances: ");
int n = 0;
for (Strat st : s.strats()) {
n += a.img().unitOf(st.code()) == listing.unit ? 1 : 0;
}
b.append(n).append('\n');
b.append("\nSelect a line for help on its instruction.");
info.setText(b.toString());
info.setCaretPosition(0);
}
private int uses(String regex) {
Pattern p = Pattern.compile(regex);
int n = 0;
for (int i = 0; i < model.size(); i++) {
n += p.matcher(model.get(i).text).find() ? 1 : 0;
}
return n;
}
private static final Pattern WORD = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
private void describe(Row r) {
if (r == null) {
return;
}
StringBuilder b = new StringBuilder();
Set<String> seen = new HashSet<>();
Matcher m = WORD.matcher(r.text.replaceAll("\"[^\"]*\"", "").replaceAll("//.*|;.*", ""));
OpTable t = OpTable.get();
while (m.find()) {
String w = m.group();
if (!seen.add(w)) {
continue;
}
OpTable.Op op = t.byName(w);
if (op != null && !w.startsWith("v")) {
b.append(op.name())
.append(String.format(Locale.ROOT, " (opcode 0x%02x", op.code()))
.append(op.pops() >= 0 ? ", pops " + op.pops() : "")
.append(op.pushes() >= 0 ? ", pushes " + op.pushes() : "")
.append(", ")
.append(op.confidence())
.append(" confidence")
.append(op.ref().equals("-") || op.ref().isEmpty() ? "" : ", Argonaut name " + op.ref())
.append(")\n")
.append(op.doc())
.append("\n\n");
}
OpTable.Named pr = t.propByName(w);
if (pr != null && r.text.contains("." + w) || pr != null && r.text.matches(".*[Pp]rop " + w + "\\b.*")) {
b.append("property ")
.append(w)
.append(" (")
.append(pr.id())
.append(pr.readOnly() ? ", read-only" : "")
.append(")\n")
.append(pr.doc())
.append("\n\n");
}
}
for (StratListing.Line c : r.consts) {
b.append("Constant ")
.append(c.constKind() == null ? "" : c.constKind().name().toLowerCase(Locale.ROOT))
.append(": ")
.append(ConstantDialog.describe(c.value(), c.constKind()))
.append(editedAt.contains(c.constAt()) ? " (edited)" : "")
.append('\n');
}
if (b.isEmpty()) {
describeUnit();
return;
}
info.setText(b.toString());
info.setCaretPosition(0);
}
private void find(int dir) {
String q = search.getText().trim().toLowerCase(Locale.ROOT);
if (q.isEmpty() || model.isEmpty()) {
return;
}
int n = model.size();
int from = list.getSelectedIndex();
for (int k = 1; k <= n; k++) {
int i = Math.floorMod(from + dir * k, n);
if (model.get(i).text.toLowerCase(Locale.ROOT).contains(q)) {
list.setSelectedIndex(i);
list.ensureIndexIsVisible(i);
status.setText("'" + search.getText().trim() + "': "
+ uses(Pattern.quote(search.getText().trim())) + " lines");
return;
}
}
status.setText("'" + search.getText().trim() + "' not found in this unit");
}
/** Uses of the selected line's main operand inside the unit (variable, property, label …). */
private void references() {
Row r = list.getSelectedValue();
if (r == null) {
return;
}
String text =
r.text.replaceAll("\"[^\"]*\"", "").replaceAll("//.*|;.*", "").trim();
Matcher m = WORD.matcher(text);
String best = null;
while (m.find()) {
String w = m.group();
if (OpTable.get().byName(w) == null
&& !w.matches(
"if|goto|loop|else|strat|push|self|player|parent|camera|target|target2|boss|dialog|return|yield|call")) {
best = w;
break;
}
}
if (best == null) {
status.setText("No operand to look up on this line");
return;
}
search.setText(best);
Pattern p = Pattern.compile("\\b" + Pattern.quote(best) + "\\b");
StringBuilder b = new StringBuilder("Uses of " + best + " in this unit:\n");
int n = 0;
for (int i = 0; i < model.size(); i++) {
if (p.matcher(model.get(i).text).find()) {
n++;
if (n <= 200) {
b.append(String.format(
Locale.ROOT, "%5d %s%n", i + 1, model.get(i).text.trim()));
}
}
}
info.setText(b.toString());
info.setCaretPosition(0);
status.setText(best + ": " + n + " lines (Find → Next to step through them)");
}
private void editSelected() {
Row r = list.getSelectedValue();
if (r == null || listing == null || ctx.document() == null) {
return;
}
if (r.consts.isEmpty()) {
status.setText(
"This line has no editable constant (numbers pushed with push / pushShort, sounds, textures, texts)");
return;
}
if (StratEdits.isCustomUnit(ctx.scene(), listing.unit)) {
status.setText("Custom types are edited through their source: clone it again with the changed script");
return;
}
Map<StratListing.Line, Integer> changes = ConstantDialog.show(ctx, r.consts);
if (changes == null || changes.isEmpty()) {
return;
}
Project p = ctx.document().project();
int unit = listing.unit;
List<Command> cmds = new ArrayList<>();
for (var e : changes.entrySet()) {
cmds.add(new ConstantEdit(p, unit, e.getKey(), e.getValue()));
}
for (Command c : cmds) {
try {
ctx.execute(c);
} catch (RuntimeException ex) {
ctx.error("Edit script constant", ex);
return;
}
}
ctx.status("Script constant changed – rebuild/play to see the effect");
}
/** Undoable constant change (reference + scene + project record). */
static final class ConstantEdit implements Command {
private final Project p;
private final int unit, constAt, old, value;
private final boolean isShort;
private final String text;
ConstantEdit(Project p, int unit, StratListing.Line l, int value) {
this.p = p;
this.unit = unit;
this.constAt = l.constAt();
this.isShort = l.constShort();
this.old = l.value();
this.value = value;
this.text = l.text();
}
@Override
public String label() {
return "Edit script constant (" + text + ")";
}
@Override
public void apply(Scene s) {
StratEdits.setConstant(p, unit, constAt, isShort, old, value);
}
@Override
public void revert(Scene s) {
StratEdits.setConstant(p, unit, constAt, isShort, value, old);
}
}
private final class RowRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<?> l, Object v, int i, boolean sel, boolean focus) {
Row r = (Row) v;
super.getListCellRendererComponent(l, r.text.isEmpty() ? " " : r.text, i, sel, focus);
boolean edited = r.consts.stream().anyMatch(c -> editedAt.contains(c.constAt()));
if (!sel) {
String t = r.text.trim();
if (edited) {
setForeground(new Color(0xd0, 0x70, 0x00));
} else if (t.startsWith("//") || t.startsWith(";")) {
setForeground(Color.GRAY);
} else if (t.startsWith("strat ") || t.endsWith(":") && !t.contains(" ")) {
setForeground(new Color(0x2a, 0x7a, 0xd8));
} else if (t.startsWith(".")) {
setForeground(new Color(0x80, 0x80, 0x80));
}
}
setFont(r.text.trim().startsWith("strat ") ? getFont().deriveFont(Font.BOLD) : getFont());
setToolTipText(
r.consts.isEmpty()
? null
: "Double-click to edit " + r.consts.size() + " constant"
+ (r.consts.size() == 1 ? "" : "s"));
return this;
}
}
}
@@ -0,0 +1,731 @@
package com.eliasmueller.llamamod.editor.tools.strats;
import com.eliasmueller.llamamod.editor.io.Bases;
import com.eliasmueller.llamamod.editor.io.Project;
import com.eliasmueller.llamamod.editor.io.ProjectIO;
import com.eliasmueller.llamamod.editor.model.RawChunk;
import com.eliasmueller.llamamod.editor.model.Scene;
import com.eliasmueller.llamamod.editor.model.SceneEvent;
import com.eliasmueller.llamamod.editor.model.Strat;
import com.eliasmueller.llamamod.editor.model.StratCatalog;
import com.eliasmueller.llamamod.editor.tools.objects.ParamSchema;
import com.eliasmueller.llamamod.editor.tools.objects.Schemas;
import com.eliasmueller.llamamod.format.Bytes;
import com.eliasmueller.llamamod.format.stpcmerge.StpcImage;
import com.eliasmueller.llamamod.format.stpcmerge.StratCode;
import com.eliasmueller.llamamod.format.stpcmerge.StratOps;
import com.eliasmueller.llamamod.format.strat.StratAssembler;
import com.eliasmueller.llamamod.format.strat.StratListing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.WeakHashMap;
/**
* Script edits of a project (level.json key {@value #KEY}, notes/strat-language.md): constant
* patches of existing code units and custom strat types (source text, appended as new units at
* the end of the STPC code region). Like imported types they are applied to the reference and the
* current scene on load, so the scene diff keeps describing only placements and other edits.
* Load order: constants of base-level units, imports (CrossImport), constants of imported units,
* custom types.
*/
public final class StratEdits {
public static final String KEY = "stratEdits";
/** Patch of one operand word: unit index, byte offset in the unit, old/new value, short = high 16 bits. */
public record Constant(int unit, String unitName, int at, int old, int value, boolean isShort) {
Map<String, Object> json() {
Map<String, Object> m = new LinkedHashMap<>();
m.put("unit", (long) unit);
m.put("name", unitName);
m.put("at", (long) at);
m.put("old", (long) old);
m.put("new", (long) value);
if (isShort) {
m.put("short", true);
}
return m;
}
static Constant of(Map<?, ?> m) {
return new Constant(
num(m, "unit"),
String.valueOf(m.get("name")),
num(m, "at"),
num(m, "old"),
num(m, "new"),
Boolean.TRUE.equals(m.get("short")));
}
}
/**
* A custom type: name, source text, the entry label in the source, type fields (MAP record
* prototype), and where it came from.
*/
public record CustomType(
String name, String basedOn, String entryLabel, String source, StratCatalog.Variant proto) {
Map<String, Object> json() {
Map<String, Object> m = new LinkedHashMap<>();
m.put("name", name);
m.put("basedOn", basedOn == null ? "" : basedOn);
m.put("entry", entryLabel);
m.put("source", source);
Map<String, Object> p = new LinkedHashMap<>();
p.put("nVars", (long) proto.nVars());
p.put("slots", (long) proto.slots());
p.put("typeFlags", (long) proto.typeFlags());
p.put("extra", (long) proto.extra());
p.put("flags", (long) proto.flags());
List<Object> prm = new ArrayList<>();
for (int v : proto.params()) {
prm.add((long) v);
}
p.put("params", prm);
List<Object> ch = new ArrayList<>();
for (int[] n : proto.chain()) {
List<Object> r = new ArrayList<>();
for (int v : n) {
r.add((long) v);
}
ch.add(r);
}
p.put("chain", ch);
m.put("proto", p);
return m;
}
static CustomType of(Map<?, ?> m) {
Map<?, ?> p = (Map<?, ?>) m.get("proto");
List<?> prm = (List<?>) p.get("params");
int[] params = new int[prm.size()];
for (int i = 0; i < params.length; i++) {
params[i] = ((Number) prm.get(i)).intValue();
}
List<int[]> chain = new ArrayList<>();
if (p.get("chain") instanceof List<?> ch) {
for (Object o : ch) {
List<?> r = (List<?>) o;
int[] n = new int[r.size()];
for (int i = 0; i < n.length; i++) {
n[i] = ((Number) r.get(i)).intValue();
}
chain.add(n);
}
}
StratCatalog.Variant v = new StratCatalog.Variant(
0,
num(p, "nVars"),
num(p, "slots"),
num(p, "typeFlags"),
num(p, "extra"),
num(p, "flags"),
0,
0,
0,
params,
List.copyOf(chain),
"custom");
return new CustomType(
(String) m.get("name"),
(String) m.get("basedOn"),
(String) m.get("entry"),
(String) m.get("source"),
v);
}
}
/** Edits of a project, in order. */
public record Records(List<Constant> constants, List<CustomType> types) {
static Records of(Map<String, Object> extra) {
List<Constant> c = new ArrayList<>();
List<CustomType> t = new ArrayList<>();
if (extra.get(KEY) instanceof Map<?, ?> m) {
if (m.get("constants") instanceof List<?> l) {
l.forEach(o -> c.add(Constant.of((Map<?, ?>) o)));
}
if (m.get("types") instanceof List<?> l) {
l.forEach(o -> t.add(CustomType.of((Map<?, ?>) o)));
}
}
return new Records(c, t);
}
void store(Map<String, Object> extra, Map<String, Object> session) {
if (constants.isEmpty() && types.isEmpty()) {
extra.remove(KEY);
return;
}
Map<String, Object> m = new LinkedHashMap<>();
List<Object> c = new ArrayList<>();
constants.forEach(x -> c.add(x.json()));
List<Object> t = new ArrayList<>();
types.forEach(x -> t.add(x.json()));
m.put("constants", c);
m.put("types", t);
if (session != null) {
m.put("session", session);
}
extra.put(KEY, m);
}
}
private StratEdits() {}
/** Units of the base level (before imports) and of custom types per scene. */
private static final class State {
int baseUnits = -1;
boolean edited;
int firstCustom = -1;
final List<Integer> customEntries = new ArrayList<>();
}
private static final Map<Scene, State> STATE = Collections.synchronizedMap(new WeakHashMap<>());
private static State state(Scene s) {
return STATE.computeIfAbsent(s, k -> new State());
}
static {
ProjectIO.addLoadHook(new ProjectIO.LoadHook() {
@Override
public void beforeChanges(Map<String, Object> json, Bases cd, Scene ref, Scene cur, List<String> w) {
beforeImports(json, ref, cur, w);
}
@Override
public int order() {
return -10;
}
});
ProjectIO.addLoadHook(new ProjectIO.LoadHook() {
@Override
public void beforeChanges(Map<String, Object> json, Bases cd, Scene ref, Scene cur, List<String> w) {
afterImports(json, cd, ref, cur, w);
}
@Override
public int order() {
return 10;
}
});
}
/** Makes sure the load hooks are registered. */
public static void init() {}
private static int num(Map<?, ?> m, String k) {
return ((Number) m.get(k)).intValue();
}
// ------------------------------------------------------------------ load
private static void beforeImports(Map<String, Object> json, Scene ref, Scene cur, List<String> warnings) {
Records r = Records.of(json);
int base = units(cur);
state(ref).baseUnits = base;
state(cur).baseUnits = base;
for (Constant c : r.constants) {
if (c.unit < base) {
applyLoaded(c, ref, cur, warnings);
}
}
}
private static void afterImports(Map<String, Object> json, Bases cd, Scene ref, Scene cur, List<String> warnings) {
Records r = Records.of(json);
int base = state(cur).baseUnits;
for (Constant c : r.constants) {
if (c.unit >= base && (state(cur).firstCustom < 0 || c.unit < state(cur).firstCustom)) {
applyLoaded(c, ref, cur, warnings);
}
}
if (r.types.isEmpty()) {
return;
}
int startBefore = codeEnd(cur);
for (CustomType t : r.types) {
try {
appendType(ref, t, false);
appendType(cur, t, false);
} catch (RuntimeException e) {
warnings.add("custom strat type " + t.name + " could not be assembled: " + e.getMessage());
}
}
if (json.get(KEY) instanceof Map<?, ?> m && m.get("session") instanceof Map<?, ?> ses) {
int cs = num(ses, "start"), ce = num(ses, "end");
int b = startBefore - cs;
if (b != 0) {
remapCodes(json, cs, ce, b);
}
}
}
private static void applyLoaded(Constant c, Scene ref, Scene cur, List<String> warnings) {
String e = check(ref, c);
if (e != null) {
warnings.add(
"script constant edit skipped (" + c.unitName + " +0x" + Integer.toHexString(c.at) + "): " + e);
return;
}
for (Scene s : List.of(ref, cur)) {
chunk(s).setRaw("data", patched(s, c, c.value));
state(s).edited = true;
}
}
/**
* Code offsets in the stored diff were written with the custom units at [cs, ce) followed by b
* bytes of later imports; on load the imports come first.
*/
@SuppressWarnings("unchecked")
private static void remapCodes(Map<String, Object> json, int cs, int ce, int b) {
if (!(json.get("nodes") instanceof Map<?, ?> nodes) || !(nodes.get("strat") instanceof Map<?, ?> st)) {
return;
}
List<Map<String, Object>> recs = new ArrayList<>();
if (st.get("added") instanceof List<?> l) {
l.forEach(o -> recs.add((Map<String, Object>) o));
}
if (st.get("changed") instanceof Map<?, ?> ch) {
ch.values().forEach(o -> recs.add((Map<String, Object>) o));
}
for (Map<String, Object> m : recs) {
if (m.get("code") instanceof Number n) {
int v = n.intValue();
if (v >= cs && v < ce) {
m.put("code", (long) (v + b));
} else if (v >= ce && v < ce + b) {
m.put("code", (long) (v - (ce - cs)));
}
}
}
}
// ------------------------------------------------------------------ analysis
/** Disassembly of a scene's STPC (cached per chunk data array). */
public record Analysis(StpcImage img, StratCode code, Set<Integer> entries) {
public int unitOfCode(int code) {
return img.unitOf(code);
}
}
private record Cached(byte[] data, Analysis a) {}
private static final Map<Scene, Cached> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
static RawChunk chunk(Scene s) {
for (RawChunk c : s.chunks()) {
if (c.tag().equals("STPC")) {
return c;
}
}
return null;
}
public static Analysis analysis(Scene s) {
RawChunk c = chunk(s);
if (c == null || c.data() == null) {
return null;
}
Cached k = CACHE.get(s);
if (k != null && k.data == c.data()) {
return k.a;
}
StpcImage img = new StpcImage(c.data(), s.settings().wfpc());
Set<Integer> roots = new TreeSet<>();
for (Strat x : s.strats()) {
roots.add(x.code());
}
roots.addAll(s.catalog().names().keySet());
roots.removeIf(o -> o < img.codeStart || img.unitOf(o) < 0);
Analysis a = new Analysis(img, StratCode.analyse(img, roots), roots);
CACHE.put(s, new Cached(c.data(), a));
return a;
}
/** Listing of a unit with parameter names from the object schemas. */
public static StratListing listing(Scene s, int unit) {
Analysis a = analysis(s);
StratCatalog cat = s.catalog();
return StratListing.of(a.img, a.code, unit, a.entries, new StratListing.Names() {
@Override
public String param(int code, int i) {
String n = cat.nameOf(code);
if (n == null) {
n = com.eliasmueller.llamamod.format.Strats.name(a.img.data, code, a.img.codeStart);
}
ParamSchema.TypeSchema ts = Schemas.of(n);
ParamSchema.ParamDef d = ts == null ? null : ts.param(i);
return d == null ? null : ident(d.name());
}
@Override
public String entry(int code) {
return cat.nameOf(code);
}
});
}
static String ident(String label) {
StringBuilder b = new StringBuilder();
boolean up = false;
for (char c : label.toCharArray()) {
if (Character.isLetterOrDigit(c) && c < 128) {
b.append(up && !b.isEmpty() ? Character.toUpperCase(c) : b.isEmpty() ? Character.toLowerCase(c) : c);
up = false;
} else {
up = true;
}
}
if (b.isEmpty() || Character.isDigit(b.charAt(0))) {
return null;
}
return b.toString();
}
static int units(Scene s) {
Analysis a = analysis(s);
return a == null ? 0 : a.img.units.size();
}
static int codeEnd(Scene s) {
return chunk(s).data().length;
}
/** First unit index of the custom types in this scene (−1 = none). */
public static int firstCustomUnit(Scene s) {
return state(s).firstCustom;
}
/** Whether constant edits or custom types were applied to this scene. */
public static boolean hasEdits(Scene s) {
return state(s).edited || state(s).firstCustom >= 0;
}
public static boolean isCustomUnit(Scene s, int unit) {
int f = state(s).firstCustom;
return f >= 0 && unit >= f && unit < f + state(s).customEntries.size();
}
/** Index into the project's custom types of a unit, or −1. */
public static int customIndex(Scene s, int unit) {
return isCustomUnit(s, unit) ? unit - state(s).firstCustom : -1;
}
// ------------------------------------------------------------------ constants
/** Why a constant edit does not fit the scene's code (null = fits). */
static String check(Scene s, Constant c) {
Analysis a = analysis(s);
if (a == null || c.unit < 0 || c.unit >= a.img.units.size()) {
return "unit " + c.unit + " does not exist";
}
StpcImage.Span u = a.img.units.get(c.unit);
int o = u.start() + c.at;
int ins = c.isShort ? o : o - 4;
if (o < u.start() || o + 4 > u.end() || a.code.length(ins) == 0) {
return "no instruction there";
}
int op = a.img.u32(ins) & 0xffff;
if (c.isShort
? op != StratOps.PUSH_SHORT
: op != StratOps.PUSH_IMM
&& op != StratOps.PUSH_SOUND
&& op != StratOps.TEXT_A
&& op != StratOps.TEXT_B) {
return "the instruction is not a constant";
}
int cur = c.isShort ? a.img.u32(o) >> 16 : a.img.u32(o);
if (cur != c.old) {
return "the code differs (found " + cur + ", expected " + c.old + ")";
}
if (c.isShort && (c.value < Short.MIN_VALUE || c.value > Short.MAX_VALUE)) {
return "value out of the 16-bit range";
}
return null;
}
static byte[] patched(Scene s, Constant c, int value) {
Analysis a = analysis(s);
byte[] d = a.img.data.clone();
int o = a.img.units.get(c.unit).start() + c.at;
int w = c.isShort ? (value << 16) | (Bytes.i32(d, o) & 0xffff) : value;
d[o] = (byte) w;
d[o + 1] = (byte) (w >> 8);
d[o + 2] = (byte) (w >> 16);
d[o + 3] = (byte) (w >> 24);
return d;
}
/**
* Sets the constant word at STPC offset constAt (current value cur) of a unit to value in
* reference and scene and records it (merged with an earlier edit of the same word).
*/
static void setConstant(Project p, int unit, int constAt, boolean isShort, int cur, int value) {
Scene sc = p.scene();
Analysis a = analysis(sc);
StpcImage.Span u = a.img.units.get(unit);
int at = constAt - u.start();
String name = unitName(sc, unit);
Records before = Records.of(p.extra());
List<Constant> cs = new ArrayList<>(before.constants);
int orig = cur;
for (int i = 0; i < cs.size(); i++) {
Constant c = cs.get(i);
if (c.unit == unit && c.at == at && c.isShort == isShort) {
orig = c.old;
cs.remove(i);
break;
}
}
Constant c0 = new Constant(unit, name, at, cur, value, isShort);
String e = check(sc, c0);
if (e != null) {
throw new IllegalArgumentException(e);
}
for (Scene s : List.of(p.reference(), sc)) {
RawChunk ch = chunk(s);
byte[] old = ch.data();
ch.setRaw("data", patched(s, c0, value));
state(s).edited = true;
if (s == sc) {
s.fire(SceneEvent.changed(ch, ch.prop("data"), old));
}
}
if (value != orig) {
cs.add(new Constant(unit, name, at, orig, value, isShort));
}
new Records(cs, before.types).store(p.extra(), session(sc));
}
static String unitName(Scene s, int unit) {
Analysis a = analysis(s);
List<Integer> sites = a.img.nameSites(a.img.units.get(unit));
return sites.isEmpty()
? "unit " + unit
: Bytes.cstr(a.img.data, (int) (sites.get(0) + 12L + a.img.u32(sites.get(0) + 8)));
}
// ------------------------------------------------------------------ custom types
/** Result of assembling a custom type against a scene (not yet applied). */
public record Built(byte[] stpc, int base, int entry, StratAssembler.Result asm, List<String> problems) {}
/** Assembles a type's source as a new unit appended to the scene's code and verifies it. */
public static Built build(Scene s, CustomType t) {
Analysis a = analysis(s);
byte[] old = a.img.data;
int base = old.length + 4;
StratAssembler.Result r = StratAssembler.assemble(t.source, StratListing.layout(a.img, base));
Integer e = r.labels().get(t.entryLabel);
if (e == null) {
throw new IllegalArgumentException("entry label " + t.entryLabel + " not found in the source");
}
byte[] words = r.bytes();
byte[] d = new byte[old.length + 4 + words.length];
System.arraycopy(old, 0, d, 0, old.length);
int n = words.length / 4;
d[old.length] = (byte) n;
d[old.length + 1] = (byte) (n >> 8);
d[old.length + 2] = (byte) (n >> 16);
d[old.length + 3] = (byte) (n >> 24);
System.arraycopy(words, 0, d, old.length + 4, words.length);
int nu = Bytes.i32(d, a.img.codeStart) + 1;
d[a.img.codeStart] = (byte) nu;
d[a.img.codeStart + 1] = (byte) (nu >> 8);
d[a.img.codeStart + 2] = (byte) (nu >> 16);
d[a.img.codeStart + 3] = (byte) (nu >> 24);
List<String> problems = verify(d, s.settings().wfpc(), a.entries, base, base + e, r);
return new Built(d, base, base + e, r, problems);
}
/**
* Re-disassembly of the merged code: no decode errors, the new unit's listing assembles back
* to the same words, and every address operand of the new unit hits a model, an animation set
* or code.
*/
static List<String> verify(byte[] d, int wfpc, Set<Integer> entries, int base, int entry, StratAssembler.Result r) {
List<String> out = new ArrayList<>();
StpcImage img = new StpcImage(d, wfpc);
Set<Integer> roots = new TreeSet<>(entries);
roots.add(entry);
for (int v : r.strats().values()) {
roots.add(base + v);
}
StratCode c = StratCode.analyse(img, roots);
int u = img.units.size() - 1;
c.errors.stream().limit(5).forEach(out::add);
if (c.length(entry) == 0) {
out.add("the entry is not decoded as code");
}
for (StratCode.Site site : c.addrSites) {
if (img.unitOf(site.offset()) != u) {
continue;
}
int v = site.value();
boolean ok = v < 0 || img.modelAt(v) >= 0 || img.animAt(v) >= 0 || img.unitOf(v) >= 0;
if (!ok) {
out.add("address operand at +0x" + Integer.toHexString(site.offset() - base) + " points nowhere (0x"
+ Integer.toHexString(v) + ")");
}
}
StratListing li = StratListing.of(img, c, u, roots, StratListing.Names.NONE);
int[] back = StratAssembler.assemble(li.text(), StratListing.layout(img, base))
.words();
if (!java.util.Arrays.equals(back, r.words())) {
out.add("re-disassembly does not reproduce the assembled code");
}
return out;
}
/** Appends a custom type to a scene (no events unless fire) and adds it to the catalog. */
static int appendType(Scene s, CustomType t, boolean fire) {
Built b = build(s, t);
if (!b.problems.isEmpty()) {
throw new IllegalArgumentException(String.join("; ", b.problems));
}
State st = state(s);
int unit = units(s);
Runnable r = () -> {
chunk(s).setRaw("data", b.stpc);
StratCatalog.Variant p = t.proto;
s.catalog()
.add(
t.name,
new StratCatalog.Variant(
b.entry,
p.nVars(),
p.slots(),
p.typeFlags(),
p.extra(),
p.flags(),
p.rx(),
p.ry(),
p.rz(),
p.params(),
p.chain(),
"custom"));
for (var e : b.asm.strats().entrySet()) {
s.catalog().addName(b.base + e.getValue(), e.getKey());
}
};
if (fire) {
s.bulk(r);
} else {
r.run();
}
if (st.firstCustom < 0) {
st.firstCustom = unit;
}
st.customEntries.add(b.entry);
return b.entry;
}
/** Adds a custom type to an open project (reference + scene) and records it. Returns the entry code. */
public static int addType(Project p, CustomType t) {
appendType(p.reference(), t, false);
int code = appendType(p.scene(), t, true);
Records r = Records.of(p.extra());
List<CustomType> ts = new ArrayList<>(r.types);
ts.add(t);
new Records(r.constants, ts).store(p.extra(), session(p.scene()));
return code;
}
/** Current position of the custom units (for remapping codes on load), or null. */
static Map<String, Object> session(Scene s) {
State st = state(s);
Analysis a = analysis(s);
if (st.firstCustom < 0 || a == null || st.firstCustom >= a.img.units.size()) {
return null;
}
int last = st.firstCustom + st.customEntries.size() - 1;
Map<String, Object> m = new LinkedHashMap<>();
m.put("start", (long) (a.img.units.get(st.firstCustom).start() - 4));
m.put("end", (long)
a.img.units.get(Math.min(last, a.img.units.size() - 1)).end());
return m;
}
/** Refreshes the stored custom-unit position (after imports moved nothing else, they come after). */
static void updateSession(Project p) {
Records r = Records.of(p.extra());
if (!r.types.isEmpty()) {
r.store(p.extra(), session(p.scene()));
}
}
public static Records records(Project p) {
return Records.of(p.extra());
}
/**
* A custom type copied from a placeable type: the source of its unit with the strat renamed,
* the type fields of its first variant.
*/
public static CustomType cloneOf(Scene s, String type, String newName) {
StratCatalog.Type t = s.catalog().type(type);
if (t == null) {
throw new IllegalArgumentException("unknown type " + type);
}
StratCatalog.Variant v = t.variant(0);
Analysis a = analysis(s);
int unit = a.img.unitOf(v.code());
StratListing li = listing(s, unit);
String entry = li.labels.get(v.code());
String oldName = null;
for (var e : li.strats.entrySet()) {
if (StratListing.ident(e.getValue()).equals(entry) || e.getKey() == v.code()) {
oldName = e.getValue();
}
}
String src = li.text();
String newEntry = entry;
if (oldName != null && entry != null && entry.equals(identOf(oldName))) {
src = rename(src, oldName, newName);
newEntry = identOf(newName);
}
if (entry == null) {
throw new IllegalArgumentException(type + " has no entry label");
}
return new CustomType(newName, type, newEntry, src, v);
}
static String identOf(String name) {
StringBuilder b = new StringBuilder();
for (char c : name.toCharArray()) {
b.append(Character.isLetterOrDigit(c) && c < 128 ? c : '_');
}
return b.toString();
}
/** Renames a strat in a listing: its label, references to it and its name string. */
static String rename(String src, String oldName, String newName) {
String ol = java.util.regex.Pattern.quote(identOf(oldName));
String q = java.util.regex.Pattern.quote("\"" + oldName + "\"");
StringBuilder b = new StringBuilder();
for (String line : src.split("\n", -1)) {
String t = line.trim();
if (t.startsWith(".string") || t.startsWith("debugName")) {
line = line.replaceAll(
q + "(?=\\s*(,|$|;))", java.util.regex.Matcher.quoteReplacement("\"" + newName + "\""));
} else if (!t.startsWith(";")) {
line = line.replaceAll(
"(?<![\\w\"])" + ol + "(?![\\w\"])",
java.util.regex.Matcher.quoteReplacement(identOf(newName)));
}
b.append(line).append('\n');
}
return b.substring(0, b.length() - 1);
}
/** Custom type names of the project. */
public static List<String> customNames(Project p) {
return records(p).types.stream().map(CustomType::name).toList();
}
}
@@ -0,0 +1,133 @@
package com.eliasmueller.llamamod.editor.tools.strats;
import com.eliasmueller.llamamod.editor.model.Node;
import com.eliasmueller.llamamod.editor.model.RawChunk;
import com.eliasmueller.llamamod.editor.model.SceneEvent;
import com.eliasmueller.llamamod.editor.model.Strat;
import com.eliasmueller.llamamod.editor.ui.ActionSpec;
import com.eliasmueller.llamamod.editor.ui.Dock;
import com.eliasmueller.llamamod.editor.ui.EditorContext;
import com.eliasmueller.llamamod.editor.ui.EditorExtensions;
import com.eliasmueller.llamamod.editor.ui.EditorPlugin;
import com.eliasmueller.llamamod.editor.ui.Icons;
import com.eliasmueller.llamamod.editor.ui.InspectorSection;
import com.eliasmueller.llamamod.editor.ui.PanelSpec;
import com.eliasmueller.llamamod.editor.ui.Problem;
import com.eliasmueller.llamamod.editor.ui.Widgets;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.geom.Line2D;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
/** Script view and editing of strat code (notes/strat-language.md). */
public final class StratsEditorPlugin implements EditorPlugin {
static final String PANEL = "strats.script";
@Override
public void install(EditorExtensions x) {
StratEdits.init();
Icons.register("script", (g, c) -> {
g.setStroke(new BasicStroke(1.4f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g.draw(new Line2D.Double(5.5, 3.5, 2.5, 8));
g.draw(new Line2D.Double(2.5, 8, 5.5, 12.5));
g.draw(new Line2D.Double(10.5, 3.5, 13.5, 8));
g.draw(new Line2D.Double(13.5, 8, 10.5, 12.5));
g.draw(new Line2D.Double(9, 3, 7, 13));
});
x.panel(new PanelSpec(PANEL, "Script", "script", Dock.BOTTOM, 40, true, ScriptPanel::new));
x.action(ActionSpec.of(
"strats.show",
"View/Panels",
50,
40,
"Script",
"script",
"ctrl shift K",
c -> c.showPanel(PANEL))
.tip("Strat script of the selected object type"));
x.action(ActionSpec.of(
"strats.newType",
"Level",
42,
10,
"New object type from script…",
"script",
null,
c -> TypeDialog.showNew(c))
.enabledWhen(c -> c.document() != null)
.tip("Create a custom object type from strat source text"));
x.inspector(new ScriptSection());
x.check((scene, out) -> {
if (!StratEdits.hasEdits(scene)) {
return;
}
StratEdits.Analysis a = StratEdits.analysis(scene);
if (a != null && !a.code().errors.isEmpty()) {
out.add(Problem.error(
"Strat code does not disassemble cleanly: "
+ a.code().errors.get(0),
null));
}
});
x.onOpen(ctx -> ctx.addSceneListener(e -> {
if (ctx.document() != null
&& e.node() instanceof RawChunk c
&& c.tag().equals("STPC")
|| e.type() == SceneEvent.Type.RELOADED && ctx.document() != null) {
StratEdits.updateSession(ctx.document().project());
}
}));
}
/** Inspector: the object's script type, edited constants, button to the Script panel. */
private static final class ScriptSection implements InspectorSection {
@Override
public String title() {
return "Script";
}
@Override
public int order() {
return 30;
}
@Override
public boolean appliesTo(List<Node> sel) {
return sel.size() == 1 && sel.get(0) instanceof Strat s && s.scene() != null;
}
@Override
public JComponent create(EditorContext ctx, List<Node> sel) {
Strat s = (Strat) sel.get(0);
StratEdits.Analysis a = StratEdits.analysis(s.scene());
String info;
if (a == null || a.img().unitOf(s.code()) < 0) {
info = "No strat code at this object's code offset.";
} else {
int u = a.img().unitOf(s.code());
long edited = ctx.document() == null
? 0
: StratEdits.records(ctx.document().project()).constants().stream()
.filter(c -> c.unit() == u)
.count();
info = "<html><b>" + s.type() + "</b> · code unit " + u + " · "
+ (a.img().units.get(u).size() / 4) + " words"
+ (StratEdits.isCustomUnit(s.scene(), u) ? " · <b>custom type</b>" : "")
+ (edited > 0 ? " · " + edited + " edited constant" + (edited == 1 ? "" : "s") : "")
+ "</html>";
}
JPanel p = new JPanel(new BorderLayout(6, 0));
p.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
p.add(new JLabel(info, Icons.get("script", 16), JLabel.LEADING), BorderLayout.CENTER);
p.add(
Widgets.button(
"Open script", null, "Show and edit this type's strat script", () -> ctx.showPanel(PANEL)),
BorderLayout.EAST);
return p;
}
}
}
@@ -0,0 +1,223 @@
package com.eliasmueller.llamamod.editor.tools.strats;
import com.eliasmueller.llamamod.editor.io.Project;
import com.eliasmueller.llamamod.editor.model.Scene;
import com.eliasmueller.llamamod.editor.model.StratCatalog;
import com.eliasmueller.llamamod.editor.ui.EditorContext;
import com.eliasmueller.llamamod.format.strat.StratAssembler;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
/** Creates a custom object type from strat source (cloned from an existing type or from a template). */
final class TypeDialog {
private TypeDialog() {}
static final String TEMPLATE =
"""
; A minimal custom strat: shows a model and spins it.
.unit MyStrat
.string "MyStrat"
MyStrat:
debugName "MyStrat"
address model:0 ; first model of the level (pick one from another type's script)
setModel
spin:
push 4deg ; turn per frame
turnLeft
jmpYield spin ; wait one frame, loop
end
""";
static void showClone(EditorContext ctx, String type) {
if (ctx.document() == null) {
return;
}
if (type == null) {
JOptionPane.showMessageDialog(
ctx.frame(),
"This code unit has no placeable type to clone.",
"Clone type",
JOptionPane.INFORMATION_MESSAGE);
return;
}
Scene s = ctx.scene();
String name = freeName(s, type + "Custom");
StratEdits.CustomType t = StratEdits.cloneOf(s, type, name);
open(ctx, "Clone " + type + " as a new type", t.name(), t.source(), type, t.entryLabel(), true);
}
static void showNew(EditorContext ctx) {
if (ctx.document() == null) {
return;
}
Scene s = ctx.scene();
String base =
s.catalog().has("Coin") ? "Coin" : s.catalog().types().get(0).name();
open(ctx, "New object type from script", freeName(s, "MyStrat"), TEMPLATE, base, "MyStrat", false);
}
static String freeName(Scene s, String base) {
String n = base;
for (int k = 2; s.catalog().has(n) || s.catalog().names().containsValue(n); k++) {
n = base + k;
}
return n;
}
private static void open(
EditorContext ctx,
String title,
String name,
String source,
String fieldsFrom,
String entry,
boolean clone) {
JDialog d = new JDialog(ctx.frame(), title, true);
JTextField nameField = new JTextField(name, 18);
JTextField entryField = new JTextField(entry, 14);
JComboBox<String> from = new JComboBox<>();
ctx.scene().catalog().types().forEach(t -> from.addItem(t.name()));
from.setSelectedItem(fieldsFrom);
JTextArea text = new JTextArea(source, 30, 90);
text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
text.setTabSize(8);
JLabel result = new JLabel(" ");
JPanel top = new JPanel(new FlowLayout(FlowLayout.LEADING));
top.add(new JLabel("Name"));
top.add(nameField);
top.add(new JLabel("Entry label"));
top.add(entryField);
top.add(new JLabel("Type fields / parameters from"));
top.add(from);
JButton check = new JButton("Check");
JButton create = new JButton("Create type");
JButton cancel = new JButton("Cancel");
JPanel bottom = new JPanel(new BorderLayout());
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.TRAILING));
buttons.add(check);
buttons.add(create);
buttons.add(cancel);
result.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
bottom.add(result, BorderLayout.CENTER);
bottom.add(buttons, BorderLayout.EAST);
JPanel help = new JPanel(new BorderLayout());
help.add(
new JLabel(
"<html><font color='gray'>"
+ (clone
? "The source of the original type, renamed. Edit constants or code freely; references to models, animations and other code stay valid. "
: "")
+ "Syntax: notes/strat-language.md (manual chapter “Strat scripts”). The new code is appended to the level and verified by disassembling it again.</font></html>"));
JPanel north = new JPanel(new BorderLayout());
north.add(top, BorderLayout.NORTH);
north.add(help, BorderLayout.SOUTH);
d.getContentPane().add(north, BorderLayout.NORTH);
d.getContentPane().add(new JScrollPane(text), BorderLayout.CENTER);
d.getContentPane().add(bottom, BorderLayout.SOUTH);
Runnable[] verify = new Runnable[1];
StratEdits.CustomType[] ok = new StratEdits.CustomType[1];
verify[0] = () -> {
ok[0] = null;
String n = nameField.getText().trim();
Scene s = ctx.scene();
if (!n.matches("[A-Za-z_][A-Za-z0-9_]{0,40}")) {
result.setText("⚠ The name must be letters, digits and _ (max 40).");
return;
}
if (s.catalog().has(n) || s.catalog().names().containsValue(n)) {
result.setText("⚠ A type named " + n + " already exists.");
return;
}
String src = text.getText();
StratCatalog.Type ft = s.catalog().type((String) from.getSelectedItem());
if (ft == null) {
result.setText("⚠ Choose the type whose type fields and parameters the new type uses.");
return;
}
StratEdits.CustomType t = new StratEdits.CustomType(
n, clone ? fieldsFrom : null, entryField.getText().trim(), src, ft.variant(0));
try {
StratEdits.Built b = StratEdits.build(s, t);
if (!b.problems().isEmpty()) {
result.setText("⚠ " + String.join("; ", b.problems()));
return;
}
if (!b.asm().strats().isEmpty()
&& b.asm().strats().keySet().stream().noneMatch(k -> k.equals(t.entryLabel()))) {
result.setText(
"Note: the entry label is not a strat with debugName – the type gets its name only in the editor. "
+ b.asm().words().length + " words OK.");
} else {
result.setText("✓ " + b.asm().words().length + " words, verified. Entry " + t.entryLabel() + ".");
}
ok[0] = t;
} catch (StratAssembler.AsmException e) {
List<StratAssembler.Problem> ps = e.problems;
result.setText("⚠ " + ps.size() + " error" + (ps.size() == 1 ? "" : "s") + ": " + ps.get(0));
highlight(text, ps.get(0).line());
} catch (RuntimeException e) {
result.setText("⚠ " + e.getMessage());
}
};
check.addActionListener(e -> verify[0].run());
create.addActionListener(e -> {
verify[0].run();
if (ok[0] == null) {
return;
}
Project p = ctx.document().project();
try {
StratEdits.addType(p, ok[0]);
} catch (RuntimeException ex) {
ctx.error("Create object type", ex);
return;
}
ctx.status("Object type " + ok[0].name() + " created – place it from Assets → Objects");
d.dispose();
});
cancel.addActionListener(e -> d.dispose());
d.setMinimumSize(new Dimension(800, 500));
d.pack();
java.awt.Rectangle screen = ctx.frame().getGraphicsConfiguration().getBounds();
d.setSize(Math.min(1100, screen.width - 40), Math.min(760, screen.height - 60));
int at = text.getText().indexOf("\n" + entry + ":");
if (at >= 0) {
text.setCaretPosition(at + 1);
SwingUtilities.invokeLater(() -> {
try {
java.awt.geom.Rectangle2D r = text.modelToView2D(at + 1);
text.scrollRectToVisible(
new java.awt.Rectangle(0, (int) r.getY(), 10, text.getVisibleRect().height));
} catch (javax.swing.text.BadLocationException e) {
// keep the top
}
});
}
d.setLocationRelativeTo(ctx.frame());
d.setVisible(true);
}
private static void highlight(JTextArea t, int line) {
try {
int a = t.getLineStartOffset(line - 1), b = t.getLineEndOffset(line - 1);
t.requestFocusInWindow();
t.select(a, b);
} catch (javax.swing.text.BadLocationException | IllegalArgumentException e) {
// line outside the text
}
}
}
@@ -0,0 +1,118 @@
package com.eliasmueller.llamamod.format.strat;
import java.math.BigDecimal;
import java.util.Locale;
/** Number literals of the strat language: integers, hex, 20.12 fixed point ("1.5"), angles ("90deg"). */
public final class Num {
private Num() {}
/** 20.12 fixed point: 0x1000 = 1.0. */
public static final int ONE = 0x1000;
/** Full turn in angle units: 4096.0 in 20.12 (angles wrap at 24 bits). */
public static final int TURN = 0x1000000;
/** Shortest decimal (≤ 4 fraction digits) d with round(d·4096) = v. */
public static String fixed(int v) {
for (int digits = 1; digits <= 4; digits++) {
BigDecimal d =
BigDecimal.valueOf(v).divide(BigDecimal.valueOf(ONE), digits, java.math.RoundingMode.HALF_EVEN);
if (Math.round(d.doubleValue() * ONE) == v) {
String s = d.stripTrailingZeros().toPlainString();
return s.contains(".") ? s : s + ".0";
}
}
return Double.toString(v / (double) ONE);
}
/** Literal for a constant: fixed when it reads naturally as one, else decimal / hex. */
public static String literal(int v) {
if (Math.abs((long) v) >= 64 && v != Integer.MIN_VALUE) {
String f = fixed(v);
int dot = f.indexOf('.');
if (dot >= 0 && f.length() - dot - 1 <= 3 && Math.round(Double.parseDouble(f) * ONE) == v) {
return f;
}
}
return integer(v);
}
public static String integer(int v) {
return Math.abs((long) v) < 0x10000 ? Integer.toString(v) : hex(v);
}
public static String hex(int v) {
return v < 0 && v > -0x10000 ? "-0x" + Integer.toHexString(-v) : "0x" + Integer.toHexString(v);
}
/** Readable value for comments: "1.5 (6144)". */
public static String describe(int v) {
return fixed(v) + " = " + v;
}
public static String degrees(int v) {
double d = v * 360.0 / TURN;
String t = Math.abs(d) >= 1 || d == 0
? String.format(Locale.ROOT, "%.2f", d)
: String.format(Locale.ROOT, "%.4f", d);
return t.replaceAll("\\.?0+$", "") + "°";
}
/** "90deg" if that literal (≤ 3 decimals) parses back to exactly v, else null. */
public static String degreeLiteral(int v) {
for (int digits = 0; digits <= 3; digits++) {
String d = BigDecimal.valueOf(v * 360.0 / TURN)
.setScale(digits, java.math.RoundingMode.HALF_EVEN)
.stripTrailingZeros()
.toPlainString();
String lit = d + "deg";
Integer back = parse(lit);
if (back != null && back == v) {
return lit;
}
}
return null;
}
/**
* Parses a literal: decimal, 0x hex, -0x hex, fixed with a '.', "90deg" (angle units) or 'c'
* character literal. Returns null if it is not a number.
*/
public static Integer parse(String s) {
s = s.trim().replace("_", "");
if (s.isEmpty()) {
return null;
}
try {
if (s.endsWith("deg")) {
double d = Double.parseDouble(s.substring(0, s.length() - 3));
return (int) Math.round(d * (TURN / 360.0));
}
boolean neg = s.startsWith("-");
String b = neg || s.startsWith("+") ? s.substring(1) : s;
if (b.startsWith("0x") || b.startsWith("0X")) {
long v = Long.parseLong(b.substring(2), 16);
if (v > 0xffffffffL) {
return null;
}
return (int) (neg ? -v : v);
}
if (b.contains(".")) {
double d = Double.parseDouble(s);
long v = Math.round(d * ONE);
return v < Integer.MIN_VALUE || v > Integer.MAX_VALUE ? null : (int) v;
}
if (!b.chars().allMatch(Character::isDigit)) {
return null;
}
long v = Long.parseLong(s);
if (v < Integer.MIN_VALUE || v > 0xffffffffL) {
return null;
}
return (int) v;
} catch (NumberFormatException e) {
return null;
}
}
}
@@ -0,0 +1,173 @@
package com.eliasmueller.llamamod.format.strat;
import com.eliasmueller.llamamod.format.stpcmerge.StratOps;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Names and documentation of the strat VM opcodes, properties and globals (resources ops.tsv,
* props.tsv, globals.tsv; notes/strat-opcodes.md).
*/
public final class OpTable {
/** One opcode. pops/pushes = -1 when variable. */
public record Op(
int code, String name, String operands, int pops, int pushes, String doc, String confidence, String ref) {
public boolean isShort() {
return code < StratOps.PUSH_IMM;
}
}
/** A named property or global: id, name, documentation. */
public record Named(int id, String name, String doc, boolean readOnly) {}
private static final OpTable INSTANCE = new OpTable();
private final Op[] ops = new Op[StratOps.COUNT];
private final Map<String, Op> byName = new HashMap<>();
private final Map<Integer, Named> props = new HashMap<>();
private final Map<String, Named> propsByName = new HashMap<>();
private final Map<Integer, Named> globals = new HashMap<>();
private final Map<String, Named> globalsByName = new HashMap<>();
public static OpTable get() {
return INSTANCE;
}
private OpTable() {
for (String[] f : rows("ops.tsv")) {
int code = Integer.decode(f[0]);
if (code < 0 || code >= ops.length || ops[code] != null) {
continue;
}
String name = f[1];
if (name.isEmpty() || byName.containsKey(name.toLowerCase(Locale.ROOT))) {
name = String.format(Locale.ROOT, "op_%02x", code);
}
Op op = new Op(code, name, at(f, 2), count(at(f, 3)), count(at(f, 4)), at(f, 5), at(f, 6), at(f, 7));
ops[code] = op;
byName.put(name.toLowerCase(Locale.ROOT), op);
}
for (int i = 0; i < ops.length; i++) {
if (ops[i] == null) {
String name = String.format(Locale.ROOT, "op_%02x", i);
ops[i] = new Op(i, name, "", -1, -1, "", "none", "-");
byName.put(name, ops[i]);
}
}
named("props.tsv", props, propsByName, true);
named("globals.tsv", globals, globalsByName, false);
}
private static void named(String res, Map<Integer, Named> m, Map<String, Named> byName, boolean withAccess) {
for (String[] f : rows(res)) {
int id;
try {
id = Integer.decode(f[0]);
} catch (NumberFormatException e) {
continue;
}
String name = at(f, 1);
if (name.isEmpty() || !name.matches("[A-Za-z_][A-Za-z0-9_]*") || byName.containsKey(lc(name))) {
continue;
}
boolean ro = withAccess && at(f, 3).equalsIgnoreCase("no");
String doc = withAccess ? at(f, 4) : at(f, 2);
Named n = new Named(id, name, doc, ro);
m.put(id, n);
byName.put(lc(name), n);
}
}
private static String lc(String s) {
return s.toLowerCase(Locale.ROOT);
}
private static String at(String[] f, int i) {
return i < f.length ? f[i].trim() : "";
}
private static int count(String s) {
try {
return Integer.parseInt(s.trim());
} catch (NumberFormatException e) {
return -1;
}
}
private static List<String[]> rows(String res) {
List<String[]> out = new ArrayList<>();
try (InputStream in = OpTable.class.getResourceAsStream(res)) {
if (in == null) {
return out;
}
BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
String line;
while ((line = r.readLine()) != null) {
if (line.isBlank() || line.startsWith("#")) {
continue;
}
out.add(line.split("\t", -1));
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return out;
}
public Op op(int code) {
return ops[code];
}
/** Opcode by mnemonic (case-insensitive) or "op_XX", or null. */
public Op byName(String name) {
Op o = byName.get(lc(name));
if (o == null && lc(name).startsWith("op_")) {
try {
int c = Integer.parseInt(name.substring(3), 16);
return c >= 0 && c < ops.length ? ops[c] : null;
} catch (NumberFormatException e) {
return null;
}
}
return o;
}
public List<Op> all() {
return Collections.unmodifiableList(List.of(ops));
}
public Named prop(int id) {
return props.get(id);
}
public Named propByName(String name) {
return propsByName.get(lc(name));
}
public Named global(int id) {
return globals.get(id);
}
public Named globalByName(String name) {
return globalsByName.get(lc(name));
}
/** Number of opcodes with a real name and semantics (not op_XX). */
public int namedCount() {
int n = 0;
for (Op o : ops) {
n += o.name.startsWith("op_") ? 0 : 1;
}
return n;
}
}
@@ -0,0 +1,688 @@
package com.eliasmueller.llamamod.format.strat;
import com.eliasmueller.llamamod.format.stpcmerge.StratOps;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** Assembler for the strat language (notes/strat-language.md): text → words of one STPC code unit. */
public final class StratAssembler {
/**
* Where the unit goes and what absolute references resolve to: base = STPC offset of the
* unit's first word; models/anims/units = STPC offsets of model headers, animation sets, unit
* starts.
*/
public record Layout(int base, List<Integer> models, List<Integer> anims, List<Integer> units) {
public Layout withBase(int b) {
return new Layout(b, models, anims, units);
}
}
/** A problem at a source line (1-based). */
public record Problem(int line, String message) {
@Override
public String toString() {
return "line " + line + ": " + message;
}
}
public static final class AsmException extends RuntimeException {
public final List<Problem> problems;
AsmException(List<Problem> problems) {
super(
problems.size() == 1
? problems.get(0).toString()
: problems.size() + " errors, first: " + problems.get(0));
this.problems = List.copyOf(problems);
}
}
/** Assembly result: words, labels (byte offset in the unit), unit name, strat entry labels. */
public record Result(int[] words, Map<String, Integer> labels, String unitName, Map<String, Integer> strats) {
public byte[] bytes() {
byte[] b = new byte[words.length * 4];
for (int i = 0; i < words.length; i++) {
int v = words[i];
b[4 * i] = (byte) v;
b[4 * i + 1] = (byte) (v >> 8);
b[4 * i + 2] = (byte) (v >> 16);
b[4 * i + 3] = (byte) (v >> 24);
}
return b;
}
}
private record Stmt(int line, String head, List<String> args, String rest, int size, int offset) {}
private final OpTable ops = OpTable.get();
private final List<Problem> problems = new ArrayList<>();
private final Map<String, Integer> labels = new LinkedHashMap<>();
private final Map<String, Integer> aliases = new HashMap<>();
private final Map<String, Integer> eventBase = new HashMap<>();
private final Map<String, Integer> switchBase = new HashMap<>();
private final List<Integer> nameSites = new ArrayList<>();
private String unitName = "";
private final List<int[]> pendingNames = new ArrayList<>();
private final List<String> pendingNameText = new ArrayList<>();
private StratAssembler() {}
public static Result assemble(String text, Layout layout) {
return new StratAssembler().run(text, layout);
}
private void err(int line, String m) {
problems.add(new Problem(line, m));
}
private Result run(String text, Layout layout) {
List<Stmt> stmts = new ArrayList<>();
String[] src = text.split("\n", -1);
int off = 0;
for (int i = 0; i < src.length; i++) {
int ln = i + 1;
String s = stripComment(src[i]).trim();
while (true) {
int c = labelEnd(s);
if (c < 0) {
break;
}
String l = s.substring(0, c).trim();
define(ln, l, off);
s = s.substring(c + 1).trim();
}
if (s.isEmpty()) {
continue;
}
String head, rest;
int sp = indexOfSpace(s);
head = sp < 0 ? s : s.substring(0, sp);
rest = sp < 0 ? "" : s.substring(sp + 1).trim();
List<String> args = head.equals(".string") ? List.of(rest) : splitArgs(rest);
int size = size(ln, head, args, rest);
if (head.equals(".at") && args.size() == 2) {
Integer k = Num.parse(args.get(1));
if (k == null) {
err(ln, "bad offset " + args.get(1));
} else {
define(ln, args.get(0), off + k);
}
}
if (head.equals(".alias")) {
alias(ln, rest);
}
if (head.equals(".unit")) {
unitName = rest;
}
stmts.add(new Stmt(ln, head, args, rest, size, off));
off += size * 4;
}
int[] words = new int[off / 4];
// bases of event entries and switch tables
for (Stmt st : stmts) {
Integer op = opcode(st.head);
if (op == null) {
continue;
}
if (op == StratOps.HANDLER && !st.args.isEmpty()) {
eventBase.put(st.args.get(0), st.offset + 8);
} else if (op == StratOps.SWITCH && !st.args.isEmpty()) {
switchBase.put(st.args.get(0), st.offset + 4);
} else if (op == StratOps.NAME && !st.args.isEmpty()) {
nameSites.add(st.offset);
}
}
Integer curSwitch = null;
Map<Integer, String> labelAt = new HashMap<>();
labels.forEach((k, v) -> labelAt.putIfAbsent(v, k));
for (Stmt st : stmts) {
if (st.head.equals(".switch")) {
String l = labelAt.get(st.offset);
curSwitch = null;
for (var e : labels.entrySet()) {
if (e.getValue() == st.offset && switchBase.containsKey(e.getKey())) {
curSwitch = switchBase.get(e.getKey());
}
}
if (curSwitch == null) {
err(st.line, "no switch instruction refers to this table" + (l == null ? " (no label)" : ""));
}
}
encode(st, words, layout, curSwitch);
}
resolveNames(words);
if (!problems.isEmpty()) {
throw new AsmException(problems);
}
Map<String, Integer> strats = new LinkedHashMap<>();
for (int site : nameSites) {
for (var l : labels.entrySet()) {
if (l.getValue() == site) {
strats.put(l.getKey(), l.getValue());
break;
}
}
}
return new Result(words, Map.copyOf(labels), unitName, strats);
}
/** debugName "x": the first occurrence of x + NUL that starts a word or follows a NUL byte. */
private void resolveNames(int[] words) {
byte[] d = new byte[words.length * 4];
for (int i = 0; i < words.length; i++) {
for (int k = 0; k < 4; k++) {
d[4 * i + k] = (byte) (words[i] >> (8 * k));
}
}
for (int j = 0; j < pendingNames.size(); j++) {
int i = pendingNames.get(j)[0], ln = pendingNames.get(j)[1];
String name = pendingNameText.get(j);
if (name == null) {
continue;
}
byte[] n = name.getBytes(StandardCharsets.ISO_8859_1);
int found = -1, skip = pendingNames.get(j)[2];
for (int o = 0; o + n.length < d.length && found < 0; o++) {
if (o % 4 != 0 && d[o - 1] != 0) {
continue;
}
boolean m = d[o + n.length] == 0;
for (int k = 0; m && k < n.length; k++) {
m = d[o + k] == n[k];
}
if (m && skip-- == 0) {
found = o;
}
}
if (found < 0) {
err(ln, "no .string \"" + name + "\" in the unit");
} else {
words[i + 2] = found - (i * 4 + 12);
}
}
}
private void define(int ln, String l, int off) {
if (!l.matches("[A-Za-z_][A-Za-z0-9_]*")) {
err(ln, "bad label name '" + l + "'");
} else if (labels.putIfAbsent(l, off) != null) {
err(ln, "label " + l + " defined twice");
}
}
private void alias(int ln, String rest) {
String[] p = rest.split("=");
if (p.length != 2 || !p[1].trim().matches("v\\d+") || !p[0].trim().matches("[A-Za-z_][A-Za-z0-9_]*")) {
err(ln, "expected .alias name = vN");
return;
}
aliases.put(
p[0].trim().toLowerCase(Locale.ROOT),
Integer.parseInt(p[1].trim().substring(1)));
}
private static int labelEnd(String s) {
int c = s.indexOf(':');
if (c <= 0) {
return -1;
}
String l = s.substring(0, c);
return l.matches("\\s*[A-Za-z_][A-Za-z0-9_]*\\s*") ? c : -1;
}
private static int indexOfSpace(String s) {
for (int i = 0; i < s.length(); i++) {
if (Character.isWhitespace(s.charAt(i))) {
return i;
}
}
return -1;
}
static String stripComment(String s) {
boolean q = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\\' && q) {
i++;
} else if (c == '"') {
q = !q;
} else if (c == ';' && !q) {
return s.substring(0, i);
}
}
return s;
}
private static List<String> splitArgs(String s) {
List<String> out = new ArrayList<>();
if (s.isBlank()) {
return out;
}
for (String p : s.split(",")) {
out.add(p.trim());
}
return out;
}
private Integer opcode(String head) {
if (head.startsWith(".")) {
return null;
}
OpTable.Op o = ops.byName(head);
return o == null ? null : o.code();
}
private int size(int ln, String head, List<String> args, String rest) {
switch (head) {
case ".unit", ".alias", ".at" -> {
return 0;
}
case ".word" -> {
return args.size();
}
case ".string" -> {
byte[] b = packed(ln, rest);
return b == null ? 0 : (b.length + 3) / 4;
}
case ".event" -> {
return 3;
}
case ".switch" -> {
return 1;
}
case ".case" -> {
return args.size() == 2 && args.get(0).startsWith("imm ") ? 3 : 2;
}
default -> {}
}
Integer op = opcode(head);
if (op == null) {
err(ln, "unknown instruction '" + head + "'");
return 0;
}
if (op == StratOps.SKIP_A || op == StratOps.SKIP_B) {
int n = args.size();
if (n > 0 && args.get(0).startsWith("hi=")) {
n--;
}
return 1 + n;
}
return 1 + StratOps.operands(op);
}
/** Comma-separated quoted strings, each NUL-terminated, packed. */
private byte[] packed(int ln, String rest) {
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
String r = rest.trim();
while (true) {
int e = closingQuote(r);
if (e < 0) {
err(ln, "expected a quoted string");
return null;
}
String s = unquote(ln, r.substring(0, e + 1));
if (s == null) {
return null;
}
out.writeBytes(s.getBytes(StandardCharsets.ISO_8859_1));
out.write(0);
r = r.substring(e + 1).trim();
if (r.isEmpty()) {
return out.toByteArray();
}
if (!r.startsWith(",")) {
err(ln, "expected ',' between strings");
return null;
}
r = r.substring(1).trim();
}
}
private static int closingQuote(String s) {
if (!s.startsWith("\"")) {
return -1;
}
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == '\\') {
i++;
} else if (s.charAt(i) == '"') {
return i;
}
}
return -1;
}
private String unquote(int ln, String s) {
s = s.trim();
if (s.length() < 2 || !s.startsWith("\"") || !s.endsWith("\"")) {
err(ln, "expected a quoted string");
return null;
}
StringBuilder b = new StringBuilder();
for (int i = 1; i < s.length() - 1; i++) {
char c = s.charAt(i);
if (c == '\\' && i + 1 < s.length() - 1) {
c = s.charAt(++i);
}
if (c > 0xff || c == 0) {
err(ln, "character not allowed in a string: " + c);
return null;
}
b.append(c);
}
return b.toString();
}
private Integer label(int ln, String l) {
Integer v = labels.get(l);
if (v == null) {
err(ln, "unknown label '" + l + "'");
}
return v;
}
private Integer number(int ln, String s) {
Integer v = Num.parse(s);
if (v == null) {
err(ln, "expected a number, got '" + s + "'");
}
return v;
}
private int rel(int ln, int target, int base) {
int d = target - base;
if ((d & 3) != 0) {
err(ln, "target not word aligned");
}
return d >> 2;
}
private void encode(Stmt st, int[] w, Layout layout, Integer curSwitch) {
int ln = st.line, i = st.offset / 4;
List<String> a = st.args;
switch (st.head) {
case ".unit", ".alias", ".at" -> {
return;
}
case ".word" -> {
for (int k = 0; k < a.size(); k++) {
Integer v = a.get(k).startsWith("@") ? addr(ln, a.get(k), layout) : number(ln, a.get(k));
w[i + k] = v == null ? 0 : v;
}
return;
}
case ".string" -> {
byte[] b = packed(ln, st.rest);
if (b != null) {
for (int k = 0; k < b.length; k++) {
w[i + k / 4] |= (b[k] & 0xff) << (8 * (k % 4));
}
}
return;
}
case ".event" -> {
if (a.size() != 3) {
err(ln, "expected .event type, param, label");
return;
}
String own = null;
for (var e : labels.entrySet()) {
if (e.getValue() == st.offset && eventBase.containsKey(e.getKey())) {
own = e.getKey();
}
}
if (own == null) {
err(ln, "no onEvent instruction refers to this entry");
return;
}
Integer t = number(ln, a.get(0)), p = number(ln, a.get(1)), tg = label(ln, a.get(2));
if (t != null && p != null && tg != null) {
w[i] = t;
w[i + 1] = p;
w[i + 2] = rel(ln, tg, eventBase.get(own));
}
return;
}
case ".switch" -> {
Integer n = a.size() == 1 ? number(ln, a.get(0)) : null;
if (n == null) {
err(ln, "expected .switch count");
return;
}
w[i] = n;
return;
}
case ".case" -> {
if (a.size() != 2 || curSwitch == null) {
err(ln, "expected .case [imm] value, label after .switch");
return;
}
boolean imm = a.get(0).startsWith("imm ");
Integer v = number(ln, imm ? a.get(0).substring(4) : a.get(0));
Integer tg = label(ln, a.get(1));
if (v == null || tg == null) {
return;
}
if (imm) {
w[i] = StratOps.PUSH_IMM;
w[i + 1] = v;
w[i + 2] = rel(ln, tg, curSwitch);
} else {
if (v < Short.MIN_VALUE || v > Short.MAX_VALUE) {
err(ln, "short case value out of range (use .case imm)");
}
w[i] = (v << 16) | StratOps.PUSH_SHORT;
w[i + 1] = rel(ln, tg, curSwitch);
}
return;
}
default -> {}
}
Integer opc = opcode(st.head);
if (opc == null) {
return;
}
int op = opc;
int next = st.offset + 4;
if (op == StratOps.HANDLER) {
if (a.size() < 2) {
err(ln, "expected onEvent entryLabel, slot");
return;
}
int hi = a.size() > 2 ? hiArg(ln, a.get(2)) : 0;
Integer e = label(ln, a.get(0)), slot = number(ln, a.get(1));
if (e != null && slot != null) {
w[i] = (hi << 16) | op;
w[i + 1] = rel(ln, e, st.offset + 8);
w[i + 2] = slot;
}
return;
}
if (op == StratOps.SKIP_A || op == StratOps.SKIP_B) {
int hi = 0, k0 = 0;
if (!a.isEmpty() && a.get(0).startsWith("hi=")) {
hi = hiArg(ln, a.get(0));
k0 = 1;
}
w[i] = (hi << 16) | op;
for (int k = k0; k < a.size(); k++) {
Integer v = number(ln, a.get(k));
w[i + 1 + k - k0] = v == null ? 0 : v;
}
return;
}
if (op < StratOps.PUSH_IMM) {
if (a.size() != 1) {
err(ln, st.head + " takes one operand");
return;
}
String x = a.get(0);
Integer arg;
if (StratOps.relJump(op) || op == StratOps.PUSH_REL_WORD || op == StratOps.PUSH_REL_ADDR || op == 0x42) {
Integer t = labels.containsKey(x) ? labels.get(x) : null;
arg = t != null ? rel(ln, t, next) : number(ln, x);
} else if (op == StratOps.SWITCH) {
Integer t = label(ln, x);
arg = t == null ? null : rel(ln, t - 4, next);
} else {
arg = shortValue(ln, op, x);
}
if (arg == null) {
return;
}
if (arg < Short.MIN_VALUE || arg > Short.MAX_VALUE) {
err(ln, "operand out of the 16-bit range: " + arg);
return;
}
w[i] = (arg << 16) | op;
return;
}
int n = StratOps.operands(op);
w[i] = op;
switch (op) {
case StratOps.NAME -> {
if (a.size() >= 1 && a.size() <= 2 && a.get(0).startsWith("\"")) {
w[i + 1] = StratOps.NAME_TAG;
Integer occ = a.size() == 2 ? number(ln, a.get(1)) : Integer.valueOf(0);
pendingNames.add(new int[] {i, ln, occ == null ? 0 : occ});
pendingNameText.add(unquote(ln, a.get(0)));
return;
}
Integer s = a.size() == 1 ? label(ln, a.get(0)) : null;
if (s != null) {
w[i + 1] = StratOps.NAME_TAG;
w[i + 2] = s - (st.offset + 12);
} else if (a.size() != 1) {
err(ln, "expected name stringLabel");
}
return;
}
case StratOps.PUSH_ADDR -> {
Integer v = a.size() == 1 ? addr(ln, a.get(0), layout) : null;
if (a.size() != 1) {
err(ln, "expected one address operand");
}
w[i + 1] = v == null ? 0 : v;
return;
}
default -> {}
}
if (a.size() != n) {
err(ln, st.head + " takes " + n + " operand" + (n == 1 ? "" : "s") + ", got " + a.size());
return;
}
for (int k = 0; k < n; k++) {
String x = a.get(k);
int c = x.indexOf(':');
if (c > 0 && x.substring(0, c).matches("tex|sound|text")) {
x = x.substring(c + 1);
}
Integer v = number(ln, x);
w[i + 1 + k] = v == null ? 0 : v;
}
}
private int hiArg(int ln, String s) {
Integer v = s.startsWith("hi=") ? Num.parse(s.substring(3)) : null;
if (v == null) {
err(ln, "expected hi=N");
return 0;
}
return v;
}
private Integer shortValue(int ln, int op, String x) {
Integer v = Num.parse(x);
if (v != null) {
return v;
}
String kind = StratListing.shortKind(op);
String lx = x.toLowerCase(Locale.ROOT);
switch (kind) {
case "var" -> {
if (aliases.containsKey(lx)) {
return aliases.get(lx);
}
if (lx.matches("v\\d+")) {
return Integer.parseInt(lx.substring(1));
}
}
case "global" -> {
if (lx.matches("g\\d+")) {
return Integer.parseInt(lx.substring(1));
}
OpTable.Named n = ops.globalByName(x);
if (n != null) {
return n.id();
}
}
case "prop", "playerProp" -> {
if (lx.matches("prop\\d+")) {
return Integer.parseInt(lx.substring(4));
}
OpTable.Named n = ops.propByName(x);
if (n != null) {
return n.id();
}
}
default -> {}
}
err(ln, "bad " + kind + " operand '" + x + "'");
return null;
}
private Integer addr(int ln, String s, Layout l) {
s = s.trim();
if (s.startsWith("@")) {
String name = s.substring(1);
int plus = name.indexOf('+');
int add = 0;
if (plus > 0) {
Integer d = number(ln, name.substring(plus + 1));
add = d == null ? 0 : d;
name = name.substring(0, plus);
}
Integer v = label(ln, name.trim());
return v == null ? null : l.base() + v + add;
}
int c = s.indexOf(':');
if (c > 0) {
String kind = s.substring(0, c);
String r = s.substring(c + 1);
int plus = r.indexOf('+');
int add = 0;
if (plus > 0) {
Integer d = number(ln, r.substring(plus + 1));
add = d == null ? 0 : d;
r = r.substring(0, plus);
}
Integer idx = number(ln, r);
if (idx == null) {
return null;
}
List<Integer> list =
switch (kind) {
case "model" -> l.models();
case "anim" -> l.anims();
case "unit" -> l.units();
default -> null;
};
if (list == null) {
err(ln, "unknown reference kind '" + kind + "' (model, anim, unit)");
return null;
}
if (idx < 0 || idx >= list.size()) {
err(ln, kind + " " + idx + " does not exist (0.." + (list.size() - 1) + ")");
return null;
}
return list.get(idx) + add;
}
return number(ln, s);
}
}
@@ -0,0 +1,847 @@
package com.eliasmueller.llamamod.format.strat;
import com.eliasmueller.llamamod.format.Bytes;
import com.eliasmueller.llamamod.format.stpcmerge.StpcImage;
import com.eliasmueller.llamamod.format.stpcmerge.StratCode;
import com.eliasmueller.llamamod.format.stpcmerge.StratOps;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.IntFunction;
/**
* Disassembly of one STPC code unit into the strat language (notes/strat-language.md). Every
* word of the unit is emitted either as an instruction or as data, with labels for all relative
* targets, so {@link StratAssembler} rebuilds the unit byte-identically.
*/
public final class StratListing {
/** What a line is. */
public enum Kind {
HEADER,
LABEL,
INSTR,
DATA,
COMMENT
}
/** Kind of an editable constant. */
public enum ConstKind {
NUMBER,
TEXTURE,
SOUND,
TEXT
}
/**
* One listing line. offset: STPC byte offset (−1 for header/comment lines); constAt: STPC
* offset of the editable constant word (−1 = none); constShort: the constant is the high 16
* bits of an opcode word.
*/
public record Line(
Kind kind,
int offset,
int words,
String label,
String text,
String comment,
int op,
int constAt,
boolean constShort,
int value,
ConstKind constKind) {
public String render() {
StringBuilder b = new StringBuilder();
switch (kind) {
case HEADER, COMMENT -> b.append(text);
case LABEL -> b.append(label).append(':');
default -> {
b.append(" ").append(text);
if (comment != null && !comment.isEmpty()) {
while (b.length() < 44) {
b.append(' ');
}
b.append(" ; ").append(comment);
}
}
}
return b.toString();
}
}
/** Naming context: parameter names of the unit's strats, names of code entries elsewhere. */
public interface Names {
/** Alias for variable i of the strat whose entry is at code (null = none). */
default String param(int code, int i) {
return null;
}
/** Type/entry name of a code offset outside the unit (null = unknown). */
default String entry(int code) {
return null;
}
Names NONE = new Names() {};
}
public final StpcImage img;
public final int unit;
public final int start;
public final int end;
public final List<Line> lines = new ArrayList<>();
/** Label name by STPC offset. */
public final Map<Integer, String> labels = new TreeMap<>();
/** Strat names declared in the unit by name site offset. */
public final Map<Integer, String> strats = new LinkedHashMap<>();
/** Variable aliases (index → name) declared at the top. */
public final Map<Integer, String> aliases = new TreeMap<>();
private final StratCode code;
private final Map<Integer, Integer> eventBase = new HashMap<>();
private final Map<Integer, Integer> switchBase = new HashMap<>();
private final Set<Integer> rawData = new HashSet<>();
private final OpTable ops = OpTable.get();
private StratListing(StpcImage img, StratCode code, int unit) {
this.img = img;
this.code = code;
this.unit = unit;
this.start = img.units.get(unit).start();
this.end = img.units.get(unit).end();
}
public static StratListing of(StpcImage img, StratCode code, int unit, Set<Integer> entries, Names names) {
StratListing l = new StratListing(img, code, unit);
l.build(entries, names);
return l;
}
private int w(int o) {
return Bytes.i32(img.data, o);
}
private boolean in(int o) {
return o >= start && o < end;
}
private void build(Set<Integer> entries, Names names) {
Map<Integer, Integer> eventRefs = new HashMap<>(), switchRefs = new HashMap<>();
Set<Integer> targets = new HashSet<>();
for (int o = start; o < end; ) {
int ln = code.length(o);
if (ln == 0) {
o += 4;
continue;
}
int wd = w(o), op = wd & 0xffff, arg = wd >> 16;
int next = o + 4;
if (op == StratOps.HANDLER) {
int e = o + 8 + w(o + 4) * 4;
eventRefs.merge(e, 1, Integer::sum);
eventBase.put(e, o + 8);
targets.add(e);
} else if (op == StratOps.SWITCH) {
int t = next + arg * 4 + 4;
switchRefs.merge(t, 1, Integer::sum);
switchBase.put(t, next);
targets.add(t);
} else if (relOp(op)) {
if (in(next + arg * 4)) {
targets.add(next + arg * 4);
}
} else if (op == StratOps.NAME && w(o + 4) == StratOps.NAME_TAG) {
long s = o + 12L + w(o + 8);
if (s >= start && s < end && (s & 3) == ((start) & 3)) {
targets.add((int) s);
}
} else if (op == StratOps.PUSH_ADDR && in(w(o + 4))) {
targets.add(w(o + 4));
}
o += ln;
}
for (var e : eventRefs.entrySet()) {
int t = e.getKey();
if (e.getValue() != 1
|| !in(t)
|| !in(t + 8)
|| code.length(t) != 0
|| code.length(t + 4) != 0
|| code.length(t + 8) != 0) {
eventBase.remove(t);
} else {
targets.add(eventBase.get(t) + w(t + 8) * 4);
}
}
for (var e : switchRefs.entrySet()) {
int t = e.getKey();
int base = switchBase.get(t);
boolean ok = e.getValue() == 1 && in(t) && code.length(t) == 0;
List<Integer> caseTargets = new ArrayList<>();
if (ok) {
int n = w(t);
int q = t + 4;
for (int k = 0; k < n && ok; k++) {
if (!in(q)) {
ok = false;
break;
}
int c = w(q) & 0xffff;
int cl = c == StratOps.PUSH_SHORT ? 4 : c == StratOps.PUSH_IMM ? 8 : -1;
if (cl < 0 || !in(q + cl) || (c == StratOps.PUSH_IMM && (w(q) >>> 16) != 0)) {
ok = false;
break;
}
for (int x = q; x <= q + cl; x += 4) {
ok &= code.length(x) == 0;
}
caseTargets.add(base + w(q + cl) * 4);
q += cl + 4;
}
ok &= n >= 0 && n < 4096;
}
if (ok) {
targets.addAll(caseTargets);
} else {
switchBase.remove(t);
}
}
targets.addAll(entries.stream().filter(this::in).toList());
for (int s : img.nameSites(img.units.get(unit))) {
String n = Bytes.cstr(img.data, (int) (s + 12L + w(s + 8)));
strats.put(s, n);
targets.add(s);
}
nameLabels(targets, entries);
for (int s : strats.keySet()) {
for (int i = 0; i < 64; i++) {
String a = names.param(s, i);
if (a != null
&& a.matches("[A-Za-z_][A-Za-z0-9_]*")
&& !aliases.containsValue(a)
&& !labels.containsValue(a)
&& ops.byName(a) == null) {
aliases.putIfAbsent(i, a);
}
}
}
emit(names);
angleComments();
}
/** Constants consumed by turning/rotation ops get a comment in degrees. */
private void angleComments() {
for (int i = 0; i + 1 < lines.size(); i++) {
Line l = lines.get(i);
if (l.constKind() != ConstKind.NUMBER || l.kind() != Kind.INSTR) {
continue;
}
Line n = null;
for (int k = i + 1; k < lines.size() && n == null; k++) {
n = lines.get(k).kind() == Kind.INSTR ? lines.get(k) : null;
if (lines.get(k).kind() != Kind.INSTR && lines.get(k).kind() != Kind.LABEL) {
break;
}
}
if (n != null && angleConsumer(n.text()) && Math.abs((long) l.value()) >= 0x100) {
String deg = Num.degreeLiteral(l.value());
String text = l.text();
String cm = "= " + Num.degrees(l.value());
if (deg != null) {
text = text.substring(0, text.indexOf(' ') + 1) + deg;
cm = "= " + Num.literal(l.value());
}
lines.set(
i,
new Line(
l.kind(),
l.offset(),
l.words(),
l.label(),
text,
cm,
l.op(),
l.constAt(),
l.constShort(),
l.value(),
l.constKind()));
}
}
}
static boolean angleConsumer(String t) {
String s = t.toLowerCase(java.util.Locale.ROOT);
return s.startsWith("turn")
|| s.startsWith("tilt")
|| s.startsWith("sin")
|| s.startsWith("cos")
|| s.matches("pop\\w*prop rot[xyz].*")
|| s.matches("pop\\w*prop \\w*(angle|yaw)\\w*");
}
private void nameLabels(Set<Integer> targets, Set<Integer> entries) {
Set<String> used = new HashSet<>();
for (var e : strats.entrySet()) {
String n = ident(e.getValue());
String u = n;
for (int k = 2; used.contains(u.toLowerCase()) || OpTable.get().byName(u) != null; k++) {
u = n + "_" + k;
}
used.add(u.toLowerCase());
labels.put(e.getKey(), u);
}
for (int t : new java.util.TreeSet<>(targets)) {
if (labels.containsKey(t) || !in(t)) {
continue;
}
String p = eventBase.containsKey(t)
? "ev_"
: switchBase.containsKey(t) ? "sw_" : entries.contains(t) ? "entry_" : "L_";
labels.put(t, p + String.format("%04x", t - start));
}
}
public static String ident(String s) {
StringBuilder b = new StringBuilder();
for (char c : s.toCharArray()) {
b.append(Character.isLetterOrDigit(c) && c < 128 ? c : '_');
}
if (b.isEmpty() || Character.isDigit(b.charAt(0))) {
b.insert(0, '_');
}
return b.toString();
}
private String label(int o) {
String l = labels.get(o);
return l != null ? l : null;
}
/** Reference text for an absolute STPC offset (operand of pushAddr). */
String addrRef(int v) {
if (in(v) && labels.containsKey(v)) {
return "@" + labels.get(v);
}
int m = indexIn(img.models, v);
if (m >= 0) {
return "model:" + m + plus(v - img.models.get(m).start());
}
int a = indexIn(img.anims, v);
if (a >= 0) {
return "anim:" + a + plus(v - img.anims.get(a).start());
}
int u = img.unitOf(v);
if (u >= 0 && u != unit) {
return "unit:" + u + plus(v - img.units.get(u).start());
}
return Num.integer(v);
}
private static String plus(int d) {
return d == 0 ? "" : "+" + Num.hex(d);
}
private static int indexIn(List<StpcImage.Span> l, int o) {
for (int i = 0; i < l.size(); i++) {
if (l.get(i).contains(o)) {
return i;
}
}
return -1;
}
private void add(Kind k, int off, int words, String text, String comment) {
lines.add(new Line(k, off, words, null, text, comment, -1, -1, false, 0, null));
}
private void emit(Names names) {
String title = strats.isEmpty() ? "unit " + unit : String.join(", ", strats.values());
add(
Kind.HEADER,
-1,
0,
"; unit " + unit + " (" + (end - start) / 4 + " words at STPC 0x" + Integer.toHexString(start) + "): "
+ title,
null);
add(
Kind.HEADER,
-1,
0,
".unit "
+ ident(
strats.isEmpty()
? "unit" + unit
: strats.values().iterator().next()),
null);
for (var a : aliases.entrySet()) {
add(Kind.HEADER, -1, 0, ".alias " + a.getValue() + " = v" + a.getKey(), null);
}
int o = start;
while (o < end) {
String lb = label(o);
if (lb != null) {
lines.add(new Line(Kind.LABEL, o, 0, lb, null, null, -1, -1, false, 0, null));
if (strats.containsKey(o)) {
// nothing extra: the label carries the strat name
}
}
int ln = code.length(o);
if (ln > 0) {
// labels inside the instruction (rare)
for (int k = 4; k < ln; k += 4) {
if (labels.containsKey(o + k)) {
add(
Kind.DATA,
-1,
0,
".at " + labels.get(o + k) + ", " + k,
"label inside the next instruction");
}
}
instr(o, ln, names);
o += ln;
continue;
}
if (eventBase.containsKey(o)) {
int base = eventBase.get(o);
int tgt = base + w(o + 8) * 4;
String tl = label(tgt);
if (tl != null && !labels.containsKey(o + 4) && !labels.containsKey(o + 8)) {
add(
Kind.DATA,
o,
3,
".event " + Num.integer(w(o)) + ", " + Num.integer(w(o + 4)) + ", " + tl,
"handler entry");
o += 12;
continue;
}
}
if (switchBase.containsKey(o) && switchTable(o)) {
o = switchEnd(o);
continue;
}
o = data(o);
}
}
private boolean switchTable(int t) {
int base = switchBase.get(t);
int n = w(t);
int q = t + 4;
List<String> rows = new ArrayList<>();
List<int[]> spans = new ArrayList<>();
for (int k = 0; k < n; k++) {
int c = w(q) & 0xffff;
boolean sh = c == StratOps.PUSH_SHORT;
int cl = sh ? 4 : 8;
for (int x = q + 4; x <= q + cl; x += 4) {
if (labels.containsKey(x)) {
return false;
}
}
String tl = label(base + w(q + cl) * 4);
if (tl == null) {
return false;
}
int val = sh ? w(q) >> 16 : w(q + 4);
rows.add(".case " + (sh ? "" : "imm ") + Num.literal(val) + ", " + tl);
spans.add(new int[] {q, cl / 4 + 1});
q += cl + 4;
}
for (int x = t + 4; x < q; x += 4) {
if (labels.containsKey(x) && spans.stream().noneMatch(s -> s[0] == -1)) {
return false;
}
}
add(Kind.DATA, t, 1, ".switch " + n, "switch table");
for (int i = 0; i < rows.size(); i++) {
add(Kind.DATA, spans.get(i)[0], spans.get(i)[1], rows.get(i), null);
}
return true;
}
private int switchEnd(int t) {
int n = w(t);
int q = t + 4;
for (int k = 0; k < n; k++) {
q += (w(q) & 0xffff) == StratOps.PUSH_SHORT ? 8 : 12;
}
return q;
}
/** Emits uncovered words from o up to the next label/instruction; returns the next offset. */
private int data(int o) {
int e = o + 4;
while (e < end
&& code.length(e) == 0
&& !labels.containsKey(e)
&& !eventBase.containsKey(e)
&& !switchBase.containsKey(e)) {
e += 4;
}
int p = o;
while (p < e) {
List<String> strs = new ArrayList<>();
int s = stringAt(p, e, strs);
if (s > 0) {
StringBuilder b = new StringBuilder(".string ");
for (int i = 0; i < strs.size(); i++) {
b.append(i == 0 ? "" : ", ").append(quote(strs.get(i)));
}
add(Kind.DATA, p, s / 4, b.toString(), null);
p += s;
continue;
}
int q = p;
StringBuilder b = new StringBuilder(".word ");
int n = 0;
while (q < e && n < 4 && (n == 0 || stringAt(q, e, new ArrayList<>()) == 0)) {
if (n > 0) {
b.append(", ");
}
b.append(Num.hex(w(q)));
q += 4;
n++;
}
add(Kind.DATA, p, n, b.toString(), null);
p = q;
}
return e;
}
/**
* Length in bytes of packed NUL-terminated printable strings at o (inside [o, e)), zero padded to
* the next word, or 0. The strings are added to out.
*/
private int stringAt(int o, int e, List<String> out) {
int k = o;
while (true) {
int s = k;
while (k < e && img.data[k] != 0) {
int c = img.data[k] & 0xff;
if (c < 0x20 || c > 0x7e) {
return 0;
}
k++;
}
if (k >= e || k - s < (out.isEmpty() ? 2 : 1)) {
return 0;
}
out.add(new String(img.data, s, k - s, StandardCharsets.ISO_8859_1));
k++;
if (((k - start) & 3) == 0) {
return k - o;
}
if (img.data[k] == 0) {
int lim = start + ((k - start + 3) & ~3);
for (int x = k; x < lim; x++) {
if (x >= e || img.data[x] != 0) {
return 0;
}
}
return lim - o;
}
}
}
/**
* Where the assembler finds a quoted name: the first occurrence of name + NUL in the unit that
* starts a word or follows a NUL byte (−1 = none).
*/
List<Integer> findString(String name) {
List<Integer> out = new ArrayList<>();
byte[] n = name.getBytes(StandardCharsets.ISO_8859_1);
for (int o = start; o + n.length < end; o++) {
if ((o - start) % 4 != 0 && img.data[o - 1] != 0) {
continue;
}
boolean m = img.data[o + n.length] == 0;
for (int i = 0; m && i < n.length; i++) {
m = img.data[o + i] == n[i];
}
if (m) {
out.add(o);
}
}
return out;
}
static String quote(String s) {
StringBuilder b = new StringBuilder("\"");
for (char c : s.toCharArray()) {
if (c == '"' || c == '\\') {
b.append('\\');
}
b.append(c);
}
return b.append('"').toString();
}
private void instr(int o, int ln, Names names) {
int wd = w(o), op = wd & 0xffff, arg = wd >> 16;
OpTable.Op info = ops.op(op);
String mn = info.name();
int next = o + 4;
String text;
String comment = null;
int constAt = -1;
boolean constShort = false;
int value = 0;
ConstKind ck = null;
if (!info.isShort() && (wd >>> 16) != 0) {
raw(o, ln, "long opcode with high bits set");
return;
}
if (op == StratOps.HANDLER) {
int e = o + 8 + w(o + 4) * 4;
String el = label(e);
if (el == null) {
raw(o, ln, "event entry outside the unit");
return;
}
text = mn + " " + el + ", " + Num.integer(w(o + 8));
if (arg != 0) {
text += ", hi=" + arg;
}
if (eventBase.containsKey(e)) {
comment = "event type " + Num.integer(w(e)) + " → " + label(eventBase.get(e) + w(e + 8) * 4);
}
line(o, ln, text, comment, op, -1, false, 0, null);
return;
}
if (info.isShort()) {
if (op == StratOps.SKIP_A || op == StratOps.SKIP_B) {
StringBuilder b = new StringBuilder(mn);
if (arg != 0) {
b.append(" hi=").append(arg).append(",");
}
for (int x = o + 4; x < o + ln; x += 4) {
b.append(x == o + 4 && arg == 0 ? " " : x == o + 4 ? " " : ", ")
.append(Num.hex(w(x)));
}
line(o, ln, b.toString(), "debug list (skipped)", op, -1, false, 0, null);
return;
}
if (ln != 4) {
raw(o, ln, "unexpected length");
return;
}
if (relOp(op)) {
String t = label(next + arg * 4);
text = mn + " " + (t != null ? t : Integer.toString(arg));
if (op == StratOps.PUSH_REL_WORD && in(next + arg * 4)) {
comment = "= " + Num.literal(w(next + arg * 4));
}
} else if (op == StratOps.SWITCH) {
int t = next + arg * 4 + 4;
String tl = label(t);
if (tl == null) {
raw(o, ln, "switch table outside the unit");
return;
}
text = mn + " " + tl;
} else {
Operand a = shortOperand(op, arg);
text = mn + " " + a.text;
comment = a.comment;
if (a.editable) {
constAt = o;
constShort = true;
value = arg;
ck = ConstKind.NUMBER;
}
}
line(o, ln, text, comment, op, constAt, constShort, value, ck);
return;
}
int nOps = ln / 4 - 1;
List<String> args = new ArrayList<>();
switch (op) {
case StratOps.PUSH_IMM -> {
int v = w(o + 4);
boolean tex = o + 8 < end && StratOps.texConsumer(w(o + 8) & 0xffff);
args.add(tex ? "tex:" + v : Num.literal(v));
comment = tex ? "texture index" : comment(v);
constAt = o + 4;
value = v;
ck = tex ? ConstKind.TEXTURE : ConstKind.NUMBER;
}
case StratOps.PUSH_ADDR -> {
int v = w(o + 4);
args.add(addrRef(v));
comment = addrComment(v, names);
}
case StratOps.PUSH_SOUND -> {
args.add("sound:" + w(o + 4));
constAt = o + 4;
value = w(o + 4);
ck = ConstKind.SOUND;
}
case StratOps.TEXT_A, StratOps.TEXT_B -> {
args.add("text:" + w(o + 4));
constAt = o + 4;
value = w(o + 4);
ck = ConstKind.TEXT;
}
case StratOps.NAME -> {
long s = o + 12L + w(o + 8);
if (w(o + 4) != StratOps.NAME_TAG || s < start || s >= end) {
raw(o, ln, "name site with unusual operands");
return;
}
String str = Bytes.cstr(img.data, (int) s);
String sl = label((int) s);
int occ = findString(str).indexOf((int) s);
if (occ >= 0 && str.chars().allMatch(c -> c >= 0x20 && c <= 0x7e)) {
args.add(quote(str) + (occ > 0 ? ", " + occ : ""));
} else if (sl != null) {
args.add(sl);
comment = quote(str);
} else {
raw(o, ln, "name string not addressable");
return;
}
}
default -> {
for (int k = 1; k <= nOps; k++) {
args.add(Num.integer(w(o + 4 * k)));
}
}
}
text = args.isEmpty() ? mn : mn + " " + String.join(", ", args);
if (comment == null && !info.doc().isEmpty() && nOps == 0) {
comment = null;
}
line(o, ln, text, comment, op, constAt, false, value, ck);
}
private record Operand(String text, String comment, boolean editable) {}
private Operand shortOperand(int op, int arg) {
String kind = shortKind(op);
switch (kind) {
case "var" -> {
String a = aliases.get(arg);
return new Operand(a != null ? a : "v" + arg, a != null ? "v" + arg : null, false);
}
case "global" -> {
OpTable.Named n = ops.global(arg);
return new Operand(n != null ? n.name() : "g" + arg, n != null ? "g" + arg : null, false);
}
case "prop", "playerProp" -> {
OpTable.Named n = ops.prop(arg);
return new Operand(n != null ? n.name() : "prop" + arg, null, false);
}
default -> {
return new Operand(Num.literal(arg), comment(arg), op == StratOps.PUSH_SHORT);
}
}
}
/** Short opcodes whose argument is a relative code offset (target = next + arg·4). */
static boolean relOp(int op) {
return StratOps.relJump(op) || op == StratOps.PUSH_REL_WORD || op == StratOps.PUSH_REL_ADDR || op == 0x42;
}
/** Operand kind of a short opcode: var, global, prop, playerProp or number. */
static String shortKind(int op) {
return switch (op) {
case 0x04, 0x41 -> "var";
case 0x06, 0x43 -> "global";
case 0x07, 0x40 -> "prop";
case 0x1c, 0x37 -> "playerProp";
default -> {
String k = OpTable.get().op(op).operands().split("[ ,]")[0];
yield switch (k) {
case "var", "global", "prop", "playerProp" -> k;
default -> "number";
};
}
};
}
private static String comment(int v) {
if (Math.abs((long) v) >= 64 && Math.abs((long) v) < 0x10000000L) {
String f = Num.fixed(v);
String lit = Num.literal(v);
return lit.contains(".") ? "= " + v : "≈ " + f;
}
return null;
}
private String addrComment(int v, Names names) {
if (in(v)) {
String s = strats.get(v);
return s != null ? "strat " + s : "code in this unit";
}
if (indexIn(img.models, v) >= 0) {
return "model";
}
if (indexIn(img.anims, v) >= 0) {
return "animation set";
}
if (v < 0) {
return "DEFANIM.WAD animation " + (-v);
}
int u = img.unitOf(v);
if (u >= 0) {
String n = names.entry(v);
if (n == null) {
for (int s : img.nameSites(img.units.get(u))) {
if (s == v) {
n = Bytes.cstr(img.data, (int) (s + 12L + w(s + 8)));
}
}
}
return n != null ? "strat " + n : "code of unit " + u;
}
return null;
}
private void raw(int o, int ln, String why) {
StringBuilder b = new StringBuilder(".word ");
for (int x = o; x < o + ln; x += 4) {
b.append(x == o ? "" : ", ").append(Num.hex(w(x)));
}
line(o, ln, b.toString(), ops.op(w(o) & 0xffff).name() + ": " + why, w(o) & 0xffff, -1, false, 0, null);
}
private void line(int o, int ln, String text, String comment, int op, int constAt, boolean sh, int v, ConstKind k) {
lines.add(new Line(Kind.INSTR, o, ln / 4, null, text, comment, op, constAt, sh, v, k));
}
public String text() {
StringBuilder b = new StringBuilder();
for (Line l : lines) {
b.append(l.render()).append('\n');
}
return b.toString();
}
/** Word values of the unit (for comparisons). */
public int[] words() {
int[] out = new int[(end - start) / 4];
for (int i = 0; i < out.length; i++) {
out[i] = w(start + 4 * i);
}
return out;
}
/** Resolves pushAddr references and other units' code for {@link StratAssembler} in the same STPC. */
public static StratAssembler.Layout layout(StpcImage img, int base) {
List<Integer> models = new ArrayList<>(), anims = new ArrayList<>(), units = new ArrayList<>();
img.models.forEach(s -> models.add(s.start()));
img.anims.forEach(s -> anims.add(s.start()));
img.units.forEach(s -> units.add(s.start()));
return new StratAssembler.Layout(base, models, anims, units);
}
static IntFunction<String> none() {
return i -> null;
}
}
@@ -0,0 +1,482 @@
package com.eliasmueller.llamamod.format.strat;
import com.eliasmueller.llamamod.format.stpcmerge.StratOps;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Read-only structured view of a {@link StratListing}: stack code folded into expressions,
* forward conditional branches into if/else blocks and backward jumps into loops where the blocks
* nest; everything else stays as labels and gotos.
*/
public final class StratPseudo {
/** One output line; offset = STPC offset of the first instruction (−1 for structure lines). */
public record Line(int offset, int depth, String text) {}
private static final Map<String, String> BINARY = Map.ofEntries(
Map.entry("add", "+"),
Map.entry("sub", "-"),
Map.entry("mul", "*"),
Map.entry("div", "/"),
Map.entry("eq", "=="),
Map.entry("notEqual", "!="),
Map.entry("lt", "<"),
Map.entry("gt", ">"),
Map.entry("lessEqual", "<="),
Map.entry("greaterEqual", ">="),
Map.entry("and", "&&"),
Map.entry("or", "|"),
Map.entry("bitwiseAnd", "&"),
Map.entry("shl", "<<"),
Map.entry("shr", ">>"));
/** Push/pop opcodes by target prefix ("" = own variable). */
private static final Map<Integer, String> PUSH = new HashMap<>(), POP = new HashMap<>(), ADDR = new HashMap<>();
static {
Object[][] t = {
{0x04, 0x41, 0x08, ""},
{0x07, 0x40, 0x0b, "self."},
{0x06, 0x43, 0x0a, ""},
{0x18, 0x3e, 0x19, "parent."},
{0x1a, 0x3f, 0x1b, "parent."},
{0x1c, 0x37, 0x1d, "player."},
{0x1e, 0x38, 0x1f, "camera."},
{0x20, 0x39, 0x21, "target."},
{0x24, 0x3a, 0x25, "target2."},
{0x26, 0x3d, 0x27, "boss."},
{0x28, 0x3b, 0x29, "dialog."}
};
for (Object[] r : t) {
PUSH.put((Integer) r[0], (String) r[3]);
POP.put((Integer) r[1], (String) r[3]);
ADDR.put((Integer) r[2], (String) r[3]);
}
}
private final StratListing src;
private final List<StratListing.Line> ls;
private final OpTable ops = OpTable.get();
public final List<Line> lines = new ArrayList<>();
private final Deque<String> stack = new ArrayDeque<>();
private int stackAt = -1;
private int depth;
private StratPseudo(StratListing src) {
this.src = src;
this.ls = src.lines;
}
public static StratPseudo of(StratListing l) {
StratPseudo p = new StratPseudo(l);
p.build();
return p;
}
/** Editable constants per output line: those of the instructions from its offset to the next line's. */
public List<List<StratListing.Line>> constants() {
List<StratListing.Line> consts = new ArrayList<>();
for (StratListing.Line l : src.lines) {
if (l.constAt() >= 0) {
consts.add(l);
}
}
int[] nextOff = new int[lines.size()];
int nxt = src.end;
for (int i = lines.size() - 1; i >= 0; i--) {
nextOff[i] = nxt;
if (lines.get(i).offset() >= 0) {
nxt = Math.min(nxt, lines.get(i).offset());
}
}
List<List<StratListing.Line>> out = new ArrayList<>();
for (int i = 0; i < lines.size(); i++) {
Line l = lines.get(i);
List<StratListing.Line> cs = new ArrayList<>();
if (l.offset() >= 0) {
for (StratListing.Line c : consts) {
if (c.offset() >= l.offset() && c.offset() < nextOff[i]) {
cs.add(c);
}
}
}
out.add(cs);
}
return out;
}
public String text() {
StringBuilder b = new StringBuilder();
for (Line l : lines) {
b.append(" ".repeat(l.depth)).append(l.text).append('\n');
}
return b.toString();
}
private record Block(int open, int close, String kind) {}
private String mn(StratListing.Line l) {
String t = l.text();
int sp = t.indexOf(' ');
return sp < 0 ? t : t.substring(0, sp);
}
private String operand(StratListing.Line l) {
String t = l.text();
int sp = t.indexOf(' ');
return sp < 0 ? "" : t.substring(sp + 1).trim();
}
private void build() {
Map<String, Integer> labelIdx = new HashMap<>();
for (int i = 0; i < ls.size(); i++) {
if (ls.get(i).kind() == StratListing.Kind.LABEL) {
labelIdx.put(ls.get(i).label(), i);
}
}
// candidate blocks
List<Block> cand = new ArrayList<>();
Map<Integer, Integer> elseOf = new HashMap<>();
for (int i = 0; i < ls.size(); i++) {
StratListing.Line l = ls.get(i);
if (l.kind() != StratListing.Kind.INSTR) {
continue;
}
int op = l.op();
Integer t = labelIdx.get(operand(l));
if (t == null) {
continue;
}
if ((op == 0x14 || op == 0x15 || op == 0x12 || op == 0x13) && t > i && clean(i, t)) {
cand.add(new Block(i, t, "if"));
int pj = prevInstr(t);
if (pj > i && ls.get(pj).op() == 0x16) {
Integer m = labelIdx.get(operand(ls.get(pj)));
if (m != null && m > t && clean(t, m)) {
elseOf.put(i, m);
}
}
} else if ((op == 0x16 || op == 0x2b) && t <= i && clean(t, i)) {
cand.add(new Block(t, i, op == 0x2b ? "loopYield" : "loop"));
}
}
List<Block> accepted = new ArrayList<>();
Set<Integer> elseAccepted = new HashSet<>();
cand.sort((a, b) -> a.open != b.open ? Integer.compare(a.open, b.open) : Integer.compare(b.close, a.close));
for (Block b : cand) {
int end = b.kind.equals("if") && elseOf.containsKey(b.open) ? elseOf.get(b.open) : b.close;
boolean ok = true;
for (Block a : accepted) {
int ae = a.kind.equals("if") && elseAccepted.contains(a.open) ? elseOf.get(a.open) : a.close;
ok &= nests(b.open, end, a.open, ae);
if (ok && b.kind.equals("if") && elseOf.containsKey(b.open)) {
ok &= nests(b.open, b.close, a.open, ae) && nests(b.close, end, a.open, ae);
}
if (ok && a.kind.equals("if") && elseAccepted.contains(a.open)) {
ok &= nests(b.open, end, a.open, a.close) && nests(b.open, end, a.close, ae);
}
}
if (ok && accepted.stream().noneMatch(a -> a.open == b.open)) {
accepted.add(b);
if (elseOf.containsKey(b.open) && b.kind.equals("if")) {
elseAccepted.add(b.open);
}
}
}
Map<Integer, Block> ifAt = new HashMap<>(), loopAt = new HashMap<>(), loopEnd = new HashMap<>();
Map<Integer, Integer> closeCount = new HashMap<>(), elseAt = new HashMap<>();
Set<Integer> elseJumps = new HashSet<>();
Map<String, Integer> consumed = new HashMap<>();
for (Block b : accepted) {
if (b.kind.equals("if")) {
ifAt.put(b.open, b);
consumed.merge(ls.get(b.close).label(), 1, Integer::sum);
if (elseAccepted.contains(b.open)) {
int m = elseOf.get(b.open);
elseAt.put(b.close, b.open);
elseJumps.add(prevInstr(b.close));
consumed.merge(ls.get(m).label(), 1, Integer::sum);
closeCount.merge(m, 1, Integer::sum);
} else {
closeCount.merge(b.close, 1, Integer::sum);
}
} else {
loopAt.computeIfAbsent(b.open, k -> b);
loopEnd.put(b.close, b);
consumed.merge(ls.get(b.open).label(), 1, Integer::sum);
}
}
Map<String, Integer> refs = new HashMap<>();
for (StratListing.Line l : ls) {
if (l.kind() == StratListing.Kind.INSTR || l.kind() == StratListing.Kind.DATA) {
for (String w : l.text().split("[ ,@+]+")) {
if (labelIdx.containsKey(w)) {
refs.merge(w, 1, Integer::sum);
}
}
}
}
for (String s : src.strats.values()) {
refs.merge(StratListing.ident(s), 1, Integer::sum);
}
for (int i = 0; i < ls.size(); i++) {
StratListing.Line l = ls.get(i);
switch (l.kind()) {
case HEADER, COMMENT -> {
if (l.text().startsWith(";")) {
out(-1, "//" + l.text().substring(1));
}
}
case LABEL -> {
flush();
Integer c = closeCount.get(i);
for (int k = 0; c != null && k < c; k++) {
depth--;
out(-1, "}");
}
if (elseAt.containsKey(i)) {
depth--;
out(-1, "} else {");
depth++;
}
int r = refs.getOrDefault(l.label(), 0) - consumed.getOrDefault(l.label(), 0);
if (src.strats.containsKey(l.offset())) {
out(l.offset(), "");
out(l.offset(), "strat " + src.strats.get(l.offset()) + ":");
} else if (r > 0
|| l.label().startsWith("entry_")
|| l.label().startsWith("ev_")) {
out(l.offset(), l.label() + ":");
}
if (loopAt.containsKey(i)) {
out(-1, "loop {");
depth++;
}
}
case DATA -> {
flush();
out(l.offset(), l.text());
}
case INSTR -> instr(i, l, ifAt, loopEnd, elseJumps);
}
}
flush();
}
private static boolean nests(int a0, int a1, int b0, int b1) {
return a1 <= b0 || b1 <= a0 || (a0 >= b0 && a1 <= b1) || (b0 >= a0 && b1 <= a1);
}
/** No data lines between the two line indexes. */
private boolean clean(int a, int b) {
for (int k = Math.min(a, b); k <= Math.max(a, b); k++) {
if (ls.get(k).kind() == StratListing.Kind.DATA) {
return false;
}
}
return true;
}
private int prevInstr(int i) {
for (int k = i - 1; k >= 0; k--) {
if (ls.get(k).kind() == StratListing.Kind.INSTR) {
return k;
}
if (ls.get(k).kind() != StratListing.Kind.LABEL) {
return -1;
}
}
return -1;
}
private void out(int off, String t) {
lines.add(new Line(off, Math.max(0, depth), t));
}
private void flush() {
while (!stack.isEmpty()) {
out(stackAt, "push " + stack.pollLast());
}
stackAt = -1;
}
/** Offset where the statement being emitted starts: its first pushed operand if the stack is now empty. */
private int begin(int off) {
return stack.isEmpty() && stackAt >= 0 ? stackAt : off;
}
private String pop() {
return stack.isEmpty() ? "pop()" : stack.pop();
}
private void push(int off, String e) {
if (stack.isEmpty() && stackAt < 0) {
stackAt = off;
}
stack.push(e);
}
private static String strip(String e) {
return e.startsWith("(") && e.endsWith(")") && balanced(e.substring(1, e.length() - 1))
? e.substring(1, e.length() - 1)
: e;
}
private static boolean balanced(String s) {
int d = 0;
for (char c : s.toCharArray()) {
d += c == '(' ? 1 : c == ')' ? -1 : 0;
if (d < 0) {
return false;
}
}
return d == 0;
}
private void instr(
int i, StratListing.Line l, Map<Integer, Block> ifAt, Map<Integer, Block> loopEnd, Set<Integer> elseJumps) {
int op = l.op();
String mn = mn(l);
String a = operand(l);
String c = l.comment() == null ? "" : " // " + l.comment();
int off = l.offset();
if (PUSH.containsKey(op)) {
push(off, PUSH.get(op) + a);
return;
}
if (ADDR.containsKey(op)) {
push(off, "&" + ADDR.get(op) + a);
return;
}
if (POP.containsKey(op)) {
String v = pop();
int b0 = begin(off);
flush();
out(b0, POP.get(op) + a + " = " + strip(v) + c);
return;
}
if (op == StratOps.PUSH_SHORT
|| op == StratOps.PUSH_IMM
|| op == StratOps.PUSH_ADDR
|| op == StratOps.PUSH_SOUND) {
push(off, a + (l.comment() != null && l.comment().startsWith("strat") ? " /*" + l.comment() + "*/" : ""));
return;
}
if (op == 0x47 || op == 0x48) {
String v = pop();
int b0 = begin(off);
flush();
out(b0, (v.startsWith("&") ? v.substring(1) : "*" + v) + (op == 0x47 ? " += 1.0" : " -= 1.0"));
return;
}
if (BINARY.containsKey(mn)) {
String b = pop(), x = pop();
push(off, "(" + x + " " + BINARY.get(mn) + " " + b + ")");
return;
}
if (op == 0x46) {
push(off, "-" + pop());
return;
}
if (op == 0xaf) {
push(off, "!" + pop());
return;
}
boolean cond = op >= 0x12 && op <= 0x15;
if (cond) {
String e = strip(pop());
boolean neg = op == 0x13 || op == 0x15;
String yield = op <= 0x13 ? " /*yield*/" : "";
int b0 = begin(off);
flush();
if (ifAt.containsKey(i)) {
out(b0, "if (" + (neg ? "!(" + e + ")" : e) + ")" + yield + " {");
depth++;
} else {
out(b0, "if (" + (neg ? e : "!(" + strip(e) + ")") + ") goto " + a + yield);
}
return;
}
if (op == 0x16 || op == 0x2b) {
flush();
Block lp = loopEnd.get(i);
if (lp != null) {
if (op == 0x2b) {
out(off, "yield");
}
depth--;
out(-1, "}");
} else if (elseJumps.contains(i)) {
if (op == 0x2b) {
out(off, "yield");
}
} else {
out(off, (op == 0x2b ? "yield; " : "") + "goto " + a);
}
return;
}
if (op == 0x11 || op == 0x10) {
flush();
out(off, "call " + a + (op == 0x10 ? " /*yield*/" : "") + "()");
return;
}
if (op == StratOps.JUMP_POP) {
String v = pop();
flush();
out(off, v.equals("pop()") ? "return" : "goto *" + v);
return;
}
if (op == StratOps.END || op == 0x8e) {
flush();
out(off, mn);
return;
}
if (op == StratOps.NAME) {
flush();
out(off, "debugName " + a);
return;
}
if (op == StratOps.HANDLER) {
flush();
out(off, "onEvent " + a + c);
return;
}
OpTable.Op info = ops.op(op);
int pops = info.pops(), pushes = info.pushes();
String inline = a;
if (op == StratOps.SPAWN || op == 0x94 || op == 0x95) {
String first = a.split(",")[0].trim();
Integer n = Num.parse(first);
pops = n == null ? -1 : n + 1;
}
if (pops < 0
|| pushes < 0
|| pushes > 1
|| l.text().startsWith(".")
|| op == StratOps.SKIP_A
|| op == StratOps.SKIP_B
|| op == StratOps.SWITCH) {
flush();
out(off, l.text() + c);
return;
}
List<String> args = new ArrayList<>();
for (int k = 0; k < pops; k++) {
args.add(0, strip(pop()));
}
String call = mn + (inline.isEmpty() ? "" : "<" + inline + ">") + "(" + String.join(", ", args) + ")";
if (pushes == 1) {
push(off, call);
} else {
int b0 = begin(off);
flush();
out(b0, call + c);
}
}
}
@@ -179,3 +179,12 @@ dialog.Viewport navigation* = 04-navigation.md#navigation-presets
dialog.Keyboard shortcuts = 19-shortcuts.md
dialog.Shortcut sheet = 19-shortcuts.md
dialog.Getting started = tutorial-first-level.md
# ---- strat scripts
panel.strats.script = 22-strats.md#the-script-panel
inspector.x:Script = 22-strats.md#the-script-panel
action.strats.show = 22-strats.md#the-script-panel
action.strats.newType = 22-strats.md#your-own-object-types
dialog.Edit script constant = 22-strats.md#changing-numbers
dialog.New object type from script = 22-strats.md#your-own-object-types
dialog.Clone* = 22-strats.md#your-own-object-types
@@ -0,0 +1,435 @@
# opcode name operands pops pushes semantics confidence reference (Croc 2 PSX jump table / Croc 1 name)
0x00 nop_0x00 - 0 0 No-op (shared empty handler 00426500); unused in levels. high stCommandError
0x01 hide imm 0 0 arg!=0 sets, arg==0 clears flag 0x1000000 in +0xE8; renderer and shadow pass skip strats with this bit, so it hides the strat. medium stHide
0x02 skipArgs count 0 0 Inline block: count word, then count push instructions (0x44 one word, 0x45/0xea two words) that are skipped without executing; data block read by 0x0c event descriptors (same use count as 0x0c). medium -
0x03 print count 0 0 Debug print with output stripped: skips count inline push instructions like 0x02 and advances debug text line y DAT_006d9db8 by 12 (reset to 0x20/0x48 each frame). medium stPrint
0x04 pushVar var 0 1 Pushes own variable n (+0xF4 array). high stLocal
0x05 pushCodeWord rel 0 1 Pushes the 32-bit word at pc+arg*4 (inline data in the code stream, pc = next instruction). high -
0x06 pushGlobal global 0 1 Pushes global n (0x6D9E8C[n]). high stGlobal
0x07 pushProp prop 0 1 Pushes own property n via FUN_00550E60(self,self,n). high stAlienVar
0x08 pushVarAddr var 0 1 Pushes address of own variable n (for 0x47/0x48/0x4d). high stLocalAddress
0x09 pushCodeAddr rel 0 1 Pushes address pc+arg*4 (address of inline code-stream data word). high -
0x0a pushGlobalAddr global 0 1 Pushes address of global n. high stGlobalAddress
0x0b pushPropAddr prop 0 1 Pushes address of own property n (FUN_005509F0(self,self,n)). high stAlienVarAddress
0x0c onEvent rel,imm 0 0 Installs event handler slot (2nd word) in table +0xF8 (16-byte slots): descriptor at word pc+1+off holds type, param, rel code address of the handler; type 0x40 = timer (param converted to frames), types 0x80/0x100 enable player-distance tracking (+0xEC bit 0x4000). high -
0x0d clearEvent imm 0 0 Clears event handler slot arg (type word := 0). high -
0x0e disableEvent imm 0 0 Disables event handler slot arg (sets bit 31 of its type word). high -
0x0f enableEvent imm 0 0 Re-enables event handler slot arg (clears bit 31 of its type word). high -
0x10 callYield rel 0 1 Pushes return address (next pc), jumps to next+arg*4 and yields (+0xE8 bit 2); unused in levels. high stJsr
0x11 call rel 0 1 Pushes return address (next pc) and jumps to next+arg*4 without yielding; the call form actually used by the compiler. high stJsrImm
0x12 jzYield rel 1 0 Pops cond; if 0 jumps to next+arg*4; always yields this frame. high stBeq
0x13 jnzYield rel 1 0 Pops cond; if !=0 jumps to next+arg*4; always yields this frame. high stBne
0x14 jz rel 1 0 Pops cond; if 0 jumps to next+arg*4. high stBeqImm
0x15 jnz rel 1 0 Pops cond; if !=0 jumps to next+arg*4; unused. high stBneImm
0x16 jmp rel 0 0 Jumps to next+arg*4. high stJumpImm
0x17 switch rel 1 0 Pops value; table at next+arg*4+4: count, then entries (case value as 0x44 short or 0x45 imm, then rel offset); on match jumps to next+offset*4, otherwise falls through. high stIndex_Jump
0x18 pushParentVar var 0 1 Pushes variable n of the linked strat at +0xC (parent/creator). high stExt_Local
0x19 pushParentVarAddr var 0 1 Pushes address of variable n of the +0xC strat. high stExt_LocalAddress
0x1a pushParentProp prop 0 1 Pushes property n of the +0xC strat. high stExt_AlienVar
0x1b pushParentPropAddr prop 0 1 Pushes address of property n of the +0xC strat; unused. high stExt_AlienVarAddress
0x1c pushPlayerProp playerProp 0 1 Pushes property n of the player strat (DAT_006D9E1C, set by 0xb5), 0 if none. high stPlayer_AlienVar
0x1d pushPlayerPropAddr playerProp 0 1 Pushes address of player property n (0 if no player); unused. high stPlayer_AlienVarAddress
0x1e pushCameraProp prop 0 1 Pushes property n of the camera strat (DAT_006D9E28, set by 0xc8), 0 if none. high stCamera_AlienVar
0x1f pushCameraPropAddr prop 0 1 Pushes address of camera property n (0 if none); unused. high stCamera_AlienVarAddress
0x20 pushTargetProp prop 0 1 Pushes property n of the camera target strat (DAT_006D9E30, set by 0xc9 from the player character strats), 0 if none. medium stTarget_AlienVar
0x21 pushTargetPropAddr prop 0 1 Pushes address of target property n (0 if none); unused. medium stTarget_AlienVarAddress
0x22 error_0x22 - 0 0 Prints "stCollide_AlienVar" not-implemented error; unused. high stCollide_AlienVar
0x23 error_0x23 - 0 0 Prints "stCollide_AlienVarAddress" not-implemented error; unused. high stCollide_AlienVarAddress
0x24 pushTarget2Prop prop 0 1 Pushes property n of the second target strat (DAT_006D9E34, set by 0xca), 0 if none; unused. medium stTarget_AlienVar
0x25 pushTarget2PropAddr prop 0 1 Pushes address of target2 property n (0 if none); unused. medium stTarget_AlienVarAddress
0x26 pushBossProp prop 0 1 Pushes property n of the strat in DAT_006D9E24 (set by 0xef), no null check; unused. low stBoss_AlienVar
0x27 pushBossPropAddr prop 0 1 Pushes address of property n of the DAT_006D9E24 strat; unused. low stBoss_AlienVarAddress
0x28 pushDialogProp prop 0 1 Pushes property n of the strat in DAT_006D9E2C (set by 0x113, used by NPC/puzzle strats), 0 if none. low stDialog_AlienVar
0x29 pushDialogPropAddr prop 0 1 Pushes address of property n of the DAT_006D9E2C strat (0 if none); unused. low stDialog_AlienVarAddress
0x2a shadow imm 0 0 arg!=0 sets, 0 clears +0xE8 bit 0x20 (shadow on; shadow pass requires it plus size from 0x59 and texture from 0x5a). medium stShadow
0x2b jmpYield rel 0 0 Jumps to next+arg*4 and yields this frame (loop back-edge / wait one frame). high -
0x2c nop_0x2c - 0 0 No-op (shared empty handler); unused. high -
0x2d nop_0x2d - 0 0 No-op (shared empty handler); unused. high -
0x2e nop_0x2e - 0 0 No-op (shared empty handler); unused. high -
0x2f flagEC10000 imm 0 0 arg!=0 sets, 0 clears +0xEC bit 0x10000 (with 0x8000 both set the strat skips player-distance updates; property 0xf2 reads it); meaning unclear. low -
0x30 trackPlayer imm 0 0 If no player yet: re-executes itself next frame (pc-=4, yield); else arg sets/clears +0xEC bit 0x4000 = update distance to player each frame (+0x148). medium -
0x31 flagEC2 imm 0 0 arg sets/clears +0xEC bit 2 (checked by collision/floor code via +0xC0, readable as property 0x1f). low -
0x32 flagE8_80000000 imm 0 0 arg sets/clears +0xE8 bit 31; unused. low -
0x33 flagEC100 imm 0 0 arg sets/clears +0xEC bit 0x100 (tested in the collision response, skips floor handling; used by drops/projectiles/rollercoaster). low -
0x34 flagEC200000 imm 0 0 arg sets/clears +0xEC bit 0x200000 (used by NPC helpers/canoe/llama). low -
0x35 nop_0x35 - 0 0 No-op (shared empty handler); unused. high -
0x36 nop_0x36 - 0 0 No-op (shared empty handler); unused. high -
0x37 popPlayerProp playerProp 1 0 Pops value and stores it into player property n (discarded if no player). high -
0x38 popCameraProp prop 1 0 Pops value into camera strat property n (discarded if none). high -
0x39 popTargetProp prop 1 0 Pops value into camera target (DAT_006D9E30) property n (discarded if none). medium -
0x3a popTarget2Prop prop 1 0 Pops value into DAT_006D9E34 strat property n (discarded if none); unused. medium -
0x3b popDialogProp prop 1 0 Pops value into DAT_006D9E2C strat property n (discarded if none); unused. low -
0x3c error_0x3c - 0 0 Prints "stCollide_AlienVar_Equals" not-implemented error; unused. high -
0x3d popBossProp prop 1 0 Pops value into DAT_006D9E24 strat property n (no null check); unused. low -
0x3e popParentVar var 1 0 Pops value into variable n of the +0xC strat. high -
0x3f popParentProp prop 1 0 Pops value into property n of the +0xC strat; unused. high -
0x40 popProp prop 1 0 Pops value into own property n (via FUN_005509F0 address). high -
0x41 popVar var 1 0 Pops value into own variable n. high -
0x42 popCodeWord rel 1 0 Pops value into the code-stream word at pc+arg*4 (self-modifying inline data, pairs with 0x05). high -
0x43 popGlobal global 1 0 Pops value into global n. high -
0x44 pushShort imm 0 1 Pushes the sign-extended 16-bit arg (raw, not shifted to 20.12). high stNumber
0x45 push imm 0 1 Pushes inline 32-bit word (handler shared with 0xd7). high stNumber
0x46 neg - 1 1 Pops a, pushes -a. high stUMinus
0x47 inc - 1 0 Pops address, adds 1.0 (0x1000) to the word there. high stIncrease
0x48 dec - 1 0 Pops address, subtracts 1.0 (0x1000) from the word there. high stDecrease
0x49 add - 2 1 Pops b, a; pushes a+b. high stAdd
0x4a sub - 2 1 Pops b, a; pushes a-b. high stSub
0x4b mul - 2 1 Pops b, a; pushes fixed-point a*b>>12. high stMul
0x4c div - 2 1 Pops b, a; pushes fixed-point (a<<8)/b<<4. high stDiv
0x4d store - 2 0 Pops value, then address; stores value at address; unused (compiler uses pop ops). high stEquals
0x4e eq - 2 1 Pops b, a; pushes a==b (0/1, raw). high stCompare
0x4f lt - 2 1 Pops b, a; pushes a<b. high stLessThan
0x50 shl - 2 1 Pops b, a; pushes a << (b>>12) (b fixed shift count). high stShiftLeft
0x51 shr - 2 1 Pops b, a; pushes a >> (b>>12) arithmetic (also stashes a in 0x6DA2D8). high stShiftRight
0x52 animToFrame - 1 0 Pops frame (fixed); steps current anim frame +0x110 forward (wrapping) until it reaches frame (clamped to last frame of anim +0x14), applying each frame to the model. medium -
0x53 gt - 2 1 Pops b, a; pushes a>b. high stGreaterThan
0x54 setModel - 1 0 Pops model address; sets +0x10 and updates +0xEC bit 0x20 / +0xE8 bit 0x80 from model header fields. high stSetModel
0x55 scale - 1 0 Pops scale (fixed); sets x/y/z scale +0x60/+0x64/+0x68. high stScale
0x56 scaleX - 1 0 Pops scale (fixed) into +0x60. high stScaleX
0x57 scaleY - 1 0 Pops scale (fixed) into +0x64. high stScaleY
0x58 scaleZ - 1 0 Pops scale (fixed) into +0x68. high stScalez
0x59 shadowSize - 1 0 Pops shadow size (fixed) into +0x12C (shadow drawn only if >=0x80). high stShadowSize
0x5a shadowTexture - 1 0 Pops shadow texture index (TEXT texture) into +0x128 (short). medium stShadowType
0x5b error_0x5b - 0 0 Prints "stFlash" not-implemented error; unused. high stFlash
0x5c error_0x5c - 0 0 Prints "stTrans" not-implemented error; unused. high stTrans
0x5d moveUp - 1 0 Pops dist (fixed); moves strat by local (0,+d,0) via FUN_005501D0. high stMoveUp
0x5e moveDown - 1 0 Pops dist; moves by local (0,-d,0). high stMoveDown
0x5f moveForward - 1 0 Pops dist; moves by local (0,0,+d) along its facing. high stMoveForward
0x60 moveBackward - 1 0 Pops dist; moves by local (0,0,-d). high stMoveBackward
0x61 moveLeft - 1 0 Pops dist; moves by local (+d,0,0). high stMoveLeft
0x62 moveRight - 1 0 Pops dist; moves by local (-d,0,0). high stMoveRight
0x63 turnRight - 1 0 Pops angle; yaw +0x24 -= angle (24-bit wrap; 4096.0 = full turn). high stTurnRight
0x64 turnLeft - 1 0 Pops angle; yaw +0x24 += angle. high stTurnLeft
0x65 tiltLeft - 1 0 Pops angle; roll +0x28 -= angle. high stTiltLeft
0x66 tiltRight - 1 0 Pops angle; roll +0x28 += angle. high stTiltRight
0x67 tiltForward - 1 0 Pops angle; pitch +0x20 += angle. high stTiltForward
0x68 tiltBackward - 1 0 Pops angle; pitch +0x20 -= angle; unused. high stTiltBackward
0x69 turnToPlayerX - 1 0 Pops max step (angle); turns pitch +0x20 toward the player by at most step; unused. high stTurnToPlayerX
0x6a turnToPlayerY - 1 0 Pops max step (angle); turns yaw +0x24 toward the player by at most step. high stTurnToPlayerY
0x6b turnToPlayerXY - 1 0 Pops max step (angle); turns yaw and pitch toward the player. high stTurnToPlayerXY
0x6c turnToX - 4 0 Pops step, z, y, x; turns pitch toward point (x,y,z) by at most step. high stTurnToX
0x6d turnToY - 4 0 Pops step, z, y, x; turns yaw toward point (x,y,z) by at most step. high stTurnToY
0x6e turnToXY - 4 0 Pops step, z, y, x; turns yaw and pitch toward point (x,y,z). high stTurnToXY
0x6f wobble - 1 0 Pops and discards one value (unimplemented; shared pop handler); unused. medium stWobble
0x70 resetPos - 0 0 Restores 8 words (angles+position, +0x20..+0x3C) from the saved copy at +0xA0. high stReSetPos
0x71 savePos - 0 0 Saves 8 words (+0x20..+0x3C angles+position) to +0xA0 for 0x70. high stSetPos
0x72 nop_0x72 - 0 0 No-op (shared empty handler); unused. medium stJump
0x73 wpFirst - 0 0 Current waypoint +0x120 := first waypoint +0x118. high stWPFirst
0x74 wpLast - 0 0 Current waypoint +0x120 := last waypoint +0x11C. high stWPLast
0x75 wpNext - 0 0 Current waypoint := its next link; wraps to first. high stWPNext
0x76 wpPrev - 0 0 Current waypoint := its prev link; wraps to last. high stWPPrev
0x77 error_0x77 - 0 0 Prints "stWpdel" not-implemented error; unused. high stWPDel
0x78 error_0x78 - 0 0 Prints "stWpnew" not-implemented error; unused. high stWPNew
0x79 wpNearest - 0 0 Sets current waypoint to the one nearest the strat position; unused. high stWPNearest
0x7a wpFurthest - 0 0 Sets current waypoint to the one furthest from the strat (excluding last); unused. high stWPFurthest
0x7b wpTurnToX - 1 0 Pops max step (angle); turns pitch toward the current waypoint (pos at wp+0x18). high stWPTurnToX
0x7c wpTurnToY - 1 0 Pops max step (angle); turns yaw toward the current waypoint (if any). high stWPTurnToY
0x7d wpTurnToXY - 1 0 Pops max step (angle); turns yaw and pitch toward the current waypoint. high stWPTurnToXY
0x7e animPlay - 1 0 Pops anim address; sets +0x14, frame +0x110 := 0, sets +0xE8 bit 8 (playing) and applies frame 0 to the model. high stAnimPlay
0x7f animStop - 0 0 Clears +0xE8 bit 8 (anim playing). high stAnimStop
0x80 animClear - 0 0 Clears the current anim pointer (+0x14) and flag 0x8 in +0xE8 (no anim playing). medium stAnimClear
0x81 animSetSpeed - 0 0 Unimplemented on PC: only logs "stAnimsetspeed" via the debug print; no stack effect. high stAnimSetSpeed
0x82 collisionType - 1 0 pops type (raw int); clears bits 0x42C of +0xEC then sets 1->0x8, 2->0x400, 3->0x20, 4->none, other->0x4 (collision shape/type flags). high stCollisionType
0x83 collRadius - 1 0 pops radius; stores its low 16 bits raw into collision radius short +0xD2. high stCollRadius
0x84 collHeight - 0 0 Unimplemented: logs "stCollheight" only. high stCollHeight
0x85 collExtent - 1 0 pops extent; stores its low 16 bits raw into short +0xC4. medium stCollextent
0x86 collView - 0 0 Unimplemented: logs "stCollview" only. high stCollView
0x87 nop_0x87 - 0 0 Shared no-op handler (0x426500); unused. Slot of Croc2 stCollPoints. low stCollPoints
0x88 nop_0x88 - 0 0 Shared no-op handler (0x426500); unused. Slot of Croc2 stCollSetPoint. low stCollSetPoint
0x89 createTrigger - 0 0 Sets flag 0x40 in +0xE8 (strat becomes a trigger/touchable, used by pickups like Health, Carrot). medium stCreateTrigger
0x8a killTrigger - 0 0 Clears flags 0x40 and 0x4000000 in +0xE8 (undoes createTrigger). medium stKillTrigger
0x8b wait - 1 0 pops frames (fixed, >>12) into wait counter short +0xFE and sets flag 0x2 in +0xE8 (strat sleeps until the counter runs out). high stWait
0x8c hold - 0 0 Sets flags 0x12 (0x2 wait + 0x10 stopped) in +0xE8: suspends the strat indefinitely until released. medium stHold
0x8d release - 0 0 Unimplemented: logs "stRelease" only. high stRelease
0x8e remove - 0 0 Sets flags 0x6 in +0xE8 (yield + kill); same handler as 0xb4 end. high stRemove
0x8f nop_0x8f - 0 0 Shared no-op handler (0x426500); unused. Slot of Croc2 stMapRemove. low stMapRemove
0x90 mapAdd - 0 0 Unimplemented: logs "stMapadd" only. high stMapAdd
0x91 mapReplace - 0 0 Unimplemented: logs "stMapreplace" only. high stMapReplace
0x92 activated - 0 0 Sets bit 0x8 in byte +0x44 of the strat's map-instance record (+0xF0): marks the map object as activated (persistent state). medium stActivated
0x93 collected - 0 0 Sets bit 0x4 in byte +0x44 of the map-instance record (+0xF0): marks the map object as collected so it does not respawn. medium stCollected
0x94 spawnLegacy count,imm,imm,flags,imm ? 0 pops code address of the new strat, then argCount (first inline word) argument values (popped inside FUN_0054bfc0); spawns a child strat at this strat's position (+0x20 block); inline words 3-5 are spawn parameters/flags (word 3 tested for 0x40000/0x80000/0x8000 collision-info allocation). Unused in shipped levels. medium stSpawn
0x95 spawnFrom count,imm,imm,flags,imm,imm ? 0 Like spawn (pops code address + args) but with an extra inline block giving a local offset (x/y/z) that is rotated by the strat's matrix (+0x70..) and added to its translation (+0x94..): spawn at an offset relative to this strat. Unused. medium stSpawnFrom
0x96 link - 0 0 Unimplemented: logs "stLink" only. high stLink
0x97 unlink - 0 0 Unimplemented: logs "stUnlink" only. high stUnlink
0x98 soundShift - 2 0 pops pitch (fixed, >>12), then sound channel; changes pitch of a playing sound channel owned by this strat (FUN_00546400). medium stSoundShift
0x99 soundStop - 1 0 pops sound channel; stops that channel if owned by this strat, or (channel <0/invalid) all sounds of this strat. high stSoundStop
0x9a musicPlay - 1 0 pops music track index (fixed, >>12); if different from the current track (0x586430) starts that ASF music stream (FUN_00546b70). high stCdPlay
0x9b streamPlay - 0 0 Unimplemented: logs "stStreamPlay" only. high -
0x9c speechSetPos - 1 0 pops a value (fixed, >>12) into global 0x6DA2B8 (meaning unknown, likely speech volume/range) and moves the speech/voice stream (handle 0x6D94B4, started by '#' text) to this strat's position. low -
0x9d discard_0x9d - 1 0 pops a value (>>12) and passes it to the no-op debug print; effectively drop. Croc2 slot midiVolume/cdFade. low stMidiVolume
0x9e speechStop - 0 0 Stops the current speech/voice stream (handle 0x6D94B4) if one is playing and resets the handle to -1. high stMidiStop
0x9f streamQueue - 0 0 Unimplemented: logs "stStreamQueue" only. high stMidiQueue
0xa0 isLight - 0 0 Unimplemented: logs "stIsLight" only. high stIsLight
0xa1 lightCol - 0 0 Unimplemented: logs "stLightCol" only. high stLightCol
0xa2 lightFade - 0 0 Unimplemented: logs "stLightFade" only. high stLightFade
0xa3 lightAtten - 0 0 Unimplemented: logs "stLightAtten" only. high stLightAtten
0xa4 lightType - 0 0 Unimplemented: logs "stLightType" only. high stLightType
0xa5 collisionOn flags 0 0 ORs the inline flag word into +0xE8; if any of 0x34000 is set also sets 0x4000 in +0xEC. high stCollisionOn
0xa6 collisionOff flags 0 0 Clears the inline flag word's bits in +0xE8. high stCollisionOff
0xa7 collisionOffAll - 0 0 Clears all collision flag bits 0x7E000 in +0xE8. high stCollisionOffAll
0xa8 soundPlayEx - 3 0 pops pitch (fixed), volume (fixed), sound id; in game states 3/4 plays the sound on this strat and sets its volume and pitch (no channel returned). high stSoundPlay
0xa9 soundPlay4_0xa9 - 4 0 pops 4 values and discards them (stubbed sound-play variant). medium stSoundPlay
0xaa soundPlayAssEx - 3 1 pops pitch, volume, sound id; plays it like 0xa8 and pushes the channel (raw int), or -1 if not in game state 3/4. high stSoundPlayAss
0xab soundPlayAss4_0xab - 4 0 pops 4 values and discards them (same stub as 0xa9; pushes nothing). medium stSoundPlayAss
0xac int - 1 1 pops x; pushes x & 0xFFFFF000 (fixed-point floor/truncate to integer). high stInt
0xad sin - 1 1 pops angle (fixed, 4096 table units = full turn); pushes sin from table 0x574318 (20.12). high stSin
0xae cos - 1 1 pops angle; pushes cos (table 0x574318 offset by quarter turn 0x400). high stCos
0xaf not - 1 1 pops x; pushes 1 if x==0 else 0. high stNot
0xb0 pop - 1 0 Drops the top stack value (stack pointer +0x104 -= 4). high stPop
0xb1 stkCmp imm 0 1 Peeks (does not pop) the top stack entry, dereferences it as a pointer and pushes imm minus the pointed-to int (compare helper). low stStkCmp
0xb2 address addr 0 1 Patches itself into 0x45 (push immediate) after resolving the operand to an absolute address (negative = import table 0x6DA324, else + STPC base 0x6D9DBC) and pushes it (code/model/anim address). high stAddress
0xb3 return - 1 0 pops code address and jumps there (pc = popped). high stReturn
0xb4 end - 0 0 Sets flags 0x6 in +0xE8 (yield + kill): ends the strat. Same handler as 0x8e. high stEndStrat
0xb5 isPlayer - 0 0 Registers this strat as the player (global 0x6D9E1C = strat) and sets flag 0x800 in +0xE8. high stIsPlayer
0xb6 and - 2 1 pops b, a; pushes 1 if both are non-zero else 0 (logical and). high stAnd
0xb7 or - 2 1 pops b, a; pushes a | b (bitwise, used as logical or). high stOr
0xb8 bitwiseAnd - 2 1 pops b, a; pushes a & b. high stBitwiseAnd
0xb9 discard_0xb9 - 1 0 Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 ext-var slots). medium -
0xba discard_0xba - 1 0 Shared stub 0x554fb0: pops one value and discards it. Unused. medium -
0xbb nop_0xbb - 0 0 Shared no-op handler (0x426500); unused. low -
0xbc notEqual - 2 1 pops b, a; pushes 1 if a != b else 0. high stNotEqual
0xbd greaterEqual - 2 1 pops b, a; pushes 1 if a >= b else 0. high stGreaterEqual
0xbe lessEqual - 2 1 pops b, a; pushes 1 if a <= b else 0. high stLessEqual
0xbf rnd - 1 1 pops n; pushes random value (two rand() halves) modulo n, 0 if n==0; result is in the same units as n (fixed in, fixed out). high stRnd
0xc0 loseHeart - 0 0 In game state 3, unless player invulnerable (0x584710 bit 0): health 0x584E70 -= 1.0, or if already 0 sets 0x584638 = 2 (player dies); refreshes HUD (FUN_004268c0(1)). high stLoseHeart
0xc1 nop_0xc1 - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stResettoCheckPoint. low stResettoCheckPoint
0xc2 forceCollision - 0 0 Runs world collision for this strat (FUN_00403020 with level geometry): updates floor/ceiling/wall info in the collision block +0xC0 (ground height, surface type). medium stForceCollision
0xc3 turnFromPlayerY - 1 0 pops max turn step (angle, 0x1000000 = full turn); turns yaw +0x24 toward the direction from the player (0x6D9E1C) to this strat, i.e. away from the player, by at most step. medium stTurnFromPlayerY
0xc4 rumble - 2 0 Shared stub 0x555090: pops two values and discards them (PSX pad rumble/vibrate not implemented on PC). low stRumble
0xc5 suspendIfTooFar - 1 0 Shared stub 0x554fb0: pops one value (likely distance) and discards it. low stSuspendIfTooFar
0xc6 collisionBone - 2 0 pops value (short), then bone id (fixed, >>12); sets/adds entry {id, value} in the bone table at +0xC8 (count +0xC6). medium stCollisionBone
0xc7 useBone - 1 0 pops one value (ignored); if a model/anim is set (+0x14) rebuilds the rotation matrix and stores a transformed point (matrix * local vector + translation +0x94) into +0xD8/+0xDC/+0xE0. low stUseBone
0xc8 isCamera - 0 0 Registers this strat as the camera (global 0x6D9E28) and sets flag 0x8000000 in +0xE8. high stIsCamera
0xc9 lookAtMe - 0 0 Sets camera look-at target global 0x6D9E30 = this strat. high stLookatme
0xca lookAtMe2 - 0 0 Sets secondary look-at global 0x6D9E34 = this strat. Unused. medium stLookatme
0xcb pushCamera - 0 0 Pushes current camera (0x6D9E28) and look-at (0x6D9E30) pair onto the camera stack 0x6D9DCC (index 0x6D9E40). high stPushCamera
0xcc popCamera - 0 0 If the camera stack is not empty, restores camera and look-at from it and increments camera-change counter 0x6D9D34. high stPopCamera
0xcd nop_0xcd - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stResetCamerapos. low stResetCamerapos
0xce gainHeart - 0 0 Health 0x584E70 += 1.0 if below max 0x584E6C; refreshes HUD (FUN_004268c0(1)). high stGainHeart
0xcf nop_0xcf - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stGainHeartPot. low stGainHeartPot
0xd0 addInv imm 0 0 Adds inline item to inventory via FUN_00426860(item,1,1,0,0), which is a stub returning 0 on PC (no effect). medium stAddInv
0xd1 nop_0xd1 - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stGainCrystal. low stGainCrystal
0xd2 discard_0xd2 - 1 0 Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 slot stCutscene). low stCutscene
0xd3 inventory imm 0 1 Queries inline item via stub FUN_00426860 and pushes result<<12 (always 0 on PC). medium stInventory
0xd4 debugName imm,count 0 0 Strat name: skips the first inline word, reads the byte length of the following name string, sets the name pointer (+0x18) to that string and jumps past it. high stDebugname
0xd5 soundPlay - 1 0 pops sound id; plays it on this strat (FUN_00546170); outside game states 3/4 first sets game state 0x5846E4 = 5. high stSoundPlay
0xd6 soundPlayAss - 1 1 pops sound id; in game states 3/4 plays it and pushes the channel (raw int), else pushes -1. high stSoundPlayass
0xd7 soundAddress sound 0 1 Pushes the inline sound id (same handler as 0x45 push immediate). high stSoundAddress
0xd8 nop_0xd8 - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stOnground/stObjectFallslow. low stOnground
0xd9 collisionOffset - 3 0 pops z, y, x; stores low 16 bits into shorts +0xD0, +0xCE, +0xCC (collision centre offset). high stCollisionOffset
0xda abs - 1 1 pops x; pushes |x|. high stAbs
0xdb pickup imm 0 0 inline flag: nonzero sets bit 0x200 in +0xEC, zero clears it (strat can be picked up/carried). medium stPickup
0xdc min - 2 1 pops b, a; pushes the smaller (signed). high stMin
0xdd max - 2 1 pops b, a; pushes the larger (signed). high stMax
0xde spawnParticle - 6 0 pops param (texture or colour, type-dependent), vz, vy, vx (velocity, fixed), scale (fixed, 0x1000 = default), particle type (raw int); emits a particle at this strat's position (FUN_00428ea0). medium stSpawnparticle
0xdf sgn - 1 1 pops x; pushes 1.0 (0x1000), -1.0 or 0 by sign. high stSgn
0xe0 spawn count,count,imm,flags,imm ? 0 Spawns a child strat via FUN_0054bfc0: pops code address of the new strat (top), then pops operand1 (param count) values from the parent stack as the child's MAP parameters; operand2 = local variable count of the child, operand4 = spawn flags (ORed with own +0x138; bits 0x40000/0x80000/0x8000 select model/collision setup), operands 3/5 extra sizes/flags; child copies parent orientation/position block (+0x20) and waypoint fields; fails silently (params still popped) if strat pool full medium stSpawn
0xe1 nop_0xe1 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stDontLookatme?
0xe2 nop_0xe2 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stRunat?
0xe3 moveForwardQ - 1 0 pops distance (fixed); x(+0x30) += d*sin(yaw), z(+0x38) += d*cos(yaw), yaw = +0x24 (4096 units per turn, table 0x574318) high stMoveForwardq
0xe4 moveBackwardQ - 1 0 pops distance (fixed); moves the strat backwards along its yaw (x -= d*sin, z -= d*cos) high stMoveBackwardq
0xe5 playSoundVol - 2 0 pops volume (fixed, >>12), sound id; if game state DAT_005846e4 is 3 or 4 plays the sound for this strat (FUN_00546170) and sets the channel volume (+0x6a, FUN_00546360) high stSoundPlay
0xe6 playSoundChannel - 2 1 pops volume (fixed), sound id; if game state DAT_005846e4==3 plays the sound with that volume and pushes the channel index (raw int, not fixed), else pushes -1 high stSoundPlayass
0xe7 setWP - 0 0 saves the current waypoint: +0x124 = +0x120 high stSetWP
0xe8 resetWP - 0 0 restores the saved waypoint: +0x120 = +0x124 high stResetWP
0xe9 soundVolume - 2 0 pops volume (fixed), sound channel; sets the channel volume (+0x6a) if the channel belongs to this strat (FUN_00546360) high stSoundVolume
0xea nop_0xea - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stString?
0xeb setBossHearts - 1 0 pops count (fixed); sets boss heart max DAT_00584f0c and current DAT_00584f10 to count high stSetbossHearts
0xec loseBossHeart - 0 0 decrements boss hearts DAT_00584f10 (not below 0) and refreshes the HUD (FUN_004268c0(1)) high stLosebossHeart
0xed soundShiftRelative - 2 0 pops shift/pitch (fixed, >>12), sound channel; sets channel +0x70 and updates a playing voice (FUN_00546460) if the channel belongs to this strat medium stSoundShiftrelative
0xee smin - 2 1 pops limit (top), value; pushes value with its magnitude clamped to limit, keeping the sign (sign(v)*min(|v|,limit)) high stSmin
0xef isBoss - 0 0 registers this strat as the boss: DAT_006d9e24 = self high stIsboss
0xf0 topSay text 0 0 shows dialog text (inline text index) in the top text box: sets DAT_006d9858=1 and formats via FUN_0040d4c0 high stTopsay
0xf1 getParentPos - 0 0 copies parent's (+0xC) orientation+position block (+0x20..+0x3C, 8 words) into this strat high stGetparentpos
0xf2 setDrawMode - 1 0 pops mode (raw int): 1 sets flag 0x80000 in +0xEC, 2 sets 0x100000, other clears both (exact effect of the bits unknown); unused low -
0xf3 afterBoss - 0 0 if no boss (DAT_006d9e24) rewinds pc and yields (waits); else relinks this strat in the process list right after the boss high stAfterboss
0xf4 afterPlayer - 0 0 if no player (DAT_006d9e1c) rewinds pc and yields (waits); else relinks this strat in the process list right after the player high stAfterPlayer
0xf5 beforePlayer - 0 0 if no player rewinds pc and yields (waits); else relinks this strat in the process list right before the player high stBeforePlayer
0xf6 nop_0xf6 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stBeforeboss?/stNohang?
0xf7 pushZero - 0 1 pushes 0 (same handler as 0x1a7); unused high stZero
0xf8 nop_0xf8 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stTophead?
0xf9 popDiscard_0xf9 - 1 0 pops and discards one value (stub FUN_00554fb0, shared with 0x6f etc.); unused high stTopdialog?
0xfa bottomSay text 0 0 shows dialog text (inline text index) in the bottom text box: sets DAT_006d94e8=2 and formats via FUN_0040d4c0 high stBottomsay
0xfb nop_0xfb - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stBottomhead?
0xfc popDiscard_0xfc - 1 0 pops and discards one value (stub FUN_00554fb0); unused high stBottomdialog?
0xfd getPlayerPos - 0 0 if a player exists copies its orientation+position block (+0x20..+0x3C) into this strat high stGetPlayerpos
0xfe getWPPos - 0 0 sets rotation x/y/z (+0x20..+0x28) from the current waypoint's (+0x120) ints <<12 and position (+0x30..+0x3C) from waypoint +0x18..+0x24 high stGetWPpos
0xff getBossPos - 0 0 copies the boss's (DAT_006d9e24) orientation+position block (+0x20..+0x3C) into this strat (no null check); unused high stGetbosspos
0x100 getDoorPos - 0 0 finds the level door record (DAT_00584648+0x24) in this strat's x/z cell nearest in y; if found sets current door DAT_00584644, places the strat at the door position/yaw (FUN_0054de00) and, unless door flag 2, moves it forward by the door's distance (+0x38) medium stGetdoorpos
0x101 fadeOut - 1 0 pops speed selector (fixed, 1/2/3/other) and starts a screen fade-out (FUN_00415390 mode 1/5/0xb/3); sets DAT_00584638=6 unless it is 7 or 10 medium stFadeout
0x102 fadeIn - 1 0 pops speed selector (fixed, 1/3/other) and starts a screen fade-in (FUN_00415390 mode 2/10/4); DAT_00584638=0 medium stFadein
0x103 moveUpQ - 1 0 pops distance (fixed); y(+0x34) += d high stMoveUpq
0x104 moveDownQ - 1 0 pops distance (fixed); y(+0x34) -= d high stMoveDownq
0x105 forcePlayerDist - 0 0 recomputes the cached squared player distance +0x148 (coords >>6) and clears +0x1C; 0x200000 if no player; unused high stForcePlayerdist
0x106 nop_0x106 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stShadeType?
0x107 setAnimSpeed - 1 0 pops value and stores its low 16 bits (raw, not >>12) in halfword +0x114 (animation timing field, zeroed when the anim frame is set) low stSetanimSpeed?
0x108 nop_0x108 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stCheckleveldoor?
0x109 nop_0x109 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x10a nop_0x10a - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x10b nop_0x10b - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stGainjigsaw?
0x10c nop_0x10c - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stGaingoldengobbo?
0x10d gainSecret - 0 0 increments the collected-secrets counter DAT_00584e64 (copied to the HUD/stats at level reset); used only by strat Secret high stGainCrystal?
0x10e nop_0x10e - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x10f nop_0x10f - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); used once (LlamaChase) high stCheckPoint?
0x110 setMainCameraFlag - 0 0 sets flag 0x8000000 in own flags +0xE8 (same flag the camera-register opcode FUN_005500f0 sets, without registering DAT_006d9e28); used by CameraCollisionInFront medium stIsmainCamera?
0x111 nop_0x111 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x112 nop_0x112 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x113 isDialog - 0 0 registers this strat as the dialog partner: DAT_006d9e2c = self high stIsdialog
0x114 distance - 3 1 pops z (top), y, x; pushes the 3D distance from the strat's position (+0x30) to (x,y,z) (FUN_00404bb0, float sqrt -> int) high stDistance
0x115 showLevelStartInfo imm 0 0 inline word: low byte stored in DAT_006da294 (level start info / title card index), logs 'show level start info %d' medium -
0x116 topCloseDialog - 0 0 closes the top dialog text box: DAT_006d9858 = 0 high stTopclosedialog
0x117 bottomCloseDialog - 0 0 closes the bottom dialog text box: DAT_006d94e8 = 0 high stBottomclosedialog
0x118 nop_0x118 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stNextinventory?
0x119 nop_0x119 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stPrevinventory?
0x11a otherPiece - 0 0 finds the switchable map piece at the strat's grid cell (FUN_00553f10, table DAT_00584648+0x50) and sets it to its alternate state (*piece[5] = piece[4]), swapping its texture/collision table entries if DAT_006da330 bit 0x10000000 medium stOtherpiece
0x11b normalPiece - 0 0 same as otherPiece but restores the piece's normal state (*piece[5] = piece[3]) medium stNormalpiece
0x11c delInv imm 0 0 inline word (inventory item id) passed to FUN_00426860(item,1,-1,0,0), which is a stub returning 0 in this build: effectively no-op; unused medium stDelinv
0x11d nop_0x11d - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x11e nop_0x11e - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x11f nop_0x11f - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x120 nop_0x120 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x121 nop_0x121 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x122 nop_0x122 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x123 nop_0x123 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x124 nop_0x124 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
0x125 moveLeftQ - 1 0 pops distance (fixed); strafes: x += d*cos(yaw), z += d*sin(yaw-half turn) (= -d*sin) high stMoveLeftq
0x126 moveRightQ - 1 0 pops distance (fixed); strafes opposite to moveLeft (x -= d*cos(yaw), z += d*sin(yaw)) high stMoveRightq
0x127 bitwiseNot - 1 1 pops v; pushes ~v high stBitwisenot
0x128 bordersOn - 0 0 enables cinematic letterbox borders: DAT_005fd090 = 1 high stBorderson
0x129 bordersOff - 0 0 disables letterbox borders: DAT_005fd090 = 0 high stBordersoff
0x12a soundAdsr - 5 0 pops and discards 5 values (stubbed); unused high stSoundadsr
0x12b soundAdsrRelative - 3 0 pops a (top), b (fixed), sound channel; if the channel belongs to this strat sets channel +0x34 = b>>12, +0x38 = a>>12 (FUN_005463b0, envelope/range params) medium stSoundadsrrelative
0x12c nop_0x12c - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stRotatepiece?
0x12d setAmbient - 3 0 pops b, g, r (fixed, >>12, top = b) into bytes +0x135/+0x134/+0x133 and enables the own ambient light colour (+0x136 = 1) high stSet_ambient
0x12e resetAmbient - 0 0 disables the own ambient colour: +0x136 = 0; unused high stReset_ambient
0x12f invActive - 0 0 calls inventory stub FUN_00426860(0,1,0,1,0) (returns 0, no effect in this build); unused high stInvactive
0x130 invInactive - 0 0 calls inventory stub FUN_00426860(0,1,0,1,1) (no effect); unused high stInvinactive
0x131 sampleStatus - 1 1 pops sound id; pushes (fixed) the channel index on which this strat is playing that sound, or -1.0 if not playing (FUN_005461e0); unused high stSamplestatus
0x132 nop_0x132 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stResettoCheckPointnlh?
0x133 resetDoor - 0 0 restores the stored door (DAT_006da28c -> DAT_00584644) and, if a player exists, places the player at that door (FUN_0054de00) and restores its saved orientation/position block from DAT_006d9e48 high stResetdoor
0x134 storeDoor - 0 0 stores the current door (DAT_00584644 -> DAT_006da28c) and the player's orientation/position block (+0x20..+0x3C) into DAT_006d9e48 high stStoredoor
0x135 cameraModified - 0 0 if a camera strat (DAT_006d9e28) exists copies this strat's orientation/position block into it and its position into globals DAT_006d9d20..2c; unused medium stCamera_modified
0x136 pushPlayer - 0 0 saves the current player pointer DAT_006d9e1c into DAT_006d9e20 (logs an error if no player); unused high stPushPlayer
0x137 popPlayer - 0 0 restores the player pointer (DAT_006d9e1c and DAT_006d9e30) from DAT_006d9e20 and clears it; unused high stPopPlayer
0x138 resetToStart - 0 0 restores position +0x30..+0x38 from the saved (start) position +0xB0..+0xB8; unused high stReSetPostrn
0x139 gainItem - 0 0 increments item counter DAT_005fd09c up to the maximum DAT_005fd0a4; unused high stGainitem
0x13a setItem - 1 0 pops maximum (fixed); DAT_005fd0a4 = max, counter DAT_005fd09c = 0, enables item counter display (DAT_005fd0a0 = 1); unused high stSetitem
0x13b setTimer - 1 0 pops time (fixed, >>12) into DAT_005fd0ac and enables the timer (DAT_005fd0a8 = 1); unused high stSettimer
0x13c timerOff - 0 0 disables the timer: DAT_005fd0a8 = 0; unused high stTimeroff
0x13d distanceNoY - 2 1 pops z (top), x; pushes the horizontal (x/z) distance from the strat's position to (x,z) (FUN_00404c50) high stDistancenoy
0x13e nop_0x13e - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stSwim?
0x13f nop_0x13f - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stLoseCrystal?
0x140 nop_0x140 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x141 setDebugFlag5849a0On - 0 0 Writes short 1 to global 0x5849A0; that global is never read anywhere in groove.exe (dead PSX leftover). medium -
0x142 setDebugFlag5849a0Off - 0 0 Writes short 0xFFFF to global 0x5849A0 (never read; dead leftover). medium -
0x143 clearFlag5fd0b8 - 0 0 Sets global 0x5FD0B8 = 0; global is never read elsewhere (dead leftover). medium -
0x144 setFlag5fd0b8 - 0 0 Sets global 0x5FD0B8 = 1; global is never read elsewhere (dead leftover). medium -
0x145 nop_0x145 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x146 nop_0x146 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x147 nop_0x147 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x148 gotoLevel4_1 - 0 0 Hardcoded level change: next level (0x584EB4/EB8/EBC/EC0) = 4/1/1/0, previous level (0x585008..14) = 4/1/4/0, game state 0x584638 = 7 (load level). medium -
0x149 nop_0x149 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x14a hudReset2 - 0 0 Calls HUD/panel init FUN_004268c0(2): re-initialises the on-screen counter panels (coins, lives, etc.) for the current level. low -
0x14b setGlobal5fd086 imm 0 0 Stores the low 16 bits of the inline word in global 0x5FD086 (never read elsewhere; dead leftover). medium -
0x14c removeCollPoint - 1 0 pops id (fixed, >>12); removes the collision point with that id from the strat's collision point list (+0xC8 array of 4-byte entries, count +0xC6). Counterpart of 0xc6 (add/set collision point). medium stSetCollPoint (inverse)
0x14d setFlagEC_400000 flags 0 0 Inline word: nonzero sets, zero clears bit 0x400000 of strat flags2 (+0xEC); the bit selects which height constant own property 0xF2 returns. high -
0x14e atan2 - 2 1 pops b (top), a; pushes angle atan2(a, b) as (0..0xFFF)<<12, i.e. full turn = 0x1000000 (4096.0); used with a=dx, b=dz like yaw-to-target. high -
0x14f nop_0x14f - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x150 nop_0x150 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing (used once in HelpGirlSkip). high -
0x151 nop_0x151 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x152 nop_0x152 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x153 findNearestLight - 3 1 pops z (top), y, x; searches the level light table (0x586598, 0x1C-byte entries, count 0x5865D0) for the nearest light of type 2 (point light) to (x,y,z) and pushes its pointer (0 if none). medium -
0x154 setLightIntensity - 2 0 pops intensity (fixed; top), light pointer (from 0x153); stores intensity in light+0x18, rebuilds the light (FUN_0042c4a0) and re-flags/relights the map polygons within its radius (0 = off, 0x1000 = full). medium -
0x155 nop_0x155 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x156 nop_0x156 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x157 nop_0x157 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x158 nop_0x158 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x159 nop_0x159 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x15a setPosToPlayer - 0 0 Copies the player's position x/y/z (+0x30..+0x38) and +0x3C into the strat (player strat = 0x6D9E1C). high -
0x15b setPosToPlayerXZ - 0 0 Copies the player's x and z (+0x30, +0x38) into the strat; y unchanged. high -
0x15c setShadowHeight - 1 0 pops v; stores it in strat +0x14C, used by the shadow renderer as the shadow/ground height override (-0x1000 = use default 0x586424). low -
0x15d packRgb - 3 1 pops c (top), b, a (fixed 0..255); pushes (c<<16)|(b<<8)|a as a raw packed colour int (push order r,g,b gives 0x00BBGGRR). high -
0x15e trailCreate - 2 1 pops vertB (top), vertA (fixed vertex indices of the strat's model/anim frame); allocates a free trail/ribbon slot (0x5FD180, 0x42C bytes each) owned by this strat and pushes its handle (0 if none free). medium -
0x15f trailAddPoint - 3 0 pops colourB (top), colourA, trail handle; transforms the two trail vertices of the current anim frame into world space and appends them to the trail history (20 points) with the given colours; no-op if handle 0. medium -
0x160 trailFree - 1 0 pops trail handle; releases it (clears owner +0x414) if nonzero. high -
0x161 setPauseState - 1 0 pops v (fixed); sets global pause state 0x584640 = v>>12 (0 = not paused, 1 = paused, 2 = pausing disabled). medium -
0x162 setFlagE8_1000 flags 0 0 Inline word: nonzero sets, zero clears bit 0x1000 of strat flags (+0xE8). high -
0x163 setCollOffset - 3 0 pops z (top), y, x; stores them as 16-bit shorts into the strat's collision descriptor (+0xC0) at +0x32/+0x34/+0x36 = collision sphere centre offset. medium -
0x164 angleDiff - 2 1 pops b (top), a (angles, full turn 0x1000000); pushes (a-b) wrapped to the signed half-turn range (-0x800000..0x7FF000). high -
0x165 collideAsPlayer - 0 0 Sets flag 0x800 on self (excludes self), temporarily makes self the player (0x6D9E1C) and runs the player-vs-strat collision pass FUN_004051d0, then restores the player; lets projectiles (LlamaSpit) hit strats like the player does. medium -
0x166 setGlobalTint - 3 0 pops b (top), g, r (fixed, clamped 0..255); sets the global strat tint 0x6D9E14..16 and enables it, and applies it to every strat that has no own tint yet (+0x133..+0x136). high -
0x167 screenOverlay flags 0 0 Inline word: nonzero enables the full-screen colour overlay (0x583740 = 1, colour reset to 0), zero disables it. high -
0x168 setScreenOverlayColour - 4 0 pops d (top), c, b, a (fixed 0..255, >>12); sets the full-screen overlay colour to a<<24 | d<<16 | b<<8 | c (a = alpha; d,b,c land in the R,G,B byte positions of the D3D colour). medium -
0x169 nop_0x169 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x16a turnTowardStratY - 2 0 pops maxStep (top, angle), target strat pointer; turns own yaw (+0x24) toward the target's position by at most maxStep (full turn 0x1000000). high stTurnTowardY
0x16b pushSelf - 0 1 Pushes the pointer of the executing strat (this). high -
0x16c nop_0x16c - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x16d nop_0x16d - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x16e lerp - 3 1 pops t (top), b, a; pushes b + (a-b)*t (fixed): t=0 gives b, t=1.0 gives a. high -
0x16f clamp - 3 1 pops hi (top), x, lo; pushes max(lo, min(x, hi)) (push order lo, x, hi). high -
0x170 distSqToPos - 3 1 pops z (top), y, x; pushes squared 3D distance from own position, computed on coordinates >>6 (dx>>6)^2+(dy>>6)^2+(dz>>6)^2. high -
0x171 distSqToPosXZ - 2 1 pops z (top), x; pushes squared horizontal distance from own position, (dx>>6)^2+(dz>>6)^2. high -
0x172 approach - 3 1 pops step (top), target, value; pushes value moved toward target by step without overshooting. high -
0x173 distSqXZ - 4 1 pops z2 (top), x2, z1, x1; pushes (x2-x1)^2 + (z2-z1)^2 in fixed point. high -
0x174 turnTowardAngleY - 2 0 pops maxStep (top), targetYaw; turns own yaw (+0x24) toward targetYaw by at most maxStep, wrapping (full turn 0x1000000). high stTurnTowardY
0x175 setFlagEC_80 flags 0 0 Inline word: nonzero sets, zero clears bit 0x80 of strat flags2 (+0xEC). high -
0x176 inRectXZ - 6 1 pops maxZ (top), maxX, minZ, minX, z, x; pushes integer 1 (not 0x1000) if minX<=x<=maxX and minZ<=z<=maxZ, else 0. high -
0x177 waypointStep - 1 0 pops n (fixed); moves current waypoint (+0x120) n steps forward via next links (wraps to first +0x118) or -n steps back via prev links (wraps to last +0x11C). high stNextWaypoint/stPrevWaypoint
0x178 inWaypointBoxXZ - 2 1 pops z (top), x; pushes integer 1 if (x,z) lies inside the axis-aligned XZ box spanned by the current waypoint and the next one (wrapping to first), else 0. high -
0x179 nearestWaypoint - 0 0 Sets current waypoint (+0x120) to the waypoint of the list (+0x118) nearest (XZ) to the strat. high stNearestWaypointNext
0x17a nearestWaypointToPlayer - 0 0 Sets current waypoint to the waypoint nearest (XZ) to the player. high -
0x17b nearestOtherWaypoint - 0 0 Sets current waypoint to the waypoint nearest (XZ) to the strat, excluding the current one. high -
0x17c nop_0x17c - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x17d nop_0x17d - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x17e nop_0x17e - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x17f nop_0x17f - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x180 addTint - 3 0 pops b (top), g, r (fixed deltas); adds them to the strat's tint (own +0x133..135, or the level ambient at 0x584644+0x4C if none), clamps 0..255, stores and enables own tint (+0x136). high -
0x181 setAnimFrame - 1 0 pops frame (fixed); if an anim is set (+0x14) sets current frame +0x110 (out of range/negative -> 0), clears +0x114 and sets flag 8 (anim changed). high -
0x182 nop_0x182 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x183 showSaveSlotInfo - 1 0 pops slot (fixed 1..3); draws the save-game slot info text ('Gespeichertes Spiel n', chapter, etc.) for the main menu (FUN_0040db90). medium -
0x184 pushTintR - 0 1 Pushes own tint red byte (+0x133) as fixed. high -
0x185 pushTintG - 0 1 Pushes own tint green byte (+0x134) as fixed. high -
0x186 pushTintB - 0 1 Pushes own tint blue byte (+0x135) as fixed. high -
0x187 emitterInit - 7 0 pops colour/texture (top, raw), z, y, x (vector/offset), type (fixed 1..0x2B preset, or >=0x1000 type flags), size/speed (fixed, 0x1000 default), slot (0..2, >=3 = extra slot +0x153); (re)initialises the strat's particle emitter in that slot (records at 0x5FE230, 0x34 bytes; texture/colour stored per type). medium stSpawnparticle
0x188 emitParticles - 3 0 pops yStep (top, fixed), count, slot; emits count particles from the slot's emitter at own position with random XZ velocity, each raised by yStep. medium -
0x189 randSigned - 1 1 pops n; pushes a random value in (-n, n) ((rand32 - rand32) % n, same fixed scale as n). high -
0x18a emitterSetParams - 5 0 pops e (top), d, c, b, slot; sets emitter slot params: +0x254 = b (rate/speed), +0x24A = c>>12 (life); if e==0 +0x25C = (d>>12)*32-32 else +0x246 = e, +0x23C = d. Allocates the slot if empty. low -
0x18b emitterSetColour - 5 0 pops colour (top, raw packed, see 0x15d), b, g, r, slot; sets emitter colour (+0x250) and fade/alpha (+0x240..243): if r has no fraction it is an alpha value only, else r,g,b are per-channel fade values. low -
0x18c emitParticlesStill - 3 0 pops yStep (top, raw fixed), count, slot; emits count particles from the slot's emitter at own position without velocity, stacked by yStep. medium -
0x18d stubPop2_0x18d - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
0x18e stubPop2_0x18e - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
0x18f clampToWaypointBox - 0 0 Clamps own x/z (+0x30/+0x38) into the XZ box spanned by the current waypoint and the next one. high -
0x190 processBeforeCamera - 0 0 If no camera strat (0x6D9E28) yet: re-executes next frame (pc-=4, yield). Else unlinks self from the strat list (0x6D9E38) and reinserts it directly before the camera so it updates before the camera. high -
0x191 processBeforeCamera2 - 0 0 Same as 0x190 (waits for camera, then moves self just before the camera in the update list); functionally identical alias. high -
0x192 endLevel - 3 0 pops sub (top), level, world (fixed); saves current level as previous, sets next level, sets 'all coins collected'/completion flags, updates furthest-progress 0x585018, state 0x584638 = 10 and starts the fade-out (world=level=sub=7 means return to the level map). medium stEndlevel
0x193 stubPop1Push0_0x193 - 1 1 Stub FUN_005550b0: pops 1 value and pushes 0. high -
0x194 stubPop1Push0_0x194 - 1 1 Stub FUN_005550b0: pops 1 value and pushes 0. high -
0x195 stubPop2_0x195 - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
0x196 stubPop2_0x196 - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
0x197 nop_0x197 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x198 nop_0x198 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x199 nop_0x199 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x19a nop_0x19a - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x19b nop_0x19b - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x19c nop_0x19c - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x19d sqrt - 1 1 pops v (fixed); pushes fixed sqrt(v) (fsqrt of v/4096, rescaled). high -
0x19e nop_0x19e - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x19f nop_0x19f - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x1a0 setWaypointParam1 - 1 0 pops v; sets current waypoint +0x08 = (v>>12)&0xFFF (readable as own property 9). high -
0x1a1 setWaypointParam2 - 1 0 pops v; sets current waypoint +0x0C = (v>>12)&0xFFF (readable as own property 10). high -
0x1a2 setWaypointParam3 - 1 0 pops v; sets current waypoint +0x10 = (v>>12)&0xFFF (readable as own property 11). high -
0x1a3 stubPop1_0x1a3 - 1 0 Stub FUN_00554fb0: pops and discards 1 value. high -
0x1a4 stubPop1_0x1a4 - 1 0 Stub FUN_00554fb0: pops and discards 1 value. high -
0x1a5 nop_0x1a5 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
0x1a6 emitterKill - 1 0 pops slot (fixed); frees the strat's particle emitter in that slot (+0x150+slot = 0xFF) and resets the record. high -
0x1a7 stubPush0_0x1a7 - 0 1 Stub FUN_0054e880: pushes 0. high -
0x1a8 processFirst - 0 0 Moves self to the head of the strat update list (0x6D9E38) if not already there. high -
0x1a9 setRain - 4 0 pops colour (top, raw -> 0x6D9BE8), b (->0x578EF4), a2 (->0x6D9BF4), a1 (->0x578EF0) (all >>12); if the three counts/params are 0 rain is stopped and cleared, else rain is enabled (0x6D9BE4 = 1). low -
0x1aa stubPop1Push0_0x1aa - 1 1 Stub FUN_005550b0: pops 1 value and pushes 0. high -
0x1ab stubPop2_0x1ab - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
0x1ac stubPop1Push0_0x1ac - 1 1 Stub FUN_005550b0: pops 1 value and pushes 0. high -
0x1ad stubPop2Push0_0x1ad - 2 1 Stub FUN_005550d0: pops 2 values and pushes 0. high -
0x1ae stubPop3_0x1ae - 3 0 Stub FUN_005550f0: pops and discards 3 values. high -
0x1af setWeatherRender - 1 0 pops mode (fixed): 0 installs the normal rain/weather update+render callbacks (0x5FF034/38), 1 installs no-op callbacks and clears 0x6D94D0 (weather off). medium -
0x1b0 emitParticlesVel - 6 0 pops vz (top), vy, vx (velocity), yStep, count, slot (last three fixed >>12); emits count particles from the slot's emitter at own position with the given velocity, stacked by yStep. medium -
0x1b1 nop_0x1b1 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
Can't render this file because it contains an unexpected character in line 36 and column 30.
@@ -0,0 +1,195 @@
# id name get set semantics confidence
0 zero_0 yes no Always pushes 0 (unused id; also 0x13, 0x15, 0x47, 0x75, 0x76, 0x77, 0x98, 0x9e, 0xc1, 0xd8, 0xed, 0xee, 0x102 push 0). high
1 unk_1 yes no DAT_006d94d0 << 12 (int global, cleared in FUN_00426... init; meaning unknown). low
3 wpEndFlags yes no Current waypoint (+0x120) link bits: bit0 = no previous node (node[0]==0, first), bit1 = no next node (node[1]==0, last). Raw int. high
4 wpParam yes yes Current waypoint field +0x28 (raw 32-bit, per-node parameter). medium
5 distToWp yes no 3D distance from strat position (+0x30) to current waypoint position (wp+0x18), 20.12 (FUN_00404bb0, sqrt). high
6 wpX yes yes Current waypoint x (wp+0x18), 20.12. high
7 wpY yes yes Current waypoint y (wp+0x1C), 20.12. high
8 wpZ yes yes Current waypoint z (wp+0x20), 20.12. high
9 wpRotX yes no Current waypoint int field +0x08 << 12 (node rotation x in 4096 table units, per notes). medium
10 wpRotY yes no Current waypoint int field +0x0C << 12 (node yaw in 4096 table units; e.g. checkpoint respawn yaw). medium
11 wpRotZ yes no Current waypoint int field +0x10 << 12 (third node angle/param). low
12 playerDistToWp yes no 3D distance from the player (0x6D9E1C) to this strat's current waypoint, 20.12; 100.0 (0x64000) if no player. high
13 angleToWp yes no Yaw from the strat to its current waypoint: atan2(dx,dz) (FUN_00404d60), 24-bit angle (0x1000000 = full turn, 4096 table units <<12). high
14 animFrame yes no Current anim frame +0x110 << 12. high
15 animPlaying yes no Flag +0xE8 bit 3 (0x8, anim playing) as 0/1. high
16 floorHeight yes yes Floor height under the strat, collision record (+0xC0)+0x00, 20.12 (0xFFFF0000 = no floor). Get returns 0 if no record. high
17 floorMaterial yes no Floor surface/material id, short at (+0xC0)+0x04, << 12 (0xFFFF = none; 16 = level door trigger). 0 if no record. high
18 ceilingMaterial yes no Short at (+0xC0)+0x30 << 12: material of the surface above (set together with ceiling height +0x28 in the collision code, 0xFFFF none). medium
19 zero_19 yes no Always 0. high
20 slideAngle yes no Short at (+0xC0)+0x2C << 12: direction (atan2 of old->new pos) written when the strat is pushed back off an edge/wall; 4096 units. low
21 zero_21 yes no Always 0 (both branches push 0). high
22 collisionRadius yes yes Collision radius, short at +0xD2 (raw, not shifted). Set writes a full 32-bit word at +0xD2 (also clobbers the short at +0xD4). high
29 isActivated yes no Map-instance record (+0xF0) byte +0x44 bit 0x8 (persistent "activated" state, set by op 0x92). Returns 8 or 0. No null check. high
30 isCollected yes no Map-instance record (+0xF0) byte +0x44 bit 0x4 (persistent "collected", set by op 0x93). Returns 4 or 0. No null check. high
31 onGround yes no Flag +0xEC & 0x2 (standing on a floor; slope/platform code FUN_004054c0 requires it). Returns 2 or 0. medium
32 flagE8_400000 yes no Flag +0xE8 bit 22 (0x400000) as 0/1. low
33 flagE8_800000 yes no Flag +0xE8 bit 23 (0x800000) as 0/1. low
34 x yes yes Position x +0x30, 20.12. high
35 y yes yes Position y +0x34, 20.12. high
36 z yes yes Position z +0x38, 20.12. high
37 rotX yes yes Rotation x (pitch) +0x20; get masks to 24 bits (0x1000000 = full turn), set stores raw. high
38 rotY yes yes Rotation y (yaw) +0x24; 24-bit angle, 0x1000000 = full turn. high
39 rotZ yes yes Rotation z (roll) +0x28; 24-bit angle. high
41 angleToPlayer yes no Yaw from this strat to the player: atan2(px-x, pz-z) (FUN_00404d60(player+0x30, self+0x30)), 24-bit angle; 0 if no player. (Notes called 41 "distance to player" - code is an angle.) high
42 padHeld yes no Joypad buttons currently held, DAT_005FCF20 (bit mask: 0x10 up, 0x40 down, 0x80 left?, 0x20 right?, 0x1/0x8/0x400.. buttons; 0x10000-0x80000 = direction relative to player yaw). high
43 padReleased yes no Buttons released this frame, DAT_005FCF34 (= old & ~new). high
44 padPressed yes no Buttons newly pressed this frame, DAT_005FCF30 (= new & ~old). high
45 distToPlayer yes no 3D distance to the player, cached in +0x1C (computed lazily as sqrt of +0x148, the squared distance updated per frame), 20.12. high
47 scale yes no Scale y +0x64 (used as the uniform scale value), 20.12; see op 0x55. high
48 scaleX yes no Scale x +0x60, 20.12. high
49 scaleY yes no Scale y +0x64, 20.12. high
50 scaleZ yes no Scale z +0x68, 20.12. high
51 stratCount yes no Number of strats in use DAT_006D9DC8 << 12. medium
52 stratCapacity yes no Level header (DAT_00584648)+0x08 << 12 (max strat count of the level; compared against stratCount on spawn). medium
53 distToWpXZ yes no Horizontal (x/z) distance from strat to current waypoint, 20.12 (FUN_00404c50). high
54 boneX yes no +0xD8: x of the bone/attachment point computed by op 0xc7 (useBone), 20.12 world. medium
55 boneY yes no +0xDC bone point y. medium
56 boneZ yes no +0xE0 bone point z. medium
57 playerPtr yes no Pointer to the player strat (DAT_006D9E1C); 0 if none (usable as "player exists"). high
58 camLookAtX yes yes Camera look-at point x DAT_006D9D80, 20.12 (default 0; cut-scene cameras write it). medium
59 camLookAtY yes yes Camera look-at point y DAT_006D9D84 (default 1.0). medium
60 camLookAtZ yes yes Camera look-at point z DAT_006D9D88. medium
61 tribe yes no Current level tribe DAT_00584EB4 << 12 (level id part 1, %d_%d_%d file names). high
62 world yes no Current level world DAT_00584EB8 << 12. high
63 chapter yes no Current level chapter DAT_00584EBC << 12. high
64 map yes no Current map/sub-level DAT_00584EC0 << 12. high
65 hasCamTarget yes no -1 if a camera target strat (DAT_006D9E30, op 0xc9) is set, else 0. medium
66 hasCamTarget2 yes no -1 if the second camera target (DAT_006D9E34, op 0xca) is set, else 0. medium
67 padType yes no DAT_005FCF18 << 12: 3 = digital pad, 2 = direction/analog input active this frame. low
68 camDistToTarget yes no 3D distance camera strat (0x6D9E28) -> camera target (0x6D9E30), 20.12; 0 if no camera. high
69 camDistToTarget2 yes no 3D distance camera -> second target (0x6D9E34), 20.12; 0 if no camera. high
70 camTargetYawDiff yes no Signed yaw difference target.rotY - camera.rotY (wrapped to +-0x800000, FUN_0054dae0). No null checks. high
72 camDistToTargetXZ yes no Horizontal distance camera -> target (FUN_00404cc0), 20.12. No null check. high
73 camDistToTarget2XZ yes no Horizontal distance camera -> second target, 20.12. No null check. high
75 stickX yes no Analog/digital stick x DAT_005FCF1C (-0x2000..0x1FC0, i.e. about +-2.0 fixed). high
76 stickY yes no Stick y DAT_005FCF2C (same range). high
77 stickMagnitude yes no Stick deflection DAT_005FCEFC, raw int 0..0xB5 (thresholds: 42 deadzone, 84 direction bits, see ids 94-97/130-131). high
78 stickAngle yes no Stick direction DAT_005FCF4E (short, 4096 units) << 12. high
79 stickAngleWorld yes no (stick angle + camera yaw DAT_005FCF00) << 12, masked to 24 bits: stick direction in world yaw. high
80 stick2X yes no DAT_005FCF28, never written by the exe (second stick set, always 0). low
81 stick2Y yes no DAT_005FCF24, never written (always 0). low
82 stick2Magnitude yes no DAT_005FCEF8, never written (always 0). low
83 stick2Angle yes no Short DAT_005FCF4C << 12, never written. low
84 stick2AngleWorld yes no (camera yaw + DAT_005FCF4C) << 12 masked 24 bits. low
85 slopeForward yes no Floor normal rotated into the strat's yaw frame, forward component DAT_0057DDA8 (FUN_004054c0; 0 unless onGround), 20.12. medium
86 slopeSide yes no Floor normal, sideways component in the strat's frame DAT_0057DD90, 20.12. medium
87 slopeAngle yes no Downhill direction relative to the strat's yaw DAT_0057DD94 (0..0xFFF) << 12. medium
88 padAux yes no DAT_005FCF58 (extra input bits, cleared when >1 pads). low
90 padAuxPressed yes no DAT_005FCF60 = new & ~old of padAux. low
91 unk_5b yes no DAT_00584F10 << 12 (game state global, unknown). low
92 health yes no Player health DAT_00584E70 (fixed, starts at 5.0, HUD shows it; reset to 5.0 on respawn). Same value as id 189 (settable there). medium
93 unk_5d yes no DAT_0058464C (raw, game-state pointer/flag). low
94 stickWalkThreshold yes no Constant 84 (0x54, raw int): stick magnitude above which direction bits are generated. medium
95 stickRunThreshold yes no Constant 123 (0x7B, raw int). medium
96 stickWalkThreshold2 yes no Constant 84 (duplicate of 94). medium
97 stickRunThreshold2 yes no Constant 123 (duplicate of 95). medium
98 hasWaypoints yes no 1.0 if the EXECUTING strat (not the queried one) has a first waypoint (+0x118), else 0. For 0x1c (player prop) this still checks the caller. high
99 distToBoss yes no 3D distance from the boss strat (DAT_006D9E24, op 0xef) to this strat, 20.12; 100.0 if none. high
100 distToBossXZ yes no Horizontal distance boss -> this strat, 20.12; 100.0 if none. high
101 zoneX yes no Map zone/cell record containing the strat (FUN_0054d560, same x/z grid cell, nearest y) field +0x0C, integer part (& 0xFFFFF000); -1.0 if none. low
102 zoneY yes no Zone record field +0x10 integer part; -1.0 if none. low
103 zoneZ yes no Zone record field +0x14 integer part; -1.0 if none. low
104 distToWpSq yes no Squared 3D distance to current waypoint ((d>>6)^2 summed, i.e. 20.12 of world units squared). Same as 194. high
105 distToWpSqXZ yes no Squared horizontal distance to current waypoint. high
106 flag83730Clear yes no 1.0 if DAT_00583730 == 0 (FUN_00414fb0), else 0. low
107 distToPlayerXZ yes no Horizontal distance player -> this strat, 20.12; 100.0 if no player. high
108 saveByte5219 yes no DAT_00585219 << 12 (per-save-slot byte, reset to 0 on new game). low
110 queueAEmpty yes no 1.0 if DAT_006D9858 < 1 (message/stream queue A empty), else 0. low
111 queueBEmpty yes no 1.0 if DAT_006D94E8 < 1 (queue B empty; also blocks HUD counters), else 0. low
120 camFloorHeight yes yes Get: floor height of the map cell under the camera DAT_00586424 (-16.0 if outside the map). Set: writes the OVERRIDE DAT_0058641C (non-zero replaces the computed value). Get and set use different variables. medium
121 camX yes yes Camera position x DAT_006D9D20 (copied from the camera strat each frame), 20.12. high
122 camY yes yes Camera position y DAT_006D9D24. high
123 camZ yes yes Camera position z DAT_006D9D28. high
124 flagEC_1000000 yes no Flag +0xEC bit 24 as 0/1. low
125 flagEC_2000000 yes no Flag +0xEC bit 25 as 0/1. low
126 hitDepth yes no Short +0xD4 (raw): penetration depth of the last player-vs-strat sphere hit (FUN_004051d0), 0xFFFF none. medium
127 hitAngle yes no Short +0xD6 << 12: direction of the last sphere hit (4096 units; player gets the opposite of the strat). medium
128 unk_80 yes no DAT_00585004 << 12 (unknown game global). low
129 zoneFlags yes no Zone record (FUN_0054d560) +0x20 & 0xC00, << 12; -1.0 if none. low
130 stickDeadzone yes no Constant 42 (0x2A, raw int). medium
131 stickDeadzone2 yes no Constant 42 (duplicate). medium
132 oldX yes yes Previous-frame position x +0x50, 20.12 (collision code restores it on edge push-back). medium
133 oldY yes yes Previous position y +0x54. medium
134 oldZ yes yes Previous position z +0x58. medium
135 unk_6da2c0 yes no DAT_006DA2C0, no other reference (always 0). low
136 unk_6da2c4 yes no DAT_006DA2C4, no other reference (always 0). low
137 queuesEmpty yes no 1.0 if both queue A and queue B are empty (ids 110 and 111), else 0. low
138 alpha yes yes Transparency +0x10C (renderer: < 0x1000 = translucent, 1.0 = opaque), 20.12. medium
139 oldRotX yes yes Previous rotation x +0x40 (24-bit masked on get). medium
140 oldRotY yes yes Previous yaw +0x44 (compared with +0x24 by the collision code). medium
141 oldRotZ yes yes Previous rotation z +0x48. medium
142 optionFlag80000 yes no DAT_006DA330 & 0x80000 (global option/debug flag word). low
143 camScale yes yes DAT_006D9D30, camera value initialised to 1.0 (zoom/fov factor?). low
144 levelHeaderC yes no Level header (DAT_00584648)+0x0C ushort << 12. low
145 slopeSteepness yes no 1.0 - floor normal y (DAT_0057DDA4) << 12: 0 on flat ground (FUN_004054c0). medium
147 levelTableByte yes no Byte table 0x585024[map + (chapter + tribe*10)*4] << 12 (per-level constant). low
149 flagEC_1 yes no Flag +0xEC & 0x1. low
150 flagEC_8000000 yes no Flag +0xEC & 0x8000000. low
151 cheat5 yes no Cheat toggle bits DAT_00584710 & 0x20 (button-sequence cheats, FUN_0041a0xx). medium
153 frameCounter yes no Strat-update frame counter DAT_006D9E10 (+1.0 per frame). high
154 hitSphere yes no Byte +0x130 << 12: index of the collision sphere hit by the player (FUN_004051d0). medium
155 unk_6d9d38 yes no DAT_006D9D38, no other reference. low
156 unk_6d9cf0 yes no DAT_006D9CF0, no other reference. low
157 isFloorCollisionSolid yes no 1 (raw) if +0xEC has both 0x8000 and 0x10000 set, else 0. low
159 scratch6d9e68 yes yes DAT_006D9E68 (only accessed by strats). low
160 saveEnabled yes yes DAT_006D9E74 (1.0 enables the save-game prompt, reset to 0 on level start). low
161 hudValueA yes yes DAT_006DA300 (HUD element value, shown via >>12, reset per level). low
162 hudValueB yes yes DAT_006DA2F8 (HUD element value, reset per level). low
163 audioMode yes yes DAT_006DA2FC: 3.0/4.0/5.0 from the sound output menu (Stereo/Quatro/Surround); also written (slot+1) by save code. low
164 instanceData0 yes yes Map-instance record (+0xF0)+0x28 -> word 0. Set returns NULL (crash) unless record+0x24 != 0. Get pushes nothing if no record. low
165 instanceData1 yes yes Map-instance record (+0xF0)+0x28 -> word 1. low
166 floorCollisionOn yes no Flag +0xEC & 0x8000 (floor collision, ops 0xa5/0xa6). medium
169 lives yes yes DAT_00584E6C (fixed, starts at 5.0, saved per slot) - lives or max health. low
171 saveProgressA yes no DAT_00585008 << 12 (saved progress byte). low
172 progressWorld yes no DAT_0058500C << 12 (saved; world of the furthest/selected level, 8 = finale). low
173 progressChapter yes no DAT_00585010 << 12 (saved; chapter, 6 with world 8 = last level). low
174 saveProgressB yes no DAT_00585014 << 12 (saved progress byte). low
175 unk_6da308 yes no DAT_006DA308, no other reference. low
178 cheat4 yes no DAT_00584710 & 0x10 (cheat toggle). medium
179 hudFlags yes yes DAT_006DA2E0 HUD element enable mask (default 0x3F000; 0x2000 timer, 0x8000/0x10000/0x20000/0x40000 counters). medium
181 gameState1d74 yes yes DAT_00581D74 (game/menu state, checked == 0 for normal play). low
182 camFlag800000 yes no Camera strat +0xEC & 0x800000 (0 if no camera). low
183 slopeNormalX yes no Floor normal x DAT_0057DD9C (world, 20.12; FUN_004054c0). medium
184 slopeNormalZ yes no Floor normal z DAT_0057DDA0. medium
185 wpParam2 yes yes Current waypoint field +0x2C (raw); 0 / NULL if no waypoint. medium
186 hitRadius yes yes +0x13C: player interaction radius (player collides when squared player distance +0x148 <= r*r), 20.12. medium
187 hitDamage yes yes +0x140: damage dealt on contact (copied into the other strat's +0x144). medium
188 damageTaken yes no +0x144: damage received from the last contact (player cleared to 0 each frame). medium
189 health2 yes yes DAT_00584E70 = health (same as 92, settable). medium
190 levelRecord yes yes Per-level saved word DAT_00584F1C[chapter + world*6] (best/collected value for the current level; HUD shows it). low
191 counterC yes yes DAT_00584FFC: persistent HUD counter (icon 0x150), saved. low
192 counterA yes yes DAT_00584FF4: persistent HUD counter (icon 0x184, displayed up to 999) - probably coins. low
201 distToPlayerSq yes no +0x148: cached squared distance to the player ((d>>6)^2 sum), updated each frame when tracking (op 0x30). high
202 distToPlayerSqXZ yes no Squared horizontal distance player -> strat (FUN_00404c90); 100.0 if no player. high
203 soundVecX yes yes DAT_006DA2CC; on the next set of 203/204 the magnitude sqrt(x^2+y^2)*4 is passed to FUN_00546140 (sound/volume parameter). low
204 soundVecY yes yes DAT_006DA2D0, see 203. low
210 collisionHeight yes yes Collision record (+0xC0)+0x18: strat height/size used for ceiling tests (ceiling hit if ceiling <= y + this), 20.12. Get 0 if no record. medium
211 mapFloorHeight yes no (+0xC0)+0x1C: floor height from the landscape test (FUN_004036d0), 0xFFFF0000 none. No null check. low
212 mapFloorMaterial yes no Short (+0xC0)+0x24 << 12: material of that floor (0xFFFF none). low
213 objFloorHeight yes no (+0xC0)+0x20: floor height from the strat-object test (FUN_00403ad0, skipped with +0xE8 bit 0x400). low
214 objFloorMaterial yes no Short (+0xC0)+0x26 << 12: material of the object floor. low
215 wallMaterial yes no Short (+0xC0)+0x3A << 12 (material of the wall/side surface hit, set with object/face +0x3C/+0x3E). low
217 collisionField38 yes yes Short (+0xC0)+0x38 raw on get; set writes 32 bits at +0x38 (also clobbers +0x3A). low
218 hudDigit yes yes DAT_006DA290: HUD single-digit counter (shown as '0'+value>>12). low
226 animFrameCount yes no Anim header (+0x14)+0x08 << 12 (number of frames). No null check. high
227 angleToCamLookAt yes no Yaw from the strat to the camera look-at point (DAT_006D9D80), 24-bit angle. medium
228 skyParamA yes yes DAT_0057D830 (render global, sky/background). low
229 skyParamB yes yes DAT_0057D83C. low
230 skyParamC yes yes DAT_0057D840. low
231 skyRotation yes yes DAT_0057D834: rotation added to a background model's yaw (>>12 & 0xFFF). low
232 hudTimer yes yes DAT_006DA304: HUD timer/counter value (>>12 displayed, element bit 0x2000). low
233 animPtr yes no Current anim address +0x14 (0 = none). high
234 cheat1 yes no DAT_00584710 & 0x2 (cheat toggle). medium
236 hudRefresh yes yes DAT_006DA2DC (forces HUD counter redraw when non-zero). low
239 unk_57ddb0 yes no DAT_0057DDB0, no other reference. low
240 unk_57ddb4 yes no DAT_0057DDB4, no other reference. low
241 unk_57ddb8 yes no DAT_0057DDB8, no other reference. low
242 drawDistance yes no 1000.0 unless +0xEC bit 0x10000; else (bit 0x400000 ? DAT_0058658C : DAT_00586590) + 0.5 (view/clip distance globals). low
243 ceilingHeight yes no (+0xC0)+0x28: height of the surface above (1000.0 default when ceiling test enabled by +0xE8 0x200000). No null check. medium
244 zoneParam yes no DAT_006DA298: short +0x38 of the zone record the strat entered (op FUN_0054df30, used as conveyor/offset). low
249 wpPtr yes yes Current waypoint pointer +0x120 (set allows jumping to a stored node). high
250 counterB yes yes DAT_00584E60: HUD counter (icon 0x11C), reset when entering a different level. low
253 musicBusy yes no 1 (raw) if FUN_00546600(DAT_006D94B4,0) == 2 (stream/voice state), else 0. low
265 musicTrack yes no Current music track DAT_00586430 << 12 (level header +0x58, 0x23/0x27 for special modes). medium
Can't render this file because it contains an unexpected character in line 24 and column 82.
@@ -0,0 +1,136 @@
package com.eliasmueller.llamamod.editor.tools.strats;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import com.eliasmueller.llamamod.editor.io.Bases;
import com.eliasmueller.llamamod.editor.io.Project;
import com.eliasmueller.llamamod.editor.io.ProjectIO;
import com.eliasmueller.llamamod.editor.io.WadBuilder;
import com.eliasmueller.llamamod.editor.model.Strat;
import com.eliasmueller.llamamod.editor.model.StratCatalog;
import com.eliasmueller.llamamod.editor.model.cmd.Edits;
import com.eliasmueller.llamamod.editor.model.cmd.History;
import com.eliasmueller.llamamod.editor.tools.catalog.CrossImport;
import com.eliasmueller.llamamod.format.Level;
import com.eliasmueller.llamamod.format.MapChunk;
import com.eliasmueller.llamamod.format.Strats;
import com.eliasmueller.llamamod.format.stpcmerge.StpcImage;
import com.eliasmueller.llamamod.format.stpcmerge.StratCode;
import com.eliasmueller.llamamod.format.strat.StratListing;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.junit.jupiter.api.Test;
/** Script edits in a project: constant patch, custom type, save/reload, import afterwards (needs -Dllamamod.cd). */
class StratEditsTest {
static Bases cd() {
String cd = System.getProperty("llamamod.cd", "");
assumeTrue(!cd.isEmpty() && Files.isDirectory(Path.of(cd, "wads")), "original CD not available");
return Bases.fromCd(Path.of(cd));
}
/** The constant pushed right before Coin's turnLeft (spin speed per frame). */
static StratListing.Line spin(Project p, String type) {
int code = p.scene().catalog().type(type).variant(0).code();
int unit = StratEdits.analysis(p.scene()).img().unitOf(code);
List<StratListing.Line> ls = StratEdits.listing(p.scene(), unit).lines;
for (int i = 0; i + 1 < ls.size(); i++) {
if ("turnLeft".equals(ls.get(i + 1).text()) && ls.get(i).constAt() >= 0) {
return ls.get(i);
}
}
throw new AssertionError("no turnLeft constant in " + type);
}
static String nameAt(byte[] wad, int code) {
Level lv = Level.parse(wad);
byte[] st = lv.wad().require("STPC");
return Strats.nameOr(st, code, new StpcImage(st, lv.wfpc()).codeStart, "?");
}
@Test
void constantCloneSaveReloadImport() throws Exception {
Bases cd = cd();
StratEdits.init();
CrossImport.init();
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "edit", "t1l1m001"), cd), cd);
StratListing.Line l = spin(p, "Coin");
assertEquals(0x40000, l.value());
int unit = StratEdits.analysis(p.scene()).img().unitOf(l.offset());
StratEdits.setConstant(p, unit, l.constAt(), l.constShort(), l.value(), 0x100000);
assertEquals(0x100000, spin(p, "Coin").value());
StratEdits.CustomType t = StratEdits.cloneOf(p.scene(), "Coin", "TestCoin");
assertTrue(t.source().contains("debugName \"TestCoin\""), t.source());
String src = t.source().replace("push 22.5deg", "push 90deg");
int code =
StratEdits.addType(p, new StratEdits.CustomType(t.name(), t.basedOn(), t.entryLabel(), src, t.proto()));
assertNotNull(p.scene().catalog().type("TestCoin"));
History h = new History(p.scene());
h.execute(Edits.placeStrat(p.scene(), "TestCoin", 0, 30, 2, 30, 0.0));
Strat placed = p.scene().strats().get(p.scene().strats().size() - 1);
assertEquals(code, placed.code());
assertEquals(0x400000, spin(p, "TestCoin").value());
byte[] wad = WadBuilder.build(p.scene());
assertEquals("TestCoin", nameAt(wad, code));
Project q = ProjectIO.read(ProjectIO.write(p, cd), cd);
assertTrue(q.warnings().isEmpty(), q.warnings().toString());
assertArrayEquals(wad, WadBuilder.build(q.scene()), "reloaded project builds the same WAD");
// import after the custom type: custom units move behind the imports on reload
CrossImport.importInto(q, List.of(new CrossImport.TypeRef("t1l2m001", "Spider", spiderCode(cd))), cd);
StratEdits.updateSession(q);
byte[] wad2 = WadBuilder.build(q.scene());
Project r = ProjectIO.read(ProjectIO.write(q, cd), cd);
assertTrue(r.warnings().isEmpty(), r.warnings().toString());
Strat last = r.scene().strats().get(r.scene().strats().size() - 1);
assertEquals("TestCoin", nameAt(WadBuilder.build(r.scene()), last.code()));
assertEquals(0x400000, spin(r, "TestCoin").value());
assertEquals(0x100000, spin(r, "Coin").value());
Level lv = Level.parse(WadBuilder.build(r.scene()));
byte[] st = lv.wad().require("STPC");
StpcImage img = new StpcImage(st, lv.wfpc());
Set<Integer> roots = new TreeSet<>();
for (MapChunk.Strat s : lv.map().strats) {
roots.add(s.code());
}
assertTrue(StratCode.analyse(img, roots).errors.isEmpty());
assertEquals(wad2.length, WadBuilder.build(r.scene()).length);
}
@Test
void templateAssemblesAndVerifies() throws Exception {
Bases cd = cd();
Project p = Project.create(cd, "edit", "t1l1m001");
StratCatalog.Variant v = p.scene().catalog().type("Coin").variant(0);
StratEdits.Built b = StratEdits.build(
p.scene(), new StratEdits.CustomType("MyStrat", null, "MyStrat", TypeDialog.TEMPLATE, v));
assertTrue(b.problems().isEmpty(), b.problems().toString());
assertEquals("MyStrat", nameAt(wadWith(p, b), b.entry()));
}
static byte[] wadWith(Project p, StratEdits.Built b) {
StratEdits.chunk(p.scene()).setRaw("data", b.stpc());
return WadBuilder.build(p.scene());
}
static int spiderCode(Bases cd) throws Exception {
Level lv = Level.parse(cd.wad("t1l2m001"));
byte[] st = lv.wad().require("STPC");
int lo = new StpcImage(st, lv.wfpc()).codeStart;
for (MapChunk.Strat s : lv.map().strats) {
if ("Spider".equals(Strats.nameOr(st, s.code(), lo, ""))) {
return s.code();
}
}
throw new AssertionError();
}
}
@@ -0,0 +1,84 @@
package com.eliasmueller.llamamod.format.strat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import com.eliasmueller.llamamod.format.Level;
import com.eliasmueller.llamamod.format.stpcmerge.StpcImage;
import com.eliasmueller.llamamod.format.stpcmerge.StratCode;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
/** Disassemble → assemble gives byte-identical units for every strat of every level (needs -Dllamamod.cd). */
class StratRoundTripTest {
static List<Path> levels() throws IOException {
String cd = System.getProperty("llamamod.cd", "");
Path dir = Path.of(cd, "wads");
assumeTrue(!cd.isEmpty() && Files.isDirectory(dir), "original CD not available (llamamod.cd)");
try (Stream<Path> s = Files.list(dir)) {
return s.filter(p ->
p.getFileName().toString().toLowerCase(Locale.ROOT).matches("t\\d[il]\\dm\\d{3}\\.wad"))
.sorted()
.toList();
}
}
record Loaded(StpcImage img, StratCode code, Set<Integer> entries) {}
/** null for WADs without a MAP (front-end test WADs). */
static Loaded load(Path p) throws IOException {
com.eliasmueller.llamamod.format.Wad w = com.eliasmueller.llamamod.format.Wad.parse(Files.readAllBytes(p));
if (!w.has("MAP ") || !w.has("STPC")) {
return null;
}
Level lv = Level.parse(Files.readAllBytes(p));
StpcImage img = new StpcImage(lv.wad().require("STPC"), lv.wfpc());
Set<Integer> roots = new TreeSet<>();
lv.map().strats.forEach(s -> roots.add(s.code()));
return new Loaded(img, StratCode.analyse(img, roots), roots);
}
@Test
void everyUnitOfEveryLevelRoundTrips() throws IOException {
int levels = 0, units = 0, raw = 0, words = 0;
for (Path p : levels()) {
Loaded l = load(p);
if (l == null) {
continue;
}
levels++;
for (int u = 0; u < l.img.units.size(); u++) {
StratListing li = StratListing.of(l.img, l.code, u, l.entries, StratListing.Names.NONE);
String text = li.text();
StratAssembler.Result r = StratAssembler.assemble(text, StratListing.layout(l.img, li.start));
assertArrayEquals(li.words(), r.words(), p.getFileName() + " unit " + u);
units++;
StratPseudo ps = StratPseudo.of(li);
long inRows = ps.constants().stream().mapToLong(List::size).sum();
long all = li.lines.stream().filter(x -> x.constAt() >= 0).count();
assertEquals(
all, inRows, p.getFileName() + " unit " + u + ": constants reachable from the script view");
words += r.words().length;
for (StratListing.Line x : li.lines) {
if (x.kind() == StratListing.Kind.INSTR && x.text().startsWith(".word")) {
raw++;
System.out.println("raw: " + p.getFileName() + " unit " + u + ": " + x.render());
}
}
}
}
System.out.printf(
"round trip: %d levels, %d units, %d words, %d raw instructions%n", levels, units, words, raw);
assertTrue(levels >= 33);
assertEquals(0, raw, "instructions emitted as raw words");
}
}