# 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).