strats: custom types stay editable after importing object types (custom code kept last), references between custom types by name, edit can change where fields come from, opcode aliases with clearer names, script view gotos 400 -> 31 (labeled break/continue, condition chains, jumps to return), script language 2: event handlers, switch, short-circuit && ||, constant folding, schema parameter names, plain numbers in cells
This commit is contained in:
1 parent
d6f84be125
commit
386a06006d
21 files changed
+1771
-252
No files matched your search
+23
-13
@@ -36,7 +36,9 @@ one, and how many objects of it are placed.
|
||||
meaning, the script uses its name (`snapToFloor`).
|
||||
* `g12` is a global variable shared by all scripts.
|
||||
* Numbers are in game units: `1.0` = one cell (or "true"), angles are written like `5.625deg`.
|
||||
Speeds and turn rates are **per frame** (the game runs at 30 frames per second).
|
||||
Speeds and turn rates are **per frame** (the game runs at 30 frames per second). Small raw
|
||||
numbers have an `r`: `204r` is 204/4096 of a cell (about 0.05).
|
||||
* `break name` / `continue name` leave or repeat the outer loop labelled `name:`.
|
||||
* `yield` ends the object's turn for this frame; a `loop { … yield }` runs once per frame.
|
||||
* `onEvent` installs a handler, for example "when the player touches me" (event type 4).
|
||||
* `spawn<…>(…)` creates another object, `address model:5` names a 3D model of the level.
|
||||
@@ -55,7 +57,7 @@ can be edited show a tooltip; edited lines turn **orange**.
|
||||
* It applies to **every object of that type** in the level, also to objects other scripts spawn.
|
||||
* It is saved in the project and written into the level when you build or play.
|
||||
|
||||
Example: in the Coin's script, `self.y = self.floorHeight + 204` puts snapping coins just above
|
||||
Example: in the Coin's script, `self.y = self.floorHeight + 204r` puts snapping coins just above
|
||||
the floor. Changing `204` to `1.5` makes them float one and a half cells high:
|
||||
|
||||

|
||||
@@ -70,7 +72,8 @@ empty template) opens the type editor:
|
||||
1. **Name** – the name of the new type (letters, digits, `_`).
|
||||
2. **Entry label** – where the script starts (for clones: the renamed strat).
|
||||
3. **Type fields / parameters from** – which existing type's settings (parameters, waypoints,
|
||||
flags) new objects of this type start with.
|
||||
flags) new objects of this type start with. In a script you can use that type's parameter
|
||||
names (for a Coin: `snapToFloor` for parameter 0).
|
||||
4. Edit the source. **Check** assembles it and verifies the result; errors name the line.
|
||||
5. **Create type**. The type appears in **Assets → Objects** and can be placed like any other.
|
||||
|
||||
@@ -83,7 +86,9 @@ Select an object of a custom type (or pick the type in the Script panel's *Type*
|
||||
|
||||
* **Edit type…** (Script panel or the Inspector's *Script* section) opens the type editor with
|
||||
its script. Change it, **Check**, then **Apply**. Objects of the type that are already placed
|
||||
keep their place and run the new script. **Ctrl+Z** undoes the change.
|
||||
keep their place and run the new script. **Ctrl+Z** undoes the change. *Type fields /
|
||||
parameters from* can be changed here too: new objects start with the other type's settings,
|
||||
placed ones keep theirs.
|
||||
* **Delete type…** removes the type. If objects in the level use it, the editor tells you how many
|
||||
and deletes them together with the type (also undoable).
|
||||
|
||||
@@ -113,12 +118,19 @@ strat Pulser {
|
||||
```
|
||||
|
||||
* Statements: `x = value`, `x += value`, `if (…) { } else { }`, `while (…) { }`, `loop { }`,
|
||||
`do { } while (…)`, `break`, `continue`, `yield`, `end`, `call name` (for `sub name { … }`).
|
||||
`do { } while (…)`, `switch (…) { case 1: … break default: … }`, `break`, `continue`, `yield`,
|
||||
`end`, `call name` (for `sub name { … }`).
|
||||
* `on touched { … }` runs its block when the player touches the object (the object needs
|
||||
`collisionOn<0x20000>()` and `collRadius(…)`, like a coin); `on event(type, param) { … }` for
|
||||
other events. The block ends like a sub (`remove` deletes the object).
|
||||
* `a && b` and `a || b` in conditions stop as soon as the answer is known.
|
||||
* Any instruction can be called by name with its values in brackets: `turnLeft(4deg)`,
|
||||
`scale(size)`; values that are part of the instruction go in `<>`: `collisionOn<0x8000>()`.
|
||||
* Places: `v0`… (or a name given with `alias`), globals `g12`, properties `self.y`, `player.x`…
|
||||
* Numbers mean the same as in the assembly: write `10.0` for ten (a plain `10` is ten 1/4096
|
||||
steps), angles as `90deg`.
|
||||
* Numbers: `10` and `10.0` both mean ten cells, angles are written `90deg`. For a raw value (as
|
||||
the Script view shows them) add `r`: `215r` = 215/4096; hex like `0x8000` is raw too. Values
|
||||
inside `<…>` are raw. Scripts written before this rule start with `use rawNumbers` when you open
|
||||
them, so they keep working.
|
||||
* **Check** compiles and verifies; errors name the line. Switching *Language* back to *Assembly*
|
||||
shows the generated instructions. The full reference is in `notes/strat-language.md`.
|
||||
|
||||
@@ -138,11 +150,11 @@ in one test level below: on the left the waypoint way, on the right the script w
|
||||
|
||||
1. Import Bucky (**Assets → All worlds**, search "Bucky", drag it into the level).
|
||||
2. Select it and open the **Script** panel. The script shows `wpFirst()`, `wpNext()` and
|
||||
`wpTurnToY(v1)` / `moveForwardQ(215)`: Bucky **runs along its waypoint chain**, turning
|
||||
`wpTurnToY(v1)` / `moveForwardQ(215r)`: Bucky **runs along its waypoint chain**, turning
|
||||
with the rate in parameter 1 and moving 215/4096 cells per frame.
|
||||
3. At every node it reads `self.wpParam` (the node's field **+26**): the low part chooses run
|
||||
(`0`) or jump (`1.0`, `2.0`), the high part `128.0` means "run on", `64.0` "wait until the
|
||||
player comes close". At the end: `if (!(!v0 && (self.wpEndFlags & 2))) goto …` – with
|
||||
player comes close". At the end: `} while (!(!v0 && (self.wpEndFlags & 2r)))` – with
|
||||
**parameter 0** set the chain repeats instead of ending.
|
||||
|
||||
### Way 1: a circular waypoint chain (no script change)
|
||||
@@ -182,15 +194,13 @@ circle:
|
||||
|
||||
## Limits
|
||||
|
||||
* Custom types can't be edited or deleted right after importing object types from another level
|
||||
in the same session – save and reopen the project first.
|
||||
* Big structural changes are up to you: the editor checks that the code is well-formed and that
|
||||
every model/animation/code reference points somewhere valid, not that the object behaves well.
|
||||
* Names of rarely used instructions are educated guesses (the opcode list shows a confidence for
|
||||
each).
|
||||
* Small constants stored as short numbers only accept −8.0 … 7.99 (see *Changing numbers*).
|
||||
* The Script view shows `while`, `do … while`, `switch`, `break` and `continue` where it can and
|
||||
`goto` where the original jumps don't form nested blocks (a few hundred places in the whole
|
||||
game); the Assembly view is always exact.
|
||||
`goto` where the original jumps don't form nested blocks (31 places in the whole game, all in
|
||||
the llama's script); the Assembly view is always exact.
|
||||
* In the clone dialog, renaming the type doesn't yet rename the entry label or the name inside
|
||||
the script text – edit those by hand if you want them to match.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
+82
-33
@@ -74,11 +74,12 @@ L_0218:
|
||||
| – 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) |
|
||||
| – renamed opcodes | former names (`flagEC10000` …, `op-aliases.tsv`) still assemble |
|
||||
| – `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) |
|
||||
| `address ref` (0xb2) | `model:N[+off]`, `anim:N[+off]`, `unit:N[+off]`, `unit:Name[+off]` (custom types, see below), `@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`) |
|
||||
@@ -108,21 +109,31 @@ where the blocks nest:
|
||||
| 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)`.
|
||||
| jump out of an outer loop (to its end / head) | `break name` / `continue name`, the loop shown as `name: loop {` (`while`, `do`) |
|
||||
| jump to a `return` / `end` / `remove` | `return` / `end` / `remove` (the same instruction) |
|
||||
| conditional jumps separated only by expression code, to a common false target / the body | one condition with `&&` / `||` (short-circuit; also what the compiler generates) |
|
||||
| `if (a) { if (b) {…} }` with nothing else in the outer block | `if (a && b) {…}`; leading `if (c) break` lines of a loop → `while (!c && …)` |
|
||||
|
||||
Everything else stays `goto label`. Over all 1461 units: **11 019 → 400 → 31 gotos** (2026-11-07,
|
||||
second pass; 138 labeled break/continue). The 31 left are one pattern in the Llama strat of every
|
||||
level: a jump from inside a then-part into the else part (`if (a) { call; if (!b) goto else; … }
|
||||
else {…}`); earlier versions showed these as a nested `if` without the else, which was wrong
|
||||
(`fits` now refuses an inner if that targets the outer if's else label). Guarded by
|
||||
`StratPseudoTest` (balanced blocks, every goto has its label, every `break name` names a loop).
|
||||
Inline operands in `<>`: `spawn<3, 6, 0, 0, 0>(10.0, 204r, 450r, unit:35+0x28)`. Raw integers
|
||||
pushed on the stack are written with an `r` suffix (`204r`), as in the script language (below);
|
||||
`1.0`-style and `deg` values as in the assembly.
|
||||
Example (Coin):
|
||||
|
||||
```
|
||||
strat Coin:
|
||||
setModel(model:5)
|
||||
collRadius(409)
|
||||
collRadius(409r)
|
||||
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.y = self.floorHeight + 204r
|
||||
self.rotY = rnd(4096.0)
|
||||
}
|
||||
}
|
||||
@@ -132,10 +143,15 @@ 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`).
|
||||
everything the assembler checks still applies). Language version 2 (2026-11-07): strats, subs,
|
||||
aliases, parameter names, assignments, expressions with constant folding, short-circuit `&&`/`||`,
|
||||
if/else if/else, while, loop, do-while, switch, break/continue, yield, event handlers, calls of any
|
||||
opcode by name. Tests: `StratCompilerTest`. In game (village 1, apptest, 2026-11-07): version 1
|
||||
scripts `img/mods/strats_script_game.jpg`; version 2 (`StratsGameLevel … v2`: a custom type, then
|
||||
an imported Spider in the same session, the custom type edited afterwards, compiled Toucher/Ticker
|
||||
with `on touched` / `on event(0x40, 30)`, switch, `&&`/`||`) loads and plays without errors
|
||||
(`img/mods/strats2_game_import.jpg`); the handlers' visible effect was not confirmed (the test
|
||||
objects were out of the camera's view under the slow harness).
|
||||
|
||||
```
|
||||
// bobs one cell up and down and turns
|
||||
@@ -143,8 +159,11 @@ 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
|
||||
base = self.y + 1 // 1 = one cell
|
||||
phase = 0
|
||||
on touched { // event handler (type 4), runs as a subroutine
|
||||
soundPlay(sound:6)
|
||||
}
|
||||
loop {
|
||||
phase = phase + 4deg
|
||||
self.y = base + sin(phase)
|
||||
@@ -160,26 +179,44 @@ sub wobble { // called with "call wobble", ends with return
|
||||
|
||||
| Construct | Compiles to |
|
||||
|---|---|
|
||||
| `strat Name { … }` | `.string "Name"`, label `Name:`, `debugName "Name"`, body, `end` unless the body cannot fall through |
|
||||
| `strat Name { … }` | `.string "Name"`, label `Name:`, `debugName "Name"`, body, `end` unless the body cannot fall through; event handler code behind it |
|
||||
| `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` |
|
||||
| `x = e`, `x += e` (`-=`, `*=`, `/=`) | e, pop op of x; `x += 1` / `x -= 1` (= 1.0) → address + `inc`/`dec` |
|
||||
| places | `vN`, alias, parameter name, `gN`, `self.prop`, `player.prop`, `parent.prop`, `parent.vN`, `camera.`/`target.`/`target2.`/`boss.`/`dialog.prop` (props.tsv names or `propN`) |
|
||||
| numbers | plain integer = game units: `10` = 10.0 (40960 raw); `1.5`, `90deg` as in the assembly; **raw words**: `215r` or hex `0x10` |
|
||||
| references | `model:N`, `anim:N`, `unit:N` / `unit:Name` (`address`), `sound:N` (`soundAddress`), `tex:N` (`push`) |
|
||||
| operators (low → high) | `||` · `&&` · `|` · `&` · `==` `!=` · `<` `>` `<=` `>=` · `<<` `>>` · `+` `-` · `*` `/` · unary `-` `!` |
|
||||
| constant folding | operators over constants are computed with the VM's rules (20.12 `*` = a·b>>12, `/` = (a<<8)/b<<4, shifts by b>>12, comparisons 0/1): `2 * 3 + 0.5` → `pushShort 6.5` |
|
||||
| `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`. Inline operands `<…>` are raw words as in the assembly |
|
||||
| conditions (`if`, `while`, `do … while`) | **short-circuit**: `a && b` → `a; jz F; b; jz F`, `a || b` → `a; jnz T; b; jz F; T:`; `!` swaps the jump; constant conditions need no code. In a value (`x = a && b`) both sides are evaluated (`and`/`or` opcodes) |
|
||||
| `if (c) {…} else if … else {…}` | `c → else; …; jmp end; else: …` |
|
||||
| `while (c) {…}` / `loop {…}` | head label, `c → end`, body, `jmp`/`jmpYield head` |
|
||||
| `do {…} while (c)` | body, `c → head` |
|
||||
| `switch (e) { case 1: … break … default: … }` | `e; switch tab; jmp default/end`, case bodies in order (C rules: fall through, `break` leaves the switch, `continue` the enclosing loop); table `.switch n` + `.case [imm] v, label` (short form when v fits 16 bits). Case values are constants |
|
||||
| `on touched { … }` / `on event(type[, param]) [slot n] { … }` | `onEvent ev, slot` where it stands (slots 0, 1, … in order unless given); entry `.event type, param, handler`; the handler body (ends with `return`; `remove`/`end` allowed) behind the strat. type/param/slot are raw words; type 4 = touched (`collisionOn` + `collRadius` needed, like Coin), 0x40 = timer, 0x80/0x100 = player distance |
|
||||
| `break` / `continue` / `yield` / `end` | `jmp end` / `jmp head` (do: the condition) / `jmpYield next` / `end` |
|
||||
| `use rawNumbers` | the version-1 number rule (plain integers are raw words) for the rest of the script |
|
||||
|
||||
Layout: event entries and switch tables go to the **start** of the unit (like the original code):
|
||||
data behind a terminating instruction would be decoded as dead code by `StratCode`'s
|
||||
terminator-follower pass and break the listing.
|
||||
|
||||
**Parameter names**: `StratCompiler.compile(src, names)` takes predefined names → `vN`; the editor
|
||||
passes the parameter names of the object schema of the type the custom type's fields come from
|
||||
(`StratEdits.paramNames`, the same names as the script view's aliases, e.g. `snapToFloor` = v0
|
||||
for Coin). The script's own `alias` wins.
|
||||
|
||||
**Numbers, compatibility**: language version 1 (before 2026-11-07) read `10` as 10 raw units. The
|
||||
compiler now writes the marker line `; script language 2` behind the embedded source;
|
||||
`embeddedSource` of older output (no marker) puts `use rawNumbers` in front of the script, so an
|
||||
old type opened with *Edit type…* compiles to the same code. Stored types are assembly anyway
|
||||
(the script is a comment), so loading never recompiles.
|
||||
|
||||
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).
|
||||
**Not implemented**: `for`, `spawn` with typed arguments, typed variables (int vs fixed), a view of
|
||||
event handlers as `on … { }` (the script view shows `onEvent` + the handler label).
|
||||
|
||||
## Editing in the level editor (`editor.tools.strats`)
|
||||
|
||||
@@ -197,7 +234,8 @@ short-circuit `&&`/`||` (now: both sides are evaluated, like the original `and`/
|
||||
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}`.
|
||||
catalogue (Assets → Objects), stores `stratEdits.types` `{name, basedOn, entry, source, proto
|
||||
[, fieldsFrom]}` (`fieldsFrom` = type of the fields/parameter names when it differs from basedOn).
|
||||
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).
|
||||
@@ -206,8 +244,20 @@ short-circuit `&&`/`||` (now: both sides are evaluated, like the original `and`/
|
||||
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).
|
||||
Creating is undoable too (`TypeEdits.create`). In edit mode *Type fields / parameters from* can
|
||||
be switched to another type (new placements start with its fields; placed objects keep theirs);
|
||||
`(keep current fields)` is the default. Renaming a type also renames `unit:Old` in the other
|
||||
custom types.
|
||||
* **References between custom types**: `unit:N` operands that point at a custom type are stored
|
||||
as `unit:TypeName` (`StratEdits.symbolic` on create/edit; on load for older projects, with the
|
||||
saved `session.firstUnit`); the assembler resolves names through `Layout.withUnitNames`, the
|
||||
listing prints them (`Names.unitName`). Deleting a referenced type is refused ("unknown unit
|
||||
name").
|
||||
* **Imports in the same session** (CrossImport `CodeHook`): the merge is computed on the code
|
||||
without the custom units (`mergeBase`), and the commit cuts the custom units off, merges, and
|
||||
appends them again behind the imported code (`StratEdits.aroundImport`; objects follow by type
|
||||
index and offset from the entry) – the same order as on load, so editing/deleting/undoable
|
||||
creating keep working without reopening. (Imports themselves still clear the undo history.)
|
||||
* **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
|
||||
@@ -218,10 +268,9 @@ short-circuit `&&`/`||` (now: both sides are evaluated, like the original `and`/
|
||||
## Limits
|
||||
|
||||
* 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.
|
||||
appended again (objects follow their type's new entry). Projects saved by older versions with
|
||||
imports behind the custom types are reordered on load as before. 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).
|
||||
|
||||
+11
-6
@@ -21,8 +21,13 @@ properties). Language/syntax: `notes/strat-language.md`. Disassembler/assembler:
|
||||
registered with 0x113; 0x14a is a no-op (FUN_004268c0 only acts for 0); 0x9c's value is dead;
|
||||
0x18a/0x18b set the pending particle settings consumed by the next spawnParticle; 0x1a9's top
|
||||
value is the rain texture and the others are wind x, amount, wind z; all no-op/drop stubs verified.
|
||||
Still low: 0x34 and 0xf2 (no reader of their flag bits found). Names unchanged (stored sources use
|
||||
them); see the table for the new descriptions.
|
||||
Still low: 0x34 and 0xf2 (no reader of their flag bits found); see the table for the new descriptions.
|
||||
* **Renamed opcodes (aliases)**: placeholder names whose meaning the review established got clear
|
||||
names – 0x2f `flagEC10000` → `freezeOffCamera`, 0x31 `flagEC2` → `touchable`, 0x33 `flagEC100` →
|
||||
`noFloorPhysics`, 0x14a `hudReset2` → `hudResetNoop`. The old names stay valid mnemonics
|
||||
(resource `op-aliases.tsv`, `OpTable.alias`), so stored sources still assemble; listings and the
|
||||
compiler use the new names. Names of flags without a known reader (0x32, 0x34, 0x141–0x144,
|
||||
0x14b) keep their address-style names.
|
||||
* **252 opcodes matched to Argonaut names** (column *ref*: Croc 2 PSX demo jump table / Croc 1
|
||||
handler names), by handler behaviour and table position. 99 slots are no-ops, error stubs or
|
||||
pop-only stubs (dropped Croc features).
|
||||
@@ -120,11 +125,11 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x2c | `nop_0x2c` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
|
||||
| 0x2d | `nop_0x2d` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
|
||||
| 0x2e | `nop_0x2e` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
|
||||
| 0x2f | `flagEC10000` | imm | 0 | 0 | 1888 | medium | - | arg!=0 sets, 0 clears +0xEC bit 0x10000 = "freeze when off-camera": the camera pass (FUN_00436190) sets +0xEC 0x8000 when the strat is outside the camera range; with both bits set the frame loop (0x54c603) and the strat-vs-strat collision loop (0x54cc1a) skip it. Used by ~1350 strats (pickups, triggers, enemies). <sub>54f720</sub> |
|
||||
| 0x2f | `freezeOffCamera` (was `flagEC10000`) | imm | 0 | 0 | 1888 | medium | - | arg!=0 sets, 0 clears +0xEC bit 0x10000 = "freeze when off-camera": the camera pass (FUN_00436190) sets +0xEC 0x8000 when the strat is outside the camera range; with both bits set the frame loop (0x54c603) and the strat-vs-strat collision loop (0x54cc1a) skip it. Used by ~1350 strats (pickups, triggers, enemies). <sub>54f720</sub> |
|
||||
| 0x30 | `trackPlayer` | imm | 0 | 0 | | medium | - | If no player yet: re-executes itself next frame (pc-=4, yield); else arg sets/clears +0xEC bit 0x4000 = update distance to player each frame (+0x148). <sub>54fa80</sub> |
|
||||
| 0x31 | `flagEC2` | imm | 0 | 0 | 410 | medium | - | arg sets/clears +0xEC bit 2 = "others can stand on / touch me": in the strat collision loop (0x54cc96) a strat touching a strat with this bit gets +0xE8 0x400000 (and 0x800000 when it moves) = properties 32/33; also readable as property 0x1f. Set by Llama, Coin, Jar, EnemyCoin, RubyIdol. <sub>54e090</sub> |
|
||||
| 0x31 | `touchable` (was `flagEC2`) | imm | 0 | 0 | 410 | medium | - | arg sets/clears +0xEC bit 2 = "others can stand on / touch me": in the strat collision loop (0x54cc96) a strat touching a strat with this bit gets +0xE8 0x400000 (and 0x800000 when it moves) = properties 32/33; also readable as property 0x1f. Set by Llama, Coin, Jar, EnemyCoin, RubyIdol. <sub>54e090</sub> |
|
||||
| 0x32 | `flagE8_80000000` | imm | 0 | 0 | | medium | - | arg sets/clears +0xE8 bit 31; no reader of the bit found in the exe (scan of all +0xE8 flag tests); unused in the levels. <sub>54dd90</sub> |
|
||||
| 0x33 | `flagEC100` | imm | 0 | 0 | 60 | medium | - | arg sets/clears +0xEC bit 0x100; the collision response (0x4044f7) skips the floor/slope handling when it is set. Used by LlamaSpit, DropObject, RollerCoaster, KuscoTurtle (objects that move by script, not by floor physics). <sub>54dd30</sub> |
|
||||
| 0x33 | `noFloorPhysics` (was `flagEC100`) | imm | 0 | 0 | 60 | medium | - | arg sets/clears +0xEC bit 0x100; the collision response (0x4044f7) skips the floor/slope handling when it is set. Used by LlamaSpit, DropObject, RollerCoaster, KuscoTurtle (objects that move by script, not by floor physics). <sub>54dd30</sub> |
|
||||
| 0x34 | `flagEC200000` | imm | 0 | 0 | 50 | low | - | arg sets/clears +0xEC bit 0x200000; no direct test of the bit found in the exe (all +0xEC flag tests scanned). Used by Llama, RiverCroc, RiverLog, Rock, river NPCs (river/canoe level objects). <sub>54dd60</sub> |
|
||||
| 0x35 | `nop_0x35` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
|
||||
| 0x36 | `nop_0x36` | - | 0 | 0 | | high | - | No-op (shared empty handler); unused. <sub>426500</sub> |
|
||||
@@ -403,7 +408,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x147 | `nop_0x147` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
|
||||
| 0x148 | `gotoLevel4_1` | - | 0 | 0 | | medium | - | Hardcoded level change: next level (0x584EB4/EB8/EBC/EC0) = 4/1/1/0, previous level (0x585008..14) = 4/1/4/0, game state 0x584638 = 7 (load level). <sub>54e430</sub> |
|
||||
| 0x149 | `nop_0x149` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
|
||||
| 0x14a | `hudReset2` | - | 0 | 0 | | high | - | Calls FUN_004268c0(2), which returns at once for any argument other than 0 (verified in the assembly): a no-op. Unused. <sub>54f700</sub> |
|
||||
| 0x14a | `hudResetNoop` (was `hudReset2`) | - | 0 | 0 | | high | - | Calls FUN_004268c0(2), which returns at once for any argument other than 0 (verified in the assembly): a no-op. Unused. <sub>54f700</sub> |
|
||||
| 0x14b | `setGlobal5fd086` | imm | 0 | 0 | | medium | - | Stores the low 16 bits of the inline word in global 0x5FD086 (never read elsewhere; dead leftover). <sub>54e890</sub> |
|
||||
| 0x14c | `removeCollPoint` | - | 1 | 0 | 49 | medium | stSetCollPoint (inverse) | pops id (fixed, >>12); removes the collision point with that id from the strat's collision point list (+0xC8 array of 4-byte entries, count +0xC6). Counterpart of 0xc6 (add/set collision point). <sub>54e720</sub> |
|
||||
| 0x14d | `setFlagEC_400000` | flags | 0 | 0 | | high | - | Inline word: nonzero sets, zero clears bit 0x400000 of strat flags2 (+0xEC); the bit selects which height constant own property 0xF2 returns. <sub>54f780</sub> |
|
||||
|
||||
@@ -56,6 +56,23 @@ public final class CrossImport {
|
||||
/** Makes sure the load hook is registered. */
|
||||
public static void init() {}
|
||||
|
||||
/**
|
||||
* Lets the custom strat types (editor.tools.strats) step aside during an import: the merge
|
||||
* runs on mergeBase (the code without them, null = unchanged) and the commit inside around,
|
||||
* which takes them off and appends them again behind the imported code.
|
||||
*/
|
||||
public interface CodeHook {
|
||||
byte[] mergeBase(Scene s);
|
||||
|
||||
void around(Project p, Runnable commit);
|
||||
}
|
||||
|
||||
private static volatile CodeHook codeHook;
|
||||
|
||||
public static void setCodeHook(CodeHook h) {
|
||||
codeHook = h;
|
||||
}
|
||||
|
||||
/** One imported type: source level, type name and its code entry in the source STPC. */
|
||||
public record TypeRef(String level, String type, int code) {
|
||||
Map<String, Object> json() {
|
||||
@@ -172,8 +189,16 @@ public final class CrossImport {
|
||||
int pages = ref.pages().size(),
|
||||
tex = ref.textures().size(),
|
||||
anims = ref.textureAnims().size();
|
||||
apply(o, ref, false, -1, -1, -1);
|
||||
apply(o, cur, true, pages, tex, anims);
|
||||
Runnable merge = () -> {
|
||||
apply(o, ref, false, -1, -1, -1);
|
||||
apply(o, cur, true, pages, tex, anims);
|
||||
};
|
||||
CodeHook h = codeHook;
|
||||
if (h != null) {
|
||||
h.around(p, merge);
|
||||
} else {
|
||||
merge.run();
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> l = p.extra().get(KEY) instanceof List<?> x ? (List<Object>) x : new ArrayList<>();
|
||||
List<Object> nl = new ArrayList<>(l);
|
||||
@@ -326,12 +351,16 @@ public final class CrossImport {
|
||||
if (c == null) {
|
||||
throw new IllegalStateException("the level has no STPC chunk");
|
||||
}
|
||||
return analysis(s, c.data());
|
||||
}
|
||||
|
||||
private static StpcMerge.Analysis analysis(Scene s, byte[] stpc) {
|
||||
Set<Integer> roots = new TreeSet<>();
|
||||
for (Strat x : s.strats()) {
|
||||
roots.add(x.code());
|
||||
}
|
||||
roots.addAll(s.catalog().names().keySet());
|
||||
StpcImage img = new StpcImage(c.data(), s.settings().wfpc());
|
||||
StpcImage img = new StpcImage(stpc, s.settings().wfpc());
|
||||
roots.removeIf(o -> o < img.codeStart || img.unitOf(o) < 0);
|
||||
return new StpcMerge.Analysis(img, roots);
|
||||
}
|
||||
@@ -361,7 +390,9 @@ public final class CrossImport {
|
||||
|
||||
/** Merges a batch into (a copy of) the scene's level data; the scene is not changed. */
|
||||
public static Outcome compute(Scene s, List<TypeRef> batch, Bases cd) throws IOException {
|
||||
StpcMerge.Analysis tgt = analysis(s);
|
||||
CodeHook h = codeHook;
|
||||
byte[] base = h == null ? null : h.mergeBase(s);
|
||||
StpcMerge.Analysis tgt = base == null ? analysis(s) : analysis(s, base);
|
||||
TextMerge.Textures tex = textures(s);
|
||||
int p0 = tex.pages.size(), e0 = tex.entries.size(), a0 = tex.anims.size();
|
||||
RawChunk sm = chunk(s, "SMPC");
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.model.SceneEvent;
|
||||
import com.eliasmueller.llamamod.editor.model.Strat;
|
||||
import com.eliasmueller.llamamod.editor.model.StratCatalog;
|
||||
import com.eliasmueller.llamamod.editor.tools.catalog.CrossImport;
|
||||
import com.eliasmueller.llamamod.editor.tools.objects.ParamSchema;
|
||||
import com.eliasmueller.llamamod.editor.tools.objects.Schemas;
|
||||
import com.eliasmueller.llamamod.format.Bytes;
|
||||
@@ -64,14 +65,32 @@ public final class StratEdits {
|
||||
|
||||
/**
|
||||
* A custom type: name, source text, the entry label in the source, type fields (MAP record
|
||||
* prototype), and where it came from.
|
||||
* prototype), where it came from (basedOn, the cloned type) and the type its fields and
|
||||
* parameter names are taken from (fieldsFrom).
|
||||
*/
|
||||
public record CustomType(
|
||||
String name, String basedOn, String entryLabel, String source, StratCatalog.Variant proto) {
|
||||
String name,
|
||||
String basedOn,
|
||||
String entryLabel,
|
||||
String source,
|
||||
StratCatalog.Variant proto,
|
||||
String fieldsFrom) {
|
||||
/** A type whose fields come from the type it is based on. */
|
||||
public CustomType(String name, String basedOn, String entryLabel, String source, StratCatalog.Variant proto) {
|
||||
this(name, basedOn, entryLabel, source, proto, basedOn);
|
||||
}
|
||||
|
||||
public CustomType withSource(String src) {
|
||||
return new CustomType(name, basedOn, entryLabel, src, proto, fieldsFrom);
|
||||
}
|
||||
|
||||
Map<String, Object> json() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("name", name);
|
||||
m.put("basedOn", basedOn == null ? "" : basedOn);
|
||||
if (fieldsFrom != null && !fieldsFrom.isEmpty() && !fieldsFrom.equals(basedOn)) {
|
||||
m.put("fieldsFrom", fieldsFrom);
|
||||
}
|
||||
m.put("entry", entryLabel);
|
||||
m.put("source", source);
|
||||
Map<String, Object> p = new LinkedHashMap<>();
|
||||
@@ -129,12 +148,14 @@ public final class StratEdits {
|
||||
params,
|
||||
List.copyOf(chain),
|
||||
"custom");
|
||||
String based = (String) m.get("basedOn");
|
||||
return new CustomType(
|
||||
(String) m.get("name"),
|
||||
(String) m.get("basedOn"),
|
||||
based,
|
||||
(String) m.get("entry"),
|
||||
(String) m.get("source"),
|
||||
v);
|
||||
v,
|
||||
m.get("fieldsFrom") instanceof String ff ? ff : based);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +202,8 @@ public final class StratEdits {
|
||||
boolean edited;
|
||||
int firstCustom = -1;
|
||||
final List<Integer> customEntries = new ArrayList<>();
|
||||
final List<String> customNames = new ArrayList<>();
|
||||
final List<String> customFields = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static final Map<Scene, State> STATE = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
@@ -217,6 +240,20 @@ public final class StratEdits {
|
||||
/** Makes sure the load hooks are registered. */
|
||||
public static void init() {}
|
||||
|
||||
static {
|
||||
CrossImport.setCodeHook(new CrossImport.CodeHook() {
|
||||
@Override
|
||||
public byte[] mergeBase(Scene s) {
|
||||
return importBase(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void around(Project p, Runnable commit) {
|
||||
aroundImport(p, commit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int num(Map<?, ?> m, String k) {
|
||||
return ((Number) m.get(k)).intValue();
|
||||
}
|
||||
@@ -247,7 +284,24 @@ public final class StratEdits {
|
||||
return;
|
||||
}
|
||||
int startBefore = codeEnd(cur);
|
||||
for (CustomType t : r.types) {
|
||||
Map<?, ?> ses = json.get(KEY) instanceof Map<?, ?> m && m.get("session") instanceof Map<?, ?> x ? x : null;
|
||||
// unit:N operands of older projects that point at custom types → unit:Name
|
||||
int savedFirst = ses != null && ses.get("firstUnit") instanceof Number n ? n.intValue() : units(cur);
|
||||
List<String> names = r.types.stream().map(CustomType::name).toList();
|
||||
List<Object> stored = json.get(KEY) instanceof Map<?, ?> m && m.get("types") instanceof List<?> l
|
||||
? List.copyOf(l)
|
||||
: List.of();
|
||||
for (int i = 0; i < r.types.size(); i++) {
|
||||
CustomType t = r.types.get(i);
|
||||
String src = symbolic(t.source, savedFirst, names.subList(0, i));
|
||||
if (!src.equals(t.source)) {
|
||||
t = t.withSource(src);
|
||||
if (i < stored.size() && stored.get(i) instanceof Map<?, ?> sm) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> w = (Map<String, Object>) sm;
|
||||
w.put("source", src);
|
||||
}
|
||||
}
|
||||
try {
|
||||
appendType(ref, t, false);
|
||||
appendType(cur, t, false);
|
||||
@@ -255,7 +309,7 @@ public final class StratEdits {
|
||||
warnings.add("custom strat type " + t.name + " could not be assembled: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (json.get(KEY) instanceof Map<?, ?> m && m.get("session") instanceof Map<?, ?> ses) {
|
||||
if (ses != null) {
|
||||
int cs = num(ses, "start"), ce = num(ses, "end");
|
||||
int b = startBefore - cs;
|
||||
if (b != 0) {
|
||||
@@ -359,6 +413,10 @@ public final class StratEdits {
|
||||
if (n == null) {
|
||||
n = com.eliasmueller.llamamod.format.Strats.name(a.img.data, code, a.img.codeStart);
|
||||
}
|
||||
int k = customIndex(s, a.img.unitOf(code));
|
||||
if (k >= 0 && state(s).customFields.get(k) != null) {
|
||||
n = state(s).customFields.get(k); // parameter names of the type the fields come from
|
||||
}
|
||||
ParamSchema.TypeSchema ts = Schemas.of(n);
|
||||
ParamSchema.ParamDef d = ts == null ? null : ts.param(i);
|
||||
return d == null ? null : ident(d.name());
|
||||
@@ -368,9 +426,30 @@ public final class StratEdits {
|
||||
public String entry(int code) {
|
||||
return cat.nameOf(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String unitName(int u) {
|
||||
int k = customIndex(s, u);
|
||||
return k >= 0 ? state(s).customNames.get(k) : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Script names of a type's parameters from its object schema: name → "vN" (empty if unknown). */
|
||||
public static Map<String, String> paramNames(String type) {
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
ParamSchema.TypeSchema ts = type == null ? null : Schemas.of(type);
|
||||
if (ts != null) {
|
||||
for (ParamSchema.ParamDef d : ts.params()) {
|
||||
String n = ident(d.name());
|
||||
if (n != null && !m.containsKey(n)) {
|
||||
m.put(n, "v" + d.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static String ident(String label) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean up = false;
|
||||
@@ -519,14 +598,28 @@ public final class StratEdits {
|
||||
/** Assembles a type's source as a new unit appended to the scene's code and verifies it. */
|
||||
public static Built build(Scene s, CustomType t) {
|
||||
Analysis a = analysis(s);
|
||||
return build(a.img.data, s.settings().wfpc(), a.entries, t);
|
||||
return build(a.img.data, s.settings().wfpc(), a.entries, t, customUnits(s));
|
||||
}
|
||||
|
||||
/** Assembles a type's source as a new unit appended to the STPC data old and verifies it. */
|
||||
static Built build(byte[] old, int wfpc, Set<Integer> entries, CustomType t) {
|
||||
/** Unit index of each custom type of the scene by name (for unit:Name operands). */
|
||||
static Map<String, Integer> customUnits(Scene s) {
|
||||
State st = state(s);
|
||||
Map<String, Integer> m = new LinkedHashMap<>();
|
||||
for (int i = 0; i < st.customNames.size(); i++) {
|
||||
m.put(st.customNames.get(i), st.firstCustom + i);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles a type's source as a new unit appended to the STPC data old and verifies it;
|
||||
* names = unit index of the custom types before it (unit:Name).
|
||||
*/
|
||||
static Built build(byte[] old, int wfpc, Set<Integer> entries, CustomType t, Map<String, Integer> names) {
|
||||
StpcImage img0 = new StpcImage(old, wfpc);
|
||||
int base = old.length + 4;
|
||||
StratAssembler.Result r = StratAssembler.assemble(t.source, StratListing.layout(img0, base));
|
||||
StratAssembler.Result r = StratAssembler.assemble(
|
||||
t.source, StratListing.layout(img0, base).withUnitNames(names));
|
||||
Integer e = r.labels().get(t.entryLabel);
|
||||
if (e == null) {
|
||||
throw new IllegalArgumentException("entry label " + t.entryLabel + " not found in the source");
|
||||
@@ -628,11 +721,14 @@ public final class StratEdits {
|
||||
st.firstCustom = unit;
|
||||
}
|
||||
st.customEntries.add(b.entry);
|
||||
st.customNames.add(t.name);
|
||||
st.customFields.add(t.fieldsFrom);
|
||||
return b.entry;
|
||||
}
|
||||
|
||||
/** Adds a custom type to an open project (reference + scene) and records it. Returns the entry code. */
|
||||
public static int addType(Project p, CustomType t) {
|
||||
t = t.withSource(symbolic(p.scene(), t.source));
|
||||
appendType(p.reference(), t, false);
|
||||
int code = appendType(p.scene(), t, true);
|
||||
Records r = Records.of(p.extra());
|
||||
@@ -642,6 +738,36 @@ public final class StratEdits {
|
||||
return code;
|
||||
}
|
||||
|
||||
private static final java.util.regex.Pattern UNIT_REF = java.util.regex.Pattern.compile("\\bunit:(\\d+)\\b");
|
||||
|
||||
/** Source with unit:N operands that point at custom types written as unit:TypeName (survives reordering). */
|
||||
public static String symbolic(Scene s, String src) {
|
||||
State st = state(s);
|
||||
return symbolic(src, st.firstCustom, st.customNames);
|
||||
}
|
||||
|
||||
static String symbolic(String src, int first, List<String> names) {
|
||||
if (first < 0 || names.isEmpty()) {
|
||||
return src;
|
||||
}
|
||||
java.util.regex.Matcher m = UNIT_REF.matcher(src);
|
||||
StringBuilder b = new StringBuilder();
|
||||
while (m.find()) {
|
||||
int k = Integer.parseInt(m.group(1)) - first;
|
||||
String rep = k >= 0 && k < names.size() ? "unit:" + names.get(k) : m.group();
|
||||
m.appendReplacement(b, java.util.regex.Matcher.quoteReplacement(rep));
|
||||
}
|
||||
m.appendTail(b);
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
/** Source with unit:Old operands renamed to unit:New. */
|
||||
static String renameUnitRefs(String src, String oldName, String newName) {
|
||||
return src.replaceAll(
|
||||
"\\bunit:" + java.util.regex.Pattern.quote(oldName) + "(?![\\w])",
|
||||
java.util.regex.Matcher.quoteReplacement("unit:" + newName));
|
||||
}
|
||||
|
||||
/** Why the custom types of a scene cannot be rebuilt in place (null = they can). */
|
||||
public static String rebuildBlocker(Scene s) {
|
||||
State st = state(s);
|
||||
@@ -678,9 +804,19 @@ public final class StratEdits {
|
||||
Set<Integer> entries = new TreeSet<>(analysis(s).entries);
|
||||
entries.removeIf(o -> o >= d.length);
|
||||
byte[] cur = d;
|
||||
int unit = new StpcImage(d, s.settings().wfpc()).units.size();
|
||||
Map<String, Integer> names = new LinkedHashMap<>();
|
||||
for (int i = 0; i < types.size(); i++) {
|
||||
CustomType t = types.get(i);
|
||||
Built x = build(cur, s.settings().wfpc(), entries, t);
|
||||
Built x;
|
||||
try {
|
||||
x = build(cur, s.settings().wfpc(), entries, t, names);
|
||||
} catch (StratAssembler.AsmException e) {
|
||||
if (i == index) {
|
||||
throw e;
|
||||
}
|
||||
return List.of(t.name + ": " + e.getMessage());
|
||||
}
|
||||
if (!x.problems.isEmpty()) {
|
||||
return x.problems.stream().map(m -> t.name + ": " + m).toList();
|
||||
}
|
||||
@@ -691,6 +827,7 @@ public final class StratEdits {
|
||||
for (int v : x.asm.strats().values()) {
|
||||
entries.add(x.base + v);
|
||||
}
|
||||
names.put(t.name, unit + i);
|
||||
cur = x.stpc;
|
||||
}
|
||||
return null;
|
||||
@@ -709,7 +846,7 @@ public final class StratEdits {
|
||||
}
|
||||
}
|
||||
for (Scene s : List.of(p.reference(), p.scene())) {
|
||||
Runnable r = () -> rebuild(s, types, remap);
|
||||
Runnable r = () -> append(s, types, remap, cut(s));
|
||||
if (s == p.scene()) {
|
||||
s.bulk(r);
|
||||
} else {
|
||||
@@ -720,20 +857,24 @@ public final class StratEdits {
|
||||
new Records(rec.constants, List.copyOf(types)).store(p.extra(), session(p.scene()));
|
||||
}
|
||||
|
||||
private static void rebuild(Scene s, List<CustomType> types, int[] remap) {
|
||||
/** Objects of the custom types while their code is cut off: custom type index and old code; old entries. */
|
||||
private record Detached(Map<Strat, int[]> objects, List<Integer> entries) {}
|
||||
|
||||
/** Cuts the custom units off a scene's code and removes them from the catalog (objects keep code 0). */
|
||||
private static Detached cut(Scene s) {
|
||||
State st = state(s);
|
||||
List<Integer> oldEntries = new ArrayList<>(st.customEntries);
|
||||
Analysis a = analysis(s);
|
||||
int from = st.firstCustom < 0
|
||||
? a.img.data.length
|
||||
: a.img.units.get(st.firstCustom).start() - 4;
|
||||
int to = a.img.data.length;
|
||||
List<Integer> oldUnitOf = new ArrayList<>(), oldCode = new ArrayList<>();
|
||||
int to = st.firstCustom < 0
|
||||
? a.img.data.length
|
||||
: a.img.units.get(st.firstCustom + st.customEntries.size() - 1).end();
|
||||
Map<Strat, int[]> objects = new java.util.IdentityHashMap<>();
|
||||
for (Strat x : s.strats()) {
|
||||
boolean custom = x.code() >= from && x.code() < to;
|
||||
oldUnitOf.add(custom ? a.img.unitOf(x.code()) - st.firstCustom : -1);
|
||||
oldCode.add(x.code());
|
||||
if (custom) {
|
||||
if (x.code() >= from && x.code() < to) {
|
||||
objects.put(x, new int[] {a.img.unitOf(x.code()) - st.firstCustom, x.code()});
|
||||
s.set(x, "code", 0); // keep stale codes out of the analysis roots while appending
|
||||
}
|
||||
}
|
||||
@@ -751,28 +892,61 @@ public final class StratEdits {
|
||||
chunk(s).setRaw("data", cut);
|
||||
st.firstCustom = -1;
|
||||
st.customEntries.clear();
|
||||
st.customNames.clear();
|
||||
st.customFields.clear();
|
||||
return new Detached(objects, oldEntries);
|
||||
}
|
||||
|
||||
/** Appends types to a scene whose custom units were cut off; objects of old type k follow type remap[k]. */
|
||||
private static void append(Scene s, List<CustomType> types, int[] remap, Detached old) {
|
||||
State st = state(s);
|
||||
for (CustomType t : types) {
|
||||
appendType(s, t, false);
|
||||
}
|
||||
List<Strat> strats = List.copyOf(s.strats());
|
||||
for (int i = 0; i < strats.size(); i++) {
|
||||
int k = oldUnitOf.get(i);
|
||||
if (k < 0) {
|
||||
for (Strat x : s.strats()) {
|
||||
int[] o = old.objects.get(x);
|
||||
if (o == null) {
|
||||
continue;
|
||||
}
|
||||
Strat x = strats.get(i);
|
||||
int nk = k < remap.length ? remap[k] : -1;
|
||||
int k = o[0];
|
||||
int nk = k >= 0 && k < remap.length ? remap[k] : -1;
|
||||
if (nk < 0 || nk >= st.customEntries.size()) {
|
||||
throw new IllegalStateException(x.id() + " still uses a removed custom type");
|
||||
}
|
||||
int oldEntry = oldEntries.get(k);
|
||||
int oldEntry = old.entries.get(k);
|
||||
// objects placed at the entry follow the entry; other codes keep their offset in the unit
|
||||
int c0 = oldCode.get(i);
|
||||
int c0 = o[1];
|
||||
int code = c0 == oldEntry ? st.customEntries.get(nk) : c0 - oldEntry + st.customEntries.get(nk);
|
||||
s.set(x, "code", code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an import of object types (CrossImport) with the custom units cut off first and
|
||||
* appended again afterwards, so that they stay the last units (as on load) and can still be
|
||||
* edited and deleted.
|
||||
*/
|
||||
static void aroundImport(Project p, Runnable commit) {
|
||||
List<CustomType> types = records(p).types;
|
||||
if (state(p.scene()).firstCustom < 0 || types.isEmpty() || rebuildBlocker(p.scene()) != null) {
|
||||
commit.run();
|
||||
return;
|
||||
}
|
||||
Detached r = cut(p.reference());
|
||||
Detached[] sc = new Detached[1];
|
||||
p.scene().bulk(() -> sc[0] = cut(p.scene()));
|
||||
commit.run();
|
||||
int[] id = TypeEdits.identity(types.size());
|
||||
append(p.reference(), types, id, r);
|
||||
p.scene().bulk(() -> append(p.scene(), types, id, sc[0]));
|
||||
updateSession(p);
|
||||
}
|
||||
|
||||
/** STPC data an import merges into: the code without the custom units (null = the scene's own). */
|
||||
static byte[] importBase(Scene s) {
|
||||
return state(s).firstCustom < 0 || rebuildBlocker(s) != null ? null : withoutCustom(s);
|
||||
}
|
||||
|
||||
/** Objects of the scene whose code lies in custom type index k's unit. */
|
||||
public static List<Strat> users(Scene s, int k) {
|
||||
State st = state(s);
|
||||
@@ -802,6 +976,7 @@ public final class StratEdits {
|
||||
m.put("start", (long) (a.img.units.get(st.firstCustom).start() - 4));
|
||||
m.put("end", (long)
|
||||
a.img.units.get(Math.min(last, a.img.units.size() - 1)).end());
|
||||
m.put("firstUnit", (long) st.firstCustom);
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ final class TypeDialog {
|
||||
open(ctx, "New object type from script", freeName(s, "MyStrat"), TEMPLATE, base, "MyStrat", false, -1);
|
||||
}
|
||||
|
||||
/** Edits custom type k in place: its name, entry label and source (type fields are kept). */
|
||||
/** Edits custom type k in place: its name, entry label, source and (optionally) where its type fields come from. */
|
||||
static void showEdit(EditorContext ctx, int k) {
|
||||
if (ctx.document() == null) {
|
||||
return;
|
||||
@@ -105,7 +105,7 @@ final class TypeDialog {
|
||||
"Edit object type " + t.name(),
|
||||
t.name(),
|
||||
t.source(),
|
||||
t.basedOn(),
|
||||
t.fieldsFrom(),
|
||||
t.entryLabel(),
|
||||
t.basedOn() != null && !t.basedOn().isEmpty(),
|
||||
k);
|
||||
@@ -170,6 +170,13 @@ final class TypeDialog {
|
||||
return n;
|
||||
}
|
||||
|
||||
static final String KEEP = "(keep current fields)";
|
||||
|
||||
/** Parameter names (from the object schema) the script may use for the chosen fields type. */
|
||||
private static java.util.Map<String, String> names(String fieldsType) {
|
||||
return StratEdits.paramNames(fieldsType);
|
||||
}
|
||||
|
||||
private static void open(
|
||||
EditorContext ctx,
|
||||
String title,
|
||||
@@ -183,14 +190,19 @@ final class TypeDialog {
|
||||
JTextField nameField = new JTextField(name, 18);
|
||||
JTextField entryField = new JTextField(entry, 14);
|
||||
JComboBox<String> from = new JComboBox<>();
|
||||
ctx.scene().catalog().types().forEach(t -> from.addItem(t.name()));
|
||||
from.setSelectedItem(fieldsFrom);
|
||||
boolean editing = editIndex >= 0;
|
||||
String oldName = editing ? name : null;
|
||||
if (editing) {
|
||||
from.setEnabled(false);
|
||||
from.setToolTipText("An edited type keeps its type fields and parameters");
|
||||
from.addItem(KEEP);
|
||||
from.setToolTipText("New objects of the type start with these type fields and parameters;"
|
||||
+ " objects already placed keep theirs");
|
||||
}
|
||||
ctx.scene().catalog().types().forEach(t -> {
|
||||
if (!t.name().equals(oldName)) {
|
||||
from.addItem(t.name());
|
||||
}
|
||||
});
|
||||
from.setSelectedItem(editing ? KEEP : fieldsFrom);
|
||||
String script = StratCompiler.embeddedSource(source);
|
||||
JTextArea text = new JTextArea(script != null ? script : source, 30, 90);
|
||||
JComboBox<String> lang = new JComboBox<>(new String[] {"Assembly", "Script"});
|
||||
@@ -206,8 +218,9 @@ final class TypeDialog {
|
||||
top.add(entryField);
|
||||
top.add(new JLabel("Language"));
|
||||
top.add(lang);
|
||||
top.add(new JLabel("Type fields / parameters from"));
|
||||
top.add(from);
|
||||
JPanel fieldsRow = new JPanel(new FlowLayout(FlowLayout.LEADING));
|
||||
fieldsRow.add(new JLabel("Type fields / parameters from"));
|
||||
fieldsRow.add(from);
|
||||
JButton check = new JButton("Check");
|
||||
JButton create = new JButton(editing ? "Apply" : "Create type");
|
||||
JButton cancel = new JButton("Cancel");
|
||||
@@ -228,7 +241,10 @@ final class TypeDialog {
|
||||
: "")
|
||||
+ "Syntax: notes/strat-language.md (manual chapter “Strat scripts”). The new code is appended to the level and verified by disassembling it again.</font></html>"));
|
||||
JPanel north = new JPanel(new BorderLayout());
|
||||
north.add(top, BorderLayout.NORTH);
|
||||
JPanel rows = new JPanel(new java.awt.GridLayout(2, 1));
|
||||
rows.add(top);
|
||||
rows.add(fieldsRow);
|
||||
north.add(rows, BorderLayout.NORTH);
|
||||
north.add(help, BorderLayout.SOUTH);
|
||||
d.getContentPane().add(north, BorderLayout.NORTH);
|
||||
d.getContentPane().add(new JScrollPane(text), BorderLayout.CENTER);
|
||||
@@ -243,7 +259,8 @@ final class TypeDialog {
|
||||
String cur = text.getText();
|
||||
if (to == 0) {
|
||||
try {
|
||||
text.setText(StratCompiler.compile(cur));
|
||||
String ft = (String) from.getSelectedItem();
|
||||
text.setText(StratCompiler.compile(cur, names(KEEP.equals(ft) ? fieldsFrom : ft)));
|
||||
} catch (StratCompiler.CompileException ex) {
|
||||
result.setText("⚠ " + ex.getMessage() + " – fix the script before switching to assembly");
|
||||
highlight(text, ex.line);
|
||||
@@ -281,9 +298,11 @@ final class TypeDialog {
|
||||
return;
|
||||
}
|
||||
String src = text.getText();
|
||||
String chosen = (String) from.getSelectedItem();
|
||||
String fieldsType = KEEP.equals(chosen) ? fieldsFrom : chosen;
|
||||
if (lang.getSelectedIndex() == 1) {
|
||||
try {
|
||||
src = StratCompiler.compile(src);
|
||||
src = StratCompiler.compile(src, names(fieldsType));
|
||||
} catch (StratCompiler.CompileException e) {
|
||||
result.setText("⚠ " + e.getMessage());
|
||||
highlight(text, e.line);
|
||||
@@ -294,12 +313,18 @@ final class TypeDialog {
|
||||
entryField.setText(first);
|
||||
}
|
||||
}
|
||||
StratCatalog.Type ft = s.catalog().type((String) from.getSelectedItem());
|
||||
StratCatalog.Type ft = s.catalog().type(chosen);
|
||||
if (editing) {
|
||||
StratEdits.CustomType old =
|
||||
StratEdits.records(ctx.document().project()).types().get(editIndex);
|
||||
boolean keep = KEEP.equals(chosen) || ft == null;
|
||||
StratEdits.CustomType t = new StratEdits.CustomType(
|
||||
n, old.basedOn(), entryField.getText().trim(), src, old.proto());
|
||||
n,
|
||||
old.basedOn(),
|
||||
entryField.getText().trim(),
|
||||
src,
|
||||
keep ? old.proto() : ft.variant(0),
|
||||
keep ? old.fieldsFrom() : chosen);
|
||||
List<StratEdits.CustomType> all = new java.util.ArrayList<>(
|
||||
StratEdits.records(ctx.document().project()).types());
|
||||
all.set(editIndex, t);
|
||||
@@ -330,7 +355,7 @@ final class TypeDialog {
|
||||
return;
|
||||
}
|
||||
StratEdits.CustomType t = new StratEdits.CustomType(
|
||||
n, clone ? fieldsFrom : null, entryField.getText().trim(), src, ft.variant(0));
|
||||
n, clone ? fieldsFrom : null, entryField.getText().trim(), src, ft.variant(0), chosen);
|
||||
try {
|
||||
StratEdits.Built b = StratEdits.build(s, t);
|
||||
if (!b.problems().isEmpty()) {
|
||||
|
||||
@@ -44,16 +44,26 @@ final class TypeEdits {
|
||||
static Command create(Project p, StratEdits.CustomType t) {
|
||||
List<StratEdits.CustomType> before = StratEdits.records(p).types();
|
||||
List<StratEdits.CustomType> after = new ArrayList<>(before);
|
||||
after.add(t);
|
||||
after.add(t.withSource(StratEdits.symbolic(p.scene(), t.source())));
|
||||
return new SetTypes(
|
||||
p, "Create object type " + t.name(), before, after, identity(before.size()), identity(before.size()));
|
||||
}
|
||||
|
||||
/** Replaces custom type k by t (objects of the type stay and use the new code). */
|
||||
/**
|
||||
* Replaces custom type k by t (objects of the type stay and use the new code); a new name is
|
||||
* also changed in the unit:Name references of the other types.
|
||||
*/
|
||||
static Command edit(Project p, int k, StratEdits.CustomType t) {
|
||||
List<StratEdits.CustomType> before = StratEdits.records(p).types();
|
||||
List<StratEdits.CustomType> after = new ArrayList<>(before);
|
||||
after.set(k, t);
|
||||
after.set(k, t.withSource(StratEdits.symbolic(p.scene(), t.source())));
|
||||
String oldName = before.get(k).name();
|
||||
if (!oldName.equals(t.name())) {
|
||||
for (int i = 0; i < after.size(); i++) {
|
||||
StratEdits.CustomType x = after.get(i);
|
||||
after.set(i, x.withSource(StratEdits.renameUnitRefs(x.source(), oldName, t.name())));
|
||||
}
|
||||
}
|
||||
return new SetTypes(
|
||||
p, "Edit object type " + t.name(), before, after, identity(before.size()), identity(before.size()));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ public final class OpTable {
|
||||
|
||||
private final Op[] ops = new Op[StratOps.COUNT];
|
||||
private final Map<String, Op> byName = new HashMap<>();
|
||||
/** Former mnemonics (lower case) that still assemble (resource op-aliases.tsv). */
|
||||
private final Map<String, Op> aliases = new HashMap<>();
|
||||
|
||||
private final Map<Integer, Named> props = new HashMap<>();
|
||||
private final Map<String, Named> propsByName = new HashMap<>();
|
||||
private final Map<Integer, Named> globals = new HashMap<>();
|
||||
@@ -64,6 +67,14 @@ public final class OpTable {
|
||||
byName.put(name, ops[i]);
|
||||
}
|
||||
}
|
||||
for (String[] f : rows("op-aliases.tsv")) {
|
||||
Op op = f.length < 2 ? null : byName.get(lc(f[1].trim()));
|
||||
String old = lc(f[0].trim());
|
||||
if (op != null && !old.isEmpty() && !byName.containsKey(old)) {
|
||||
byName.put(old, op);
|
||||
aliases.put(old, op);
|
||||
}
|
||||
}
|
||||
named("props.tsv", props, propsByName, true);
|
||||
named("globals.tsv", globals, globalsByName, false);
|
||||
}
|
||||
@@ -128,7 +139,12 @@ public final class OpTable {
|
||||
return ops[code];
|
||||
}
|
||||
|
||||
/** Opcode by mnemonic (case-insensitive) or "op_XX", or null. */
|
||||
/** The opcode a former mnemonic now names, or null if name is not an old alias. */
|
||||
public Op alias(String name) {
|
||||
return aliases.get(lc(name));
|
||||
}
|
||||
|
||||
/** Opcode by mnemonic (case-insensitive, current or former name) or "op_XX", or null. */
|
||||
public Op byName(String name) {
|
||||
Op o = byName.get(lc(name));
|
||||
if (o == null && lc(name).startsWith("op_")) {
|
||||
|
||||
@@ -16,9 +16,19 @@ public final class StratAssembler {
|
||||
* unit's first word; models/anims/units = STPC offsets of model headers, animation sets, unit
|
||||
* starts.
|
||||
*/
|
||||
public record Layout(int base, List<Integer> models, List<Integer> anims, List<Integer> units) {
|
||||
public record Layout(
|
||||
int base, List<Integer> models, List<Integer> anims, List<Integer> units, Map<String, Integer> unitNames) {
|
||||
public Layout(int base, List<Integer> models, List<Integer> anims, List<Integer> units) {
|
||||
this(base, models, anims, units, Map.of());
|
||||
}
|
||||
|
||||
public Layout withBase(int b) {
|
||||
return new Layout(b, models, anims, units);
|
||||
return new Layout(b, models, anims, units, unitNames);
|
||||
}
|
||||
|
||||
/** The same layout where unit:Name resolves to the unit with that index (type names). */
|
||||
public Layout withUnitNames(Map<String, Integer> names) {
|
||||
return new Layout(base, models, anims, units, Map.copyOf(names));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +672,17 @@ public final class StratAssembler {
|
||||
add = d == null ? 0 : d;
|
||||
r = r.substring(0, plus);
|
||||
}
|
||||
Integer idx = number(ln, r);
|
||||
String key = r.trim();
|
||||
Integer idx = kind.equals("unit") && key.matches("[A-Za-z_]\\w*")
|
||||
? l.unitNames().get(key)
|
||||
: null;
|
||||
if (idx == null && kind.equals("unit") && key.matches("[A-Za-z_]\\w*")) {
|
||||
err(ln, "unknown unit name '" + key + "' (custom object types are named, others use unit:N)");
|
||||
return null;
|
||||
}
|
||||
if (idx == null) {
|
||||
idx = number(ln, r);
|
||||
}
|
||||
if (idx == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import java.util.Map;
|
||||
/**
|
||||
* Compiler for strat scripts, a small structured language for NEW behaviour (notes/strat-language.md,
|
||||
* "Script language"): statements and expressions in the style of the script view, compiled to the
|
||||
* assembly of {@link StratAssembler}. Literals mean the same as in the assembly (10.0 = ten, 10 =
|
||||
* ten raw 1/4096 units, 90deg).
|
||||
* assembly of {@link StratAssembler}. Numbers: a plain integer is in game units ({@code 10} = 10.0),
|
||||
* {@code 10r} and hex are raw words, {@code 1.5} and {@code 90deg} as in the assembly. Scripts
|
||||
* compiled before this rule ({@code use rawNumbers}) read plain integers as raw words.
|
||||
*/
|
||||
public final class StratCompiler {
|
||||
/** A compile error at a 1-based source line. */
|
||||
@@ -27,8 +28,24 @@ public final class StratCompiler {
|
||||
/** Prefix of the comment lines that carry the script source inside the generated assembly. */
|
||||
public static final String SOURCE_PREFIX = ";| ";
|
||||
|
||||
/** Marker line behind the embedded source of scripts with unit numbers (language version 2). */
|
||||
static final String VERSION_MARK = "; script language 2";
|
||||
|
||||
/** Statement that keeps the old number rule (plain integers are raw words). */
|
||||
public static final String RAW_NUMBERS = "use rawNumbers";
|
||||
|
||||
private record Tok(String kind, String text, int line) {}
|
||||
|
||||
/**
|
||||
* Expression tree. num: value (raw word) and text (assembly literal); ins: one instruction that
|
||||
* pushes a value; bin/neg/not: operator over kids; call: instruction text after the kids.
|
||||
*/
|
||||
private record Node(String kind, String text, int value, List<Node> kids, int line) {
|
||||
boolean isNum() {
|
||||
return kind.equals("num");
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<String, Integer> PREC = Map.ofEntries(
|
||||
Map.entry("||", 1),
|
||||
Map.entry("&&", 2),
|
||||
@@ -78,43 +95,78 @@ public final class StratCompiler {
|
||||
|
||||
private static final List<String> REFS = List.of("model", "anim", "unit", "sound", "tex", "text");
|
||||
|
||||
/** Named event types for {@code on name { … }}. */
|
||||
private static final Map<String, Integer> EVENTS = Map.of("touched", 4);
|
||||
|
||||
private final OpTable ops = OpTable.get();
|
||||
private final List<Tok> toks;
|
||||
private int p;
|
||||
private final StringBuilder out = new StringBuilder();
|
||||
private StringBuilder out = new StringBuilder();
|
||||
/** Event handler code of the current strat/sub, placed behind its code. */
|
||||
private StringBuilder deferred = new StringBuilder();
|
||||
/** Event entries and switch tables, placed at the start of the unit. */
|
||||
private final StringBuilder tables = new StringBuilder();
|
||||
|
||||
private final Map<String, String> aliases = new HashMap<>();
|
||||
private int labels;
|
||||
/** Enclosing loops: {break label, continue label}. */
|
||||
private final Deque<String[]> loops = new ArrayDeque<>();
|
||||
private int eventSlot;
|
||||
private boolean rawNumbers;
|
||||
/** Enclosing loops/switches: {break label, continue label (null = none)}. */
|
||||
private Deque<String[]> loops = new ArrayDeque<>();
|
||||
|
||||
private StratCompiler(String src) {
|
||||
private StratCompiler(String src, Map<String, String> names) {
|
||||
toks = lex(src);
|
||||
names.forEach((n, v) -> {
|
||||
if (v.matches("v\\d+") && n.matches("[A-Za-z_]\\w*")) {
|
||||
aliases.put(n, v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Compiles a script to assembly text; the script is kept as {@link #SOURCE_PREFIX} comments. */
|
||||
public static String compile(String src) {
|
||||
StratCompiler c = new StratCompiler(src);
|
||||
for (String l : src.split("\n", -1)) {
|
||||
c.out.append(SOURCE_PREFIX).append(l.stripTrailing()).append('\n');
|
||||
}
|
||||
c.program();
|
||||
return c.out.toString();
|
||||
return compile(src, Map.of());
|
||||
}
|
||||
|
||||
/** The script embedded in compiled assembly, or null if the text is not compiler output. */
|
||||
/**
|
||||
* Compiles a script; names = predefined variable names (parameter names of the object schema →
|
||||
* "vN"), overridden by the script's own aliases.
|
||||
*/
|
||||
public static String compile(String src, Map<String, String> names) {
|
||||
StratCompiler c = new StratCompiler(src, names);
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (String l : src.split("\n", -1)) {
|
||||
b.append(SOURCE_PREFIX).append(l.stripTrailing()).append('\n');
|
||||
}
|
||||
b.append(VERSION_MARK).append('\n');
|
||||
c.program();
|
||||
// tables first: data behind a terminating instruction would be tried as dead code
|
||||
return b.append(c.tables).append(c.out).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The script embedded in compiled assembly, or null if the text is not compiler output. Scripts
|
||||
* compiled with the old number rule get a {@value #RAW_NUMBERS} line, so they mean the same.
|
||||
*/
|
||||
public static String embeddedSource(String asm) {
|
||||
if (!asm.startsWith(SOURCE_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder b = new StringBuilder();
|
||||
boolean v2 = false;
|
||||
for (String l : asm.split("\n", -1)) {
|
||||
if (!l.startsWith(SOURCE_PREFIX.trim())) {
|
||||
v2 = l.equals(VERSION_MARK);
|
||||
break;
|
||||
}
|
||||
b.append(l.length() >= SOURCE_PREFIX.length() ? l.substring(SOURCE_PREFIX.length()) : "")
|
||||
.append('\n');
|
||||
}
|
||||
return b.toString().stripTrailing() + "\n";
|
||||
String src = b.toString().stripTrailing() + "\n";
|
||||
if (!v2 && !src.contains(RAW_NUMBERS)) {
|
||||
src = RAW_NUMBERS + " // written when a plain 10 meant 10 raw units (1/4096)\n" + src;
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
/** Entry label of the first strat of a script (null if none). */
|
||||
@@ -165,6 +217,10 @@ public final class StratCompiler {
|
||||
}
|
||||
if (s.startsWith("deg", j)) {
|
||||
j += 3;
|
||||
} else if (j < s.length()
|
||||
&& s.charAt(j) == 'r'
|
||||
&& (j + 1 >= s.length() || !Character.isLetterOrDigit(s.charAt(j + 1)))) {
|
||||
j++;
|
||||
}
|
||||
}
|
||||
out.add(new Tok("num", s.substring(i, j), line));
|
||||
@@ -215,10 +271,6 @@ public final class StratCompiler {
|
||||
return peek().text.equals(text) && !peek().kind.equals("str");
|
||||
}
|
||||
|
||||
private boolean at1(String text) {
|
||||
return peek(1).text.equals(text) && !peek(1).kind.equals("str");
|
||||
}
|
||||
|
||||
private boolean accept(String text) {
|
||||
if (at(text)) {
|
||||
p++;
|
||||
@@ -270,10 +322,12 @@ public final class StratCompiler {
|
||||
out.append(" .string \"").append(name).append("\"\n");
|
||||
place(name);
|
||||
emit("debugName \"" + name + "\"");
|
||||
eventSlot = 0;
|
||||
boolean ends = block();
|
||||
if (!ends) {
|
||||
emit("end");
|
||||
}
|
||||
flushDeferred();
|
||||
} else if (accept("sub")) {
|
||||
String name = ident();
|
||||
place(name);
|
||||
@@ -283,14 +337,20 @@ public final class StratCompiler {
|
||||
if (!ends) {
|
||||
emit("return");
|
||||
}
|
||||
} else if (at("alias")) {
|
||||
flushDeferred();
|
||||
} else if (at("alias") || at("use")) {
|
||||
statement();
|
||||
} else {
|
||||
throw err("expected 'strat', 'sub' or 'alias' but found " + describe(peek()));
|
||||
throw err("expected 'strat', 'sub', 'alias' or 'use' but found " + describe(peek()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void flushDeferred() {
|
||||
out.append(deferred);
|
||||
deferred = new StringBuilder();
|
||||
}
|
||||
|
||||
/** Parses { statements }; returns whether control never falls out of its end. */
|
||||
private boolean block() {
|
||||
expect("{");
|
||||
@@ -319,7 +379,7 @@ public final class StratCompiler {
|
||||
String top = label(), done = label();
|
||||
place(top);
|
||||
expect("(");
|
||||
cond(done);
|
||||
jumpIfFalse(expr(), done);
|
||||
expect(")");
|
||||
loops.push(new String[] {done, top});
|
||||
loopBody(top);
|
||||
@@ -347,18 +407,35 @@ public final class StratCompiler {
|
||||
expect("while");
|
||||
expect("(");
|
||||
place(check);
|
||||
expr();
|
||||
jumpIfTrue(expr(), top);
|
||||
expect(")");
|
||||
emit("jnz " + top);
|
||||
place(done);
|
||||
return false;
|
||||
}
|
||||
case "switch" -> {
|
||||
next();
|
||||
switchStatement();
|
||||
return false;
|
||||
}
|
||||
case "on" -> {
|
||||
next();
|
||||
onEvent(t);
|
||||
return false;
|
||||
}
|
||||
case "break", "continue" -> {
|
||||
next();
|
||||
if (loops.isEmpty()) {
|
||||
throw new CompileException(t.line, t.text + " outside a loop");
|
||||
boolean brk = t.text.equals("break");
|
||||
String target = null;
|
||||
for (String[] l : loops) {
|
||||
target = brk ? l[0] : l[1];
|
||||
if (target != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
emit("jmp " + loops.peek()[t.text.equals("break") ? 0 : 1]);
|
||||
if (target == null) {
|
||||
throw new CompileException(t.line, t.text + " outside a " + (brk ? "loop or switch" : "loop"));
|
||||
}
|
||||
emit("jmp " + target);
|
||||
return true;
|
||||
}
|
||||
case "yield" -> {
|
||||
@@ -399,6 +476,15 @@ public final class StratCompiler {
|
||||
out.append(" .alias ").append(n).append(" = ").append(v).append('\n');
|
||||
return false;
|
||||
}
|
||||
case "use" -> {
|
||||
next();
|
||||
String what = ident();
|
||||
if (!what.equals("rawNumbers")) {
|
||||
throw new CompileException(t.line, "unknown option " + what + " (use rawNumbers)");
|
||||
}
|
||||
rawNumbers = true;
|
||||
return false;
|
||||
}
|
||||
default -> {
|
||||
// fall through to assignments and calls
|
||||
}
|
||||
@@ -407,12 +493,13 @@ public final class StratCompiler {
|
||||
throw err("expected a statement but found " + describe(t));
|
||||
}
|
||||
if (isOp(t.text) && !isLvalueStart()) {
|
||||
String mn = callExpr();
|
||||
OpTable.Op op = ops.byName(mn);
|
||||
Node call = callExpr();
|
||||
value(call);
|
||||
OpTable.Op op = ops.byName(call.text.split(" ")[0]);
|
||||
if (op.pushes() == 1) {
|
||||
emit("pop");
|
||||
}
|
||||
return mn.equalsIgnoreCase("end") || mn.equalsIgnoreCase("remove");
|
||||
return op.name().equalsIgnoreCase("end") || op.name().equalsIgnoreCase("remove");
|
||||
}
|
||||
assignment();
|
||||
return false;
|
||||
@@ -421,7 +508,7 @@ public final class StratCompiler {
|
||||
private void ifRest() {
|
||||
String other = label();
|
||||
expect("(");
|
||||
cond(other);
|
||||
jumpIfFalse(expr(), other);
|
||||
expect(")");
|
||||
block();
|
||||
if (accept("else")) {
|
||||
@@ -439,29 +526,157 @@ public final class StratCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
/** Condition: jumps to falseTarget when it is 0. */
|
||||
private void cond(String falseTarget) {
|
||||
expr();
|
||||
emit("jz " + falseTarget);
|
||||
}
|
||||
|
||||
/** Loop body; a trailing yield becomes the yielding back edge. */
|
||||
private void loopBody(String top) {
|
||||
expect("{");
|
||||
int mark = -1;
|
||||
boolean yieldEdge = false;
|
||||
while (!at("}")) {
|
||||
if (peek().kind.equals("eof")) {
|
||||
throw err("missing '}'");
|
||||
}
|
||||
mark = at("yield") && peek(1).text.equals("}") ? out.length() : -1;
|
||||
if (mark >= 0) {
|
||||
if (at("yield") && peek(1).text.equals("}")) {
|
||||
next();
|
||||
yieldEdge = true;
|
||||
break;
|
||||
}
|
||||
statement();
|
||||
}
|
||||
expect("}");
|
||||
emit((mark >= 0 ? "jmpYield " : "jmp ") + top);
|
||||
emit((yieldEdge ? "jmpYield " : "jmp ") + top);
|
||||
}
|
||||
|
||||
/**
|
||||
* switch (e) { case c: … default: … }: C rules (cases fall through, break leaves the switch).
|
||||
* Compiles to the VM's switch table (placed behind the strat) and a jump to default or the end.
|
||||
*/
|
||||
private void switchStatement() {
|
||||
Tok t = peek();
|
||||
expect("(");
|
||||
Node v = expr();
|
||||
expect(")");
|
||||
expect("{");
|
||||
String table = "sw_" + (++labels), done = label();
|
||||
value(v);
|
||||
emit("switch " + table);
|
||||
int jmpAt = out.length();
|
||||
emit("jmp " + done);
|
||||
List<String[]> cases = new ArrayList<>();
|
||||
String dflt = null;
|
||||
String cont = loops.isEmpty() ? null : loops.peek()[1];
|
||||
loops.push(new String[] {done, cont});
|
||||
while (!at("}")) {
|
||||
if (peek().kind.equals("eof")) {
|
||||
throw err("missing '}'");
|
||||
}
|
||||
if (accept("case")) {
|
||||
Tok ct = peek();
|
||||
Node c = expr();
|
||||
if (!c.isNum()) {
|
||||
throw new CompileException(ct.line, "a case value must be a constant");
|
||||
}
|
||||
expect(":");
|
||||
String l = label();
|
||||
for (String[] x : cases) {
|
||||
if (Integer.parseInt(x[0]) == c.value) {
|
||||
throw new CompileException(ct.line, "duplicate case " + c.text);
|
||||
}
|
||||
}
|
||||
cases.add(new String[] {Integer.toString(c.value), l});
|
||||
place(l);
|
||||
} else if (accept("default")) {
|
||||
expect(":");
|
||||
if (dflt != null) {
|
||||
throw new CompileException(t.line, "two default labels");
|
||||
}
|
||||
dflt = label();
|
||||
place(dflt);
|
||||
} else {
|
||||
statement();
|
||||
}
|
||||
}
|
||||
expect("}");
|
||||
loops.pop();
|
||||
place(done);
|
||||
if (dflt != null) {
|
||||
out.replace(jmpAt, jmpAt + (" jmp " + done + "\n").length(), " jmp " + dflt + "\n");
|
||||
}
|
||||
if (cases.isEmpty()) {
|
||||
throw new CompileException(t.line, "a switch needs at least one case");
|
||||
}
|
||||
tables.append(table).append(":\n");
|
||||
tables.append(" .switch ").append(cases.size()).append('\n');
|
||||
for (String[] c : cases) {
|
||||
int x = Integer.parseInt(c[0]);
|
||||
boolean small = x >= Short.MIN_VALUE && x <= Short.MAX_VALUE;
|
||||
tables.append(" .case ")
|
||||
.append(small ? "" : "imm ")
|
||||
.append(Num.literal(x))
|
||||
.append(", ")
|
||||
.append(c[1])
|
||||
.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* on touched { … } / on event(type[, param]) [slot n] { … }: installs an event handler here
|
||||
* (onEvent); the handler body is placed behind the strat and ends with return.
|
||||
*/
|
||||
private void onEvent(Tok t) {
|
||||
String type, param = "0";
|
||||
if (accept("event")) {
|
||||
expect("(");
|
||||
type = rawWord();
|
||||
if (accept(",")) {
|
||||
param = rawWord();
|
||||
}
|
||||
expect(")");
|
||||
} else {
|
||||
String n = ident();
|
||||
Integer k = EVENTS.get(n);
|
||||
if (k == null) {
|
||||
throw new CompileException(t.line, "unknown event " + n + " (touched, or event(type, param))");
|
||||
}
|
||||
type = Integer.toString(k);
|
||||
}
|
||||
int slot = eventSlot;
|
||||
if (accept("slot")) {
|
||||
slot = Num.parse(rawWord());
|
||||
}
|
||||
eventSlot = Math.max(eventSlot, slot + 1);
|
||||
String entry = "ev_" + (++labels), handler = "on_" + labels;
|
||||
emit("onEvent " + entry + ", " + slot);
|
||||
// the handler: compiled into its own buffer with its own loop context
|
||||
StringBuilder saved = out;
|
||||
Deque<String[]> savedLoops = loops;
|
||||
out = new StringBuilder();
|
||||
loops = new ArrayDeque<>();
|
||||
place(handler);
|
||||
if (!block()) {
|
||||
emit("return");
|
||||
}
|
||||
StringBuilder body = out;
|
||||
out = saved;
|
||||
loops = savedLoops;
|
||||
tables.append(entry).append(":\n");
|
||||
tables.append(" .event ")
|
||||
.append(type)
|
||||
.append(", ")
|
||||
.append(param)
|
||||
.append(", ")
|
||||
.append(handler)
|
||||
.append('\n');
|
||||
deferred.append(body);
|
||||
}
|
||||
|
||||
/** A raw word operand (event type/param, slot): number as in the assembly. */
|
||||
private String rawWord() {
|
||||
boolean neg = accept("-");
|
||||
Tok t = next();
|
||||
String s = (neg ? "-" : "") + (t.text.endsWith("r") ? t.text.substring(0, t.text.length() - 1) : t.text);
|
||||
if (!t.kind.equals("num") || Num.parse(s) == null) {
|
||||
throw new CompileException(t.line, "expected a number but found " + describe(t));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private boolean isOp(String name) {
|
||||
@@ -505,7 +720,7 @@ public final class StratCompiler {
|
||||
}
|
||||
throw new CompileException(
|
||||
t.line,
|
||||
"unknown name " + a + " (variables are vN or aliases, globals gN, "
|
||||
"unknown name " + a + " (variables are vN, aliases or parameter names, globals gN, "
|
||||
+ "properties self.name, player.name, …)");
|
||||
}
|
||||
|
||||
@@ -515,18 +730,17 @@ public final class StratCompiler {
|
||||
String op = next().text;
|
||||
switch (op) {
|
||||
case "=" -> {
|
||||
expr();
|
||||
value(expr());
|
||||
emit(pl.pop);
|
||||
}
|
||||
case "+=", "-=", "*=", "/=" -> {
|
||||
boolean one = at("1.0") && (peek(1).line != peek().line || at1("}"));
|
||||
if ((op.equals("+=") || op.equals("-=")) && pl.addr != null && one) {
|
||||
next();
|
||||
Node e = expr();
|
||||
if ((op.equals("+=") || op.equals("-=")) && pl.addr != null && e.isNum() && e.value == Num.ONE) {
|
||||
emit(pl.addr);
|
||||
emit(op.equals("+=") ? "inc" : "dec");
|
||||
} else {
|
||||
emit(pl.push);
|
||||
expr();
|
||||
value(e);
|
||||
emit(BINOP.get(op.substring(0, 1)));
|
||||
emit(pl.pop);
|
||||
}
|
||||
@@ -535,45 +749,44 @@ public final class StratCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ expressions
|
||||
// ------------------------------------------------------------------ expressions (parse)
|
||||
|
||||
private void expr() {
|
||||
binary(1);
|
||||
private Node expr() {
|
||||
return binary(1);
|
||||
}
|
||||
|
||||
private void binary(int min) {
|
||||
unary();
|
||||
private Node binary(int min) {
|
||||
Node l = unary();
|
||||
while (peek().kind.equals("op") && PREC.containsKey(peek().text) && PREC.get(peek().text) >= min) {
|
||||
String op = next().text;
|
||||
binary(PREC.get(op) + 1);
|
||||
emit(BINOP.get(op));
|
||||
Tok t = next();
|
||||
Node r = binary(PREC.get(t.text) + 1);
|
||||
l = fold(new Node("bin", t.text, 0, List.of(l, r), t.line));
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
private void unary() {
|
||||
private Node unary() {
|
||||
Tok t = peek();
|
||||
if (accept("-")) {
|
||||
if (peek().kind.equals("num")) {
|
||||
number("-" + next().text);
|
||||
return;
|
||||
}
|
||||
unary();
|
||||
emit("neg");
|
||||
} else if (accept("!")) {
|
||||
unary();
|
||||
emit("not");
|
||||
} else {
|
||||
primary();
|
||||
return fold(new Node("neg", "-", 0, List.of(unary()), t.line));
|
||||
}
|
||||
if (accept("!")) {
|
||||
return fold(new Node("not", "!", 0, List.of(unary()), t.line));
|
||||
}
|
||||
return primary();
|
||||
}
|
||||
|
||||
private void primary() {
|
||||
private Node primary() {
|
||||
Tok t = peek();
|
||||
if (t.kind.equals("num")) {
|
||||
number(next().text);
|
||||
} else if (accept("(")) {
|
||||
expr();
|
||||
return number(next());
|
||||
}
|
||||
if (accept("(")) {
|
||||
Node e = expr();
|
||||
expect(")");
|
||||
} else if (t.kind.equals("id") && REFS.contains(t.text) && peek(1).text.equals(":")) {
|
||||
return e;
|
||||
}
|
||||
if (t.kind.equals("id") && REFS.contains(t.text) && peek(1).text.equals(":")) {
|
||||
next();
|
||||
next();
|
||||
StringBuilder ref = new StringBuilder(t.text + ":" + next().text);
|
||||
@@ -581,34 +794,46 @@ public final class StratCompiler {
|
||||
next();
|
||||
ref.append('+').append(next().text);
|
||||
}
|
||||
switch (t.text) {
|
||||
case "sound" -> emit("soundAddress " + ref);
|
||||
case "tex" -> emit("push " + ref);
|
||||
return switch (t.text) {
|
||||
case "sound" -> new Node("ins", "soundAddress " + ref, 0, List.of(), t.line);
|
||||
case "tex" -> new Node("ins", "push " + ref, 0, List.of(), t.line);
|
||||
case "text" -> throw new CompileException(t.line, "text:N is an inline operand: topSay<text:N>()");
|
||||
default -> emit("address " + ref);
|
||||
}
|
||||
} else if (t.kind.equals("id") && isOp(t.text) && !isLvalueStart()) {
|
||||
String mn = callExpr();
|
||||
if (ops.byName(mn).pushes() != 1) {
|
||||
throw new CompileException(t.line, mn + " gives no value");
|
||||
}
|
||||
} else if (t.kind.equals("id")) {
|
||||
emit(place().push);
|
||||
} else {
|
||||
throw err("expected a value but found " + describe(t));
|
||||
default -> new Node("ins", "address " + ref, 0, List.of(), t.line);
|
||||
};
|
||||
}
|
||||
if (t.kind.equals("id") && isOp(t.text) && !isLvalueStart()) {
|
||||
Node c = callExpr();
|
||||
if (ops.byName(c.text.split(" ")[0]).pushes() != 1) {
|
||||
throw new CompileException(t.line, c.text.split(" ")[0] + " gives no value");
|
||||
}
|
||||
return c;
|
||||
}
|
||||
if (t.kind.equals("id")) {
|
||||
return new Node("ins", place().push, 0, List.of(), t.line);
|
||||
}
|
||||
throw err("expected a value but found " + describe(t));
|
||||
}
|
||||
|
||||
private void number(String text) {
|
||||
/** A number token: plain integers are game units (×4096) unless rawNumbers; "r" = raw. */
|
||||
private Node number(Tok t) {
|
||||
String s = t.text;
|
||||
String text;
|
||||
if (s.endsWith("r")) {
|
||||
text = s.substring(0, s.length() - 1);
|
||||
} else if (rawNumbers || s.startsWith("0x") || s.startsWith("0X") || s.contains(".") || s.endsWith("deg")) {
|
||||
text = s;
|
||||
} else {
|
||||
text = s + ".0";
|
||||
}
|
||||
Integer v = Num.parse(text);
|
||||
if (v == null) {
|
||||
throw err("bad number " + text);
|
||||
throw new CompileException(t.line, "bad number " + s);
|
||||
}
|
||||
emit((v >= Short.MIN_VALUE && v <= Short.MAX_VALUE ? "pushShort " : "push ") + text);
|
||||
return new Node("num", text, v, List.of(), t.line);
|
||||
}
|
||||
|
||||
/** op<inline, …>(args): pushes the arguments and emits the opcode; returns its mnemonic. */
|
||||
private String callExpr() {
|
||||
/** op<inline, …>(args): a call node; the argument count must match the opcode's pops. */
|
||||
private Node callExpr() {
|
||||
Tok t = peek();
|
||||
String mn = ident();
|
||||
OpTable.Op op = ops.byName(mn);
|
||||
@@ -625,28 +850,150 @@ public final class StratCompiler {
|
||||
inline.add(cur.toString());
|
||||
cur.setLength(0);
|
||||
} else {
|
||||
cur.append(x.text);
|
||||
cur.append(
|
||||
x.kind.equals("num") && x.text.endsWith("r")
|
||||
? x.text.substring(0, x.text.length() - 1)
|
||||
: x.text);
|
||||
}
|
||||
}
|
||||
expect(">");
|
||||
inline.add(cur.toString());
|
||||
}
|
||||
int args = 0;
|
||||
List<Node> args = new ArrayList<>();
|
||||
if (accept("(")) {
|
||||
while (!at(")")) {
|
||||
expr();
|
||||
args++;
|
||||
args.add(expr());
|
||||
if (!accept(",")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(")");
|
||||
}
|
||||
if (op.pops() >= 0 && args != op.pops()) {
|
||||
if (op.pops() >= 0 && args.size() != op.pops()) {
|
||||
throw new CompileException(
|
||||
t.line, mn + " takes " + op.pops() + " value" + (op.pops() == 1 ? "" : "s") + ", got " + args);
|
||||
t.line,
|
||||
mn + " takes " + op.pops() + " value" + (op.pops() == 1 ? "" : "s") + ", got " + args.size());
|
||||
}
|
||||
return new Node("call", mn + (inline.isEmpty() ? "" : " " + String.join(", ", inline)), 0, args, t.line);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ constant folding
|
||||
|
||||
/** Folds operators over constants with the VM's integer semantics (20.12 mul/div). */
|
||||
private static Node fold(Node n) {
|
||||
if (!n.kids.stream().allMatch(Node::isNum)) {
|
||||
return n;
|
||||
}
|
||||
int a = n.kids.get(0).value;
|
||||
Integer v =
|
||||
switch (n.kind) {
|
||||
case "neg" -> -a;
|
||||
case "not" -> a == 0 ? 1 : 0;
|
||||
default -> {
|
||||
int b = n.kids.get(1).value;
|
||||
yield switch (n.text) {
|
||||
case "+" -> a + b;
|
||||
case "-" -> a - b;
|
||||
case "*" -> (int) (((long) a * b) >> 12);
|
||||
case "/" -> b == 0 ? null : ((a << 8) / b) << 4;
|
||||
case "&" -> a & b;
|
||||
case "|", "||" -> a | b;
|
||||
case "&&" -> a != 0 && b != 0 ? 1 : 0;
|
||||
case "==" -> a == b ? 1 : 0;
|
||||
case "!=" -> a != b ? 1 : 0;
|
||||
case "<" -> a < b ? 1 : 0;
|
||||
case ">" -> a > b ? 1 : 0;
|
||||
case "<=" -> a <= b ? 1 : 0;
|
||||
case ">=" -> a >= b ? 1 : 0;
|
||||
case "<<" -> a << (b >> 12);
|
||||
case ">>" -> a >> (b >> 12);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
};
|
||||
if (v == null) {
|
||||
return n;
|
||||
}
|
||||
String text =
|
||||
n.kind.equals("neg") && !n.kids.get(0).text.startsWith("-") ? "-" + n.kids.get(0).text : Num.literal(v);
|
||||
return new Node("num", text, v, List.of(), n.line);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ code generation
|
||||
|
||||
/** Code that pushes the value of n (&& and || as the and/or opcodes: both sides evaluated). */
|
||||
private void value(Node n) {
|
||||
switch (n.kind) {
|
||||
case "num" -> emit(
|
||||
(n.value >= Short.MIN_VALUE && n.value <= Short.MAX_VALUE ? "pushShort " : "push ") + n.text);
|
||||
case "ins" -> emit(n.text);
|
||||
case "neg" -> {
|
||||
value(n.kids.get(0));
|
||||
emit("neg");
|
||||
}
|
||||
case "not" -> {
|
||||
value(n.kids.get(0));
|
||||
emit("not");
|
||||
}
|
||||
case "bin" -> {
|
||||
value(n.kids.get(0));
|
||||
value(n.kids.get(1));
|
||||
emit(BINOP.get(n.text));
|
||||
}
|
||||
case "call" -> {
|
||||
n.kids.forEach(this::value);
|
||||
emit(n.text);
|
||||
}
|
||||
default -> throw new IllegalStateException(n.kind);
|
||||
}
|
||||
}
|
||||
|
||||
/** Jumps to target when n is false (0), falls through otherwise; && and || short-circuit. */
|
||||
private void jumpIfFalse(Node n, String target) {
|
||||
if (n.isNum()) {
|
||||
if (n.value == 0) {
|
||||
emit("jmp " + target);
|
||||
}
|
||||
} else if (n.kind.equals("not") && isLogic(n.kids.get(0))) {
|
||||
jumpIfTrue(n.kids.get(0), target);
|
||||
} else if (n.kind.equals("bin") && n.text.equals("&&")) {
|
||||
jumpIfFalse(n.kids.get(0), target);
|
||||
jumpIfFalse(n.kids.get(1), target);
|
||||
} else if (n.kind.equals("bin") && n.text.equals("||")) {
|
||||
String ok = label();
|
||||
jumpIfTrue(n.kids.get(0), ok);
|
||||
jumpIfFalse(n.kids.get(1), target);
|
||||
place(ok);
|
||||
} else {
|
||||
value(n);
|
||||
emit("jz " + target);
|
||||
}
|
||||
}
|
||||
|
||||
/** && / || / ! (whose negation is folded into the jumps); a plain !x stays "not" + jump. */
|
||||
private static boolean isLogic(Node n) {
|
||||
return n.kind.equals("not") || n.kind.equals("bin") && (n.text.equals("&&") || n.text.equals("||"));
|
||||
}
|
||||
|
||||
/** Jumps to target when n is true (non-zero), falls through otherwise. */
|
||||
private void jumpIfTrue(Node n, String target) {
|
||||
if (n.isNum()) {
|
||||
if (n.value != 0) {
|
||||
emit("jmp " + target);
|
||||
}
|
||||
} else if (n.kind.equals("not") && isLogic(n.kids.get(0))) {
|
||||
jumpIfFalse(n.kids.get(0), target);
|
||||
} else if (n.kind.equals("bin") && n.text.equals("||")) {
|
||||
jumpIfTrue(n.kids.get(0), target);
|
||||
jumpIfTrue(n.kids.get(1), target);
|
||||
} else if (n.kind.equals("bin") && n.text.equals("&&")) {
|
||||
String no = label();
|
||||
jumpIfFalse(n.kids.get(0), no);
|
||||
jumpIfTrue(n.kids.get(1), target);
|
||||
place(no);
|
||||
} else {
|
||||
value(n);
|
||||
emit("jnz " + target);
|
||||
}
|
||||
emit(mn + (inline.isEmpty() ? "" : " " + String.join(", ", inline)));
|
||||
return mn;
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,11 @@ public final class StratListing {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Symbolic name of another unit for "unit:Name" operands (null = by index). */
|
||||
default String unitName(int unit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Names NONE = new Names() {};
|
||||
}
|
||||
|
||||
@@ -323,6 +328,10 @@ public final class StratListing {
|
||||
|
||||
/** Reference text for an absolute STPC offset (operand of pushAddr). */
|
||||
String addrRef(int v) {
|
||||
return addrRef(v, Names.NONE);
|
||||
}
|
||||
|
||||
String addrRef(int v, Names names) {
|
||||
if (in(v) && labels.containsKey(v)) {
|
||||
return "@" + labels.get(v);
|
||||
}
|
||||
@@ -336,7 +345,9 @@ public final class StratListing {
|
||||
}
|
||||
int u = img.unitOf(v);
|
||||
if (u >= 0 && u != unit) {
|
||||
return "unit:" + u + plus(v - img.units.get(u).start());
|
||||
String n = names.unitName(u);
|
||||
return "unit:" + (n != null ? n : Integer.toString(u))
|
||||
+ plus(v - img.units.get(u).start());
|
||||
}
|
||||
return Num.integer(v);
|
||||
}
|
||||
@@ -673,7 +684,7 @@ public final class StratListing {
|
||||
}
|
||||
case StratOps.PUSH_ADDR -> {
|
||||
int v = w(o + 4);
|
||||
args.add(addrRef(v));
|
||||
args.add(addrRef(v, names));
|
||||
comment = addrComment(v, names);
|
||||
}
|
||||
case StratOps.PUSH_SOUND -> {
|
||||
|
||||
@@ -183,6 +183,7 @@ public final class StratPseudo {
|
||||
}
|
||||
}
|
||||
Map<String, List<String[]>> tables = switchTables();
|
||||
findChains();
|
||||
// candidate blocks
|
||||
List<Block> cand = new ArrayList<>();
|
||||
Map<Integer, Integer> elseOf = new HashMap<>();
|
||||
@@ -199,7 +200,7 @@ public final class StratPseudo {
|
||||
continue;
|
||||
}
|
||||
Integer t = labelIdx.get(operand(l));
|
||||
if (t == null) {
|
||||
if (t == null || chainMember.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
if (condJump(op) && t > i && clean(i, t)) {
|
||||
@@ -292,6 +293,9 @@ public final class StratPseudo {
|
||||
}
|
||||
opensAt.values().forEach(l -> l.sort((a, b) -> Integer.compare(b.close, a.close)));
|
||||
resolveJumps(accepted, consumed);
|
||||
for (int m : chainMember) {
|
||||
consumed.merge(ls.get(target(m)).label(), 1, Integer::sum);
|
||||
}
|
||||
Map<String, Integer> refs = new HashMap<>();
|
||||
for (StratListing.Line l : ls) {
|
||||
if (l.kind() == StratListing.Kind.INSTR || l.kind() == StratListing.Kind.DATA) {
|
||||
@@ -342,7 +346,8 @@ public final class StratPseudo {
|
||||
out(l.offset(), l.label() + ":");
|
||||
}
|
||||
for (Block b : opensAt.getOrDefault(i, List.of())) {
|
||||
out(-1, b.kind.equals("do") ? "do {" : "loop {");
|
||||
String name = loopNames.get(b.open);
|
||||
out(-1, (name == null ? "" : name + ": ") + (b.kind.equals("do") ? "do {" : "loop {"));
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
@@ -356,6 +361,7 @@ public final class StratPseudo {
|
||||
flush();
|
||||
whileLoops();
|
||||
elseIfChains();
|
||||
andChains();
|
||||
}
|
||||
|
||||
private final Map<Integer, Integer> caseOwner = new HashMap<>();
|
||||
@@ -375,6 +381,8 @@ public final class StratPseudo {
|
||||
}
|
||||
if (ok && a.kind.equals("if") && elseAccepted.contains(a.open)) {
|
||||
ok = nests(b.open, end, a.open, a.close) && nests(b.open, end, a.close, ae);
|
||||
// a jump from the then-part to the else label enters the else part: not a nested if
|
||||
ok &= !(b.kind.equals("if") && b.close == a.close && b.open > a.open);
|
||||
}
|
||||
if (!ok) {
|
||||
return false;
|
||||
@@ -442,19 +450,22 @@ public final class StratPseudo {
|
||||
// the end: the default target when it lies behind every case, else the first label after
|
||||
// the last case's code that a "jmp" in a case body targets
|
||||
int endIdx = d;
|
||||
if (d < e) {
|
||||
endIdx = -1;
|
||||
for (int i = s + 2; i < ls.size() && endIdx < 0; i++) {
|
||||
if (ls.get(i).kind() == StratListing.Kind.INSTR && ls.get(i).op() == 0x16) {
|
||||
Integer t = target(i);
|
||||
if (t != null && t > e) {
|
||||
endIdx = t;
|
||||
}
|
||||
// a default part behind the cases (compiled switches): a case body jumps over it
|
||||
int last = d < e ? e : d;
|
||||
int found = -1;
|
||||
for (int i = s + 2; i < ls.size() && i < (d < e ? ls.size() : d) && found < 0; i++) {
|
||||
if (ls.get(i).kind() == StratListing.Kind.INSTR && ls.get(i).op() == 0x16) {
|
||||
Integer t = target(i);
|
||||
if (t != null && t > last) {
|
||||
found = t;
|
||||
}
|
||||
}
|
||||
if (endIdx < 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (d < e && found < 0) {
|
||||
return;
|
||||
}
|
||||
if (found >= 0) {
|
||||
endIdx = found;
|
||||
cs.add(new int[] {d, -1});
|
||||
}
|
||||
if (!clean(s, endIdx)) {
|
||||
@@ -487,7 +498,8 @@ public final class StratPseudo {
|
||||
|| ifAt.containsKey(i)
|
||||
|| loopEnd.containsKey(i)
|
||||
|| elseJumps.contains(i)
|
||||
|| silentJumps.contains(i)) {
|
||||
|| silentJumps.contains(i)
|
||||
|| chainMember.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
Integer t = target(i);
|
||||
@@ -503,15 +515,20 @@ public final class StratPseudo {
|
||||
in = b;
|
||||
}
|
||||
}
|
||||
if (in == null) {
|
||||
continue;
|
||||
}
|
||||
int endAt = in.kind.equals("switch") ? in.close : in.close + 1;
|
||||
String as = null;
|
||||
if (anchor(t) == anchor(endAt)) {
|
||||
as = "break";
|
||||
} else if (in.isLoop() && anchor(t) == anchor(in.open)) {
|
||||
as = "continue";
|
||||
if (in != null) {
|
||||
int endAt = in.kind.equals("switch") ? in.close : in.close + 1;
|
||||
if (anchor(t) == anchor(endAt)) {
|
||||
as = "break";
|
||||
} else if (in.isLoop() && anchor(t) == anchor(in.open)) {
|
||||
as = "continue";
|
||||
}
|
||||
}
|
||||
if (as == null) {
|
||||
as = terminal(anchor(t));
|
||||
}
|
||||
if (as == null) {
|
||||
as = labeledJump(accepted, in, i, t);
|
||||
}
|
||||
if (as != null) {
|
||||
jumpAs.put(i, as);
|
||||
@@ -520,24 +537,306 @@ public final class StratPseudo {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- short-circuit condition chains
|
||||
|
||||
/** Conditional jumps folded into the condition of a later one (chain members). */
|
||||
private final Set<Integer> chainMember = new HashSet<>();
|
||||
|
||||
/** Last jump of a chain → its members in order and the body condition. */
|
||||
private record Chain(List<Integer> members, Cond body) {}
|
||||
|
||||
private final Map<Integer, Chain> chains = new HashMap<>();
|
||||
|
||||
private final List<String> pendingChain = new ArrayList<>();
|
||||
private int chainOff = -1;
|
||||
|
||||
/** Boolean formula over the jumps of a chain: T, F, L (leaf k: jump k is taken; pos=false negated), AND, OR. */
|
||||
private record Cond(String kind, int k, boolean pos, Cond a, Cond b) {
|
||||
static final Cond TRUE = new Cond("T", 0, true, null, null), FALSE = new Cond("F", 0, true, null, null);
|
||||
|
||||
static Cond leaf(int k) {
|
||||
return new Cond("L", k, true, null, null);
|
||||
}
|
||||
|
||||
Cond not() {
|
||||
return new Cond("L", k, !pos, null, null);
|
||||
}
|
||||
|
||||
static Cond and(Cond x, Cond y) {
|
||||
return y == TRUE ? x : x == TRUE ? y : new Cond("AND", 0, true, x, y);
|
||||
}
|
||||
|
||||
static Cond or(Cond x, Cond y) {
|
||||
return y == FALSE ? x : x == FALSE ? y : new Cond("OR", 0, true, x, y);
|
||||
}
|
||||
|
||||
/** "if j then a else b" as and/or of j, a and b, or null if it does not simplify. */
|
||||
static Cond ite(Cond j, Cond a, Cond b) {
|
||||
if (a == TRUE) {
|
||||
return or(j, b);
|
||||
}
|
||||
if (a == FALSE) {
|
||||
return and(j.not(), b);
|
||||
}
|
||||
if (b == TRUE) {
|
||||
return or(j.not(), a);
|
||||
}
|
||||
if (b == FALSE) {
|
||||
return and(j, a);
|
||||
}
|
||||
if (b.kind.equals("OR") && b.b.equals(a)) {
|
||||
return or(and(j.not(), b.a), a);
|
||||
}
|
||||
if (b.kind.equals("AND") && b.b.equals(a)) {
|
||||
return and(or(j, b.a), a);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds chains of conditional jumps that compiled || / && conditions leave ("a; jnz T; b; jz F;
|
||||
* T:"): separated only by expression code, each going to the false target F of the last one,
|
||||
* to the code right behind it, or to the start of a later member's expression; at least one
|
||||
* member (an else part would otherwise be hidden behind nested ifs that all jump to it).
|
||||
*/
|
||||
private void findChains() {
|
||||
for (int n = ls.size() - 1; n >= 0; n--) {
|
||||
if (ls.get(n).kind() != StratListing.Kind.INSTR
|
||||
|| !condJump(ls.get(n).op())
|
||||
|| chainMember.contains(n)) {
|
||||
continue;
|
||||
}
|
||||
Integer f = target(n);
|
||||
if (f == null || f <= n) {
|
||||
continue;
|
||||
}
|
||||
int body = anchor(n + 1);
|
||||
// members backwards: segment starts (anchors) → formula from there
|
||||
List<Integer> members = new ArrayList<>();
|
||||
Map<Integer, Cond> from = new HashMap<>();
|
||||
Cond last = Cond.leaf(n).not();
|
||||
int seg = segmentStart(n);
|
||||
from.put(seg, last);
|
||||
Cond best = null;
|
||||
List<Integer> bestMembers = null;
|
||||
Cond cur = last;
|
||||
while (seg > 0) {
|
||||
int j = seg - 1;
|
||||
while (j >= 0 && ls.get(j).kind() == StratListing.Kind.LABEL) {
|
||||
j--;
|
||||
}
|
||||
if (j < 0
|
||||
|| ls.get(j).kind() != StratListing.Kind.INSTR
|
||||
|| !condJump(ls.get(j).op())
|
||||
|| chainMember.contains(j)) {
|
||||
break;
|
||||
}
|
||||
Integer t = target(j);
|
||||
if (t == null || t <= j || !labelsOnlyFrom(j, seg)) {
|
||||
break;
|
||||
}
|
||||
int at = anchor(t);
|
||||
Cond whenJumps = at == body ? Cond.TRUE : at == anchor(f) ? Cond.FALSE : from.get(at);
|
||||
if (whenJumps == null) {
|
||||
break;
|
||||
}
|
||||
cur = Cond.ite(Cond.leaf(j), whenJumps, cur);
|
||||
if (cur == null) {
|
||||
break;
|
||||
}
|
||||
members.add(0, j);
|
||||
seg = segmentStart(j);
|
||||
if (seg < 0) {
|
||||
break;
|
||||
}
|
||||
from.put(seg, cur);
|
||||
best = cur;
|
||||
bestMembers = List.copyOf(members);
|
||||
}
|
||||
if (best == null) {
|
||||
continue;
|
||||
}
|
||||
chainMember.addAll(bestMembers);
|
||||
chains.put(n, new Chain(bestMembers, best));
|
||||
}
|
||||
}
|
||||
|
||||
/** Labels between line indexes j and seg are only targets of conditional jumps at or before j. */
|
||||
private boolean labelsOnlyFrom(int j, int seg) {
|
||||
for (int k = j + 1; k < seg; k++) {
|
||||
String lb = ls.get(k).label();
|
||||
for (int x = 0; x < ls.size(); x++) {
|
||||
StratListing.Line l = ls.get(x);
|
||||
boolean uses = l.kind() != StratListing.Kind.LABEL
|
||||
&& List.of(l.text().split("[ ,@+]+")).contains(lb);
|
||||
if (uses && (x > j || l.kind() != StratListing.Kind.INSTR || !condJump(l.op()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start (line index) of the expression code that the conditional jump at j pops: the lines
|
||||
* before it must be pure expression code pushing exactly one value; −1 if not.
|
||||
*/
|
||||
private int segmentStart(int j) {
|
||||
int need = 1;
|
||||
for (int k = j - 1; k >= 0; k--) {
|
||||
StratListing.Line l = ls.get(k);
|
||||
if (l.kind() != StratListing.Kind.INSTR) {
|
||||
return -1;
|
||||
}
|
||||
int d = stackEffect(l);
|
||||
if (d == Integer.MIN_VALUE) {
|
||||
return -1;
|
||||
}
|
||||
need -= d;
|
||||
if (need == 0) {
|
||||
return k;
|
||||
}
|
||||
if (need < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Net pushes of a pure expression instruction (MIN_VALUE if it is not one). */
|
||||
private int stackEffect(StratListing.Line l) {
|
||||
int op = l.op();
|
||||
if (PUSH.containsKey(op)
|
||||
|| ADDR.containsKey(op)
|
||||
|| op == StratOps.PUSH_SHORT
|
||||
|| op == StratOps.PUSH_IMM
|
||||
|| op == StratOps.PUSH_ADDR
|
||||
|| op == StratOps.PUSH_SOUND) {
|
||||
return 1;
|
||||
}
|
||||
if (BINARY.containsKey(mn(l))) {
|
||||
return -1;
|
||||
}
|
||||
if (op == 0x46 || op == 0xaf) {
|
||||
return 0;
|
||||
}
|
||||
OpTable.Op info = ops.op(op);
|
||||
if (info.pushes() == 1 && info.pops() >= 0 && op >= StratOps.PUSH_IMM && op != StratOps.SPAWN) {
|
||||
return 1 - info.pops();
|
||||
}
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
/** Text of a chain's body condition; leaves: members' expressions (pendingChain), last = e. */
|
||||
private String chainCondition(int n, Chain c, String e) {
|
||||
Map<Integer, String> text = new HashMap<>();
|
||||
for (int k = 0; k < c.members.size(); k++) {
|
||||
text.put(c.members.get(k), k < pendingChain.size() ? pendingChain.get(k) : "pop()");
|
||||
}
|
||||
text.put(n, e);
|
||||
pendingChain.clear();
|
||||
return render(c.body, text);
|
||||
}
|
||||
|
||||
private String render(Cond c, Map<Integer, String> text) {
|
||||
return switch (c.kind) {
|
||||
case "L" -> {
|
||||
int op = ls.get(c.k).op();
|
||||
boolean jnz = op == 0x13 || op == 0x15;
|
||||
String e = text.get(c.k);
|
||||
yield jnz == c.pos ? e : not(e);
|
||||
}
|
||||
case "AND" -> andOperand(render(c.a, text)) + " && " + andOperand(render(c.b, text));
|
||||
case "OR" -> orOperand(render(c.a, text)) + " || " + orOperand(render(c.b, text));
|
||||
default -> c.kind.equals("T") ? "1r" : "0";
|
||||
};
|
||||
}
|
||||
|
||||
/** Parenthesizes an || operand with a top-level && (for reading). */
|
||||
private static String orOperand(String c) {
|
||||
return topLevel(c).contains("&&") ? "(" + c + ")" : c;
|
||||
}
|
||||
|
||||
private static String not(String e) {
|
||||
if (e.startsWith("!(") && e.endsWith(")") && balanced(e.substring(2, e.length() - 1))) {
|
||||
return e.substring(2, e.length() - 1);
|
||||
}
|
||||
return e.matches("[\\w.]+") ? "!" + e : "!(" + e + ")";
|
||||
}
|
||||
|
||||
/** Loops that a labeled break/continue names: open line index → name. */
|
||||
private final Map<Integer, String> loopNames = new HashMap<>();
|
||||
|
||||
/** "break name"/"continue name" for a jump from line i to t out of an outer loop (null if none). */
|
||||
private String labeledJump(List<Block> accepted, Block inner, int i, int t) {
|
||||
Block best = null;
|
||||
String as = null;
|
||||
for (Block b : accepted) {
|
||||
if (b == inner || !(b.isLoop() || b.kind.equals("do")) || b.open >= i || i >= b.close) {
|
||||
continue;
|
||||
}
|
||||
String k = anchor(t) == anchor(b.close + 1)
|
||||
? "break"
|
||||
: b.isLoop() && anchor(t) == anchor(b.open) ? "continue" : null;
|
||||
if (k != null && (best == null || b.open > best.open)) {
|
||||
best = b;
|
||||
as = k;
|
||||
}
|
||||
}
|
||||
if (best == null || inner == null) {
|
||||
return null;
|
||||
}
|
||||
String name = ls.get(best.open).label();
|
||||
loopNames.put(best.open, name);
|
||||
return as + " " + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* "return"/"end"/"remove" when the instruction at line index i ends the strat or subroutine,
|
||||
* so that a jump to it is the same as executing it (null otherwise).
|
||||
*/
|
||||
private String terminal(int i) {
|
||||
if (i >= ls.size() || ls.get(i).kind() != StratListing.Kind.INSTR) {
|
||||
return null;
|
||||
}
|
||||
int op = ls.get(i).op();
|
||||
return op == StratOps.JUMP_POP ? "return" : op == StratOps.END || op == 0x8e ? mn(ls.get(i)) : null;
|
||||
}
|
||||
|
||||
/** "loop {" directly followed by "if (c) break" becomes "while (!c) {". */
|
||||
private void whileLoops() {
|
||||
for (int i = 0; i + 1 < lines.size(); i++) {
|
||||
Line a = lines.get(i), b = lines.get(i + 1);
|
||||
if (a.text.equals("loop {")
|
||||
if (a.text.endsWith("loop {")
|
||||
&& (a.text.equals("loop {") || a.text.matches("\\w+: loop \\{"))
|
||||
&& b.depth == a.depth + 1
|
||||
&& b.text.startsWith("if (")
|
||||
&& b.text.endsWith(") break")) {
|
||||
String c = b.text.substring(4, b.text.length() - 7);
|
||||
String w = c.startsWith("!(") && c.endsWith(")") && balanced(c.substring(2, c.length() - 1))
|
||||
? c.substring(2, c.length() - 1)
|
||||
: c.startsWith("!") && !c.contains(" ") ? c.substring(1) : "!(" + c + ")";
|
||||
lines.set(i, new Line(b.offset, a.depth, "while (" + w + ") {"));
|
||||
String w = breakCondition(b.text);
|
||||
lines.remove(i + 1);
|
||||
// further "if (c) break" lines at the top: while (w && !c)
|
||||
while (i + 1 < lines.size()
|
||||
&& lines.get(i + 1).depth == a.depth + 1
|
||||
&& lines.get(i + 1).text.startsWith("if (")
|
||||
&& lines.get(i + 1).text.endsWith(") break")) {
|
||||
w = andOperand(w) + " && " + andOperand(breakCondition(lines.get(i + 1).text));
|
||||
lines.remove(i + 1);
|
||||
}
|
||||
String name = a.text.equals("loop {") ? "" : a.text.substring(0, a.text.length() - 6);
|
||||
lines.set(i, new Line(b.offset, a.depth, name + "while (" + w + ") {"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The loop condition that "if (c) break" leaves: !c. */
|
||||
private static String breakCondition(String line) {
|
||||
String c = line.substring(4, line.length() - 7);
|
||||
return c.startsWith("!(") && c.endsWith(")") && balanced(c.substring(2, c.length() - 1))
|
||||
? c.substring(2, c.length() - 1)
|
||||
: c.startsWith("!") && !c.contains(" ") ? c.substring(1) : "!(" + c + ")";
|
||||
}
|
||||
|
||||
/** "} else {" + a lone "if" up to the else's end becomes "} else if (…) {"; an empty then-block is negated. */
|
||||
private void elseIfChains() {
|
||||
for (int i = 0; i + 1 < lines.size(); i++) {
|
||||
@@ -591,6 +890,59 @@ public final class StratPseudo {
|
||||
}
|
||||
}
|
||||
|
||||
/** "if (a) {" whose whole body is "if (b) {…}" becomes "if (a && b) {…}". */
|
||||
private void andChains() {
|
||||
for (int i = 0; i + 1 < lines.size(); ) {
|
||||
Line a = lines.get(i), b = lines.get(i + 1);
|
||||
int close = -1;
|
||||
if (isPlainIf(a.text) && isPlainIf(b.text) && b.depth == a.depth + 1) {
|
||||
for (int k = i + 2; k < lines.size(); k++) {
|
||||
Line x = lines.get(k);
|
||||
if (x.depth <= b.depth) {
|
||||
close = x.depth == b.depth && x.text.equals("}") ? k : -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (close < 0
|
||||
|| close + 1 >= lines.size()
|
||||
|| !lines.get(close + 1).text.equals("}")
|
||||
|| lines.get(close + 1).depth != a.depth) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
String c = andOperand(a.text.substring(4, a.text.length() - 3)) + " && "
|
||||
+ andOperand(b.text.substring(4, b.text.length() - 3));
|
||||
lines.set(i, new Line(a.offset, a.depth, "if (" + c + ") {"));
|
||||
lines.remove(close + 1);
|
||||
lines.remove(i + 1);
|
||||
for (int k = i + 1; k < close; k++) {
|
||||
Line x = lines.get(k);
|
||||
lines.set(k, new Line(x.offset, x.depth - 1, x.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** c with everything inside brackets blanked out. */
|
||||
private static String topLevel(String c) {
|
||||
StringBuilder top = new StringBuilder();
|
||||
int d = 0;
|
||||
for (char ch : c.toCharArray()) {
|
||||
d += ch == '(' ? 1 : ch == ')' ? -1 : 0;
|
||||
top.append(d == 0 ? ch : ' ');
|
||||
}
|
||||
return top.toString();
|
||||
}
|
||||
|
||||
private static boolean isPlainIf(String t) {
|
||||
return t.startsWith("if (") && t.endsWith(") {") && balanced(t.substring(4, t.length() - 3));
|
||||
}
|
||||
|
||||
private static String andOperand(String c) {
|
||||
String t = topLevel(c);
|
||||
return t.contains("||") || t.contains(" | ") || t.contains(" & ") ? "(" + c + ")" : c;
|
||||
}
|
||||
|
||||
private static boolean nests(int a0, int a1, int b0, int b1) {
|
||||
return a1 <= b0 || b1 <= a0 || (a0 >= b0 && a1 <= b1) || (b0 >= a0 && b1 <= a1);
|
||||
}
|
||||
@@ -686,7 +1038,9 @@ public final class StratPseudo {
|
||||
|| op == StratOps.PUSH_IMM
|
||||
|| op == StratOps.PUSH_ADDR
|
||||
|| op == StratOps.PUSH_SOUND) {
|
||||
push(off, a + (l.comment() != null && l.comment().startsWith("strat") ? " /*" + l.comment() + "*/" : ""));
|
||||
// the script language reads a plain integer as units: raw words get the "r" suffix
|
||||
String v = a.matches("-?[1-9]\\d*") ? a + "r" : a;
|
||||
push(off, v + (l.comment() != null && l.comment().startsWith("strat") ? " /*" + l.comment() + "*/" : ""));
|
||||
return;
|
||||
}
|
||||
if (op == 0x47 || op == 0x48) {
|
||||
@@ -709,11 +1063,23 @@ public final class StratPseudo {
|
||||
push(off, "!" + pop());
|
||||
return;
|
||||
}
|
||||
if (condJump(op) && chainMember.contains(i)) {
|
||||
if (pendingChain.isEmpty()) {
|
||||
chainOff = begin(off);
|
||||
}
|
||||
pendingChain.add(strip(pop()));
|
||||
return;
|
||||
}
|
||||
if (condJump(op)) {
|
||||
String e = strip(pop());
|
||||
boolean neg = op == 0x13 || op == 0x15;
|
||||
String yield = op <= 0x13 ? " /*yield*/" : "";
|
||||
int b0 = begin(off);
|
||||
if (chains.containsKey(i)) {
|
||||
e = chainCondition(i, chains.get(i), e);
|
||||
neg = false;
|
||||
b0 = chainOff;
|
||||
}
|
||||
String yield = op <= 0x13 ? " /*yield*/" : "";
|
||||
flush();
|
||||
Block lp = loopEnd.get(i);
|
||||
String jumpIf = neg ? e : "!(" + e + ")";
|
||||
@@ -723,8 +1089,9 @@ public final class StratPseudo {
|
||||
} else if (lp != null) {
|
||||
depth--;
|
||||
Line last = lines.isEmpty() ? null : lines.get(lines.size() - 1);
|
||||
if (last != null && last.text.equals("do {") && last.depth == depth) {
|
||||
lines.set(lines.size() - 1, new Line(b0, depth, "do {} while (" + jumpIf + ")" + yield));
|
||||
if (last != null && last.text.endsWith("do {") && last.depth == depth) {
|
||||
String name = last.text.substring(0, last.text.length() - 4);
|
||||
lines.set(lines.size() - 1, new Line(b0, depth, name + "do {} while (" + jumpIf + ")" + yield));
|
||||
} else {
|
||||
out(b0, "} while (" + jumpIf + ")" + yield);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# old mnemonic current mnemonic (old names still assemble; stored sources keep working)
|
||||
flagEC10000 freezeOffCamera
|
||||
flagEC2 touchable
|
||||
flagEC100 noFloorPhysics
|
||||
hudReset2 hudResetNoop
|
||||
|
@@ -46,11 +46,11 @@
|
||||
0x2c nop_0x2c - 0 0 No-op (shared empty handler); unused. high -
|
||||
0x2d nop_0x2d - 0 0 No-op (shared empty handler); unused. high -
|
||||
0x2e nop_0x2e - 0 0 No-op (shared empty handler); unused. high -
|
||||
0x2f flagEC10000 imm 0 0 arg!=0 sets, 0 clears +0xEC bit 0x10000 = "freeze when off-camera": the camera pass (FUN_00436190) sets +0xEC 0x8000 when the strat is outside the camera range; with both bits set the frame loop (0x54c603) and the strat-vs-strat collision loop (0x54cc1a) skip it. Used by ~1350 strats (pickups, triggers, enemies). medium -
|
||||
0x2f freezeOffCamera imm 0 0 arg!=0 sets, 0 clears +0xEC bit 0x10000 = "freeze when off-camera": the camera pass (FUN_00436190) sets +0xEC 0x8000 when the strat is outside the camera range; with both bits set the frame loop (0x54c603) and the strat-vs-strat collision loop (0x54cc1a) skip it. Used by ~1350 strats (pickups, triggers, enemies). medium -
|
||||
0x30 trackPlayer imm 0 0 If no player yet: re-executes itself next frame (pc-=4, yield); else arg sets/clears +0xEC bit 0x4000 = update distance to player each frame (+0x148). medium -
|
||||
0x31 flagEC2 imm 0 0 arg sets/clears +0xEC bit 2 = "others can stand on / touch me": in the strat collision loop (0x54cc96) a strat touching a strat with this bit gets +0xE8 0x400000 (and 0x800000 when it moves) = properties 32/33; also readable as property 0x1f. Set by Llama, Coin, Jar, EnemyCoin, RubyIdol. medium -
|
||||
0x31 touchable imm 0 0 arg sets/clears +0xEC bit 2 = "others can stand on / touch me": in the strat collision loop (0x54cc96) a strat touching a strat with this bit gets +0xE8 0x400000 (and 0x800000 when it moves) = properties 32/33; also readable as property 0x1f. Set by Llama, Coin, Jar, EnemyCoin, RubyIdol. medium -
|
||||
0x32 flagE8_80000000 imm 0 0 arg sets/clears +0xE8 bit 31; no reader of the bit found in the exe (scan of all +0xE8 flag tests); unused in the levels. medium -
|
||||
0x33 flagEC100 imm 0 0 arg sets/clears +0xEC bit 0x100; the collision response (0x4044f7) skips the floor/slope handling when it is set. Used by LlamaSpit, DropObject, RollerCoaster, KuscoTurtle (objects that move by script, not by floor physics). medium -
|
||||
0x33 noFloorPhysics imm 0 0 arg sets/clears +0xEC bit 0x100; the collision response (0x4044f7) skips the floor/slope handling when it is set. Used by LlamaSpit, DropObject, RollerCoaster, KuscoTurtle (objects that move by script, not by floor physics). medium -
|
||||
0x34 flagEC200000 imm 0 0 arg sets/clears +0xEC bit 0x200000; no direct test of the bit found in the exe (all +0xEC flag tests scanned). Used by Llama, RiverCroc, RiverLog, Rock, river NPCs (river/canoe level objects). low -
|
||||
0x35 nop_0x35 - 0 0 No-op (shared empty handler); unused. high -
|
||||
0x36 nop_0x36 - 0 0 No-op (shared empty handler); unused. high -
|
||||
@@ -329,7 +329,7 @@
|
||||
0x147 nop_0x147 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
|
||||
0x148 gotoLevel4_1 - 0 0 Hardcoded level change: next level (0x584EB4/EB8/EBC/EC0) = 4/1/1/0, previous level (0x585008..14) = 4/1/4/0, game state 0x584638 = 7 (load level). medium -
|
||||
0x149 nop_0x149 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
|
||||
0x14a hudReset2 - 0 0 Calls FUN_004268c0(2), which returns at once for any argument other than 0 (verified in the assembly): a no-op. Unused. high -
|
||||
0x14a hudResetNoop - 0 0 Calls FUN_004268c0(2), which returns at once for any argument other than 0 (verified in the assembly): a no-op. Unused. high -
|
||||
0x14b setGlobal5fd086 imm 0 0 Stores the low 16 bits of the inline word in global 0x5FD086 (never read elsewhere; dead leftover). medium -
|
||||
0x14c removeCollPoint - 1 0 pops id (fixed, >>12); removes the collision point with that id from the strat's collision point list (+0xC8 array of 4-byte entries, count +0xC6). Counterpart of 0xc6 (add/set collision point). medium stSetCollPoint (inverse)
|
||||
0x14d setFlagEC_400000 flags 0 0 Inline word: nonzero sets, zero clears bit 0x400000 of strat flags2 (+0xEC); the bit selects which height constant own property 0xF2 returns. high -
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 36 and column 30.
|
@@ -213,4 +213,142 @@ class StratEditsTest {
|
||||
}
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/** Import in the same session behind custom types: they stay last, can be edited/deleted, creating is undoable. */
|
||||
@Test
|
||||
void customTypesAfterImportInTheSameSession() throws Exception {
|
||||
Bases cd = cd();
|
||||
StratEdits.init();
|
||||
CrossImport.init();
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "imp", "t1l1m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
h.execute(TypeEdits.create(p, StratEdits.cloneOf(p.scene(), "Coin", "CoinA")));
|
||||
h.execute(Edits.placeStrat(p.scene(), "CoinA", 0, 30, 2, 30, 0.0));
|
||||
Strat a = p.scene().strats().get(p.scene().strats().size() - 1);
|
||||
CrossImport.importInto(p, List.of(new CrossImport.TypeRef("t1l2m001", "Spider", spiderCode(cd))), cd);
|
||||
h.clear();
|
||||
int units = StratEdits.analysis(p.scene()).img().units.size();
|
||||
assertEquals(units - 1, StratEdits.firstCustomUnit(p.scene()), "the custom unit is the last one again");
|
||||
assertEquals(null, StratEdits.rebuildBlocker(p.scene()));
|
||||
assertEquals(entry(p, "CoinA"), a.code());
|
||||
assertEquals("CoinA", nameAt(WadBuilder.build(p.scene()), a.code()));
|
||||
assertNotNull(p.scene().catalog().type("Spider"));
|
||||
assertTrue(StratEdits.analysis(p.scene()).code().errors.isEmpty());
|
||||
assertArrayEquals(
|
||||
StratEdits.chunk(p.reference()).data(),
|
||||
StratEdits.chunk(p.scene()).data(),
|
||||
"reference = scene");
|
||||
|
||||
// create (undoable), edit, delete
|
||||
h.execute(TypeEdits.create(p, StratEdits.cloneOf(p.scene(), "Coin", "CoinB")));
|
||||
assertEquals(2, StratEdits.records(p).types().size());
|
||||
h.undo();
|
||||
assertEquals(1, StratEdits.records(p).types().size());
|
||||
assertTrue(p.scene().catalog().type("CoinB") == null);
|
||||
h.redo();
|
||||
StratEdits.CustomType ta = StratEdits.records(p).types().get(0);
|
||||
h.execute(TypeEdits.edit(p, 0, withSource(ta, ta.source().replace("push 5.625deg", "push 90deg"))));
|
||||
assertEquals(0x400000, spin(p, "CoinA").value());
|
||||
assertEquals(entry(p, "CoinA"), a.code());
|
||||
byte[] wad = WadBuilder.build(p.scene());
|
||||
Project q = ProjectIO.read(ProjectIO.write(p, cd), cd);
|
||||
assertTrue(q.warnings().isEmpty(), q.warnings().toString());
|
||||
assertArrayEquals(wad, WadBuilder.build(q.scene()), "reloaded project builds the same WAD");
|
||||
h.execute(TypeEdits.delete(p, 0));
|
||||
assertTrue(!p.scene().strats().contains(a));
|
||||
h.undo();
|
||||
assertEquals(entry(p, "CoinA"), a.code());
|
||||
}
|
||||
|
||||
/** unit:N operands that point at custom types are stored by name and follow the type when indexes shift. */
|
||||
@Test
|
||||
void referencesBetweenCustomTypesAreSymbolic() throws Exception {
|
||||
Bases cd = cd();
|
||||
StratEdits.init();
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "refs", "t1l1m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
h.execute(TypeEdits.create(p, StratEdits.cloneOf(p.scene(), "Coin", "First")));
|
||||
h.execute(TypeEdits.create(p, StratEdits.cloneOf(p.scene(), "Coin", "Target")));
|
||||
int target = StratEdits.firstCustomUnit(p.scene()) + 1;
|
||||
int off = entry(p, "Target")
|
||||
- StratEdits.analysis(p.scene()).img().units.get(target).start();
|
||||
String src =
|
||||
"""
|
||||
.unit RefB
|
||||
.string "RefB"
|
||||
RefB:
|
||||
debugName "RefB"
|
||||
address unit:%d+%d
|
||||
pop
|
||||
spin:
|
||||
jmpYield spin
|
||||
end
|
||||
"""
|
||||
.formatted(target, off);
|
||||
StratCatalog.Variant v = p.scene().catalog().type("Coin").variant(0);
|
||||
h.execute(TypeEdits.create(p, new StratEdits.CustomType("RefB", null, "RefB", src, v, "Coin")));
|
||||
assertTrue(StratEdits.records(p).types().get(2).source().contains("address unit:Target+" + off));
|
||||
int tu = StratEdits.analysis(p.scene()).img().unitOf(entry(p, "Target"));
|
||||
assertEquals(StratEdits.analysis(p.scene()).img().units.get(tu).start() + off, addressIn(p, "RefB"));
|
||||
// delete the first type: Target moves one unit down, RefB still points at it
|
||||
h.execute(TypeEdits.delete(p, 0));
|
||||
tu = StratEdits.analysis(p.scene()).img().unitOf(entry(p, "Target"));
|
||||
assertEquals(StratEdits.analysis(p.scene()).img().units.get(tu).start() + off, addressIn(p, "RefB"));
|
||||
int ru = StratEdits.analysis(p.scene()).img().unitOf(entry(p, "RefB"));
|
||||
assertTrue(StratEdits.listing(p.scene(), ru).text().contains("unit:Target+0x" + Integer.toHexString(off)));
|
||||
// renaming the target renames the reference; deleting it is refused
|
||||
StratEdits.CustomType t = StratEdits.records(p).types().get(0);
|
||||
h.execute(TypeEdits.edit(
|
||||
p, 0, new StratEdits.CustomType("Goal", t.basedOn(), t.entryLabel(), t.source(), t.proto())));
|
||||
assertTrue(StratEdits.records(p).types().get(1).source().contains("unit:Goal+" + off));
|
||||
List<StratEdits.CustomType> rest = List.of(StratEdits.records(p).types().get(1));
|
||||
List<String> problems = StratEdits.checkTypes(p.scene(), rest, -1, null);
|
||||
assertNotNull(problems);
|
||||
assertTrue(problems.get(0).contains("unknown unit name"), problems.toString());
|
||||
Project q = ProjectIO.read(ProjectIO.write(p, cd), cd);
|
||||
assertTrue(q.warnings().isEmpty(), q.warnings().toString());
|
||||
assertArrayEquals(WadBuilder.build(p.scene()), WadBuilder.build(q.scene()));
|
||||
}
|
||||
|
||||
/** Value of the first address operand in a type's unit. */
|
||||
static int addressIn(Project p, String type) {
|
||||
StratEdits.Analysis a = StratEdits.analysis(p.scene());
|
||||
int u = a.img().unitOf(entry(p, type));
|
||||
for (StratCode.Site s : a.code().addrSites) {
|
||||
if (a.img().unitOf(s.offset()) == u) {
|
||||
return s.value();
|
||||
}
|
||||
}
|
||||
throw new AssertionError("no address operand in " + type);
|
||||
}
|
||||
|
||||
/** Edit can switch the type the fields come from; it is saved. */
|
||||
@Test
|
||||
void editChangesTheFieldsType() throws Exception {
|
||||
Bases cd = cd();
|
||||
StratEdits.init();
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "fields", "t1l1m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
StratEdits.CustomType t = StratEdits.cloneOf(p.scene(), "Coin", "CoinF");
|
||||
h.execute(TypeEdits.create(p, t));
|
||||
String other = p.scene().catalog().types().stream()
|
||||
.map(StratCatalog.Type::name)
|
||||
.filter(n -> !n.equals("Coin")
|
||||
&& !n.equals("CoinF")
|
||||
&& p.scene().catalog().type(n).variant(0).nVars()
|
||||
!= p.scene().catalog().type("Coin").variant(0).nVars())
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
StratCatalog.Variant ov = p.scene().catalog().type(other).variant(0);
|
||||
h.execute(TypeEdits.edit(
|
||||
p, 0, new StratEdits.CustomType("CoinF", "Coin", t.entryLabel(), t.source(), ov, other)));
|
||||
assertEquals(ov.nVars(), p.scene().catalog().type("CoinF").variant(0).nVars());
|
||||
Project q = ProjectIO.read(ProjectIO.write(p, cd), cd);
|
||||
assertEquals(other, StratEdits.records(q).types().get(0).fieldsFrom());
|
||||
assertEquals("Coin", StratEdits.records(q).types().get(0).basedOn());
|
||||
h.undo();
|
||||
assertEquals(
|
||||
p.scene().catalog().type("Coin").variant(0).nVars(),
|
||||
p.scene().catalog().type("CoinF").variant(0).nVars());
|
||||
}
|
||||
}
|
||||
@@ -69,9 +69,64 @@ public final class StratsGameLevel {
|
||||
}
|
||||
""";
|
||||
|
||||
/** Script language 2: switch over a state that an event handler advances, unit numbers, && / ||. */
|
||||
static final String TOUCHER =
|
||||
"""
|
||||
// grows each time the player touches it: 1x, 2x, 3x, then spins fast
|
||||
strat Toucher {
|
||||
alias hits = v5
|
||||
alias cooldown = v6
|
||||
setModel(model:5)
|
||||
collisionOn<0x20000>()
|
||||
collRadius(0.6)
|
||||
self.y = self.y + 1
|
||||
hits = 0
|
||||
cooldown = 0
|
||||
on touched {
|
||||
if (cooldown == 0 && hits < 3) {
|
||||
hits += 1
|
||||
cooldown = 20
|
||||
soundPlay(sound:6)
|
||||
}
|
||||
}
|
||||
loop {
|
||||
if (cooldown > 0 || hits > 9) {
|
||||
cooldown -= 1
|
||||
}
|
||||
switch (hits) {
|
||||
case 0:
|
||||
scale(1)
|
||||
turnLeft(2deg)
|
||||
break
|
||||
case 1:
|
||||
scale(2)
|
||||
turnLeft(4deg)
|
||||
break
|
||||
case 2:
|
||||
scale(3)
|
||||
turnLeft(8deg)
|
||||
break
|
||||
default:
|
||||
scale(3)
|
||||
turnLeft(45deg)
|
||||
}
|
||||
yield
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
/** The same with a timer event (type 0x40) instead of touched: grows without the player. */
|
||||
static final String TICKER = TOUCHER.replace("strat Toucher", "strat Ticker")
|
||||
.replace("on touched", "on event(0x40, 30)")
|
||||
.replace(" collisionOn<0x20000>()\n collRadius(0.6)\n", "");
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
Bases cd = Bases.fromCd(Path.of(System.getProperty("user.home") + "/.wine-lama/iso-dk/Setup/Eng/Cd"));
|
||||
StratEdits.init();
|
||||
if (a.length > 1 && a[1].equals("v2")) {
|
||||
v2(cd, Path.of(a[0]));
|
||||
return;
|
||||
}
|
||||
if (a.length > 1 && a[1].equals("rain")) {
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "edit", "t1l2m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
@@ -119,4 +174,42 @@ public final class StratsGameLevel {
|
||||
System.out.println("saved " + a[0] + " (" + p.scene().strats().size() + " objects, start " + sx + "," + sy + ","
|
||||
+ sz + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Village 1: a custom type, then an import (Spider) in the same session, then the custom type
|
||||
* edited (3x scale) and a compiled script type created (Toucher, 2.5 cells ahead of the start).
|
||||
*/
|
||||
static void v2(Bases cd, Path out) throws Exception {
|
||||
com.eliasmueller.llamamod.editor.tools.catalog.CrossImport.init();
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "strats2", "t1l1m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
StratEdits.CustomType big = StratEdits.cloneOf(p.scene(), "Coin", "BigCoin");
|
||||
h.execute(TypeEdits.create(p, big));
|
||||
com.eliasmueller.llamamod.editor.tools.catalog.CrossImport.importInto(
|
||||
p,
|
||||
List.of(new com.eliasmueller.llamamod.editor.tools.catalog.CrossImport.TypeRef(
|
||||
"t1l2m001", "Spider", StratEditsTest.spiderCode(cd))),
|
||||
cd);
|
||||
h.clear();
|
||||
String src =
|
||||
big.source().replace(" setModel\n", " setModel\n pushShort 3.0\n scale\n");
|
||||
h.execute(TypeEdits.edit(p, 0, big.withSource(src)));
|
||||
String asm = StratCompiler.compile(TOUCHER, StratEdits.paramNames("Coin"));
|
||||
h.execute(TypeEdits.create(p, new StratEdits.CustomType("Toucher", null, "Toucher", asm, big.proto(), "Coin")));
|
||||
Strat start = p.scene().playerStart();
|
||||
double sx = start.x() / 4096.0, sy = start.y() / 4096.0, sz = start.z() / 4096.0;
|
||||
String tick = StratCompiler.compile(TICKER, StratEdits.paramNames("Coin"));
|
||||
h.execute(TypeEdits.create(p, new StratEdits.CustomType("Ticker", null, "Ticker", tick, big.proto(), "Coin")));
|
||||
// the start view looks along +z (the row of the first in-game test is in front of it)
|
||||
h.execute(Edits.placeStrat(p.scene(), "Toucher", 0, sx, sy, sz + 2.5, 0.0));
|
||||
h.execute(Edits.placeStrat(p.scene(), "Ticker", 0, 30.0, 8.0, 64.6, 0.0));
|
||||
String[] types = {"Coin", "BigCoin", "Spider"};
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
h.execute(Edits.placeStrat(p.scene(), types[i], 0, 31.5 + 1.5 * i, 8.0, 64.6, 0.0));
|
||||
}
|
||||
System.out.println(
|
||||
"types: " + StratEdits.customNames(p) + ", start " + sx + "," + sy + "," + sz + " yaw " + start.ry());
|
||||
ProjectIO.save(p, out, cd);
|
||||
System.out.println("saved " + out);
|
||||
}
|
||||
}
|
||||
@@ -44,12 +44,13 @@ public final class StratsTour {
|
||||
var p = w.document().project();
|
||||
StratEdits.CustomType big = StratEdits.cloneOf(w.scene(), "Coin", "BigCoin");
|
||||
w.execute(TypeEdits.create(p, big));
|
||||
String asm = StratCompiler.compile(StratsGameLevel.PULSER);
|
||||
w.execute(TypeEdits.create(p, new StratEdits.CustomType("Pulser", null, "Pulser", asm, big.proto())));
|
||||
String asm = StratCompiler.compile(StratsGameLevel.TOUCHER, StratEdits.paramNames("Coin"));
|
||||
w.execute(TypeEdits.create(
|
||||
p, new StratEdits.CustomType("Toucher", null, "Toucher", asm, big.proto(), "Coin")));
|
||||
Strat st = w.scene().playerStart();
|
||||
double x = st.x() / 4096.0, y = st.y() / 4096.0, z = st.z() / 4096.0;
|
||||
w.execute(Edits.placeStrat(w.scene(), "Pulser", 0, x + 1, y, z + 2, 0.0));
|
||||
w.execute(Edits.placeStrat(w.scene(), "Pulser", 0, x - 1, y, z + 2, 0.0));
|
||||
w.execute(Edits.placeStrat(w.scene(), "Toucher", 0, x + 1, y, z + 2, 0.0));
|
||||
w.execute(Edits.placeStrat(w.scene(), "Toucher", 0, x - 1, y, z + 2, 0.0));
|
||||
w.execute(Edits.placeStrat(w.scene(), "BigCoin", 0, x, y, z + 3, 0.0));
|
||||
Strat last = w.scene().strats().get(w.scene().strats().size() - 2);
|
||||
w.selection().set(last, null);
|
||||
|
||||
@@ -50,7 +50,7 @@ class StratCompilerTest {
|
||||
assertFalse(view.contains("goto"), view);
|
||||
assertTrue(view.contains("strat Spinner:"), view);
|
||||
assertTrue(view.contains("while (v0 < 10.0) {\n turnLeft(4deg)\n v0 += 1.0\n yield\n}"), view);
|
||||
assertTrue(view.contains("if ((self.y > 2.0) && !self.animPlaying) {"), view);
|
||||
assertTrue(view.contains("if (self.y > 2.0 && !self.animPlaying) {"), view);
|
||||
assertTrue(
|
||||
view.contains(
|
||||
"} else if (self.distToPlayerSqXZ < 4.0) {\n break\n } else {\n call L_00b4()"),
|
||||
@@ -60,13 +60,176 @@ class StratCompilerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void numbersMatchTheAssembly() {
|
||||
String asm = StratCompiler.compile("strat N { v0 = 1.5\n v1 = 100.0\n v2 = 90deg\n v3 = -3 }");
|
||||
void plainIntegersAreUnits() {
|
||||
String asm = StratCompiler.compile(
|
||||
"strat N { v0 = 1.5\n v1 = 100\n v2 = 90deg\n v3 = -3\n v4 = 215r\n v5 = 0x10\n v6 = -7r }");
|
||||
assertTrue(asm.contains("pushShort 1.5\n"), asm);
|
||||
assertTrue(asm.contains("push 100.0\n"), asm);
|
||||
assertTrue(asm.contains("push 90deg\n"), asm);
|
||||
assertTrue(asm.contains("pushShort -3\n"), asm);
|
||||
assertTrue(asm.contains("pushShort -3.0\n"), asm);
|
||||
assertTrue(asm.contains("pushShort 215\n"), asm);
|
||||
assertTrue(asm.contains("pushShort 0x10\n"), asm);
|
||||
assertTrue(asm.contains("pushShort -7\n"), asm);
|
||||
assertTrue(asm.trim().endsWith("end"), asm);
|
||||
// the old rule: plain integers are raw words
|
||||
String old = StratCompiler.compile("use rawNumbers\nstrat N { v3 = -3\n v4 = 10 }");
|
||||
assertTrue(old.contains("pushShort -3\n") && old.contains("pushShort 10\n"), old);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scriptsCompiledWithTheOldRuleKeepTheirMeaning() {
|
||||
// generated before language version 2: no marker line behind the embedded source
|
||||
String asm = ";| strat N {\n;| v0 = 10\n;| }\n .string \"N\"\nN:\n end\n";
|
||||
String src = StratCompiler.embeddedSource(asm);
|
||||
assertTrue(src.startsWith(StratCompiler.RAW_NUMBERS), src);
|
||||
assertTrue(StratCompiler.compile(src).contains("pushShort 10\n"));
|
||||
// new output: the source comes back unchanged
|
||||
String v2 = StratCompiler.compile("strat N {\n v0 = 10\n}");
|
||||
assertEquals("strat N {\n v0 = 10\n}\n", StratCompiler.embeddedSource(v2));
|
||||
}
|
||||
|
||||
static String view(String script, String entry) {
|
||||
return StratPseudo.of(StratPseudoTest.listing(StratCompiler.compile(script), entry))
|
||||
.text();
|
||||
}
|
||||
|
||||
@Test
|
||||
void foldsConstants() {
|
||||
String asm = StratCompiler.compile("strat N { v0 = 2 * 3 + 0.5\n v1 = -(1 << 4)\n v2 = 90deg / 2 }");
|
||||
assertTrue(asm.contains("pushShort 6.5\n"), asm);
|
||||
assertTrue(asm.contains("push -16.0\n"), asm);
|
||||
assertTrue(asm.contains("push 512.0\n"), asm); // 45deg
|
||||
assertFalse(asm.contains("mul") || asm.contains("add\n") || asm.contains("shl"), asm);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortCircuitConditions() {
|
||||
String script =
|
||||
"""
|
||||
strat C {
|
||||
if (v0 > 2 && v1 || !v2) {
|
||||
turnLeft(4deg)
|
||||
}
|
||||
while (v3 < 3 && v4) {
|
||||
v3 += 1
|
||||
}
|
||||
}
|
||||
""";
|
||||
String asm = StratCompiler.compile(script);
|
||||
assertFalse(asm.contains(" and\n") || asm.contains(" or\n"), asm);
|
||||
String v = view(script, "C");
|
||||
assertFalse(v.contains("goto"), v);
|
||||
assertTrue(v.contains("if ((v0 > 2.0 && v1) || !v2) {\n turnLeft(4deg)\n}"), v);
|
||||
assertTrue(v.contains("while (v3 < 3.0 && v4) {\n v3 += 1.0\n}"), v);
|
||||
// value context: both sides evaluated (the and/or opcodes)
|
||||
assertTrue(StratCompiler.compile("strat D { v0 = v1 && v2 }").contains(" and\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void switchWithFallThroughAndDefault() {
|
||||
String script =
|
||||
"""
|
||||
strat S {
|
||||
loop {
|
||||
switch (v0) {
|
||||
case 1:
|
||||
v1 = 2
|
||||
break
|
||||
case 2:
|
||||
case 3:
|
||||
v1 = 100000
|
||||
default:
|
||||
v1 = 0
|
||||
}
|
||||
yield
|
||||
}
|
||||
}
|
||||
""";
|
||||
String asm = StratCompiler.compile(script);
|
||||
assertTrue(asm.contains(".switch 3\n .case 1.0, "), asm);
|
||||
String v = view(script, "S");
|
||||
assertFalse(v.contains("goto"), v);
|
||||
assertTrue(
|
||||
v.contains("case 1.0:\n v1 = 2.0\n break\n case 2.0:\n case 3.0:\n"
|
||||
+ " v1 = 100000.0\n default:\n v1 = 0\n }"),
|
||||
v);
|
||||
CompileExceptionCheck.fails("strat S { switch (v0) { case v1: end } }", "constant");
|
||||
CompileExceptionCheck.fails("strat S { switch (v0) { case 1: case 1: end } }", "duplicate");
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventHandlers() {
|
||||
String script =
|
||||
"""
|
||||
strat E {
|
||||
on touched {
|
||||
soundPlay(sound:6)
|
||||
remove
|
||||
}
|
||||
on event(0x40, 30) slot 3 {
|
||||
v0 += 1
|
||||
}
|
||||
loop { yield }
|
||||
}
|
||||
""";
|
||||
String asm = StratCompiler.compile(script);
|
||||
assertTrue(asm.contains("onEvent ev_1, 0\n"), asm);
|
||||
assertTrue(asm.contains("onEvent ev_2, 3\n"), asm);
|
||||
assertTrue(asm.indexOf(".event 4, 0, on_1") < asm.indexOf("\nE:\n"), "entries before the code: " + asm);
|
||||
String v = view(script, "E");
|
||||
assertTrue(v.contains("onEvent ev_0000, 0 // event type 4 → "), v);
|
||||
assertTrue(v.contains("soundPlay(sound:6)\nremove\n"), v);
|
||||
assertTrue(v.contains("v0 += 1.0\nreturn\n"), v);
|
||||
assertFalse(v.contains("goto"), v);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parameterNamesFromTheSchema() {
|
||||
String asm = StratCompiler.compile(
|
||||
"strat P { if (snapToFloor) { self.y = self.floorHeight } }", java.util.Map.of("snapToFloor", "v0"));
|
||||
assertTrue(asm.contains("pushVar v0\n"), asm);
|
||||
// the script's own alias wins
|
||||
asm = StratCompiler.compile(
|
||||
"strat P { alias snapToFloor = v4\n v0 = snapToFloor }", java.util.Map.of("snapToFloor", "v0"));
|
||||
assertTrue(asm.contains("pushVar v4\n"), asm);
|
||||
}
|
||||
|
||||
/** Old and new opcode names assemble to the same words (op-aliases.tsv). */
|
||||
@Test
|
||||
void renamedOpcodesKeepTheirOldNames() {
|
||||
StratAssembler.Layout l =
|
||||
new StratAssembler.Layout(16, java.util.List.of(), java.util.List.of(), java.util.List.of());
|
||||
int[] a = StratAssembler.assemble("x:\n flagEC10000 1\n flagEC2 1\n flagEC100 0\n hudReset2\n end", l)
|
||||
.words();
|
||||
int[] b = StratAssembler.assemble(
|
||||
"x:\n freezeOffCamera 1\n touchable 1\n noFloorPhysics 0\n hudResetNoop\n end", l)
|
||||
.words();
|
||||
org.junit.jupiter.api.Assertions.assertArrayEquals(a, b);
|
||||
assertEquals("freezeOffCamera", OpTable.get().byName("flagEC10000").name());
|
||||
assertEquals("freezeOffCamera", OpTable.get().alias("FLAGEC10000").name());
|
||||
assertTrue(StratCompiler.compile("strat A { flagEC10000<1>() }").contains("freezeOffCamera 1"));
|
||||
}
|
||||
|
||||
/** unit:Name resolves through the layout's unit names. */
|
||||
@Test
|
||||
void unitNames() {
|
||||
StratAssembler.Layout l = new StratAssembler.Layout(
|
||||
100, java.util.List.of(), java.util.List.of(), java.util.List.of(16, 40))
|
||||
.withUnitNames(java.util.Map.of("Other", 1));
|
||||
int[] w =
|
||||
StratAssembler.assemble("x:\n address unit:Other+0x8\n end", l).words();
|
||||
assertEquals(48, w[1]);
|
||||
StratAssembler.AsmException e = assertThrows(
|
||||
StratAssembler.AsmException.class, () -> StratAssembler.assemble("x:\n address unit:Nope\n end", l));
|
||||
assertTrue(e.getMessage().contains("unknown unit name"), e.getMessage());
|
||||
}
|
||||
|
||||
static final class CompileExceptionCheck {
|
||||
static void fails(String src, String part) {
|
||||
StratCompiler.CompileException e =
|
||||
assertThrows(StratCompiler.CompileException.class, () -> StratCompiler.compile(src));
|
||||
assertTrue(e.getMessage().contains(part), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -125,6 +125,50 @@ class StratPseudoTest {
|
||||
assertEquals(List.of(toList(li.words())), List.of(toList(back.words())));
|
||||
}
|
||||
|
||||
/** Jumps out of nested loops and to a return: labeled break/continue and return instead of goto. */
|
||||
@Test
|
||||
void multiLevelBreaksAndJumpsToReturn() {
|
||||
String src =
|
||||
"""
|
||||
Main:
|
||||
outer:
|
||||
pushVar v0
|
||||
jz skip
|
||||
inner:
|
||||
pushVar v1
|
||||
jz next
|
||||
jmp out ; break out of both loops
|
||||
next:
|
||||
pushVar v5
|
||||
jz c2
|
||||
jmp ret ; leaves both loops to the return
|
||||
c2:
|
||||
pushVar v2
|
||||
jz cont
|
||||
jmp outer ; continue the outer loop
|
||||
cont:
|
||||
jmpYield inner
|
||||
skip:
|
||||
jmpYield outer
|
||||
out:
|
||||
pushVar v3
|
||||
jz fin
|
||||
jmp ret
|
||||
fin:
|
||||
push 1.0
|
||||
popVar v4
|
||||
ret:
|
||||
return
|
||||
""";
|
||||
String t = StratPseudo.of(listing(src, "Main")).text();
|
||||
assertFalse(t.contains("goto"), t);
|
||||
assertTrue(t.contains("entry_0000: loop {"), t);
|
||||
assertTrue(t.contains("break entry_0000"), t);
|
||||
assertTrue(t.contains("continue entry_0000"), t);
|
||||
assertTrue(t.contains("if (v5) {\n return\n }"), t);
|
||||
assertTrue(t.contains("if (!(v3)) {\n v4 = 1.0\n}\nreturn"), t);
|
||||
}
|
||||
|
||||
static List<Integer> toList(int[] a) {
|
||||
return java.util.Arrays.stream(a).boxed().toList();
|
||||
}
|
||||
@@ -132,7 +176,7 @@ class StratPseudoTest {
|
||||
/** Over all units of the CD: balanced blocks, every remaining goto has its label, few gotos left. */
|
||||
@Test
|
||||
void everyUnitIsWellFormed() throws IOException {
|
||||
int gotos = 0, units = 0;
|
||||
int gotos = 0, units = 0, labeled = 0;
|
||||
Pattern g = Pattern.compile("goto (\\w+)");
|
||||
for (Path p : StratRoundTripTest.levels()) {
|
||||
StratRoundTripTest.Loaded l = StratRoundTripTest.load(p);
|
||||
@@ -144,7 +188,7 @@ class StratPseudoTest {
|
||||
StratPseudo ps = StratPseudo.of(li);
|
||||
units++;
|
||||
int open = 0;
|
||||
Set<String> labels = new HashSet<>();
|
||||
Set<String> labels = new HashSet<>(), loopNames = new HashSet<>();
|
||||
for (StratPseudo.Line x : ps.lines) {
|
||||
String s = x.text().replaceAll("\\s*//.*$", "");
|
||||
if (s.startsWith("}")) {
|
||||
@@ -157,6 +201,10 @@ class StratPseudoTest {
|
||||
if (s.matches("\\w+:")) {
|
||||
labels.add(s.substring(0, s.length() - 1));
|
||||
}
|
||||
Matcher ln = Pattern.compile("^(\\w+): (loop|do|while)").matcher(s.trim());
|
||||
if (ln.find()) {
|
||||
loopNames.add(ln.group(1));
|
||||
}
|
||||
}
|
||||
assertEquals(0, open, p + " unit " + u + ": unclosed block");
|
||||
for (StratPseudo.Line x : ps.lines) {
|
||||
@@ -165,11 +213,16 @@ class StratPseudoTest {
|
||||
gotos++;
|
||||
assertTrue(labels.contains(m.group(1)), p + " unit " + u + ": goto without label " + x.text());
|
||||
}
|
||||
Matcher b = Pattern.compile("(break|continue) (\\w+)").matcher(x.text());
|
||||
if (b.find()) {
|
||||
labeled++;
|
||||
assertTrue(loopNames.contains(b.group(2)), p + " unit " + u + ": no loop named in " + x.text());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.printf("script view: %d units, %d gotos%n", units, gotos);
|
||||
System.out.printf("script view: %d units, %d gotos, %d labeled break/continue%n", units, gotos, labeled);
|
||||
assertTrue(units >= 1400);
|
||||
assertTrue(gotos < 500, "gotos: " + gotos);
|
||||
assertTrue(gotos <= 40, "gotos: " + gotos);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user