strats: custom object types can be edited and deleted with undo (code rebuilt, objects follow their type), low-confidence opcodes re-checked (353 high, 2 low left), script view with do-while, while, break, continue, switch and else-if (gotos 11019 -> 400), first script compiler (structured script -> assembly, script stored with the type)
This commit is contained in:
1 parent
b35792a4ed
commit
8b78958306
21 files changed
+2332
-154
No files matched your search
@@ -77,6 +77,51 @@ empty template) opens the type editor:
|
||||
The new script is added to the level's code; the original type is not changed. Custom types are
|
||||
stored in the project (as text) and rebuilt every time it is opened.
|
||||
|
||||
### Editing and deleting your types
|
||||
|
||||
Select an object of a custom type (or pick the type in the Script panel's *Type* list):
|
||||
|
||||
* **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.
|
||||
* **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).
|
||||
|
||||
### Writing a script instead of assembly
|
||||
|
||||
Set **Language** in the type editor to **Script** to write the behaviour in a structured language
|
||||
that looks like the Script view:
|
||||
|
||||
```
|
||||
strat Pulser {
|
||||
alias size = v0
|
||||
setModel(model:5)
|
||||
size = 1.0
|
||||
loop {
|
||||
while (size < 4.0) { // grow to four times the size
|
||||
size = size + 0.05
|
||||
scale(size)
|
||||
yield // wait for the next frame
|
||||
}
|
||||
do { // and shrink back
|
||||
size = size - 0.05
|
||||
scale(size)
|
||||
yield
|
||||
} while (size > 1.0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Statements: `x = value`, `x += value`, `if (…) { } else { }`, `while (…) { }`, `loop { }`,
|
||||
`do { } while (…)`, `break`, `continue`, `yield`, `end`, `call name` (for `sub name { … }`).
|
||||
* 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`.
|
||||
* **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`.
|
||||
|
||||
Language reference: every instruction is one line, labels end with `:`, comments start with `;`.
|
||||
`push 4deg` / `turnLeft` pushes a number and lets an instruction use it; `jmpYield label` waits
|
||||
for the next frame and jumps. The full syntax and the list of all 434 instructions are in the
|
||||
@@ -137,14 +182,15 @@ circle:
|
||||
|
||||
## Limits
|
||||
|
||||
* Custom types cannot be deleted or edited in place yet – clone the custom type again with the
|
||||
changes and replace the placed objects.
|
||||
* 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 `goto` where the original jumps don't form nested blocks; the Assembly
|
||||
view is always exact.
|
||||
* 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.
|
||||
* 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: 259 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
+85
-8
@@ -9,7 +9,8 @@ the STPC chunk as **code units** (one per compiled source file; notes/editor-cro
|
||||
| `OpTable` | opcode/property names and docs (resources `ops.tsv`, `props.tsv`; notes/strat-opcodes.md) |
|
||||
| `StratListing` | unit → assembly text (+ per-line metadata: offsets, editable constants) |
|
||||
| `StratAssembler` | text → unit words; layout = where the unit goes + model/anim/unit offsets |
|
||||
| `StratPseudo` | read-only structured view (expressions, if/else, loops) |
|
||||
| `StratPseudo` | read-only structured view (expressions, if/else, loops, switch, break/continue) |
|
||||
| `StratCompiler` | script language → assembly (new behaviour) |
|
||||
| `Num` | literals: `42`, `0x2a`, `1.5` (20.12), `90deg` |
|
||||
|
||||
**Round trip (golden test `StratRoundTripTest`)**: disassemble → assemble is byte-identical for
|
||||
@@ -95,11 +96,23 @@ own-unit addresses), plus the model/animation/unit offsets of the target STPC fo
|
||||
|
||||
## Script view (`StratPseudo`)
|
||||
|
||||
Read-only. Stack code folded into expressions (`self.y = self.floorHeight + 204`), properties as
|
||||
`self.x`, `player.y`, `parent.v3`, `camera.…`; forward `jz/jnz` → `if {…}` (with `else` when the
|
||||
block ends in a forward `jmp`), backward `jmp`/`jmpYield` → `loop {… yield}`; blocks are only
|
||||
built when they nest, everything else stays `goto label`. Inline operands in `<>`:
|
||||
`spawn<3, 6, 0, 0, 0>(10.0, 204, 450, unit:35+0x28)`. Example (Coin):
|
||||
Read-only (not parsed back). Stack code folded into expressions (`self.y = self.floorHeight + 204`),
|
||||
properties as `self.x`, `player.y`, `parent.v3`, `camera.…`. Control flow is rebuilt from the jumps
|
||||
where the blocks nest:
|
||||
|
||||
| Jumps | Shown as |
|
||||
|---|---|
|
||||
| forward `jz`/`jnz` over a block (+ `jmp` over a second block) | `if (…) {…} else {…}`; an `if` that is the whole else part → `} else if (…) {`; an empty then-part is negated |
|
||||
| backward `jmp`/`jmpYield` | `loop {… yield}`; first statement `if (c) break` → `while (!c) {…}` |
|
||||
| backward conditional jump | `do {…} while (c)` (`/*yield*/` for jzYield/jnzYield; empty body: `do {} while (c) /*yield*/`) |
|
||||
| jump to the end / head of the innermost loop | `break` / `continue` (also as `if (c) break`) |
|
||||
| `switch` + `jmp end` with the cases in between | `switch (v) { case 1.0: … break … }` (`default:` when the jmp goes to a case-like block) |
|
||||
|
||||
Everything else stays `goto label` (mostly multi-level breaks out of nested loops and `||` chains
|
||||
that jump to a common far target). Over all 1461 units: **11 019 → 400 gotos** (2026-11-07;
|
||||
guarded by `StratPseudoTest`, which also checks balanced blocks and that every remaining goto has
|
||||
its label). Inline operands in `<>`: `spawn<3, 6, 0, 0, 0>(10.0, 204, 450, unit:35+0x28)`.
|
||||
Example (Coin):
|
||||
|
||||
```
|
||||
strat Coin:
|
||||
@@ -116,6 +129,58 @@ if (v0) {
|
||||
loop { turnLeft(5.625deg); yield }
|
||||
```
|
||||
|
||||
## Script language (`StratCompiler`) – writing NEW behaviour
|
||||
|
||||
A small structured language in the style of the script view, compiled to the assembly above (so
|
||||
everything the assembler checks still applies). Implemented slice (2026-11-07): strats, subroutines,
|
||||
aliases, assignments, expressions, if/else if/else, while, loop, do-while, break/continue, yield,
|
||||
calls of any opcode by name. Tests: `StratCompilerTest`; verified in game (village 1:
|
||||
`img/mods/strats_script_game.jpg`).
|
||||
|
||||
```
|
||||
// bobs one cell up and down and turns
|
||||
strat Bobber {
|
||||
alias base = v0 // .alias base = v0
|
||||
alias phase = v1
|
||||
setModel(model:5) // any opcode: name<inline operands>(stack arguments)
|
||||
base = self.y + 1.0
|
||||
phase = 0
|
||||
loop {
|
||||
phase = phase + 4deg
|
||||
self.y = base + sin(phase)
|
||||
turnLeft(4deg)
|
||||
yield // last statement of a loop → the loop's jmpYield back edge
|
||||
}
|
||||
}
|
||||
|
||||
sub wobble { // called with "call wobble", ends with return
|
||||
self.rotZ = sin(self.rotY) * 0.1
|
||||
}
|
||||
```
|
||||
|
||||
| Construct | Compiles to |
|
||||
|---|---|
|
||||
| `strat Name { … }` | `.string "Name"`, label `Name:`, `debugName "Name"`, body, `end` unless the body cannot fall through |
|
||||
| `sub name { … }` / `call name` / `return` | label, body, `return` / `call name` / `return` |
|
||||
| `x = e`, `x += e` (`-=`, `*=`, `/=`) | e, pop op of x; `x += 1.0` / `x -= 1.0` → address + `inc`/`dec` |
|
||||
| places | `vN`, alias, `gN`, `self.prop`, `player.prop`, `parent.prop`, `parent.vN`, `camera.`/`target.`/`target2.`/`boss.`/`dialog.prop` (props.tsv names or `propN`) |
|
||||
| literals | as in the assembly: `10.0` = ten, `10` = 10 raw units (1/4096), `90deg`, `0x10`; 16-bit values use `pushShort`, others `push` |
|
||||
| references | `model:N`, `anim:N`, `unit:N` (`address`), `sound:N` (`soundAddress`), `tex:N` (`push`) |
|
||||
| operators (low → high) | `||` `|` (or) · `&&` (and) · `&` · `==` `!=` · `<` `>` `<=` `>=` · `<<` `>>` · `+` `-` · `*` `/` · unary `-` `!` |
|
||||
| `op<a, b>(e1, e2)` | e1, e2, `op a, b`; the argument count must match the opcode's pops; a value-returning op used as a statement is followed by `pop` |
|
||||
| `if (c) {…} else if … else {…}` | `c; jz else; …; jmp end; else: …` |
|
||||
| `while (c) {…}` / `loop {…}` | head label, `c; jz end`, body, `jmp`/`jmpYield head` |
|
||||
| `do {…} while (c)` | body, `c; jnz head` |
|
||||
| `break` / `continue` / `yield` / `end` | `jmp end` / `jmp head` (do: the condition) / `jmpYield next` / `end` |
|
||||
|
||||
The generated assembly starts with the script as `;| ` comment lines, so the editor can show the
|
||||
script again (*Edit type…*). Comments: `//`, `/* */`; `;` and newlines separate statements.
|
||||
|
||||
**Next steps (not implemented)**: event handlers (`on touched { … }` → `.event` table + `onEvent`),
|
||||
`switch`, `spawn` with typed arguments, local names for parameters from the object schema, `for`,
|
||||
constant folding, typed numbers (int vs fixed) instead of the assembly's literal rules,
|
||||
short-circuit `&&`/`||` (now: both sides are evaluated, like the original `and`/`or` opcodes).
|
||||
|
||||
## Editing in the level editor (`editor.tools.strats`)
|
||||
|
||||
* **Script panel** (bottom dock, View → Panels → Script, Ctrl+Shift+K): unit chooser (all code units
|
||||
@@ -136,6 +201,15 @@ loop { turnLeft(5.625deg); yield }
|
||||
Verification: the merged STPC is disassembled again (no decode errors, entry decoded, every
|
||||
`address` operand of the new unit hits a model, animation set or code, and the new unit's
|
||||
listing re-assembles to the same words).
|
||||
* **Edit type… / Delete type…** (Script panel, Inspector *Script* section of a custom object):
|
||||
edit opens the same dialog with the type's source (script or assembly), *Apply* rebuilds all
|
||||
custom units (`TypeEdits.edit`, one undo step) and the placed objects move to the new entry.
|
||||
Delete warns with the number of objects that use the type and removes them with it
|
||||
(`TypeEdits.delete`: `Edits.delete` of the users + the type list change, one undo step).
|
||||
Creating is undoable too (`TypeEdits.create`), except when imported types follow the custom
|
||||
block (then appended as before).
|
||||
* **Language** (dialog): *Assembly* or *Script* (compiled by `StratCompiler`, see above);
|
||||
switching Script → Assembly shows the generated code.
|
||||
* **Load order** (`ProjectIO.LoadHook.order()`): constants of base units (−10) → imported types
|
||||
(CrossImport, 0) → constants of imported units and custom types (+10). Custom units are always
|
||||
last on load; if types were imported after a custom type in the session, the stored strat codes
|
||||
@@ -143,8 +217,11 @@ loop { turnLeft(5.625deg); yield }
|
||||
|
||||
## Limits
|
||||
|
||||
* Custom types cannot be removed or re-edited in place (clone again; the old unit stays as dead
|
||||
code until the project is recreated). Constants of custom types are changed through their source.
|
||||
* Custom types are edited and deleted as a whole list: the custom units are cut off the code and
|
||||
appended again (objects follow their type's new entry). Not possible while object types imported
|
||||
after the custom ones sit behind them in the code (save and reopen the project first; on load the
|
||||
custom types always come last). A custom type that addresses another custom type's unit with
|
||||
`unit:N` breaks when that index changes. Constants of custom types are changed through their source.
|
||||
* `pushShort` holds only −8.0…7.99; bigger values need a source edit (`push`).
|
||||
* The script view is a reading aid (gotos where blocks do not nest; expressions assume the
|
||||
documented pop/push counts; ops with variable stack use are shown raw).
|
||||
|
||||
+47
-36
@@ -10,8 +10,19 @@ properties). Language/syntax: `notes/strat-language.md`. Disassembler/assembler:
|
||||
of the word, 0x45+ "long" with inline operand words). All of them have a name and a one-line
|
||||
semantics from reading every handler (headless Ghidra decompile of the two handler tables
|
||||
0x5796bc / 0x578ff4, plus the helpers they call).
|
||||
* Confidence: **329 high, 71 medium, 34 low**. Of the **222 opcodes the 34 level WADs use**: 170 high,
|
||||
37 medium, 15 low; high-confidence opcodes cover **96.8 %** of the 556314 instructions in the levels.
|
||||
* Confidence: **353 high, 79 medium, 2 low**. Of the **222 opcodes the 34 level WADs use**: 177 high,
|
||||
44 medium, 1 low; high-confidence opcodes cover **97.3 %** of the 556314 instructions in the levels.
|
||||
* **Low-confidence review (2026-11-07)**: all 34 former low opcodes re-read (handler table entry checked
|
||||
against the exe, Ghidra decompile of handler + callees, `objdump` of the exe where Ghidra's C was
|
||||
incomplete – e.g. it drops the bone index of 0xc7 and every reader of +0x114), and cross-checked
|
||||
with their uses in the WADs. Findings: 0x107 setAnimSpeed is the per-frame animation step (sub-frame
|
||||
accumulator +0x116, 1.0 = one frame per game frame); 0x2f freezes a strat while it is off-camera
|
||||
(with the camera pass's +0xEC 0x8000); 0x28 is read by the camera scripts to look at the strat
|
||||
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.
|
||||
* **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).
|
||||
@@ -100,30 +111,30 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x23 | `error_0x23` | - | 0 | 0 | | high | stCollide_AlienVarAddress | Prints "stCollide_AlienVarAddress" not-implemented error; unused. <sub>54d220</sub> |
|
||||
| 0x24 | `pushTarget2Prop` | prop | 0 | 1 | | medium | stTarget_AlienVar | Pushes property n of the second target strat (DAT_006D9E34, set by 0xca), 0 if none; unused. <sub>552bd0</sub> |
|
||||
| 0x25 | `pushTarget2PropAddr` | prop | 0 | 1 | | medium | stTarget_AlienVarAddress | Pushes address of target2 property n (0 if none); unused. <sub>552c00</sub> |
|
||||
| 0x26 | `pushBossProp` | prop | 0 | 1 | | low | stBoss_AlienVar | Pushes property n of the strat in DAT_006D9E24 (set by 0xef), no null check; unused. <sub>552b10</sub> |
|
||||
| 0x27 | `pushBossPropAddr` | prop | 0 | 1 | | low | stBoss_AlienVarAddress | Pushes address of property n of the DAT_006D9E24 strat; unused. <sub>552b30</sub> |
|
||||
| 0x28 | `pushDialogProp` | prop | 0 | 1 | 1678 | low | stDialog_AlienVar | Pushes property n of the strat in DAT_006D9E2C (set by 0x113, used by NPC/puzzle strats), 0 if none. <sub>552cb0</sub> |
|
||||
| 0x29 | `pushDialogPropAddr` | prop | 0 | 1 | | low | stDialog_AlienVarAddress | Pushes address of property n of the DAT_006D9E2C strat (0 if none); unused. <sub>552ce0</sub> |
|
||||
| 0x26 | `pushBossProp` | prop | 0 | 1 | | high | stBoss_AlienVar | Pushes property n of the boss strat DAT_006D9E24 (set by 0xef isBoss) via the getter FUN_00550E60; no null check; unused in the levels. <sub>552b10</sub> |
|
||||
| 0x27 | `pushBossPropAddr` | prop | 0 | 1 | | high | stBoss_AlienVarAddress | Pushes the address of property n of the boss strat DAT_006D9E24 (setter FUN_005509F0 returns the address); no null check; unused. <sub>552b30</sub> |
|
||||
| 0x28 | `pushDialogProp` | prop | 0 | 1 | 1678 | high | stDialog_AlienVar | Pushes property n of the strat registered by 0x113 (DAT_006D9E2C: the NPC/puzzle/door that a cut-scene is about), 0 if none. WAD use: Camera and CutSceneCameraDirectional read its x/y/z/rotY to look at it (1585 of 1678 uses). <sub>552cb0</sub> |
|
||||
| 0x29 | `pushDialogPropAddr` | prop | 0 | 1 | | high | stDialog_AlienVarAddress | Pushes the address of property n of the DAT_006D9E2C strat (0 if none); unused. <sub>552ce0</sub> |
|
||||
| 0x2a | `shadow` | imm | 0 | 0 | 887 | medium | stShadow | arg!=0 sets, 0 clears +0xE8 bit 0x20 (shadow on; shadow pass requires it plus size from 0x59 and texture from 0x5a). <sub>54e8e0</sub> |
|
||||
| 0x2b | `jmpYield` | rel | 0 | 0 | 2958 | high | - | Jumps to next+arg*4 and yields this frame (loop back-edge / wait one frame). <sub>553da0</sub> |
|
||||
| 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 | low | - | arg!=0 sets, 0 clears +0xEC bit 0x10000 (with 0x8000 both set the strat skips player-distance updates; property 0xf2 reads it); meaning unclear. <sub>54f720</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> |
|
||||
| 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 | low | - | arg sets/clears +0xEC bit 2 (checked by collision/floor code via +0xC0, readable as property 0x1f). <sub>54e090</sub> |
|
||||
| 0x32 | `flagE8_80000000` | imm | 0 | 0 | | low | - | arg sets/clears +0xE8 bit 31; unused. <sub>54dd90</sub> |
|
||||
| 0x33 | `flagEC100` | imm | 0 | 0 | 60 | low | - | arg sets/clears +0xEC bit 0x100 (tested in the collision response, skips floor handling; used by drops/projectiles/rollercoaster). <sub>54dd30</sub> |
|
||||
| 0x34 | `flagEC200000` | imm | 0 | 0 | 50 | low | - | arg sets/clears +0xEC bit 0x200000 (used by NPC helpers/canoe/llama). <sub>54dd60</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> |
|
||||
| 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> |
|
||||
| 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> |
|
||||
| 0x37 | `popPlayerProp` | playerProp | 1 | 0 | 1131 | high | - | Pops value and stores it into player property n (discarded if no player). <sub>553920</sub> |
|
||||
| 0x38 | `popCameraProp` | prop | 1 | 0 | 564 | high | - | Pops value into camera strat property n (discarded if none). <sub>553950</sub> |
|
||||
| 0x39 | `popTargetProp` | prop | 1 | 0 | 186 | medium | - | Pops value into camera target (DAT_006D9E30) property n (discarded if none). <sub>553980</sub> |
|
||||
| 0x3a | `popTarget2Prop` | prop | 1 | 0 | | medium | - | Pops value into DAT_006D9E34 strat property n (discarded if none); unused. <sub>5539b0</sub> |
|
||||
| 0x3b | `popDialogProp` | prop | 1 | 0 | | low | - | Pops value into DAT_006D9E2C strat property n (discarded if none); unused. <sub>5539e0</sub> |
|
||||
| 0x3b | `popDialogProp` | prop | 1 | 0 | | high | - | Pops a value into property n of the DAT_006D9E2C strat (discarded if none); unused. <sub>5539e0</sub> |
|
||||
| 0x3c | `error_0x3c` | - | 0 | 0 | | high | - | Prints "stCollide_AlienVar_Equals" not-implemented error; unused. <sub>54d240</sub> |
|
||||
| 0x3d | `popBossProp` | prop | 1 | 0 | | low | - | Pops value into DAT_006D9E24 strat property n (no null check); unused. <sub>553a10</sub> |
|
||||
| 0x3d | `popBossProp` | prop | 1 | 0 | | high | - | Pops a value into property n of the boss strat DAT_006D9E24 (no null check); unused. <sub>553a10</sub> |
|
||||
| 0x3e | `popParentVar` | var | 1 | 0 | 204 | high | - | Pops value into variable n of the +0xC strat. <sub>553a40</sub> |
|
||||
| 0x3f | `popParentProp` | prop | 1 | 0 | | high | - | Pops value into property n of the +0xC strat; unused. <sub>553a60</sub> |
|
||||
| 0x40 | `popProp` | prop | 1 | 0 | 15730 | high | - | Pops value into own property n (via FUN_005509F0 address). <sub>553a90</sub> |
|
||||
@@ -197,15 +208,15 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x84 | `collHeight` | - | 0 | 0 | | high | stCollHeight | Unimplemented: logs "stCollheight" only. <sub>54d300</sub> |
|
||||
| 0x85 | `collExtent` | - | 1 | 0 | | medium | stCollextent | pops extent; stores its low 16 bits raw into short +0xC4. <sub>54e930</sub> |
|
||||
| 0x86 | `collView` | - | 0 | 0 | | high | stCollView | Unimplemented: logs "stCollview" only. <sub>54d320</sub> |
|
||||
| 0x87 | `nop_0x87` | - | 0 | 0 | | low | stCollPoints | Shared no-op handler (0x426500); unused. Slot of Croc2 stCollPoints. <sub>426500</sub> |
|
||||
| 0x88 | `nop_0x88` | - | 0 | 0 | | low | stCollSetPoint | Shared no-op handler (0x426500); unused. Slot of Croc2 stCollSetPoint. <sub>426500</sub> |
|
||||
| 0x87 | `nop_0x87` | - | 0 | 0 | | high | stCollPoints | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Slot of Croc2 stCollPoints. <sub>426500</sub> |
|
||||
| 0x88 | `nop_0x88` | - | 0 | 0 | | high | stCollSetPoint | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Slot of Croc2 stCollSetPoint. <sub>426500</sub> |
|
||||
| 0x89 | `createTrigger` | - | 0 | 0 | 45 | medium | stCreateTrigger | Sets flag 0x40 in +0xE8 (strat becomes a trigger/touchable, used by pickups like Health, Carrot). <sub>54e4c0</sub> |
|
||||
| 0x8a | `killTrigger` | - | 0 | 0 | | medium | stKillTrigger | Clears flags 0x40 and 0x4000000 in +0xE8 (undoes createTrigger). <sub>54e4b0</sub> |
|
||||
| 0x8b | `wait` | - | 1 | 0 | 1671 | high | stWait | pops frames (fixed, >>12) into wait counter short +0xFE and sets flag 0x2 in +0xE8 (strat sleeps until the counter runs out). <sub>54fb40</sub> |
|
||||
| 0x8c | `hold` | - | 0 | 0 | 110 | medium | stHold | Sets flags 0x12 (0x2 wait + 0x10 stopped) in +0xE8: suspends the strat indefinitely until released. <sub>54fce0</sub> |
|
||||
| 0x8d | `release` | - | 0 | 0 | | high | stRelease | Unimplemented: logs "stRelease" only. <sub>54d340</sub> |
|
||||
| 0x8e | `remove` | - | 0 | 0 | 985 | high | stRemove | Sets flags 0x6 in +0xE8 (yield + kill); same handler as 0xb4 end. <sub>5535e0</sub> |
|
||||
| 0x8f | `nop_0x8f` | - | 0 | 0 | | low | stMapRemove | Shared no-op handler (0x426500); unused. Slot of Croc2 stMapRemove. <sub>426500</sub> |
|
||||
| 0x8f | `nop_0x8f` | - | 0 | 0 | | high | stMapRemove | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Slot of Croc2 stMapRemove. <sub>426500</sub> |
|
||||
| 0x90 | `mapAdd` | - | 0 | 0 | | high | stMapAdd | Unimplemented: logs "stMapadd" only. <sub>54d360</sub> |
|
||||
| 0x91 | `mapReplace` | - | 0 | 0 | | high | stMapReplace | Unimplemented: logs "stMapreplace" only. <sub>54d380</sub> |
|
||||
| 0x92 | `activated` | - | 0 | 0 | | medium | stActivated | Sets bit 0x8 in byte +0x44 of the strat's map-instance record (+0xF0): marks the map object as activated (persistent state). <sub>54db40</sub> |
|
||||
@@ -218,8 +229,8 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x99 | `soundStop` | - | 1 | 0 | 209 | high | stSoundStop | pops sound channel; stops that channel if owned by this strat, or (channel <0/invalid) all sounds of this strat. <sub>54e1c0</sub> |
|
||||
| 0x9a | `musicPlay` | - | 1 | 0 | 108 | high | stCdPlay | pops music track index (fixed, >>12); if different from the current track (0x586430) starts that ASF music stream (FUN_00546b70). <sub>54d4c0</sub> |
|
||||
| 0x9b | `streamPlay` | - | 0 | 0 | | high | - | Unimplemented: logs "stStreamPlay" only. <sub>54d3e0</sub> |
|
||||
| 0x9c | `speechSetPos` | - | 1 | 0 | 31 | low | - | pops a value (fixed, >>12) into global 0x6DA2B8 (meaning unknown, likely speech volume/range) and moves the speech/voice stream (handle 0x6D94B4, started by '#' text) to this strat's position. <sub>54d530</sub> |
|
||||
| 0x9d | `discard_0x9d` | - | 1 | 0 | 28 | low | stMidiVolume | pops a value (>>12) and passes it to the no-op debug print; effectively drop. Croc2 slot midiVolume/cdFade. <sub>54d4f0</sub> |
|
||||
| 0x9c | `speechSetPos` | - | 1 | 0 | 31 | high | - | Pops a value into global 0x6DA2B8 (written only, never read: no effect) and moves the speech/voice channel (DAT_006D94A0, started by "#" texts) to this strat's position (+0x30..0x38), so the voice plays from the speaker. <sub>54d530</sub> |
|
||||
| 0x9d | `discard_0x9d` | - | 1 | 0 | 28 | high | stMidiVolume | Pops a value (>>12) and passes it to the empty function 0x426500: a pure drop on PC. Croc2 slot midiVolume/cdFade. <sub>54d4f0</sub> |
|
||||
| 0x9e | `speechStop` | - | 0 | 0 | 141 | high | stMidiStop | Stops the current speech/voice stream (handle 0x6D94B4) if one is playing and resets the handle to -1. <sub>54d510</sub> |
|
||||
| 0x9f | `streamQueue` | - | 0 | 0 | | high | stMidiQueue | Unimplemented: logs "stStreamQueue" only. <sub>54d400</sub> |
|
||||
| 0xa0 | `isLight` | - | 0 | 0 | | high | stIsLight | Unimplemented: logs "stIsLight" only. <sub>54d420</sub> |
|
||||
@@ -239,7 +250,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0xae | `cos` | - | 1 | 1 | 3149 | high | stCos | pops angle; pushes cos (table 0x574318 offset by quarter turn 0x400). <sub>54e810</sub> |
|
||||
| 0xaf | `not` | - | 1 | 1 | 4952 | high | stNot | pops x; pushes 1 if x==0 else 0. <sub>54fc20</sub> |
|
||||
| 0xb0 | `pop` | - | 1 | 0 | | high | stPop | Drops the top stack value (stack pointer +0x104 -= 4). <sub>553dd0</sub> |
|
||||
| 0xb1 | `stkCmp` | imm | 0 | 1 | | low | stStkCmp | Peeks (does not pop) the top stack entry, dereferences it as a pointer and pushes imm minus the pointed-to int (compare helper). <sub>553de0</sub> |
|
||||
| 0xb1 | `stkCmp` | imm | 0 | 1 | | high | stStkCmp | Reads one inline word imm (pc += 4), peeks the top stack entry (no pop), dereferences it as an int pointer and pushes imm − *top. Unused in the levels. <sub>553de0</sub> |
|
||||
| 0xb2 | `address` | addr | 0 | 1 | 7890 | high | stAddress | Patches itself into 0x45 (push immediate) after resolving the operand to an absolute address (negative = import table 0x6DA324, else + STPC base 0x6D9DBC) and pushes it (code/model/anim address). <sub>553630</sub> |
|
||||
| 0xb3 | `return` | - | 1 | 0 | 4974 | high | stReturn | pops code address and jumps there (pc = popped). <sub>5537d0</sub> |
|
||||
| 0xb4 | `end` | - | 0 | 0 | 2006 | high | stEndStrat | Sets flags 0x6 in +0xE8 (yield + kill): ends the strat. Same handler as 0x8e. <sub>5535e0</sub> |
|
||||
@@ -249,36 +260,36 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0xb8 | `bitwiseAnd` | - | 2 | 1 | 6814 | high | stBitwiseAnd | pops b, a; pushes a & b. <sub>552e30</sub> |
|
||||
| 0xb9 | `discard_0xb9` | - | 1 | 0 | | medium | - | Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 ext-var slots). <sub>554fb0</sub> |
|
||||
| 0xba | `discard_0xba` | - | 1 | 0 | | medium | - | Shared stub 0x554fb0: pops one value and discards it. Unused. <sub>554fb0</sub> |
|
||||
| 0xbb | `nop_0xbb` | - | 0 | 0 | | low | - | Shared no-op handler (0x426500); unused. <sub>426500</sub> |
|
||||
| 0xbb | `nop_0xbb` | - | 0 | 0 | | high | - | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. <sub>426500</sub> |
|
||||
| 0xbc | `notEqual` | - | 2 | 1 | 2576 | high | stNotEqual | pops b, a; pushes 1 if a != b else 0. <sub>54fbd0</sub> |
|
||||
| 0xbd | `greaterEqual` | - | 2 | 1 | 1169 | high | stGreaterEqual | pops b, a; pushes 1 if a >= b else 0. <sub>553b80</sub> |
|
||||
| 0xbe | `lessEqual` | - | 2 | 1 | 2129 | high | stLessEqual | pops b, a; pushes 1 if a <= b else 0. <sub>553b50</sub> |
|
||||
| 0xbf | `rnd` | - | 1 | 1 | 4990 | high | stRnd | pops n; pushes random value (two rand() halves) modulo n, 0 if n==0; result is in the same units as n (fixed in, fixed out). <sub>54f960</sub> |
|
||||
| 0xc0 | `loseHeart` | - | 0 | 0 | 219 | high | stLoseHeart | In game state 3, unless player invulnerable (0x584710 bit 0): health 0x584E70 -= 1.0, or if already 0 sets 0x584638 = 2 (player dies); refreshes HUD (FUN_004268c0(1)). <sub>54f5c0</sub> |
|
||||
| 0xc1 | `nop_0xc1` | - | 0 | 0 | | low | stResettoCheckPoint | Shared no-op handler (0x426500); unused. Croc2 slot stResettoCheckPoint. <sub>426500</sub> |
|
||||
| 0xc1 | `nop_0xc1` | - | 0 | 0 | | high | stResettoCheckPoint | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stResettoCheckPoint. <sub>426500</sub> |
|
||||
| 0xc2 | `forceCollision` | - | 0 | 0 | 1619 | medium | stForceCollision | Runs world collision for this strat (FUN_00403020 with level geometry): updates floor/ceiling/wall info in the collision block +0xC0 (ground height, surface type). <sub>54e9c0</sub> |
|
||||
| 0xc3 | `turnFromPlayerY` | - | 1 | 0 | | medium | stTurnFromPlayerY | pops max turn step (angle, 0x1000000 = full turn); turns yaw +0x24 toward the direction from the player (0x6D9E1C) to this strat, i.e. away from the player, by at most step. <sub>54f1c0</sub> |
|
||||
| 0xc4 | `rumble` | - | 2 | 0 | 93 | low | stRumble | Shared stub 0x555090: pops two values and discards them (PSX pad rumble/vibrate not implemented on PC). <sub>555090</sub> |
|
||||
| 0xc5 | `suspendIfTooFar` | - | 1 | 0 | 232 | low | stSuspendIfTooFar | Shared stub 0x554fb0: pops one value (likely distance) and discards it. <sub>554fb0</sub> |
|
||||
| 0xc4 | `rumble` | - | 2 | 0 | 93 | high | stRumble | Shared stub 0x555090: pops two values and discards them (PSX pad rumble is not implemented on PC). <sub>555090</sub> |
|
||||
| 0xc5 | `suspendIfTooFar` | - | 1 | 0 | 232 | high | stSuspendIfTooFar | Shared stub 0x554fb0: pops one value (distance) and discards it – no effect on PC (op 0x2f gives the off-camera freeze instead). <sub>554fb0</sub> |
|
||||
| 0xc6 | `collisionBone` | - | 2 | 0 | 90 | medium | stCollisionBone | pops value (short), then bone id (fixed, >>12); sets/adds entry {id, value} in the bone table at +0xC8 (count +0xC6). <sub>54e6c0</sub> |
|
||||
| 0xc7 | `useBone` | - | 1 | 0 | 345 | low | stUseBone | pops one value (ignored); if a model/anim is set (+0x14) rebuilds the rotation matrix and stores a transformed point (matrix * local vector + translation +0x94) into +0xD8/+0xDC/+0xE0. <sub>54e4d0</sub> |
|
||||
| 0xc7 | `useBone` | - | 1 | 0 | 345 | high | stUseBone | Pops bone n (>>12); if the strat has an animated model (+0x14) it takes bone n of the current animation frame (+0x110; anim+0x1C frame table, 64 bytes per bone, offset +0x30..0x38 = bone position), rotates it by the strat's rotation matrix and adds the position: world point in +0xD8/+0xDC/+0xE0 = properties boneX/Y/Z. (The Ghidra C output drops the bone index; read from the assembly.) Used to spawn effects at hands/mouths. <sub>54e4d0</sub> |
|
||||
| 0xc8 | `isCamera` | - | 0 | 0 | 34 | high | stIsCamera | Registers this strat as the camera (global 0x6D9E28) and sets flag 0x8000000 in +0xE8. <sub>5500f0</sub> |
|
||||
| 0xc9 | `lookAtMe` | - | 0 | 0 | 86 | high | stLookatme | Sets camera look-at target global 0x6D9E30 = this strat. <sub>5500e0</sub> |
|
||||
| 0xca | `lookAtMe2` | - | 0 | 0 | | medium | stLookatme | Sets secondary look-at global 0x6D9E34 = this strat. Unused. <sub>5500d0</sub> |
|
||||
| 0xcb | `pushCamera` | - | 0 | 0 | | high | stPushCamera | Pushes current camera (0x6D9E28) and look-at (0x6D9E30) pair onto the camera stack 0x6D9DCC (index 0x6D9E40). <sub>54dbb0</sub> |
|
||||
| 0xcc | `popCamera` | - | 0 | 0 | | high | stPopCamera | If the camera stack is not empty, restores camera and look-at from it and increments camera-change counter 0x6D9D34. <sub>54db00</sub> |
|
||||
| 0xcd | `nop_0xcd` | - | 0 | 0 | | low | stResetCamerapos | Shared no-op handler (0x426500); unused. Croc2 slot stResetCamerapos. <sub>426500</sub> |
|
||||
| 0xcd | `nop_0xcd` | - | 0 | 0 | | high | stResetCamerapos | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stResetCamerapos. <sub>426500</sub> |
|
||||
| 0xce | `gainHeart` | - | 0 | 0 | 94 | high | stGainHeart | Health 0x584E70 += 1.0 if below max 0x584E6C; refreshes HUD (FUN_004268c0(1)). <sub>54f650</sub> |
|
||||
| 0xcf | `nop_0xcf` | - | 0 | 0 | | low | stGainHeartPot | Shared no-op handler (0x426500); unused. Croc2 slot stGainHeartPot. <sub>426500</sub> |
|
||||
| 0xcf | `nop_0xcf` | - | 0 | 0 | | high | stGainHeartPot | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stGainHeartPot. <sub>426500</sub> |
|
||||
| 0xd0 | `addInv` | imm | 0 | 0 | | medium | stAddInv | Adds inline item to inventory via FUN_00426860(item,1,1,0,0), which is a stub returning 0 on PC (no effect). <sub>54da00</sub> |
|
||||
| 0xd1 | `nop_0xd1` | - | 0 | 0 | | low | stGainCrystal | Shared no-op handler (0x426500); unused. Croc2 slot stGainCrystal. <sub>426500</sub> |
|
||||
| 0xd2 | `discard_0xd2` | - | 1 | 0 | | low | stCutscene | Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 slot stCutscene). <sub>554fb0</sub> |
|
||||
| 0xd1 | `nop_0xd1` | - | 0 | 0 | | high | stGainCrystal | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stGainCrystal. <sub>426500</sub> |
|
||||
| 0xd2 | `discard_0xd2` | - | 1 | 0 | | high | stCutscene | Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 slot stCutscene). <sub>554fb0</sub> |
|
||||
| 0xd3 | `inventory` | imm | 0 | 1 | | medium | stInventory | Queries inline item via stub FUN_00426860 and pushes result<<12 (always 0 on PC). <sub>54d9d0</sub> |
|
||||
| 0xd4 | `debugName` | imm,count | 0 | 0 | 1804 | high | stDebugname | Strat name: skips the first inline word, reads the byte length of the following name string, sets the name pointer (+0x18) to that string and jumps past it. <sub>553ef0</sub> |
|
||||
| 0xd5 | `soundPlay` | - | 1 | 0 | 1835 | high | stSoundPlay | pops sound id; plays it on this strat (FUN_00546170); outside game states 3/4 first sets game state 0x5846E4 = 5. <sub>54e230</sub> |
|
||||
| 0xd6 | `soundPlayAss` | - | 1 | 1 | 288 | high | stSoundPlayass | pops sound id; in game states 3/4 plays it and pushes the channel (raw int), else pushes -1. <sub>54e1e0</sub> |
|
||||
| 0xd7 | `soundAddress` | sound | 0 | 1 | 2996 | high | stSoundAddress | Pushes the inline sound id (same handler as 0x45 push immediate). <sub>5535f0</sub> |
|
||||
| 0xd8 | `nop_0xd8` | - | 0 | 0 | | low | stOnground | Shared no-op handler (0x426500); unused. Croc2 slot stOnground/stObjectFallslow. <sub>426500</sub> |
|
||||
| 0xd8 | `nop_0xd8` | - | 0 | 0 | | high | stOnground | Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stOnground/stObjectFallslow. <sub>426500</sub> |
|
||||
| 0xd9 | `collisionOffset` | - | 3 | 0 | 403 | high | stCollisionOffset | pops z, y, x; stores low 16 bits into shorts +0xD0, +0xCE, +0xCC (collision centre offset). <sub>54e050</sub> |
|
||||
| 0xda | `abs` | - | 1 | 1 | 1847 | high | stAbs | pops x; pushes \|x\|. <sub>5508d0</sub> |
|
||||
| 0xdb | `pickup` | imm | 0 | 0 | | medium | stPickup | inline flag: nonzero sets bit 0x200 in +0xEC, zero clears it (strat can be picked up/carried). <sub>54dd00</sub> |
|
||||
@@ -304,7 +315,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0xef | `isBoss` | - | 0 | 0 | | high | stIsboss | registers this strat as the boss: DAT_006d9e24 = self <sub>550130</sub> |
|
||||
| 0xf0 | `topSay` | text | 0 | 0 | 1308 | high | stTopsay | shows dialog text (inline text index) in the top text box: sets DAT_006d9858=1 and formats via FUN_0040d4c0 <sub>40d480</sub> |
|
||||
| 0xf1 | `getParentPos` | - | 0 | 0 | 48 | high | stGetparentpos | copies parent's (+0xC) orientation+position block (+0x20..+0x3C, 8 words) into this strat <sub>54e030</sub> |
|
||||
| 0xf2 | `setDrawMode` | - | 1 | 0 | | low | - | pops mode (raw int): 1 sets flag 0x80000 in +0xEC, 2 sets 0x100000, other clears both (exact effect of the bits unknown); unused <sub>54fe40</sub> |
|
||||
| 0xf2 | `setDrawMode` | - | 1 | 0 | | low | - | Pops mode (raw int): 1 sets +0xEC 0x80000, 2 sets 0x100000, anything else clears both; no reader of these two bits found in the exe; unused in the levels. <sub>54fe40</sub> |
|
||||
| 0xf3 | `afterBoss` | - | 0 | 0 | | high | stAfterboss | if no boss (DAT_006d9e24) rewinds pc and yields (waits); else relinks this strat in the process list right after the boss <sub>553290</sub> |
|
||||
| 0xf4 | `afterPlayer` | - | 0 | 0 | 144 | high | stAfterPlayer | if no player (DAT_006d9e1c) rewinds pc and yields (waits); else relinks this strat in the process list right after the player <sub>553380</sub> |
|
||||
| 0xf5 | `beforePlayer` | - | 0 | 0 | 22 | high | stBeforePlayer | if no player rewinds pc and yields (waits); else relinks this strat in the process list right before the player <sub>553300</sub> |
|
||||
@@ -325,7 +336,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x104 | `moveDownQ` | - | 1 | 0 | 314 | high | stMoveDownq | pops distance (fixed); y(+0x34) -= d <sub>550940</sub> |
|
||||
| 0x105 | `forcePlayerDist` | - | 0 | 0 | | high | stForcePlayerdist | recomputes the cached squared player distance +0x148 (coords >>6) and clears +0x1C; 0x200000 if no player; unused <sub>54e950</sub> |
|
||||
| 0x106 | `nop_0x106` | - | 0 | 0 | | high | stShadeType? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
|
||||
| 0x107 | `setAnimSpeed` | - | 1 | 0 | 645 | low | stSetanimSpeed? | pops value and stores its low 16 bits (raw, not >>12) in halfword +0x114 (animation timing field, zeroed when the anim frame is set) <sub>54d880</sub> |
|
||||
| 0x107 | `setAnimSpeed` | - | 1 | 0 | 645 | high | stSetanimSpeed? | Pops the animation speed (20.12, 1.0 = one animation frame per game frame) into halfword +0x114; while animating (+0xE8 bit 8) the frame loop adds it to the sub-frame counter +0x116 and advances the frame (+0x110) each time it passes 1.0 (0x54c630). WAD use: 1.0, 0.25, 1.5 and computed values. <sub>54d880</sub> |
|
||||
| 0x108 | `nop_0x108` | - | 0 | 0 | | high | stCheckleveldoor? | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
|
||||
| 0x109 | `nop_0x109` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
|
||||
| 0x10a | `nop_0x10a` | - | 0 | 0 | | high | - | no-op handler FUN_00426500 (does nothing, no inline operands); unused <sub>426500</sub> |
|
||||
@@ -392,7 +403,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 | | low | - | Calls HUD/panel init FUN_004268c0(2): re-initialises the on-screen counter panels (coins, lives, etc.) for the current level. <sub>54f700</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> |
|
||||
| 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> |
|
||||
@@ -410,7 +421,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x159 | `nop_0x159` | - | 0 | 0 | | high | - | Empty handler FUN_00426500 (shared no-op); does nothing. <sub>426500</sub> |
|
||||
| 0x15a | `setPosToPlayer` | - | 0 | 0 | 27 | high | - | Copies the player's position x/y/z (+0x30..+0x38) and +0x3C into the strat (player strat = 0x6D9E1C). <sub>554480</sub> |
|
||||
| 0x15b | `setPosToPlayerXZ` | - | 0 | 0 | | high | - | Copies the player's x and z (+0x30, +0x38) into the strat; y unchanged. <sub>5544b0</sub> |
|
||||
| 0x15c | `setShadowHeight` | - | 1 | 0 | 1 | low | - | pops v; stores it in strat +0x14C, used by the shadow renderer as the shadow/ground height override (-0x1000 = use default 0x586424). <sub>5544d0</sub> |
|
||||
| 0x15c | `setShadowHeight` | - | 1 | 0 | 1 | medium | - | Pops v into +0x14C, the water/floor height limit used by the shadow pass (FUN_00555140: -0x1000 = use the camera-cell floor height DAT_00586424, prop camFloorHeight); the shadow is skipped when the floor under the strat lies below it. One use (SinkingStatuePad). <sub>5544d0</sub> |
|
||||
| 0x15d | `packRgb` | - | 3 | 1 | 98 | high | - | pops c (top), b, a (fixed 0..255); pushes (c<<16)\|(b<<8)\|a as a raw packed colour int (push order r,g,b gives 0x00BBGGRR). <sub>5544f0</sub> |
|
||||
| 0x15e | `trailCreate` | - | 2 | 1 | 6 | medium | - | pops vertB (top), vertA (fixed vertex indices of the strat's model/anim frame); allocates a free trail/ribbon slot (0x5FD180, 0x42C bytes each) owned by this strat and pushes its handle (0 if none free). <sub>554530</sub> |
|
||||
| 0x15f | `trailAddPoint` | - | 3 | 0 | 6 | medium | - | pops colourB (top), colourA, trail handle; transforms the two trail vertices of the current anim frame into world space and appends them to the trail history (20 points) with the given colours; no-op if handle 0. <sub>5545c0</sub> |
|
||||
@@ -456,8 +467,8 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x187 | `emitterInit` | - | 7 | 0 | 113 | medium | stSpawnparticle | pops colour/texture (top, raw), z, y, x (vector/offset), type (fixed 1..0x2B preset, or >=0x1000 type flags), size/speed (fixed, 0x1000 default), slot (0..2, >=3 = extra slot +0x153); (re)initialises the strat's particle emitter in that slot (records at 0x5FE230, 0x34 bytes; texture/colour stored per type). <sub>552f40</sub> |
|
||||
| 0x188 | `emitParticles` | - | 3 | 0 | | medium | - | pops yStep (top, fixed), count, slot; emits count particles from the slot's emitter at own position with random XZ velocity, each raised by yStep. <sub>552fb0</sub> |
|
||||
| 0x189 | `randSigned` | - | 1 | 1 | 922 | high | - | pops n; pushes a random value in (-n, n) ((rand32 - rand32) % n, same fixed scale as n). <sub>54f9a0</sub> |
|
||||
| 0x18a | `emitterSetParams` | - | 5 | 0 | 113 | low | - | pops e (top), d, c, b, slot; sets emitter slot params: +0x254 = b (rate/speed), +0x24A = c>>12 (life); if e==0 +0x25C = (d>>12)*32-32 else +0x246 = e, +0x23C = d. Allocates the slot if empty. <sub>5530b0</sub> |
|
||||
| 0x18b | `emitterSetColour` | - | 5 | 0 | 9 | low | - | pops colour (top, raw packed, see 0x15d), b, g, r, slot; sets emitter colour (+0x250) and fade/alpha (+0x240..243): if r has no fraction it is an alpha value only, else r,g,b are per-channel fade values. <sub>553100</sub> |
|
||||
| 0x18a | `emitterSetParams` | - | 5 | 0 | 113 | medium | - | Pops e (top), d, c, b, slot (>>12): pending particle settings of this strat (slot 0–2 → record index at strat+0x150+slot, ≥3 → +0x153, record allocated on demand in the 0x34-byte table 0x5FE23C): b = size scale (1.0 = default), c>>12 = extra lifetime in frames, e≠0: +0x246 = e (drift), +0x23C = d; e==0: +0x25C = (d>>12)*32-32 (sprite size). spawnParticle consumes and resets the +0x153 record; same as spawnParticle type 0x19. <sub>5530b0</sub> |
|
||||
| 0x18b | `emitterSetColour` | - | 5 | 0 | 9 | medium | - | Pops colour (top, raw packed, see 0x15d), b, g, r, slot: pending particle colour for the next particle(s) of this strat (record as 0x18a; same as spawnParticle type 0x1e): +0x250 = colour; r without fraction = alpha r>>12 (7 bits), else r,g,b>>12 = per-channel fade, +0x243 = 0x80; alpha 0 = 0xFF/lifetime. <sub>553100</sub> |
|
||||
| 0x18c | `emitParticlesStill` | - | 3 | 0 | 50 | medium | - | pops yStep (top, raw fixed), count, slot; emits count particles from the slot's emitter at own position without velocity, stacked by yStep. <sub>552ff0</sub> |
|
||||
| 0x18d | `stubPop2_0x18d` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
|
||||
| 0x18e | `stubPop2_0x18e` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
|
||||
@@ -487,7 +498,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 0x1a6 | `emitterKill` | - | 1 | 0 | | high | - | pops slot (fixed); frees the strat's particle emitter in that slot (+0x150+slot = 0xFF) and resets the record. <sub>553150</sub> |
|
||||
| 0x1a7 | `stubPush0_0x1a7` | - | 0 | 1 | | high | - | Stub FUN_0054e880: pushes 0. <sub>54e880</sub> |
|
||||
| 0x1a8 | `processFirst` | - | 0 | 0 | 1 | high | - | Moves self to the head of the strat update list (0x6D9E38) if not already there. <sub>554e20</sub> |
|
||||
| 0x1a9 | `setRain` | - | 4 | 0 | 4 | low | - | pops colour (top, raw -> 0x6D9BE8), b (->0x578EF4), a2 (->0x6D9BF4), a1 (->0x578EF0) (all >>12); if the three counts/params are 0 rain is stopped and cleared, else rain is enabled (0x6D9BE4 = 1). <sub>554fc0</sub> |
|
||||
| 0x1a9 | `setRain` | - | 4 | 0 | 4 | medium | - | Weather: pops texture (top, raw tex index → 0x6D9BE8, read by the rain renderer), windZ (>>12 → 0x578EF4), c (>>12 → 0x6D9BF4, only used for the off test), windX (>>12 → 0x578EF0); each raindrop restarts with velocity (windX, -256, windZ) per frame. windX, c, windZ all 0 = rain off (drops cleared, 0x6D9BE4 = 0), else on. Rain type: setRain(v0, v1, v2, tex:768) with params 16.0, 0, 16.0. <sub>554fc0</sub> |
|
||||
| 0x1aa | `stubPop1Push0_0x1aa` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. <sub>5550b0</sub> |
|
||||
| 0x1ab | `stubPop2_0x1ab` | - | 2 | 0 | | high | - | Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). <sub>555090</sub> |
|
||||
| 0x1ac | `stubPop1Push0_0x1ac` | - | 1 | 1 | | high | - | Stub FUN_005550b0: pops 1 value and pushes 0. <sub>5550b0</sub> |
|
||||
|
||||
@@ -74,6 +74,20 @@ public final class StratCatalog {
|
||||
names.putIfAbsent(code, name);
|
||||
}
|
||||
|
||||
/** Removes a type and the names of its variants' codes (custom types being rebuilt). */
|
||||
public void remove(String name) {
|
||||
Type t = types.remove(name);
|
||||
if (t != null) {
|
||||
t.variants.forEach(v -> names.remove(v.code, name));
|
||||
}
|
||||
codeOnly.remove(name);
|
||||
}
|
||||
|
||||
/** Forgets the name of a code offset. */
|
||||
public void removeName(int code) {
|
||||
names.remove(code);
|
||||
}
|
||||
|
||||
/** Placeable types, sorted by name. */
|
||||
public List<Type> types() {
|
||||
return List.copyOf(types.values());
|
||||
|
||||
@@ -97,6 +97,23 @@ final class ScriptPanel extends JPanel {
|
||||
"duplicate",
|
||||
"Copy this type into a custom type and edit its script",
|
||||
() -> TypeDialog.showClone(ctx, primaryType())));
|
||||
row2.add(Widgets.button("Edit type…", null, "Edit the script of this custom type (undoable)", () -> {
|
||||
int k = customIndex();
|
||||
if (k < 0) {
|
||||
status.setText("Only custom types can be edited – use Clone as new type… for original types");
|
||||
} else {
|
||||
TypeDialog.showEdit(ctx, k);
|
||||
}
|
||||
}));
|
||||
row2.add(Widgets.button(
|
||||
"Delete type…", "delete", "Delete this custom type (warns about objects using it)", () -> {
|
||||
int k = customIndex();
|
||||
if (k < 0) {
|
||||
status.setText("Only custom types can be deleted");
|
||||
} else {
|
||||
TypeDialog.delete(ctx, k);
|
||||
}
|
||||
}));
|
||||
JPanel top = new JPanel(new java.awt.GridLayout(2, 1));
|
||||
top.add(row1);
|
||||
top.add(row2);
|
||||
@@ -148,6 +165,11 @@ final class ScriptPanel extends JPanel {
|
||||
reload();
|
||||
}
|
||||
|
||||
private int customIndex() {
|
||||
Scene s = ctx.scene();
|
||||
return s == null || listing == null ? -1 : StratEdits.customIndex(s, listing.unit);
|
||||
}
|
||||
|
||||
private int selectedUnit() {
|
||||
UnitItem u = (UnitItem) units.getSelectedItem();
|
||||
return u == null ? -1 : u.unit;
|
||||
@@ -172,7 +194,7 @@ final class ScriptPanel extends JPanel {
|
||||
RawChunk c = s == null ? null : StratEdits.chunk(s);
|
||||
if (s != shownScene || c == null || c.data() != shownData) {
|
||||
int keep = list.getSelectedIndex();
|
||||
int u = selectedUnit();
|
||||
int u = listing != null ? listing.unit : selectedUnit();
|
||||
reloadUnits();
|
||||
if (u >= 0) {
|
||||
selectUnit(u);
|
||||
@@ -501,7 +523,7 @@ final class ScriptPanel extends JPanel {
|
||||
return;
|
||||
}
|
||||
if (StratEdits.isCustomUnit(ctx.scene(), listing.unit)) {
|
||||
status.setText("Custom types are edited through their source: clone it again with the changed script");
|
||||
status.setText("Custom types are edited through their source: Edit type…");
|
||||
return;
|
||||
}
|
||||
Map<StratListing.Line, Integer> changes = ConstantDialog.show(ctx, r.consts);
|
||||
|
||||
@@ -519,9 +519,14 @@ 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);
|
||||
byte[] old = a.img.data;
|
||||
return build(a.img.data, s.settings().wfpc(), a.entries, t);
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
StpcImage img0 = new StpcImage(old, wfpc);
|
||||
int base = old.length + 4;
|
||||
StratAssembler.Result r = StratAssembler.assemble(t.source, StratListing.layout(a.img, base));
|
||||
StratAssembler.Result r = StratAssembler.assemble(t.source, StratListing.layout(img0, base));
|
||||
Integer e = r.labels().get(t.entryLabel);
|
||||
if (e == null) {
|
||||
throw new IllegalArgumentException("entry label " + t.entryLabel + " not found in the source");
|
||||
@@ -530,20 +535,20 @@ public final class StratEdits {
|
||||
byte[] d = new byte[old.length + 4 + words.length];
|
||||
System.arraycopy(old, 0, d, 0, old.length);
|
||||
int n = words.length / 4;
|
||||
d[old.length] = (byte) n;
|
||||
d[old.length + 1] = (byte) (n >> 8);
|
||||
d[old.length + 2] = (byte) (n >> 16);
|
||||
d[old.length + 3] = (byte) (n >> 24);
|
||||
putInt(d, old.length, n);
|
||||
System.arraycopy(words, 0, d, old.length + 4, words.length);
|
||||
int nu = Bytes.i32(d, a.img.codeStart) + 1;
|
||||
d[a.img.codeStart] = (byte) nu;
|
||||
d[a.img.codeStart + 1] = (byte) (nu >> 8);
|
||||
d[a.img.codeStart + 2] = (byte) (nu >> 16);
|
||||
d[a.img.codeStart + 3] = (byte) (nu >> 24);
|
||||
List<String> problems = verify(d, s.settings().wfpc(), a.entries, base, base + e, r);
|
||||
putInt(d, img0.codeStart, Bytes.i32(d, img0.codeStart) + 1);
|
||||
List<String> problems = verify(d, wfpc, entries, base, base + e, r);
|
||||
return new Built(d, base, base + e, r, problems);
|
||||
}
|
||||
|
||||
private static void putInt(byte[] d, int o, int v) {
|
||||
d[o] = (byte) v;
|
||||
d[o + 1] = (byte) (v >> 8);
|
||||
d[o + 2] = (byte) (v >> 16);
|
||||
d[o + 3] = (byte) (v >> 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-disassembly of the merged code: no decode errors, the new unit's listing assembles back
|
||||
* to the same words, and every address operand of the new unit hits a model, an animation set
|
||||
@@ -637,6 +642,154 @@ public final class StratEdits {
|
||||
return code;
|
||||
}
|
||||
|
||||
/** 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);
|
||||
if (st.firstCustom >= 0 && st.firstCustom + st.customEntries.size() != units(s)) {
|
||||
return "object types were imported after the custom types – save and reopen the project first";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** STPC data of a scene without its custom units. */
|
||||
private static byte[] withoutCustom(Scene s) {
|
||||
State st = state(s);
|
||||
Analysis a = analysis(s);
|
||||
if (st.firstCustom < 0) {
|
||||
return a.img.data;
|
||||
}
|
||||
int cut = a.img.units.get(st.firstCustom).start() - 4;
|
||||
byte[] d = java.util.Arrays.copyOf(a.img.data, cut);
|
||||
putInt(d, a.img.codeStart, Bytes.i32(d, a.img.codeStart) - st.customEntries.size());
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Problems of a new list of custom types for a scene (assembled one after another behind the
|
||||
* base code, as on load); null when all of them assemble and verify. index = the type to report
|
||||
* words for (out[0], −1 = none).
|
||||
*/
|
||||
public static List<String> checkTypes(Scene s, List<CustomType> types, int index, int[] words) {
|
||||
String blocker = rebuildBlocker(s);
|
||||
if (blocker != null) {
|
||||
return List.of(blocker);
|
||||
}
|
||||
byte[] d = withoutCustom(s);
|
||||
Set<Integer> entries = new TreeSet<>(analysis(s).entries);
|
||||
entries.removeIf(o -> o >= d.length);
|
||||
byte[] cur = d;
|
||||
for (int i = 0; i < types.size(); i++) {
|
||||
CustomType t = types.get(i);
|
||||
Built x = build(cur, s.settings().wfpc(), entries, t);
|
||||
if (!x.problems.isEmpty()) {
|
||||
return x.problems.stream().map(m -> t.name + ": " + m).toList();
|
||||
}
|
||||
if (i == index && words != null) {
|
||||
words[0] = x.asm.words().length;
|
||||
}
|
||||
entries.add(x.entry);
|
||||
for (int v : x.asm.strats().values()) {
|
||||
entries.add(x.base + v);
|
||||
}
|
||||
cur = x.stpc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the custom types of reference and scene by types (the custom units are cut off the
|
||||
* code and the new ones appended); objects of old type k move to the entry of type remap[k]
|
||||
* (objects of a type mapped to −1 must have been removed). Records the list in the project.
|
||||
*/
|
||||
static void setTypes(Project p, List<CustomType> types, int[] remap) {
|
||||
for (Scene s : List.of(p.reference(), p.scene())) {
|
||||
List<String> problems = checkTypes(s, types, -1, null);
|
||||
if (problems != null) {
|
||||
throw new IllegalStateException(String.join("; ", problems));
|
||||
}
|
||||
}
|
||||
for (Scene s : List.of(p.reference(), p.scene())) {
|
||||
Runnable r = () -> rebuild(s, types, remap);
|
||||
if (s == p.scene()) {
|
||||
s.bulk(r);
|
||||
} else {
|
||||
r.run();
|
||||
}
|
||||
}
|
||||
Records rec = Records.of(p.extra());
|
||||
new Records(rec.constants, List.copyOf(types)).store(p.extra(), session(p.scene()));
|
||||
}
|
||||
|
||||
private static void rebuild(Scene s, List<CustomType> types, int[] remap) {
|
||||
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<>();
|
||||
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) {
|
||||
s.set(x, "code", 0); // keep stale codes out of the analysis roots while appending
|
||||
}
|
||||
}
|
||||
byte[] cut = withoutCustom(s);
|
||||
StratCatalog cat = s.catalog();
|
||||
for (var e : List.copyOf(cat.names().entrySet())) {
|
||||
if (e.getKey() >= from && e.getKey() < to) {
|
||||
StratCatalog.Type t = cat.type(e.getValue());
|
||||
if (t != null && t.variants().stream().allMatch(v -> v.code() >= from && v.code() < to)) {
|
||||
cat.remove(e.getValue());
|
||||
}
|
||||
cat.removeName(e.getKey());
|
||||
}
|
||||
}
|
||||
chunk(s).setRaw("data", cut);
|
||||
st.firstCustom = -1;
|
||||
st.customEntries.clear();
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
Strat x = strats.get(i);
|
||||
int nk = 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);
|
||||
// objects placed at the entry follow the entry; other codes keep their offset in the unit
|
||||
int c0 = oldCode.get(i);
|
||||
int code = c0 == oldEntry ? st.customEntries.get(nk) : c0 - oldEntry + st.customEntries.get(nk);
|
||||
s.set(x, "code", code);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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);
|
||||
Analysis a = analysis(s);
|
||||
List<Strat> out = new ArrayList<>();
|
||||
if (st.firstCustom < 0 || k < 0 || k >= st.customEntries.size()) {
|
||||
return out;
|
||||
}
|
||||
int unit = st.firstCustom + k;
|
||||
for (Strat x : s.strats()) {
|
||||
if (a.img.unitOf(x.code()) == unit) {
|
||||
out.add(x);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Current position of the custom units (for remapping codes on load), or null. */
|
||||
static Map<String, Object> session(Scene s) {
|
||||
State st = state(s);
|
||||
|
||||
+11
-4
@@ -123,10 +123,17 @@ public final class StratsEditorPlugin implements EditorPlugin {
|
||||
JPanel p = new JPanel(new BorderLayout(6, 0));
|
||||
p.setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
|
||||
p.add(new JLabel(info, Icons.get("script", 16), JLabel.LEADING), BorderLayout.CENTER);
|
||||
p.add(
|
||||
Widgets.button(
|
||||
"Open script", null, "Show and edit this type's strat script", () -> ctx.showPanel(PANEL)),
|
||||
BorderLayout.EAST);
|
||||
JPanel buttons = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.TRAILING, 4, 0));
|
||||
int k = a == null || a.img().unitOf(s.code()) < 0
|
||||
? -1
|
||||
: StratEdits.customIndex(s.scene(), a.img().unitOf(s.code()));
|
||||
if (k >= 0) {
|
||||
buttons.add(Widgets.button(
|
||||
"Edit type…", null, "Edit this custom type's script", () -> TypeDialog.showEdit(ctx, k)));
|
||||
}
|
||||
buttons.add(Widgets.button(
|
||||
"Open script", null, "Show and edit this type's strat script", () -> ctx.showPanel(PANEL)));
|
||||
p.add(buttons, BorderLayout.EAST);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.model.StratCatalog;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorContext;
|
||||
import com.eliasmueller.llamamod.format.strat.StratAssembler;
|
||||
import com.eliasmueller.llamamod.format.strat.StratCompiler;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
@@ -42,6 +43,18 @@ final class TypeDialog {
|
||||
end
|
||||
""";
|
||||
|
||||
static final String SCRIPT_TEMPLATE =
|
||||
"""
|
||||
// A custom strat in the script language (manual chapter "Strat scripts").
|
||||
strat MyStrat {
|
||||
setModel(model:0) // first model of the level (pick one from another type's script)
|
||||
loop {
|
||||
turnLeft(4deg) // turn per frame
|
||||
yield // wait one frame
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
static void showClone(EditorContext ctx, String type) {
|
||||
if (ctx.document() == null) {
|
||||
return;
|
||||
@@ -57,7 +70,7 @@ final class TypeDialog {
|
||||
Scene s = ctx.scene();
|
||||
String name = freeName(s, type + "Custom");
|
||||
StratEdits.CustomType t = StratEdits.cloneOf(s, type, name);
|
||||
open(ctx, "Clone " + type + " as a new type", t.name(), t.source(), type, t.entryLabel(), true);
|
||||
open(ctx, "Clone " + type + " as a new type", t.name(), t.source(), type, t.entryLabel(), true, -1);
|
||||
}
|
||||
|
||||
static void showNew(EditorContext ctx) {
|
||||
@@ -67,7 +80,86 @@ final class TypeDialog {
|
||||
Scene s = ctx.scene();
|
||||
String base =
|
||||
s.catalog().has("Coin") ? "Coin" : s.catalog().types().get(0).name();
|
||||
open(ctx, "New object type from script", freeName(s, "MyStrat"), TEMPLATE, base, "MyStrat", false);
|
||||
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). */
|
||||
static void showEdit(EditorContext ctx, int k) {
|
||||
if (ctx.document() == null) {
|
||||
return;
|
||||
}
|
||||
String blocker = StratEdits.rebuildBlocker(ctx.scene());
|
||||
List<StratEdits.CustomType> types =
|
||||
StratEdits.records(ctx.document().project()).types();
|
||||
if (blocker != null || k < 0 || k >= types.size()) {
|
||||
JOptionPane.showMessageDialog(
|
||||
ctx.frame(),
|
||||
blocker != null ? "Cannot edit the type: " + blocker + "." : "Not a custom type.",
|
||||
"Edit object type",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
return;
|
||||
}
|
||||
StratEdits.CustomType t = types.get(k);
|
||||
open(
|
||||
ctx,
|
||||
"Edit object type " + t.name(),
|
||||
t.name(),
|
||||
t.source(),
|
||||
t.basedOn(),
|
||||
t.entryLabel(),
|
||||
t.basedOn() != null && !t.basedOn().isEmpty(),
|
||||
k);
|
||||
}
|
||||
|
||||
/** Deletes custom type k after a confirmation that names the objects using it. */
|
||||
static void delete(EditorContext ctx, int k) {
|
||||
if (ctx.document() == null) {
|
||||
return;
|
||||
}
|
||||
Project p = ctx.document().project();
|
||||
Scene s = ctx.scene();
|
||||
List<StratEdits.CustomType> types = StratEdits.records(p).types();
|
||||
if (k < 0 || k >= types.size()) {
|
||||
return;
|
||||
}
|
||||
String name = types.get(k).name();
|
||||
String blocker = StratEdits.rebuildBlocker(s);
|
||||
List<StratEdits.CustomType> rest = new java.util.ArrayList<>(types);
|
||||
rest.remove(k);
|
||||
List<String> problems = blocker != null ? List.of(blocker) : StratEdits.checkTypes(s, rest, -1, null);
|
||||
if (problems != null) {
|
||||
JOptionPane.showMessageDialog(
|
||||
ctx.frame(),
|
||||
"Cannot delete " + name + ": " + problems.get(0) + ".",
|
||||
"Delete object type",
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
int n = StratEdits.users(s, k).size();
|
||||
String msg = n == 0
|
||||
? "Delete the custom type " + name + "? No object in the level uses it."
|
||||
: n + " object" + (n == 1 ? "" : "s") + " in the level use" + (n == 1 ? "s " : " ") + name
|
||||
+ ".\nDelete the type and " + (n == 1 ? "this object" : "these objects") + "?";
|
||||
Object[] opts = {n == 0 ? "Delete type" : "Delete type and objects", "Cancel"};
|
||||
int r = JOptionPane.showOptionDialog(
|
||||
ctx.frame(),
|
||||
msg,
|
||||
"Delete object type",
|
||||
JOptionPane.DEFAULT_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE,
|
||||
null,
|
||||
opts,
|
||||
opts[1]);
|
||||
if (r != 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ctx.execute(TypeEdits.delete(p, k));
|
||||
} catch (RuntimeException ex) {
|
||||
ctx.error("Delete object type", ex);
|
||||
return;
|
||||
}
|
||||
ctx.status("Object type " + name + " deleted" + (n == 0 ? "" : " with " + n + " object" + (n == 1 ? "" : "s")));
|
||||
}
|
||||
|
||||
static String freeName(Scene s, String base) {
|
||||
@@ -85,14 +177,25 @@ final class TypeDialog {
|
||||
String source,
|
||||
String fieldsFrom,
|
||||
String entry,
|
||||
boolean clone) {
|
||||
boolean clone,
|
||||
int editIndex) {
|
||||
JDialog d = new JDialog(ctx.frame(), title, true);
|
||||
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);
|
||||
JTextArea text = new JTextArea(source, 30, 90);
|
||||
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");
|
||||
}
|
||||
String script = StratCompiler.embeddedSource(source);
|
||||
JTextArea text = new JTextArea(script != null ? script : source, 30, 90);
|
||||
JComboBox<String> lang = new JComboBox<>(new String[] {"Assembly", "Script"});
|
||||
lang.setSelectedIndex(script != null ? 1 : 0);
|
||||
lang.setToolTipText("Assembly = the exact instruction list; Script = structured language compiled to assembly");
|
||||
text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
text.setTabSize(8);
|
||||
JLabel result = new JLabel(" ");
|
||||
@@ -101,10 +204,12 @@ final class TypeDialog {
|
||||
top.add(nameField);
|
||||
top.add(new JLabel("Entry label"));
|
||||
top.add(entryField);
|
||||
top.add(new JLabel("Language"));
|
||||
top.add(lang);
|
||||
top.add(new JLabel("Type fields / parameters from"));
|
||||
top.add(from);
|
||||
JButton check = new JButton("Check");
|
||||
JButton create = new JButton("Create type");
|
||||
JButton create = new JButton(editing ? "Apply" : "Create type");
|
||||
JButton cancel = new JButton("Cancel");
|
||||
JPanel bottom = new JPanel(new BorderLayout());
|
||||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.TRAILING));
|
||||
@@ -128,6 +233,39 @@ final class TypeDialog {
|
||||
d.getContentPane().add(north, BorderLayout.NORTH);
|
||||
d.getContentPane().add(new JScrollPane(text), BorderLayout.CENTER);
|
||||
d.getContentPane().add(bottom, BorderLayout.SOUTH);
|
||||
String[] shown = {text.getText()};
|
||||
int[] shownLang = {lang.getSelectedIndex()};
|
||||
lang.addActionListener(e -> {
|
||||
int to = lang.getSelectedIndex();
|
||||
if (to == shownLang[0]) {
|
||||
return;
|
||||
}
|
||||
String cur = text.getText();
|
||||
if (to == 0) {
|
||||
try {
|
||||
text.setText(StratCompiler.compile(cur));
|
||||
} catch (StratCompiler.CompileException ex) {
|
||||
result.setText("⚠ " + ex.getMessage() + " – fix the script before switching to assembly");
|
||||
highlight(text, ex.line);
|
||||
lang.setSelectedIndex(1);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String embedded = StratCompiler.embeddedSource(cur);
|
||||
if (embedded != null) {
|
||||
text.setText(embedded);
|
||||
} else if (cur.equals(TEMPLATE) || cur.equals(shown[0]) && !clone && editIndex < 0) {
|
||||
text.setText(SCRIPT_TEMPLATE);
|
||||
} else {
|
||||
result.setText(
|
||||
"⚠ Assembly cannot be turned into script; start from the script template (New object type from script…)");
|
||||
lang.setSelectedIndex(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
shownLang[0] = to;
|
||||
text.setCaretPosition(0);
|
||||
});
|
||||
Runnable[] verify = new Runnable[1];
|
||||
StratEdits.CustomType[] ok = new StratEdits.CustomType[1];
|
||||
verify[0] = () -> {
|
||||
@@ -138,12 +276,55 @@ final class TypeDialog {
|
||||
result.setText("⚠ The name must be letters, digits and _ (max 40).");
|
||||
return;
|
||||
}
|
||||
if (s.catalog().has(n) || s.catalog().names().containsValue(n)) {
|
||||
if (!n.equals(oldName) && (s.catalog().has(n) || s.catalog().names().containsValue(n))) {
|
||||
result.setText("⚠ A type named " + n + " already exists.");
|
||||
return;
|
||||
}
|
||||
String src = text.getText();
|
||||
if (lang.getSelectedIndex() == 1) {
|
||||
try {
|
||||
src = StratCompiler.compile(src);
|
||||
} catch (StratCompiler.CompileException e) {
|
||||
result.setText("⚠ " + e.getMessage());
|
||||
highlight(text, e.line);
|
||||
return;
|
||||
}
|
||||
String first = StratCompiler.firstStrat(text.getText());
|
||||
if (first != null && !src.contains("\n" + entryField.getText().trim() + ":\n")) {
|
||||
entryField.setText(first);
|
||||
}
|
||||
}
|
||||
StratCatalog.Type ft = s.catalog().type((String) from.getSelectedItem());
|
||||
if (editing) {
|
||||
StratEdits.CustomType old =
|
||||
StratEdits.records(ctx.document().project()).types().get(editIndex);
|
||||
StratEdits.CustomType t = new StratEdits.CustomType(
|
||||
n, old.basedOn(), entryField.getText().trim(), src, old.proto());
|
||||
List<StratEdits.CustomType> all = new java.util.ArrayList<>(
|
||||
StratEdits.records(ctx.document().project()).types());
|
||||
all.set(editIndex, t);
|
||||
int[] words = new int[1];
|
||||
try {
|
||||
List<String> ps = StratEdits.checkTypes(s, all, editIndex, words);
|
||||
if (ps != null) {
|
||||
result.setText("⚠ " + String.join("; ", ps));
|
||||
return;
|
||||
}
|
||||
} catch (StratAssembler.AsmException e) {
|
||||
List<StratAssembler.Problem> ps = e.problems;
|
||||
result.setText("⚠ " + ps.size() + " error" + (ps.size() == 1 ? "" : "s") + ": " + ps.get(0));
|
||||
highlight(text, ps.get(0).line());
|
||||
return;
|
||||
} catch (RuntimeException e) {
|
||||
result.setText("⚠ " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
int users = StratEdits.users(s, editIndex).size();
|
||||
result.setText("✓ " + words[0] + " words, verified. " + users + " object" + (users == 1 ? "" : "s")
|
||||
+ " in the level use" + (users == 1 ? "s" : "") + " this type.");
|
||||
ok[0] = t;
|
||||
return;
|
||||
}
|
||||
if (ft == null) {
|
||||
result.setText("⚠ Choose the type whose type fields and parameters the new type uses.");
|
||||
return;
|
||||
@@ -181,12 +362,21 @@ final class TypeDialog {
|
||||
}
|
||||
Project p = ctx.document().project();
|
||||
try {
|
||||
StratEdits.addType(p, ok[0]);
|
||||
if (editing) {
|
||||
ctx.execute(TypeEdits.edit(p, editIndex, ok[0]));
|
||||
} else if (StratEdits.rebuildBlocker(ctx.scene()) == null) {
|
||||
ctx.execute(TypeEdits.create(p, ok[0]));
|
||||
} else {
|
||||
StratEdits.addType(p, ok[0]); // types imported after the custom ones: append, no undo
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
ctx.error("Create object type", ex);
|
||||
ctx.error(editing ? "Edit object type" : "Create object type", ex);
|
||||
return;
|
||||
}
|
||||
ctx.status("Object type " + ok[0].name() + " created – place it from Assets → Objects");
|
||||
ctx.status(
|
||||
editing
|
||||
? "Object type " + ok[0].name() + " updated – rebuild/play to see the effect"
|
||||
: "Object type " + ok[0].name() + " created – place it from Assets → Objects");
|
||||
d.dispose();
|
||||
});
|
||||
cancel.addActionListener(e -> d.dispose());
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.eliasmueller.llamamod.editor.tools.strats;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.io.Project;
|
||||
import com.eliasmueller.llamamod.editor.model.Scene;
|
||||
import com.eliasmueller.llamamod.editor.model.Strat;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Batch;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Command;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Edits;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Undoable create / edit / delete of custom object types (the custom code block is rebuilt). */
|
||||
final class TypeEdits {
|
||||
private TypeEdits() {}
|
||||
|
||||
/** Replaces the custom type list; fwd/back map type indexes of the old list to the new one and back. */
|
||||
record SetTypes(
|
||||
Project p,
|
||||
String label,
|
||||
List<StratEdits.CustomType> before,
|
||||
List<StratEdits.CustomType> after,
|
||||
int[] fwd,
|
||||
int[] back)
|
||||
implements Command {
|
||||
@Override
|
||||
public void apply(Scene s) {
|
||||
StratEdits.setTypes(p, after, fwd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revert(Scene s) {
|
||||
StratEdits.setTypes(p, before, back);
|
||||
}
|
||||
}
|
||||
|
||||
static int[] identity(int n) {
|
||||
int[] a = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
a[i] = i;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
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);
|
||||
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). */
|
||||
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);
|
||||
return new SetTypes(
|
||||
p, "Edit object type " + t.name(), before, after, identity(before.size()), identity(before.size()));
|
||||
}
|
||||
|
||||
/** Deletes custom type k together with the objects that use it. */
|
||||
static Command delete(Project p, int k) {
|
||||
List<StratEdits.CustomType> before = StratEdits.records(p).types();
|
||||
List<StratEdits.CustomType> after = new ArrayList<>(before);
|
||||
StratEdits.CustomType gone = after.remove(k);
|
||||
int[] fwd = new int[before.size()], back = new int[after.size()];
|
||||
for (int i = 0; i < fwd.length; i++) {
|
||||
fwd[i] = i < k ? i : i == k ? -1 : i - 1;
|
||||
}
|
||||
for (int j = 0; j < back.length; j++) {
|
||||
back[j] = j < k ? j : j + 1;
|
||||
}
|
||||
List<Command> cmds = new ArrayList<>();
|
||||
List<Strat> users = StratEdits.users(p.scene(), k);
|
||||
if (!users.isEmpty()) {
|
||||
cmds.add(Edits.delete(p.scene(), users));
|
||||
}
|
||||
cmds.add(new SetTypes(p, "Delete object type " + gone.name(), before, after, fwd, back));
|
||||
return new Batch("Delete object type " + gone.name(), cmds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
package com.eliasmueller.llamamod.format.strat;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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).
|
||||
*/
|
||||
public final class StratCompiler {
|
||||
/** A compile error at a 1-based source line. */
|
||||
public static final class CompileException extends RuntimeException {
|
||||
public final int line;
|
||||
|
||||
CompileException(int line, String message) {
|
||||
super("line " + line + ": " + message);
|
||||
this.line = line;
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefix of the comment lines that carry the script source inside the generated assembly. */
|
||||
public static final String SOURCE_PREFIX = ";| ";
|
||||
|
||||
private record Tok(String kind, String text, int line) {}
|
||||
|
||||
private static final Map<String, Integer> PREC = Map.ofEntries(
|
||||
Map.entry("||", 1),
|
||||
Map.entry("&&", 2),
|
||||
Map.entry("|", 3),
|
||||
Map.entry("&", 4),
|
||||
Map.entry("==", 5),
|
||||
Map.entry("!=", 5),
|
||||
Map.entry("<", 6),
|
||||
Map.entry(">", 6),
|
||||
Map.entry("<=", 6),
|
||||
Map.entry(">=", 6),
|
||||
Map.entry("<<", 7),
|
||||
Map.entry(">>", 7),
|
||||
Map.entry("+", 8),
|
||||
Map.entry("-", 8),
|
||||
Map.entry("*", 9),
|
||||
Map.entry("/", 9));
|
||||
|
||||
private static final Map<String, String> BINOP = Map.ofEntries(
|
||||
Map.entry("||", "or"),
|
||||
Map.entry("|", "or"),
|
||||
Map.entry("&&", "and"),
|
||||
Map.entry("&", "bitwiseAnd"),
|
||||
Map.entry("==", "eq"),
|
||||
Map.entry("!=", "notEqual"),
|
||||
Map.entry("<", "lt"),
|
||||
Map.entry(">", "gt"),
|
||||
Map.entry("<=", "lessEqual"),
|
||||
Map.entry(">=", "greaterEqual"),
|
||||
Map.entry("<<", "shl"),
|
||||
Map.entry(">>", "shr"),
|
||||
Map.entry("+", "add"),
|
||||
Map.entry("-", "sub"),
|
||||
Map.entry("*", "mul"),
|
||||
Map.entry("/", "div"));
|
||||
|
||||
/** Target prefix → push, pop, address mnemonics (variables of the parent use the *Var ops). */
|
||||
private static final Map<String, String[]> TARGETS = Map.of(
|
||||
"self", new String[] {"pushProp", "popProp", "pushPropAddr"},
|
||||
"player", new String[] {"pushPlayerProp", "popPlayerProp", null},
|
||||
"parent", new String[] {"pushParentProp", "popParentProp", "pushParentPropAddr"},
|
||||
"camera", new String[] {"pushCameraProp", "popCameraProp", null},
|
||||
"target", new String[] {"pushTargetProp", "popTargetProp", null},
|
||||
"target2", new String[] {"pushTarget2Prop", "popTarget2Prop", null},
|
||||
"boss", new String[] {"pushBossProp", "popBossProp", null},
|
||||
"dialog", new String[] {"pushDialogProp", "popDialogProp", null});
|
||||
|
||||
private static final List<String> REFS = List.of("model", "anim", "unit", "sound", "tex", "text");
|
||||
|
||||
private final OpTable ops = OpTable.get();
|
||||
private final List<Tok> toks;
|
||||
private int p;
|
||||
private final StringBuilder out = 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 StratCompiler(String src) {
|
||||
toks = lex(src);
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
|
||||
/** The script embedded in compiled assembly, or null if the text is not compiler output. */
|
||||
public static String embeddedSource(String asm) {
|
||||
if (!asm.startsWith(SOURCE_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (String l : asm.split("\n", -1)) {
|
||||
if (!l.startsWith(SOURCE_PREFIX.trim())) {
|
||||
break;
|
||||
}
|
||||
b.append(l.length() >= SOURCE_PREFIX.length() ? l.substring(SOURCE_PREFIX.length()) : "")
|
||||
.append('\n');
|
||||
}
|
||||
return b.toString().stripTrailing() + "\n";
|
||||
}
|
||||
|
||||
/** Entry label of the first strat of a script (null if none). */
|
||||
public static String firstStrat(String src) {
|
||||
List<Tok> t = lex(src);
|
||||
for (int i = 0; i + 1 < t.size(); i++) {
|
||||
if (t.get(i).text.equals("strat") && t.get(i + 1).kind.equals("id")) {
|
||||
return t.get(i + 1).text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ lexer
|
||||
|
||||
private static List<Tok> lex(String s) {
|
||||
List<Tok> out = new ArrayList<>();
|
||||
int line = 1;
|
||||
int i = 0;
|
||||
while (i < s.length()) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '\n') {
|
||||
line++;
|
||||
i++;
|
||||
} else if (Character.isWhitespace(c) || c == ';') {
|
||||
i++;
|
||||
} else if (s.startsWith("//", i)) {
|
||||
while (i < s.length() && s.charAt(i) != '\n') {
|
||||
i++;
|
||||
}
|
||||
} else if (s.startsWith("/*", i)) {
|
||||
int e = s.indexOf("*/", i + 2);
|
||||
e = e < 0 ? s.length() : e + 2;
|
||||
for (int k = i; k < e; k++) {
|
||||
line += s.charAt(k) == '\n' ? 1 : 0;
|
||||
}
|
||||
i = e;
|
||||
} else if (Character.isDigit(c)) {
|
||||
int j = i;
|
||||
if (s.startsWith("0x", i) || s.startsWith("0X", i)) {
|
||||
j += 2;
|
||||
while (j < s.length() && Character.digit(s.charAt(j), 16) >= 0) {
|
||||
j++;
|
||||
}
|
||||
} else {
|
||||
while (j < s.length() && (Character.isDigit(s.charAt(j)) || s.charAt(j) == '.')) {
|
||||
j++;
|
||||
}
|
||||
if (s.startsWith("deg", j)) {
|
||||
j += 3;
|
||||
}
|
||||
}
|
||||
out.add(new Tok("num", s.substring(i, j), line));
|
||||
i = j;
|
||||
} else if (Character.isLetter(c) || c == '_') {
|
||||
int j = i;
|
||||
while (j < s.length() && (Character.isLetterOrDigit(s.charAt(j)) || s.charAt(j) == '_')) {
|
||||
j++;
|
||||
}
|
||||
out.add(new Tok("id", s.substring(i, j), line));
|
||||
i = j;
|
||||
} else if (c == '"') {
|
||||
int j = i + 1;
|
||||
while (j < s.length() && s.charAt(j) != '"' && s.charAt(j) != '\n') {
|
||||
j += s.charAt(j) == '\\' ? 2 : 1;
|
||||
}
|
||||
out.add(new Tok("str", s.substring(i, Math.min(j + 1, s.length())), line));
|
||||
i = j + 1;
|
||||
} else {
|
||||
String two = i + 1 < s.length() ? s.substring(i, i + 2) : "";
|
||||
String op = List.of("==", "!=", "<=", ">=", "&&", "||", "<<", ">>", "+=", "-=", "*=", "/=")
|
||||
.contains(two)
|
||||
? two
|
||||
: String.valueOf(c);
|
||||
out.add(new Tok("op", op, line));
|
||||
i += op.length();
|
||||
}
|
||||
}
|
||||
out.add(new Tok("eof", "", line));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ parser helpers
|
||||
|
||||
private Tok peek() {
|
||||
return toks.get(p);
|
||||
}
|
||||
|
||||
private Tok peek(int k) {
|
||||
return toks.get(Math.min(p + k, toks.size() - 1));
|
||||
}
|
||||
|
||||
private Tok next() {
|
||||
return toks.get(p < toks.size() - 1 ? p++ : p);
|
||||
}
|
||||
|
||||
private boolean at(String text) {
|
||||
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++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Tok expect(String text) {
|
||||
if (!at(text)) {
|
||||
throw err("expected '" + text + "' but found " + describe(peek()));
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
private String ident() {
|
||||
if (!peek().kind.equals("id")) {
|
||||
throw err("expected a name but found " + describe(peek()));
|
||||
}
|
||||
return next().text;
|
||||
}
|
||||
|
||||
private static String describe(Tok t) {
|
||||
return t.kind.equals("eof") ? "the end of the script" : "'" + t.text + "'";
|
||||
}
|
||||
|
||||
private CompileException err(String m) {
|
||||
return new CompileException(peek().line, m);
|
||||
}
|
||||
|
||||
private String label() {
|
||||
return "c_" + (++labels);
|
||||
}
|
||||
|
||||
private void emit(String instr) {
|
||||
out.append(" ").append(instr).append('\n');
|
||||
}
|
||||
|
||||
private void place(String label) {
|
||||
out.append(label).append(":\n");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ program
|
||||
|
||||
private void program() {
|
||||
while (!peek().kind.equals("eof")) {
|
||||
if (accept("strat")) {
|
||||
String name = ident();
|
||||
out.append(" .string \"").append(name).append("\"\n");
|
||||
place(name);
|
||||
emit("debugName \"" + name + "\"");
|
||||
boolean ends = block();
|
||||
if (!ends) {
|
||||
emit("end");
|
||||
}
|
||||
} else if (accept("sub")) {
|
||||
String name = ident();
|
||||
place(name);
|
||||
accept("(");
|
||||
accept(")");
|
||||
boolean ends = block();
|
||||
if (!ends) {
|
||||
emit("return");
|
||||
}
|
||||
} else if (at("alias")) {
|
||||
statement();
|
||||
} else {
|
||||
throw err("expected 'strat', 'sub' or 'alias' but found " + describe(peek()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses { statements }; returns whether control never falls out of its end. */
|
||||
private boolean block() {
|
||||
expect("{");
|
||||
boolean ends = false;
|
||||
while (!at("}")) {
|
||||
if (peek().kind.equals("eof")) {
|
||||
throw err("missing '}'");
|
||||
}
|
||||
ends = statement();
|
||||
}
|
||||
expect("}");
|
||||
return ends;
|
||||
}
|
||||
|
||||
/** One statement; returns whether control never continues after it (end, endless loop, return). */
|
||||
private boolean statement() {
|
||||
Tok t = peek();
|
||||
switch (t.text) {
|
||||
case "if" -> {
|
||||
next();
|
||||
ifRest();
|
||||
return false;
|
||||
}
|
||||
case "while" -> {
|
||||
next();
|
||||
String top = label(), done = label();
|
||||
place(top);
|
||||
expect("(");
|
||||
cond(done);
|
||||
expect(")");
|
||||
loops.push(new String[] {done, top});
|
||||
loopBody(top);
|
||||
loops.pop();
|
||||
place(done);
|
||||
return false;
|
||||
}
|
||||
case "loop" -> {
|
||||
next();
|
||||
String top = label(), done = label();
|
||||
place(top);
|
||||
loops.push(new String[] {done, top});
|
||||
loopBody(top);
|
||||
loops.pop();
|
||||
place(done);
|
||||
return false;
|
||||
}
|
||||
case "do" -> {
|
||||
next();
|
||||
String top = label(), check = label(), done = label();
|
||||
place(top);
|
||||
loops.push(new String[] {done, check});
|
||||
block();
|
||||
loops.pop();
|
||||
expect("while");
|
||||
expect("(");
|
||||
place(check);
|
||||
expr();
|
||||
expect(")");
|
||||
emit("jnz " + top);
|
||||
place(done);
|
||||
return false;
|
||||
}
|
||||
case "break", "continue" -> {
|
||||
next();
|
||||
if (loops.isEmpty()) {
|
||||
throw new CompileException(t.line, t.text + " outside a loop");
|
||||
}
|
||||
emit("jmp " + loops.peek()[t.text.equals("break") ? 0 : 1]);
|
||||
return true;
|
||||
}
|
||||
case "yield" -> {
|
||||
next();
|
||||
String l = label();
|
||||
emit("jmpYield " + l);
|
||||
place(l);
|
||||
return false;
|
||||
}
|
||||
case "return" -> {
|
||||
next();
|
||||
emit("return");
|
||||
return true;
|
||||
}
|
||||
case "end" -> {
|
||||
next();
|
||||
emit("end");
|
||||
return true;
|
||||
}
|
||||
case "call" -> {
|
||||
next();
|
||||
String l = ident();
|
||||
if (accept("(")) {
|
||||
expect(")");
|
||||
}
|
||||
emit("call " + l);
|
||||
return false;
|
||||
}
|
||||
case "alias" -> {
|
||||
next();
|
||||
String n = ident();
|
||||
expect("=");
|
||||
String v = ident();
|
||||
if (!v.matches("v\\d+")) {
|
||||
throw new CompileException(t.line, "an alias names a variable vN");
|
||||
}
|
||||
aliases.put(n, v);
|
||||
out.append(" .alias ").append(n).append(" = ").append(v).append('\n');
|
||||
return false;
|
||||
}
|
||||
default -> {
|
||||
// fall through to assignments and calls
|
||||
}
|
||||
}
|
||||
if (!t.kind.equals("id")) {
|
||||
throw err("expected a statement but found " + describe(t));
|
||||
}
|
||||
if (isOp(t.text) && !isLvalueStart()) {
|
||||
String mn = callExpr();
|
||||
OpTable.Op op = ops.byName(mn);
|
||||
if (op.pushes() == 1) {
|
||||
emit("pop");
|
||||
}
|
||||
return mn.equalsIgnoreCase("end") || mn.equalsIgnoreCase("remove");
|
||||
}
|
||||
assignment();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ifRest() {
|
||||
String other = label();
|
||||
expect("(");
|
||||
cond(other);
|
||||
expect(")");
|
||||
block();
|
||||
if (accept("else")) {
|
||||
String done = label();
|
||||
emit("jmp " + done);
|
||||
place(other);
|
||||
if (accept("if")) {
|
||||
ifRest();
|
||||
} else {
|
||||
block();
|
||||
}
|
||||
place(done);
|
||||
} else {
|
||||
place(other);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
while (!at("}")) {
|
||||
if (peek().kind.equals("eof")) {
|
||||
throw err("missing '}'");
|
||||
}
|
||||
mark = at("yield") && peek(1).text.equals("}") ? out.length() : -1;
|
||||
if (mark >= 0) {
|
||||
next();
|
||||
break;
|
||||
}
|
||||
statement();
|
||||
}
|
||||
expect("}");
|
||||
emit((mark >= 0 ? "jmpYield " : "jmp ") + top);
|
||||
}
|
||||
|
||||
private boolean isOp(String name) {
|
||||
return ops.byName(name) != null;
|
||||
}
|
||||
|
||||
private boolean isLvalueStart() {
|
||||
String a = peek().text;
|
||||
return aliases.containsKey(a)
|
||||
|| a.matches("[vg]\\d+")
|
||||
|| TARGETS.containsKey(a) && peek(1).text.equals(".");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ assignments and lvalues
|
||||
|
||||
/** A writable place: push/pop/address instructions with their operand. */
|
||||
private record Place(String push, String pop, String addr) {}
|
||||
|
||||
private Place place() {
|
||||
Tok t = peek();
|
||||
String a = ident();
|
||||
if (aliases.containsKey(a)) {
|
||||
a = aliases.get(a);
|
||||
}
|
||||
if (a.matches("v\\d+")) {
|
||||
return new Place("pushVar " + a, "popVar " + a, "pushVarAddr " + a);
|
||||
}
|
||||
if (a.matches("g\\d+")) {
|
||||
return new Place("pushGlobal " + a, "popGlobal " + a, "pushGlobalAddr " + a);
|
||||
}
|
||||
if (TARGETS.containsKey(a) && accept(".")) {
|
||||
String n = ident();
|
||||
if (a.equals("parent") && n.matches("v\\d+")) {
|
||||
return new Place("pushParentVar " + n, "popParentVar " + n, "pushParentVarAddr " + n);
|
||||
}
|
||||
if (ops.propByName(n) == null && !n.matches("prop\\d+")) {
|
||||
throw new CompileException(t.line, "unknown property " + n);
|
||||
}
|
||||
String[] m = TARGETS.get(a);
|
||||
return new Place(m[0] + " " + n, m[1] + " " + n, m[2] == null ? null : m[2] + " " + n);
|
||||
}
|
||||
throw new CompileException(
|
||||
t.line,
|
||||
"unknown name " + a + " (variables are vN or aliases, globals gN, "
|
||||
+ "properties self.name, player.name, …)");
|
||||
}
|
||||
|
||||
private void assignment() {
|
||||
Tok t = peek();
|
||||
Place pl = place();
|
||||
String op = next().text;
|
||||
switch (op) {
|
||||
case "=" -> {
|
||||
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();
|
||||
emit(pl.addr);
|
||||
emit(op.equals("+=") ? "inc" : "dec");
|
||||
} else {
|
||||
emit(pl.push);
|
||||
expr();
|
||||
emit(BINOP.get(op.substring(0, 1)));
|
||||
emit(pl.pop);
|
||||
}
|
||||
}
|
||||
default -> throw new CompileException(t.line, "expected '=', '+=', '-=', '*=' or '/=' after a name");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ expressions
|
||||
|
||||
private void expr() {
|
||||
binary(1);
|
||||
}
|
||||
|
||||
private void binary(int min) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private void unary() {
|
||||
if (accept("-")) {
|
||||
if (peek().kind.equals("num")) {
|
||||
number("-" + next().text);
|
||||
return;
|
||||
}
|
||||
unary();
|
||||
emit("neg");
|
||||
} else if (accept("!")) {
|
||||
unary();
|
||||
emit("not");
|
||||
} else {
|
||||
primary();
|
||||
}
|
||||
}
|
||||
|
||||
private void primary() {
|
||||
Tok t = peek();
|
||||
if (t.kind.equals("num")) {
|
||||
number(next().text);
|
||||
} else if (accept("(")) {
|
||||
expr();
|
||||
expect(")");
|
||||
} else if (t.kind.equals("id") && REFS.contains(t.text) && peek(1).text.equals(":")) {
|
||||
next();
|
||||
next();
|
||||
StringBuilder ref = new StringBuilder(t.text + ":" + next().text);
|
||||
if (at("+") && peek(1).kind.equals("num") && !t.text.equals("sound") && !t.text.equals("tex")) {
|
||||
next();
|
||||
ref.append('+').append(next().text);
|
||||
}
|
||||
switch (t.text) {
|
||||
case "sound" -> emit("soundAddress " + ref);
|
||||
case "tex" -> emit("push " + ref);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private void number(String text) {
|
||||
Integer v = Num.parse(text);
|
||||
if (v == null) {
|
||||
throw err("bad number " + text);
|
||||
}
|
||||
emit((v >= Short.MIN_VALUE && v <= Short.MAX_VALUE ? "pushShort " : "push ") + text);
|
||||
}
|
||||
|
||||
/** op<inline, …>(args): pushes the arguments and emits the opcode; returns its mnemonic. */
|
||||
private String callExpr() {
|
||||
Tok t = peek();
|
||||
String mn = ident();
|
||||
OpTable.Op op = ops.byName(mn);
|
||||
mn = op.name();
|
||||
List<String> inline = new ArrayList<>();
|
||||
if (accept("<")) {
|
||||
StringBuilder cur = new StringBuilder();
|
||||
while (!at(">")) {
|
||||
if (peek().kind.equals("eof")) {
|
||||
throw err("missing '>'");
|
||||
}
|
||||
Tok x = next();
|
||||
if (x.text.equals(",")) {
|
||||
inline.add(cur.toString());
|
||||
cur.setLength(0);
|
||||
} else {
|
||||
cur.append(x.text);
|
||||
}
|
||||
}
|
||||
expect(">");
|
||||
inline.add(cur.toString());
|
||||
}
|
||||
int args = 0;
|
||||
if (accept("(")) {
|
||||
while (!at(")")) {
|
||||
expr();
|
||||
args++;
|
||||
if (!accept(",")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(")");
|
||||
}
|
||||
if (op.pops() >= 0 && args != op.pops()) {
|
||||
throw new CompileException(
|
||||
t.line, mn + " takes " + op.pops() + " value" + (op.pops() == 1 ? "" : "s") + ", got " + args);
|
||||
}
|
||||
emit(mn + (inline.isEmpty() ? "" : " " + String.join(", ", inline)));
|
||||
return mn;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,19 @@ public final class StratPseudo {
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
private record Block(int open, int close, String kind) {}
|
||||
/**
|
||||
* A structured block. if: open = the conditional jump, close = its target label; loop/do: open
|
||||
* = head label, close = the backward jump; switch: open = the switch, close = the end label.
|
||||
*/
|
||||
private record Block(int open, int close, String kind) {
|
||||
boolean breakable() {
|
||||
return !kind.equals("if");
|
||||
}
|
||||
|
||||
boolean isLoop() {
|
||||
return kind.startsWith("loop");
|
||||
}
|
||||
}
|
||||
|
||||
private String mn(StratListing.Line l) {
|
||||
String t = l.text();
|
||||
@@ -133,27 +145,64 @@ public final class StratPseudo {
|
||||
return sp < 0 ? "" : t.substring(sp + 1).trim();
|
||||
}
|
||||
|
||||
private static boolean condJump(int op) {
|
||||
return op >= 0x12 && op <= 0x15;
|
||||
}
|
||||
|
||||
private static boolean jump(int op) {
|
||||
return condJump(op) || op == 0x16 || op == 0x2b;
|
||||
}
|
||||
|
||||
// structure found by build(), used while emitting
|
||||
private final Map<Integer, Block> ifAt = new HashMap<>(), loopEnd = new HashMap<>(), switchAt = new HashMap<>();
|
||||
private final Map<Integer, List<Block>> opensAt = new HashMap<>();
|
||||
private final Map<Integer, Integer> closeCount = new HashMap<>(), elseAt = new HashMap<>();
|
||||
private final Map<Integer, Integer> switchDepth = new HashMap<>();
|
||||
private final Map<Integer, List<String>> caseAt = new HashMap<>();
|
||||
private final Set<Integer> elseJumps = new HashSet<>(), silentJumps = new HashSet<>();
|
||||
private final Map<Integer, String> jumpAs = new HashMap<>();
|
||||
private Map<String, Integer> labelIdx;
|
||||
|
||||
private Integer target(int i) {
|
||||
return labelIdx.get(operand(ls.get(i)));
|
||||
}
|
||||
|
||||
/** Index of the first non-label line at or after i (labels at the same address share it). */
|
||||
private int anchor(int i) {
|
||||
while (i < ls.size() && ls.get(i).kind() == StratListing.Kind.LABEL) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
private void build() {
|
||||
Map<String, Integer> labelIdx = new HashMap<>();
|
||||
labelIdx = new HashMap<>();
|
||||
for (int i = 0; i < ls.size(); i++) {
|
||||
if (ls.get(i).kind() == StratListing.Kind.LABEL) {
|
||||
labelIdx.put(ls.get(i).label(), i);
|
||||
}
|
||||
}
|
||||
Map<String, List<String[]>> tables = switchTables();
|
||||
// candidate blocks
|
||||
List<Block> cand = new ArrayList<>();
|
||||
Map<Integer, Integer> elseOf = new HashMap<>();
|
||||
List<Block> switches = new ArrayList<>();
|
||||
Map<Integer, List<int[]>> switchCases = new HashMap<>();
|
||||
for (int i = 0; i < ls.size(); i++) {
|
||||
StratListing.Line l = ls.get(i);
|
||||
if (l.kind() != StratListing.Kind.INSTR) {
|
||||
continue;
|
||||
}
|
||||
int op = l.op();
|
||||
if (op == StratOps.SWITCH) {
|
||||
switchCandidate(i, tables, switches, switchCases);
|
||||
continue;
|
||||
}
|
||||
Integer t = labelIdx.get(operand(l));
|
||||
if (t == null) {
|
||||
continue;
|
||||
}
|
||||
if ((op == 0x14 || op == 0x15 || op == 0x12 || op == 0x13) && t > i && clean(i, t)) {
|
||||
if (condJump(op) && t > i && clean(i, t)) {
|
||||
cand.add(new Block(i, t, "if"));
|
||||
int pj = prevInstr(t);
|
||||
if (pj > i && ls.get(pj).op() == 0x16) {
|
||||
@@ -164,54 +213,85 @@ public final class StratPseudo {
|
||||
}
|
||||
} else if ((op == 0x16 || op == 0x2b) && t <= i && clean(t, i)) {
|
||||
cand.add(new Block(t, i, op == 0x2b ? "loopYield" : "loop"));
|
||||
} else if (condJump(op) && t <= i && clean(t, i)) {
|
||||
cand.add(new Block(t, i, "do"));
|
||||
}
|
||||
}
|
||||
List<Block> accepted = new ArrayList<>();
|
||||
Set<Integer> elseAccepted = new HashSet<>();
|
||||
cand.sort((a, b) -> a.open != b.open ? Integer.compare(a.open, b.open) : Integer.compare(b.close, a.close));
|
||||
for (Block b : cand) {
|
||||
int end = b.kind.equals("if") && elseOf.containsKey(b.open) ? elseOf.get(b.open) : b.close;
|
||||
boolean ok = true;
|
||||
for (Block a : accepted) {
|
||||
int ae = a.kind.equals("if") && elseAccepted.contains(a.open) ? elseOf.get(a.open) : a.close;
|
||||
ok &= nests(b.open, end, a.open, ae);
|
||||
if (ok && b.kind.equals("if") && elseOf.containsKey(b.open)) {
|
||||
ok &= nests(b.open, b.close, a.open, ae) && nests(b.close, end, a.open, ae);
|
||||
}
|
||||
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);
|
||||
}
|
||||
boolean withElse = b.kind.equals("if") && elseOf.containsKey(b.open);
|
||||
if (withElse && !fits(b, true, accepted, elseOf, elseAccepted)) {
|
||||
withElse = false;
|
||||
}
|
||||
if (ok && accepted.stream().noneMatch(a -> a.open == b.open)) {
|
||||
if (fits(b, withElse, accepted, elseOf, elseAccepted)
|
||||
&& accepted.stream().noneMatch(a -> a.open == b.open && a.close == b.close)
|
||||
&& !(b.isLoop() && accepted.stream().anyMatch(a -> a.open == b.open && a.isLoop()))
|
||||
&& (b.kind.equals("if")
|
||||
|| accepted.stream().noneMatch(a -> a.open == b.open && a.kind.equals("if")))) {
|
||||
accepted.add(b);
|
||||
if (elseOf.containsKey(b.open) && b.kind.equals("if")) {
|
||||
if (withElse) {
|
||||
elseAccepted.add(b.open);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<Integer, Block> ifAt = new HashMap<>(), loopAt = new HashMap<>(), loopEnd = new HashMap<>();
|
||||
Map<Integer, Integer> closeCount = new HashMap<>(), elseAt = new HashMap<>();
|
||||
Set<Integer> elseJumps = new HashSet<>();
|
||||
Map<String, Integer> consumed = new HashMap<>();
|
||||
for (Block b : accepted) {
|
||||
if (b.kind.equals("if")) {
|
||||
ifAt.put(b.open, b);
|
||||
consumed.merge(ls.get(b.close).label(), 1, Integer::sum);
|
||||
if (elseAccepted.contains(b.open)) {
|
||||
int m = elseOf.get(b.open);
|
||||
elseAt.put(b.close, b.open);
|
||||
elseJumps.add(prevInstr(b.close));
|
||||
consumed.merge(ls.get(m).label(), 1, Integer::sum);
|
||||
closeCount.merge(m, 1, Integer::sum);
|
||||
} else {
|
||||
closeCount.merge(b.close, 1, Integer::sum);
|
||||
for (Block sw : switches) {
|
||||
if (!fits(sw, false, accepted, elseOf, elseAccepted)) {
|
||||
continue;
|
||||
}
|
||||
boolean ok = true;
|
||||
for (int[] c : switchCases.get(sw.open)) {
|
||||
for (Block a : accepted) {
|
||||
int ae = end(a, elseOf, elseAccepted);
|
||||
if (a.open > sw.open && ae <= sw.close && a.open < c[0] && c[0] < ae) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loopAt.computeIfAbsent(b.open, k -> b);
|
||||
loopEnd.put(b.close, b);
|
||||
consumed.merge(ls.get(b.open).label(), 1, Integer::sum);
|
||||
}
|
||||
if (ok) {
|
||||
accepted.add(sw);
|
||||
}
|
||||
}
|
||||
Map<String, Integer> consumed = new HashMap<>();
|
||||
for (Block b : accepted) {
|
||||
switch (b.kind) {
|
||||
case "if" -> {
|
||||
ifAt.put(b.open, b);
|
||||
consumed.merge(ls.get(b.close).label(), 1, Integer::sum);
|
||||
if (elseAccepted.contains(b.open)) {
|
||||
int m = elseOf.get(b.open);
|
||||
elseAt.put(b.close, b.open);
|
||||
elseJumps.add(prevInstr(b.close));
|
||||
consumed.merge(ls.get(m).label(), 1, Integer::sum);
|
||||
closeCount.merge(m, 1, Integer::sum);
|
||||
} else {
|
||||
closeCount.merge(b.close, 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
case "switch" -> {
|
||||
switchAt.put(b.open, b);
|
||||
closeCount.merge(b.close, 1, Integer::sum);
|
||||
silentJumps.add(b.open + 1);
|
||||
consumed.merge(ls.get(b.close).label(), 1, Integer::sum);
|
||||
for (int[] c : switchCases.get(b.open)) {
|
||||
String text = c[1] < 0
|
||||
? "default:"
|
||||
: "case " + tables.get(operand(ls.get(b.open))).get(c[1])[0] + ":";
|
||||
caseAt.computeIfAbsent(c[0], k -> new ArrayList<>()).add(text);
|
||||
caseOwner.put(c[0], b.open);
|
||||
consumed.merge(ls.get(c[0]).label(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
opensAt.computeIfAbsent(b.open, k -> new ArrayList<>()).add(b);
|
||||
loopEnd.put(b.close, b);
|
||||
consumed.merge(ls.get(b.open).label(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
opensAt.values().forEach(l -> l.sort((a, b) -> Integer.compare(b.close, a.close)));
|
||||
resolveJumps(accepted, consumed);
|
||||
Map<String, Integer> refs = new HashMap<>();
|
||||
for (StratListing.Line l : ls) {
|
||||
if (l.kind() == StratListing.Kind.INSTR || l.kind() == StratListing.Kind.DATA) {
|
||||
@@ -245,6 +325,13 @@ public final class StratPseudo {
|
||||
out(-1, "} else {");
|
||||
depth++;
|
||||
}
|
||||
if (caseAt.containsKey(i)) {
|
||||
depth = switchDepth.get(caseOwner.get(i));
|
||||
for (String t : caseAt.get(i)) {
|
||||
out(-1, t);
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
int r = refs.getOrDefault(l.label(), 0) - consumed.getOrDefault(l.label(), 0);
|
||||
if (src.strats.containsKey(l.offset())) {
|
||||
out(l.offset(), "");
|
||||
@@ -254,8 +341,8 @@ public final class StratPseudo {
|
||||
|| l.label().startsWith("ev_")) {
|
||||
out(l.offset(), l.label() + ":");
|
||||
}
|
||||
if (loopAt.containsKey(i)) {
|
||||
out(-1, "loop {");
|
||||
for (Block b : opensAt.getOrDefault(i, List.of())) {
|
||||
out(-1, b.kind.equals("do") ? "do {" : "loop {");
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
@@ -263,10 +350,245 @@ public final class StratPseudo {
|
||||
flush();
|
||||
out(l.offset(), l.text());
|
||||
}
|
||||
case INSTR -> instr(i, l, ifAt, loopEnd, elseJumps);
|
||||
case INSTR -> instr(i, l);
|
||||
}
|
||||
}
|
||||
flush();
|
||||
whileLoops();
|
||||
elseIfChains();
|
||||
}
|
||||
|
||||
private final Map<Integer, Integer> caseOwner = new HashMap<>();
|
||||
|
||||
private static int end(Block a, Map<Integer, Integer> elseOf, Set<Integer> elseAccepted) {
|
||||
return a.kind.equals("if") && elseAccepted.contains(a.open) ? elseOf.get(a.open) : a.close;
|
||||
}
|
||||
|
||||
private static boolean fits(
|
||||
Block b, boolean withElse, List<Block> accepted, Map<Integer, Integer> elseOf, Set<Integer> elseAccepted) {
|
||||
int end = withElse ? elseOf.get(b.open) : b.close;
|
||||
for (Block a : accepted) {
|
||||
int ae = end(a, elseOf, elseAccepted);
|
||||
boolean ok = nests(b.open, end, a.open, ae);
|
||||
if (ok && withElse) {
|
||||
ok = nests(b.open, b.close, a.open, ae) && nests(b.close, end, a.open, ae);
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Switch tables of the unit: table label → rows {case value text, target label}. */
|
||||
private Map<String, List<String[]>> switchTables() {
|
||||
Map<String, List<String[]>> out = new HashMap<>();
|
||||
for (int i = 0; i + 1 < ls.size(); i++) {
|
||||
if (ls.get(i).kind() == StratListing.Kind.LABEL
|
||||
&& ls.get(i + 1).kind() == StratListing.Kind.DATA
|
||||
&& ls.get(i + 1).text().startsWith(".switch ")) {
|
||||
Integer n = Num.parse(ls.get(i + 1).text().substring(8).trim());
|
||||
List<String[]> rows = new ArrayList<>();
|
||||
for (int k = 0; n != null && k < n && i + 2 + k < ls.size(); k++) {
|
||||
String t = ls.get(i + 2 + k).text();
|
||||
if (!t.startsWith(".case ")) {
|
||||
break;
|
||||
}
|
||||
t = t.substring(6).trim();
|
||||
if (t.startsWith("imm ")) {
|
||||
t = t.substring(4);
|
||||
}
|
||||
int comma = t.lastIndexOf(',');
|
||||
rows.add(new String[] {
|
||||
t.substring(0, comma).trim(), t.substring(comma + 1).trim()
|
||||
});
|
||||
}
|
||||
if (n != null && rows.size() == n) {
|
||||
out.put(ls.get(i).label(), rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** switch at s followed by "jmp E": cases (and default) inside (s, E] become a switch block. */
|
||||
private void switchCandidate(
|
||||
int s, Map<String, List<String[]>> tables, List<Block> out, Map<Integer, List<int[]>> cases) {
|
||||
List<String[]> rows = tables.get(operand(ls.get(s)));
|
||||
if (rows == null || s + 1 >= ls.size()) {
|
||||
return;
|
||||
}
|
||||
StratListing.Line j = ls.get(s + 1);
|
||||
if (j.kind() != StratListing.Kind.INSTR || j.op() != 0x16) {
|
||||
return;
|
||||
}
|
||||
Integer d = target(s + 1);
|
||||
if (d == null || d <= s + 1) {
|
||||
return;
|
||||
}
|
||||
List<int[]> cs = new ArrayList<>();
|
||||
int e = d;
|
||||
for (int k = 0; k < rows.size(); k++) {
|
||||
Integer t = labelIdx.get(rows.get(k)[1]);
|
||||
if (t == null || t <= s + 1) {
|
||||
return;
|
||||
}
|
||||
cs.add(new int[] {t, k});
|
||||
e = Math.max(e, t);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (endIdx < 0) {
|
||||
return;
|
||||
}
|
||||
cs.add(new int[] {d, -1});
|
||||
}
|
||||
if (!clean(s, endIdx)) {
|
||||
return;
|
||||
}
|
||||
for (int[] c : cs) {
|
||||
if (c[0] > endIdx) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
int stop = endIdx;
|
||||
cs.removeIf(c -> c[0] == stop);
|
||||
if (cs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
cs.sort((a, b) -> Integer.compare(a[0], b[0]));
|
||||
if (anchor(cs.get(0)[0]) != anchor(s + 2)) {
|
||||
return;
|
||||
}
|
||||
out.add(new Block(s, endIdx, "switch"));
|
||||
cases.put(s, cs);
|
||||
}
|
||||
|
||||
/** Remaining jumps to the end (or head) of the innermost enclosing loop/switch become break/continue. */
|
||||
private void resolveJumps(List<Block> accepted, Map<String, Integer> consumed) {
|
||||
for (int i = 0; i < ls.size(); i++) {
|
||||
StratListing.Line l = ls.get(i);
|
||||
if (l.kind() != StratListing.Kind.INSTR
|
||||
|| !jump(l.op())
|
||||
|| ifAt.containsKey(i)
|
||||
|| loopEnd.containsKey(i)
|
||||
|| elseJumps.contains(i)
|
||||
|| silentJumps.contains(i)) {
|
||||
continue;
|
||||
}
|
||||
Integer t = target(i);
|
||||
if (t == null) {
|
||||
continue;
|
||||
}
|
||||
Block in = null;
|
||||
for (Block b : accepted) {
|
||||
if (b.breakable()
|
||||
&& b.open < i
|
||||
&& i < b.close
|
||||
&& (in == null || b.open >= in.open && b.close <= in.close)) {
|
||||
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 (as != null) {
|
||||
jumpAs.put(i, as);
|
||||
consumed.merge(ls.get(t).label(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** "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 {")
|
||||
&& 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 + ") {"));
|
||||
lines.remove(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** "} 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++) {
|
||||
Line a = lines.get(i), b = lines.get(i + 1);
|
||||
if (a.text.startsWith("if (")
|
||||
&& a.text.endsWith(") {")
|
||||
&& b.text.equals("} else {")
|
||||
&& b.depth == a.depth) {
|
||||
String c = a.text.substring(4, a.text.length() - 3);
|
||||
String n = c.startsWith("!(") && c.endsWith(")") && balanced(c.substring(2, c.length() - 1))
|
||||
? c.substring(2, c.length() - 1)
|
||||
: "!(" + c + ")";
|
||||
lines.set(i, new Line(a.offset, a.depth, "if (" + n + ") {"));
|
||||
lines.remove(i + 1);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i + 1 < lines.size(); i++) {
|
||||
Line a = lines.get(i), b = lines.get(i + 1);
|
||||
if (!a.text.equals("} else {")
|
||||
|| b.depth != a.depth + 1
|
||||
|| !b.text.startsWith("if (")
|
||||
|| !b.text.endsWith(" {")) {
|
||||
continue;
|
||||
}
|
||||
int d = a.depth;
|
||||
int close = -1;
|
||||
boolean ok = true;
|
||||
for (int k = i + 2; k < lines.size(); k++) {
|
||||
Line x = lines.get(k);
|
||||
if (x.depth <= d) {
|
||||
close = x.depth == d && x.text.equals("}") ? k : -1;
|
||||
break;
|
||||
}
|
||||
if (x.depth == d + 1 && !x.text.startsWith("}")) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok || close < 0 || !lines.get(close - 1).text.equals("}") || lines.get(close - 1).depth != d + 1) {
|
||||
continue;
|
||||
}
|
||||
lines.set(i, new Line(b.offset, d, "} else " + b.text));
|
||||
lines.remove(i + 1);
|
||||
close--;
|
||||
lines.remove(close - 1);
|
||||
close--;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean nests(int a0, int a1, int b0, int b1) {
|
||||
@@ -339,8 +661,7 @@ public final class StratPseudo {
|
||||
return d == 0;
|
||||
}
|
||||
|
||||
private void instr(
|
||||
int i, StratListing.Line l, Map<Integer, Block> ifAt, Map<Integer, Block> loopEnd, Set<Integer> elseJumps) {
|
||||
private void instr(int i, StratListing.Line l) {
|
||||
int op = l.op();
|
||||
String mn = mn(l);
|
||||
String a = operand(l);
|
||||
@@ -388,18 +709,29 @@ public final class StratPseudo {
|
||||
push(off, "!" + pop());
|
||||
return;
|
||||
}
|
||||
boolean cond = op >= 0x12 && op <= 0x15;
|
||||
if (cond) {
|
||||
if (condJump(op)) {
|
||||
String e = strip(pop());
|
||||
boolean neg = op == 0x13 || op == 0x15;
|
||||
String yield = op <= 0x13 ? " /*yield*/" : "";
|
||||
int b0 = begin(off);
|
||||
flush();
|
||||
Block lp = loopEnd.get(i);
|
||||
String jumpIf = neg ? e : "!(" + e + ")";
|
||||
if (ifAt.containsKey(i)) {
|
||||
out(b0, "if (" + (neg ? "!(" + e + ")" : e) + ")" + yield + " {");
|
||||
depth++;
|
||||
} 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));
|
||||
} else {
|
||||
out(b0, "} while (" + jumpIf + ")" + yield);
|
||||
}
|
||||
} else if (jumpAs.containsKey(i)) {
|
||||
out(b0, "if (" + jumpIf + ") " + jumpAs.get(i) + yield);
|
||||
} else {
|
||||
out(b0, "if (" + (neg ? e : "!(" + strip(e) + ")") + ") goto " + a + yield);
|
||||
out(b0, "if (" + jumpIf + ") goto " + a + yield);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -412,15 +744,26 @@ public final class StratPseudo {
|
||||
}
|
||||
depth--;
|
||||
out(-1, "}");
|
||||
} else if (elseJumps.contains(i)) {
|
||||
} else if (elseJumps.contains(i) || silentJumps.contains(i)) {
|
||||
if (op == 0x2b) {
|
||||
out(off, "yield");
|
||||
}
|
||||
} else if (jumpAs.containsKey(i)) {
|
||||
out(off, (op == 0x2b ? "yield; " : "") + jumpAs.get(i));
|
||||
} else {
|
||||
out(off, (op == 0x2b ? "yield; " : "") + "goto " + a);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (op == StratOps.SWITCH && switchAt.containsKey(i)) {
|
||||
String v = strip(pop());
|
||||
int b0 = begin(off);
|
||||
flush();
|
||||
switchDepth.put(i, depth);
|
||||
out(b0, "switch (" + v + ") { // " + a);
|
||||
depth++;
|
||||
return;
|
||||
}
|
||||
if (op == 0x11 || op == 0x10) {
|
||||
flush();
|
||||
out(off, "call " + a + (op == 0x10 ? " /*yield*/" : "") + "()");
|
||||
|
||||
@@ -37,30 +37,30 @@
|
||||
0x23 error_0x23 - 0 0 Prints "stCollide_AlienVarAddress" not-implemented error; unused. high stCollide_AlienVarAddress
|
||||
0x24 pushTarget2Prop prop 0 1 Pushes property n of the second target strat (DAT_006D9E34, set by 0xca), 0 if none; unused. medium stTarget_AlienVar
|
||||
0x25 pushTarget2PropAddr prop 0 1 Pushes address of target2 property n (0 if none); unused. medium stTarget_AlienVarAddress
|
||||
0x26 pushBossProp prop 0 1 Pushes property n of the strat in DAT_006D9E24 (set by 0xef), no null check; unused. low stBoss_AlienVar
|
||||
0x27 pushBossPropAddr prop 0 1 Pushes address of property n of the DAT_006D9E24 strat; unused. low stBoss_AlienVarAddress
|
||||
0x28 pushDialogProp prop 0 1 Pushes property n of the strat in DAT_006D9E2C (set by 0x113, used by NPC/puzzle strats), 0 if none. low stDialog_AlienVar
|
||||
0x29 pushDialogPropAddr prop 0 1 Pushes address of property n of the DAT_006D9E2C strat (0 if none); unused. low stDialog_AlienVarAddress
|
||||
0x26 pushBossProp prop 0 1 Pushes property n of the boss strat DAT_006D9E24 (set by 0xef isBoss) via the getter FUN_00550E60; no null check; unused in the levels. high stBoss_AlienVar
|
||||
0x27 pushBossPropAddr prop 0 1 Pushes the address of property n of the boss strat DAT_006D9E24 (setter FUN_005509F0 returns the address); no null check; unused. high stBoss_AlienVarAddress
|
||||
0x28 pushDialogProp prop 0 1 Pushes property n of the strat registered by 0x113 (DAT_006D9E2C: the NPC/puzzle/door that a cut-scene is about), 0 if none. WAD use: Camera and CutSceneCameraDirectional read its x/y/z/rotY to look at it (1585 of 1678 uses). high stDialog_AlienVar
|
||||
0x29 pushDialogPropAddr prop 0 1 Pushes the address of property n of the DAT_006D9E2C strat (0 if none); unused. high stDialog_AlienVarAddress
|
||||
0x2a shadow imm 0 0 arg!=0 sets, 0 clears +0xE8 bit 0x20 (shadow on; shadow pass requires it plus size from 0x59 and texture from 0x5a). medium stShadow
|
||||
0x2b jmpYield rel 0 0 Jumps to next+arg*4 and yields this frame (loop back-edge / wait one frame). high -
|
||||
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 (with 0x8000 both set the strat skips player-distance updates; property 0xf2 reads it); meaning unclear. low -
|
||||
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 -
|
||||
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 (checked by collision/floor code via +0xC0, readable as property 0x1f). low -
|
||||
0x32 flagE8_80000000 imm 0 0 arg sets/clears +0xE8 bit 31; unused. low -
|
||||
0x33 flagEC100 imm 0 0 arg sets/clears +0xEC bit 0x100 (tested in the collision response, skips floor handling; used by drops/projectiles/rollercoaster). low -
|
||||
0x34 flagEC200000 imm 0 0 arg sets/clears +0xEC bit 0x200000 (used by NPC helpers/canoe/llama). low -
|
||||
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 -
|
||||
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 -
|
||||
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 -
|
||||
0x37 popPlayerProp playerProp 1 0 Pops value and stores it into player property n (discarded if no player). high -
|
||||
0x38 popCameraProp prop 1 0 Pops value into camera strat property n (discarded if none). high -
|
||||
0x39 popTargetProp prop 1 0 Pops value into camera target (DAT_006D9E30) property n (discarded if none). medium -
|
||||
0x3a popTarget2Prop prop 1 0 Pops value into DAT_006D9E34 strat property n (discarded if none); unused. medium -
|
||||
0x3b popDialogProp prop 1 0 Pops value into DAT_006D9E2C strat property n (discarded if none); unused. low -
|
||||
0x3b popDialogProp prop 1 0 Pops a value into property n of the DAT_006D9E2C strat (discarded if none); unused. high -
|
||||
0x3c error_0x3c - 0 0 Prints "stCollide_AlienVar_Equals" not-implemented error; unused. high -
|
||||
0x3d popBossProp prop 1 0 Pops value into DAT_006D9E24 strat property n (no null check); unused. low -
|
||||
0x3d popBossProp prop 1 0 Pops a value into property n of the boss strat DAT_006D9E24 (no null check); unused. high -
|
||||
0x3e popParentVar var 1 0 Pops value into variable n of the +0xC strat. high -
|
||||
0x3f popParentProp prop 1 0 Pops value into property n of the +0xC strat; unused. high -
|
||||
0x40 popProp prop 1 0 Pops value into own property n (via FUN_005509F0 address). high -
|
||||
@@ -134,15 +134,15 @@
|
||||
0x84 collHeight - 0 0 Unimplemented: logs "stCollheight" only. high stCollHeight
|
||||
0x85 collExtent - 1 0 pops extent; stores its low 16 bits raw into short +0xC4. medium stCollextent
|
||||
0x86 collView - 0 0 Unimplemented: logs "stCollview" only. high stCollView
|
||||
0x87 nop_0x87 - 0 0 Shared no-op handler (0x426500); unused. Slot of Croc2 stCollPoints. low stCollPoints
|
||||
0x88 nop_0x88 - 0 0 Shared no-op handler (0x426500); unused. Slot of Croc2 stCollSetPoint. low stCollSetPoint
|
||||
0x87 nop_0x87 - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Slot of Croc2 stCollPoints. high stCollPoints
|
||||
0x88 nop_0x88 - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Slot of Croc2 stCollSetPoint. high stCollSetPoint
|
||||
0x89 createTrigger - 0 0 Sets flag 0x40 in +0xE8 (strat becomes a trigger/touchable, used by pickups like Health, Carrot). medium stCreateTrigger
|
||||
0x8a killTrigger - 0 0 Clears flags 0x40 and 0x4000000 in +0xE8 (undoes createTrigger). medium stKillTrigger
|
||||
0x8b wait - 1 0 pops frames (fixed, >>12) into wait counter short +0xFE and sets flag 0x2 in +0xE8 (strat sleeps until the counter runs out). high stWait
|
||||
0x8c hold - 0 0 Sets flags 0x12 (0x2 wait + 0x10 stopped) in +0xE8: suspends the strat indefinitely until released. medium stHold
|
||||
0x8d release - 0 0 Unimplemented: logs "stRelease" only. high stRelease
|
||||
0x8e remove - 0 0 Sets flags 0x6 in +0xE8 (yield + kill); same handler as 0xb4 end. high stRemove
|
||||
0x8f nop_0x8f - 0 0 Shared no-op handler (0x426500); unused. Slot of Croc2 stMapRemove. low stMapRemove
|
||||
0x8f nop_0x8f - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Slot of Croc2 stMapRemove. high stMapRemove
|
||||
0x90 mapAdd - 0 0 Unimplemented: logs "stMapadd" only. high stMapAdd
|
||||
0x91 mapReplace - 0 0 Unimplemented: logs "stMapreplace" only. high stMapReplace
|
||||
0x92 activated - 0 0 Sets bit 0x8 in byte +0x44 of the strat's map-instance record (+0xF0): marks the map object as activated (persistent state). medium stActivated
|
||||
@@ -155,8 +155,8 @@
|
||||
0x99 soundStop - 1 0 pops sound channel; stops that channel if owned by this strat, or (channel <0/invalid) all sounds of this strat. high stSoundStop
|
||||
0x9a musicPlay - 1 0 pops music track index (fixed, >>12); if different from the current track (0x586430) starts that ASF music stream (FUN_00546b70). high stCdPlay
|
||||
0x9b streamPlay - 0 0 Unimplemented: logs "stStreamPlay" only. high -
|
||||
0x9c speechSetPos - 1 0 pops a value (fixed, >>12) into global 0x6DA2B8 (meaning unknown, likely speech volume/range) and moves the speech/voice stream (handle 0x6D94B4, started by '#' text) to this strat's position. low -
|
||||
0x9d discard_0x9d - 1 0 pops a value (>>12) and passes it to the no-op debug print; effectively drop. Croc2 slot midiVolume/cdFade. low stMidiVolume
|
||||
0x9c speechSetPos - 1 0 Pops a value into global 0x6DA2B8 (written only, never read: no effect) and moves the speech/voice channel (DAT_006D94A0, started by "#" texts) to this strat's position (+0x30..0x38), so the voice plays from the speaker. high -
|
||||
0x9d discard_0x9d - 1 0 Pops a value (>>12) and passes it to the empty function 0x426500: a pure drop on PC. Croc2 slot midiVolume/cdFade. high stMidiVolume
|
||||
0x9e speechStop - 0 0 Stops the current speech/voice stream (handle 0x6D94B4) if one is playing and resets the handle to -1. high stMidiStop
|
||||
0x9f streamQueue - 0 0 Unimplemented: logs "stStreamQueue" only. high stMidiQueue
|
||||
0xa0 isLight - 0 0 Unimplemented: logs "stIsLight" only. high stIsLight
|
||||
@@ -176,7 +176,7 @@
|
||||
0xae cos - 1 1 pops angle; pushes cos (table 0x574318 offset by quarter turn 0x400). high stCos
|
||||
0xaf not - 1 1 pops x; pushes 1 if x==0 else 0. high stNot
|
||||
0xb0 pop - 1 0 Drops the top stack value (stack pointer +0x104 -= 4). high stPop
|
||||
0xb1 stkCmp imm 0 1 Peeks (does not pop) the top stack entry, dereferences it as a pointer and pushes imm minus the pointed-to int (compare helper). low stStkCmp
|
||||
0xb1 stkCmp imm 0 1 Reads one inline word imm (pc += 4), peeks the top stack entry (no pop), dereferences it as an int pointer and pushes imm − *top. Unused in the levels. high stStkCmp
|
||||
0xb2 address addr 0 1 Patches itself into 0x45 (push immediate) after resolving the operand to an absolute address (negative = import table 0x6DA324, else + STPC base 0x6D9DBC) and pushes it (code/model/anim address). high stAddress
|
||||
0xb3 return - 1 0 pops code address and jumps there (pc = popped). high stReturn
|
||||
0xb4 end - 0 0 Sets flags 0x6 in +0xE8 (yield + kill): ends the strat. Same handler as 0x8e. high stEndStrat
|
||||
@@ -186,36 +186,36 @@
|
||||
0xb8 bitwiseAnd - 2 1 pops b, a; pushes a & b. high stBitwiseAnd
|
||||
0xb9 discard_0xb9 - 1 0 Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 ext-var slots). medium -
|
||||
0xba discard_0xba - 1 0 Shared stub 0x554fb0: pops one value and discards it. Unused. medium -
|
||||
0xbb nop_0xbb - 0 0 Shared no-op handler (0x426500); unused. low -
|
||||
0xbb nop_0xbb - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. high -
|
||||
0xbc notEqual - 2 1 pops b, a; pushes 1 if a != b else 0. high stNotEqual
|
||||
0xbd greaterEqual - 2 1 pops b, a; pushes 1 if a >= b else 0. high stGreaterEqual
|
||||
0xbe lessEqual - 2 1 pops b, a; pushes 1 if a <= b else 0. high stLessEqual
|
||||
0xbf rnd - 1 1 pops n; pushes random value (two rand() halves) modulo n, 0 if n==0; result is in the same units as n (fixed in, fixed out). high stRnd
|
||||
0xc0 loseHeart - 0 0 In game state 3, unless player invulnerable (0x584710 bit 0): health 0x584E70 -= 1.0, or if already 0 sets 0x584638 = 2 (player dies); refreshes HUD (FUN_004268c0(1)). high stLoseHeart
|
||||
0xc1 nop_0xc1 - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stResettoCheckPoint. low stResettoCheckPoint
|
||||
0xc1 nop_0xc1 - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stResettoCheckPoint. high stResettoCheckPoint
|
||||
0xc2 forceCollision - 0 0 Runs world collision for this strat (FUN_00403020 with level geometry): updates floor/ceiling/wall info in the collision block +0xC0 (ground height, surface type). medium stForceCollision
|
||||
0xc3 turnFromPlayerY - 1 0 pops max turn step (angle, 0x1000000 = full turn); turns yaw +0x24 toward the direction from the player (0x6D9E1C) to this strat, i.e. away from the player, by at most step. medium stTurnFromPlayerY
|
||||
0xc4 rumble - 2 0 Shared stub 0x555090: pops two values and discards them (PSX pad rumble/vibrate not implemented on PC). low stRumble
|
||||
0xc5 suspendIfTooFar - 1 0 Shared stub 0x554fb0: pops one value (likely distance) and discards it. low stSuspendIfTooFar
|
||||
0xc4 rumble - 2 0 Shared stub 0x555090: pops two values and discards them (PSX pad rumble is not implemented on PC). high stRumble
|
||||
0xc5 suspendIfTooFar - 1 0 Shared stub 0x554fb0: pops one value (distance) and discards it – no effect on PC (op 0x2f gives the off-camera freeze instead). high stSuspendIfTooFar
|
||||
0xc6 collisionBone - 2 0 pops value (short), then bone id (fixed, >>12); sets/adds entry {id, value} in the bone table at +0xC8 (count +0xC6). medium stCollisionBone
|
||||
0xc7 useBone - 1 0 pops one value (ignored); if a model/anim is set (+0x14) rebuilds the rotation matrix and stores a transformed point (matrix * local vector + translation +0x94) into +0xD8/+0xDC/+0xE0. low stUseBone
|
||||
0xc7 useBone - 1 0 Pops bone n (>>12); if the strat has an animated model (+0x14) it takes bone n of the current animation frame (+0x110; anim+0x1C frame table, 64 bytes per bone, offset +0x30..0x38 = bone position), rotates it by the strat's rotation matrix and adds the position: world point in +0xD8/+0xDC/+0xE0 = properties boneX/Y/Z. (The Ghidra C output drops the bone index; read from the assembly.) Used to spawn effects at hands/mouths. high stUseBone
|
||||
0xc8 isCamera - 0 0 Registers this strat as the camera (global 0x6D9E28) and sets flag 0x8000000 in +0xE8. high stIsCamera
|
||||
0xc9 lookAtMe - 0 0 Sets camera look-at target global 0x6D9E30 = this strat. high stLookatme
|
||||
0xca lookAtMe2 - 0 0 Sets secondary look-at global 0x6D9E34 = this strat. Unused. medium stLookatme
|
||||
0xcb pushCamera - 0 0 Pushes current camera (0x6D9E28) and look-at (0x6D9E30) pair onto the camera stack 0x6D9DCC (index 0x6D9E40). high stPushCamera
|
||||
0xcc popCamera - 0 0 If the camera stack is not empty, restores camera and look-at from it and increments camera-change counter 0x6D9D34. high stPopCamera
|
||||
0xcd nop_0xcd - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stResetCamerapos. low stResetCamerapos
|
||||
0xcd nop_0xcd - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stResetCamerapos. high stResetCamerapos
|
||||
0xce gainHeart - 0 0 Health 0x584E70 += 1.0 if below max 0x584E6C; refreshes HUD (FUN_004268c0(1)). high stGainHeart
|
||||
0xcf nop_0xcf - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stGainHeartPot. low stGainHeartPot
|
||||
0xcf nop_0xcf - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stGainHeartPot. high stGainHeartPot
|
||||
0xd0 addInv imm 0 0 Adds inline item to inventory via FUN_00426860(item,1,1,0,0), which is a stub returning 0 on PC (no effect). medium stAddInv
|
||||
0xd1 nop_0xd1 - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stGainCrystal. low stGainCrystal
|
||||
0xd2 discard_0xd2 - 1 0 Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 slot stCutscene). low stCutscene
|
||||
0xd1 nop_0xd1 - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stGainCrystal. high stGainCrystal
|
||||
0xd2 discard_0xd2 - 1 0 Shared stub 0x554fb0: pops one value and discards it. Unused (Croc2 slot stCutscene). high stCutscene
|
||||
0xd3 inventory imm 0 1 Queries inline item via stub FUN_00426860 and pushes result<<12 (always 0 on PC). medium stInventory
|
||||
0xd4 debugName imm,count 0 0 Strat name: skips the first inline word, reads the byte length of the following name string, sets the name pointer (+0x18) to that string and jumps past it. high stDebugname
|
||||
0xd5 soundPlay - 1 0 pops sound id; plays it on this strat (FUN_00546170); outside game states 3/4 first sets game state 0x5846E4 = 5. high stSoundPlay
|
||||
0xd6 soundPlayAss - 1 1 pops sound id; in game states 3/4 plays it and pushes the channel (raw int), else pushes -1. high stSoundPlayass
|
||||
0xd7 soundAddress sound 0 1 Pushes the inline sound id (same handler as 0x45 push immediate). high stSoundAddress
|
||||
0xd8 nop_0xd8 - 0 0 Shared no-op handler (0x426500); unused. Croc2 slot stOnground/stObjectFallslow. low stOnground
|
||||
0xd8 nop_0xd8 - 0 0 Shared empty handler 0x426500 (verified in the handler table); unused in the levels. Croc2 slot stOnground/stObjectFallslow. high stOnground
|
||||
0xd9 collisionOffset - 3 0 pops z, y, x; stores low 16 bits into shorts +0xD0, +0xCE, +0xCC (collision centre offset). high stCollisionOffset
|
||||
0xda abs - 1 1 pops x; pushes |x|. high stAbs
|
||||
0xdb pickup imm 0 0 inline flag: nonzero sets bit 0x200 in +0xEC, zero clears it (strat can be picked up/carried). medium stPickup
|
||||
@@ -241,7 +241,7 @@
|
||||
0xef isBoss - 0 0 registers this strat as the boss: DAT_006d9e24 = self high stIsboss
|
||||
0xf0 topSay text 0 0 shows dialog text (inline text index) in the top text box: sets DAT_006d9858=1 and formats via FUN_0040d4c0 high stTopsay
|
||||
0xf1 getParentPos - 0 0 copies parent's (+0xC) orientation+position block (+0x20..+0x3C, 8 words) into this strat high stGetparentpos
|
||||
0xf2 setDrawMode - 1 0 pops mode (raw int): 1 sets flag 0x80000 in +0xEC, 2 sets 0x100000, other clears both (exact effect of the bits unknown); unused low -
|
||||
0xf2 setDrawMode - 1 0 Pops mode (raw int): 1 sets +0xEC 0x80000, 2 sets 0x100000, anything else clears both; no reader of these two bits found in the exe; unused in the levels. low -
|
||||
0xf3 afterBoss - 0 0 if no boss (DAT_006d9e24) rewinds pc and yields (waits); else relinks this strat in the process list right after the boss high stAfterboss
|
||||
0xf4 afterPlayer - 0 0 if no player (DAT_006d9e1c) rewinds pc and yields (waits); else relinks this strat in the process list right after the player high stAfterPlayer
|
||||
0xf5 beforePlayer - 0 0 if no player rewinds pc and yields (waits); else relinks this strat in the process list right before the player high stBeforePlayer
|
||||
@@ -262,7 +262,7 @@
|
||||
0x104 moveDownQ - 1 0 pops distance (fixed); y(+0x34) -= d high stMoveDownq
|
||||
0x105 forcePlayerDist - 0 0 recomputes the cached squared player distance +0x148 (coords >>6) and clears +0x1C; 0x200000 if no player; unused high stForcePlayerdist
|
||||
0x106 nop_0x106 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stShadeType?
|
||||
0x107 setAnimSpeed - 1 0 pops value and stores its low 16 bits (raw, not >>12) in halfword +0x114 (animation timing field, zeroed when the anim frame is set) low stSetanimSpeed?
|
||||
0x107 setAnimSpeed - 1 0 Pops the animation speed (20.12, 1.0 = one animation frame per game frame) into halfword +0x114; while animating (+0xE8 bit 8) the frame loop adds it to the sub-frame counter +0x116 and advances the frame (+0x110) each time it passes 1.0 (0x54c630). WAD use: 1.0, 0.25, 1.5 and computed values. high stSetanimSpeed?
|
||||
0x108 nop_0x108 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high stCheckleveldoor?
|
||||
0x109 nop_0x109 - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); unused high -
|
||||
0x10a nop_0x10a - 0 0 no-op handler FUN_00426500 (does nothing, no inline operands); 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 HUD/panel init FUN_004268c0(2): re-initialises the on-screen counter panels (coins, lives, etc.) for the current level. low -
|
||||
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 -
|
||||
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 -
|
||||
@@ -347,7 +347,7 @@
|
||||
0x159 nop_0x159 - 0 0 Empty handler FUN_00426500 (shared no-op); does nothing. high -
|
||||
0x15a setPosToPlayer - 0 0 Copies the player's position x/y/z (+0x30..+0x38) and +0x3C into the strat (player strat = 0x6D9E1C). high -
|
||||
0x15b setPosToPlayerXZ - 0 0 Copies the player's x and z (+0x30, +0x38) into the strat; y unchanged. high -
|
||||
0x15c setShadowHeight - 1 0 pops v; stores it in strat +0x14C, used by the shadow renderer as the shadow/ground height override (-0x1000 = use default 0x586424). low -
|
||||
0x15c setShadowHeight - 1 0 Pops v into +0x14C, the water/floor height limit used by the shadow pass (FUN_00555140: -0x1000 = use the camera-cell floor height DAT_00586424, prop camFloorHeight); the shadow is skipped when the floor under the strat lies below it. One use (SinkingStatuePad). medium -
|
||||
0x15d packRgb - 3 1 pops c (top), b, a (fixed 0..255); pushes (c<<16)|(b<<8)|a as a raw packed colour int (push order r,g,b gives 0x00BBGGRR). high -
|
||||
0x15e trailCreate - 2 1 pops vertB (top), vertA (fixed vertex indices of the strat's model/anim frame); allocates a free trail/ribbon slot (0x5FD180, 0x42C bytes each) owned by this strat and pushes its handle (0 if none free). medium -
|
||||
0x15f trailAddPoint - 3 0 pops colourB (top), colourA, trail handle; transforms the two trail vertices of the current anim frame into world space and appends them to the trail history (20 points) with the given colours; no-op if handle 0. medium -
|
||||
@@ -393,8 +393,8 @@
|
||||
0x187 emitterInit - 7 0 pops colour/texture (top, raw), z, y, x (vector/offset), type (fixed 1..0x2B preset, or >=0x1000 type flags), size/speed (fixed, 0x1000 default), slot (0..2, >=3 = extra slot +0x153); (re)initialises the strat's particle emitter in that slot (records at 0x5FE230, 0x34 bytes; texture/colour stored per type). medium stSpawnparticle
|
||||
0x188 emitParticles - 3 0 pops yStep (top, fixed), count, slot; emits count particles from the slot's emitter at own position with random XZ velocity, each raised by yStep. medium -
|
||||
0x189 randSigned - 1 1 pops n; pushes a random value in (-n, n) ((rand32 - rand32) % n, same fixed scale as n). high -
|
||||
0x18a emitterSetParams - 5 0 pops e (top), d, c, b, slot; sets emitter slot params: +0x254 = b (rate/speed), +0x24A = c>>12 (life); if e==0 +0x25C = (d>>12)*32-32 else +0x246 = e, +0x23C = d. Allocates the slot if empty. low -
|
||||
0x18b emitterSetColour - 5 0 pops colour (top, raw packed, see 0x15d), b, g, r, slot; sets emitter colour (+0x250) and fade/alpha (+0x240..243): if r has no fraction it is an alpha value only, else r,g,b are per-channel fade values. low -
|
||||
0x18a emitterSetParams - 5 0 Pops e (top), d, c, b, slot (>>12): pending particle settings of this strat (slot 0–2 → record index at strat+0x150+slot, ≥3 → +0x153, record allocated on demand in the 0x34-byte table 0x5FE23C): b = size scale (1.0 = default), c>>12 = extra lifetime in frames, e≠0: +0x246 = e (drift), +0x23C = d; e==0: +0x25C = (d>>12)*32-32 (sprite size). spawnParticle consumes and resets the +0x153 record; same as spawnParticle type 0x19. medium -
|
||||
0x18b emitterSetColour - 5 0 Pops colour (top, raw packed, see 0x15d), b, g, r, slot: pending particle colour for the next particle(s) of this strat (record as 0x18a; same as spawnParticle type 0x1e): +0x250 = colour; r without fraction = alpha r>>12 (7 bits), else r,g,b>>12 = per-channel fade, +0x243 = 0x80; alpha 0 = 0xFF/lifetime. medium -
|
||||
0x18c emitParticlesStill - 3 0 pops yStep (top, raw fixed), count, slot; emits count particles from the slot's emitter at own position without velocity, stacked by yStep. medium -
|
||||
0x18d stubPop2_0x18d - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
|
||||
0x18e stubPop2_0x18e - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
|
||||
@@ -424,7 +424,7 @@
|
||||
0x1a6 emitterKill - 1 0 pops slot (fixed); frees the strat's particle emitter in that slot (+0x150+slot = 0xFF) and resets the record. high -
|
||||
0x1a7 stubPush0_0x1a7 - 0 1 Stub FUN_0054e880: pushes 0. high -
|
||||
0x1a8 processFirst - 0 0 Moves self to the head of the strat update list (0x6D9E38) if not already there. high -
|
||||
0x1a9 setRain - 4 0 pops colour (top, raw -> 0x6D9BE8), b (->0x578EF4), a2 (->0x6D9BF4), a1 (->0x578EF0) (all >>12); if the three counts/params are 0 rain is stopped and cleared, else rain is enabled (0x6D9BE4 = 1). low -
|
||||
0x1a9 setRain - 4 0 Weather: pops texture (top, raw tex index → 0x6D9BE8, read by the rain renderer), windZ (>>12 → 0x578EF4), c (>>12 → 0x6D9BF4, only used for the off test), windX (>>12 → 0x578EF0); each raindrop restarts with velocity (windX, -256, windZ) per frame. windX, c, windZ all 0 = rain off (drops cleared, 0x6D9BE4 = 0), else on. Rain type: setRain(v0, v1, v2, tex:768) with params 16.0, 0, 16.0. medium -
|
||||
0x1aa stubPop1Push0_0x1aa - 1 1 Stub FUN_005550b0: pops 1 value and pushes 0. high -
|
||||
0x1ab stubPop2_0x1ab - 2 0 Stub FUN_00555090: pops and discards 2 values (feature removed in PC port). high -
|
||||
0x1ac stubPop1Push0_0x1ac - 1 1 Stub FUN_005550b0: pops 1 value and pushes 0. high -
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 36 and column 30.
|
@@ -106,6 +106,86 @@ class StratEditsTest {
|
||||
assertEquals(wad2.length, WadBuilder.build(r.scene()).length);
|
||||
}
|
||||
|
||||
static int entry(Project p, String type) {
|
||||
return p.scene().catalog().type(type).variant(0).code();
|
||||
}
|
||||
|
||||
static StratEdits.CustomType withSource(StratEdits.CustomType t, String src) {
|
||||
return new StratEdits.CustomType(t.name(), t.basedOn(), t.entryLabel(), src, t.proto());
|
||||
}
|
||||
|
||||
/** Edit and delete custom types: codes of placed objects follow, undo/redo, save/reload. */
|
||||
@Test
|
||||
void editAndDeleteCustomTypes() throws Exception {
|
||||
Bases cd = cd();
|
||||
StratEdits.init();
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "types", "t1l1m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
StratEdits.CustomType a = StratEdits.cloneOf(p.scene(), "Coin", "CoinA");
|
||||
h.execute(TypeEdits.create(p, a));
|
||||
StratEdits.CustomType b = StratEdits.cloneOf(p.scene(), "Coin", "CoinB");
|
||||
h.execute(TypeEdits.create(p, b));
|
||||
h.execute(Edits.placeStrat(p.scene(), "CoinA", 0, 30, 2, 30, 0.0));
|
||||
h.execute(Edits.placeStrat(p.scene(), "CoinB", 0, 31, 2, 30, 0.0));
|
||||
h.execute(Edits.placeStrat(p.scene(), "CoinB", 0, 32, 2, 30, 0.0));
|
||||
int n = p.scene().strats().size();
|
||||
List<Strat> placed = List.copyOf(p.scene().strats().subList(n - 3, n));
|
||||
int b0 = entry(p, "CoinB");
|
||||
assertEquals(
|
||||
List.of(1, 2),
|
||||
List.of(
|
||||
StratEdits.users(p.scene(), 0).size(),
|
||||
StratEdits.users(p.scene(), 1).size()));
|
||||
|
||||
// edit A: three more words, faster spin → B's entry moves by 12 bytes
|
||||
String src = a.source()
|
||||
.replace("push 5.625deg", "push 90deg")
|
||||
.replace("\nCoinA:\n", "\nCoinA:\n nop_0x00 0\n nop_0x00 0\n nop_0x00 0\n");
|
||||
assertTrue(src.contains("nop_0x00"), src);
|
||||
assertTrue(StratEdits.checkTypes(p.scene(), List.of(withSource(a, src), b), 0, null) == null);
|
||||
h.execute(TypeEdits.edit(p, 0, withSource(a, src)));
|
||||
assertEquals(b0 + 12, entry(p, "CoinB"));
|
||||
assertEquals(entry(p, "CoinB"), placed.get(1).code());
|
||||
assertEquals(entry(p, "CoinB"), placed.get(2).code());
|
||||
assertEquals("CoinB", placed.get(2).type());
|
||||
assertEquals(entry(p, "CoinA"), placed.get(0).code());
|
||||
assertEquals(0x400000, spin(p, "CoinA").value());
|
||||
assertTrue(StratEdits.records(p).types().get(0).source().contains("nop_0x00"));
|
||||
assertTrue(StratEdits.analysis(p.scene()).code().errors.isEmpty());
|
||||
h.undo();
|
||||
assertEquals(b0, entry(p, "CoinB"));
|
||||
assertEquals(b0, placed.get(1).code());
|
||||
assertEquals(0x40000, spin(p, "CoinA").value());
|
||||
h.redo();
|
||||
assertEquals(b0 + 12, placed.get(2).code());
|
||||
|
||||
// delete A: its object goes, B moves to A's place
|
||||
int a0 = entry(p, "CoinA");
|
||||
byte[] before = WadBuilder.build(p.scene());
|
||||
h.execute(TypeEdits.delete(p, 0));
|
||||
assertTrue(p.scene().catalog().type("CoinA") == null);
|
||||
assertTrue(!p.scene().strats().contains(placed.get(0)));
|
||||
assertEquals(1, StratEdits.records(p).types().size());
|
||||
assertEquals(entry(p, "CoinB"), placed.get(1).code());
|
||||
assertTrue(entry(p, "CoinB") < b0, "CoinB moved down to the start of the custom block");
|
||||
assertEquals("CoinB", nameAt(WadBuilder.build(p.scene()), placed.get(1).code()));
|
||||
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()), "reloaded project builds the same WAD");
|
||||
h.undo();
|
||||
assertTrue(p.scene().strats().contains(placed.get(0)));
|
||||
assertEquals(a0, placed.get(0).code());
|
||||
assertEquals("CoinA", placed.get(0).type());
|
||||
assertArrayEquals(before, WadBuilder.build(p.scene()), "undo restores the level");
|
||||
// undo the creation of both types: no custom code left
|
||||
while (h.canUndo()) {
|
||||
h.undo();
|
||||
}
|
||||
assertTrue(StratEdits.records(p).types().isEmpty());
|
||||
assertEquals(-1, StratEdits.firstCustomUnit(p.scene()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateAssemblesAndVerifies() throws Exception {
|
||||
Bases cd = cd();
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.eliasmueller.llamamod.editor.tools.strats;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.io.Bases;
|
||||
import com.eliasmueller.llamamod.editor.io.Project;
|
||||
import com.eliasmueller.llamamod.editor.io.ProjectIO;
|
||||
import com.eliasmueller.llamamod.editor.model.Strat;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Edits;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.History;
|
||||
import com.eliasmueller.llamamod.format.strat.StratCompiler;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds the in-game test level for custom types and compiled scripts (not a unit test):
|
||||
* {@code java -cp all.jar:build/classes/java/test ...StratsGameLevel OUT.llamalevel [rain]}. Village 1
|
||||
* with, in front of the start: BigCoin (a Coin clone, edited afterwards to scale 3), Bobber (script:
|
||||
* bobs up and down), Pulser (script: grows and shrinks), and a deleted type (its object is gone).
|
||||
*/
|
||||
public final class StratsGameLevel {
|
||||
static final String BOBBER =
|
||||
"""
|
||||
// bobs one cell up and down and turns
|
||||
strat Bobber {
|
||||
alias base = v0
|
||||
alias phase = v1
|
||||
setModel(model:5)
|
||||
base = self.y + 1.0
|
||||
phase = 0
|
||||
loop {
|
||||
phase = phase + 4deg
|
||||
self.y = base + sin(phase)
|
||||
turnLeft(4deg)
|
||||
yield
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
static final String PULSER =
|
||||
"""
|
||||
// grows to 4x, shrinks back to 1x, forever
|
||||
strat Pulser {
|
||||
alias size = v0
|
||||
setModel(model:5)
|
||||
self.y = self.y + 1.0
|
||||
size = 1.0
|
||||
loop {
|
||||
while (size < 4.0) {
|
||||
size = size + 0.05
|
||||
scale(size)
|
||||
yield
|
||||
}
|
||||
do {
|
||||
size = size - 0.05
|
||||
scale(size)
|
||||
yield
|
||||
} while (size > 1.0)
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
/** Rain with strong wind in x from the start (setRain: windX, unused, windZ, texture). */
|
||||
static final String STORM =
|
||||
"""
|
||||
strat Storm {
|
||||
setRain(200.0, 0, 0, tex:768)
|
||||
loop {
|
||||
yield
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
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("rain")) {
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "edit", "t1l2m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
StratEdits.CustomType coin = StratEdits.cloneOf(p.scene(), "Coin", "X");
|
||||
h.execute(TypeEdits.create(
|
||||
p, new StratEdits.CustomType("Storm", null, "Storm", StratCompiler.compile(STORM), coin.proto())));
|
||||
Strat start = p.scene().playerStart();
|
||||
h.execute(Edits.placeStrat(
|
||||
p.scene(), "Storm", 0, start.x() / 4096.0, start.y() / 4096.0, start.z() / 4096.0, 0.0));
|
||||
ProjectIO.save(p, Path.of(a[0]), cd);
|
||||
System.out.println("saved " + a[0]);
|
||||
return;
|
||||
}
|
||||
Project p = ProjectIO.read(ProjectIO.write(Project.create(cd, "edit", "t1l1m001"), cd), cd);
|
||||
History h = new History(p.scene());
|
||||
StratEdits.CustomType big = StratEdits.cloneOf(p.scene(), "Coin", "BigCoin");
|
||||
h.execute(TypeEdits.create(p, big));
|
||||
StratEdits.CustomType temp = StratEdits.cloneOf(p.scene(), "Coin", "TempCoin");
|
||||
h.execute(TypeEdits.create(p, temp));
|
||||
for (String[] s : List.of(new String[] {"Bobber", BOBBER}, new String[] {"Pulser", PULSER})) {
|
||||
String asm = StratCompiler.compile(s[1]);
|
||||
h.execute(TypeEdits.create(p, new StratEdits.CustomType(s[0], null, s[0], asm, big.proto())));
|
||||
}
|
||||
Strat start = p.scene().playerStart();
|
||||
double sx = start.x() / 4096.0, sy = start.y() / 4096.0, sz = start.z() / 4096.0;
|
||||
double yaw = start.ry() * 2 * Math.PI / 4096.0;
|
||||
double fx = Math.sin(yaw), fz = Math.cos(yaw), rx = Math.cos(yaw), rz = -Math.sin(yaw);
|
||||
// a row between the start view and the VillagerHerder (34.5, 7.25, 66.1), 1 cell apart
|
||||
String[] types = {"Coin", "BigCoin", "Bobber", "Pulser", "TempCoin"};
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
h.execute(Edits.placeStrat(p.scene(), types[i], 0, 31.5 + i, 8.0, 64.6, 0.0));
|
||||
}
|
||||
// edit BigCoin after placing it: three times the size (the object follows the new code)
|
||||
String src =
|
||||
big.source().replace(" setModel\n", " setModel\n pushShort 3.0\n scale\n");
|
||||
if (src.equals(big.source())) {
|
||||
throw new IllegalStateException("no setModel line in the Coin source");
|
||||
}
|
||||
h.execute(
|
||||
TypeEdits.edit(p, 0, new StratEdits.CustomType("BigCoin", "Coin", big.entryLabel(), src, big.proto())));
|
||||
// delete TempCoin together with its object
|
||||
h.execute(TypeEdits.delete(p, 1));
|
||||
System.out.println("types: " + StratEdits.customNames(p));
|
||||
ProjectIO.save(p, Path.of(a[0]), cd);
|
||||
System.out.println("saved " + a[0] + " (" + p.scene().strats().size() + " objects, start " + sx + "," + sy + ","
|
||||
+ sz + ")");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.eliasmueller.llamamod.editor.tools.strats;
|
||||
|
||||
import com.eliasmueller.llamamod.editor.model.Strat;
|
||||
import com.eliasmueller.llamamod.editor.model.cmd.Edits;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorApp;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorEnv;
|
||||
import com.eliasmueller.llamamod.editor.ui.EditorWindow;
|
||||
import com.eliasmueller.llamamod.format.strat.StratCompiler;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Robot;
|
||||
import java.awt.Window;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
/**
|
||||
* Screenshots of custom-type editing and the script view (not a unit test):
|
||||
* {@code java -cp all.jar:build/classes/java/test -Dllamamod.data=DIR ...StratsTour OUTDIR}.
|
||||
*/
|
||||
public final class StratsTour {
|
||||
static EditorWindow w;
|
||||
static Path out;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
out = Path.of(args[0]);
|
||||
Files.createDirectories(out);
|
||||
EditorEnv env = EditorEnv.resolve();
|
||||
edt(() -> {
|
||||
EditorApp.applyTheme("light");
|
||||
w = EditorApp.open(env, "t1l1m001");
|
||||
w.setBounds(0, 0, 1600, 1000);
|
||||
return null;
|
||||
});
|
||||
sleep(4000);
|
||||
for (int i = 0; i < 300 && edt(() -> w.busy() || w.scene() == null); i++) {
|
||||
sleep(200);
|
||||
}
|
||||
sleep(2000);
|
||||
edt(() -> {
|
||||
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())));
|
||||
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(), "BigCoin", 0, x, y, z + 3, 0.0));
|
||||
Strat last = w.scene().strats().get(w.scene().strats().size() - 2);
|
||||
w.selection().set(last, null);
|
||||
w.showPanel(StratsEditorPlugin.PANEL);
|
||||
return null;
|
||||
});
|
||||
sleep(2500);
|
||||
shot("strats_custom_panel");
|
||||
SwingUtilities.invokeLater(() -> TypeDialog.showEdit(w, 1));
|
||||
sleep(2500);
|
||||
shot("strats_edit_script");
|
||||
closeDialogs();
|
||||
SwingUtilities.invokeLater(() -> TypeDialog.delete(w, 1));
|
||||
sleep(2000);
|
||||
shot("strats_delete_warning");
|
||||
closeDialogs();
|
||||
edt(() -> {
|
||||
w.selection().set(w.scene().playerStart(), null);
|
||||
return null;
|
||||
});
|
||||
sleep(2500);
|
||||
shot("strats_view_llama");
|
||||
System.out.println("tour done");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
static void closeDialogs() throws Exception {
|
||||
edt(() -> {
|
||||
for (Window x : Window.getWindows()) {
|
||||
if (x instanceof JDialog d && d.isShowing()) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
sleep(800);
|
||||
}
|
||||
|
||||
static void shot(String name) throws Exception {
|
||||
Rectangle r = new Rectangle(0, 0, 1600, 1000);
|
||||
ImageIO.write(
|
||||
new Robot().createScreenCapture(r),
|
||||
"png",
|
||||
out.resolve(name + ".png").toFile());
|
||||
System.out.println("shot " + name);
|
||||
}
|
||||
|
||||
static void sleep(long ms) throws InterruptedException {
|
||||
Thread.sleep(ms);
|
||||
}
|
||||
|
||||
static <T> T edt(Callable<T> c) throws Exception {
|
||||
AtomicReference<T> r = new AtomicReference<>();
|
||||
AtomicReference<Exception> e = new AtomicReference<>();
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
try {
|
||||
r.set(c.call());
|
||||
} catch (Exception x) {
|
||||
e.set(x);
|
||||
}
|
||||
});
|
||||
if (e.get() != null) {
|
||||
throw e.get();
|
||||
}
|
||||
return r.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.eliasmueller.llamamod.format.strat;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Script language → assembly → words → script view again. */
|
||||
class StratCompilerTest {
|
||||
static final String SPINNER =
|
||||
"""
|
||||
// spins up, bobs, then waits for the player
|
||||
strat Spinner {
|
||||
alias count = v0
|
||||
count = 0
|
||||
while (count < 10.0) {
|
||||
turnLeft(4deg)
|
||||
count += 1.0
|
||||
yield
|
||||
}
|
||||
loop {
|
||||
if (self.y > 2.0 && !self.animPlaying) {
|
||||
self.y = self.y - 0.01
|
||||
} else if (self.distToPlayerSqXZ < 4.0) {
|
||||
break
|
||||
} else {
|
||||
call wobble
|
||||
}
|
||||
yield
|
||||
}
|
||||
do {
|
||||
count -= 1.0
|
||||
} while (count > 0)
|
||||
}
|
||||
|
||||
sub wobble {
|
||||
self.rotZ = sin(self.rotY) * 0.1
|
||||
}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void compilesStructuredCode() {
|
||||
String asm = StratCompiler.compile(SPINNER);
|
||||
assertEquals(SPINNER.stripTrailing() + "\n", StratCompiler.embeddedSource(asm));
|
||||
assertEquals("Spinner", StratCompiler.firstStrat(SPINNER));
|
||||
StratListing li = StratPseudoTest.listing(asm, "Spinner");
|
||||
String view = StratPseudo.of(li).text();
|
||||
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(
|
||||
"} else if (self.distToPlayerSqXZ < 4.0) {\n break\n } else {\n call L_00b4()"),
|
||||
view);
|
||||
assertTrue(view.contains("do {\n v0 -= 1.0\n} while (v0 > 0)"), view);
|
||||
assertTrue(view.contains("self.rotZ = sin(self.rotY) * 0.1\nreturn"), view);
|
||||
}
|
||||
|
||||
@Test
|
||||
void numbersMatchTheAssembly() {
|
||||
String asm = StratCompiler.compile("strat N { v0 = 1.5\n v1 = 100.0\n v2 = 90deg\n v3 = -3 }");
|
||||
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.trim().endsWith("end"), asm);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsErrorsWithLines() {
|
||||
StratCompiler.CompileException e = assertThrows(
|
||||
StratCompiler.CompileException.class, () -> StratCompiler.compile("strat A {\n v0 = \n}"));
|
||||
assertEquals(3, e.line);
|
||||
e = assertThrows(
|
||||
StratCompiler.CompileException.class, () -> StratCompiler.compile("strat A {\n turnLeft(1, 2)\n}"));
|
||||
assertTrue(e.getMessage().contains("turnLeft takes 1 value"), e.getMessage());
|
||||
e = assertThrows(StratCompiler.CompileException.class, () -> StratCompiler.compile("strat A {\n break\n}"));
|
||||
assertEquals(2, e.line);
|
||||
e = assertThrows(
|
||||
StratCompiler.CompileException.class, () -> StratCompiler.compile("strat A {\n self.nope = 1\n}"));
|
||||
assertTrue(e.getMessage().contains("unknown property nope"), e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.eliasmueller.llamamod.format.strat;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.eliasmueller.llamamod.format.stpcmerge.StpcImage;
|
||||
import com.eliasmueller.llamamod.format.stpcmerge.StratCode;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Structuring of the script view: loops, do-while, while, break/continue, switch, else-if. */
|
||||
class StratPseudoTest {
|
||||
/** A one-unit STPC (no models/animations) holding the assembled source. */
|
||||
static StratListing listing(String src, String entry) {
|
||||
StratAssembler.Result r =
|
||||
StratAssembler.assemble(src, new StratAssembler.Layout(16, List.of(), List.of(), List.of()));
|
||||
int[] w = r.words();
|
||||
// header: 0 models, 0 animation sets, 1 unit
|
||||
byte[] stpc = new byte[16 + 4 * w.length];
|
||||
put(stpc, 0, 0);
|
||||
put(stpc, 4, 0);
|
||||
put(stpc, 8, 1);
|
||||
put(stpc, 12, w.length);
|
||||
for (int i = 0; i < w.length; i++) {
|
||||
put(stpc, 16 + 4 * i, w[i]);
|
||||
}
|
||||
StpcImage img = new StpcImage(stpc, 0);
|
||||
Set<Integer> roots = Set.of(16 + r.labels().get(entry));
|
||||
return StratListing.of(img, StratCode.analyse(img, roots), 0, roots, StratListing.Names.NONE);
|
||||
}
|
||||
|
||||
static void put(byte[] d, int o, int v) {
|
||||
d[o] = (byte) v;
|
||||
d[o + 1] = (byte) (v >> 8);
|
||||
d[o + 2] = (byte) (v >> 16);
|
||||
d[o + 3] = (byte) (v >> 24);
|
||||
}
|
||||
|
||||
static final String SRC =
|
||||
"""
|
||||
Main:
|
||||
push 0
|
||||
popVar v0
|
||||
top:
|
||||
pushVar v0 ; while (v0 < 10)
|
||||
push 10.0
|
||||
lt
|
||||
jz done
|
||||
pushVar v0
|
||||
push 5.0
|
||||
eq
|
||||
jz notFive
|
||||
jmp done ; break
|
||||
notFive:
|
||||
pushVar v1
|
||||
jz skip
|
||||
jmp top ; continue
|
||||
skip:
|
||||
pushVarAddr v0
|
||||
inc
|
||||
jmpYield top
|
||||
done:
|
||||
again:
|
||||
pushVar v2 ; do … while (v2)
|
||||
push 1.0
|
||||
sub
|
||||
popVar v2
|
||||
pushVar v2
|
||||
jnz again
|
||||
pushVar v3
|
||||
switch tab
|
||||
jmp end
|
||||
one:
|
||||
push 1.0
|
||||
popVar v4
|
||||
jmp end
|
||||
two:
|
||||
push 2.0
|
||||
popVar v4
|
||||
end:
|
||||
pushVar v5
|
||||
jz e1
|
||||
push 1.0
|
||||
popVar v6
|
||||
jmp e3
|
||||
e1:
|
||||
pushVar v7
|
||||
jz e2
|
||||
push 2.0
|
||||
popVar v6
|
||||
jmp e3
|
||||
e2:
|
||||
push 3.0
|
||||
popVar v6
|
||||
e3:
|
||||
end
|
||||
tab:
|
||||
.switch 2
|
||||
.case imm 1.0, one
|
||||
.case imm 2.0, two
|
||||
""";
|
||||
|
||||
@Test
|
||||
void structuresLoopsSwitchAndElseIf() {
|
||||
StratListing li = listing(SRC, "Main");
|
||||
String t = StratPseudo.of(li).text();
|
||||
assertFalse(t.contains("goto"), t);
|
||||
assertTrue(t.contains("while (v0 < 10.0) {"), t);
|
||||
assertTrue(t.contains("if (v0 == 5.0) {\n break\n }"), t);
|
||||
assertTrue(t.contains("if (v1) {\n continue\n }"), t);
|
||||
assertTrue(t.contains("do {\n v2 = v2 - 1.0\n} while (v2)"), t);
|
||||
assertTrue(
|
||||
t.contains("switch (v3) { // sw_00d4\ncase 1.0:\n v4 = 1.0\n break\ncase 2.0:\n v4 = 2.0\n}"),
|
||||
t);
|
||||
assertTrue(t.contains("if (v5) {\n v6 = 1.0\n} else if (v7) {\n v6 = 2.0\n} else {\n v6 = 3.0\n}"), t);
|
||||
// the assembly still round-trips
|
||||
StratAssembler.Result back = StratAssembler.assemble(li.text(), StratListing.layout(li.img, li.start));
|
||||
assertEquals(List.of(toList(li.words())), List.of(toList(back.words())));
|
||||
}
|
||||
|
||||
static List<Integer> toList(int[] a) {
|
||||
return java.util.Arrays.stream(a).boxed().toList();
|
||||
}
|
||||
|
||||
/** 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;
|
||||
Pattern g = Pattern.compile("goto (\\w+)");
|
||||
for (Path p : StratRoundTripTest.levels()) {
|
||||
StratRoundTripTest.Loaded l = StratRoundTripTest.load(p);
|
||||
if (l == null) {
|
||||
continue;
|
||||
}
|
||||
for (int u = 0; u < l.img().units.size(); u++) {
|
||||
StratListing li = StratListing.of(l.img(), l.code(), u, l.entries(), StratListing.Names.NONE);
|
||||
StratPseudo ps = StratPseudo.of(li);
|
||||
units++;
|
||||
int open = 0;
|
||||
Set<String> labels = new HashSet<>();
|
||||
for (StratPseudo.Line x : ps.lines) {
|
||||
String s = x.text().replaceAll("\\s*//.*$", "");
|
||||
if (s.startsWith("}")) {
|
||||
open--;
|
||||
}
|
||||
assertTrue(open >= 0, p + " unit " + u + ": unbalanced at " + s);
|
||||
if (s.endsWith("{")) {
|
||||
open++;
|
||||
}
|
||||
if (s.matches("\\w+:")) {
|
||||
labels.add(s.substring(0, s.length() - 1));
|
||||
}
|
||||
}
|
||||
assertEquals(0, open, p + " unit " + u + ": unclosed block");
|
||||
for (StratPseudo.Line x : ps.lines) {
|
||||
Matcher m = g.matcher(x.text());
|
||||
if (m.find()) {
|
||||
gotos++;
|
||||
assertTrue(labels.contains(m.group(1)), p + " unit " + u + ": goto without label " + x.text());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.printf("script view: %d units, %d gotos%n", units, gotos);
|
||||
assertTrue(units >= 1400);
|
||||
assertTrue(gotos < 500, "gotos: " + gotos);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user