Files
llamamod/notes/strat-language.md
T

8.3 KiB
Raw Blame History

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