153 lines
12 KiB
Markdown
153 lines
12 KiB
Markdown
# Object parameter schemas (strat bytecode analysis)
|
||
|
||
Registry in code: `editor.tools.objects.Schemas` (named, typed, documented parameters with ranges,
|
||
choices and waypoint meaning), statistics of all original instances:
|
||
`src/main/resources/.../editor/tools/objects/type-stats.tsv` (`TypeStats`). Types without a schema
|
||
fall back to raw values plus "values used in the original levels". Shown in the Inspector section
|
||
"Object parameters" and checked by the Problems panel (`ObjectChecks`).
|
||
|
||
## How parameters reach the script
|
||
|
||
* MAP strat record: count n + index into the parameter pool. The level spawner FUN_0054cfc0 passes
|
||
a fake stack, FUN_0054bfc0 pops the n values into the new strat's **variables 0..n−1**
|
||
(`strat+0xF4` = variable array). So *parameter i = variable i* of the strat code.
|
||
* Values are 20.12 fixed point by convention (0x1000 = 1.0); booleans are 0 / 0x1000, counts and
|
||
world/chapter numbers `n << 12`.
|
||
|
||
## Strat VM (FUN_0054d180) – enough to disassemble
|
||
|
||
Word-coded: `op = w & 0xFFFF`. `op < 0x45`: table 0x5796BC, operand = signed high 16 bits, one
|
||
word. `op ≥ 0x45`: table 0x578FF4 + op·4 (up to 0x1F6), inline operand words read via the pc.
|
||
Inline lengths (found by decompiling all 337 handlers with headless Ghidra and following helper
|
||
calls): 1 word for 0x45 0xA5 0xA6 0xB1 0xB2 0xD0 0xD3 0xD7 0xDB 0x115 0x11C 0x14B 0x14D 0x162
|
||
0x167 0x175; 2 for 0xD4 (name: `d4 ea rel`); 5 for 0x94 and 0xE0 (spawn: FUN_00553170 reads
|
||
nParams + 4 type fields); 6 for 0x95 (spawn at offset); short 0x0C (event slot) has 2; 0x02/0x03
|
||
(debug print: count then items, 0x45/0xEA items have one extra word) are variable. A linear sweep
|
||
restarted at every `d4` name site disassembles all 31 level WADs without desync and hits every MAP
|
||
strat entry (the entry is the name site). Strings live between strats.
|
||
|
||
| short op | meaning | short op | meaning |
|
||
|---|---|---|---|
|
||
| 04 n / 41 n | push / pop variable n | 06 n / 43 n | push / pop global n (0x6D9E8C) |
|
||
| 07 n / 40 n | push / pop own property n (FUN_00550E60 / FUN_005509F0) | 1C n / 37 n | player property |
|
||
| 44 v | push 16-bit constant | 14 / 15 / 16 | jz / jnz / jmp (pc+4+v·4) |
|
||
| 12 / 13 / 2B | jz / jnz / jmp + yield | 17 | switch (table at pc+4+v·4) |
|
||
|
||
Long ops used for the decoding: 45 push word, 49 add, 4A sub, 4B mul (fixed), 4C div, 4E eq,
|
||
4F lt, 53 gt, BC ne, BD/BE ge/le, AF not, B6 and-logical, B7/B8 bit or/and, 50/51 shl/shr,
|
||
AD/AE sin/cos (table 0x574318), AC floor, BF rand, 16E lerp, 54 set model, 8B radius (+0xFE),
|
||
83 +0xD2 (collision radius), A5/A6 set/clear strat flags (0x8000 floor collision, 0x40000 walls),
|
||
C2 update floor, 73/75/177 waypoint first/next/advance (+0x118 first, +0x120 current; next wraps
|
||
to the first node), FE move to waypoint, 178 "inside the box of the current and next waypoint
|
||
(x/z)", 192 level change (pops tribe, world, chapter), B4/8E die, E0/95 spawn.
|
||
Properties: 16 floor height, 17 floor material, 34–36 x/y/z, 37–39 rx/ry/rz, 3 waypoint
|
||
first/last bits, 4 waypoint field +26, 6–8 waypoint x/y/z, 9/10 waypoint rx/ry, 41 distance to
|
||
the player, 61–64 tribe/world/chapter/map, 202 distance to player (squared compare).
|
||
Camera globals: 47 horizontal distance, 48 height above the target, 29/44 yaw, 36–38 position
|
||
and 40–42 look-at of cut-scene cameras, 66 camera flag word, 95 progress bits.
|
||
Patterns: `push var i; push 30; mul` = seconds → frames; `push var; push var; mul; pop var` =
|
||
squared distance (radius).
|
||
|
||
## Decoded types
|
||
|
||
Confidence: **high** = code read and behaviour seen in game; **medium** = code read; **low** =
|
||
values only.
|
||
|
||
| Type | Parameters | Waypoints |
|
||
|---|---|---|
|
||
| Coin (high) | p0 snap to floor (drops onto floor, random spin), p1 falls (floor collision flag 0x8000), p2 no shadow | – |
|
||
| Coin5 (high) | p0 snap, p1 falls | – |
|
||
| Health | p0 snap, p1 no shadow, p2 falls | – |
|
||
| HealthFull, HealthGenerator, Wompy | p0 snap, p1 falls | – |
|
||
| Grapes, HealthBanana, ChargeBooster | p0 snap | – |
|
||
| RubyIdol | p0 falls, p1 snap | – |
|
||
| Secret (high) | p0 box mode (player inside the box of the 2 nodes, op 0x178) | box (2 nodes) |
|
||
| CoinLine / CoinCircle (low) | count (counted down), spacing / radius | – |
|
||
| Llama | p0 spawns a helper, p1 unused, p2 camera mode (global 107) | camera starts: first node within 2.5 cells of the llama (see "Start camera") |
|
||
| CheckPointPost (high) | p0 collision radius (+0xD2, 0.2) | 1 node = respawn position + yaw (98 of 102) |
|
||
| LevelDoor (high) | tribe, world, chapter of the next level (7,7,7 = world finished); fires on material-16 floor of a tile in its cell (any height; often only in the swap model a RubyIdolDoor/other door object switches to when it opens) | – |
|
||
| CameraSide | p0 height (g48), p1 distance (g47), p2 yaw mode (1 node yaw, 2 node yaw or ±180°), p3 only below node y, p4 flag 0x10000, p10 several boxes, p11 height offset, p12/p13 alt. height/distance | box pairs, node 0 yaw = camera yaw |
|
||
| CameraLift | p0 yaw source (1 object, 2 player), p1 box test on helper strat, p3 smooth (g97), p4 height limit, p5 flag | box |
|
||
| CameraSetDistance | g82, g68, g83, height g48, g73 (each with a flag bit in g66) | – |
|
||
| CameraFocusPoint | radius², min height², max angle, height limit, 3 flags, blend factor | – |
|
||
| CutSceneCameraDirectional | p0 event bit (1..31), p1 mode, p2 wait, p3 disabled, p5 end mode | camera nodes: position, yaw/pitch = view (globals 36–42) |
|
||
| CutSceneTrigger | p0 character (floor snap, g72 counter), p2 radius², p3 event bit (skip if set in g95), p5 height check, p6 bits (1 face player, 2 not skippable) | camera path |
|
||
| ForcePlayerMovement | p0 active, p1 radius², p2 game state, p3 lock, p4 align yaw | target points |
|
||
| ObjectDontDropInBox, FallOffNoHurt | box mode (+ game state) | box |
|
||
| WaterReboundPoints | active | rebound points |
|
||
| RubyIdolDoor | p1 idols needed, p4 sign offset | – |
|
||
| CityGuard family (high) | p0 view range², p1 chase range² (view capped to it), p2 event bit (0 = active at once), p3 wall collision (CityGuard only) | **boxes** (node pairs), must start inside one – see below |
|
||
| JumpingBeetleBlue, Spider, Bunny | times in seconds (×30), ranges² | patrol path |
|
||
|
||
### Guards (CityGuard, CityGuardWithRubyIdol, CowGuard(NoCoin), GorillaGuard, LizardGuard(NoCoin), WarthogGuard)
|
||
|
||
One compiled template (same code in t1l1m002 unit 20 and the others). Start of the script:
|
||
1. `if (p2 > 0) { hide; wait until g95 & (1 << (p2−1)); unhide } else wait until playerPtr ≠ 0` –
|
||
p2 is an **event bit**: bits of global 95 are set by talking characters (TipoVillage,
|
||
ChacaVillage), smash statues, LabBigCutscene, LabSlidingDoorPad … with the same number. All
|
||
village guards have p2 = 0; 4 lab guards wait for an event.
|
||
2. Waypoint scan: `wpFirst`, then pairs (`wpNext` twice): odd count → debug "Uneven number of
|
||
WPs" + **remove**; the start box = first pair whose x/z box (`inWaypointBoxXZ`, op 0x178,
|
||
inclusive, fixed point) contains the guard and whose first node has field +0x28 (`wpParam`,
|
||
editor `unk2`) = 0; none → "Guard not in WP box" + **remove**. So the waypoints are **walking
|
||
areas**, not a patrol path: the guard wanders (random turns) inside the current box; when it
|
||
leaves it, `waypointStep(2)` looks for another box containing it (boxes with `wpParam ≠ 0` only
|
||
from another such box), else it turns back (`clampToWaypointBox`). Chasing stays clamped to
|
||
the boxes; it gives up beyond p1 or when the player leaves.
|
||
All 41 original guards satisfy this (unit test `GuardsTest`). The editor: placing a guard gives
|
||
it one 4×4 box and p2 = 0; `ObjectChecks` reports an error when the game would remove it and an
|
||
info for p2 > 0; the Inspector shows the state plus a **Box around it** button (`Guards`,
|
||
`Setups.guardBox`). Verified in game: CityGuard with its box on a new level (base village 2)
|
||
appears and attacks the llama; a second one with a 4-node "square path" next to it never appears.
|
||
|
||
### Other box walkers (2026-11-07, objs2)
|
||
|
||
Same pair scan, kind-specific rules (`Guards`, model list `editor.model.WaypointBoxes`):
|
||
* **CowGuardSpawned** (spawned by CowGuardSpawner, same guard code): p0 view, p1 chase, p2 event
|
||
bit (spawner passes 0), p3 not read (overwritten by the box scan). Box rule as guards (unk2 = 0).
|
||
* **CowGuardSpawner** (6 params, t1l8m005 ×8): `spawn<4,…>(v0, v1, 0, v5, CowGuardSpawned)` at its
|
||
own position with its chain, then `wait(v3·30)` until the player is > 1.5 cells away and fewer
|
||
than v2 of its guards live (the guard does `parent.v6 −= 1` when it dies). So p0 = view range,
|
||
p1 = chase range, p2 = max guards, p3 = interval (s), p4 not read, p5 = guard p3 (unused). The
|
||
spawner must stand in a box with unk2 = 0 or each guard is removed at once – 1 of 8 originals
|
||
(t1l8m005 strat 82) stands 0.12 outside (warning, not verified in game).
|
||
* **Alligator** (no params; v0–v6 are locals), **SpawnedAlligator**: odd node count → "Odd number
|
||
of WPs!" + remove; outside every box → "Forcing alligator inside first WP box", clamped into the
|
||
first box. No unk2 rule. Bites only when `|player.floorHeight − y| < 0.2` and inside its boxes.
|
||
* **AlligatorSpawner** (t1l6m003 ×2): p0 max alive (`v3 < v0`, `parent.v3 −= 1` on death), p1
|
||
interval s, p2 → spawned `collisionOn 0x8400` when set. The spawned alligator (spawner's chain)
|
||
walks straight ahead (`moveForwardQ`, spawner yaw) with a fade-in until it is inside the
|
||
*first* box – both originals stand outside it, facing it. Check: warning if the ray misses.
|
||
Placement (every path through `Edits.placeStrat`): one 4×4 box around the object, unk2 = 0, guards
|
||
event bit 0. In game (TestLevel 6 on t1l4m002): an Alligator added this way appears and snaps at
|
||
the llama (`img/editor/obj_game_alligator.jpg`).
|
||
|
||
### Event bits (global 95)
|
||
|
||
Setters found by a sweep over the strat code of all 31 levels (`g95 = g95 | (1 << (v − 1))`),
|
||
v = parameter: TipoVillage p6, ChacaVillage p6, SmashStatueSmall/Big/Door p0, RiverCrocGame p1,
|
||
RiverTriggerBox p0, KidOnCanoe p4, YzmaRiver p2 and p6, Condor p2 (also clears it), GoldIdol p1,
|
||
CityWall p4, NakedPeacock p0, LabSlidingDoor p5, LabSlidingDoorJamie p5, LabBigCutscene p1, LabWall
|
||
p4, EndGameTriggerBox p0 (`EventBits`). No native code touches global 95. Check: lab level
|
||
t1l8m001 – LizardGuardNoCoin bit 8 ← LabBigCutscene p1 = 8, GorillaGuard bit 30 ← LabWall p4 = 30;
|
||
CowGuard bit 20 and CowGuardNoCoin bit 1 have no setter in the level (they never appear in the
|
||
original either). `ObjectChecks` adds this to the guard info, naming the setter types of the base.
|
||
|
||
### Start camera (Llama waypoints)
|
||
|
||
Camera strat (spawned by the Llama with `self.hasWaypoints`), case g0 = 4 at level start, L_1330:
|
||
walks the Llama's chain; the first node with `playerDistToWp < 2.5` becomes the camera position,
|
||
yaw = angle to the player; if the node's wpParam (unk2) ≠ 0, also g47 = x/z distance and g48 =
|
||
height difference, and g43 = 3 blocks the approach to the normal distance until the llama moves.
|
||
The defaults are g47 = 1.0, g48 = 0.5 (Camera params from the Llama spawn). Originals: one node per
|
||
entry door (unk2 4096 on some), none within 2.5 of the village starts. The old editor setup (and
|
||
levelbuild) copied the prototype chain onto the llama: village 2's node 2 has unk2 = 4096 → camera
|
||
in the llama's head until it moves (seen in game). Fix: `PlayerStart` (one node 2 behind, unk2 0;
|
||
moving keeps door nodes). Village fly-by: CutSceneTrigger on the start, radius² 0.5; p6 bit 1 makes
|
||
it (L_0148) rewrite its last path node to a point behind the player – set when the start moves.
|
||
|
||
The rest (~145 placed types) shows raw values with the original statistics. Next candidates:
|
||
PressurePadDoor (bit field), TargetRopePlatform, CataSwingingBlade (speeds/angles), RiverLog.
|
||
Tooling used (not in the repo): Ghidra script decompiling the handler table, a Python sweep
|
||
disassembler with an op-name table, "parameter use" context dumps per type.
|