diff --git a/docs/manual/03-editor-window.md b/docs/manual/03-editor-window.md
index e9c848e..3de8a40 100644
--- a/docs/manual/03-editor-window.md
+++ b/docs/manual/03-editor-window.md
@@ -57,6 +57,7 @@ a saved `.llamalevel` file.
| **Problems** | bottom | yes |
| **Environment** | left | yes |
| **Visibility** | right | yes |
+| **Script** | bottom | yes |
### Outliner
diff --git a/docs/manual/19-shortcuts.md b/docs/manual/19-shortcuts.md
index a002c59..26de492 100644
--- a/docs/manual/19-shortcuts.md
+++ b/docs/manual/19-shortcuts.md
@@ -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
diff --git a/docs/manual/22-strats.md b/docs/manual/22-strats.md
new file mode 100644
index 0000000..7232bd0
--- /dev/null
+++ b/docs/manual/22-strats.md
@@ -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
+
+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**.
+
+
+
+* 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:
+
+
+
+## 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:
+
+
+
+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.
+
+
+
+### 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.
diff --git a/docs/manual/img/strat_bucky_circle.jpg b/docs/manual/img/strat_bucky_circle.jpg
new file mode 100644
index 0000000..5239ed4
Binary files /dev/null and b/docs/manual/img/strat_bucky_circle.jpg differ
diff --git a/docs/manual/img/strat_clone_dialog.jpg b/docs/manual/img/strat_clone_dialog.jpg
new file mode 100644
index 0000000..cdbf68e
Binary files /dev/null and b/docs/manual/img/strat_clone_dialog.jpg differ
diff --git a/docs/manual/img/strat_coin_constant_ab.jpg b/docs/manual/img/strat_coin_constant_ab.jpg
new file mode 100644
index 0000000..e2439b1
Binary files /dev/null and b/docs/manual/img/strat_coin_constant_ab.jpg differ
diff --git a/docs/manual/img/strat_edit_constant.jpg b/docs/manual/img/strat_edit_constant.jpg
new file mode 100644
index 0000000..f07812d
Binary files /dev/null and b/docs/manual/img/strat_edit_constant.jpg differ
diff --git a/docs/manual/img/strat_script_panel.jpg b/docs/manual/img/strat_script_panel.jpg
new file mode 100644
index 0000000..5fd0046
Binary files /dev/null and b/docs/manual/img/strat_script_panel.jpg differ
diff --git a/docs/manual/index.md b/docs/manual/index.md
index 3d287c0..e48a91f 100644
--- a/docs/manual/index.md
+++ b/docs/manual/index.md
@@ -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
diff --git a/notes/img/editor/strat_bucky_circle_sequence.jpg b/notes/img/editor/strat_bucky_circle_sequence.jpg
new file mode 100644
index 0000000..5239ed4
Binary files /dev/null and b/notes/img/editor/strat_bucky_circle_sequence.jpg differ
diff --git a/notes/img/editor/strat_clone_dialog.jpg b/notes/img/editor/strat_clone_dialog.jpg
new file mode 100644
index 0000000..cdbf68e
Binary files /dev/null and b/notes/img/editor/strat_clone_dialog.jpg differ
diff --git a/notes/img/editor/strat_coin_constant_ab.jpg b/notes/img/editor/strat_coin_constant_ab.jpg
new file mode 100644
index 0000000..e2439b1
Binary files /dev/null and b/notes/img/editor/strat_coin_constant_ab.jpg differ
diff --git a/notes/img/editor/strat_edit_constant.jpg b/notes/img/editor/strat_edit_constant.jpg
new file mode 100644
index 0000000..f07812d
Binary files /dev/null and b/notes/img/editor/strat_edit_constant.jpg differ
diff --git a/notes/img/editor/strat_script_panel.jpg b/notes/img/editor/strat_script_panel.jpg
new file mode 100644
index 0000000..5fd0046
Binary files /dev/null and b/notes/img/editor/strat_script_panel.jpg differ
diff --git a/notes/strat-language.md b/notes/strat-language.md
new file mode 100644
index 0000000..1892622
--- /dev/null
+++ b/notes/strat-language.md
@@ -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).
diff --git a/notes/strat-opcodes.md b/notes/strat-opcodes.md
new file mode 100644
index 0000000..b2f88d8
--- /dev/null
+++ b/notes/strat-opcodes.md
@@ -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. 426500 |
+| 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. 54fa50 |
+| 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). 553850 |
+| 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). 5537f0 |
+| 0x04 | `pushVar` | var | 0 | 1 | 50710 | high | stLocal | Pushes own variable n (+0xF4 array). 553710 |
+| 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). 5537b0 |
+| 0x06 | `pushGlobal` | global | 0 | 1 | 27756 | high | stGlobal | Pushes global n (0x6D9E8C[n]). 5536b0 |
+| 0x07 | `pushProp` | prop | 0 | 1 | 38636 | high | stAlienVar | Pushes own property n via FUN_00550E60(self,self,n). 552ab0 |
+| 0x08 | `pushVarAddr` | var | 0 | 1 | 2074 | high | stLocalAddress | Pushes address of own variable n (for 0x47/0x48/0x4d). 5536f0 |
+| 0x09 | `pushCodeAddr` | rel | 0 | 1 | 28 | high | - | Pushes address pc+arg*4 (address of inline code-stream data word). 553790 |
+| 0x0a | `pushGlobalAddr` | global | 0 | 1 | 1087 | high | stGlobalAddress | Pushes address of global n. 553690 |
+| 0x0b | `pushPropAddr` | prop | 0 | 1 | 248 | high | stAlienVarAddress | Pushes address of own property n (FUN_005509F0(self,self,n)). 552ad0 |
+| 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). 54fc40 |
+| 0x0d | `clearEvent` | imm | 0 | 0 | 35 | high | - | Clears event handler slot arg (type word := 0). 54dbe0 |
+| 0x0e | `disableEvent` | imm | 0 | 0 | | high | - | Disables event handler slot arg (sets bit 31 of its type word). 54dc20 |
+| 0x0f | `enableEvent` | imm | 0 | 0 | | high | - | Re-enables event handler slot arg (clears bit 31 of its type word). 54dc00 |
+| 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. 553d40 |
+| 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. 553d10 |
+| 0x12 | `jzYield` | rel | 1 | 0 | 3012 | high | stBeq | Pops cond; if 0 jumps to next+arg*4; always yields this frame. 553e10 |
+| 0x13 | `jnzYield` | rel | 1 | 0 | 202 | high | stBne | Pops cond; if !=0 jumps to next+arg*4; always yields this frame. 553eb0 |
+| 0x14 | `jz` | rel | 1 | 0 | 32026 | high | stBeqImm | Pops cond; if 0 jumps to next+arg*4. 553e50 |
+| 0x15 | `jnz` | rel | 1 | 0 | | high | stBneImm | Pops cond; if !=0 jumps to next+arg*4; unused. 553e80 |
+| 0x16 | `jmp` | rel | 0 | 0 | 15125 | high | stJumpImm | Jumps to next+arg*4. 553d80 |
+| 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. 550060 |
+| 0x18 | `pushParentVar` | var | 0 | 1 | 711 | high | stExt_Local | Pushes variable n of the linked strat at +0xC (parent/creator). 553750 |
+| 0x19 | `pushParentVarAddr` | var | 0 | 1 | 135 | high | stExt_LocalAddress | Pushes address of variable n of the +0xC strat. 553770 |
+| 0x1a | `pushParentProp` | prop | 0 | 1 | 1362 | high | stExt_AlienVar | Pushes property n of the +0xC strat. 552af0 |
+| 0x1b | `pushParentPropAddr` | prop | 0 | 1 | | high | stExt_AlienVarAddress | Pushes address of property n of the +0xC strat; unused. 553730 |
+| 0x1c | `pushPlayerProp` | playerProp | 0 | 1 | 4320 | high | stPlayer_AlienVar | Pushes property n of the player strat (DAT_006D9E1C, set by 0xb5), 0 if none. 552d20 |
+| 0x1d | `pushPlayerPropAddr` | playerProp | 0 | 1 | | high | stPlayer_AlienVarAddress | Pushes address of player property n (0 if no player); unused. 552d50 |
+| 0x1e | `pushCameraProp` | prop | 0 | 1 | 855 | high | stCamera_AlienVar | Pushes property n of the camera strat (DAT_006D9E28, set by 0xc8), 0 if none. 552b60 |
+| 0x1f | `pushCameraPropAddr` | prop | 0 | 1 | | high | stCamera_AlienVarAddress | Pushes address of camera property n (0 if none); unused. 552b90 |
+| 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. 552c40 |
+| 0x21 | `pushTargetPropAddr` | prop | 0 | 1 | | medium | stTarget_AlienVarAddress | Pushes address of target property n (0 if none); unused. 552c70 |
+| 0x22 | `error_0x22` | - | 0 | 0 | | high | stCollide_AlienVar | Prints "stCollide_AlienVar" not-implemented error; unused. 54d200 |
+| 0x23 | `error_0x23` | - | 0 | 0 | | high | stCollide_AlienVarAddress | Prints "stCollide_AlienVarAddress" not-implemented error; unused. 54d220 |
+| 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. 552bd0 |
+| 0x25 | `pushTarget2PropAddr` | prop | 0 | 1 | | medium | stTarget_AlienVarAddress | Pushes address of target2 property n (0 if none); unused. 552c00 |
+| 0x26 | `pushBossProp` | prop | 0 | 1 | | low | stBoss_AlienVar | Pushes property n of the strat in DAT_006D9E24 (set by 0xef), no null check; unused. 552b10 |
+| 0x27 | `pushBossPropAddr` | prop | 0 | 1 | | low | stBoss_AlienVarAddress | Pushes address of property n of the DAT_006D9E24 strat; unused. 552b30 |
+| 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. 552cb0 |
+| 0x29 | `pushDialogPropAddr` | prop | 0 | 1 | | low | stDialog_AlienVarAddress | Pushes address of property n of the DAT_006D9E2C strat (0 if none); unused. 552ce0 |
+| 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). 54e8e0 |
+| 0x2b | `jmpYield` | rel | 0 | 0 | 2958 | high | - | Jumps to next+arg*4 and yields this frame (loop back-edge / wait one frame). 553da0 |
+| 0x2c | `nop_0x2c` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. 426500 |
+| 0x2d | `nop_0x2d` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. 426500 |
+| 0x2e | `nop_0x2e` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. 426500 |
+| 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. 54f720 |
+| 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). 54fa80 |
+| 0x31 | `flagEC2` | imm | 0 | 0 | 410 | low | - | arg sets/clears +0xEC bit 2 (checked by collision/floor code via +0xC0, readable as property 0x1f). 54e090 |
+| 0x32 | `flagE8_80000000` | imm | 0 | 0 | | low | - | arg sets/clears +0xE8 bit 31; unused. 54dd90 |
+| 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). 54dd30 |
+| 0x34 | `flagEC200000` | imm | 0 | 0 | 50 | low | - | arg sets/clears +0xEC bit 0x200000 (used by NPC helpers/canoe/llama). 54dd60 |
+| 0x35 | `nop_0x35` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. 426500 |
+| 0x36 | `nop_0x36` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. 426500 |
+| 0x37 | `popPlayerProp` | playerProp | 1 | 0 | 1131 | high | - | Pops value and stores it into player property n (discarded if no player). 553920 |
+| 0x38 | `popCameraProp` | prop | 1 | 0 | 564 | high | - | Pops value into camera strat property n (discarded if none). 553950 |
+| 0x39 | `popTargetProp` | prop | 1 | 0 | 186 | medium | - | Pops value into camera target (DAT_006D9E30) property n (discarded if none). 553980 |
+| 0x3a | `popTarget2Prop` | prop | 1 | 0 | | medium | - | Pops value into DAT_006D9E34 strat property n (discarded if none); unused. 5539b0 |
+| 0x3b | `popDialogProp` | prop | 1 | 0 | | low | - | Pops value into DAT_006D9E2C strat property n (discarded if none); unused. 5539e0 |
+| 0x3c | `error_0x3c` | - | 0 | 0 | | high | - | Prints "stCollide_AlienVar_Equals" not-implemented error; unused. 54d240 |
+| 0x3d | `popBossProp` | prop | 1 | 0 | | low | - | Pops value into DAT_006D9E24 strat property n (no null check); unused. 553a10 |
+| 0x3e | `popParentVar` | var | 1 | 0 | 204 | high | - | Pops value into variable n of the +0xC strat. 553a40 |
+| 0x3f | `popParentProp` | prop | 1 | 0 | | high | - | Pops value into property n of the +0xC strat; unused. 553a60 |
+| 0x40 | `popProp` | prop | 1 | 0 | 15730 | high | - | Pops value into own property n (via FUN_005509F0 address). 553a90 |
+| 0x41 | `popVar` | var | 1 | 0 | 30379 | high | - | Pops value into own variable n. 553ac0 |
+| 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). 553ae0 |
+| 0x43 | `popGlobal` | global | 1 | 0 | 20214 | high | - | Pops value into global n. 553b00 |
+| 0x44 | `pushShort` | imm | 0 | 1 | 82749 | high | stNumber | Pushes the sign-extended 16-bit arg (raw, not shifted to 20.12). 553610 |
+| 0x45 | `push` | imm | 0 | 1 | 25870 | high | stNumber | Pushes inline 32-bit word (handler shared with 0xd7). 5535f0 |
+| 0x46 | `neg` | - | 1 | 1 | 923 | high | stUMinus | Pops a, pushes -a. 5508b0 |
+| 0x47 | `inc` | - | 1 | 0 | 1648 | high | stIncrease | Pops address, adds 1.0 (0x1000) to the word there. 553890 |
+| 0x48 | `dec` | - | 1 | 0 | 1924 | high | stDecrease | Pops address, subtracts 1.0 (0x1000) from the word there. 5538b0 |
+| 0x49 | `add` | - | 2 | 1 | 15901 | high | stAdd | Pops b, a; pushes a+b. 5538d0 |
+| 0x4a | `sub` | - | 2 | 1 | 10042 | high | stSub | Pops b, a; pushes a-b. 552d90 |
+| 0x4b | `mul` | - | 2 | 1 | 11877 | high | stMul | Pops b, a; pushes fixed-point a*b>>12. 552dc0 |
+| 0x4c | `div` | - | 2 | 1 | 468 | high | stDiv | Pops b, a; pushes fixed-point (a<<8)/b<<4. 552e00 |
+| 0x4d | `store` | - | 2 | 0 | | high | stEquals | Pops value, then address; stores value at address; unused (compiler uses pop ops). 553900 |
+| 0x4e | `eq` | - | 2 | 1 | 10075 | high | stCompare | Pops b, a; pushes a==b (0/1, raw). 553be0 |
+| 0x4f | `lt` | - | 2 | 1 | 5556 | high | stLessThan | Pops b, a; pushes a553bb0 |
+| 0x50 | `shl` | - | 2 | 1 | 1162 | high | stShiftLeft | Pops b, a; pushes a << (b>>12) (b fixed shift count). 54fb70 |
+| 0x51 | `shr` | - | 2 | 1 | 2496 | high | stShiftRight | Pops b, a; pushes a >> (b>>12) arithmetic (also stashes a in 0x6DA2D8). 54fba0 |
+| 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. 54fad0 |
+| 0x53 | `gt` | - | 2 | 1 | 3662 | high | stGreaterThan | Pops b, a; pushes a>b. 553b20 |
+| 0x54 | `setModel` | - | 1 | 0 | 1439 | high | stSetModel | Pops model address; sets +0x10 and updates +0xEC bit 0x20 / +0xE8 bit 0x80 from model header fields. 553c10 |
+| 0x55 | `scale` | - | 1 | 0 | 376 | high | stScale | Pops scale (fixed); sets x/y/z scale +0x60/+0x64/+0x68. 54ffe0 |
+| 0x56 | `scaleX` | - | 1 | 0 | 102 | high | stScaleX | Pops scale (fixed) into +0x60. 550000 |
+| 0x57 | `scaleY` | - | 1 | 0 | 242 | high | stScaleY | Pops scale (fixed) into +0x64. 550020 |
+| 0x58 | `scaleZ` | - | 1 | 0 | 24 | high | stScalez | Pops scale (fixed) into +0x68. 550040 |
+| 0x59 | `shadowSize` | - | 1 | 0 | 548 | high | stShadowSize | Pops shadow size (fixed) into +0x12C (shadow drawn only if >=0x80). 54e840 |
+| 0x5a | `shadowTexture` | - | 1 | 0 | 548 | medium | stShadowType | Pops shadow texture index (TEXT texture) into +0x128 (short). 54e860 |
+| 0x5b | `error_0x5b` | - | 0 | 0 | | high | stFlash | Prints "stFlash" not-implemented error; unused. 54d260 |
+| 0x5c | `error_0x5c` | - | 0 | 0 | | high | stTrans | Prints "stTrans" not-implemented error; unused. 54d280 |
+| 0x5d | `moveUp` | - | 1 | 0 | 50 | high | stMoveUp | Pops dist (fixed); moves strat by local (0,+d,0) via FUN_005501D0. 550910 |
+| 0x5e | `moveDown` | - | 1 | 0 | 29 | high | stMoveDown | Pops dist; moves by local (0,-d,0). 550960 |
+| 0x5f | `moveForward` | - | 1 | 0 | 95 | high | stMoveForward | Pops dist; moves by local (0,0,+d) along its facing. 550590 |
+| 0x60 | `moveBackward` | - | 1 | 0 | 12 | high | stMoveBackward | Pops dist; moves by local (0,0,-d). 5505c0 |
+| 0x61 | `moveLeft` | - | 1 | 0 | 20 | high | stMoveLeft | Pops dist; moves by local (+d,0,0). 550720 |
+| 0x62 | `moveRight` | - | 1 | 0 | 4 | high | stMoveRight | Pops dist; moves by local (-d,0,0). 550750 |
+| 0x63 | `turnRight` | - | 1 | 0 | 380 | high | stTurnRight | Pops angle; yaw +0x24 -= angle (24-bit wrap; 4096.0 = full turn). 553c90 |
+| 0x64 | `turnLeft` | - | 1 | 0 | 775 | high | stTurnLeft | Pops angle; yaw +0x24 += angle. 553cb0 |
+| 0x65 | `tiltLeft` | - | 1 | 0 | 4 | high | stTiltLeft | Pops angle; roll +0x28 -= angle. 54e9e0 |
+| 0x66 | `tiltRight` | - | 1 | 0 | 6 | high | stTiltRight | Pops angle; roll +0x28 += angle. 54ea00 |
+| 0x67 | `tiltForward` | - | 1 | 0 | 26 | high | stTiltForward | Pops angle; pitch +0x20 += angle. 553cd0 |
+| 0x68 | `tiltBackward` | - | 1 | 0 | | high | stTiltBackward | Pops angle; pitch +0x20 -= angle; unused. 553cf0 |
+| 0x69 | `turnToPlayerX` | - | 1 | 0 | | high | stTurnToPlayerX | Pops max step (angle); turns pitch +0x20 toward the player by at most step; unused. 54ef50 |
+| 0x6a | `turnToPlayerY` | - | 1 | 0 | 345 | high | stTurnToPlayerY | Pops max step (angle); turns yaw +0x24 toward the player by at most step. 54f0d0 |
+| 0x6b | `turnToPlayerXY` | - | 1 | 0 | 1 | high | stTurnToPlayerXY | Pops max step (angle); turns yaw and pitch toward the player. 54eff0 |
+| 0x6c | `turnToX` | - | 4 | 0 | 2 | high | stTurnToX | Pops step, z, y, x; turns pitch toward point (x,y,z) by at most step. 54eb10 |
+| 0x6d | `turnToY` | - | 4 | 0 | 209 | high | stTurnToY | Pops step, z, y, x; turns yaw toward point (x,y,z) by at most step. 54ebc0 |
+| 0x6e | `turnToXY` | - | 4 | 0 | 28 | high | stTurnToXY | Pops step, z, y, x; turns yaw and pitch toward point (x,y,z). 54ec60 |
+| 0x6f | `wobble` | - | 1 | 0 | | medium | stWobble | Pops and discards one value (unimplemented; shared pop handler); unused. 554fb0 |
+| 0x70 | `resetPos` | - | 0 | 0 | 613 | high | stReSetPos | Restores 8 words (angles+position, +0x20..+0x3C) from the saved copy at +0xA0. 54fa10 |
+| 0x71 | `savePos` | - | 0 | 0 | 566 | high | stSetPos | Saves 8 words (+0x20..+0x3C angles+position) to +0xA0 for 0x70. 54f9f0 |
+| 0x72 | `nop_0x72` | - | 0 | 0 | | medium | stJump | No-op (shared empty handler); unused. 426500 |
+| 0x73 | `wpFirst` | - | 0 | 0 | 833 | high | stWPFirst | Current waypoint +0x120 := first waypoint +0x118. 54ea60 |
+| 0x74 | `wpLast` | - | 0 | 0 | 83 | high | stWPLast | Current waypoint +0x120 := last waypoint +0x11C. 54f240 |
+| 0x75 | `wpNext` | - | 0 | 0 | 1129 | high | stWPNext | Current waypoint := its next link; wraps to first. 54f260 |
+| 0x76 | `wpPrev` | - | 0 | 0 | 177 | high | stWPPrev | Current waypoint := its prev link; wraps to last. 54f550 |
+| 0x77 | `error_0x77` | - | 0 | 0 | | high | stWPDel | Prints "stWpdel" not-implemented error; unused. 54d2a0 |
+| 0x78 | `error_0x78` | - | 0 | 0 | | high | stWPNew | Prints "stWpnew" not-implemented error; unused. 54d2c0 |
+| 0x79 | `wpNearest` | - | 0 | 0 | | high | stWPNearest | Sets current waypoint to the one nearest the strat position; unused. 54f3f0 |
+| 0x7a | `wpFurthest` | - | 0 | 0 | | high | stWPFurthest | Sets current waypoint to the one furthest from the strat (excluding last); unused. 54f500 |
+| 0x7b | `wpTurnToX` | - | 1 | 0 | 12 | high | stWPTurnToX | Pops max step (angle); turns pitch toward the current waypoint (pos at wp+0x18). 54ee40 |
+| 0x7c | `wpTurnToY` | - | 1 | 0 | 230 | high | stWPTurnToY | Pops max step (angle); turns yaw toward the current waypoint (if any). 54eed0 |
+| 0x7d | `wpTurnToXY` | - | 1 | 0 | 25 | high | stWPTurnToXY | Pops max step (angle); turns yaw and pitch toward the current waypoint. 54ed60 |
+| 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. 550160 |
+| 0x7f | `animStop` | - | 0 | 0 | 458 | high | stAnimStop | Clears +0xE8 bit 8 (anim playing). 54f7c0 |
+| 0x80 | `animClear` | - | 0 | 0 | | medium | stAnimClear | Clears the current anim pointer (+0x14) and flag 0x8 in +0xE8 (no anim playing). 54f7d0 |
+| 0x81 | `animSetSpeed` | - | 0 | 0 | | high | stAnimSetSpeed | Unimplemented on PC: only logs "stAnimsetspeed" via the debug print; no stack effect. 54d2e0 |
+| 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). 54fd60 |
+| 0x83 | `collRadius` | - | 1 | 0 | 821 | high | stCollRadius | pops radius; stores its low 16 bits raw into collision radius short +0xD2. 54e910 |
+| 0x84 | `collHeight` | - | 0 | 0 | | high | stCollHeight | Unimplemented: logs "stCollheight" only. 54d300 |
+| 0x85 | `collExtent` | - | 1 | 0 | | medium | stCollextent | pops extent; stores its low 16 bits raw into short +0xC4. 54e930 |
+| 0x86 | `collView` | - | 0 | 0 | | high | stCollView | Unimplemented: logs "stCollview" only. 54d320 |
+| 0x87 | `nop_0x87` | - | 0 | 0 | | low | stCollPoints | Shared no-op handler (0x426500); unused. Slot of Croc2 stCollPoints. 426500 |
+| 0x88 | `nop_0x88` | - | 0 | 0 | | low | stCollSetPoint | Shared no-op handler (0x426500); unused. Slot of Croc2 stCollSetPoint. 426500 |
+| 0x89 | `createTrigger` | - | 0 | 0 | 45 | medium | stCreateTrigger | Sets flag 0x40 in +0xE8 (strat becomes a trigger/touchable, used by pickups like Health, Carrot). 54e4c0 |
+| 0x8a | `killTrigger` | - | 0 | 0 | | medium | stKillTrigger | Clears flags 0x40 and 0x4000000 in +0xE8 (undoes createTrigger). 54e4b0 |
+| 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). 54fb40 |
+| 0x8c | `hold` | - | 0 | 0 | 110 | medium | stHold | Sets flags 0x12 (0x2 wait + 0x10 stopped) in +0xE8: suspends the strat indefinitely until released. 54fce0 |
+| 0x8d | `release` | - | 0 | 0 | | high | stRelease | Unimplemented: logs "stRelease" only. 54d340 |
+| 0x8e | `remove` | - | 0 | 0 | 985 | high | stRemove | Sets flags 0x6 in +0xE8 (yield + kill); same handler as 0xb4 end. 5535e0 |
+| 0x8f | `nop_0x8f` | - | 0 | 0 | | low | stMapRemove | Shared no-op handler (0x426500); unused. Slot of Croc2 stMapRemove. 426500 |
+| 0x90 | `mapAdd` | - | 0 | 0 | | high | stMapAdd | Unimplemented: logs "stMapadd" only. 54d360 |
+| 0x91 | `mapReplace` | - | 0 | 0 | | high | stMapReplace | Unimplemented: logs "stMapreplace" only. 54d380 |
+| 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). 54db40 |
+| 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. 54db50 |
+| 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. 5531d0 |
+| 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. 5533f0 |
+| 0x96 | `link` | - | 0 | 0 | | high | stLink | Unimplemented: logs "stLink" only. 54d3a0 |
+| 0x97 | `unlink` | - | 0 | 0 | | high | stUnlink | Unimplemented: logs "stUnlink" only. 54d3c0 |
+| 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). 54e0c0 |
+| 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. 54e1c0 |
+| 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). 54d4c0 |
+| 0x9b | `streamPlay` | - | 0 | 0 | | high | - | Unimplemented: logs "stStreamPlay" only. 54d3e0 |
+| 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. 54d530 |
+| 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. 54d4f0 |
+| 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. 54d510 |
+| 0x9f | `streamQueue` | - | 0 | 0 | | high | stMidiQueue | Unimplemented: logs "stStreamQueue" only. 54d400 |
+| 0xa0 | `isLight` | - | 0 | 0 | | high | stIsLight | Unimplemented: logs "stIsLight" only. 54d420 |
+| 0xa1 | `lightCol` | - | 0 | 0 | | high | stLightCol | Unimplemented: logs "stLightCol" only. 54d440 |
+| 0xa2 | `lightFade` | - | 0 | 0 | | high | stLightFade | Unimplemented: logs "stLightFade" only. 54d460 |
+| 0xa3 | `lightAtten` | - | 0 | 0 | | high | stLightAtten | Unimplemented: logs "stLightAtten" only. 54d480 |
+| 0xa4 | `lightType` | - | 0 | 0 | | high | stLightType | Unimplemented: logs "stLightType" only. 54d4a0 |
+| 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. 54fcf0 |
+| 0xa6 | `collisionOff` | flags | 0 | 0 | 1134 | high | stCollisionOff | Clears the inline flag word's bits in +0xE8. 54fd40 |
+| 0xa7 | `collisionOffAll` | - | 0 | 0 | | high | stCollisionOffAll | Clears all collision flag bits 0x7E000 in +0xE8. 54fd30 |
+| 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). 54e3a0 |
+| 0xa9 | `soundPlay4_0xa9` | - | 4 | 0 | | medium | stSoundPlay | pops 4 values and discards them (stubbed sound-play variant). 54e400 |
+| 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. 54e320 |
+| 0xab | `soundPlayAss4_0xab` | - | 4 | 0 | | medium | stSoundPlayAss | pops 4 values and discards them (same stub as 0xa9; pushes nothing). 54e400 |
+| 0xac | `int` | - | 1 | 1 | 1508 | high | stInt | pops x; pushes x & 0xFFFFF000 (fixed-point floor/truncate to integer). 5501b0 |
+| 0xad | `sin` | - | 1 | 1 | 2994 | high | stSin | pops angle (fixed, 4096 table units = full turn); pushes sin from table 0x574318 (20.12). 54e790 |
+| 0xae | `cos` | - | 1 | 1 | 3149 | high | stCos | pops angle; pushes cos (table 0x574318 offset by quarter turn 0x400). 54e810 |
+| 0xaf | `not` | - | 1 | 1 | 4952 | high | stNot | pops x; pushes 1 if x==0 else 0. 54fc20 |
+| 0xb0 | `pop` | - | 1 | 0 | | high | stPop | Drops the top stack value (stack pointer +0x104 -= 4). 553dd0 |
+| 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). 553de0 |
+| 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). 553630 |
+| 0xb3 | `return` | - | 1 | 0 | 4974 | high | stReturn | pops code address and jumps there (pc = popped). 5537d0 |
+| 0xb4 | `end` | - | 0 | 0 | 2006 | high | stEndStrat | Sets flags 0x6 in +0xE8 (yield + kill): ends the strat. Same handler as 0x8e. 5535e0 |
+| 0xb5 | `isPlayer` | - | 0 | 0 | 62 | high | stIsPlayer | Registers this strat as the player (global 0x6D9E1C = strat) and sets flag 0x800 in +0xE8. 550140 |
+| 0xb6 | `and` | - | 2 | 1 | 3004 | high | stAnd | pops b, a; pushes 1 if both are non-zero else 0 (logical and). 552e60 |
+| 0xb7 | `or` | - | 2 | 1 | 5411 | high | stOr | pops b, a; pushes a \| b (bitwise, used as logical or). 552ea0 |
+| 0xb8 | `bitwiseAnd` | - | 2 | 1 | 6814 | high | stBitwiseAnd | pops b, a; pushes a & b. 552e30 |
+| 0xb9 | `discard_0xb9` | - | 1 | 0 | | medium | - | Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 ext-var slots). 554fb0 |
+| 0xba | `discard_0xba` | - | 1 | 0 | | medium | - | Shared stub 0x554fb0: pops one value and discards it. Unused. 554fb0 |
+| 0xbb | `nop_0xbb` | - | 0 | 0 | | low | - | Shared no-op handler (0x426500); unused. 426500 |
+| 0xbc | `notEqual` | - | 2 | 1 | 2576 | high | stNotEqual | pops b, a; pushes 1 if a != b else 0. 54fbd0 |
+| 0xbd | `greaterEqual` | - | 2 | 1 | 1169 | high | stGreaterEqual | pops b, a; pushes 1 if a >= b else 0. 553b80 |
+| 0xbe | `lessEqual` | - | 2 | 1 | 2129 | high | stLessEqual | pops b, a; pushes 1 if a <= b else 0. 553b50 |
+| 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). 54f960 |
+| 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)). 54f5c0 |
+| 0xc1 | `nop_0xc1` | - | 0 | 0 | | low | stResettoCheckPoint | Shared no-op handler (0x426500); unused. Croc2 slot stResettoCheckPoint. 426500 |
+| 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). 54e9c0 |
+| 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. 54f1c0 |
+| 0xc4 | `rumble` | - | 2 | 0 | 93 | low | stRumble | Shared stub 0x555090: pops two values and discards them (PSX pad rumble/vibrate not implemented on PC). 555090 |
+| 0xc5 | `suspendIfTooFar` | - | 1 | 0 | 232 | low | stSuspendIfTooFar | Shared stub 0x554fb0: pops one value (likely distance) and discards it. 554fb0 |
+| 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). 54e6c0 |
+| 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. 54e4d0 |
+| 0xc8 | `isCamera` | - | 0 | 0 | 34 | high | stIsCamera | Registers this strat as the camera (global 0x6D9E28) and sets flag 0x8000000 in +0xE8. 5500f0 |
+| 0xc9 | `lookAtMe` | - | 0 | 0 | 86 | high | stLookatme | Sets camera look-at target global 0x6D9E30 = this strat. 5500e0 |
+| 0xca | `lookAtMe2` | - | 0 | 0 | | medium | stLookatme | Sets secondary look-at global 0x6D9E34 = this strat. Unused. 5500d0 |
+| 0xcb | `pushCamera` | - | 0 | 0 | | high | stPushCamera | Pushes current camera (0x6D9E28) and look-at (0x6D9E30) pair onto the camera stack 0x6D9DCC (index 0x6D9E40). 54dbb0 |
+| 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. 54db00 |
+| 0xcd | `nop_0xcd` | - | 0 | 0 | | low | stResetCamerapos | Shared no-op handler (0x426500); unused. Croc2 slot stResetCamerapos. 426500 |
+| 0xce | `gainHeart` | - | 0 | 0 | 94 | high | stGainHeart | Health 0x584E70 += 1.0 if below max 0x584E6C; refreshes HUD (FUN_004268c0(1)). 54f650 |
+| 0xcf | `nop_0xcf` | - | 0 | 0 | | low | stGainHeartPot | Shared no-op handler (0x426500); unused. Croc2 slot stGainHeartPot. 426500 |
+| 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). 54da00 |
+| 0xd1 | `nop_0xd1` | - | 0 | 0 | | low | stGainCrystal | Shared no-op handler (0x426500); unused. Croc2 slot stGainCrystal. 426500 |
+| 0xd2 | `discard_0xd2` | - | 1 | 0 | | low | stCutscene | Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 slot stCutscene). 554fb0 |
+| 0xd3 | `inventory` | imm | 0 | 1 | | medium | stInventory | Queries inline item via stub FUN_00426860 and pushes result<<12 (always 0 on PC). 54d9d0 |
+| 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. 553ef0 |
+| 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. 54e230 |
+| 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. 54e1e0 |
+| 0xd7 | `soundAddress` | sound | 0 | 1 | 2996 | high | stSoundAddress | Pushes the inline sound id (same handler as 0x45 push immediate). 5535f0 |
+| 0xd8 | `nop_0xd8` | - | 0 | 0 | | low | stOnground | Shared no-op handler (0x426500); unused. Croc2 slot stOnground/stObjectFallslow. 426500 |
+| 0xd9 | `collisionOffset` | - | 3 | 0 | 403 | high | stCollisionOffset | pops z, y, x; stores low 16 bits into shorts +0xD0, +0xCE, +0xCC (collision centre offset). 54e050 |
+| 0xda | `abs` | - | 1 | 1 | 1847 | high | stAbs | pops x; pushes \|x\|. 5508d0 |
+| 0xdb | `pickup` | imm | 0 | 0 | | medium | stPickup | inline flag: nonzero sets bit 0x200 in +0xEC, zero clears it (strat can be picked up/carried). 54dd00 |
+| 0xdc | `min` | - | 2 | 1 | 1546 | high | stMin | pops b, a; pushes the smaller (signed). 54dc80 |
+| 0xdd | `max` | - | 2 | 1 | 768 | high | stMax | pops b, a; pushes the larger (signed). 54dc40 |
+| 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). 552ed0 |
+| 0xdf | `sgn` | - | 1 | 1 | | high | stSgn | pops x; pushes 1.0 (0x1000), -1.0 or 0 by sign. 54e7c0 |
+| 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 553230 |
+| 0xe1 | `nop_0xe1` | - | 0 | 0 | | high | stDontLookatme? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0xe2 | `nop_0xe2` | - | 0 | 0 | | high | stRunat? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 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) 550690 |
+| 0xe4 | `moveBackwardQ` | - | 1 | 0 | 436 | high | stMoveBackwardq | pops distance (fixed); moves the strat backwards along its yaw (x -= d*sin, z -= d*cos) 550600 |
+| 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) 54e2d0 |
+| 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 54e270 |
+| 0xe7 | `setWP` | - | 0 | 0 | 213 | high | stSetWP | saves the current waypoint: +0x124 = +0x120 54ea20 |
+| 0xe8 | `resetWP` | - | 0 | 0 | 193 | high | stResetWP | restores the saved waypoint: +0x120 = +0x124 54ea40 |
+| 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) 54e190 |
+| 0xea | `nop_0xea` | - | 0 | 0 | | high | stString? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0xeb | `setBossHearts` | - | 1 | 0 | | high | stSetbossHearts | pops count (fixed); sets boss heart max DAT_00584f0c and current DAT_00584f10 to count 54f580 |
+| 0xec | `loseBossHeart` | - | 0 | 0 | | high | stLosebossHeart | decrements boss hearts DAT_00584f10 (not below 0) and refreshes the HUD (FUN_004268c0(1)) 54f5a0 |
+| 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 54e0f0 |
+| 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)) 54dcc0 |
+| 0xef | `isBoss` | - | 0 | 0 | | high | stIsboss | registers this strat as the boss: DAT_006d9e24 = self 550130 |
+| 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 40d480 |
+| 0xf1 | `getParentPos` | - | 0 | 0 | 48 | high | stGetparentpos | copies parent's (+0xC) orientation+position block (+0x20..+0x3C, 8 words) into this strat 54e030 |
+| 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 54fe40 |
+| 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 553290 |
+| 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 553380 |
+| 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 553300 |
+| 0xf6 | `nop_0xf6` | - | 0 | 0 | | high | stBeforeboss?/stNohang? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0xf7 | `pushZero` | - | 0 | 1 | | high | stZero | pushes 0 (same handler as 0x1a7); unused 54e880 |
+| 0xf8 | `nop_0xf8` | - | 0 | 0 | | high | stTophead? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0xf9 | `popDiscard_0xf9` | - | 1 | 0 | | high | stTopdialog? | pops and discards one value (stub FUN_00554fb0, shared with 0x6f etc.); unused 554fb0 |
+| 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 40d8d0 |
+| 0xfb | `nop_0xfb` | - | 0 | 0 | | high | stBottomhead? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0xfc | `popDiscard_0xfc` | - | 1 | 0 | | high | stBottomdialog? | pops and discards one value (stub FUN_00554fb0); unused 554fb0 |
+| 0xfd | `getPlayerPos` | - | 0 | 0 | 193 | high | stGetPlayerpos | if a player exists copies its orientation+position block (+0x20..+0x3C) into this strat 54dde0 |
+| 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 54dfe0 |
+| 0xff | `getBossPos` | - | 0 | 0 | | high | stGetbosspos | copies the boss's (DAT_006d9e24) orientation+position block (+0x20..+0x3C) into this strat (no null check); unused 54ddc0 |
+| 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) 54df30 |
+| 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 54d910 |
+| 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 54d8a0 |
+| 0x103 | `moveUpQ` | - | 1 | 0 | 1018 | high | stMoveUpq | pops distance (fixed); y(+0x34) += d 5508f0 |
+| 0x104 | `moveDownQ` | - | 1 | 0 | 314 | high | stMoveDownq | pops distance (fixed); y(+0x34) -= d 550940 |
+| 0x105 | `forcePlayerDist` | - | 0 | 0 | | high | stForcePlayerdist | recomputes the cached squared player distance +0x148 (coords >>6) and clears +0x1C; 0x200000 if no player; unused 54e950 |
+| 0x106 | `nop_0x106` | - | 0 | 0 | | high | stShadeType? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 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) 54d880 |
+| 0x108 | `nop_0x108` | - | 0 | 0 | | high | stCheckleveldoor? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x109 | `nop_0x109` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x10a | `nop_0x10a` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x10b | `nop_0x10b` | - | 0 | 0 | | high | stGainjigsaw? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x10c | `nop_0x10c` | - | 0 | 0 | | high | stGaingoldengobbo? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 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 54f710 |
+| 0x10e | `nop_0x10e` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x10f | `nop_0x10f` | - | 0 | 0 | 1 | high | stCheckPoint? | no-op handler FUN_00426500 (does nothing, no inline operands); used once (LlamaChase) 426500 |
+| 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 550120 |
+| 0x111 | `nop_0x111` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x112 | `nop_0x112` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x113 | `isDialog` | - | 0 | 0 | 157 | high | stIsdialog | registers this strat as the dialog partner: DAT_006d9e2c = self 550110 |
+| 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) 54f840 |
+| 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' 54e8b0 |
+| 0x116 | `topCloseDialog` | - | 0 | 0 | 32 | high | stTopclosedialog | closes the top dialog text box: DAT_006d9858 = 0 40d900 |
+| 0x117 | `bottomCloseDialog` | - | 0 | 0 | 4 | high | stBottomclosedialog | closes the bottom dialog text box: DAT_006d94e8 = 0 40d910 |
+| 0x118 | `nop_0x118` | - | 0 | 0 | | high | stNextinventory? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x119 | `nop_0x119` | - | 0 | 0 | | high | stPrevinventory? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 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 553f70 |
+| 0x11b | `normalPiece` | - | 0 | 0 | 264 | medium | stNormalpiece | same as otherPiece but restores the piece's normal state (*piece[5] = piece[3]) 554090 |
+| 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 54da20 |
+| 0x11d | `nop_0x11d` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x11e | `nop_0x11e` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x11f | `nop_0x11f` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x120 | `nop_0x120` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x121 | `nop_0x121` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x122 | `nop_0x122` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x123 | `nop_0x123` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x124 | `nop_0x124` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x125 | `moveLeftQ` | - | 1 | 0 | 131 | high | stMoveLeftq | pops distance (fixed); strafes: x += d*cos(yaw), z += d*sin(yaw-half turn) (= -d*sin) 550790 |
+| 0x126 | `moveRightQ` | - | 1 | 0 | 299 | high | stMoveRightq | pops distance (fixed); strafes opposite to moveLeft (x -= d*cos(yaw), z += d*sin(yaw)) 550820 |
+| 0x127 | `bitwiseNot` | - | 1 | 1 | 1 | high | stBitwisenot | pops v; pushes ~v 54fc00 |
+| 0x128 | `bordersOn` | - | 0 | 0 | 157 | high | stBorderson | enables cinematic letterbox borders: DAT_005fd090 = 1 54dac0 |
+| 0x129 | `bordersOff` | - | 0 | 0 | 176 | high | stBordersoff | disables letterbox borders: DAT_005fd090 = 0 54dad0 |
+| 0x12a | `soundAdsr` | - | 5 | 0 | | high | stSoundadsr | pops and discards 5 values (stubbed); unused 54e120 |
+| 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) 54e150 |
+| 0x12c | `nop_0x12c` | - | 0 | 0 | | high | stRotatepiece? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 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) 54fea0 |
+| 0x12e | `resetAmbient` | - | 0 | 0 | | high | stReset_ambient | disables the own ambient colour: +0x136 = 0; unused 54fee0 |
+| 0x12f | `invActive` | - | 0 | 0 | | high | stInvactive | calls inventory stub FUN_00426860(0,1,0,1,0) (returns 0, no effect in this build); unused 54da60 |
+| 0x130 | `invInactive` | - | 0 | 0 | | high | stInvinactive | calls inventory stub FUN_00426860(0,1,0,1,1) (no effect); unused 54da40 |
+| 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 54e480 |
+| 0x132 | `nop_0x132` | - | 0 | 0 | | high | stResettoCheckPointnlh? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 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 54def0 |
+| 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 54dec0 |
+| 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 54d830 |
+| 0x136 | `pushPlayer` | - | 0 | 0 | | high | stPushPlayer | saves the current player pointer DAT_006d9e1c into DAT_006d9e20 (logs an error if no player); unused 54db60 |
+| 0x137 | `popPlayer` | - | 0 | 0 | | high | stPopPlayer | restores the player pointer (DAT_006d9e1c and DAT_006d9e30) from DAT_006d9e20 and clears it; unused 54db90 |
+| 0x138 | `resetToStart` | - | 0 | 0 | | high | stReSetPostrn | restores position +0x30..+0x38 from the saved (start) position +0xB0..+0xB8; unused 54fa30 |
+| 0x139 | `gainItem` | - | 0 | 0 | | high | stGainitem | increments item counter DAT_005fd09c up to the maximum DAT_005fd0a4; unused 54f680 |
+| 0x13a | `setItem` | - | 1 | 0 | | high | stSetitem | pops maximum (fixed); DAT_005fd0a4 = max, counter DAT_005fd09c = 0, enables item counter display (DAT_005fd0a0 = 1); unused 54f6a0 |
+| 0x13b | `setTimer` | - | 1 | 0 | | high | stSettimer | pops time (fixed, >>12) into DAT_005fd0ac and enables the timer (DAT_005fd0a8 = 1); unused 54f6d0 |
+| 0x13c | `timerOff` | - | 0 | 0 | | high | stTimeroff | disables the timer: DAT_005fd0a8 = 0; unused 54f6f0 |
+| 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) 54f890 |
+| 0x13e | `nop_0x13e` | - | 0 | 0 | | high | stSwim? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x13f | `nop_0x13f` | - | 0 | 0 | | high | stLoseCrystal? | no-op handler FUN_00426500 (does nothing, no inline operands); unused 426500 |
+| 0x140 | `nop_0x140` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x141 | `setDebugFlag5849a0On` | - | 0 | 0 | | medium | - | Writes short 1 to global 0x5849A0; that global is never read anywhere in groove.exe (dead PSX leftover). 54dab0 |
+| 0x142 | `setDebugFlag5849a0Off` | - | 0 | 0 | | medium | - | Writes short 0xFFFF to global 0x5849A0 (never read; dead leftover). 54daa0 |
+| 0x143 | `clearFlag5fd0b8` | - | 0 | 0 | | medium | - | Sets global 0x5FD0B8 = 0; global is never read elsewhere (dead leftover). 54da80 |
+| 0x144 | `setFlag5fd0b8` | - | 0 | 0 | | medium | - | Sets global 0x5FD0B8 = 1; global is never read elsewhere (dead leftover). 54da90 |
+| 0x145 | `nop_0x145` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x146 | `nop_0x146` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x147 | `nop_0x147` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 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). 54e430 |
+| 0x149 | `nop_0x149` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 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. 54f700 |
+| 0x14b | `setGlobal5fd086` | imm | 0 | 0 | | medium | - | Stores the low 16 bits of the inline word in global 0x5FD086 (never read elsewhere; dead leftover). 54e890 |
+| 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). 54e720 |
+| 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. 54f780 |
+| 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. 54f7f0 |
+| 0x14f | `nop_0x14f` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x150 | `nop_0x150` | - | 0 | 0 | 1 | high | - | Empty handler FUN_00426500 (shared no-op); does nothing (used once in HelpGirlSkip). 426500 |
+| 0x151 | `nop_0x151` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x152 | `nop_0x152` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 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). 554280 |
+| 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). 554310 |
+| 0x155 | `nop_0x155` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x156 | `nop_0x156` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x157 | `nop_0x157` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x158 | `nop_0x158` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x159 | `nop_0x159` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x15a | `setPosToPlayer` | - | 0 | 0 | 27 | high | - | Copies the player's position x/y/z (+0x30..+0x38) and +0x3C into the strat (player strat = 0x6D9E1C). 554480 |
+| 0x15b | `setPosToPlayerXZ` | - | 0 | 0 | | high | - | Copies the player's x and z (+0x30, +0x38) into the strat; y unchanged. 5544b0 |
+| 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). 5544d0 |
+| 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). 5544f0 |
+| 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). 554530 |
+| 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. 5545c0 |
+| 0x160 | `trailFree` | - | 1 | 0 | 6 | high | - | pops trail handle; releases it (clears owner +0x414) if nonzero. 554950 |
+| 0x161 | `setPauseState` | - | 1 | 0 | | medium | - | pops v (fixed); sets global pause state 0x584640 = v>>12 (0 = not paused, 1 = paused, 2 = pausing disabled). 554970 |
+| 0x162 | `setFlagE8_1000` | flags | 0 | 0 | | high | - | Inline word: nonzero sets, zero clears bit 0x1000 of strat flags (+0xE8). 54fdd0 |
+| 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. 54fe00 |
+| 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). 554990 |
+| 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. 554bc0 |
+| 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). 554bf0 |
+| 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. 54d970 |
+| 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). 54d990 |
+| 0x169 | `nop_0x169` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 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). 554cd0 |
+| 0x16b | `pushSelf` | - | 0 | 1 | | high | - | Pushes the pointer of the executing strat (this). 554cc0 |
+| 0x16c | `nop_0x16c` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x16d | `nop_0x16d` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 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. 5549c0 |
+| 0x16f | `clamp` | - | 3 | 1 | 459 | high | - | pops hi (top), x, lo; pushes max(lo, min(x, hi)) (push order lo, x, hi). 554a10 |
+| 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. 54f8d0 |
+| 0x171 | `distSqToPosXZ` | - | 2 | 1 | 394 | high | - | pops z (top), x; pushes squared horizontal distance from own position, (dx>>6)^2+(dz>>6)^2. 54f920 |
+| 0x172 | `approach` | - | 3 | 1 | 365 | high | - | pops step (top), target, value; pushes value moved toward target by step without overshooting. 554a70 |
+| 0x173 | `distSqXZ` | - | 4 | 1 | 28 | high | - | pops z2 (top), x2, z1, x1; pushes (x2-x1)^2 + (z2-z1)^2 in fixed point. 554ad0 |
+| 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). 54f150 |
+| 0x175 | `setFlagEC_80` | flags | 0 | 0 | | high | - | Inline word: nonzero sets, zero clears bit 0x80 of strat flags2 (+0xEC). 54f750 |
+| 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. 554b40 |
+| 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). 54f290 |
+| 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. 54f2f0 |
+| 0x179 | `nearestWaypoint` | - | 0 | 0 | | high | stNearestWaypointNext | Sets current waypoint (+0x120) to the waypoint of the list (+0x118) nearest (XZ) to the strat. 54f430 |
+| 0x17a | `nearestWaypointToPlayer` | - | 0 | 0 | | high | - | Sets current waypoint to the waypoint nearest (XZ) to the player. 54f470 |
+| 0x17b | `nearestOtherWaypoint` | - | 0 | 0 | | high | - | Sets current waypoint to the waypoint nearest (XZ) to the strat, excluding the current one. 54f4b0 |
+| 0x17c | `nop_0x17c` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x17d | `nop_0x17d` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x17e | `nop_0x17e` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x17f | `nop_0x17f` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 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). 54fef0 |
+| 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). 554d50 |
+| 0x182 | `nop_0x182` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 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). 554da0 |
+| 0x184 | `pushTintR` | - | 0 | 1 | | high | - | Pushes own tint red byte (+0x133) as fixed. 554dc0 |
+| 0x185 | `pushTintG` | - | 0 | 1 | | high | - | Pushes own tint green byte (+0x134) as fixed. 554de0 |
+| 0x186 | `pushTintB` | - | 0 | 1 | | high | - | Pushes own tint blue byte (+0x135) as fixed. 554e00 |
+| 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). 552f40 |
+| 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. 552fb0 |
+| 0x189 | `randSigned` | - | 1 | 1 | 922 | high | - | pops n; pushes a random value in (-n, n) ((rand32 - rand32) % n, same fixed scale as n). 54f9a0 |
+| 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. 5530b0 |
+| 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. 553100 |
+| 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. 552ff0 |
+| 0x18d | `stubPop2_0x18d` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). 555090 |
+| 0x18e | `stubPop2_0x18e` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). 555090 |
+| 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. 54f390 |
+| 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. 554e60 |
+| 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. 554f30 |
+| 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). 54d620 |
+| 0x193 | `stubPop1Push0_0x193` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. 5550b0 |
+| 0x194 | `stubPop1Push0_0x194` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. 5550b0 |
+| 0x195 | `stubPop2_0x195` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). 555090 |
+| 0x196 | `stubPop2_0x196` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). 555090 |
+| 0x197 | `nop_0x197` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x198 | `nop_0x198` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x199 | `nop_0x199` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x19a | `nop_0x19a` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x19b | `nop_0x19b` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x19c | `nop_0x19c` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x19d | `sqrt` | - | 1 | 1 | 67 | high | - | pops v (fixed); pushes fixed sqrt(v) (fsqrt of v/4096, rescaled). 54f610 |
+| 0x19e | `nop_0x19e` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x19f | `nop_0x19f` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x1a0 | `setWaypointParam1` | - | 1 | 0 | 167 | high | - | pops v; sets current waypoint +0x08 = (v>>12)&0xFFF (readable as own property 9). 54ea80 |
+| 0x1a1 | `setWaypointParam2` | - | 1 | 0 | 245 | high | - | pops v; sets current waypoint +0x0C = (v>>12)&0xFFF (readable as own property 10). 54eab0 |
+| 0x1a2 | `setWaypointParam3` | - | 1 | 0 | | high | - | pops v; sets current waypoint +0x10 = (v>>12)&0xFFF (readable as own property 11). 54eae0 |
+| 0x1a3 | `stubPop1_0x1a3` | - | 1 | 0 | | high | - | Stub FUN_00554fb0: pops and discards 1 value. 554fb0 |
+| 0x1a4 | `stubPop1_0x1a4` | - | 1 | 0 | | high | - | Stub FUN_00554fb0: pops and discards 1 value. 554fb0 |
+| 0x1a5 | `nop_0x1a5` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+| 0x1a6 | `emitterKill` | - | 1 | 0 | | high | - | pops slot (fixed); frees the strat's particle emitter in that slot (+0x150+slot = 0xFF) and resets the record. 553150 |
+| 0x1a7 | `stubPush0_0x1a7` | - | 0 | 1 | | high | - | Stub FUN_0054e880: pushes 0. 54e880 |
+| 0x1a8 | `processFirst` | - | 0 | 0 | 1 | high | - | Moves self to the head of the strat update list (0x6D9E38) if not already there. 554e20 |
+| 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). 554fc0 |
+| 0x1aa | `stubPop1Push0_0x1aa` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. 5550b0 |
+| 0x1ab | `stubPop2_0x1ab` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). 555090 |
+| 0x1ac | `stubPop1Push0_0x1ac` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. 5550b0 |
+| 0x1ad | `stubPop2Push0_0x1ad` | - | 2 | 1 | | high | - | Stub FUN_005550d0: pops 2 values and pushes 0. 5550d0 |
+| 0x1ae | `stubPop3_0x1ae` | - | 3 | 0 | | high | - | Stub FUN_005550f0: pops and discards 3 values. 5550f0 |
+| 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). 555040 |
+| 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. 553030 |
+| 0x1b1 | `nop_0x1b1` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. 426500 |
+
+## 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). |
diff --git a/src/main/java/com/eliasmueller/llamamod/editor/io/ProjectIO.java b/src/main/java/com/eliasmueller/llamamod/editor/io/ProjectIO.java
index e605a33..210b011 100644
--- a/src/main/java/com/eliasmueller/llamamod/editor/io/ProjectIO.java
+++ b/src/main/java/com/eliasmueller/llamamod/editor/io/ProjectIO.java
@@ -56,15 +56,24 @@ public final class ProjectIO {
public interface LoadHook {
void beforeChanges(Map json, Bases cd, Scene reference, Scene scene, List warnings)
throws IOException;
+
+ /** Hooks run in ascending order (imports = 0). */
+ default int order() {
+ return 0;
+ }
}
private static final List 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));
diff --git a/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/ConstantDialog.java b/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/ConstantDialog.java
new file mode 100644
index 0000000..8ab0412
--- /dev/null
+++ b/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/ConstantDialog.java
@@ -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 show(EditorContext ctx, List consts) {
+ JPanel p = new JPanel(new GridBagLayout());
+ GridBagConstraints g = new GridBagConstraints();
+ g.insets = new Insets(3, 4, 3, 4);
+ g.anchor = GridBagConstraints.WEST;
+ List fields = new ArrayList<>();
+ List 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(
+ "Fixed point: 1.0 = 4096 · angles: 360deg = 4096.0 · speeds are per frame (30 fps)."
+ + "
The change is stored in the project and applied to the level's code when it is built."),
+ 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 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);
+ }
+ }
+}
diff --git a/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/ScriptPanel.java b/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/ScriptPanel.java
new file mode 100644
index 0000000..34301f4
--- /dev/null
+++ b/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/ScriptPanel.java
@@ -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 consts, StratListing.Line source) {}
+
+ private record UnitItem(int unit, String label) {
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
+
+ private final EditorContext ctx;
+ private final JComboBox units = new JComboBox<>();
+ private final JComboBox 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 model = new DefaultListModel<>();
+ private final JList list = new JList<>(model);
+ private final JTextArea info = new JTextArea();
+ private final JLabel status = new JLabel(" ");
+ private StratListing listing;
+ private Set 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 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> 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 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 rows = new ArrayList<>();
+ if ("Script".equals(mode.getSelectedItem())) {
+ StratPseudo ps = StratPseudo.of(listing);
+ List> 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 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 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 changes = ConstantDialog.show(ctx, r.consts);
+ if (changes == null || changes.isEmpty()) {
+ return;
+ }
+ Project p = ctx.document().project();
+ int unit = listing.unit;
+ List 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;
+ }
+ }
+}
diff --git a/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/StratEdits.java b/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/StratEdits.java
new file mode 100644
index 0000000..54c87ca
--- /dev/null
+++ b/src/main/java/com/eliasmueller/llamamod/editor/tools/strats/StratEdits.java
@@ -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 json() {
+ Map 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 json() {
+ Map m = new LinkedHashMap<>();
+ m.put("name", name);
+ m.put("basedOn", basedOn == null ? "" : basedOn);
+ m.put("entry", entryLabel);
+ m.put("source", source);
+ Map 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