152 lines
8.3 KiB
Markdown
152 lines
8.3 KiB
Markdown
# 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).
|