Skip to main content

Objects, instances and agents

Three layers​

Game objectA type: a name, script slots, and the models, animations and sounds it uses. One per kind of thing in the level.
InstanceA placement: a position, a rotation, and which object it is. The Tiki Mon boss is one instance of object 658.
TriggerA box in the world. When the player enters it, it activates or messages the objects it points at.
object 658 |HubD|old__HubD|act_TIKI_MON
scripts: 0:4026(TIKI_MON_INIT) 12:4116(CORTEX_TIKI_FIGHT_SUCKED)
objects: anims: 65535 ogis: 65535

inst 1 Instance[0] pos=(10.74, 25.6, 44.16) obj 658 |HubD|old__HubD|act_TIKI_MON

trig 1 c1=(-9.44,26.96,7.46) c2=(17.6,3.16,2.51) args=(87,0,4104,35) -> |HubD|old__HubD|act_TIKI_MON

c1 is the centre of the trigger box and c2 its half-extents, which is all you need to teleport the test rig into one. A trigger's args carry the message number and whatever the target script expects.

The agent flags​

Every instance has a creation helper (at instance + 16), and that helper holds a flags word (at helper + 16) that decides how the thing behaves physically. Scripts change those flags with one command, SetAgent, and the argument packs a mask and a value:

SetAgent(0x00400040) mask 0x40, value 0x40 → turn bit 6 on
SetAgent(0x00000040) mask 0x40, value 0 → turn bit 6 off
SetAgent(6) mask 0x06, value 0 → turn bits 1 and 2 off

The low half is the mask - which properties to touch - and the high half is the value. Each mask bit selects a different property:

Agent bitWhat it changes
0Activate / deactivate the actor entirely
1Instance flag 0x400
2Instance flag 0x10
3Instance flag 0x8
4Instance flag 0x1000
5Collision on/off (modelCollisionData bit 0)
6Hurts the player on contact (helper flags bit 8)
7Collision flag bit 10, inverted
8Collision flag bit 12
9Collision flag bit 9

Contact damage​

Bit 6 is the one behind "why did that hurt me?". The engine's contact handler (0x13E010) reads the toucher's helper flags and only hurts the player when bit 8 is set:

if ((helper->flags >> 8 & 1) != 0) {
if ((helper->flags >> 10 & 1) != 0) hurt_player(); // always
else if (event == 3 && player_state == 3) hurt_player(); // only in the right state
}

The hit itself is a flags word of 0x400 and a damage of 1 - one mask, or one life if you have none.

SetAgent(64) - mask bit 6, value 0 - is how the game turns contact damage off, and it does so in 718 places. The Uka Uka ice monster's death script uses SetAgent(582), which clears bits 1, 2, 6 and 9 in one go.

This is the engine's general "does this thing hurt you" switch, not something bosses have. Evil Crash turns his own off with SetAgent(64) the moment the Bandicoot Pursuit chase starts, so that the thing chasing you cannot damage you by brushing past, and back on with SetAgent(4194368) when the chase ends.

The Tiki Mon bug

The Totem Hokum boss never clears it. His fight ends with BossModeExit, a defeat cutscene and nothing else, so the wreck lying in the arena still hits anything that touches it - a mask, or a life and the whole fight again. The fix is one line: add the game's own SetAgent(64) to the last step of his script. His collision is untouched, so you can still climb on him. See mod/levels/harmless_after_defeat.ops.

Waking an object​

An instance does not run its script because it exists. Something has to activate it:

ActivateObjectInstance 0x263390 clears the ignore-events bit, binds resources, then
RunObjectSpawnScript 0x2634C8 onSpawnScriptId == -1 ? ExecuteEvent(ctx, 0) : run that script id
ExecuteEvent 0x2616C0 scriptId = gameObject->scriptIdsScriptSlots[eventIndex]

So the script slots printed by twinsdump objects are event indices: event 0 is the spawn / _DEFAULT script, event 1 is the _ACTIVATED one. Command_TriggerLinkedObjects_Run walks game node 6 - the linked-objects list, up to 31 entries - and sends event 1 to each. An object with only a slot 0 therefore cannot be started that way at all; it has to be woken at spawn.

ObjectInstanceContextBase, which is what you read to tell a sleeping object from a disabled one:

OffsetField
+0x00instance context
+0x04onSpawnScriptId (short; -1 means "use event 0")
+0x06object id - the number twinsdump objects prints, and the way to find an instance in a RAM dump
+0x08game object (the type)
+0x1Cflags; bit 0 = this object ignores every event

ExecuteEvent returns immediately when that bit is set, before touching any script, and both activation wrappers clear it as part of waking an object. So one bit separates "disabled" from "never started", and they need completely different fixes.

Finding out what hit you​

tools/rig/hurthook.py installs a live hook on the contact-damage call and logs, for each hit, the attacking instance, its creation helper, and the helper's flags. That is how the Tiki Mon was pinned down and how the fix was verified: 19 hits before the fight, none after the defeat.

import rig, hurthook
rig.level("hubd"); p = rig.Pine(); hurthook.install(p)
rig.teleport(10.74, 27.6, 44.16); time.sleep(3)
print(hurthook.read(p)) # (hits, [(attacker, helper, flags, player), …])

tools/rig/hurt_sweep.py does the same thing across a whole level, teleporting onto every instance in turn, and prints the ones that landed a hit.