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:
MineTec committed 2026-11-07 12:17:32 +01:00
1 parent b35792a4ed
commit 8b78958306
21 files changed
+2332 -154

No files matched your search

+85 -8
View File
@@ -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).