strats: custom object types can be edited and deleted with undo (code rebuilt, objects follow their type), low-confidence opcodes re-checked (353 high, 2 low left), script view with do-while, while, break, continue, switch and else-if (gotos 11019 -> 400), first script compiler (structured script -> assembly, script stored with the type)
This commit is contained in:
1 parent
b35792a4ed
commit
8b78958306
21 files changed
+2332
-154
No files matched your search
+85
-8
@@ -9,7 +9,8 @@ the STPC chunk as **code units** (one per compiled source file; notes/editor-cro
|
||||
| `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) |
|
||||
| `StratPseudo` | read-only structured view (expressions, if/else, loops, switch, break/continue) |
|
||||
| `StratCompiler` | script language → assembly (new behaviour) |
|
||||
| `Num` | literals: `42`, `0x2a`, `1.5` (20.12), `90deg` |
|
||||
|
||||
**Round trip (golden test `StratRoundTripTest`)**: disassemble → assemble is byte-identical for
|
||||
@@ -95,11 +96,23 @@ own-unit addresses), plus the model/animation/unit offsets of the target STPC fo
|
||||
|
||||
## 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):
|
||||
Read-only (not parsed back). Stack code folded into expressions (`self.y = self.floorHeight + 204`),
|
||||
properties as `self.x`, `player.y`, `parent.v3`, `camera.…`. Control flow is rebuilt from the jumps
|
||||
where the blocks nest:
|
||||
|
||||
| Jumps | Shown as |
|
||||
|---|---|
|
||||
| forward `jz`/`jnz` over a block (+ `jmp` over a second block) | `if (…) {…} else {…}`; an `if` that is the whole else part → `} else if (…) {`; an empty then-part is negated |
|
||||
| backward `jmp`/`jmpYield` | `loop {… yield}`; first statement `if (c) break` → `while (!c) {…}` |
|
||||
| backward conditional jump | `do {…} while (c)` (`/*yield*/` for jzYield/jnzYield; empty body: `do {} while (c) /*yield*/`) |
|
||||
| jump to the end / head of the innermost loop | `break` / `continue` (also as `if (c) break`) |
|
||||
| `switch` + `jmp end` with the cases in between | `switch (v) { case 1.0: … break … }` (`default:` when the jmp goes to a case-like block) |
|
||||
|
||||
Everything else stays `goto label` (mostly multi-level breaks out of nested loops and `||` chains
|
||||
that jump to a common far target). Over all 1461 units: **11 019 → 400 gotos** (2026-11-07;
|
||||
guarded by `StratPseudoTest`, which also checks balanced blocks and that every remaining goto has
|
||||
its label). Inline operands in `<>`: `spawn<3, 6, 0, 0, 0>(10.0, 204, 450, unit:35+0x28)`.
|
||||
Example (Coin):
|
||||
|
||||
```
|
||||
strat Coin:
|
||||
@@ -116,6 +129,58 @@ if (v0) {
|
||||
loop { turnLeft(5.625deg); yield }
|
||||
```
|
||||
|
||||
## Script language (`StratCompiler`) – writing NEW behaviour
|
||||
|
||||
A small structured language in the style of the script view, compiled to the assembly above (so
|
||||
everything the assembler checks still applies). Implemented slice (2026-11-07): strats, subroutines,
|
||||
aliases, assignments, expressions, if/else if/else, while, loop, do-while, break/continue, yield,
|
||||
calls of any opcode by name. Tests: `StratCompilerTest`; verified in game (village 1:
|
||||
`img/mods/strats_script_game.jpg`).
|
||||
|
||||
```
|
||||
// bobs one cell up and down and turns
|
||||
strat Bobber {
|
||||
alias base = v0 // .alias base = v0
|
||||
alias phase = v1
|
||||
setModel(model:5) // any opcode: name<inline operands>(stack arguments)
|
||||
base = self.y + 1.0
|
||||
phase = 0
|
||||
loop {
|
||||
phase = phase + 4deg
|
||||
self.y = base + sin(phase)
|
||||
turnLeft(4deg)
|
||||
yield // last statement of a loop → the loop's jmpYield back edge
|
||||
}
|
||||
}
|
||||
|
||||
sub wobble { // called with "call wobble", ends with return
|
||||
self.rotZ = sin(self.rotY) * 0.1
|
||||
}
|
||||
```
|
||||
|
||||
| Construct | Compiles to |
|
||||
|---|---|
|
||||
| `strat Name { … }` | `.string "Name"`, label `Name:`, `debugName "Name"`, body, `end` unless the body cannot fall through |
|
||||
| `sub name { … }` / `call name` / `return` | label, body, `return` / `call name` / `return` |
|
||||
| `x = e`, `x += e` (`-=`, `*=`, `/=`) | e, pop op of x; `x += 1.0` / `x -= 1.0` → address + `inc`/`dec` |
|
||||
| places | `vN`, alias, `gN`, `self.prop`, `player.prop`, `parent.prop`, `parent.vN`, `camera.`/`target.`/`target2.`/`boss.`/`dialog.prop` (props.tsv names or `propN`) |
|
||||
| literals | as in the assembly: `10.0` = ten, `10` = 10 raw units (1/4096), `90deg`, `0x10`; 16-bit values use `pushShort`, others `push` |
|
||||
| references | `model:N`, `anim:N`, `unit:N` (`address`), `sound:N` (`soundAddress`), `tex:N` (`push`) |
|
||||
| operators (low → high) | `||` `|` (or) · `&&` (and) · `&` · `==` `!=` · `<` `>` `<=` `>=` · `<<` `>>` · `+` `-` · `*` `/` · unary `-` `!` |
|
||||
| `op<a, b>(e1, e2)` | e1, e2, `op a, b`; the argument count must match the opcode's pops; a value-returning op used as a statement is followed by `pop` |
|
||||
| `if (c) {…} else if … else {…}` | `c; jz else; …; jmp end; else: …` |
|
||||
| `while (c) {…}` / `loop {…}` | head label, `c; jz end`, body, `jmp`/`jmpYield head` |
|
||||
| `do {…} while (c)` | body, `c; jnz head` |
|
||||
| `break` / `continue` / `yield` / `end` | `jmp end` / `jmp head` (do: the condition) / `jmpYield next` / `end` |
|
||||
|
||||
The generated assembly starts with the script as `;| ` comment lines, so the editor can show the
|
||||
script again (*Edit type…*). Comments: `//`, `/* */`; `;` and newlines separate statements.
|
||||
|
||||
**Next steps (not implemented)**: event handlers (`on touched { … }` → `.event` table + `onEvent`),
|
||||
`switch`, `spawn` with typed arguments, local names for parameters from the object schema, `for`,
|
||||
constant folding, typed numbers (int vs fixed) instead of the assembly's literal rules,
|
||||
short-circuit `&&`/`||` (now: both sides are evaluated, like the original `and`/`or` opcodes).
|
||||
|
||||
## Editing in the level editor (`editor.tools.strats`)
|
||||
|
||||
* **Script panel** (bottom dock, View → Panels → Script, Ctrl+Shift+K): unit chooser (all code units
|
||||
@@ -136,6 +201,15 @@ loop { turnLeft(5.625deg); yield }
|
||||
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).
|
||||
* **Edit type… / Delete type…** (Script panel, Inspector *Script* section of a custom object):
|
||||
edit opens the same dialog with the type's source (script or assembly), *Apply* rebuilds all
|
||||
custom units (`TypeEdits.edit`, one undo step) and the placed objects move to the new entry.
|
||||
Delete warns with the number of objects that use the type and removes them with it
|
||||
(`TypeEdits.delete`: `Edits.delete` of the users + the type list change, one undo step).
|
||||
Creating is undoable too (`TypeEdits.create`), except when imported types follow the custom
|
||||
block (then appended as before).
|
||||
* **Language** (dialog): *Assembly* or *Script* (compiled by `StratCompiler`, see above);
|
||||
switching Script → Assembly shows the generated code.
|
||||
* **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
|
||||
@@ -143,8 +217,11 @@ loop { turnLeft(5.625deg); yield }
|
||||
|
||||
## 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.
|
||||
* Custom types are edited and deleted as a whole list: the custom units are cut off the code and
|
||||
appended again (objects follow their type's new entry). Not possible while object types imported
|
||||
after the custom ones sit behind them in the code (save and reopen the project first; on load the
|
||||
custom types always come last). A custom type that addresses another custom type's unit with
|
||||
`unit:N` breaks when that index changes. 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).
|
||||
|
||||
Reference in new issue
Block a user