strat scripts: all 434 vm opcodes named from groove.exe handlers (argonaut names via croc 2 lineage), own assembly syntax with byte-exact round trip over all 1461 code units, script view and script panel in the editor, editing values with undo, custom object types from script or clone, manual chapter with the bucky circle tutorial
This commit is contained in:
1 parent
b36a366240
commit
2386443fac
32 files changed
+5983
-5
No files matched your search
@@ -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).
|
||||
Reference in new issue
Block a user