World objects: spawning, models, tint, glass and doors¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4), static analysis only
(Ghidra), a disc check (2026-10-06) of what the scripts pass and of the Object List and the WonderWheel_100
scene, reported as counts and values, and, for the objective markers,
held objects and trash props only, PCSX2 2.9.94 over PINE in level99 (2026-10-06, -07).
Purpose¶
What level scripts set on the world they build and what the player breaks or opens in it: how an object a script spawns finds its model, where it stands and what moves it (Dynamic objects, with the front end's Wonder Wheel as the worked case), an object's tint, the objective markers (the "W" a mission asks the player to walk into), the breakable glass panes, the doors and the breakable barriers. The lists are Object tints, Glass types and Doors; the classes and pools of world objects and glass panes are on Tasks, the crimes a pane can raise on AI: crimes, and what the player's attacks do to them on Combat: breakables.
Original structure¶
The code sits in TaskEngine/, in the stretches placed by position
(Source map). Names are ours. The script types
(the table at 0x00512f28, Particles) involved: glass_script (61), sub_glass (210),
sub_stained_glass (247), glasstest (62, the shards), dyn_door_swinging (32), sub_swinging_door (249, a leaf),
dyn_door_sliding (30), sub_triglint (254), and the barriers dyn_door_fence (27), dyn_door_bar_bani (23),
dyn_door_bnstr (25), dyn_door_chain_s (26), dyn_door_fence_o (28), dyn_door_parapet (29). Confirmed (code):
each index read from the table's name pointer.
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00390f18 |
Cfg_AddObjectType |
CfgObj: one 0x90-byte object type (Object types) |
confirmed (code) |
0x003913d8 |
ObjectDb_FindByName |
a type's index by name (a hash table over the names) | confirmed (code) |
0x00396858 |
Obj_Spawn |
ObjSpawn: the unlockable checks, then a spawn record |
confirmed (code) |
0x00398940 |
ObjRecord_Add |
ObjSpawn's spawn record (Spawn records); returns its handle |
confirmed (code) |
0x00398fe0 |
ObjRecord_GetHandle |
a record's object, spawning it first when not live | confirmed (code) |
0x00399080 |
ObjRecord_Spawn |
makes the object from a record; copies the tint | confirmed (code) |
0x00399428 / 0x003996a0 |
ObjRecord_Store / ObjRecord_Remove |
an object back into its record / gone for good | confirmed (code) |
0x00398df8 |
ObjRecord_SetPinned |
record bit 0x10000 and object +0x10f: never stored |
confirmed (code) |
0x00399d88 |
ObjectTaskManager_UpdateSpawns |
streams records in and out around the cameras (Streaming) | confirmed (code) |
0x003918b8 |
WorldObject_Init |
the world object's initialiser: type, script type, defaults, body | confirmed (code) |
0x003a44c8 |
Obj_SetModel(object, hash) |
the model by Object List hash (0: the type's) | confirmed (code) |
0x001811b0 |
ObjectList_FindByHash |
an Object List record by name hash | confirmed (code) |
0x001808b8 / 0x001809c0 |
ObjectModel_IsLoaded / ObjectModel_Request |
a record's three resources resident / requested | confirmed (code) |
0x00180f58 |
ObjectModel_MakeInstance |
a model instance over the record's clump and dictionaries | confirmed (code) |
0x001897a8 |
ResourceManager_LoadNearestModel |
per frame: loads one object's model that is missing | confirmed (code) |
0x003eeea0 / 0x003ef840 / 0x003ef188 |
simple_object init / update / message |
the plain prop (simple_object) |
confirmed (code) |
0x0017fd78 |
ObjectRender_Draw |
draws one object instance: lights, size cull, fade | confirmed (code) |
0x0017fa80 |
ObjectRender_ApplyFadeDistance |
the alpha of an object near its ObjShow distance |
confirmed (code) |
0x00395b70 |
WorldObject_Update |
integrates, copies +0xcc to +0xc8, runs the class's update |
confirmed (code) |
0x003e98e8 / 0x003e9b08 / 0x003e9be0 |
dyn_objective init / update / message |
an objective marker's disc (Objective markers) | confirmed (code) |
0x003e9828 |
ObjectiveMarker_SetShown |
a marker's shown state, passed on to its column | confirmed (code) |
0x003e9d60 / 0x003e9fd8 / 0x003e9ea0 |
sub_objective_column init / update / message |
the marker's column | confirmed (code) |
0x003a1810 / 0x003a1858 |
Obj_Attach / Obj_Detach |
parent, bone and local pose; detaching keeps the world pose (Held objects) | confirmed (code) |
0x003a19b0 / 0x003a1ad8 |
Obj_GetWorldPose / Obj_GetPoseNow |
vtable function words +0x9c / +0xa4 (slots +0x98 / +0xa0 of 0x005453a0 and the car's 0x00544c08): an attached object's pose through its parent's bone |
confirmed (code), runtime |
0x003fe490 / 0x003fe6b8 |
MeleeWeapon_Take / MeleeWeapon_Detach |
messages 0x1b / 0x1c of melee_weapon |
confirmed (code) |
0x00257f38 / 0x002586d8 |
Human_DropHeld / Human_ReleaseThrow |
a held object let go: dropped / thrown | confirmed (code) |
0x00221ed0 |
Human_SetCarryMoveStyle |
human message 3: pushes the held object's anim set as a move style (Two-handed objects) | confirmed (code), runtime |
0x00253688 |
Human_ApplyMoveStyle |
a move style's anim slot overrides; style 4 is the two-handed carry | confirmed (code), runtime |
0x003ff918 / 0x003c0108 |
OverheadWeapon_OnPickedUp / DynCashreg_OnPickUp |
message 0x1b: attached at the event's bone with the type's hold pose (CfgObj holdPos / holdRot) |
confirmed (code), runtime |
0x003a3978 |
ScriptObj_GetVectorProperty |
property 11 / 12: the type record's hold position (+0x00) / rotation (+0x0c) |
confirmed (code) |
0x003a5530 |
ScriptObj_SpawnThrownDebris |
a broken prop's piece: created by name at the prop's pose, knocked with message 0x30 (Trash props) |
confirmed (code), runtime |
0x003a53f0 |
Obj_SpawnChildByName |
creates an object by name at an object's pose, attached to it | confirmed (code) |
0x00396bd0 |
Obj_SetColour |
ObjColor: packs {r, g, b, a} into the tint word |
confirmed (code) |
0x0038fab8 |
GlassTypes_Set |
CfgSetGlassProperties: one entry of the glass type table |
confirmed (code) |
0x0039c0e0 |
Glass_Spawn |
pushes the pane's arguments, creates it by type name | confirmed (code) |
0x0038f8a8 |
GlassManager_Create |
allocates the pane, runs its initialiser, the window link | confirmed (code) |
0x003e29e8 / 0x003e3058 / 0x003e2d90 |
glass_script init / update / message |
the pane | confirmed (code) |
0x0038f378 |
Glass_Break(pane, breaker, object) |
the alarm, the window link, the window flags | confirmed (code) |
0x003e4be0 / 0x003e4cb8 |
sub_glass init / update |
the shatter: sound and shards | confirmed (code) |
0x003e44e0 / 0x003e4838 |
GlassTest_Init / GlassTest_Update |
a shard (glasstest) |
confirmed (code) |
0x0038f1c8 |
GlassPane_UpdateBodyByDistance |
per frame (vtable +0x13c): the pane's body within 50 m |
confirmed (code) |
0x0038ed68 / 0x0038eec0 |
GlassPane_CreateBody / GlassPane_RemoveBody |
the pane's collision body (+0xe8) |
confirmed (code) |
0x0033d070 |
Physics_CreateBoxBody |
a static box body in the 255-body pool, flags 0x8000007a (the pane's) |
confirmed (code) |
0x0033f110 |
Human_TestStrikes |
per update, after the move: the human's switched-on strike shapes against nearby bodies | confirmed (code), runtime |
0x00219d50 |
Human_OnContact |
the human body's contact handler; an airborne body touching a pane breaks it (Moving into a pane) | confirmed (code), runtime |
0x0038fa50 → 0x0038ef60 |
GlassManager_QueueDraws → GlassPane_QueueDraw |
per frame: each pane into its sprite batch or the near list | confirmed (code) |
0x0038f090 → 0x001831c0 |
GlassPane_DrawNear → Instance_DrawOneSpriteIm3D |
a pane near a camera, drawn on its own | confirmed (code) |
0x00185b38 |
ResourceMgr_Render3DSprites |
the 3D sprite pass (Drawing a pane) | confirmed (code) |
0x003966c8 → 0x003963b8 |
World_BreakGlassInRadius |
BreakGlassInRadius |
confirmed (code) |
0x00396390 → 0x003961d0 |
World_BreakObjectsInRadius |
BreakObjectsInRadius |
confirmed (code) |
0x00397230 |
Door_Spawn |
pushes the door's arguments, creates it by type name | confirmed (code) |
0x003fb5f8 / 0x003fbba0 / 0x003fb8d0 |
dyn_door_swinging init / update / message |
the swinging door | confirmed (code) |
0x003f80f0 |
DoorSwing_SetUpType |
per type name: leaf models, sounds, flags | confirmed (code) |
0x003f8fd0 |
DoorSwing_SwingTo |
turns the leaves to the angle at data +0x00 |
confirmed (code) |
0x003f9368 |
DoorSwing_ResetLeaves |
puts the leaves back in the closed pose | confirmed (code) |
0x003f9770 |
DoorSwing_StateCommand |
message 0x22: open, close, collision, pickable |
confirmed (code) |
0x003f9910 |
DoorSwing_OpenBy |
message 0x0b: a human opens it, away from them |
confirmed (code) |
0x003f9588 |
DoorSwing_SetPickable |
the lock-pick glint | confirmed (code) |
0x003f9ad0 / 0x003fa438 |
DoorSwing_Hit / DoorSwing_HitBreakable |
message 1 | confirmed (code) |
0x003fb330 / 0x003fb5a0 / 0x003fb430 |
sub_swinging_door init / update / message |
a leaf | confirmed (code) |
0x003a2310 / 0x00336a00 |
Task_Integrate / Quat_Slerp |
a leaf's rotation step and its in-between pose (Leaves) | confirmed (code) |
0x003b2f40 / 0x003b3220 / 0x003b3158 / 0x003b2180 |
dyn_door_fence init / update / message / hit |
a breakable barrier | confirmed (code), runtime |
0x00391c10 |
WorldObject_Remove |
vtable +0x4c: message 2 to the object's handlers, out of its record, deleted (Barriers) |
confirmed (code), runtime |
0x00250c00 |
NavLinks_SetKindByNumber(number, kind) |
retags every navigation link carrying a number | confirmed (code) |
0x00250c50 / 0x00250d00 |
NavLinks_OpenByNumber / NavLinks_CloseByNumber |
bit 31 of every link with a number, path flag 8 of its door hole | confirmed (code) |
0x002508b8 |
NavLink_DoorPolygon(link) |
the hole at the middle of a door link | confirmed (code) |
0x00250100 |
PathPolygon_FindAtPoint(pos) |
the flag-4 hole whose box holds a point | confirmed (code) |
0x00250960 |
NavLink_FindNearest(pos, kinds, &link, &back) |
the nearest link of a kind within 5 m, and its reverse | confirmed (code) |
0x00417480 |
Flags_DisableNear(radius, pos, activity, 0) |
disables flags of one activity near a point | confirmed (code) |
0x003a5340 / 0x003a52c8 |
Obj_RemoveBody / Obj_AddBody |
an object's body in the collision world (0x00597198) |
confirmed (code); names inferred |
0x00117280 |
Sound_PlayMaterialPair(a, b, pos) |
the sound matrix entry of two materials (Sound) | confirmed (code) |
0x003a6ee8 |
Sound_PlayHashAt(hash, pos) |
a 3D sound by name hash | confirmed (code) |
Data¶
Object types¶
CfgObj fills one 0x90-byte record of the object database (0x00512c04, records from its start, a count at
+0x34bc0; the binding's fields are on CfgObj). Confirmed (code) at
0x00390f18, 0x00391778, 0x003917a0, 0x00391340:
| Offset | Meaning |
|---|---|
+0x28 |
name, up to 26 characters (dyn_s_wwheel_a): the name scripts spawn and the task is created by |
+0x43 |
class name, up to 20 characters (simple_object) |
+0x5c |
the class's index in the script type table (Particles), or −1 |
+0x60 |
the record's own index |
+0x8c |
CRC-32 of the name (the standard table at 0x005d91e0, as typed: the names are lower case): the Object List key of its model |
The other fields (+0x58-+0x88) are the binding's arguments. A name is found by ObjectDb_FindByName
(0x003913d8): a hash table at +0x34dfc keyed by h = 5h + c over the characters. Confirmed (code).
Spawn records¶
ObjSpawn does not make an object: it adds a spawn record (0x28 bytes) to the ObjectTaskManager's array (+0x14,
count +0x18; CfgSetDatabaseSizes objects + 500 of them) and returns a handle naming the record: its index << 16
with serial 0 (Tasks: handles). Confirmed (code) at 0x00398940, 0x00399080, 0x00399428:
| Offset | Meaning |
|---|---|
+0x00 |
rotation: four s16, the quaternion × 4,096 (0x004f3c70 packs, vitof12 unpacks) |
+0x08 |
position x, y, z |
+0x14 |
tint word (The tint) |
+0x18 |
u16 the flag (marker) named by flagName, 0xffff for none |
+0x1c |
parent handle (none: NilHandle) |
+0x20 |
the type's index in its low 14 bits, the object zone in its top 10 (<< 22) |
+0x24 |
low 16 bits: the live object's handle index (0xffff none); bit 0x10000 pinned, 0x20000 live, 0x40000 removed (never spawned again), 0x80000 ObjSpawn flag 2, 0x100000 stored hidden, 0x1000000 ObjSpawn flag 64, 0x400000 / 0x800000 a door's left / right leaf; bits 26-29 a state handed to the object on spawn (6: none, the value a new record gets) |
When a record is stored (Streaming) the object's current pose and tint are written back, so it comes back where it was left.
The tint¶
A world object's tint is a colour word at +0xc8, copied at +0xcc: 0xRRGGBBAA read as a little-endian word
(bytes a, b, g, r in memory). Confirmed (code):
ObjSpawn's seventh argument is stored in the spawn record (+0x14,0x00398940) and copied to the object when it spawns (0x003992a4). The default,0xFFFFFFFF, is white: no tint. The scripts pass 33 others, all with alpha0xFF.ObjColor(object, {r, g, b, a})multiplies each component by 255, and the packer (0x0017aca8) multiplies by 255 again; the soft-float conversion (0x0042c718) keeps the low 8 bits. Since 65,025 is 1 modulo 256, a whole component 0-255 comes out as itself and a 0-1 component does not (1.0 gives 1). The one script that calls it passes 0-255 values.HuSetSpinningIconColor(human, steps, colour)sends message0x34to the human's spinning icon (0x00238b18), which fades its tint word to the colour over the steps (Characters: the spinning icon); no script calls it.- The breakable doors write the word too: a broken
dyn_door_liz,dyn_door_dclubordyn_door_stallclears its alpha byte (+0xcc=+0xc8&0xffffff00,0x003fa438).
Drawing (ObjectRender_Draw, 0x0017fd78, confirmed (code); the copy also confirmed (runtime)): each
update WorldObject_Update (0x00395b70) copies +0xcc into +0xc8, so a class that writes +0xcc sees it take
effect one update later. The model instance holds the word as bytes r, g, b, a (instance +0x24-+0x27, equal to
+0xc8 in PCSX2), and the draw sets them as the colour of the instance's geometry before rendering it: the tint
multiplies the model's own colours and its alpha the model's opacity. The alpha is first scaled by the size fade
(The model), the ObjShow distance (below), a fade-in over the first second after the instance appears
(instance +0x2c; skipped while object flags 0x800010 has a bit set) and the camera fade (×0.3 for an object
between the camera and the player, instance +0x34, eased over 200 ms); an alpha under 10 is not drawn. While
the camera fade is below 1 the draw turns z-writing off (RenderWare render state 8) for that object.
The ObjShow distance (+0x138, ObjectRender_ApplyFadeDistance 0x0017fa80): when it is above 0 and the
camera is farther than the distance − 2 m, the alpha is multiplied by (distance − camera distance) / 2, fading the
object out over its last 2 m. An attached object (flag 0x10) uses its parent's distance and is not drawn beyond
it. Confirmed (code).
Glass types¶
The GlassTaskManager (world object 0x00512c7c +0x84c) keeps a table of 16-byte entries at +0x14 + 16 × type,
confirmed (code) at 0x0038fab8, 0x0038fb00-0x0038fb10:
| Offset | Meaning |
|---|---|
+0x00 |
window link (CfgSetGlassProperties' second argument) |
+0x04 |
alarm (third) |
+0x08 |
the whole pane's sprite word (fourth): the pane's sprite rectangle is its low 16 bits (Task_SetRect, Particles) |
+0x0c |
the broken pane's sprite word (fifth); 0 hides the pane once broken |
config_preload2.lua configures types 0-18.
The pane task (0x100 bytes), confirmed (code) at 0x003e29e8:
| Offset | Meaning |
|---|---|
+0x10 |
centre: the first corner + half of each edge |
+0x54 |
flags: 0x8001 at spawn; 4 hidden (also set while no camera is within 50 m), 0x800000 broken |
+0x70 |
W, the width edge: third corner − first corner (SpawnBreakableGlass' cornerV − corner) |
+0x80 |
the unit normal, normalise(H × W) |
+0x90 |
H, the height edge: second corner − first corner (cornerU − corner) |
+0xa0 |
centre (copy); +0x70-+0xaf is the matrix the pane is drawn with (Drawing a pane) |
+0xb0 |
sprite word: batch 0 in the high half, the type's rectangle in the low half (type 14: its own batch, rectangle 0) |
+0xb4 |
colour 0xRRGGBBAA: 0x808080e0 whole (0 for type 15), 0xffffff70 once broken |
+0xb8-+0xc4 |
the script's two texture-coordinate pairs, u0, v0, u1, v1 in push order; nothing reads them back (inferred: no reader among the pane's methods or the draw) |
+0xc8 |
size word: whole metres of width, height << 16 (truncated) |
+0xcc / +0xd4 |
width / height in metres (floats), |W| / |H| |
+0xd0 |
squared distance from the centre to the nearest player camera, refreshed each frame (0x0038ef30) |
+0xd8 / +0xdc |
the two collision triangles |
+0xe8 |
its collision body, or 0 (vtable +0xf4 returns it) |
+0xf8 |
alarm bits (3 with the alarm, 0x0038ec30) |
+0xfa |
type |
The order of the pops (0x003e29e8) against the pushes (0x0039c0e0) gives the corner names. The normal's order
(vopmula vf1, vf2 then vopmsub vf2, vf2, vf1 with vf1 = H, vf2 = W, at 0x003e2b6c) is H × W; the draw does
not use it. Confirmed (code).
The pane updates every 180 ticks (3 s, Tasks); the update only calls an empty hook for an
unbroken type 1 within 100 m (0x003e3058).
Swinging doors¶
dyn_door_swinging is a frame object with one or two leaves, each a separate sub_swinging_door object. The door
task: position +0x10, rotation +0x20 (SpawnDoor's), flags +0x54 = 0x405, triangles +0xd8 / +0xdc, door
number +0xe0, hitpoints mirror +0x128 (what GetHitpoints reads). Its data block (vtable +0x18c), confirmed
(code) at 0x003fb5f8:
| Offset | Meaning |
|---|---|
+0x00 |
the angle it swings to, degrees (0 closed) |
+0x04 |
an angle DoorOpenDegree left while the door was pickable |
+0x08 |
hitpoints: the type's CfgObj third argument (+0x58); message 0x19 sets it |
+0x0c / +0x10 |
open and close sound (name hashes) |
+0x14 / +0x18 |
left / right leaf handle (GetLeftDoorHandle, GetRightDoorHandle; message 0x10) |
+0x1c |
1 once the leaves are registered with the object manager |
+0x20 |
pickable (the lock-pick door) |
+0x24 |
the pickable glint's handle |
+0x28 |
state byte, below |
Per type (0x003f80f0, by name; leaves are the dyn_dr_* objects). A second leaf stands at
position + rotation × (−2 × w, 0, 0) turned 180° about the vertical, where w is one leaf's width, CfgObj
argument 15 (Leaves); the leaf objects get +0x124 = 2 (left) and 4 (right). Confirmed (code):
| Door types | Leaves | Open / close sound |
|---|---|---|
dyn_door_big_gate, _cemgate, _dblgate, _dblgate_d, _redgate |
2 | 0x43e4b743 / 0x2d88622b |
dyn_door_chainlnk_a, _chainlnk_pick, _chainlnk_ag, _ap, _ar, _aw |
1 | 0xca9bdea4 / 0x53928f1e |
dyn_door_dblstlwin, _steel_b, _stlslot (second leaf dyn_dr_stlslot), _stlwin |
2 | 0xc09823be / 0x62b026b4 |
dyn_door_strip |
2 | 0xc09823be / 0xd8831bf0 |
dyn_door_dblwoodfnce_xl (2), dyn_door_woodfnce_xl (1) |
0xa75ac8f5 / 0x4954a9d9 |
|
dyn_door_red_fence, _sheetmtl |
1 | 0x34062c42 / 0xb0443866 |
dyn_door_corr, _ornate_single (dyn_dr_ornate), _stall |
1 | default |
dyn_door_cabin_a/_b/_c, _dclub, _liz, _ornate, _shackdoor, _store, _storeb, _subcan, _temple, _templedoor, _templeshutter, _wood |
2 | default |
The default sounds are 0xecc3ab3c / 0xb70e3ee2. Other dyn_door_swinging types (_dblwood, _woodbrd,
_woodp, _steel...) get no leaves. Also per type: _chainlnk_pick, _storeb and _templedoor start pickable;
_dclub and _liz retag their number's links to 0x40; the three cabin doors start in state 10 with collision
triangle bit 0x800 and a 180-tick update.
Collision: both triangles get triangle flag bit 1 (two-sided), bit 10 (0x400), bit 6 (0x40) when data
+0x08 is non-zero at spawn, and the type's material (CfgObj 13th argument, type +0x64) as their material byte
(Collision). Confirmed (code) at 0x003fb5f8.
Hitpoints (CfgObj third argument; disc values): dyn_door_store 1, dyn_door_wall_a/_b 3, dyn_door_fence
and dyn_door_stall 10, the cabin doors 12, dyn_door_dclub 20, dyn_door_vargas 1, the rest 100.
Navigation links¶
The level's navigation links are the path data's D records (edges, Level loading):
8 bytes, +0x00 the node, +0x04 the edge flags (u16, the kind) and +0x06 a u16 whose low 13 bits are a door
number and whose top bit is the word's bit 31, the "avoid" bit: route planning adds 1,600 to the edge's cost
(AI: path planning), so a closed door makes a detour preferred, not mandatory.
0x00510590 is the array, 0x00510594 its count. Confirmed (code) at 0x00250960, 0x00250c00-0x00250d00.
A door's hole: opening and closing by number (0x00250c50 / 0x00250d00, from the door's state commands with
the second argument 1, 0x003a4b00) change the avoid bit of every link with the number, walking the D records from
the last, and the path flags of one polygon, found from the first link they meet (the highest index):
0x002508b8(confirmed (code); the kind mask is0x50): the link's node (D+0x00), then that node's last link of kind0x10or0x40(searching its D records from the end), and the middle of those two nodes, so the middle of the doorway;PathPolygon_FindAtPoint(0x00250100, confirmed (code)) at that point: of all polygons, only those with path flag 4 and at least one vertex whose box holds the point; one such polygon is the answer, several are narrowed by the inside test (0x0024eef0) to the one whose vertex average is nearest, none gives 0 (the original then writes through a null pointer: do nothing);- opening sets bit 3 (8) of that polygon's
u16flags at+0x48, closing clears it.
The polygon is a hole of an area (Path data), never the area's outline, which
has no flag 4. Opening it lets lines and routes cross the doorway (AI: path planning). Disc
check (level87, door 15): links 1530 and 1537 (kind 0x10, nodes 219 and 220, 1.4 m apart); the middle lies in the
boxes of the street's outline (polygon 0) and of polygon 33, a clockwise 4-vertex hole of flags 7, about
0.8 × 2.8 m, the only flag-4 candidate: polygon 33 gets flag 8.
| Kind | Who sets it | How a follower takes it (Following a route) |
|---|---|---|
| 4 | the data | 0x0029baa8: faces the waypoint, then either runs or starts a jump (Human_BeginJump) |
0x10 |
the data; ConvertJumpToDoor (4 → 0x10) |
walked while the door is open; with the avoid bit set the move fails (brain +0x284 = 4) |
0x40 |
breakable doors (by number) and window-linked panes | 0x0029bca0: faces the waypoint, sets the move action to 4, and issues the charge (attack kind 19, command 0x20) |
So an AI routed through a breakable door or a pane charges it. Confirmed (code) at 0x0029ad04-0x0029adb8; every
human searches with the mask 0xff, which admits them (AI).
Behaviour¶
Dynamic objects¶
A dynamic object is a world object a script places with ObjSpawn(typeName, pos, rot, -1, zone, flags, tint,
flagName): props, weapons, pick-ups and the front end's Wonder Wheel (dyn_s_wwheel_a, its carts
dyn_s_wwcart_simple_* and the signs dyn_s_neon_*, Scripts). Its life:
a spawn record, then an object made from the record when it is wanted, then a model attached when the model's
resources are in memory, then whatever moves it.
Spawning¶
Obj_Spawn(0x00396858): a name startingdyn_key(7 characters,0x00581010) is skipped unless unlockable 13 is set, anddyn_powercuffs(0x00581018) unless unlockable 14 is set and game state0x0041d160is 0; thenObjRecord_Addwith the position, rotation, zone, flags, tint and flag name, the parentNilHandle. Confirmed (code).ObjRecord_Add(0x00398940) fills the record and returns its handle. Only in level 84, aTYPE_GLASStype is spawned at once. Confirmed (code).- The object appears when something wants it, by either path. Confirmed (code):
- resolving the record's handle (
Handle_Resolve→ObjRecord_GetHandle,0x00398fe0) spawns it now when it is not live. Any binding taking the handle does this:SceneAddObject(Scene_BindObject,0x00354280),ObjColor,ObjDestroy...; - streaming (below) spawns it when a camera comes within 70 m.
- resolving the record's handle (
ObjRecord_Spawn(0x00399080): when the pool is full it first frees a slot (0x003998f0); it pushes the position (w = 1), the rotation (unpacked) and the parent into the message scratch and creates a task by the type's name (record+0x28) in theObjectTaskManager(Tasks: classes); the record gets the object's handle index and the live bit; the object gets the tint (+0xc8,+0xcc) and the zone (+0x114); record bits then send message0x12(0x200000), mark a door leaf (+0x124= 2 or 4), hide it (0x100000: message0x3cand object flag0x800000); a state other than 6 goes to the object's vtable+0x124.WorldObject_Init(0x003918b8):Task_Init; the type found by name (object+0x112its index,+0xc0the type's name); the type's class record of the script type table handed to vtable+0x194; colour words+0xc8/+0xcc=DAT_005fd268; hitpoints+0x128from type+0x58; the collision body by shape (0x00391d48: shape 1 a box of the type's size, 2 a sphere of radius half its x); a handle (+0xe4). Then the class's owninitruns with the pushed arguments (forsimple_object, below). Confirmed (code).
ObjSpawn therefore returns at once, and nothing is drawn until the record is resolved or streamed in. In
level100.lua the 29 Wonder Wheel objects are spawned when WonderWheelAnim:startScene binds them to the scene
(SceneAddObject resolves each handle). Inferred from the paths above.
The level's placed objects (_objs.txt)¶
InitLevel's step 7 (Level loading), after the level script's main chunk, reads
<level>_objs.txt (0x00398598): a count (%d), then one line per object, {name {x,y,z}, {qx,qy,qz,qw}, -1, zone,
flags, tint, flagName ) (format at 0x005811f0), the arguments ObjSpawn takes. A name starting part (0x00581228)
makes a particle emitter task by that name at the pose instead; every other line goes to ObjRecord_Add directly (no
unlockable checks). Confirmed (code); that the fields mean what ObjSpawn's do is inferred from their order.
Emitter lines, confirmed (code) at 0x003987f4-0x0039882c: the loader pushes the position (w = 1), the rotation
and a null parent into the message scratch and creates the task by name (Task_CreateParticleByName), which is
SpawnParticle's path (Particles: spawning) with no parent; Particle_Spawn only also
returns the new task's handle. The line's other fields (−1, zone, flags, tint, flag name) are read but not used, so an
emitter is not a spawn record: it is made at once at level load, whatever its zone, is never streamed or stored,
and lives until it ends itself or the level is freed. Any distance gating is the type's own: the steam vents
(part_steam, _large, _huge) start on with built-in defaults and puff only near a camera
(Steam vents); the fly swarms keep their bugs only while a view is near
(Flies); the neon types place a sub_neon_light and end; part_strobe_red starts off
(no light, its alarm sound emitter disabled) until message 0x12, which a store break-in sends to the nearest task
whose type name contains strobe (a substring test, 0x00435d30) within 6 m of the store's flag
(Crimes: stores), and then spawns a strober (a red light flashing in four steps) and enables the
sound; 0x13 stops both (Small lights and strobes).
part_s_shack_dust_puff and part_s_subway_sparks also wait for message 0x12.
level99_objs.txt holds 112 lines: 14 emitters and 98 objects; 89 lines name zone 0 (the emitters among them) and 23
zone 26 (the two stores' jewellery and a cash register, flags 2), a zone BNESetup (global.lua, called by
level99.lua's RegisterObjects) enables, as it does each of zones 21-32 the level defines. Disc check (NTSC-U,
2026-10-06, counts only); the store's items at runtime on Combat: breakables.
Streaming¶
ObjectTaskManager_UpdateSpawns (0x00399d88, called from TaskManager_TickGame) walks up to 512 records a call,
round-robin from where it stopped (+0x44), and skips everything while the task manager's +0x82c is set.
Confirmed (code):
- In: the nearest record that is not live, not removed, whose zone is enabled (the bit mask at manager
+0x20, Tasks) and whose squared distance to the nearest camera (0x00120230) is under 4,900 (70 m) is spawned, at most one a call; when the pool is full, the farthest storable live object beyond it is stored first. - Out: a live record not pinned, whose object can be stored (
0x00395078: not pinned, flag0x40000clear, among checks not traced) and which is farther than the first camera's distance (its vtable+0x214) + 10 m, or whose zone is off, is stored (0x00399428: pose, tint and state written back, the object freed), or removed for good when its own check (0x00399718, objects that fell out of the world) says so. - Pinned records stay:
SceneAddObjectpins its object (0x00398df8, record bit0x10000, object+0x10f= 1), so a scene's objects are never streamed out while bound. Confirmed (code).
What the first camera's vtable +0x214 returns (the draw distance, inferred) is not traced.
The model¶
An object's model is the Object List record whose name hash is the type's (CRC-32 of the type name,
WAD contents); the class's initialiser asks for it. Confirmed (code) at
0x003a44c8, 0x00180f58, 0x001897a8:
Obj_SetModel(object, hash)(0x003a44c8): releases a model the object had (+0x104); hash 0 means the type's own (CRC-32 of type+0x28);ObjectList_FindByHash(0x001811b0, a linear search of resource manager+0x94, count+0x90). With no record the object has no model and the call returns 0. Otherwise it returns the hash, which the class stores at object+0xc4, and, when the record's resources are resident (ObjectModel_IsLoaded,0x001808b8), makes the instance at once.- The resources of a record: the model (
+0x08, a0x47clump resource; sizes+0x14and+0x18are what the memory pool is asked for), the texture dictionary (+0x0c, size+0x1c) and an optional second dictionary (+0x10, size+0x20), each looked up by hash in the resource manager's maps (+0x10,+0x30) and reference-counted. That packs, the level file and standalone WAD entries all feed those maps is inferred. - The instance (
ObjectModel_MakeInstance,0x00180f58): a 0x40-byte object (0x0017faf8) over the clump and the one or two dictionaries;0x00395498puts it at object+0x104, sets+0x108= 3 and points the instance back at the object (+0x3c). - When not resident: the object stays without a model.
ResourceManager_LoadNearestModel(0x001897a8, from the world manager's service0x0040f8a0, which the game modes' updates call) takes one waiting human, object or car (handles at resource manager+0xbc4,+0xbd4,+0xbf4, the nearest by0x00189750, inferred) and, for an object with no instance, requests its record's three resources (ObjectModel_Request,0x001809c0, which first checks they fit), then attaches the instance once they are in. How the three handles are chosen is not traced.
Disc check (counts and hashes): the eight Wonder Wheel types have records whose model is <name>_geo, no second
dictionary and no variant; the three cart types share one dictionary (0x4a9e1bf5) and the four neon types another
(0xf2a65bf6), the wheel has its own (0xad06c88d). 33 of the 1,406 records have a second dictionary. Whether
level100's packs hold them (resident at once) or they stream in is not checked.
The models' own frames are the z-up to y-up turn with an authored translation (the wheel none, the neons
(−29.32, 19.94, 0.07), the carts (−78.2, 0, 0)); drawn at their scene poses without that translation, the neons ring
the hub and the carts ride the rim as at runtime (Front end), so the instance is taken to
replace the frame's translation. Inferred.
Drawing (ObjectRender_Draw, 0x0017fd78, confirmed (code) for what is cited): the lights are chosen per object
(LightManager_SelectLights, Lighting); an object whose +0x124 is 0 and whose bounding radius over
its squared camera distance is under 0.0004 is not drawn, and fades out between 0.0004 and 0.0005. A
dyn_s_wwcart_simple_* (radius about 1.5 m) at the front end's 80 m would be culled by that rule; simple_object
gives the Wonder Wheel types +0x124 = 5, which exempts them (simple_object).
simple_object¶
The class of most props (dyn_s_* and others; record 0x00583a30 of the script type table: init 0x003eeea0,
update 0x003ef840, message 0x003ef188, flags 8). Confirmed (code):
- Init: update interval 240 ticks (4 s); pops the parent handle, the rotation and the position (
+0x20,+0x10); flags+0x54= 1, or0x11with a parent (attached to it,0x003a18b8);Obj_SetModel(object, 0), the hash kept at+0xc4. Then by model hash: 14 hashes (the eight Wonder Wheel types among them) set+0x124= 5; others set flags0x80,0x8080or0x20; two (0x14e8682c,0x014d30ce) get an interval of 60 ticks and a looping sound (0x326071de,0x26ac304b). - Update: nothing but the looping sound of those two: within the sound's range + 10 m it plays every 2 ticks, otherwise it stops and checks every 60. A plain prop does not move itself.
- Messages:
0x12shows it (clears flag 4);0x13hides it (sets flag 4; three model hashes set0x14000000instead);0x3chides it;8puts it back at its attached pose;0x0asets whether it can be hit;0x1b,0x1c,0x32attach it to or detach it from a human;0x20stops its sound.
That flag 4 stops the draw is inferred (it is a glass pane's hidden bit too, Glass types).
Objective markers (dyn_objective)¶
The marker a mission asks the player to walk into is two world objects: a disc with a "W" on it that turns
about the vertical, at the top of a translucent coloured column standing on the ground. Scripts place it with
ObjSpawn of a dyn_objective type, show it with ObjShow and remove it with ObjDestroy; the volume box the
player walks into is a separate object (Scripting: triggers). The marker has no radar part:
a script that wants one adds a radar objective itself (HUD).
In level99 (inferred from the script's calls): RegisterObjects spawns four dyn_w_mission (the script's
Objects.dyn_w_mission01-04), each standing in a volume box. P1.SetupCam sets the first hint, calls
ObjShow(Objects.dyn_w_mission02) and waits for message 3 of the box vMark01; P1.FirstGlow clears that handler,
ObjDestroys marker 02, sets the next hint, waits on vMark03 and shows marker 03; P1.DoneCamera destroys marker
03. Confirmed (runtime), PCSX2 2.9.94 with the first hint on screen: marker 02 stands at (−283.27, 127.07, 0.3) in
vMark01, marker 03 at (−287.32, 121.55, 0.3) in vMark03 with alpha 0 until shown.
The disc (dyn_objective, script type 45, record 0x005132ac: init 0x003e98e8, update 0x003e9b08, message
0x003e9be0, flags 8). Confirmed (code) unless marked:
- Init: pops the position and rotation; flags
+0x54= 1;Obj_SetModel(object, 0)(the type's own model, hash at+0xc4); angular velocity (+0x40, set through vtable+0x7c) = (0, 0, π/2), whichTask_Integrateapplies each update: it turns about the world z axis at 90° per second, one turn in 4 s (confirmed (runtime): 180° in 120 ticks); update interval 2 ticks (30 Hz);+0x124= 1, which exempts it from the size cull (The model). - The column: creates an object of type
dyn_objective_a(Obj_SpawnChildByName0x003a53f0, the name at0x00585a70) at the disc's position, attached to the disc, and sends it message0x34with a colour chosen by the disc's model hash (below). Then it sets itself, and so the column, hidden. - Shown state (
ObjectiveMarker_SetShown0x003e9828): message0x0awith 1 (ObjShow) or message0x3csets it shown and object flag0x800000;0x0awith 0 (ObjHide) sets it hidden and clears the flag. Either way the disc sends the same0x0ato its column. - Update (every 2 ticks): the tint's alpha byte steps 8 towards 255 while shown and towards 0 while hidden
(written to
+0xcc, so+0xc8follows an update later). A full fade takes 32 updates, about 1.07 s (confirmed (runtime): 0 → 255 and 255 → 0 each in 64 ticks, disc and column in step). Once the disc is dying (below) and its alpha is 0, the update returns 1 andWorldObject_Updateremoves it. - Messages:
0x15(destroy by message,ObjDestroy(object, true)) marks it dying, sends0x15to the column and hides both, so they fade out and go;0x19and0x22change its model (Obj_SetModelwith a popped hash);0x20removes the column at once.
The column's colour, a word 0xRRGGBBAA with alpha 0 (the fade supplies the alpha), by the disc's type:
| Disc types | Column colour | |
|---|---|---|
dyn_w_mission, dyn_objective_yellow, dyn_throwtarget |
0xC1A04700 (193, 160, 71) |
yellow-gold |
dyn_w_goto, dyn_objective_w, dyn_objective_red |
0x99121300 (153, 18, 19) |
red |
dyn_w_bonus, dyn_objective_green |
0x5F447000 (95, 68, 112) |
purple |
any other (dyn_w_cinematics, dyn_w_mission_b) |
0xFFFFFF00 |
white |
The code compares model hashes (0x27af4fe0, 0x39cbfb46, 0x646520ba; 0xa83a74da, 0xebef30bb, 0x34af4687;
0x14dc9db8, 0x7c280227); the type names are those whose CRC-32 they are (disc check). The disc's own tint stays
the spawn record's, white for every marker level99 places, so the disc shows its model's colours.
The column (sub_objective_column, script type 222, record 0x00514080: init 0x003e9d60, update 0x003e9fd8,
message 0x003e9ea0), type dyn_objective_a (TYPE_GLASS, no collision shape, box 0.68 × 0.68 × 3.68 m,
Objects). Confirmed (code) unless marked:
- Init: pops the parent, rotation and position; flags
0x11(attached; parent at+0x58); its own model (hash0x1f3b85ea); local position zero; angular velocity (0, 0, −π/2), the disc's turn backwards, so the column keeps its world orientation while the disc turns (inferred: an attached object's pose is relative to its parent's); update interval 2 ticks; shown. - Messages:
0x34the colour (+0xcc= the word,+0xc8= the word with alpha 1);0x0ashown or hidden;0x15dying and hidden. Its update is the disc's fade. - Its model is a cylinder, white at the foot and fading out towards the top (the reference image); tinted gold it is the translucent yellow column of the runtime picture.
How it looks (confirmed (runtime), PCSX2 2.9.94, the tutorial's first hint): a translucent gold column on the
ground, a little taller than a human, with the opaque "W" disc at its top, its face turning past the camera; the
column is see-through and fades upwards. The disc's model (dyn_w_mission, hash 0x27af4fe0) is a gold coin with a
black "W" (Objects), its CfgObj box 0.52 × 0.52 × 2 m.
Drawing. Both are ordinary world objects drawn by ObjectRender_Draw with their tint as the geometry colour
(The tint): no separate pass, the scene's lights selected as for any object, nothing drawn while the alpha
is under 10. The disc is never size-culled (+0x124 = 1). ObjShow's distance fades both over its last 2 m when a
script passes one; level99 passes none. Confirmed (code). How the column's translucency is blended (its material
and vertex alpha, the PS2 pipeline's blend) is not traced.
When it goes: the script's ObjDestroy(handle) (no second argument) removes the disc at once (its vtable
+0x4c) and the attached column with it, without a fade. Confirmed (code) for the disc; confirmed (runtime) for
both: their handles are empty three updates later and nothing is drawn. Only ObjHide or a destroy by message fades
a marker out.
Other marker kinds, for later:
dyn_w_gotoanddyn_w_missionas the Rumble race's carrots and the box glows (Rumble);dyn_objective_w(red, a larger "W") atlevel99checkpoint 2 for Vermin (Scripting).sub_objective_glow(script type 223, init0x003ea0b0, update0x003ea260, message0x003ea198): a 120 × 120 sprite of thelightingsheet, rectangle 3 (a corona), alpha 255, pulled 0.32 m towards the camera every 2 ticks; message0x0ashows or hides it. Confirmed (code) for the three functions (the table's record 223), the 2-tick interval, rectangle 3, alpha 255, the 0.32 m pull and message0x0a's show and hide; the size and the sheet are inferred. Who spawns it is not traced.dyn_throwtarget(gold),dyn_w_bonusanddyn_objective_green(purple),dyn_w_cinematics(white): the same class; their uses are not traced.
What moves them: the Wonder Wheel¶
No object code turns the wheel, swings a cart or flickers a neon: simple_object does not move, and the 29 types
have no class of their own. A scene moves them. level100.lua binds them to WonderWheel_100 (scene id 34; slots
0-7 _a carts, 8-15 _b, 16 the wheel, 17-24 _c, 25-28 neon_a-d, Scripts)
and plays it looping in world coordinates; level99.lua does the same with WonderWheel_99, the same tracks without
a camera (Scenes). Each update (every 2 ticks, 30 a second) the scene's runner sets every
bound object's transform from its track: positions linear, rotations slerped between keys
(Scenes: objects). Confirmed (code) for the mechanism; the motion below is read from the disc
(counts and values, 2026-10-06):
- The loop: 600 frames (20 s). Every object ends where another starts, so the loop is seamless: the wheel and the four neons each end turned 45° from their start (one rim car's spacing), every rim car ends on the next one's start, and the sliding cars end on another sliding car's start.
- The wheel (slot 16) and the neons (25-28): fixed at the hub (515.51, −68.89, −188.67), four rotation keys (frames 0, 255, 508/510, 600) turning them 45° about the axle: about 2.25° a second, a turn in 160 s. The neons turn with the wheel (their own start angles differ).
- The rim cars (
_c, slots 17-24): on a ring 21.55 m from the hub, 20 position keys and about 45 rotation keys each; they keep hanging level, swinging at most 6.6° about it. - The sliding cars (
_a,_b, slots 0-15): between 12.6 m and 22.2 m from the hub, 16-372 position keys each: the cars that run along curved tracks between the rim and the inner ring, tipping up to about 60° on the way. -
The neon flicker: scene events on the neon tracks, type 24 (message
0x12, show) and 25 (message0x13, hide), confirmed (code) at0x00354d98and0x003ef188. Frames (30 a second), from the start of the loop:Neon Hidden Shown neon_a0-60, 120-210, 270-360, 420-510, 570-600 60-120, 210-270, 360-420, 510-570 neon_b0-30, 60-180, 210-330, 360-480, 510-600 30-60, 180-210, 330-360, 480-510 neon_c30-150, 180-300, 330-450, 480-600 0-30, 150-180, 300-330, 450-480 neon_dnever always So the signs take turns (c, b, a every 5 s, each lit for 1-2 s) over the always-lit
neon_d. - The camera: the scene's own, fixed at (462.60, −122.35, −187.93) for the whole loop (Front end).
When the scene stops (SceneStop, a movie, a level start) the objects are released where they stand; playing it
again restarts every track from frame 0. Inferred from Scenes: ending.
Pickable objects¶
Triangle's pick-up search (Breakables) and Human_PickUpObject take only objects with flag
0x8000 (object +0x54). Each class's init sets the flags, confirmed (code):
| Class | Init (+0x54) |
Pickable | Exceptions |
|---|---|---|---|
melee_weapon (0x003fd420) |
0x218081 |
yes | bit 0x80 cleared for model hash 0x13ff8ee4; its message 0x19 sets or clears 0x8000 |
thrown_weapon (0x00403090) |
0x228081 |
yes | |
overhead_weapon (0x003ff6c8) |
0x228001 |
yes | not for model hash 0x8fc6ac30 |
dyn_molotv (0x00404b38, the Molotov) |
0x228081 |
yes | |
pickup_item (0x003f17e0) |
0x208081 |
yes | not for model hash 0x2fd690d6 (also cleared in 0x003f23f8) |
powerup_item (0x003f2700) |
0x808081 |
yes, but the search skips the class: it is walked over | |
simple_object |
1 (or 0x11) |
no | 0x8080 added for model hash 0xfcbe9fbb |
The search also refuses bit 0x10, bit 0x4000000, an object whose type value (CfgObj field +0x62, read through
vtable +0xdc) is 75 or more unless the human's record +0x11b is 13, the model hash 0xd2cfcd44 for a non-player,
and anything out of sight. At runtime (slot 1) dyn_masks, the cash register and the swinging doors had no 0x8000.
Weapon piles (dyn_pile)¶
A pile is a world object that is never picked up itself: each take makes a new object in the taker's hand and the
pile stays. The 13 types of class dyn_pile (Script types) are pickable (flags
0x228481, so 0x8000), update every 20 ticks and have a solid box; dyn_donut_spawner has no model.
Confirmed (code) at the cited addresses.
- Triangle (or an AI's pick-up action) sends the pile message 0 like any pickable object
(Breakables: a player must have empty hands).
DynPile_OnMessage(0x003d3d30) answers by sending the taker message0x14with the pile as the object. - The taker's
0x14(Human_HandleMessage,0x00245920) plays the pick-up clip of the pile'spickup_anim(Combat: the pick-up clip;nagives 461, or 462 when the pile is more than 0.8 m above his feet). A spray-can box (type 44) is refused first when the player's spray charges (item 3) are at the limit. - The clip's take event (event 9) sends the pile message
0x1b, which the pile answers with human message 3 (same function). That makes the object by the pile's object type (CfgObj'stype, type record+0x86), plays the pile's take cue at the pile (3D, volume 1.0), and puts the new object in his hand withHuman_PlaceItemInHand(human, name, 1, 0)(0x0024c280, theHuPlaceItemInHandpath):
| Pile type (value) | Piles | Object made | Take cue |
|---|---|---|---|
TYPE_BRICKPILE (17) |
dyn_brickpile |
dyn_brick |
25 |
TYPE_CHUNKPILE (18) |
(none of class dyn_pile in the list) |
dyn_cueball |
26 |
TYPE_BEERPILE (19) |
dyn_beerpile |
dyn_beerbottle |
27 |
TYPE_BASEBALLPILE (20) |
dyn_baseballpile |
dyn_baseball |
28 |
TYPE_LIQUORPILE (21) |
dyn_liquorpile |
dyn_bottle_a, _b or _c at random |
29 |
TYPE_MOLOTOVPILE (22) |
dyn_molotovpile, dyn_molotovpile_b |
dyn_molotv |
30 |
TYPE_POOLBALLPILE (23) |
dyn_poolballpile |
dyn_poolball08_ (a random number is drawn and not used) |
31 |
TYPE_OILPILE (41) |
dyn_oilcan_box |
dyn_oilcan |
none |
TYPE_SPRAYPILE (44) |
dyn_spraycan_box |
no object: one spray charge (item 3; the last up to 9), its pick-up sound, the spray hint once | none |
TYPE_DONUTPILE (46) |
dyn_donut_spawner |
dyn_donut_a, _b or _c at random, only while item 3 is below its limit (the spray-can test, copied) |
none |
TYPE_BEERPILEHEAVY (47) |
dyn_beerpileheavy |
dyn_beerbottleheavy |
27 |
TYPE_BOLTCUTTERBOX (49) |
dyn_boltcut_box |
dyn_boltcutter_b |
none |
So the numbers a level's CfgObj line gives a pile (17, 19, 20, 47, ...) are its object type, and the type alone
chooses what it hands out; no field names the object. dyn_blokpile is TYPE_GENERIC (2): its take falls to the
default case, Human_PickUpObject and then the pile itself in the hand (inferred from the code; not seen).
The take cue is the AudioManager cue table entry (0x00111130) of that number (Sound).
4. Running out. Only dyn_molotovpile_b (model 0x69b9d1a0) counts: each take adds 1 to record +0x18; when it
reaches the limit (record +0x14, 5 from DynPile_Init 0x003d3c28, replaced by message 0x22) the pile toggles
flag 0x8000 (no longer pickable), sets +0x1c = −5 and updates every 240 ticks; its next update
(DynPile_Update, 0x003d4058) zeroes the tint +0xcc, and the one after (tint +0xc8 now 0) is done, so it is
removed. Every other pile never runs out and never refills: it hands out an object on every take.
5. Messages 0x1b / 0x1c (handed / released) and 0x30 (knocked: 4-6 m/s along the knock, airborne) let a pile be
carried and thrown like a prop; a thrown pile that falls below −500 m is removed.
AI use (confirmed (code)):
GoalManWeaponPile(ManWeaponPileGoal_Process,0x002a3498; arguments on the binding page). Fetch state 0, empty-handed: with astyleof 1-5 the object is put straight in his hand (dyn_brick,dyn_molotv,dyn_manqheadred,dyn_porcelain_weap_b,dyn_beerbottle) with no pile; with style 0 the nearest object with flag0x20000withinpickupRadiusthat he can reach (Ai_FindObjectwith filter0x0053f2d0=ObjFilter_HasFlags0x20000; a pile has it) gets a GetItem goal (gait 3), whose pick-up action takes from it as in steps 1-3. After 21 updates with nothing found the goal ends. Holding something (state 1): wait the delay (delayMs, 500 ms), turn to the target (within 15°), check the line when asked, look only when the target is beyondthrowRange, else command0x10(throw), count the throw and waitpauseS, then fetch again (state 2 → 0) untilmaxThrows(−1: no limit) or a target withincloseRange. Since piles do not run out, a scripted thrower keeps throwing as long as the goal lives.ThrowSubTactic(ThrowSubTactic_Order,0x00322ba8), with its pile option (+0x16= 1): the objects within 30 m of the gang, nearest first, whose type's class isdyn_pile(name0x005747a0), not held, not bit 26 and not already claimed by this gang, with a reachable navigation polygon under them, are claimed for 5 s and each given to a free member (not attacked, no goal0x3f, top goal not ObjectPile or GetItem) as an ObjectPile goal (AI goals), until the order's count is met. Without the option it sends GetItem goals at loose objects of anim set 4 or 5 instead.TacticManWeaponPilegives each member a ManWeaponPile goal (AI code).
Objects in a human's hand¶
One mechanism carries every object a human holds (a bat, a bottle, a brick, a stolen stereo, a hat being picked
up): the object is attached to one bone of the human's skeleton at a fixed local offset, and both the bone
and the offset come from an event in the clip that puts it in the hand, not from CfgObj or a weapon table. The
bat is the worked case (Combat: a bat in hand); what the human does with it (anim sets, strikes,
+0x338) is there too.
The attachment (confirmed (code) at 0x003a1810, 0x003a1858, 0x003a19b0): world object fields
| Field | Meaning |
|---|---|
+0x54 bit 0x10 |
attached |
+0x58 |
the parent task (here the human) |
+0x6d |
u8, the parent's pose bone (Animation: bone transforms); 0 means the parent's own transform |
+0x10 / +0x20 |
while attached: the local position (vec4) and rotation (quaternion x, y, z, w) in the bone's frame |
Obj_Attach (0x003a1810) sets the bit, the parent and the bone. Obj_Detach (0x003a1858) first writes the
object's pose "now" (vtable +0xa4, Obj_GetPoseNow) into +0x10 / +0x20, then clears the bit and the parent, so
a detached object starts exactly where it was drawn. Confirmed (code).
The world pose (Obj_GetWorldPose, 0x003a19b0, function word +0x9c of the world object's vtable 0x005453a0
and the car's 0x00544c08; confirmed (code), both slots read): unattached, +0x10 / +0x20; attached, three
transforms composed with 0x003359b0 (rotation qa · qb, position pa + qa · pb, no scale):
world = parent's world transform (its vtable +0x9c: for a human, its position and rotation)
∘ the bone's transform, when the bone is not 0 and the parent is a human with a model instance
∘ the object's local (+0x10, +0x20)
A human's bone transform (0x0023bde8, its vtable +0xbc) is the bone's entry of the per-update bone cache
(Combat: the bone cache's 0x006b6880 + index × 0x470, built once per update from the
playing pose, model space) with the position multiplied by the human's scale (+0x65c, 0.97 for Rembrandt) and the
rotation as is. The object itself is never scaled. 0x003a1ad8 (vtable +0xac, the pose extrapolated to now) does
the same for an attached object, the local rotation advanced by its angular velocity (+0x40, zero while held).
At runtime (confirmed (runtime), PCSX2 2.9.94, slot 1 copy, the player 1 m behind a dyn_bat_tuff, triangle,
then stick 60 % up for 35 updates, square, R1 held 30 updates, stick 100 % up for 30 updates, triangle): the bat's
RenderWare frame matrix (its instance +0x1c atomic → frame, LTM at +0x50, axes as (x, z, −y)) equalled
human transform ∘ (scaled bone 25) ∘ local to within 0.005 m and 0.005 in every axis component on 256 of 257 held
updates, read from the values of the update before the draw; the one exception was one update's run distance
(0.26 m at 7.8 m/s). So the draw uses the pose of the latest update, with no smoothing or lag of its own.
Clip events that place a held object (confirmed (code) at 0x00101dd8, which sends the human message
0x80 + type with the event's bone (+6), position and rotation (Animation: events); the
human's handler is 0x00245920):
| Event | Message | What the human does |
|---|---|---|
| 9 | 0x89 |
the object being picked up (human +0x33c) is sent message 0x1b take, with the human, the bone, the event's position × the human's scale, its rotation, and whether the human is in a scene |
0x36 |
0xb6 |
the same (the left-hand clips 463 / 464 use it, bone 19) |
0x22 |
0xa2 |
with a pick target and nothing in hand, as 9; otherwise the held object (+0x338) gets message 0x32: re-attach at this bone and offset, nothing else. GhettoPickUp's clips 549 / 550 carry one per frame from 13 to 39, animating the object in the hand |
0x34 / 0x35 |
0xb4 / 0xb5 |
as 9 / 0x22, only for object types 24 and 27 |
0x37 |
0xb7 |
writes bone and offset straight into the object at human +0x348 (or +0x34c); what those hold is not traced |
| 10 | 0x8a |
release with something in hand: a throw (0x002586d8) under state flags 0x1000010, else a drop (0x00257f38) under 0x4000 or in a scene |
Take (message 0x1b; melee_weapon 0x003fe490, simple_object 0x003ef188, confirmed (code)): velocity and
angular velocity zeroed, attached to the human at the bone, flags 0x6000000 cleared, the local pose set to the
event's. When the human is not in a scene, the local position is then moved along the object's own y axis by the
type's float at CfgObj +0x70 (the local rotation applied to (0, v, 0)), so a model whose origin is not at its
grip is slid to it (0.39 m for dyn_bat_tuff). The object then sends the human message 3 (applies its anim set and
puts it in +0x338, Combat: a bat in hand) and message 0x17 (with CfgObj +0x68; not traced).
There is no blend: the object stays where it lay until the event's update and is in the hand on the next draw.
melee_weapon keeps the event's position in its data block too, and moves the local position again along y by
CfgObj +0x70 / +0x74 on its messages 0x12 / 0x13 (senders not traced).
Scripted placement (HuPlaceItemInHand, 0x00238540 → 0x0024c280 → 0x0024c6d8 → 0x00101360, confirmed
(code)): drops what the human holds, creates the object by name at the human's position, and reads the first type-9
event of the low pick-up clip its pickup_anim selects (Combat: the pick-up clip) without playing
it, sending the object message 0x1b at once; the +0x70 slide applies when object +0x110 is −1. Unlike the clip
path the event's position is not multiplied by the human's scale. A left-hand or hat pick-up clip has no type-9 event,
so nothing is attached.
Drop (0x00257f38, triangle with nothing to take, a pick-up of a two-handed or hat object, a scene's release;
confirmed (code)): pops the anim set the object pushed, records the human as its last holder (object +0x11c) unless
the type is 24 or 27, makes its physics body (Obj_CreatePhysicsBody), reschedules its task, and sends it message
0x1c detach with a zero velocity. If the object (flags 0x30000 without 0x400) now lies farther than
0.35 × scale − 0.01 m from the human across the ground, it is pulled back to that distance. A removal time is set
(object +0x120 = now + 120 s for type 11, 1 ms for type 39, a game setting at 0x0051489c +0x26c for most
weapons, none for model hash 0x03c5256c). It then falls under physics from the hand's pose. At runtime the bat
dropped from the hand came to rest on the ground (z 0.3, the ground's height there), flags 0xc321a081
(confirmed (runtime)).
Throw (Human_ReleaseThrow, 0x002586d8, confirmed (code)): as the drop, but the physics body is made per the
type (CfgObj +0x84 = 2 or +0x5a ≠ 1, else ThrownObject_MakeBody 0x003923b8) and gets body flag 1 (the
first contact after the throw, Throws), the object is rescheduled, its velocity comes from
Human_ComputeThrowVelocity and is sent in message 0x1c detach with the thrower, the thrower becomes its last
holder (+0x11c) and +0x116 is set, and the human's target is warned (0x0021d5c0). The removal time is the game
setting 0x0051489c +0x26c for every model but four (0x03c5256c gets none; three others keep theirs). The
details are in Throws.
Detach (melee_weapon, MeleeWeapon_Detach 0x003fe6b8): the velocity, given in the holder's frame, is turned
by the holder's world rotation and set (vtable +0x74). Five model hashes (object +0xc4), the knives
dyn_bowie, dyn_hunter, dyn_tknife, dyn_swhbld and dyn_swhbld_super, are tested first (0x003fe7ac-0x003fe7f0):
they take the holder's rotation and the angular velocity (−8π, 0, 0) rad/s (its vtable +0x7c) whether thrown or
dropped. Otherwise a thrown object (velocity not zero) of anim set 1 or 2 takes the holder's rotation and (−4π, 0, 0);
any other thrown object keeps its own rotation and gets (0, 0, −kπ), k random in 5-7; a drop keeps its rotation and
gets no spin. The angular velocity is stored as given (+0x7c writes task +0x40,
0x003a19a0) and the integrator applies it as q += ½ ω ⊗ q dt (Quat_IntegrateAngular 0x00335b08), so it is in
world axes, not the object's or the thrower's: a set 1 or 2 weapon thrown along world y tumbles end over end, one
thrown along world x rolls about its line of flight. Confirmed (code).
simple_object's detach only detaches.
Holstering. No code moves a weapon to another bone (back or belt): a human holds one object (+0x338), in the
hand its clip event named, until it is dropped, thrown or broken; a weapon not in a hand is a free object. Inferred
from the senders of messages 0x1b and 0x32 above (all clip events or the scripted placement).
HuRender (0x00237e98) draws a human and then every object attached to it that is not hidden (flag 4), giving each
the human's alpha (confirmed (code)). In the normal draw an attached object skips the small-size fade and is drawn
only within its parent's ObjShow distance (Drawing).
A weapon given to a Warrior¶
What a crew member does with a weapon the player hands him by triangle (the "Give Weapon" prompt, user item 37). The hand-over itself is AI: the weapon swap; this is what follows. Confirmed (code) at the addresses cited unless marked.
- It is in his hand, attached and drawn like the player's. The swap places the object with
Human_PlaceObjectInHandByPickupClip(0x0024c6d8), the same path asHuPlaceItemInHand: the first type-9 event of the low pick-up clip of the type'spickup_animgives the bone and offset (bone 25, the right hand, for 461, 481 and 498; Combat: a bat in hand), and message0x1btake attaches the object to the Warrior (+0x54bit0x10, parent, pose bone) and puts it in his+0x338; message 3 applies its anim set. There is nothing player-only in any of this, andHuRender(0x00237e98) draws every human's attached objects after the human, so the weapon shows in the Warrior's hand on the next draw, posed by his bone cache. A left-hand or hat object (pickup_anim5 or 6) has no type-9 event and stays on the ground (inferred, as for the scripted placement). The prompt that later offers it back ("Get Weapon", HUD string0xd, he holds) reads the same+0x338(AI: the Warriors' think, step 7). - He fights with it. An AI attacks through the player's dispatcher (Combat: an AI's
attacks), and square and cross choose their clip by the held object's set
(
0x00231a80, Combat: moving attacks with something in hand), so a Warrior holding a bat (set 3) swings 34 / 36 (37 at a grounded target, though thelevel34Furies also used the unarmed grounded strikes 193 / 194 with a bat in hand, Combat: a bat's hit), a baton (set 2) 39 / 41, a knife (set 1) 45 / 47. - The damage is that clip's damage table value plus the weapon's:
Strike_Contact(0x0021b290) adds the object type'sCfgObju16+0x58and multiplies by the attacker's class record's first float when the held world object's body flags have0x10000(the melee weapons) (confirmed (code); whether this branch runs for any held weapon's swing is open, below). Correction (issue #492, confirmed (runtime) fordyn_batin thelevel34Furies' hands only): their bat swings (501, 36, 37) never reachedStrike_Contact; each hit was the bat's own body touching the victim (WorldObject_OnContact0x00394050→ThrownObject_HitHuman0x00392b88), which dealsCfgObj+0x58× the holder's class float, without the clip's damage (Combat: a bat's hit). Not checked: the knife (set 1), the baton (set 2), other bats, and the player's own bat swings, so whether a weapon hits as hard in a Warrior's hand as in the player's is open. - His choices change too.
Brain_PickAttackadds the armed bonus (+100 to every candidate of kinds 0-9 with a weight; category 14, the Warriors, is not one of the categories 9, 10, 13 that get 0) (AI: picking the attack kind);Human_CanStartAttackmeasures kinds 0, 1 and 5 with the set's clips (set 3: 36, 34, 35; set 2: 41, 39, 40; set 1: 47, 45, 46), and the fight goal lets an armed fighter attack from anywhere within 1.1 × the far range (AI: objects in an AI's hands). A set 1 throwable lets kinds 0 and 1 reach 12 m (thrown), set 4 15 m, set 5 30 m. - What he keeps. Outside co-op a Warrior holding an object of kind 4 or 6 (the overhead props and the ghetto
blasters) drops it once his enemy list is empty and no steal tactic runs (
WarriorBrain_Think0x003052f0, step 3); a bat, baton, knife or throwable stays in his hand until he uses, throws or swaps it, or a hit makes him drop it: during a throw clip or clip 602 with set 1 (Human_ApplyPendingDamage0x00265f70), when the hit stuns or knocks him down while he is hurt, when it comes from a counter or special clip (620 and above), or when he is grabbed holding a bat; all but the throw case are stopped byHuSetKeepWeapon(Combat: dropping what is held when hit).
Coney fixes (user item 37; the hand-over works, the member holds the object, play_level_objects.cpp gives him
its anim set):
- Draw every human's held object, not only player 1's: Coney's world-object draw places just
m_player->human().script().heldObject(play_level_world.cpp); each AI human'sscript().heldObjectneeds the same attached pose (his hand bone from his own pose, his scale, the type's grip slideCfgObj+0x70). - Attach the given object to the receiver's right hand with the pick-up clip's type-9 offset, as the player's pick-up does, and leave a left-hand or hat object on the ground.
- Check that an AI fighter with a held set picks its square / cross clips from the held object's set (34 / 36 for a
bat) and that an AI's bat hit deals the bat's
CfgObj+0x58× his class float, as the bat's own contact does (Combat: a bat's hit). Onlydyn_batin an AI's hand was measured; the knife, the baton and the player's bat are unchecked, so keep their damage path an open question until they are. - Add the armed bonus to Coney's attack pick and the set's clips to its reach test, if missing.
- Drop a held kind 4 or 6 object from a Warrior with no enemies outside co-op.
- Test: give a bat to a Warrior; assert he holds it, its world pose equals his transform ∘ scaled hand bone ∘ local
on the next draw, and an attack of his on an enemy plays 34 or 36 and deals the bat's
CfgObj+0x58× his class float (not the clip's damage; fix 3).
Two-handed objects¶
How the original decides that a held object is carried in both hands (the cash register, a drum, a chair, an
amp) rather than hung from the right hand like a bat or a bottle. Asked by issue #433: in Coney the level99
checkpoint 2 register is lifted with the right clip but then held like a one-handed prop.
There is no "two-handed" flag. The look comes from two bytes of the object's type record (CfgObj,
Object types), set once from the objects file (config_preload3.lua's CfgObj lines, arguments 14
and 20, the binding), never per model or per level:
| Type byte | CfgObj argument |
What it chooses | Two-handed value |
|---|---|---|---|
+0x65 |
14 pickupAnim |
the pick-up clip (Combat: the pick-up clip) | 2 TwoHandPickUp: 503, or 504 above 0.8 m |
+0x87 |
20 animSet |
the move style pushed while it is held, which swaps the idle and locomotion clips | 4 OVERHEAD_WEAPON_SET |
The object is still attached to one bone, the right hand (25); both hands meet it because every clip the style
plays (the idle, walk, run and turns below) poses both arms round the object (inferred: the bone and the clips are
read, the arm pose is not measured). The object's class does not matter: overhead_weapon and dyn_cashreg behave
alike because their take handlers are alike (below), and the class only decides breaking, wear and drops.
1. The pick-up uses +0x65 (Human_PlayPickupClip, 0x0025e5a8, from the human's message 0x14): 2 plays
503 ANIM_BARREL_PICK_UP, or 504 …_HIGH when the object is more than 0.8 m above the human's feet.
Confirmed (code), as on Combat.
2. The take (the clip's type-9 event, human message 0x89 in Human_HandleMessage 0x00245920) sends the
object message 0x1b with, in push order, the human, the event's bone, the event's position × the human's scale,
its rotation, and whether the human is in a scene (Human_IsInSceneState). The two-handed classes' handlers
(OverheadWeapon_OnPickedUp 0x003ff918, DynCashreg_OnPickUp 0x003c0108) zero the velocity and spin, attach the
object at the event's bone (flag 0x10) and then set its local pose:
if not in a scene: local position = type record +0x00 (holdPos, w = 1) // ScriptObj_GetVectorProperty 11
local rotation = type record +0x0c (holdRot, x y z w) // ScriptObj_GetVectorProperty 12
else: local position, rotation = the event's
So in play the clip event gives only the bone; the offset is CfgObj's holdPos / holdRot, not scaled by the
human and not slid by CfgObj +0x70 (that slide is melee_weapon's, Take). Confirmed (code) at
0x003c01f0-0x003c0244 and 0x003ff918; the property numbers at 0x003a3978 (11 reads three floats at the record's
start, 12 four floats at +0x0c; Cfg_AddObjectType 0x00390f18 stores arguments 21 and 22 there).
overhead_weapon additionally sets path flag 8 under its first pick-up spot and, for model hash 0xac6f5902, clears
its own 0x8000.
3. The move style. The handler then sends the human message 3 with itself. The human's case 3 reads the
held object's type +0x87 and, when it differs from the human's current style (Human_GetMoveStyle), calls
Human_SetCarryMoveStyle (0x00221ed0):
- style 0 (
NONE_SET): nothing; - the human in the hidden style
0x14(Stealth) and the object's style 4 or 6: the top style is replaced (0x00253e68, a pop then a push; confirmed (code)); - no style yet: the object's style is pushed (
0x00253ed0; confirmed (code)); - another style on top: that one is popped, the object's pushed, and the old one pushed back on top.
A push applies the style with Human_ApplyMoveStyle (0x00253688): the anim slots are
reset to the defaults and the style's overrides written, then the gait speeds are recomputed from the new clips'
root motion (0x00253688 ends in the speed computation). Message 0x16 (sender not traced) calls the same function
unless the human has state 0x180000. Confirmed (code) at 0x002460f4 and 0x002469c8.
Style 4, the two-handed carry (Human_ApplyMoveStyle case 4, confirmed (code); clip names from the
anim id reference):
| Slot | Default | Style 4 | Clip |
|---|---|---|---|
| 0 idle | 388 | 509 | ANIM_BARREL_NORMAL_IDLE |
| 1 step forward | 401 | 535 | ANIM_BARREL_FIGHT_MOVEMENT_STEP_FORWARD |
| 2 dash forward | 403 | 537 | ANIM_BARREL_FIGHT_MOVEMENT_DASH_FORWARD |
| 3 sneak walk, 4 walk | 407, 408 | 515 | ANIM_BARREL_MOVEMENT_WALK |
| 5 jog | 409 | 516 | ANIM_BARREL_MOVEMENT_JOG |
| 6 run | 410 | 517 | ANIM_BARREL_MOVEMENT_RUN |
| 7 sprint | 411 | 518 | ANIM_BARREL_MOVEMENT_SPRINT |
| 8 slow turn right, 15 combat turn | 390, 360 | 529 | ANIM_BARREL_FIGHT_TURN_RIGHT |
| 9 sneak walk start, 10 walk start | 412, 413 | 519 | ANIM_BARREL_MOVEMENT_WALK_START |
| 11 combat idle | 358 | 527 | ANIM_BARREL_FIGHT_IDLE |
| 12 combat shuffle forward | 366 | 535 | as slot 1 |
| 13 combat dash forward | 368 | 537 | as slot 2 |
| 14 combat walk forward | 372 (player 380) | 541 | ANIM_BARREL_FIGHT_MOVEMENT_WALK_FORWARD |
| 33 run stop | 417 | 523 | ANIM_BARREL_MOVEMENT_RUN_STOP |
Every other slot keeps its default (the attack slots do not matter: with set 4 in hand square and cross throw,
Combat: moving attacks with something in hand). Style 6 (GHETTO_SET, the two ghetto
blasters, pickupAnim 7) is the same shape with the ghetto clips (idle 555, walk 561, run 563, run stop 569, ...).
Styles 1-3 (knife, baton, bat) change only attack and block slots, which is why a bat is carried with the plain idle
and walk (Combat: a bat in hand).
4. Letting go. The drop (Human_DropHeld, 0x00257f38, Drop) and the throw pop the style the object
pushed, so the slots go back to the defaults. Before a pick-up clip, a held object is dropped first when the new
object's pickupAnim is 5 or 6 or the human's style is 4 or 6 (Combat).
At runtime (confirmed (runtime), PCSX2 2.9.94, quick-save slot 8 copy (level99 checkpoint 2), scenario
repo:research/traces/scenarios/cashreg_carry.toml: the player put at (53.23, 56.45) facing +y, 0.8 m south of the
store's register at (53.23, 57.26, 1.63); triangle at update 20; stick 60 % for 80 updates from update 110; one sample
per update):
| Update | Clip | Move styles | What happened |
|---|---|---|---|
| 21 | 504 | none | triangle: the high clip (the register is 1.42 m above his feet) |
| 28 | 504 | depth 1, style 4 | the take: +0x338 holds the register; slots 0 / 4 / 6 / 11 = 509 / 515 / 517 / 527 |
| 36 | 509 | 4 | the clip ends in the barrel idle |
| 111-120 | 519 | 4 | the stick: the barrel walk start |
| 121-190 | 515 | 4 | the barrel walk, gait 1, 1.46 m/s |
| 191 | 509 | 4 | stick released: back to the barrel idle (no run stop from a walk) |
The register's attachment through all of it: bone 25 (the right hand), local position (0.3075, 0.1306,
0.2869) m and rotation (−0.1485, 0.0522, −0.9863, −0.0484) (x, y, z, w), exactly its type record's +0x00 /
+0x0c (type index 262; its +0x84-+0x87 read shape 1, axis 0, type 2 TYPE_GENERIC, set 4). The bone came
from clip 504's event; that 503 (the floor clip) also names bone 25 is inferred.
Which objects share it (the reference list repo:research/references/objects.yaml, from the CfgObj lines):
pickupAnimTwoHandPickUpwithanimSetOVERHEAD_WEAPON_SET: all 66overhead_weapontypes (the drums, chairs, amps, crates, carts, trash cans and bags,dyn_box_stereo, ...),dyn_cashreganddyn_cagedchic(classdyn_chicken) — 68 types, the same 68 that carry set 4.dyn_cardrack(classdyn_masks) hasTwoHandPickUpwithNONE_SET, so it would be lifted with 503 and then carried with the plain clips; adyn_masksobject was not pickable at runtime (Pickable objects).GHETTO_SET(style 6): the two ghetto blasters, carried the same way with their own clips.
So the register is not special: anything Coney lifts with 503 / 504 needs the same carry.
What Coney must do (the gap, from Coney's code: the take uses the pick-up clip's event offset for every class,
world_objects::heldAttachment in repo:src/world_objects/object_tasks.h, and the animator's
anim slots know only the hidden style; the pick-up returns to the plain idle 388):
- Read each type's
animSet(CfgObjargument 20,+0x87) andholdPos/holdRot(arguments 21 / 22) with the otherCfgObjfields. - On the take of an
overhead_weaponordyn_cashregobject outside a scene, attach it to the event's bone with local positionholdPosand rotationholdRotas given: no human scale, no+0x70slide. Keep the event's offset (× scale, slid) formelee_weaponand the classes that do so today; in a scene use the event's pose for both. - When an object is put in a human's hand (player or AI), if its
animSetis not 0 and differs from his current move style, apply it by the rules of step 3 above (push; replace the hidden style0x14for 4 or 6; otherwise slip it under the top style). - Implement move style 4's slot table above (and 6's, from the same function, when the blasters are done): reset the slots to the defaults, write the overrides, recompute the gait speeds from the new clips. The pick-up clip's follow-on then lands in slot 0, 509, not a fixed 388.
- Pop the style on the drop and the throw, restoring the default slots (or the style beneath).
- Test, unit: applying style 4 to the default slots gives exactly the table above, and popping it restores the
defaults; a take outside a scene gives
holdPos/holdRotunchanged at bone 25. - Test, disc, through the player's path (
coney::test::DiscSession,level99checkpoint 2, aLivePaddriver): walk the player with the stick at 60 % from the shop floor to the store's register (53.23, 57.26), stop facing it and tap triangle. Assert, in order: clip 504 starts; within 10 updates the register is held, his top move style is 4 and slot 0 is 509; when 504 ends the idle is 509; with the stick at 60 % he plays 519 then 515 (not 413 / 408); the register's local pose is itsholdPos/holdRotto 1e-4 at bone 25; triangle again drops it and the idle goes back to 388. Print counts only. The same test against adyn_trashcanon the street (slot 8 has one at (47.71, 2.28), Trash cans and bags) covers theoverhead_weaponpath.
Open: what the arms do in the barrel clips (that both hands reach the object is inferred from the clips' names and use), and the bone named by clip 503's take event (inferred 25, as 504's).
Throws¶
Confirmed (code) unless marked. Gravity in flight is 15.68 m/s² (the aim trace steps the z speed by −0.2613 per 1/60 s, Graphics).
Starting a throw. Square or cross with an object of set 4, 5 or 6 in hand (Player_CrossWithWeapon,
0x002880d8) plays a throw clip at once, by gait; nothing waits for the button's release:
| Set | Standing or fight stance | Walking | Jogging, running or sprinting | Target search |
|---|---|---|---|---|
4 (overhead_weapon and the like) |
505 | 506 | 507 | 10 m |
5 (thrown_weapon: bottles, bricks, balls) |
467 | 471 | 472 | 20 m |
| 6 | 551 | 552 | 553 | 20 m |
Outside the aiming state, a player with no brain target picks one (Player_PickThrowTarget, within the search
radius) and turns to face it over the time to the clip's first event; the held object is sent message 4, then
Human_StartMeleeThrow plays the clip. The release is the clip's event of type 10: the event code
(Anim_FireFrameEvents, 0x00101dd8) sends the human message 0x8a, and his handler (0x00245920) calls
Human_ReleaseThrow when he holds an object with held flag 0x1000010. The event frames (30 per second, before the
clip's rate; read from the disc's clips):
| Clip | Type 10 at frame | Clip | Type 10 at frame | Clip | Type 10 at frame |
|---|---|---|---|---|---|
467 gen_1hand_throw |
5 | 505 gen_2hand_throw_fwd |
16 | 551 ghetto_throw |
9 |
471 gen_1hand_throw_walk |
11 | 506 gen_2hand_throw_walk |
12 | 552 ghetto_throw_walk |
10 |
472 gen_1hand_throw_run |
11 | 507 gen_run_2hand_weapon_throw |
13 | 553 ghetto_throw_run |
7 |
The melee weapons' throws have one too: 491 gen_knifethrow frame 18, 502 gen_batthrow frame 15.
The aiming state (Human_MoveThrowAim, 0x00244770) is entered only with a set 5 object: L1 pressed with
no target locked, or with a friendly one or one of his own gang (Player_OnL1Pressed 0x0027da10, then
Human_EnterThrowAim 0x00227b30: state 0x200000000000, move style 0x13, clips 466-470). It stores the human's
position at +0x610, his rotation at +0x620 and the pitch 0.157 rad (9°) at +0x630. Each frame the left
stick turns the aim (outside a dead zone of 26 of 128, 0.000589 rad per step of the stick value) and pitches it,
clamped to ±0.628 rad (36°). L1 again leaves it, and so does losing the object (held flag 0x20000 gone), a grab or
a mug, or an AI in control. Square or cross then throws as above, along the aim.
The aim's heading is the human's own rotation, confirmed (code) at Human_SetThrowAimState (0x002446b0) and
Human_MoveThrowAim (0x00244770): the move function writes the human's facing every frame and copies it to
+0x620, so turning the aim turns him. It starts from the way he faces, with one exception on the first frame
(human +0x256, cleared by Human_SetThrowAimState, set by the first Human_MoveThrowAim): for a player with one
view per player camera (0x0050b198 = 0x0050b19c) whose active camera is of type 2, the
follow camera (camera vtable +0x1ec, camera types), his heading is
set once to the follow camera's: the direction from its position (vtable +0x21c) to its focus point (+0x24c),
horizontal part only. Under any other camera, such as a script's locked camera, he keeps his own facing. The follow
camera then frames the aim origin while he aims (Camera: the follow update, step 5). Each frame
after that:
Human_TraceThrowAimruns (below).- With a human target (
+0x638) he turns straight to face it (heading from his release point to the target's point,+0x620set at once) on each of the 5 frames after a new target is found (+0x63c, set to 5 when the target changes, counted down here), and on any frame both stick axes are inside the dead zone. Objects are never turned to. - Otherwise the stick's x outside the dead zone turns him by x × 0.000589 rad a frame (x = the stick's offset from the centre less 25, about ±0.06 rad a frame at full tilt), with action 10 (the turn); the stick's y pitches the aim by the same rate, clamped to ±0.628 rad.
The aim trace's targets, confirmed (code) at Human_TraceThrowAim (0x0018fee0) and IPhysics_CollideShape
(0x0033e7f0): the 18 kept arc points make 17 segments; a sphere of radius half the held object's type +0x78
is swept along each in turn, in two passes:
- Humans: a query of bodies only (option
2, not the level mesh), of type mask0x40(humans), with body flag0x20000(THROWNWEAPONTARGET, layers) and none of0x180000, the thrower's own body skipped, and the callbackThrowArc_TargetFilter(0x0018f7b8): only an AI human (+0x1b0= −1) that is not friendly to the thrower, and with the stick pushed sideways only one on that side. The first segment that meets one gives the candidate human and its fraction. - Anything in the way, from the first segment up to the candidate's (all 17 when there is none): the same query
with the level mesh added (option
1), any type, still flag0x20000and not0x180000, no callback. The first contact, unless it is in the candidate's segment and no nearer than the candidate, ends the arc there (the strip is cut at the contact) and replaces the candidate.
The results: +0x638 = the human target (0 when none or blocked); +0x634 = the hit's task when it has type
bit 2, which every task class has (Tasks: type bits): the target human, or the first world
object, car or other body with THROWNWEAPONTARGET the arc meets; 0 when the arc ends on the level mesh (a wall,
the ground) or meets nothing. So a wall ends the trace and nothing behind it is seen. HuIsAimingAt(player, object)
(Human_IsAimingAt, 0x0023a468) is true when the player's +0x634 is that object. The trace also clears the
brain's target (Brain_SetTarget(brain, 0, 1)) and stores 0x006ebd30 at brain +0x128, so a throw from the aim
has no locked target and follows the pitch rule of the velocity below. Which world objects have THROWNWEAPONTARGET
is their CfgObj PHYFLAG argument; the objective marker dyn_w_mission has exactly that one flag (config_preload3:
shape OBB 0.52 × 0.52 × 2, PHYFLAG.THROWNWEAPONTARGET), so it can be aimed at.
level80's throw lesson (Scripting): C3.throwObjective = Objects.dyn_w_mission, the marker
level80.lua spawns at (−259.542, 278.038, 1.27) (beside the flag fBottleTarget, (−259.6, 278, 1.3)) and
ShowDistractCopObjective shows (ObjShow). C3.TargetCheck, every 200 ms while the player holds a bottle
(HuWhatAmIHolding 4) and his control is throwControl, shows TT_29 ("Press X to throw") once
HuIsAimingAt(C3.throwPlayer1, C3.throwObjective); C3.ThrowCheck goes back to TT_28 when it stops being true.
Confirmed (runtime), PCSX2 2.9.94, a contributor's run of level80 (re-mission-ref, 2026-10-08): +0x634 stayed 0 while
the stick pitched the aim down from 0.304 rad and became the marker (0x00ef97c0) at pitch 0.139 rad; TT_29 was set
13 updates later. The bottle then passes through the marker and breaks beyond it (inferred: a flying world
object's sweep meets only bodies with BLOCKOBJECTS (Sweeping a body, confirmed (code)) and the
marker's body has THROWNWEAPONTARGET alone), which is why the script tests for the bottle's break inside the
8.2 × 6.5 m vBottleBox round the marker (message 6), not for a hit on the marker.
The aim's clips (move style 0x13, Human_ApplyMoveStyle 0x00253688, confirmed (code)): slot 21 (the block
start) = 466 gen_1hand_throw_enter, slot 23 (the block sustain) and the idles in slots 0 and 11 = 469
gen_1hand_throw_cycle, slot 24 (the block shuffle) = 470 gen_1hand_throw_turn. So he enters with 466, holds
469 and plays 470 while the stick turns him (action 10); inferred from the slots' names, not watched at runtime. No
code names 468 gen_1hand_throw_exit: leaving (Human_ExitThrowAim 0x00227aa8) only removes the style, so the
idle returns; whether 468 plays through the anim data is not traced. The throw from the aim is 467 (standing).
The aim trace (Human_TraceThrowAim, 0x0018fee0, each aiming frame) writes the two holder fields the first
flying update reads: +0x5f0, the release point = +0x610 + +0x620 turning (0.3009, −0.6354, 1.5865) (x
right, y forward, z up, metres), the same offset ThrowArc_PlaceAtHand (0x0018f628) uses for the arc; and
+0x600, the throw velocity = Human_ComputeThrowVelocity turned by +0x620. They are used only for a throw
released from the aiming state: Human_ReleaseThrow then sets the object's +0x10c to 1 (and leaves the aim,
putting the pitch back to 0.157), else to 0. A plain throw starts from the object's own pose in the hand with the
velocity of the detach message; the holder's stale +0x5f0 and +0x600 are not read.
The velocity (Human_ComputeThrowVelocity, 0x002570e8) is in the thrower's frame (x right, y forward,
z up; the detach and the aim turn it into the world):
- The weight factor
w= 1 for an object of kind+0x86= 11 (the knives), else 0.05 × the type's+0x62(CfgObjargument 6, read through vtable+0xd4with 1); halved when the thrower's scale is above 1.1. A bottle (+0x62= 20) hasw= 1.0, a crate or chair (50) 2.5, a fridge (75) 3.75. - Aiming with no target: (0, cos p, sin p) × 20 /
w,pthe aim's pitch+0x630. - With a target (aiming with one, or not aiming with a brain target or an object target): the direction from
the thrower's right hand (bone 25, posed) to the target point, turned into the thrower's frame. A human's target
point is his position plus, when his
+0xd8is set, a bone: bone 5 for an object of set 1 or 2; bone 6 (the head) unless the object has flag0x10000and is not of kind 10; else the middle height of bones 29 and 32 on his axis. A human in state0xe0000(down) is aimed at bone 3. An object target is its position plus its attach offset. Withuthe unit direction: when it points more sideways than forward (|u.x| > |u.y|) it is clamped to 45° (u.x = ±0.7071, u.y = 0.7071, u.z kept). When |u.z| < 0.85: speeds= 30 /w, v = u × s, and v.z += 7.84 × d / s (dthe straight distance: half of 15.68 × the flight time d / s, so the arc comes back down to the target's height there). Then, unless the thrower has flag0x40000(+0xe0), a random spread: withf= d / 30 clamped to 0.1-1, the velocity turns about world z byf× r × 8° and about the horizontal axis across it byf× r × 8° (r random in −1 to 1 each,0x6eb898; 8° × 0.6 for an AI of brain kind 3). A target almost straight above or below (|u.z| ≥ 0.85) falls to the default. - The default (no target): (0, 1, 0.1) normalised × 30 /
w, or × 50 /wwhen the human's stored gait+0x1a8(Characters) is above 2 (jogging or faster) and he holds a set 4 or 6 object or a throwable one.
So a bottle aimed at a target leaves at 30 m/s and a set 4 crate at 12 m/s, each with the lift that brings it down on the target. Nothing clamps the speed; the 45° rule is the only limit on the direction.
A thrown object meeting a human (WorldObject_OnContact, 0x00394050, when the hit body's owner is a human with
a body and a brain, +0xd4): ThrownObject_HitHumanTest (0x003928d0) handles two kinds first and the contact
ends (answer 0): kind 0x1d deals the human his own record's +0x144 damage with a particle and a sound and clears
his body flags 2 and 1; kind 0x22 deals 500 once per object (human +0x564 remembers it). Otherwise
ThrownObject_HitHuman (0x00392b88) deals the damage, plays the impact sounds (Sound events),
reports the noise and, for a knife that stays whole, sticks it in him (Human_StickThrownKnife). Only that stuck
knife makes it answer non-zero, and then the contact's answer is 0x40000 (the object's move stops there) before the
break test; for anything else the contact goes on to the break test below, so a thrown object that wears out on a
human deals its damage first and breaks on the same contact (confirmed (code) at 0x00392b88 and 0x00394050).
With no thrower (no holder and no last holder +0x11c), ThrownObject_HitHuman deals no damage and only plays the
material pair (the object's, 26), or (its own, its own) at 1.0 and (its own, 26) at 0.5 when it is breaking. The damage
is the type's +0x58 (× the holder's class factor while
held), negated against a friend of the thrower (no hurt flag), × 0.25 for kind 0x24 when the thrower's stored gait
+0x1a8 is above 2, × 0.5 from an AI of brain kind 3 against class 13, and from a player × 3 against human kinds 0x77
and
0x78 and × 1.25 against 0x87; a knife at its thrower's target or at a non-friend deals the type's +0x58 (the
victim's health for kind 0x4f). The victim's reaction (HitReact_FromObject, 0x0026b170, from his pending
damage) asks Hit_PickReactionClip with a severity from the object: set 2 → 3 (kind 10) or 2; set 3 → 2; otherwise
by the weight +0x62: under 20 → 0, 20-49 → 1, 50 and over → 2. He reacts toward the thrower's side, or toward the
object's point for kinds 0x22, 0x1d, 8 and cars (kind 8 plays no reaction).
Wear on contact (OverheadWeapon_ContactDamage, 0x003932c8, confirmed (code) from its assembly). It is the
first thing WorldObject_OnContact (0x00394050) does with each contact of the object's own sweep, flying or held
(not while its holder is in a car with +0x568 set), with the contact, the holder or else the last holder (+0x11c),
and whether the other side is a world object whose counters are already spent. One hit is taken
(WorldObject_TakeHit(obj, −1), 0x00393450: +0x10e first for an object with flag 0x4000000 (flying) or a held
dyn_cashreg, else +0x10d; 255 never wears):
| Object | Which contacts wear it |
|---|---|
class overhead_weapon (the name at 0x00580ed8), held or not |
every contact: the level mesh (floor, walls), bodies, humans |
material 0x9a (154, GUITAR), any class |
every contact |
a held dyn_cashreg (model 0x3c4e590b) |
every contact |
| any other class, held | only a contact with a human, and only when the holder's body record's +0x38 is 0 (not traced) |
| any other class, not held | only while its body has flag 1, and not against a world object already spent; with a last holder (a thrower) the flag is then cleared, so it wears once per throw |
Body flag 1 is set by Human_ReleaseThrow (0x002586d8, on whichever body the throw made) and
ThrownObject_MakeBody (0x003923b8, its only caller being the throw), and at body creation for kinds 0x1d
(TYPE_MOVINGVEHICLE) and 0x22 (TYPE_CHATTERBOXTRAIN) (Obj_CreatePhysicsBody 0x00391d48). A knock (message
0x30) does not set it: the dyn_beerbottle the trash props drop (Trash cans and bags) never wears
from its fall and stays whole, a bottle to pick up. A thrown_weapon (bottle, brick, ball) has hits 1, so it breaks on
its first contact after a throw; an overhead_weapon with 1 (the trash cans and bags, the lawn chairs) on its first
contact of any kind, thrown or swung into something while held.
At the end of WorldObject_OnContact, when either counter is 0, the object breaks (WorldObject_Break) and the
answer is 0x10000 (the move goes on: no bounce). Before that test a level contact runs WorldObject_OnImpact
(0x003939a8) and a body contact the human, car or object handlers above, so their sounds and damage come first.
Riot props lists what a break makes.
WorldObject_Break (0x00393e20, called with the contact and the holder), confirmed (code), in order:
- A holder drops it (
Human_DropHeld). - Message 1 to the object's class with, as popped: the contact normal (contact
+0x50) as the direction, the contact point (+0x90) as the hit point, kind −1, strength 100, then two handles. The class does the break: athrown_weapon(Script types), anoverhead_weapon(Trash cans and bags), adyn_cashreg(Script types); afade_objectpops and ignores it. - The collision body is freed (
Obj_FreePhysicsBody); a holder's strike contacts are cleared. - Kind 8 (
TYPE_MOLOTOV, onlydyn_molotv): an explosion of radius 1.5 m dealing half the contact damage (Explosion_DamageHumansInRadius). Nolevel99script spawns one and none was live in quick-save slot 8 (the hash0x183cc260only in the type tables; confirmed (runtime), read from the state), so nolevel99object has it. - Velocity and spin set to zero.
It plays no sound and removes nothing itself: the class's message 1 decides both. Its sounds come from the
contact handlers before it: on the level mesh WorldObject_OnImpact plays, for a breaking object, the pair (its
material, its material) at 1.0 and (its material, the surface's) at 0.5 at the contact point (else just the second
at 1 / (bounces + 1)); against another world object ThrownObject_OnContact (0x00393538) plays (its, its) at 1.0
and (its, the other's) at 0.5; against a human ThrownObject_HitHuman plays its own pairs (above).
The removal time (+0x120). WorldObject_SetRespawnTimer (0x00395d20) sets now + the time and marks the
spawn record (bit 0x1000000). The setting 0x0051489c +0x26c (CfgSetGlobalTimeToLive) held 30,000 ms in
quick-save slot 6 (confirmed (runtime)), so a thrown or dropped weapon stays 30 s. The streaming walk
(ObjectTaskManager_UpdateSpawns) checks it through ObjRecord_ShouldRemove (0x00399718): once the time has
passed the object is no longer pickable (flag 0x8000 cleared), stops at its next ground contact (0x8000000),
updates every 240 ticks (40 for one class), and its tint becomes white with alpha 0 (+0xcc = 0xffffff00,
taken the next update: it vanishes, with no fade); the next check that finds that tint removes it for good. A live
object with a removal time that is streamed out is removed, not stored.
How they get into a level¶
Glass and doors come from level scripts: SpawnBreakableGlass (929 calls in 32 chunks) and SpawnDoor (484 in
46), in the level's set-up after CfgSetDatabaseSizes; nothing else creates a glass_script or door task as far as
their creators' callers show (inferred). A pane's geometry is its three corners; a door's is its position, rotation
and type, and its model is the type name's (CfgObj +0x8c); both name two triangles of the level's static
collision mesh that stand for them. Confirmed (code) for the spawns, counts from the disc.
A pane's life¶
- Spawn (
0x0039c0e0): the three corners, the type, the texture coordinates, the flag and the two triangles are pushed, and a pane task named by the type in decimal is made. The initialiser (0x003e29e8) pops them in reverse and drops the flag. Confirmed (code). - Setup: both triangles are made two-sided (
0x003a4768) and given material 2 (GLASS,0x0038f8a8); the sprite rectangle is the type's whole-pane word, except type 14, which makes a sprite batch of its own (0x00020000); type 15 is hidden (colour 0); types 17 and 18 start broken, their triangles disabled, and 18 also sets the path polygon's flag 8 (0x003a47c0). With the alarm, pane bits+0xf8= 3 (0x0038ec30). Confirmed (code). - Window link (
0x0038f8a8): with the window link, the kind-4 link nearest the pane gets the avoid bit (0x00250b68) and, when the pane lies in a path polygon, it and its reverse are retagged kind0x40. Confirmed (code). - What breaks it: anything that sends message 1. There are no hitpoints: the first hit breaks a pane. The
senders are
Strike_Contact(0x0021b290), a thrown object (0x00393538) andBreakGlassInRadius(0x003963b8: every pane whose centre is within the radius, and everyTYPE_GLASSobject).Strike_Contactruns for a human's strike shape touching the pane's body, which covers a landed hit and a jump, and for an airborne human body touching it (Moving into a pane). Confirmed (code). - Hit (message 1,
0x003e2d90), once, while not broken: the broken sprite (or the pane hidden when it is 0), asub_glassshatter at the pane's centre with the pane's normal (+0x80), shard size 0.2 and size word (sub_stained_glassfor type 14), the triangles disabled, its collision body removed (0x003a5340→0x0038eec0,+0xe8= 0), colour0xffffff70, the pane marked broken. Type 12 also frees everydyn_carstereowithin 2 m (0x003a5870), for a pane a script places at a car; a parked car's own windows are its parts, not panes (Cars: windows). Message 0 is a shatter without the broken flag (the effect and the triangles only). Confirmed (code). -
Break (
0x0038f378, called by the human and thrown-object paths, not byBreakGlassInRadius):- with the alarm (
+0xf8both bits): theCrimeSceneflag moves to the pane and a break-in (crime type 1) is reported (AI: crimes); - with the window link: when the pane is in a path polygon, the polygon gets flag 8 and its
0x40link loses the avoid bit; otherwise the kind-4 link does; - every enabled flag of activity 9 (
_xwWindowLook, Flags) within max(width, height) of the pane is disabled (0x00417480), so nobody stands looking through a broken window.
Confirmed (code). 7. Statistics: a player's (or a player's gang member's) pane counts crime 10 (Statistics), at
0x0021b290and0x00393538. Confirmed (code). 8. No respawn: nothing clears the broken flag; a pane comes back only when its level's script spawns it again. Inferred from the handlers (messages 0 and 1 only). 9.ObjDestroy(pane)is not a break:Obj_Destroy(0x00396c58) without its second argument calls the object's vtable+0x4c, which for a pane (vtable0x00544ec8) isGlassPane_Release(0x0038ed08): body removed, script handler and handle freed, the task given back to the glass manager. No shatter, no sound, no broken sprite: the pane simply vanishes.BreakObjectsInRadiusnever reaches a pane either (it searches the object manager, and panes live in the glass manager; below). OnlyBreakGlassInRadius, a strike, a thrown object or an airborne body breaks one visibly. Confirmed (code). - with the alarm (
BreakObjectsInRadius¶
BreakObjectsInRadius(object, radius) (World_BreakObjectsInRadius 0x00396390 →
World_BreakObjectsAround 0x003961d0): takes the object's position, reports a crime there when the object's type
has class byte +0x86 = 30, finds up to 384 tasks of the object manager within radius of that point
(ObjectManager_FindObjects) and sends each message 0x15, the given object included (the "skip myself"
argument is 0 on this path). What 0x15 does is up to each type: a door takes a hit two updates later
(Doors), a dyn_molotv breaks itself (Script types: the Molotov). So the
scripts' ObjSpawn("dyn_molotv", flag position) then BreakObjectsInRadius(molotov, 0.5) is a way to set off a
molotov explosion at a point. Confirmed (code).
Moving into a pane¶
A whole pane is two things to a moving human: its two triangles in the level's collision mesh and its body
(+0xe8), a box in the physics world. The triangles are an ordinary wall; the body is what breaks. Nothing here
depends on the pane's type, its alarm or its window link: every pane gets the same body (0x0038ed68), and the only
type tests in the pane's code are the spawn and hit cases of A pane's life. Confirmed (code) unless marked.
The triangles. Material 2 (GLASS) has no rule of its own in the human's wall sweep (0x00347c08 never reads a
triangle's material) or in its contact handler (Human_OnContact, 0x00219d50, whose level-triangle branch tests
only the fence materials 30, 31 and 122 while climbing, Characters). Both triangles are
two-sided, so while they are enabled a pane stops a human from either side like any other wall face 0.25 m or taller.
The hit disables them (message 1, A pane's life), and the sweep's resolution skips a contact on a disabled
triangle (0x0033d9d8 checks the triangle's enabled bit), so a pane broken during a sweep stops blocking at once.
The body. GlassPane_CreateBody makes a box of half-extents width / 2, 0.25 and height / 2
(0x00342dc0 halves width, 0.5, height), posed by the pane's matrix, so it stands 0.25 m proud of the glass on
each side and covers the whole pane. Its body flags (0x0033d070) are 0x8000007a: bits 0x2, 0x8, 0x10,
0x20 and 0x40. Confirmed (code) at 0x0038ed68, 0x0033d070 and 0x00342dc0. Two paths reach it.
- A strike shape (the jump, a punch or kick). A shape of the human's body is a strike shape while its flag
0x2is set. The human's strike-on and strike-off messages (0x00247fc0/0x00248110, fromHuman_HandleMessage) go throughHuman_SetStrikeShape(0x0021c030) to0x003428d0(confirmed (code)), which keeps a bit mask in body+0xc0(a bit per bone byte+0x31, from bone 2) and sets or clears flag0x2on the shape of that bone (shape vtable+0x1c). In the jump it is the body's own capsule: its flags read0x11on the ground and0x13from the launch to the landing (runtime, below).Human_TestStrikes(0x0033f110) runs once per update, after the move (0x0023fea8calls the move0x0023d8c8, then it), while body+0xc0(or+0xd0) is non-zero. It gathers bodies near the human whose flags hold0x18; for each, a ray from 1 m above the feet to the body's centre must hit nothing in the level mesh, the ray excluding material 2 (its exclusion list is{2, 1}) and triangles with flag0x8(and0x40, unless the target is a human's body, body+0x44bit0x40), so glass never hides a pane from a strike. A body gathered with flag0x8or0x10(mask0x18, so0x20alone is never gathered) that the human may strike (human vtable+0x10c, not traced) is then swept against each strike shape (shape flags0x1and0x2, Physics Unit 10), and the first that hits callsStrike_Contact(0x0021b290). For a pane that is: the noise event (0x002936a8, 30 m),Glass_Break(0x0038f378), a player's crime-10 statistic and then message 1 (A pane's life, steps 5-7). - The airborne body. The move's sweep asks the physics world for bodies whose flags meet a mask the human builds
(
0x0033d498, confirmed (code), for a body with+0x44bit0x40, a human's):0x4on the ground;0x44while airborne (object flag0x4000000) or while record+0x08holds0x800; plus0x20while the sweeping shape is a strike shape and0x40000above gait 3. A pane's body has0x20and0x40but not0x4or0x40000, so only an airborne or striking human's sweep meets it: walking and running on the ground (capsule flags0x11) never touch the body, only the triangles. On the contact,Human_OnContact(object branch) goes on when the body has0x40and the human is airborne (or record+0x080x800, or the landing state flag0x2000000000), or the body has0x20and the touching shape is a strike shape; it callsStrike_Contact(vtable+0x104) once per body (the body's contact list, vtable+0x34/+0x44) and, because the pane's type flags hold0x400(0x0038f588), returns 0: the contact is ignored and the sweep goes on as if the box were not there.
So a pane breaks for a human who is in the air, or whose strike shapes are on, as soon as his body or a strike shape
reaches the box 0.25 m in front of the glass; there is no minimum speed and nothing slows him. On the ground with no
strike, the glass is a wall. Thrown objects use 0x00393538 (A pane's life).
At runtime (confirmed (runtime), PCSX2 2.9.94, a copy of slot 9, 2026-10-07; scenario
glass_jump, stick at 100 % straight ahead, hooks on Strike_Contact
and Glass_Break; their updates re-labelled one update later after the recorder fix, Recording a
trace). Slot 9 stands Rembrandt at (63.6, −1.9, 4.20), heading 88°, on the roof
of level99 checkpoint 3.4, facing the window: two type-11 panes in the plane x = 47.4, side by side (centres y 0.245
and −2.77, a 4 cm mullion between them at y −1.27 to −1.23), from z 4.36 to 7.54.
| Run | What happened |
|---|---|
| triangle at 51.4 m (update 60), launch at 9.5 m/s | the jump clip 434 from update 60; from the launch (update 61) to the landing (update 84) body +0xc0 is non-zero and the capsule's flags are 0x13 (0x11 before and after); on update 72, feet ending at x 48.07 z 5.25, Strike_Contact called from Human_TestStrikes (return 0x0033fddc) for both panes, each then Glass_Break (return 0x0021bb7c); x kept falling by 0.26 m per update (7.8 m/s) through the window, landing at x 44.95 on update 84 (clip 435) and stopping at 44.38 |
| no triangle: he runs off the roof edge at 49.6 m | falling (clip 428), feet at z 3.71 on update 73: Strike_Contact from Human_OnContact (return 0x0021a0e0) for both panes and Glass_Break for each; the panes broke, but his body was below the sill and stopped at x 47.95 against the wall under the window (0.55 m from it) and fell to the street (z 0.22) |
So the original's jump goes through the window on the jump's strike shapes, not on the airborne body path; both
break the pane, and the airborne path alone is enough when the body reaches the box. In level99 this is the only
thing that opens the window: the script's checkpoint 3.4 code breaks no glass (inferred: no BreakGlassInRadius or
pane message in level99_lesson2.lua's P3 functions).
Drawing a pane¶
A pane has no model: it is one textured quad, a sprite of the resource manager's sprite batches (GUI: sprite batches), drawn in the translucent 3D sprite pass after the world. Confirmed (code) unless marked.
Every frame, before the draw (0x0038fbe8 → GlassPane_UpdateBodyByDistance, 0x0038f1c8, for each pane):
+0xd0 = the squared distance from the centre to the nearest player camera (0x00120230); then
- within 50 m (
+0xd0< 2500): a hidden pane (flag 4) is shown again; a shown pane whose colour is still0x808080e0(whole) gets its collision body back if it has none (0x0038ed68: a box of the pane's width and height in the collision world0x00597198, posed by the pane's matrix); - beyond 50 m: a shown pane is hidden; a hidden whole pane loses its body (
0x0038eec0).
So only whole panes within 50 m of a camera have a body. (One game state, 0x0051489c +0x33a = 4 under a mode
check not traced, skips the distance test and keeps every whole pane's body.)
Queueing (GlassManager_QueueDraws, 0x0038fa50, from TaskManager_UpdateManagers, once a frame): for each
pane, GlassPane_QueueDraw (0x0038ef60):
- near,
+0xd0< 12 (within √12 ≈ 3.46 m): the pane goes on the resource manager's near-glass list (0x0018b008,+0xc54) and is drawn on its own after the batches (below); - otherwise: when its batch (
+0xb0high half) is resident and the pane has a body (vtable+0xf4, which returns+0xe8), one sprite is added to the batch (0x00183038): the rectangle'su0, v0, u1, v1from the batch's sheet (0x00181e38, rectangle =+0xb0low half), the colour+0xb4as RenderWare{R, G, B, A}(0x808080e0→ 128, 128, 128, 224, GUI: sprite colours: RenderWare's 0-255, so half brightness, 88 % opaque), and the matrix built from+0x70-+0xaf(0x003368d8).
The quad. The matrix's rows are right = W, up = H, at = the normal, position = the centre, each turned
into RenderWare's axes as (x, z, −y). The near path (Instance_DrawOneSpriteIm3D, 0x001831c0) makes the four
corners as matrix × (±0.5, ±0.5, 0, 1) (the constants at 0x00552850), so in game space the quad is
centre − W/2 − H/2 = corner → (u1, v1)
centre + W/2 − H/2 = cornerV → (u0, v1)
centre − W/2 + H/2 = cornerU → (u1, v0)
centre + W/2 + H/2 → (u0, v0)
drawn as one 4-vertex triangle strip (0x004a4f28 with 4 vertices, 0x004a49e8(4)), every vertex in the pane's
colour. The script's own texture coordinates (+0xb8-+0xc4) take no part. The batched path hands the same matrix,
rectangle and colour to the PTank, which builds its quad itself; that it gives the same corners is inferred.
Render states. Near path, set in 0x001831c0: the batch's texture raster, vertex alpha on, Z test on, Z write
off, source blend source alpha, destination blend inverse source alpha, fog off (RenderWare states 1, 12, 6, 8, 10,
11, 14). Batches (ResourceMgr_Render3DSprites, 0x00185b38, per viewport after the world and before the ground
rings): Z write off, culling off (states 8 and 20), Z test left on; each batch blends source alpha / inverse
source alpha with vertex alpha (0x001972b0 writes 5, 6 and 1 into the PTank's data). Back faces are drawn either
way: no cull state is set on the near path, and the batches turn culling off.
Order. The batches queued this frame are sorted by their key, farthest first (comparator 0x00184850: the
camera distance² − radius²; 0x00197000), and drawn; then 0x0018b058 draws the near list in pane order, unsorted
(after a list of other near sprites, 0x0039b6b8). Panes inside one batch keep the pane manager's order. So glass
is drawn after the opaque world, back to front between batches, not sorted among themselves.
Which sheet. Every pane but type 14 draws from batch 0, one of the thirteen batches the resource manager
makes at start (0x00184918): sheet part_page1 (sheet-table record 1, Particles),
matrix format (format 2), 640 sprites, the 3D sprite pass. The init clears +0xb0 and sets only the low half from
the type's table word (Task_SetRect), so the high half of CfgSetGlassProperties' words is ignored. The disc's
words for types 0-18 are rectangles 20 (whole) / 21 (broken) of part_page1, or 22 / 23 for types 10, 11 and 16;
types 17 and 18 use 21 for both (Glass types). Type 14 makes a one-sprite matrix
batch of its own (PTank_New(max(width, height), 0x20000, centre, …), 0x003e2c84) over sheet-table record 2,
rectangle 0, whose frame is placed at the pane's centre; no level places a type-14 pane.
A broken pane is not drawn. The hit sets the broken rectangle and colour 0xffffff70 (white, alpha 112) but
also removes the body, and both draw paths need the body, while 0x0038f1c8 never gives a broken pane (colour no
longer 0x808080e0) a body again. What the player sees is the shatter's shards and then an empty frame. Confirmed
(code) as a chain of the reads above; not checked at runtime. The broken words matter only for types 17 and 18,
which start with the broken rectangle as their whole sprite, keep the colour 0x808080e0 and so get a body (and
are drawn) within 50 m although their triangles are off. Type 15 (colour 0, made with a body at spawn) is drawn,
invisibly.
The shatter¶
sub_glass (0x003e4cb8) runs once and ends. Confirmed (code):
- Count = 10 × width × height (whole metres from the size word); a pane under 2, or exactly 1 × 1, uses 10
shards of size 0.06 and the sound of materials
GLASS_SMALL×GLASS_SMALL(88); others the sound ofGLASS×GLASS(2). The sound is the material pair's entry in the sound matrix (Sound). - Culling: no shards unless some active camera is within 15 m of the pane's centre
(
Cameras_IsWithinRange(15, p),0x003a5280: the smallest squared distance to any view's camera,Cameras_MinDistanceSq), and the centre is in some player view with a 10 m margin (Cameras_IsPointVisibleAny(10, p),0x003a51f8→Camera_IsPointInPlayerView0x003a50e0: the point fails only when it lies more than 10 m outside one of the view's six frustum planes), and the particle budget (0x003a5a50) allows; game state bit0x20(0x0041cf30) forces them. Both tests are about cameras, never player 1's position: in a scene the scene camera is the view's camera, so panes near the scene camera shatter wherever the player stands (inferred: the scene camera is what the view's camera answers while a scene plays). Confirmed (code) at0x003e4cb8for the arguments 15 and 10. - Shards: for each of the count, two tries, each taken at 2 in 3 (
Random_Int(2) < 2,Random_Int(n)giving 0 to n): aglasstestparticle at a random point within ±4/7 of the width (whole metres) and ±4/7 of the height on the pane's plane (0.5714286), turned by the shatter's rotation. Each gets the shard size (0.2 from a hit, 0.06 for a small pane), a count ofminiglasspieces (2-4; 0-2 when the count is over 40; 0-1 over 79) and the shatter's sprite word, which is 0 (batch 0, the particle default,0x0039aef0). Over 79 the code writes0x4finto the data's size word, but the loop count is already taken, so every shard is still made.
A shard (glasstest, script type 62: GlassTest_Init 0x003e44e0, GlassTest_Update 0x003e4838), confirmed
(code) unless marked:
- Sprite: rectangle 24 +
Random_Int(4), so 24-28, of the batch in the passed word's high half: batch 0,part_page1. - Size: the shard size × a random 0.1-1.5 (
+0xc0). Colour+0xb0: white with alpha 128-224 (Random_Int(96) − 128as0xRRGGBBAA). - Motion: flags
0x14000001(airborne; bit0x10000000cleared under game state bit 1); gravity −9.8 in its data block; a random rotation; an initial velocity with a random 1-2 m/s upward part (the rest, from the shatter's rotation, not traced). - Each update (every 2 ticks): ended at once when it fails the 10 m and 15 m tests or the particle budget (as the
shatter's culling, also when its data word
+0x24, not traced, is over 119), unless game state bit0x20; otherwise a random ±1 m/s per axis is added to its velocity and its second colour+0xb4becomes a random grey 64-255 in all four bytes (a glitter; which colour the particle draw uses is not traced). - Landing: when its flags gain
0x20000000(inferred: set by the particle's ground contact) it makes its count ofminiglassparticles around its position (their random speeds and sizes are not summarised here) and ends. A shard therefore lives from the hit until it lands, with no timer of its own.
How the particle draw blends a shard is on Particles; miniglass is not traced.
Swinging doors: states and commands¶
The state byte (data +0x28), confirmed (code) at 0x003fbba0, 0x003f9770:
| State | Meaning | Next |
|---|---|---|
| 0, 1 | closing; the update adds 1 | at 2 the leaves are reset to the closed pose |
| 2 | closed | |
| 3 | opening (the leaves swinging) | 4 on the next update |
| 4 | collision off (state command 5), then 5 | |
| 5 | open; IsDoorOpen (message 0x0c) answers true only here |
|
| 6 | broken: the update returns 1, so the task ends | |
| 10 | the cabin doors at rest |
The door updates every 28 ticks (0.47 s); opening reschedules it for the next tick, so collision goes about 29 ticks after the swing starts (inferred from the scheduling, Tasks).
State commands (message 0x22, ObjectChangeState; the bindings below send them):
| Command | Effect |
|---|---|
2, 8 (OpenDoor) |
when not pickable: reset the leaves, angle 170°, state 3, the open sound |
3, 7 (CloseDoor) |
angle 0; from state 2 only a reset to state 0, otherwise swing back with the close sound and state 0; then as 6 |
| 6 | triangles enabled, the object's collision body back (0x003a52c8), the number's links get the avoid bit and its hole loses path flag 8 (0x00250d00, A door's hole) |
5 (DisableDoorCollision) |
the number's links lose the avoid bit and its hole gets path flag 8 (0x00250c50), triangles disabled, the collision body removed (0x003a5340) |
10 / 0, 11 (SetDoorPickable) |
pickable on / off (Lock picking) |
Messages, confirmed (code) at 0x003fb8d0:
0x42(DoorOpenDegree): when not pickable, angle = the argument, swing, state 2 → 3, the open sound; when pickable, the angle is only kept at data+0x04.0x0b(DoorOpen(door, human)), only when not pickable and closed (state 2): the leaves reset, the angle is ±170°, away from the human (the sign of the dot product of the door's turned axis with the human-to-door direction), or the kept data+0x04angle after a state 5; state 3, the open sound; pickable cleared.0with a human:Door_OpenAnimated(door, human), the human's object target becomes the door (0x00227080) and its action code 26 (0x1a,0x002266a8). Action 26 plays anim 666ANIM_SPECIAL_OPEN_DOOR, and its event opens the door away from the human. Confirmed (code); the steps are in AI and the world: Doors.0x15(destroy, whatBreakObjectsInRadiussends): flag0x40and update interval 2; the update then raises the interval by one each time and, on reaching 3, sends the door a hit (message 1) from itself.0x19: hitpoints = the argument.0x20: calls each leaf's vtable+0x4c(notdyn_door_stall).0x3e: answers the angle.
Swinging: DoorSwing_SwingTo (0x003f8fd0) sends each leaf message 0x35 with its target rotation: the door's
rotation turned by the angle about the vertical axis (half-angle quaternion), the right leaf by the negated angle
from its turned base. Confirmed (code). How a leaf turns there is under Leaves.
Leaves: model, hinge and swing¶
The frame draws nothing. The dyn_door_swinging object never calls Obj_SetModel (its init leaves the model
word +0xc4 at 0 and DoorSwing_SetUpType only spawns leaves) and its flags 0x405 include 4, hidden; what is
seen of a door is its leaves, and of a type with no leaves (_dblwood, _woodbrd, _woodp, _steel...) nothing
but the level's own geometry around the doorway. Confirmed (code) at 0x003fb5f8.
A leaf (sub_swinging_door, 0x003fb330) is made by Obj_SpawnChildByName(name, position, rotation, door),
which pushes the pose and the door's handle; it is not attached to the door (no flag 0x10). Its init pops the
door's handle (data +0x10), the rotation (into +0x20 and data +0x00) and the position (+0x10), sets flags
0x1021, an update every 28 ticks, and its own type's model (Obj_SetModel(leaf, 0), the dyn_dr_* record's
model, word at +0xc4; the model hash 0xb14109ba also sets flag 0x80). Confirmed (code).
The hinge is the model's origin. A leaf only ever changes its rotation, about the vertical axis through its
position; the position stays where DoorSwing_SetUpType (0x003f80f0) and DoorSwing_ResetLeaves
(0x003f9368) put it:
| Leaf | Position (the hinge) | Rotation when closed |
|---|---|---|
first (left, +0x124 = 2) |
the door's position | the door's rotation |
second (right, +0x124 = 4) |
door position + door rotation × (−2w, 0, 0) | the door's rotation × 180° about z |
where w is the door type's CfgObj argument 15 (f15, type +0x68, read by 0x003a3898 property 5). Confirmed
(code). On the disc w is one leaf's width: dyn_door_store 1.09 with a collision box 2.18 wide (two leaves),
dyn_door_chainlnk_a 2.29 for its one 2.31 m leaf, dyn_door_big_gate 4.43 with an 8.86 m box and 4.43 m leaves.
So the two hinges are a whole doorway apart, the second leaf faces back toward the first, and each leaf's model
extends from its origin along its local −x to about −w, meeting the other at the middle: inferred from the spacing,
from the lock-pick glint at door position + door rotation × (−w, 0, 0) + 1.3 m up (the seam of a two-leaf door, the
latch edge of a one-leaf one, 0x003f9588), and from
the dyn_dr_store collision centre at x = −1.09; the model bounds themselves were not checked. A one-leaf door
(_chainlnk_*, _red_fence, _sheetmtl, _corr, _ornate_single, _stall, _woodfnce_xl) has only the first
leaf, hinged at the door's position.
The swing: a linear slerp over one leaf update (28 ticks, 0.467 s). Confirmed (code):
- Message
0x35(0x003fb430) stores the target in data+0x00-+0x0c, sets data+0x14= 1 and reschedules the leaf for the next tick. - On that update the integrate step runs first (
Task_Integrate,0x003a2310): with flag0x1000it copies the previous target+0x40into the rotation+0x20and stamps the update time+0x50(confirmed (code)). Then the leaf's update (0x003fb5a0) copies the new target into+0x40and clears data+0x14. - Between updates the object's pose "now" (
Obj_GetPoseNow,0x003a1ad8, flag0x1000) isQuat_Slerp(t, +0x20, +0x40)with t = (time since+0x50) / the update interval in seconds (+0x64, 28/60), clamped to 0-1 (confirmed (code)).Quat_Slerp(0x00336a00) takes the shorter arc (it negates the second quaternion's weight when their dot product is negative) and falls back to a plain lerp when they are within 1e-6. - The next update, 28 ticks later, makes the target the rotation, and the leaf rests.
So a leaf turns at a constant angular rate from where it was to the target in 0.467 s: 170° at about 364° per second, no ease-in or ease-out, whatever the angle. A new target that arrives mid-swing starts from the previous target, not from the in-between pose (step 2), so the leaf jumps to the old target first. That the drawn model takes this pose "now" is inferred: it is the only place the slerp happens. The door's own state steps (opening, state 3 → 4 → 5, states) run on the door's 28-tick schedule independently of the leaves.
How a human opens a door¶
- Scripts:
OpenDoor/CloseDoor/DoorOpenDegree(no human),DoorOpen(door, human)(away from the human),OpenDoorAnimated(door, human)(the human's state 26 with the door as target). - Use: a player whose current interaction record (human
+0x660) names the door and has a kind other than 0, 2 or 3 sends the door message 0 on triangle (0x0024d530, the context action of Characters: jumping), so the same animated open. Confirmed (code) for the dispatch; what puts a door in the record is not traced. - The AI: a door's number marks its links (Doors and their numbers); a closed door's links carry
the avoid bit, an open one's do not. AI humans do not open swinging doors by themselves as far as the route
follower shows (its
0x10and0x40handlers jump or charge). Inferred.
Breaking a door¶
A hit is message 1, the same as a pane's: a direction, a point, an int, the hit's kind and two handles (the
attacker). The human path (0x0021b290) sends kind 0 for a plain hit, 2 while the attacker's record +0x08 has any of
0x1400000 (0x1000000: the run attack, the charge and, at runtime, the walk attack 23; 0x400000: the dive) and 3
while airborne (object flag 0x4000000); a thrown object (0x00393538) sends 1 (3 for TYPE_MISSIONTV, 0 for an
object of animation set 5 other than a molotov). Every door and barrier takes 4 + 6 × kind hitpoints: 4, 10, 16
or 22. Confirmed (code) at 0x003f9ad0, 0x003fa438, 0x003b2180; the kinds of the moving attacks confirmed
(runtime) on a fence (Barriers).
dyn_door_store(TYPE_BREAKANDENTER_DOOR, 1 hitpoint): the first hit plays sound0xcf6586b2and theGLASS×GLASSpair, triangles off, and the door sends itself0x0b(the human pushed is inferred to be the attacker): it swings open away from them. Hitting anyTYPE_BREAKANDENTER_DOORalso reports a break-in (crime type 1) at the attacker (0x0021b290). Confirmed (code).- Cabin doors (12): each hit plays
0xcf6586b2; below 7 both leaves are flagged broken and one, at random, takes its next model (message0x19) with a small burst (0x003c6038); at 0 or below: triangles off, two loose pieces (dyn_cabin_aa,_bbor_cc), state 6. Two plain hits break the first leaf, three the door. Confirmed (code). dyn_door_liz(100),dyn_door_dclub(20),dyn_door_stall(10): every hit makes dust (0x003c57d8, radii 2.75 and 3.75) and 20 splinters (0x003c5b00), and plays the type's material againstCONCRETE.lizswaps its model at 80, 60, 40 and 20 % (with material sounds 35, 36, 106 and its own);dclubat 50 %. At 0 or below (stallon its first hit): triangles off, the number's links opened, alpha cleared, more dust and splinters, two wreck pieces (dyn_dre_liz_e/_f,dyn_dre_stall_a/_b,dyn_dre_dclub_c/_d), state 6. Confirmed (code).- Other swinging doors ignore hits.
Breakable barriers¶
dyn_door_fence and the classes beside it are single objects, not leaves: SpawnDoor with the same arguments; the
triangles get two-sided, 0x40 and 0x400, the number's links are retagged 0x40 at spawn, and the update interval is
60 ticks. Hitpoints from the type, written to data +0x04 and object +0x128. A hit (0x003b2180) takes 4 + 6 × kind
and makes dust and 20 splinters (confirmed (code)); one that leaves hitpoints does nothing more. At 0 or below: both
triangle sets off, the number's links opened (0x00250c50), then by class name:
dyn_door_wall_a: a sound, flag0x800000, the damaged model0x7f97c350and its collision body removed (Obj_RemoveBody,0x003a5340; confirmed (code));dyn_door_wall_b: sounds only;- the others (
dyn_door_fence,dyn_door_vargas): more dust (unless bit 2 of game state word 0,0x0041cf30), a sound, and threedyn_wooddmg_a/_bboards (unless bit 2 of word 3), each sent messages0x19and0x30.
Then the type's material pair sounds (unless bit 4 of word 3), and the barrier either marks itself broken (data
+0x00 = 1) and hides (flag 4), or, for dyn_door_vargas, which makes a second object at spawn (data +0x08),
destroys that object (message 0x15), takes model 0x3b4fefad and loses its body. Message 10 sets whether it can be
hit (0x003a17e8), 0x19 its hitpoints. Confirmed (code) at 0x003b2180, 0x003b2f40, 0x003b3158 for
dyn_door_fence; the other five classes are alike by their shared calls, not read in full.
Message 2 is the removal. The class's update (0x003b3220, every 60 ticks) only answers whether data +0x00 is
set. When it is, WorldObject_Update (0x00395b70) calls the object's vtable +0x4c, WorldObject_Remove
(0x00391c10), which sends the object's own handlers message 2 with +0x00 = 0 (a script's callback gets
(self, NilHandle), Scripts), takes it out of its spawn record for good
(0x00398cb0, record bit 0x40000) and deletes it (0x00391b08, which frees its collision body). So a broken
fence's script hears of it on the fence's next update, up to 1 s after the hit, and until then the hidden fence keeps
its body. Nothing in the barrier's hit sends a message itself. Confirmed (code). Every world object removed this way
sends message 2, which is why the script event list reads it as "finished or
broke".
At runtime (confirmed (runtime), PCSX2 2.9.94, slot 10, level99 checkpoint 3.5; scenario
charge_fence): the fence (type index 520, centre (23.6, −6.67))
had 10 hitpoints and broke on one charge, dive or walk-attack hit (kind 2). Its three boards were removed on their own
first update, in the same update as the hit (the pair's second tick; type indices 1368 and 1369, along the fence at x
22.7, 23.7 and 24.6); why is not traced. The fence was removed 3, 10 and 17 updates after the hit in three runs (the
hit re-labelled one update later after the recorder fix, Recording a trace),
and 21.5 and 27.5 updates (43 and 55 ticks) in two more
(fence_dealer, Scripts, item 44):
anywhere from 0 to 29 updates, by the fence's 60-tick phase. In the same update the objective marker went, and 15
updates later P3.DealerPoizo's placement put the player at (18.5, −16.3), beyond the fence. Breaking in by charge:
Combat.
Lock picking¶
The door side, confirmed (code) at 0x003f9588, 0x00397078:
- Pickable on (state command 10,
SetDoorPickable(door, true), and at spawn fordyn_door_chainlnk_pick,dyn_door_storebanddyn_door_templedoor), only while its angle (data+0x00) is under 2: asub_triglintglint at door position + door rotation × (−w, 0, 0) + 1.3 m up (Leaves), drawn while within 30 m (0x003eb0d0); door object+0x124= 10; its collision body removed (0x003a5340). - Pickable off (commands 0, 11;
SetDoorPickable(door, false)): the glint destroyed (message0x15),+0x124= 0, the collision body back (Obj_AddBody,0x003a52c8; confirmed (code)).SetDoorPickable(false)first stops every human whose object target is the door (0x0022e400, state flag0x20000000). - While pickable the door refuses
OpenDoor,DoorOpenandDoorOpenDegree(which keeps its angle for later).
The minigame (CfgSetLockPickHandler's callbacks, pick_lock.anm) is not traced here.
Doors and their numbers¶
SpawnDoor's last value is the door's number in its level (1-27; 484 doors in 45 levels, unique per level). It ties
the door to the navigation links carrying it (Navigation links):
- the breakable doors retag them kind
0x40at spawn (0x00250c00, through0x003a4b30; confirmed (code) at the five initialisers below, which call it, and fromDoorSwing_SetUpType0x003f88c0; which swinging types reach that call is inferred): the classesdyn_door_fence,dyn_door_bar_bani,dyn_door_bnstr,dyn_door_fence_oanddyn_door_parapet(initialisers0x003b2f40,0x003b3250,0x003b4128,0x003b57d0,0x003b6220), and the swinging typesdyn_door_dclubanddyn_door_liz; - opening (state command 5) or breaking clears their avoid bit and sets path flag 8 on the door's hole; closing sets it and clears the flag (A door's hole).
DisableDoorLink / EnableDoorLink do the same by position for the nearest 0x10 link within 5 m and its
reverse. Confirmed (code).
How a closed door stops a human¶
A closed door is two triangles of the level's own collision mesh, not a body. SpawnDoor names them (its fourth
argument, two triangle indices), and the door's init writes their flags in the live level mesh (the world
manager 0x005147c4 → +0x40 level → +4 mesh): DoorSwinging_Init (0x003fb5f8) calls
Collision_SetTriangleTwoSided (0x003a4768, flag 0x2) on both, CollisionMesh_OrTriangleTypeBits
(0x003a4a40) with 0x400 (and 0x800 for the cabin doors), CollisionTri_SetFlag40 (0x003a49e8) with
hitpoints, and CollisionTri_SetMaterial (0x003a4a98). Confirmed (code). Collision_SetTriangleTwoSided has ten
callers, every one an object init that owns triangles: the swinging door, the barrier classes (0x003b2f40,
0x003b3250, 0x003b4128, 0x003b5498, 0x003b57d0, 0x003b6220), dyn_door_barricade (0x003f6018),
dyn_door_sliding (0x003f5d10) and the glass pane (0x003e29e8). Nothing else is needed: the walking sweep
meets them like any wall (Characters, Physics), and it tests no type bit, so
0x400 and 0x40 do not exempt them. A dyn_door_swinging type has shape NONE (the steel doors, the gates) or an
OBB (dyn_door_store, the cabin doors), but no PHYFLAG that a walker's sweep selects, so the triangles are what
stop him. Inferred from the layers (Physics); the per-type flags were not read at runtime.
Why two-sided matters. On the disc a door's triangles are ordinary one-sided triangles: of the 150,567 triangles
of the 64 level files only 4 carry flag 0x2 (Collision). The walking sweep skips a
one-sided wall whose front the sphere is behind (0x00347edc), so before the init a doorway is a wall from one side
only. In level95 the clubhouse's five exits (dyn_door_stlslot numbers 4, 10, 11, 16 and 22) are such pairs; door
10 (triangles 729 and 730, at x −187.63, y 93.99 to 96.16, z −194.31 to −191.99) faces −x, out of the clubhouse, so
from inside the player is behind it. Read from the disc (2026-10-10, level95.lev and the SpawnDoor calls of
level95.lua, numbers only). The init's two-sided bit is what closes the doorway from both sides.
At runtime (confirmed (runtime), PCSX2 2.9.94, 2026-10-11, a level95 state outside the clubhouse with every exit
closed, read over PINE through the world manager's mesh): triangles 729 and 730 carry flags 0xf443 and material
13, where the disc has 0xf001 and material 0. That is enabled, two-sided 0x2, 0x40 and 0x400, with the
disc's bits 12-15 kept, and the type's material. The live mesh (5,943 triangles) has 202 two-sided triangles, alike
in two saved states and live: 703-746 (the doors and barriers, 0xf443 or 0xfc43, 0x800 on 703-714) and
3611-3768 (0xf003, material 2 GLASS: the panes). No triangle was disabled.
When it opens. Only the door's own state machine switches the triangles: state command 5
(DisableDoorCollision, also step 4 of an opening, states) clears their enabled bit, command 6 sets
it; a breakable door or barrier clears it when it breaks (Breaking a door, Barriers).
A script that opens a hangout exit (OpenDoorAnimated in level95.lua's exit handler on the fClubexit_<n> flag)
goes through that same machine. Confirmed (code) for the commands at 0x003fbba0; the exit handler's order of calls is
read from the script's constants, inferred.
Breakable props¶
The street props level34's riot meter counts (Scripts) are world objects (run-time type
0x08; a glass pane is 0x400, Tasks) of two classes. Values from the CfgObj lines of
config_preload3; behaviour confirmed (code) at the cited addresses unless marked.
| Type | Class | Hit points +0x58 |
Hits +0x5a |
Model hash |
|---|---|---|---|---|
dyn_newsstand_a |
dyn_masks |
0 | 1 | 0x18f31e91 |
dyn_newsstand_b |
dyn_masks |
0 | 1 | 0x81fa4f2b |
dyn_crate_stack |
dyn_masks |
0 | 1 | 0x1a0686f7 |
dyn_parkbench_a |
dyn_masks |
10 | 2 | 0x7852cedb |
dyn_trashcan |
overhead_weapon |
150 | 1 | 0xfbf3e3ae |
dyn_gbags |
overhead_weapon |
80 | 1 | 0x62502b03 |
ObjType_GetIntField (0x003a37b8) reads these fields (0: +0x58, 1: +0x5a, 3: the material +0x64).
- Two counters on every world object:
+0x10dand+0x10e, from the type's+0x5aand+0x5b(a 0 becomes0xff, unbreakable; 0 means broken).WorldObject_TakeHit(0x00393450, kind k) takes one from+0x10efirst while the object is airborne (0x4000000) or a held0x3c4e590b; otherwise+0x10dloses 1 when k is −1 or+0x128is 1, else 4 + 6k (floored at 0). - A strike (
Strike_Contact,0x0021b290) on a world object: kind 3 when the attacker has state0x4000000(Combat), 2 with something in hand (held flags0x1400000), else 0. In order: notes whether either counter was already 0,WorldObject_TakeHit, the impact sound, burns the attacker within 1.5 m of a0x1c21bdc3, message 6 to the boxes only if the object was intact before the blow (Scripts),Glass_Breakif it is a pane, a crime report for class code0x1e, then task message 1 to the object with (attacker, attacker, k, 0, contact point+0x90, direction+0x10). dyn_maskskeeps its own data:+0x00broken,+0x04path flag cleared,+0x08update interval,+0x0chit points (+0x58, −1 when 0),+0x10hits (+0x5a, −1 when 0;+0x128= 1 when set and+0x0cis not),+0x14a child.DynMasks_Init(0x003b7538) sets flags0x200001, interval 20, path-polygon flag 8 under it, and copies+0x0cinto+0x10d. Its message 1,DynMasks_OnHit(0x003b7a88): both counters −10 → ignored;+0x0cnot −1 →+0x0c −= 4 + 6k, mirrored to+0x10d; else+0x10loses 1; below 1 → broken (also kind 3 with object-flag bit 4, or model0xf21e6a91with argument −1). Every hit sets object flag0x800000, plays the material sound (survive: the material with 5; break: the material pair) and, when a visibility test passes (40 m and 10 m arguments; inferred to be distance and screen), dust; then a per-model effect. No velocity is given to the prop itself.- What each breaks into: the newsstands a sound and five paper debris particles, no piece; the crate stack a sound
and one piece object at −0.604 m, turned at random; the bench 25 splinters, a dust cloud and one piece at −0.395 m.
The trash can and the bags have no case. A broken prop then looks for a damaged model (
Model_FindByHash): if one exists it takes it, re-reads both counters from the new type and keeps its body; if not, it loses its collision body (Obj_RemoveBody), the low byte of+0xccis cleared and path flag 8 under it is cleared once. - Removal:
DynMasks_Update(0x003bf548) answers "done" while+0x00is set, soWorldObject_Updateremoves it on its next update (up to 20 ticks): message 2, spawn record bit0x40000, never spawned again (Barriers has the path). Messages 4 and0x15set flag0x40instead; the update then counts data+0x08up, using it as its interval, and when it reaches 4 sends the prop a kind-0 hit of its own (message 1, from itself; where the count restarts is not traced). Message0x30knocks a piece: velocity a random 4-6 × its horizontal part (vtable+0x74), a spin (+0x7c), flags|= 0x4200000(airborne; Physics flies it). - Counting (inferred from the code above): a newsstand or crate stack breaks on the first blow of any kind, so it
sends message 6 once. The bench takes three bare-handed blows (10 → 6 → 2 → broken) or one armed blow, and each blow
while intact sends message 6. A strike on a trash can or bags takes
+0x10dfrom 1 to 0, so only the first counts. overhead_weaponobjects are the throwables:OverheadWeapon_ContactDamage(0x003932c8) takes one point (WorldObject_TakeHit(-1)) per damaging contact while flying or held. Message 1 breaks one at once, whatever its counters say: Trash cans and bags.F.Vandalize(level34.lua) tells the newsstand apart by name, not by type bits:GetRTTI8 andGetObjectNamedyn_newsstand_aor_bgive 3 points, other 8 give 1, 1024 (a pane) gives 2.
The riot props' hit effects¶
What DynMasks_OnHit (0x003b7a88) does for the four dyn_masks riot props above. Confirmed (code) at that
address; the materials, piece names and next models are confirmed (runtime), read from level99's object database
and Object List (the hashes are the CRC-32s of the names in the table above):
| Prop | Material (+0x64) |
Next model | Piece on a break |
|---|---|---|---|
dyn_newsstand_a |
WOODFURNITURE (47) |
none | none (paper debris particles only) |
dyn_newsstand_b |
MEDWOOD (56) |
none | none (paper debris particles only) |
dyn_crate_stack |
CHAIR (86) |
none | one dyn_wooddmg_a (material NONE) at (0, 0, −0.604) m from the prop, turned about z by a random angle (quaternion (0, 0, sin θ, cos θ), θ in 0-π) |
dyn_parkbench_a |
WOODFURNITURE (47) |
none | one dyn_parkbench_aa (SMALLBOARD) at (0, 0, −0.395) m in the prop's own pose, after 25 splinters |
- Sound: unless game-state set 3 has bit
0x4, a hit that leaves the prop standing plays the material pair (prop material,CONCRETE5) at the hit point, and a break plays (prop material, prop material); one other prop (model0xe42da444) breaks with (its material,TINBOX23).Sound_PlayMaterialPairAt. - Dust: on every hit, standing or broken, when the particle pool has room and the hit point is within 40 m of a
camera and visible from one (
Cameras_IsWithinRange40,Cameras_IsPointVisibleAny10), two dust bursts at the hit point with radii 3.75 m and 2.75 m, colour0x8b7d6964, unless game-state set 0 has bit0x2. The per-prop effects come after them. Out of view, a hit that leaves the prop standing stops there; a break still swaps or removes the model. - Damaged model: none of the four has one (their Object List records'
+0x04is 0; onlydyn_trashcannames one,dyn_trashcan_b, which its own class spawns as a piece instead, below). So each broken riot prop loses its collision body and is removed on its next update, as above; the pieces fly as knocked pieces.
Trash cans and bags (overhead_weapon props)¶
level99's street trash is placed by ObjSpawn like any dynamic object: checkpoint 2 has 11
dyn_trashcan, 18 dyn_gbags, 2 dyn_parktrash_a and 5 dyn_pstack, all of class overhead_weapon (Script
types). Each is a solid box that stops a walker, a weapon the player can pick up and
throw (Pickable objects, Throws), and a prop that any strike breaks at once.
Collision layers (CfgObj argument 12, +0x5e, as PHYFLAG bits). Confirmed (runtime): read
from the object database (*0x00512c04 + index × 0x90) and from the live objects' bodies (object +0x118, body
+0x40) in quick-save slot 8 (level99, PCSX2 2.9.94):
| Type | +0x5e |
PHYFLAG bits |
Body flags | Walker | Strikes, charge, jump | Sprint (gait > 3) |
|---|---|---|---|---|---|---|
dyn_trashcan |
0xbf |
BLOCKOBJECTS, BLOCKHUMANS, MELEETARGET, WEAPONTARGET, JUMPTARGET, CHARGETARGET, COMBATPASSTHROUGH |
0x1057e |
blocked (slides) | struck | blocked |
dyn_gbags |
0x2bf |
as the trash can, plus RUNTARGET |
0x5057e |
blocked (slides) | struck | struck, then blocked |
dyn_parktrash_a |
0x3f |
as the trash can, without COMBATPASSTHROUGH |
0x57e |
blocked (slides) | struck | blocked |
dyn_pstack |
0x2bd |
as the bags, without BLOCKHUMANS |
0x5057a (inferred) |
passes through | struck | struck, passes through |
The riot props dyn_crate_stack, dyn_newsstand_a and dyn_parkbench_a (above) have 0xbf too.
The world-object body base is 0x500 (Physics); COMBATPASSTHROUGH is body bit 0x10000,
RUNTARGET 0x40000. Game-state set 3 (byte +0x3eb of the game state *0x0051489c, read by GameState_TestFlag
0x0041cf30) changes all four types: bit 0x8 clears body flag 0x4 at init, so walkers pass through; bit 0x2
spawns no pieces; bit 0x4 silences the break; bit 0x1 keeps them off the path polygons (below). In slot 8 the byte
was 0 (confirmed (runtime)); what sets it is not traced.
Walking into one (Human_OnContact, 0x00219d50, confirmed (code)): a world object that is not a powerup_item
answers 0x20001, the slide response (Physics: contacts): the walker loses the part of his
velocity that goes into the box and keeps the part along it. Nothing pushes, tips or moves the object: its own contact
handler is not run by the human's sweep, and no velocity is given to it. Two cases come first: a body with RUNTARGET
met by a human above gait 3 sets human +0x368 = 1 and is struck (Strike_Contact through his vtable +0x104,
once per body: it then joins his body's ignore list); a body with JUMPTARGET is struck the
same way while he is airborne (0x4000000, or record bit 0x800, or Human_IsTurnLimited6), and one with
CHARGETARGET when his contact shape has flag 2 (charging). After such a strike a body without 0x4 answers 0x20000
(passed through); one with 0x4 still slides. Confirmed (runtime), slot 8 (props_trashcan): the player put 1.6 m
south of the lone trash can at (47.71, 2.28) and walked at it (stick 60 %, gait 2) stopped with his root at y 1.5645,
0.72 m from the can's centre, and edged sideways 0.1 m along it over about 30 contacts while the can stayed at (47.705,
2.284, 0.703). Sprinting (L2, stick 100 %, gait 5, props_gbags_run) into the bags at (57.39, 13.72), his first contact
struck them (Strike_Contact from 0x0021a0e0), they broke, and he stopped dead 0.8 m short of their centre on the
next update; walking on into the trash can beside them (gait 2-3) only slid.
Why the sprinter stops. The bags' body has 0x4, so after the strike the same contact still answers the slide
0x20001; met head-on, the slide removes all of his velocity, and the blocked-sweep rule
(Physics: contacts) zeroes what is left horizontally, so his speed +0x1ac and gait fall to 0
and the move set drops him to idle (confirmed (code) in Human_OnContact, 0x00219d50). Confirmed (runtime),
props_gbags_run with L2 and the stick held at 100 % throughout: struck at step 35; steps 36-40 the idle clip 388 at
speed 0; step 41 the run start clip 414 at gait 2-3, building up again from there. +0x368 is read only by
OverheadWeapon_Break (the 0.65 volume), not by the movement; what holds him idle for those 5 updates is not traced.
A strike (any of the player's or an AI's strike shapes that meet a MELEETARGET body,
Combat; square at a trash can plays 661 SPECIAL_BREAK_OBJECT_LOW) reaches the object through
Strike_Contact (0x0021b290, above): the hit counter +0x10d goes 1 → 0, the impact sound
(Human_PlayImpactSound with 9, or 26 armed, and the object's material), a noise report (30 m), message 6 to the volume
boxes (the object was intact), stats crime 9 when the hit left a counter at 0 and the object has no flag 0x20, and
message 1 with (attacker, attacker, kind, 0, contact point, contact +0x10). OverheadWeapon_OnMessage
(0x00402bf8) calls OverheadWeapon_Break (0x003ffe90) for message 1 unconditionally: no hit points are read, so
one blow of any kind, a thrown object's hit or a sprint into the bags breaks it. Confirmed (code); at runtime
(props_trashcan, square at the can from 0.72 m) the strike landed 9 updates into clip 661 and the can broke on that
update.
A thrown can or bags (or one swung into something while held) breaks on its first contact of any kind: the
overhead_weapon class wears on every contact and their hits are 1 (Wear on contact). The break is the same
OverheadWeapon_Break as a strike's, through WorldObject_Break's message 1: the hit point is the contact point,
the direction d the contact normal (out of the surface), kind −1 (only −2 is tested, so −1 acts as a strike), and
+0xb0 holds this contact's vector, so c is the normal too (the pieces and litter go up off a floor, the splat probe
away from the surface). Two things differ because the object is flying (0x4000000): before the effects its position
is moved 1 m along c (the unit contact vector added; the pieces and the bottle start from there), and the break's
own material pair is skipped; the sound is WorldObject_OnImpact's on the floor or a wall, or the human or object
handler's. A human it hits takes the contact damage (ThrownObject_HitHuman) before the break, on the same
contact. Confirmed (code) at 0x003932c8, 0x00394050, 0x00393e20 and 0x003ffe90; not watched at runtime.
The break (OverheadWeapon_Break, confirmed (code) unless marked):
- Pickable flag
0x100000cleared; path-polygon flag 8 under it cleared once (data+0x0c, set when the init left none); the collision body removed (Obj_RemoveBody); unless the fourth argument is −2, the state (data+0x04) becomes −5 and+0x10d0. - Only when the particle pool has room and the object is within 60 m of a camera and visible from one
(
Cameras_IsPointVisibleAny10): the tint's alpha becomes 0 (+0xcc=+0xc8&0xffffff00, so it vanishes on the next update, no fade), flag0x200000,+0xd4= 0.1; two dust bursts 0.5 m above the hit point (radii 3.75 m and 2.75 m, colour0x8b7d6994); then the per-model effects (table); then, unless the object is airborne, its material differs from the valueScriptObj_GetUserValuereturns and set 3 has no bit0x4, the material pair (material, material) at the hit point at volume 1.0, or 0.65 when the attacker's+0x368is set (he broke it by running into it). - Rescheduled with interval 1, so
OverheadWeapon_Update(0x00402e70) runs next tick, sees state −5 and returns "done":WorldObject_Updateremoves it (WorldObject_Remove, message 2, spawn record bit0x40000, never spawned again). Out of view the object is not hidden in step 2 but is removed the same way.
At runtime both breaks hid the object (tint +0xc8 0xffffffff → 0xffffff00, flags 0x8022a001 → 0x80228001) on
the update after the strike and removed it on the next tick (WorldObject_Remove from 0x00395cb0, just after a
message 0x20, which the class ignores). Confirmed (runtime).
| Model | Effects | Pieces (ScriptObj_SpawnThrownDebris, 0x003a5530) |
|---|---|---|
dyn_trashcan (0xfbf3e3ae) |
the "trash" set below | dyn_trashcan_b (the dented can, fade_object) at the can's own pose, then message 0x15 (fade soon); plus the set's pieces |
dyn_gbags (0x62502b03) |
the "trash" set | the set's pieces only |
dyn_parktrash_a (0xb0c69542) |
the "trash" set | dyn_parktrash_aa (fade_object) at its pose, message 0x15; plus the set's pieces |
dyn_pstack (0xfbd21393) |
the "cardboard" set (as dyn_cbox): litter kind 1 and 8 brown (0x4e4338ff) sub_debris pieces of 0.35-0.55 |
none |
- The "trash" set (mode 1 of the break,
OverheadWeapon_Break0x003ffe90, for models0x57f87129,0x3792e23dand0x7f48b418; confirmed (code)), each with the hit point P, the message's direction d (normalised) and the contact vector c =+0xb0normalised (zero for a prop never touched, below):- litter:
Garbage_Throw(litter, 0, P, c)(0x00170b28): 5-9 pieces of the level's litter, when it has any waiting to respawn; - 10 black splinters:
Particles_Splinters(2π, 1.0, 0.16, 1.2, P + (0, 0, 0.5), 1.5 c, 10, 0x101010ff, 0)(0x003c5b00), asub_wood_splinter(Script types: wood splinters): sprite bits of 0.08-0.16 m within ±1.2 m of the point 0.5 m above P, at 1.5 c × 0.5-1.5 × 5-9 m/s (so none for a standing prop), spinning, that fall, lie flat and last 2 s; - a brown splat:
Spawn_SubPaintSplat(0.75-1.0, P, d, 0x55423860)(0x003c5e50), an invisible probe sent at 8 m/s along d that leaves apaint_splatdecal where it touches a surface within 15 ticks (Script types: paint splats); the colour's alpha0x60is below0x80, so that decal is removed on its first update (2 ticks): the trash's splat never stays on screen (confirmed (code) at0x003ec3a0); - a burst (
Effects_SpawnBurst) at the object's position; - a
dyn_beerbottle(thrown_weapon) placed 0.5 m above the path polygon under the hit point, when there is one, in the object's rotation, and knocked (message0x30) with no velocity and a spin of (0, a, b), a and b random in ±π; - four
fade_objectlitter pieces,dyn_trashbit_a,dyn_trashbit_band twodyn_trashbit_d, each at a random offset from the object (the arguments ±0.43, ±0.43, 0.83 ofRandom_ScaledVector; their exact use is inferred).
- litter:
The two hobo variants (dyn_hobo_trashcan, dyn_hobo_gbags) drop a random hobo food instead of the bottle.
- The default set (a model with no case): 20 light-wood (0xbea780ff) and 24 dark (0x5c3f27ff) splinters.
- A piece's flight (ScriptObj_SpawnThrownDebris): created by type name at the object's position plus the offset
turned by its rotation, in the given rotation (here identity), with the object as parent, then message 0x30 with a
spin of ±3π rad/s on each axis and a velocity of the normalised object vector +0xb0 × 1-1.5. A fade_object takes
that knock as a horizontal speed of 2-5 m/s each axis, z 0, and flies airborne (FadeObject_Launch, 0x003c6ae0).
Nothing is spawned while set 3 has bit 0x2.
- +0xb0 is the object's last contact vector (ScriptObj_GetContactVector, 0x003a4bd0, reads it): the contact
normal × the speed into the surface × dt, written by WorldObject_OnContact (0x00394050) on each contact. A prop
that has never touched anything holds (0, 0, 0): confirmed (runtime), PCSX2 2.9.94, quick-save slot 8 read from
the state, the bags at 0x00ef3780 and the trash can at 0x00ef2600. Normalising a zero vector gives zero on the
PS2's VU (its reciprocal square root of 0 is the largest float, and 0 × that is 0), so the pieces of a struck
standing prop get no velocity and drop straight from their offsets, spinning (inferred). A thrown prop that
breaks on a contact gives them that contact's normal (out of the surface), 1-1.5 m/s before the fade_object's
2-5 × scaling.
- The pieces' end (Script types: fade_object, confirmed (code)): a launched piece
updates at 30 Hz; it is removed on any update where its position is more than 30 m outside every view's frustum;
message 0x15 sets the age to 3400, so the dented can starts to fade 151 updates (5.0 s) after the break and is gone
51 updates (1.7 s) later; the litter, without 0x15, starts to fade after 3551 updates (118 s).
At runtime (props_trashcan) the can's break spawned dyn_trashcan_b (offset and rotation the identity) and
dyn_trashcan / dyn_gbags breaks each spawned dyn_trashbit_a, _b, _d, _d, in that order, in the strike's
update. Confirmed (runtime); the bottle's path was not hooked.
Path polygons. OverheadWeapon_Init (0x003ff6c8) sets path-polygon flag 8 at the object's point
(PathPolygon_SetFlag8AtPoint, the flag of door holes) for every model but 18 small ones (dyn_gascan,
dyn_cbox, dyn_tire, dyn_crate_c, dyn_crate_e, dyn_tvsmall, dyn_tv, dyn_box_stereo, dyn_ricebag_b,
dyn_keyboard, dyn_drumkit_a, dyn_drumkit_e, dyn_paintcan, dyn_detergent, dyn_cinderblok, dyn_bagcash,
dyn_bagdrug and one unnamed hash 0xe910f959), or for none while set 3 has bit 0x1; the break or the next update
clears it. So the trash props mark the navigation mesh while they stand. Confirmed (code); what flag 8 means to the AI
here is on AI.
Trains (moving hazards)¶
A train is a line of up to 12 objects (a subway train's cars) that AI humans step out of the way of; the scripts
build one with ObjSetTrainPoint and run it with
ObjStartTrain / ObjStopTrain. There are four records of 0x150 bytes at 0x006f3a10, built at boot
(0x00414220, from the static initialiser stub 0x00414280) and stopped by InitLevel and UnloadLevel
(0x004136f0); the task manager updates all four each frame (0x00413728). Confirmed (code).
| Offset | Field | Evidence |
|---|---|---|
+0x00 |
12 point positions (0x10 each), re-read from the objects every update | confirmed (code) |
+0xc0 |
a box around the points, grown by the radius | confirmed (code) |
+0xe0 |
12 object pointers (Train_SetPoint, 0x00413770 → TrainRecord_SetPoint, 0x004138a8) |
confirmed (code) |
+0x110 |
12 gap flags: non-zero means no segment starts at that point | confirmed (code) |
+0x140 |
point count (the highest index set, plus one) | confirmed (code) |
+0x144 |
the radius in metres (ObjStartTrain's first argument) |
confirmed (code) |
+0x148 |
running (Train_Start, 0x004137d8, after TrainRecord_Reset, 0x00413890; Train_Stop, 0x00413828) |
confirmed (code) |
+0x149 |
the last point index set with gap 0 | confirmed (code) |
TrainRecord_Update (0x00413d90), while running: up to 60 humans in the box (0x002274a8); each one with an AI
brain that is not in state 1 and not cuffed, and within the radius of a segment, gets the time stamped in its brain
(+0x2e0); if its brain has no goal of type 7 it drops a type-0xf goal on top and pushes the dodge goal
(0x002f9358, given the record and the nearest point). The dodge goal uses 0x004138f0 (still within the radius of
a segment, and whether it is more than 0.5 m inside and within 10 m of the front point) and 0x00413a90 (the
point's projection on the first segment long enough to hold it, and that segment's two indices: it walks the
segments from point 0, measures the point along each one's direction, gives up (0) as soon as the point lies before
a segment's start, and takes the first segment whose length is greater; it ignores the gap flags and never tries the
last segment, so with fewer than three points it finds nothing; the dodge goal's use:
RunFromTrain). Trains_IsPathClear
(0x00414188) asks each running train (TrainRecord_IsPathClear, 0x00413c48) whether a move from one point to
another passes within its radius of a segment; AI movement checks it. The record's constructor is
0x00413848. Confirmed (code).
Game-state functions¶
Functions of the game-state module (0x00417af0-0x00424e50, a range inferred from where its functions lie: inventory,
statistics, flags, configuration workers) that belong to this page, by address.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0041d4f8 |
Cfg_SetGlobalTimeToLive |
CfgSetGlobalTimeToLive: +0x26c |
confirmed (code) |
Coney's implementation¶
Object types and spawn records (src/world_objects/object_types.h, src/world_objects/spawn_records.h,
the bindings in src/scripting/spawn_bindings.h): CfgObj adds a type (name, class, hitpoints and the model hash, the
CRC-32 of the name) and still records its arguments for what reads the rest; ObjSpawn adds a spawn record (type,
position, rotation, zone, flags, tint, flag name) and returns its handle; CfgSetDatabaseSizes makes the pool (its
object count plus 500). Resolving a handle marks its record live and pinning keeps it. A fresh script state starts
with no records but keeps the types, which the legal screen's preloads configure once
(Scripting).
Coney's choices: a record's handle comes from the counter every world object's handle comes from, not the record
index << 16 (record 0 would be NilHandle); the rotation is kept as given rather than packed into s16 × 4,096; a
record a binding resolved is "live"; and the keys and the power cuffs are always
suppressed, as a fresh profile has neither unlockable. ObjEnableZone sets or clears a zone's bit of the records'
zone mask (zone 0 on at a level's start, the rest off); ObjShow and ObjHide resolve the handle and mark the record
shown or hidden and keep the show message (ObjShow's distance kept); ObjDestroy makes a human holding the object
let go, then removes the record for good, except that with its second argument an objective marker is marked dying and
fades out first (src/scripting/world_bindings.h). Disc check (NTSC-U,
2026-10-06, counts only): at the front end
level100.lua leaves 29 Wonder Wheel records, each of a configured type, the wheel tinted 0x474542FF.
Placed objects (src/world_objects/placed_objects_file.h, from the objects file): after the level
script's main chunk, runLevelScript reads <level>_objs.txt through the script source and adds a spawn record per
line, each with a handle from the world objects' counter, and makes a particle system of each part line's name at
its pose there and then, with no handle and its other fields unused, whatever its zone. A missing file adds nothing.
Disc check (NTSC-U, 2026-10-07, counts only), records and emitters of the lines: level99 98 and 14 of 112, level80
212 and 30 of 242, level87 704 and 16 of 720, level2 468 and 30 of 498, level3 128 and 0 of 128, level5 615
and 31 of 646, level34 380 and 7 of 387, level95 258 and 11 of 269; so every line is placed. Of the emitters,
only the steam vents draw yet (the flies, neon, alarm strobes, air conditioner, dust and sparks make inert systems).
Pickable objects and objects in a scene (src/world_objects/pickups.h, src/gamemodes/level_pickups.h): triangle
takes the pickable classes (world_objects::pickable(), the model-hash exceptions included); CfgObj's
20th argument is kept as the type's anim set (+0x87; inferred from the field order and a bat's 3). A scene's moves
of a bound object (position and rotation) go to its spawn record (SceneStage::setObjectMover): the record's pose
stands for the object's, so the bats l99_c8 lays down are where the pick-up finds them. Coney's reading; the original
writes the record only when the object is stored.
Models (src/platform/object_models.h, src/platform/placed_objects.h): an object's model is its type's Object
List record (ObjectList::findByHash, the type's model hash), its 0x47 model read as the level file's and its
dictionary's first texture on the first material, loaded once per type and shared. An object is drawn at its pose
with the model's vertices taken in the game's axes (z up) and carried into RenderWare's as positions are, (x, y, z) →
(x, z, −y); the model's own atomic frame is left out. Coney's reading: the frames are either the identity or the
turn of z up into RenderWare's y up, some with a stray offset (the column's 2.69 m, the Wonder Wheel neons' 35 m), and
only without the frame does the objective disc (an identity frame, its centre 1.4 m up) stand upright on its column
as the runtime sees it. The draw (PlacedObjects::draw) follows The tint: the tint word multiplies the
materials' colours and alpha, then the size cull (the model's bounding sphere), the ObjShow distance and the alpha
floor of 10; opaque objects first, then the translucent ones. Not modelled: the camera fade and the second dictionary.
The front end draws the live records' objects this way, without a camera (no cull), each lit as a world object
(Front end); the eight Wonder Wheel types' frames are all the z-up turn, so
leaving the frame out draws them as turning by it would.
World objects in play (repo:src/world_objects/object_tasks.h, repo:src/platform/play_level_world.cpp): each
step, after the scripts and the scenes, ObjectTasks brings records in as streaming does (zone enabled,
within 70 m of the camera), keeps them until they are beyond the draw distance + 10 m, and makes each object's draw;
the play mode draws them between the s and d worlds with the objects' lights. Objective markers follow
the page: the disc turns 90° a second about z; the alpha steps 8 an update towards 255 or 0 by
the last show message and is drawn one update late; the disc is never size-culled; the column (model 0x1f3b85ea)
stands at the disc's position turning back, coloured by the disc's type; a dying marker is removed at alpha 0, and
ObjDestroy without its second argument removes it at once. Held objects (In a human's hand): player 1's
object hangs from the bone of the first take event of its type's low pick-up clip, at the event's position × his scale
slid along the object's y by CfgObj +0x70 (ObjectType::grip), composed with this frame's bone and his
placement. Disc check (NTSC-U, counts only): coney_tests "[disc][objects]" at level99 checkpoint 1 after the
intro: one marker disc and one gold column at alpha 255; the three bats, laid on the ground as l99_c8 leaves them,
drawn there.
Coney's stand-ins for the world objects, where this page is silent:
- The column's blend: alpha blended over what is drawn, Z tested without Z writes, after the opaque objects.
- A new marker starts at alpha 0. Every record within reach comes in on the same step; a record a binding resolved
or a scene pinned is drawn whatever its distance and zone.
ObjHidehides a plain object (simple_object's0x0ais not a hide). The fade-in over the first second is linear. - A held object: the bone's position is not scaled by the human's scale (Coney draws the body unscaled), the lean is left out, and only player 1's object is drawn in a hand. A dropped or thrown object falls from the hand (Physics).
- Throws (
repo:src/combat/throw_velocity.h,combat::throwAttack, the play mode'sdropHeld): square, cross, the charge or the dive with a set 4-6 object plays the throw clip by gait, its type-10 event lets go, and the object leaves the hand's pose withHuman_ComputeThrowVelocity's velocity turned by the thrower's heading and the detach's spin. Stand-ins: the target is the fighter's own (noPlayer_PickThrowTarget, no turn to face it), aimed at a fixed head height and from the object's place rather than the hand bone; set 5's smash, hits on humans, a held object's wear, the removal time and the AI's throws are not built. - The aiming state (above;
repo:src/combat/throw_aim.h, the play mode'sstepThrowAim): L1 with a set 5 object and no lock enters it (466 into 469; the stick stops walking him), the raw left stick turns him (470 while it does) and pitches the aim, the first frame takes the follow camera's heading, and each frame the arc's 18 points are traced in the two passes, a human target faced for 5 frames and while the stick rests. What the arc meets is kept forHuIsAimingAt,HuGetControlNamegivesthrowControl, and a throw from the aim leaves from the release point with the aim's velocity. Stand-ins: the sweeps are sampled every 0.1 m along a segment (notIPhysics_CollideShape), humans are 2 m upright capsules, cars are not met, a target's point is a fixed head height, and the lock-on's friendly case is not modelled (Coney's lock never takes a friend). The arc is drawn (Graphics). - Wear and breaks in flight (above;
world_objects::LooseObjects,world_objects::Props::breakFlying, the play mode'sflyingBreak): a throw sets the flying object's body flag 1 and keeps its thrower. Each contact with the level mesh first takes one hit (WorldObject_TakeHit, the second counter first) as the table above says: anoverhead_weaponor aGUITARobject on every contact, thrown or dropped; any other on a throw's first contact only, the flag then cleared. The contact that leaves a counter at 0 breaks it: no bounce, the material pairs (its, its) at 1.0 and (its, the surface's) at 0.5 / (n + 1), message 6 to the boxes its thrower stands in with the object where its move left it, then its class's message 1: athrown_weapongoes to the hit point with dust of radius 1.6 (adyn_beerbottleadds 4-8 brown glass pieces) and is removed at the next step; anoverhead_weaponbreaks from 1 m along the unit contact vector without its own pair, its pieces launched along that vector; a lifted cash register leaves itsdyn_cashreg_claunched along the contact normal. The counters an object wore stay with it between flights (a register spends one per throw). Stand-ins: the flight meets the level mesh only (no body, so no human or car contact); the glass pieces are shards that do not fly, and the other models' rubble, glass and bursts are not made; the pairs' fallbacks (116, then 5) andCHAIR's 0.1 straight down are not built; another class's message 1 is not built, so such an object lies where it broke; the impact countnis counted per flight; and a held object's wear and the test against a world object already spent are not built. - Impact sounds and the pieces' flight (Sound events,
Script types:
fade_object; the play mode'simpactSound): every loose object's level contact that does not break it plays its material against the surface's at 1 / (n + 1), for its first three impacts above 2 m/s. A broken trash prop'sfade_objectpieces (the dented can, the park bin's piece, the litter) and a register's piece are launched as loose objects: velocity the unit contact vector's x and y each × 2-5, z 0, spin ±3π rad/s on each axis (none for the register's piece), so a struck standing prop's pieces drop straight down; on the update after a level contact a piece stops dead and falls from rest. Their landings sound through the same impacts, by each piece's type material against the surface's, through the sound bank's material-pair table. Which samples those pairs pick for the pieces (garbpart_01-06,debris_part_01-03,bottledrop1/2,woodsplinter_01,smallboardpart_01/02,wood_drops_01/02,chairli8,chairy3,deep_debris_01) is not on these pages yet (open question). Stand-ins: the speed tested against 2 m/s is the object's speed as it meets the surface (the research does not say which); the pieces never fade (message0x15, the age and the view test are not built), and the register's piece spills no money. - Weapon piles (above;
world_objects::pileTake,world_objects::Props::takeFromPile, the play mode'stakeFromPile): adyn_pileis pickable; player 1's pick-up of one makes the object its type hands out at the pile, plays the take cue there as a 3D sound, and puts the new object in his hand while the pile stays.dyn_molotovpile_bruns out after 5 takes (no longer pickable) and goes two 240-tick updates later. Stand-ins: the spray-can box and the donut spawner hand out nothing; message0x22, a carried or knocked pile, and the AI's pile goals (ManWeaponPile, ThrowSubTactic) are not built.
Glass and doors, written from this page and Crimes (2026-10-06), in repo:src/world_objects/:
LevelObjects(level_objects.h) holds a level's panes and doors and the world they change (ObjectWorld: the collision mesh's triangles, the path data's links, andObjectServicesfor sounds, shards, crimes, flags, statistics, bodies, loose objects and models). Play mode ticks it at 60 Hz and sends hits tohumanHit/thrownHit, finding the object by the struck triangle (objectOfTriangle);humanHitKind/thrownHitKindgive the hit's kind.- Panes (
glass.h): the type table, the spawn (geometry,GLASStwo-sided triangles, sprites, types 14, 15, 17, 18, alarm bits, window link), message 1 and 0,Glass_BreakandBreakGlassInRadius; the shatter's count, sound pair and shards drawn from the game's random numbers. - Doors (
doors.h): the per-type set-up, leaves, triangle flags, the state machine and its timing in ticks, the state commands,DoorOpenaway from the human,DoorOpenDegree, pickable doors, message0x15, the breakable types' hits and the barriers. Links (nav_links.h): retag, open and close by number (the door's hole found as A door's hole gives,NavLinks::holeOf(),world::PathMap::holeAt()), nearest by position. The edge's door number is decoded intoworld::PathEdge::door. - Bindings (
repo:src/scripting/object_bindings.h): the 22 glass, door, link and lock-pick bindings;SpawnDoorreads its type from the recordedCfgObjcalls. - In play (
repo:src/gamemodes/gameplay_mode.h,repo:src/platform/play_level_objects.cpp): gameplay owns the level'sLevelObjects; the boot scripts' recordedCfgSetGlassPropertiescalls are applied before the level script spawns into them. The play mode gives them the level's collision mesh and path data, ticks them twice a step, and sends each human's strike-shape contacts to the pane or door the shapes meet (Combat). Player 1's object attack (661 / 662) lands when those shapes reach its pane or door, or, for a car or other object, when its strike shapes come on; never at the clip's start. Stand-ins: a pane or door the window passed without touching takes the hit as the window closes, and a clip with no strike events lands it as it ends. A broken barrier is removed at its next 60-tick update: its handlers get message 2 (itself,NilHandle) and its spawn record goes for good; the three boards it throws are made at the hit and removed on the next tick (the hit's own update, as in the original), so no board stands in the gap while the barrier waits for its removal (issue #233; before this they stood there until the dealer cut, which a player saw as the broken fence staying on screen). It draws each swinging door's leaves (hinged at their positions,wthe type'sCfgObjargument 15, swinging by the 28-tick slerp of Leaves) and each barrier as its type's model (gone once broken;world_objects::doorDraws()) with the world objects, and the panes as Drawing a pane gives (world_objects::glassDraws(), drawn frompart_page1after the humans and before the rings). Their lock-pick callbacks call the scripts, a break-in moves theCrimeSceneflag, and their sounds go torepo:src/audio/object_sounds.h, which plays a name hash through theSoundPlayer. - Disc check (NTSC-U, counts only):
coney_tests "[disc][objects]":level2places its 26 doors (14 swinging, 12 barriers, 21 leaves), every type configured by aCfgObj, and 25 panes; 19 glass types are set.coney --play-level level2hands all of them to the play mode; three plain punches (4 damage each) break a 10-hitpointdyn_door_fencebarrier.
The spawn's triangle writes reach the mesh bound later (fixed for checklist item 30; found 2026-10-10).
GameplayMode runs the level script, and so every SpawnDoor and SpawnBreakableGlass, before the play mode has
loaded the level's mesh (coney --play-level level95 prints the script's player start, then level95.lev: 5943
collision triangles, then objects: 79 glass panes, 22 doors and barriers). Until the fix, markTriangles did
nothing while ObjectWorld::collision was null, so every door and barrier kept the disc's one-sided triangles (and no
0x400 / 0x40 bits or material) and every pane its own (no GLASS, one-sided): the player walked out through a
door whose face looked away from him and was stopped by it on the way back (door 10, from (−184.92, 95.45) at 60 %
stick: the feet crossed its plane at x −187.63 at about 3.3 m/s; inward he stopped at x −188.107, a radius and the
0.01 m back-off from the plane). Now every write the objects make to a triangle (markTriangles: the two-sided, door,
breakable, testable-while-disabled and CamGhostDoor bits and the material; setTrianglesEnabled: the enabled bit) is
kept in ObjectWorld::triangleWrites as well as applied to a bound mesh, and ObjectWorld::bindCollision
(repo:src/world_objects/object_services.cpp, called by PlayLevelMode::bindObjects and with null when the mode
ends) applies them all to the mesh it binds, so a freshly loaded mesh carries the same flags as the original's live
one. A script's ChangeCollision (a volume box's triangles switched on or off) is kept the same way
(ObjectWorld::boxWrites), since which triangles lie inside is known only with a mesh; the bind applies the bits and
materials, then every switch on or off, single triangles and boxes, in the order they were made. The level's unload
(LevelObjects::clear) forgets them all. Tests: repo:tests/world_objects/door_blocking_test.cpp (a synthetic
doorway walked into from both sides at 60 % stick, closed, opened by OpenDoor and closed again; a barrier; a glass
pane; box switches in order with single ones; the writes on a second mesh; the unload) and
repo:tests/platform/disc_hub_doors_test.cpp (level95 checkpoint 1 through the game session: door 10's triangles
carry 0x2 | 0x400, and player 1 steered at it from inside and from outside stops short of its plane both times).
Coney's stand-ins, where this page is silent:
- A leaf model is
dyn_dr_and the type name afterdyn_door_; a door model the Object List lacks drawsdyn_dr_and the name less a leadingdbl(dyn_door_fencedrawsdyn_dr_fence). A model stage a splintering door or a cabin leaf takes is not drawn. Panes are not sorted by their distance within the frame (all share batch 0), a pane's body is a box only an airborne human meets (Moving into a pane: the push-out sphere of Characters stands for both the airborne body and the jump's strike shapes, and every whole pane has its body whatever its distance from a camera), and the leaf's pose steps per 60 Hz tick. DoorOpen's "away" uses the door's turned y axis.OpenDoorAnimatedand a lock pick's success open at once (DoorOpen), without human state 26.- A shard's offset is a random step of 1/1000 in ±1. A link's distance
is to its middle. Of several holes whose boxes hold a doorway's middle and none of which takes it in, the
nearest vertex average wins. Wreck pieces and boards spawn at the door; a cabin door keeps
its leaves once broken. Game state bits 2 and 4 are not read. An
object type no
CfgObjnames is a swinging door of 100 hitpoints. - Square with no human in front aims at a whole pane (its centre) as
Player_PickTarget's object pass does (Combat); the object attack's hit breaks that pane. Doors are not object targets yet. Every other hit reaches an object through the strike shapes (Combat): a segment shape meets a pane's box as spheres every 5 cm along it, and a door through its two triangles while they are enabled. A broken barrier's triangles stay on until its removal, standing for the body the break leaves (dyn_door_wall_a, whose body goes, anddyn_door_vargas, which is never removed, turn them off at the break). Thrown objects do not reach the objects yet: nothing is thrown in play. - Sounds: a name hash plays as a positional sound at the object, a material pair from the
sound matrix; the lock pick's click is counted, not played (its cue is not traced). Shards,
crimes beyond the
CrimeSceneflag, statistics and models do nothing yet; a loose object (a wreck piece, a board, a broken prop's piece) becomes a spawn record that rests where it is made. - Shards (
gamemodes/level_object_services.h, with Particles): the culling step's tests are player 1's camera (the scene camera while a scene plays) within 15 m of the pane and the pane within 10 m of its view's frustum (effects::effectNearView(),repo:src/effects/effect_culling.h), and the budget is 158 particles left in the pool; dust and bursts are asub_shack_puff. A broken pane also frees the stereo of a parked car within 2 m of it (Cars). ObjDestroyon a pane releases it (GlassPanes::release()): it vanishes with no shatter or sound, its triangles left as they are.BreakObjectsInRadiussends its destroy message to the doors and to theObjSpawnobjects that are live (and the centre object) within the radius (repo:src/world_objects/object_breaks.h): adyn_molotvbreaks itself 22 ticks later into asub_explode0.7 m up (Script types: the Molotov); other types ignore it. Coney's reading: 22 ticks for every bottle, the page's figure for one spawned that frame.
Breakable props (Breakable props, repo:src/world_objects/props.h): CfgObj keeps the
counters' bytes, the body word, the material and the body's centre, size and shape; bodyFlagsOf and
isStrikeTarget decide which world objects a strike may pick. Square offers player 1 the drawn ones within 2 m
(Combat) and its object attack strikes the one picked; a moving attack's strike shapes
strike any whose body they reach (a box of the type's size about its centre, or a sphere of half its x). Props::strike
follows Strike_Contact's order: intact or not, WorldObject_TakeHit, the impact sound, message 6 only while intact, then
DynMasks_OnHit for a dyn_masks prop: its sound and two dust bursts at the hit point, and on a break the crate
stack's or the bench's piece (and the bench's splinters); then it loses its body and is removed (message 2, its record
gone). GetRTTI gives 0x400 for a pane. Coney's stand-ins: the noise, statistic 9, the burn near a fire and the
newsstands' paper are not built; the dust is the shards' sub_shack_puff; a piece rests where it is made; the crate
piece's random turn is drawn in steps of 1/1000 of pi; a broken prop goes 20 ticks after the break; the size is read as
whole extents, a cylinder or capsule body is tested as its box, and only drawn objects within 4 m are tested.
Trash props (Trash cans and bags, repo:src/world_objects/props.h): an overhead_weapon prop
breaks on its first strike (OverheadWeapon_Break): two dust bursts 0.5 m above the hit point; the trash can's dented
can or the park bin's piece at its pose; for the can, the bags and the park bin, 10 splinters, a burst at the prop and
the four litter pieces, and the dyn_beerbottle 0.5 m above the path polygon under the hit point (none off the
polygons), in the prop's rotation, falling as a loose object with a spin of (0, a, b), a and b in ±π; the paper stack's
8 debris pieces, or 44 splinters for another model; then its material against itself, at 0.65 of its volume when the
attacker ran into it. It loses its body at once and is removed the next tick. The pieces are launched as loose objects
along its contact vector (zero when struck standing, so they drop) and stop dead on landing (Wear and breaks in flight,
above); a thrown or dropped can or bags breaks on its first contact. A human above jog whose walking sphere
meets a RUNTARGET body (the bags, the paper stack) strikes it with a plain hit at the next step, once. Coney's
stand-ins: the pieces never fade; the litter system's pieces, the splat, the splinters'
colours, the camera test and the path-polygon flag 8 are not built; the polygon's height under the hit point is the
mean of its vertices' heights; the hobo variants' food is not dropped; the paper stack's
debris pieces are splinters; a litter piece's offset is drawn in ±0.43, ±0.43 and 0-0.83 m; and the sprinter is not
stopped dead after the strike (he slides along the bags that step, and they are gone the next).
Cash register (Script types: cash registers, repo:src/world_objects/props.h):
a dyn_cashreg makes its drawer (dyn_cashreg_b) when it first stands in the world, at (0, 0.02, −0.22) m in its
own frame and pose, updating every 60 ticks. It takes 2 + 8 × the hit's kind from its type's +0x5a (16), so eight
bare hits or one charge break it. A hit it survives sounds its material against concrete at the register and raises
dust at the hit; the breaking hit swaps its broken model (0x59026f53, drawn through the spawn record's model), sounds
its material against itself and opens its drawer; the register is no longer a strike target but keeps its body and is
never removed. At its first update after the break the drawer jumps 0.35 m out along its local y; at its second it
spills a dyn_money 0.22 m above it holding $25-50 (WorldObject_SetField124, the record's money), which rests there.
Player 1 takes money by walking over it (Walking over a power-up,
repo:src/gamemodes/level_pickups.h). Triangle's message 0 reaches the register's own class (DynCashreg_OnMessage,
LevelPickups' native message): its drawer is deleted whatever happens, and unless it is broken or already lifted it
asks to be picked up (0x14), so the press lifts it with its pick-up animation's clip (2: 503, or 504 above 0.8 m);
a broken one refuses and the search passes it over. Coney's stand-ins: the dust is a burst; the game state flag that
silences the hit sound is not built; a lifted register's break makes its dyn_cashreg_c piece (Wear and breaks in
flight, above) without the contact offset, and the piece spills no money (its age is not built); and the walk-over's
touch is Player state's. In level99 at checkpoint
2 the store's register stands at (53.23, 57.26, 1.63); eight squares from the shop floor south of it break it, the
money lies behind the counter, and walking into it from there takes it; triangle from the same spot instead deletes
the drawer and lifts the register with clip 504 (disc checks, 2026-10-08).
Open questions¶
- Door blocking (How a closed door stops a human): read the
PHYFLAGs of the door types with anOBBbody (dyn_door_store, the cabin doors) to settle whether their body ever meets a walker. - Which search mask admits
0x40links. - What human state 26 (the animated open) plays and when it sends the door
0x0b; what fills a player's interaction record (+0x660) for a door. - Check at runtime that a broken pane is no longer drawn (Drawing a pane), and that a leaf model spans its local x from 0 to −w (Leaves).
- How the PTank builds a matrix sprite's corners and texture coordinates (inferred equal to the near path's), and which of a shard's two colours the particle draw uses.
- How the draw blends a translucent object (the column's material and vertex alpha, the PS2 blend), and where the
instance's colour bytes are set from
+0xc8. - How the instance places the model: whether its clump's atomic frame (identity, or the z-up turn with an offset) is used; Coney leaves it out.
- Who spawns
sub_objective_glow; which levels use the purple and white marker kinds. - How the resource manager picks the object whose model it loads next (
+0xbd4), and whetherlevel100's packs hold the Wonder Wheel's models. - What the first camera's vtable
+0x214returns (the streaming-out distance). - Held objects: what fills human
+0x348/+0x34c(event0x37), what message0x17to the holder does, who sendsmelee_weaponmessages0x12/0x13, and the frame of a thrown object's angular velocity. - Trash props (Trash cans and bags): what sets game-state set 3's bits, and who sends a broken prop
message
0x20. dyn_door_vargas' second object, and the leaf models ofdyn_door_chainlnk_pick(nodyn_dr_chainlnk_pickrecord).- What a cabin door's leaves do once it breaks, and where the wreck pieces and boards appear.
- Cash register (Script types): how many throws or swings break a lifted register at runtime.
- Impact sounds (Sound events): which speed
WorldObject_OnImpacttests against 2 m/s (the object's speed, or its speed into the surface; Coney uses the first), and whether its count+0x13cis ever reset (Coney counts per flight). Which materials thefade_objectpieces (dyn_trashcan_b,dyn_parktrash_aa, thedyn_trashbit_*,dyn_cashreg_c) have, and which samples their pairs against the level's surfaces play (the debris soundsgarbpart_*,debris_part_*,smallboardpart_*,wood_drops_*and the others): Coney plays the pair the type table gives and has no check that it matches. - Wear (Wear on contact): what message 1 does for a thrown object of a class other than
thrown_weapon,overhead_weaponanddyn_cashregwhose counters run out (amelee_weaponwith hits); Coney leaves it lying. - Moving into a pane (Moving into a pane): which clip events switch the strike shapes on in a jump.
(Answered on Combat: the strike shapes' sizes and events, record
+0x08bit0x800, and a knock-back flight, which is not airborne. Human vtable+0x10c,0x00227180: any non-human body may be struck, a human unless its state word has0x100000000.)