8.3 KiB
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
addressoperands 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.jsonstratEdits.constantsas{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, everyaddressoperand 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.
pushShortholds 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).