Files
llamamod/notes/editor-object-schemas.md

12 KiB
Raw Permalink Blame History

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.