Skip to content

Physics

Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). Static reading in Ghidra (2026-10-06, the whole of Physics/ on 2026-10-07); the game under PCSX2 2.9.94 (PINE, hooks and pcsx2 record, in level99, 2026-10-07). The exact specifications of Reimplementing Physics, RayCast and World were read in the disassembly on 2026-10-12.

Purpose

The physics world is what moving things collide through: the bodies and shapes of humans and world objects, the sweep that moves a body against the level's collision mesh and the other bodies, the queries other code asks (a shape, a box or a ray against the world), and a small per-tick step. It is not a rigid-body simulator: nothing in it integrates velocities or applies impulses, and a contact changes only the moving body's velocity, as its owner's contact handler decides (Contacts). Each mover integrates itself in its own update (humans in Humans_Update, world objects on the wheel) and asks the physics world to sweep it.

The 60 Hz step (0x00340918, called on every tick of the play tick) does two small jobs: it plays the "settle" turn of a thrown or dropped object that has landed, and every 2 s it refreshes a broad-phase bound. Neither moves a human or decides a hit.

Original structure

One object of class IPhysics (name string 0x005502b0), 0x4f460 bytes, created at boot into 0x00597198 (Boot). Its code lies before and in c:/Warriors/Source/Physics/physics.cpp (path 0x005771d0; Source map): the constructor 0x0033c288 starts right after the heaps' block allocator, so 0x0033c288-0x003418f8 is Physics/ too (inferred; no path string names the file).

In-house, no middleware. Confirmed (code) for the names: the class is IPhysics and the path is the game's own c:/Warriors/Source/Physics/. Inferred for the absence: no string of the executable names Havok, Karma, Meqon, ODE or a ragdoll, and the linked libraries (Source map) include no physics library.

Every function of Physics/ (0x0033c288-0x0034f740), by area; the sections below say what each does. Names are ours (some set by the Combat research) and match the local Ghidra project; all confirmed (code) at the address unless a section says otherwise.

Area Functions Evidence
The world IPhysics_Construct 0x0033c288, IPhysics_Step 0x00340918, Physics_StaticInit 0x0034f6c0 and its stub 0x0034f718 (copy a 48-byte constant from 0x00511720 to 0x006eb9e0) confirmed (code)
Bodies create and free: IPhysics_CreateHumanBody 0x0033cc48 / _FreeHumanBody 0x0033ce00, _CreatePunchBagBody 0x0033cb08 / _FreePunchBagBody 0x0033cbf0, _CreateObjectBody 0x0033ce58 / _FreeObjectBody 0x0033cf00, _CreateCarBody 0x0033cf68 / _FreeCarBody 0x0033cff8, Physics_CreateBoxBody 0x0033d070 / IPhysics_FreeBoxBody 0x0033d158; lists: IPhysics_AddBody 0x00340668, _RemoveBody 0x00340700, _AddHumanBody 0x003407a0, _RemoveHumanBody 0x003407f8, BodyList_Insert / _Remove / _Reinsert 0x003404a8 / 0x003405d8 / 0x00340628; the body: PhysicsNode_Init 0x00341158, PhysicsBody_Construct 0x003418f8, _ConstructForOwner 0x00341970, _Destroy 0x00341a00, _SetOwner 0x00341a68, _AddShape 0x00341aa8, _GetShapes 0x00341ae0, _Update 0x00341c10, _Moved 0x00341e40, _GetContactScale 0x00341e18, _ComputeBounds 0x00341638, _AddShapeBounds 0x003417c0 confirmed (code)
Shapes pools: IPhysics_AllocObjectSphere 0x0033d1a0, _AllocObjectBox 0x0033d1e8, _AllocCarBox 0x0033d230, _AllocBigSphere 0x0033d278, _FreeShape 0x0033d2c8, _FindFreeStrikeSphere 0x003408d8; base: PhysicsShape_Construct / _Destroy 0x00342a30 / 0x00342a70, PhysicsShape_GetBounds 0x00341190; sphere 0x00342aa0 / 0x00342ae0 / pose 0x00342b08; box 0x00342d18 / 0x00342d98, PhysicsBox_SetSize 0x00342dc0, pose 0x00342df8, _SetMatrix 0x00343040, _GetCorners 0x003430e8; capsule 0x003434a8 / 0x003434e0, size 0x00343508, pose 0x00343518; segment 0x003437b0 / 0x003437e8, pose 0x00343810; AABB_FromSphere 0x00341660, AABB_FromUprightCylinder 0x00341708, AABB_ExtendByMove 0x00341840, AABB_ScaleAboutCentre 0x003414c0 confirmed (code)
The human body PhysicsHumanBody_Construct 0x00341e98, _Destroy 0x00341f00, PhysicsBody_ApplyHumanScale 0x00341f28, PhysicsHumanBody_SetPushSphere 0x003420d0, _GetShapes 0x003420c8, _OnStruck 0x00342130, _AddStrikeSphere 0x00342158, _ClearStrikeSpheres 0x00342168, _GetContactScale 0x00342288, _Update 0x003422d0, _PushAwayHumans 0x003425c0, _Moved 0x00342828, PhysBody_SetShapeEnabled 0x003428d0, _AddBoneShape 0x003429e8, Human_TestStrikes 0x0033f110 (Combat) confirmed (code)
Ignore lists IPhysics_FindFreeIgnoreSlot 0x00340890, IPhysics_ForgetIgnoredOwner 0x0033cd70, PhysicsBody_IsIgnoring / _Ignore / _StopIgnoring 0x00341ae8 / 0x00341b20 / 0x00341b80, the human body's 0x003421d0 / 0x00342230 / 0x00342260 confirmed (code)
Queries IPhysics_QueryBox 0x0033feb0, _QueryBoxFiltered 0x00340120, _CollideShape 0x0033e7f0, _OverlapShape 0x0033ebb8, _RayCastBodies 0x0033eee0 confirmed (code)
Sweeping a body PhysicsBody_Sweep 0x0033e278, IPhysics_SweepShapeVsMesh 0x0033d2d8, _SweepBodyVsMesh 0x0033d340, PhysicsBody_SweepHumanMask 0x0033d498 (against the bodies), PhysicsBody_PushOutOfWalls 0x003477c0 (Characters) confirmed (code)
Contacts Contact_InsertSorted 0x0033d718, PhysicsBody_ResolveContacts 0x0033d9d8, PhysicsVec_BackOffPlane 0x0033d7b8, Vec_RemoveNormalPart 0x0033d870, PhysicsVec_Bounce 0x0033d8f0 confirmed (code)
Shape pairs 14 overlap functions 0x00345d58-0x00347148, 16 sweep functions 0x00343cd8-0x00345d38, 2 ray functions 0x003459f0, 0x00345b90, the mesh functions 0x003473b8, 0x003473c0, 0x003473c8, 0x00347c08, 0x00348530, 0x00348a50 confirmed (code)
Mesh tests CollisionMesh_BeginShapeQuery 0x00347170, MeshContacts_SortByFraction 0x00347300, the working triangle 0x00343a90, 0x00343b20, 0x00343c30, 0x00343c90 confirmed (code)
Geometry kernels 0x00349180-0x0034ee60 (37 functions) confirmed (code)
Settling IPhysics_StartSettle 0x00340fe0, _CancelSettle 0x003410f0, Settle_ComputeTarget 0x00340d08, Settle_NearestAxis 0x00340b38 confirmed (code)

Outside Physics/:

Address Name Role Evidence
0x00336a00 Quat_Slerp(t, out, from, to) spherical interpolation, shortest arc confirmed (code)
0x00335b08 Quat_IntegrateAngular(dt, q, ω) q += ½ ω q dt, normalised confirmed (code)
0x00394050 WorldObject_OnContact the world object's contact handler (vtable 0x005453a0 +0xfc) confirmed (code); role inferred
0x00395a10 WorldObject_Integrate the world object's integrator (vtable +0x144) confirmed (code)
0x00395b70 WorldObject_Update the world object's update (vtable +0x13c) confirmed (code)
0x00391c10 WorldObject_Remove cancels a settle, leaves the ObjectTaskManager confirmed (code); role inferred

The world object here is the class of vtable 0x005453a0, allocated from the ObjectTaskManager's pool (0x004f3e98) and one of the four classes seen on the wheel (Tasks); its kind is an ObjectAttribs entry (0x00512c04, indexed by the object's +0x112).

Data

IPhysics

Confirmed (code) at 0x0033c288 and the step. Pool sizes are counts × bytes.

Offset Type Meaning
+0x04 ptr head of the human body list (below)
+0x08 ptr head of the body list, sorted by AABB min x
+0x10 u16 zeroed at construction; reader not traced
+0x12 u16 ticks since the last bound refresh (0-119)
+0x14 float the widest body in x, max(max.x − min.x)
+0x18 / +0x7c 5 × 5 fns the shape-pair overlap and sweep tables
+0xe0 / +0xf4 / +0x108 5 fns each per shape type: sweep against the level mesh, overlap with it, a ray against the shape
+0x120 300 × 0x60 spheres: the humans' five bone spheres each
+0x71a0 300 × 0x70 segments: the humans' five bone segments each
+0xf4e0 60 × 12 the ignore slots
+0xf7b0 60 × 0x60 spheres for weapon strike spheres (0x003408d8), switched off at construction and flagged 0x2 (flags 0x2)
+0x10e30 60 × 0x60 the humans' capsules, made with (2.0, 0.35) by 0x00343508
+0x124b0 60 × 0xe0 the humans' bodies: each holds its capsule (+0x30) and ten bone shapes
+0x15930 / +0x22930 / +0x2c530 416 × 0x80 / 0x60 / 0x90 world objects: bodies, spheres, boxes
+0x3af30 / +0x42eb0 255 × 0x80 / 0x90 glass panes: bodies, boxes
+0x4be20 / +0x4c720 18 × 0x80 / 36 × 0x90 cars: bodies, boxes
+0x4db60 30 × 0x60 2.0 m spheres: the humans' push spheres
+0x4e6a0 64 × 0x30 settle slots (below)
+0x4f2a0-+0x4f3f0 the punch bag's body set: a human body, one segment (bone 3, radius 0.35, length 1.5, no offset, along the bone's x) and a capsule (radius 0.35, height 1.787 m); Bodies
+0x4f450-+0x4f45c vector every live body, by index (body +0x20): +0x4f454 the array, +0x4f458 / +0x4f45c count and capacity (inferred), all zeroed at construction; +0x4f450 is not written (Unit 15)

The 60 human body sets match the 60 human slots (AI humans), inferred. Who allocates from which pool is confirmed (code) by the callers: Obj_CreatePhysicsBody (world objects), GlassPane_CreateBody, Car_MakeBodies. The ten bone shapes are the strike shapes; their bones, sizes and events are on Combat.

Bodies

Body (0x80, PhysicsBody_Construct 0x003418f8, vtable 0x00544710). Confirmed (code):

Offset Meaning
+0x00 / +0x10 AABB min / max (w = 1)
+0x20 index in the body vector, −1 when none (PhysicsNode_Init 0x00341158; IPhysics_RemoveBody does not reset it, Unit 7)
+0x24 / +0x28 previous / next in the x-sorted list
+0x30 first shape (a chain through shape +0x00)
+0x34 owner (a task object); free when 0
+0x38 ignore list (below)
+0x3c group (s16): two bodies of the same non-zero group never touch
+0x40 flags: 0x80000000 dirty (re-pose and re-sort on the next update), 0x800 update every time, 0x801 sweep the moved shape against the bodies in the update, 0x8000 no slide on contact; the starting values per kind are below
+0x44 the owner's type mask, from owner vtable +0x20 (0x40 a human); queries filter by it
+0x50 pending push-out, added to the next sweep's move
+0x60 scale per axis (1.0; the player's capsule 1.4286, Characters)
+0x70 the sweep's "could not move" counter
+0x74 vtable

Body vtable: +0x08 destructor (0x00341a00), +0x10 Update (0x00341c10), +0x18 Moved (0x00341e40), +0x20 AddShape (0x00341aa8), +0x28 GetShapes (0x00341ae0), +0x30 / +0x38 / +0x40 the ignore list, +0x48 GetContactScale (0x00341e18: 1.0 for a contact with a body, 0.98 with the mesh).

World-object bodies (Obj_CreatePhysicsBody, 0x00391d48), from the type (CfgObj), confirmed (code): shape 1 (OBB) is a box with half-extents size / 2 (+0x78, PhysicsBox_SetSize 0x00342dc0); shape 2 (SPHERE) a sphere of radius size.x / 2; both offset by centre (type +0x1c, Object_GetAttachOffset 0x003917d8). Other shapes get no body, except one class (the RTTI check against 0x00580ec8) that gets a 0.75 m sphere with flag 4. The flags are then 0x80000500 plus a bit per PHYFLAG bit of CfgObj argument 12 (+0x5e), the layers below; object kinds 0x1d, 0x21, 0x22, 0xf, 0x1a and 0x1f add 0x800, 1, 2, 0x100 or the object flag 0x200 (per kind, as listed at 0x00392160).

Collision layers

Body flag bits that a mover's mask selects (Sweeping a body), named by the scripts' PHYFLAG table (enums), which Obj_CreatePhysicsBody maps one to one (confirmed (code)):

PHYFLAG Value Body flag Who meets a body with it
BLOCKOBJECTS 1 0x2 a flying world object's sweep
BLOCKHUMANS 2 0x4 a walking human's sweep
MELEETARGET 4 0x10 strikes
WEAPONTARGET 8 0x8 weapon strikes
JUMPTARGET 16 0x40 an airborne (or holding) human's sweep
CHARGETARGET 32 0x20 a human whose capsule has flag 2 (charging)
RAYCASTTARGET 64 0x2000 rays
COMBATPASSTHROUGH 128 0x10000
THROWNWEAPONTARGET 256 0x20000 thrown weapons
RUNTARGET 512 0x40000 a human above gait 3 (running) strikes it on contact (Objects)

The "who" column for strikes, rays and thrown weapons is inferred from the names; the street props' values and what Human_OnContact does with JUMPTARGET, CHARGETARGET and RUNTARGET are on Objects. Confirmed (runtime), the body flags in level99's street: humans 0x2203f (0x2223f, 0x2343f), cars 0x2211a, glass panes 0x7a, world objects 0x500 (no layer) up to 0x5057e. So a walking human meets other humans (answered 4, Contacts) and the objects with BLOCKHUMANS, but not a car's or a glass pane's body.

What stops a walker at a parked car is the collision mesh. Confirmed (runtime), quick-save slot 6: the player put 1.5 m from a parked car's side and walked into it at an angle (stick 80 % up; hook human-contact on Human_OnContact) slid along the car's side 0.42 m from its body's box, and each of the 29 contacts had no object (+0x40 = 0) and a mesh triangle with flags 0xf00d (enabled, type bits 2 and 3, top value 15) and material 0. The car code has no collision-mesh call, so these triangles are inferred to be part of the level's static mesh at the car's place; bit 2 is the player-climbable bit (Collision), which matches the climb onto a car (Characters). The shells themselves are on Characters.

The create functions take the first free body of a pool (owner 0), set the owner (0x00341a68) and flags, and add it to the body vector and the sorted list (IPhysics_AddBody 0x00340668); the free functions undo that (IPhysics_RemoveBody 0x00340700, which first clears the owner from every ignore list) and, for objects, cars and panes, clear the shape's in-use bit 0x8 (the human free clears none, and the pane free skips StopIgnoring, Unit 9). Flags by kind: human 0x8002243f, punch bag 0x8000243f, world object 0x80000500, car 0x80000000 (then 0x8002211a once Car_MakeBodies ORs in 0x2211a, 0x00387ea8), glass pane 0x8000007a (Objects). A human body also joins the human list (0x003407a0; next +0xc8, previous +0xcc) and gets its scale (0x00341f28). The punch bag is the human class 0x1cc (HeavyBag, Characters): 0x0021ce08 gives it its own body set instead of one of the 60.

The punch bag's shapes (IPhysics_CreatePunchBagBody, 0x0033cb08: the capsule on, the bone shapes off), read at runtime (PCSX2 2.9.94, level95, the bag at rest with its origin at height −197.567; confirmed (runtime), the posing confirmed (code) at 0x00343810 and 0x00343518):

Shape Bone Offset +0x10 Size Posed
segment (type 4, inline at body set +0xe0, also listed at a pool copy) 3 (0, 0, 0) radius 0.35, length 1.5 start = bone 3's position, 2.15 m above the origin; end = start + bone 3's local x axis × 1.5, which at rest points straight down (end 0.65 m above the origin, 0.05 m off the vertical); so it swings with the bag
capsule (type 3) none (0, 0, 0) radius 0.35, height 1.787 m base on the bag's origin; the height is recomputed each update as for a human (bone 6's height + 0.2 × scale)

The segment is what strikes meet (Combat); the capsule is what sweeps and pushes meet (Workouts).

PhysicsBody_Update (0x00341c10), when the body is dirty or flag 0x800 is set: pose every shape (shape vtable +0x28), recompute the AABB from the shapes (0x00341638, 0x003417c0), re-sort (0x00340628), clear the dirty bit; with 0x801 set, it also sweeps the first shape from where it was to where it is against the other bodies and resolves the contacts, so a body moved by its owner still meets what is in the way. The candidates come from the new AABB grown forward by the move, not from a box around the whole move, so a body passed through can be missed (Unit 7). The camera builds bodies on its stack for its sphere tests (0x00341970).

Shapes

A shape is a 16-byte-aligned record with a common head (PhysicsShape_Construct 0x00342a30, vtable 0x00544870): +0x00 next shape of the body, +0x10 local offset, +0x20 the previous world position (for sweeps), +0x30 type, +0x31 bone (−1: the body's own frame), +0x32 flags (0x1 enabled, 0x2, 0x4 a target for strikes, 0x8 allocated, 0x10 reset: the next pose makes the previous position equal the new one, for the sphere, box and segment but not the capsule, Unit 6), +0x34 vtable of 8-byte {adjust, function} slots (the enable sets 0x11, a getter returns a pointer to the world position, a setter +0x0c writes it, the pose, and "set from a matrix", which moves a sphere's centre or a capsule's base to the matrix's position and sets a box's axes; slot by slot in Unit 6). The world AABB of any shape is 0x00341190: for a capsule the upright cylinder's box, and a segment pointing down on an axis gets a wrong bound there (Unit 6). Confirmed (code):

Type Shape Size, vtable Fields Pose (vtable +0x28)
1 oriented box 0x90, 0x005447f0 +0x40 half extents (PhysicsBox_SetSize 0x00342dc0 halves the sizes it gets), +0x50-+0x7c axes, +0x80 position the owner's matrix and the local offset (0x00342df8); cars set the matrix directly (0x00343040)
2 sphere 0x60, 0x00544830 +0x40 radius, +0x50 centre the local offset on its bone (owner vtable +0xb8) or in the body's frame (owner vtable +0xa0) (0x00342b08)
3 capsule 0x60, 0x005447b0 +0x40 radius, +0x44 height, +0x50 base for a human at its position, the offset unused, and the height follows the state; for another owner the position plus the unrotated offset (0x00343518, below, Unit 6)
4 segment 0x70, 0x00544770 +0x40 start, +0x50 end, +0x60 radius, +0x64 length start = its bone plus the bone-rotated offset (bone −1: the body's position, offset ignored), end = start + the bone's x axis × length (0x00343810)

The "capsule" is upright: the capsule-capsule tests treat it as the vertical range [base z, base z + height] with a horizontal radius (0x00346388, 0x003498c0 + 0x0034a8d0), so a human never tilts. The sweep swaps the two capsules' heights (0x00343e1c-0x00343e28, Unit 11); the overlap test does not. The capsule-box sweep reads no height or z range at all: only the candidate gather separates them vertically (Unit 12). Two overlap tests round its ends instead (capsule against sphere and against segment, a true capsule reaching r past both ends), and the capsule-box overlap reads no height at all (Unit 4); confirmed (code). A segment is a sphere of its radius swept along a bone: the limbs' strike volumes.

The human body

0xe0 bytes (0x00341e98, vtable 0x005446b0): a body plus +0x80 the ten bone shapes (0x003429e8), +0x84 the push sphere, +0x90 a broad bound, +0xc0 one bit per switched-on bone (PhysBody_SetShapeEnabled 0x003428d0), +0xc4 the weapon strike spheres (0x00342158, 0x00342168), +0xc8 / +0xcc the human list, +0xd0 a weapon-strike flag, +0xd4 "ignore the level". The bone shapes, their sizes and when they switch on are on Combat; PhysicsBody_ApplyHumanScale (0x00341f28) multiplies their offsets, radii and lengths and the capsule (height 0x00219890, radius 0x00219860 of the human) by the human's scale. Confirmed (code) at the cited addresses:

  • Update (vtable +0x10, 0x003422d0), unless flag 0x4000: re-pose the capsule when dirty; with weapon strike spheres, pose them on the held object (0x00226ff0); while anything strikes (and on a dirty update without strikes), rebuild the broad bound with the strike reach max(0.8 × the scale, the current attack's reach, AttackTable_GetReach), or with weapon spheres the horizontal distance to the first one plus its radius; pose the bone shapes with flag 0x1 or 0x4 and the push sphere (Unit 10).
  • Moved (vtable +0x18, 0x00342828): the broad bound +0x90 becomes an upright box 0.8 m out horizontally and 2.0 m tall from the position (Unit 10), and each bone shape with flag 0x1 or 0x4 is "set from" the new matrix (0x00342890): a sphere's centre jumps to the body's new origin (no offset) with its previous position left alone, and a segment is not touched. So it does not reset the previous positions: the next pose sweeps a sphere from the new origin to its bone, a segment from its start before the teleport (Unit 6).
  • The capsule's height (0x00343518): base height + 0.2 × scale; the bottom raised by 0.61 × scale in one mode (+0x3be) and by 0.7 × scale in the states 0x00227f68 selects (at most 0.7 above +0x560), except in a long fall; 0.35 × scale when cuffed; never under 0.2 × scale.
  • The push sphere (0x003420d0): a 2.0 m sphere from the 30 pool, switched on only by Grab_Start and Tackle_Start; every other caller switches it off (Unit 10). While it is on, 0x003425c0 (from the human's update 0x0023d8c8) overlaps it with the other humans (options 0x2012, mask 0x40) and adds to the pending push-out (body +0x50) of each one that is not its target, has no player number and has no body flag 0x200 a push of 0.25 m × w(this) / (w(other) × k(other)) along the 3-D line from this human to the other (Unit 10): bystanders are nudged out of the animation's way. w is attribute 1, 82.0 for every human, so it cancels; k is attribute 7, the other's push weight (1.0, or 1e9 while it must not move). Confirmed (code) at 0x003425c0.
  • GetContactScale (+0x48, 0x00342288): 0.25 while airborne, else 1.0. +0x50 (0x00342130) tells the human (0x00219b08).

Attributes

An owner answers numbered float attributes through its vtable +0xd0 (and sets them through +0xe0). Confirmed (code) for the human (0x00221260, setter 0x002212b0) and the world object (WorldObject_GetFloatProperty, 0x00395e40); the names are inferred from use:

# Human World object Read by
1 82.0 its type's +0x62 (CfgObj argument 6; 10 or 50 for most types) the push sphere (The human body); a weight
7 +0x27c: the push weight, 1.0, set to 1e9 to make the human immovable (grabs, arrests, Combat) 1.0 the push sphere
8 0 its type's +0x88 (CfgObj argument 11, named mass in the scripts): 0.1 for 1,359 of the 1,371 types, 0.2 for 7 (the cue ball among them), 0.15 and 0.4 for one each, 0 for 3 contact code 2's bounce: the restitution
others 0 0

A third slot, vtable +0xd8, asks whether the owner has attribute n at all; the human's answer (Human_HasAttribute2, 0x002212a0) is yes for attribute 2 only. Confirmed (code); its callers are not traced.

The values per type are confirmed (runtime), read from level99's object database. The bounce (PhysicsVec_Bounce, 0x0033d8f0) is v −= n (v·n)(1 + e) when v·n < 0, and v −= n (v·n) e (an outgoing normal speed cut to 1 − e of itself) otherwise (Unit 3); a brick that hit the floor at 5.4 m/s came back up at 0.54 m/s (Settling).

Ignore lists

A body can ignore particular owners: sixty 12-byte slots at IPhysics +0xf4e0 (+0x00 the body, +0x04 the ignored owner, +0x08 next), chained from body +0x38. Ignore (vtable +0x40, 0x00341b20) takes a free slot (0x00340890), IsIgnoring (+0x30, 0x00341ae8) walks the chain, StopIgnoring (+0x38, 0x00341b80) frees one slot or, given null, all. When a body is removed, every slot naming its owner is freed (0x0033cd70). The human body adds two rules (0x003421d0, 0x00342230, 0x00342260): its owner's grab partner (Human_GetGrabOther 0x002269d8, the other human of a grab) is always ignored, and a null owner means the level itself (+0xd4). With all sixty slots in use, Ignore does nothing. Confirmed (code).

Contacts

A contact is 0xc0 bytes, two 0x40-byte halves for the moving shape and the other one (0x0033d718: +0x00 own owner, +0x04 own shape, +0x40 the other owner or 0 for the level, +0x44 the other shape), then +0x50 the normal, +0x80 the fraction of the move at the contact (0 to 1), +0x90 the point, +0xa0 a slide vector, +0xb0 flags (0x80 a human's landing), +0xb4 the mesh triangle. A mirrored pair function swaps the halves. Lists hold at most 50 contacts, kept sorted by fraction. Confirmed (code).

Resolution (PhysicsBody_ResolveContacts, 0x0033d9d8) walks the sorted contacts and asks the moving body's owner what to do (owner vtable +0xf8, for example Human_OnContact 0x00219d50); a contact is skipped when its triangle has been disabled since it was found, or when its other owner has no physics body (owner vtable +0xf0 returns the body). The low 16 bits of the answer (the whole walk, exactly: Unit 3):

Code What the branch does Evidence
0 nothing confirmed (code); summary inferred
1 advances the body along the normal only, to 0.01 m short of the touch, when that is more than 0.01 m (owner position, vtable +0xa8 / +0xb0; the part of the move along the surface is not applied), then removes from both velocities (the move's and the real one) the part that goes into the surface (0x0033d870) confirmed (code)
2 the same advance; then bounces both velocities off the normal with the owner's restitution (owner vtable +0xd0 with 8; 0x0033d8f0) confirmed (code)
3 the same advance, then zeroes both velocities confirmed (code)
4 as 1, after pushing the other body: unless its flag 0x200 is set, 0.5 × (d · n) n × w(mover) / (w(other) × k(other)) is added to its pending push-out (+0x50), d being this pass's move and n the contact normal (pointing at the mover), so the other is moved away by half of the mover's move into it on its own next sweep; then the other body's vtable +0x54 is told (Human contacts) confirmed (code)

High bits: 0x10000 ends the walk and the move goes on (return 0), 0x40000 aborts the move (return −1), otherwise the walk stops after this contact and the sweep makes another pass (return 1). Code 0 without either bit goes on to the next contact. 0x20000 with a non-zero code keeps the largest contact scale met (body vtable +0x48), applied to the velocity at the end of the move. Contact flag 0x1 (the corner slide of Unit 2) adds the slide vector +0xa0 to the move's velocity, flag 0x2 stores it once per move in body +0x50; both first drop its part against the move's direction. A code above 4 takes none of the branches. Confirmed (code) at 0x0033d9d8 (Unit 3).

Who answers what. Owner vtable +0xf8 is set in seven classes of the task family; the vtables were found from the code that stores them, and each handler was decompiled. Confirmed (code):

Owner Handler Answers
human (vtable 0x0053f088) Human_OnContact 0x00219d50 0, 0x20000 or 0x20001 (slide) against the level and objects (Characters); 1 against a body with type flag 0x200; against another human, 1 after a strike (Strike_Contact 0x0021b290, its vtable +0x104), otherwise 4, with 0x20000 added while airborne
world object (0x005453a0) WorldObject_OnContact 0x00394050 2 against a body or a steep surface (normal z ≤ 0.7), 0x20002 on a floor, 0x10003 on a floor once settled (Settling); 0x10000 (after 0x00393e20) while its byte +0x10d or +0x10e is clear; 0x40000 when, thrown, it hits a human; 0 or 5 (no branch) when 0x003951d8 gives it a human (its holder, inferred)
car (0x00544c08) Car_OnLanded 0x00389e08 always 2; with the level it also clears airborne and zeroes the velocity (Cars)
glass pane (0x00544ed0), light task (0x00545138), particle system (0x00545660), scene (0x005458c8) (Task classes) 0x004dac20 0

So 3 is never answered: nothing in the game zeroes both velocities on a contact.

Settle slot

0x30 bytes, free when +0x00 is 0:

Offset Type Meaning
+0x00 ptr the object
+0x04 float progress t, 0 to 1
+0x08 float acceleration of t, per tick²: 0.02 (0x3ca3d70a)
+0x0c float speed of t, per tick
+0x10 quat end rotation
+0x20 quat start rotation

Object flags (+0x54, task head): 0x40000 settling, 0x8000000 settled.

Behaviour

The step

IPhysics_Step(phys) (0x00340918), once per 60 Hz tick after the wheel. Confirmed (code):

  1. Bound refresh. Count the tick in +0x12; at 120 (every 2 s), reset it and recompute +0x14 as the widest body in x over the list. Inserting a body (0x003404a8) only ever grows +0x14, so the refresh is what lets it shrink. The queries read it to know how far back along the sorted list to look (Queries).
  2. Settle turns. For each of the 64 slots in use: t += speed, speed += 0.02, clamp t to 1; set the object's rotation to slerp(start, end, t), keeping the position of its stored pose (owner slot +0xa8 / +0xac), and write the pose through owner slot +0x68 / +0x6c (the world object's 0x00395220, which changes nothing while the object is held, flag 0x10); if the object has a body, mark it (+0x40 |= 0x80000000) and update the body at once (body slot +0x10 / +0x14, PhysicsBody_Update 0x00341c10: its shapes re-posed and re-sorted). At t = 1 the object's flags lose 0x40000 and gain 0x8000000, and the slot is freed.

Starting from rest, t after n ticks is 0.01 × n(n − 1), so a settle takes 11 ticks (183 ms), slow at first and fastest at the end, like something tipping over.

Confirmed (runtime) in a level: a settle slot filled by hand (an object, start = its rotation, end a quarter turn, acceleration 0.02, speed 0) read t = 0.02, 0.12, 0.2, 0.3, 0.42, 0.56, 0.72, 0.9 on successive samples, then 1.0 with the slot freed and the object's flags 0x40000 → 0x8000000, 0.21 s after the slot was written. A real landing is in Settling a landed object.

Settling a landed object

WorldObject_OnContact (0x00394050) gets the contact (hit object at +0x40, normal at +0x50, point at +0x90). Confirmed (code): when no human holds the object (flag 0x10), it did not break on this contact (both wear bytes +0x10d and +0x10e non-zero, Contacts), the contact is the ground (no hit object and normal z > 0.7), its type's axis byte (+0x85, CfgObj argument 10) is not 0, and it is neither settling nor settled (0x8040000), it starts a settle (IPhysics_StartSettle, 0x00340fe0): the first free slot (or the object's own), speed 0, start = its rotation extrapolated to now (Obj_GetPoseNow, owner slot +0xa0 / +0xa4: the stored rotation turned by the angular velocity over the time since its last update, Unit 15), its angular velocity zeroed (owner slot +0x78 / +0x7c, 0x003a19a0, with the zero vector 0x005116c0), flag 0x40000. With all 64 slots busy nothing happens.

The end rotation (Settle_ComputeTarget, 0x00340d08) turns the object by the smallest angle that lines up one of its local axes with the contact normal, the turn wrapped to ±90°: it comes to rest on its nearest face. The axis (Settle_NearestAxis, 0x00340b38) is the one whose positive direction is nearest the normal (the smallest acos(axis · n), not |axis · n|; the ±90° wrap then lets the negative end land on the floor too, so an allowed axis pointing straight down can lose to one lying flat, which is turned through 90°) among the axes the type's axis byte allows: the byte is passed through unchanged (lbu a3,0x85(v0) at 0x003946ec), bit 0 is the local x axis, bit 1 y, bit 2 z (AXIS: X 1, Y 2, XY 3, Z 4, XZ 5, YZ 6, XYZ 7). Bit 3, in XZ_ROUND 13 and YZ_ROUND 14, is tested but the axis it picks is never read, so a _ROUND type settles like XZ or YZ. Confirmed (code). Of the 1,371 object types loaded in level99, 855 have axis 0 and never settle, 197 are Z, 119 XYZ, 76 X, 54 XZ (the brick, the beer bottle), 30 XY, 28 Y, 4 YZ and 8 XZ_ROUND (the bats and the pool cue); confirmed (runtime), read from the object database.

While it settles (0x40000 only), a floor contact starts nothing new and answers 0x20002: a bounce with the type's restitution, then the move's velocity scaled by the mesh contact scale 0.98 (its z only when upward); a wall or a body answers 2, the same bounce with no scale: no friction, the tangential velocity is kept. Confirmed (code) at 0x00394050 and 0x0033e278, and the 0.98 at runtime (−5.4886 m/s became +0.5379, not +0.5489). On a later ground contact with 0x8000000 set, the object stops: velocity and angular velocity zeroed, flags 0x4000000 (airborne) and 0x8000000 cleared, 0x2000000 (grounded) set. Removing an object (0x00391c10) cancels its settle.

Confirmed (runtime), a brick dropped from the player's hand (scenario physics_brick_settle: Human_DropHeld called on the player standing in the street of quick-save slot 6, no input; hooks on the object's update, its velocity setter, the contact handler and the bounce, so every value below is the game's own, per update):

Ticks after the drop What happens
0 the drop: velocity set to 0 (Human_DropHeld clears +0x10c, so the holder start is not used); flags 0x4000000 airborne
1 first update: vz = −0.2613 (dt 1/60: the drop's message 28 stamps the object's last-update time a tick back, Task_SendMessage28 0x002296c0, Characters)
3-19, every 2 vz −= 0.5226 (dt 1/30, interval 2 ticks), then the sweep moves it
20 floor contact at −5.49 m/s: WorldObject_OnContact starts the settle (axis mask 5) and answers 0x20002; the bounce (e = 0.1, the type's +0x88, Attributes) gives +0.5379 m/s
22-28, every 2 while settling, vz drops by only 0.2613 per update (+0.2766, +0.0153, −0.2459, −0.5072, −0.7685): the update's dt is 1/60, not 1/30 (the step's settle turn re-poses the object every tick, which inferred stamps its time)
31 the settle ends (11 ticks after it started): flags 0x40000 → 0x8000000
32 vz −= 0.5225 (dt 1/30 again), then the second floor contact finds it settled and answers 0x10003: back to 0.01 above the floor, velocity 0 (set three times: the handler, the sweep, the object), airborne and settled cleared, grounded set
36 interval back to 20 ticks

The brick (OBB 0.07 × 0.21 × 0.10 m, centre (0, 0, 0)) came to rest flat on its 0.21 × 0.07 face with its origin at z 0.2831: the floor (0.2231, where the player stands) + its half-height 0.05 + the 0.01 back-off. Four runs gave this sequence; one other run met the floor one update earlier at a different contact point, and its velocity was 0 after each bounce (not explained).

Queries: the sort-and-sweep

Every query first gathers candidate bodies from the x-sorted list with a box. Confirmed (code) at 0x0033feb0 (IPhysics_QueryBox(phys, out, box, startBody, ignoredOwner, typeMask, flagMask)) and 0x00340120 (the same with more filters):

  1. With a start body (the querying body itself, which is a candidate too), walk backwards from it, then forwards from it. Without one, walk forwards from the head. The bound (IPhysics +0x14, the widest body) stops the backward walk only at a node that would reach the box, so with a current bound the walk runs to the head; going backwards there is no min.x ≤ box.max.x test (Unit 7).
  2. Forwards, stop at the first node with min.x > box.max.x.
  3. Keep a body when its max.x ≥ box.min.x and it overlaps the box in y and z, its owner is not the ignored one, its type mask (+0x44) meets typeMask (so typeMask 0 finds nothing) and its flags (+0x40) meet flagMask (0: any). The filtered form also drops bodies with excluded flags, owners whose state flags (owner vtable +0x50) meet an excluded mask, and pairs a callback refuses.

The answer is the backward hits in reverse list order, then the forward hits. The query has no cap: 512 is the size of every caller's list, and nothing checks it (Unit 7). On top of it:

Query Address What it does Evidence
IPhysics_CollideShape(phys, out, query, move) 0x0033e7f0 sweeps a shape (the body's own, or one given with a transform: option 0x400) along a move against the candidate bodies (option 2; the sweep table) and the level mesh (option 1; the type's mesh sweep with the query's two thresholds); keeps up to 50 contacts sorted by fraction and copies the first max out. Bodies of the same non-zero group and owners that refuse (owner vtable +0x110) are skipped. Used by the follow camera's sphere tests, the throw aim and the cars; the airborne push-out and WorldObject_UpdateBodyContacts sweep the mesh only (options 0x2004d); every option bit and caller in Unit 11 confirmed (code)
IPhysics_OverlapShape(phys, out, query) 0x0033ebb8 the same without a move, with the overlap table and the mesh overlap (+0xf4); option 0x80 skips the same group, 0x100 asks the owner, 0x200 only enabled shapes; it reads no other bits than 0x1, 0x2, 0x10, 0x80, 0x100, 0x200 and 0x400 (Unit 7). Used by the push sphere and the strikes confirmed (code)
IPhysics_RayCastBodies(length, phys, out, ray, typeMask, ignoredOwner) 0x0033eee0 the box around the ray's two ends, candidates with flag 0x2000 (RAYCASTTARGET), then each candidate's enabled shapes with the type's ray function (+0x108, boxes and capsules); keeps the nearest hit's owner, shape and fraction and writes no point or normal (Unit 14). Used by the AI's line tests (0x002e7608, 0x00308c60 and others) confirmed (code)

So +0x14 is the classic sort-and-sweep bound: the step's refresh keeps it from growing for ever (The step), though it never cuts the backward walk in practice.

Sweeping a body

PhysicsBody_Sweep (0x0033e278, Characters for the human's use) moves a body by v × dt plus its pending push-out (divided by the owner's push weight, attribute 7, unless body flag 0x1000; cleared on every call), in up to three passes. A pass, confirmed (code):

  1. The body's box at the start of the move, grown to cover the move (0x00341840). A copy scaled about its centre by body +0x60 (0x003414c0) is made and never used: both queries get the unscaled box (0x0033e518, 0x0033e550).
  2. Against the mesh (0x0033d340): the first shape's mesh sweep (+0xe0 by type, through 0x0033d2d8 with the wall threshold −0.65). For a human (type mask 0x40) in the air, also the landing segment (0x0023e408), whose hit becomes a contact flagged 0x80. Faces are one-sided unless flagged. In the capsule's mesh sweep (PhysicsMesh_SweepCapsule 0x00347c08) a triangle is read only for its enabled bit (0x00347de0, no type-bit mask, so a door's 0x400 or 0x40 changes nothing), then: a wall (|n.z| ≤ 0.65, 0x00347e60); the side test n·c − n·v0 < 1e-6 (0x00347ec0), where c is the sphere's centre at the start of the move (the shape's +0x50, read at 0x00347c80, raised by r + 0.05), skips a one-sided triangle (flag 0x2 clear, 0x00347edc) and turns a two-sided one to face the sphere (0x00343c90, 0x00347ee4); then the move must go into the face, n·move < −0.001 (0x00347f2c). So a one-sided wall stops a body only from its front, and a body that starts behind it passes. Confirmed (code). What makes the level's doors block from both sides is the door's init setting flag 0x2 (Objects).
  3. Against the bodies (PhysicsBody_SweepHumanMask, 0x0033d498): the candidates of the swept box whose body flags (+0x40; the type mask is −1) share a bit with a mask that depends on the mover (collision layers): 2 BLOCKOBJECTS for a non-human; for a human 4 BLOCKHUMANS, or 0x44 (and JUMPTARGET) while airborne or holding (0x800), plus 0x20 CHARGETARGET when the capsule has flag 0x2, 0x40000 RUNTARGET above gait 3, and 0x80000 << player; same non-zero group skipped; owner asked (+0x110); each enabled shape of the candidate (body vtable +0x28) through the sweep table, stopping at the candidate's first shape that hits (one contact per candidate body, 0x0033d6c0). Confirmed (code). Between two humans this is one capsule against one capsule: Human contacts.
  4. Contacts are inserted sorted by fraction (0x0033d718) and resolved (Contacts).

The move is never shortened by a contact's fraction: each pass sweeps the whole slid V × dt again from where the last advance left the body (Unit 3). After the passes: when three passes each stopped (or a slide left no move), the body stays where the last advance put it, its horizontal velocity is zeroed and written to the owner (vtable +0x74) whatever the body's flags, and its counter +0x70 goes up; otherwise it moves by the rest and the counter is reset. The velocity the slides leave is written back to the owner (vtable +0x74) only when body flag 0x400 is set (0x0033e738) and a stopping contact asked for it (not a level triangle with flag 0x100, not two airborne humans). Among humans only a player's body has 0x400 (world objects' bodies have it too, 0x80000500): Human_CreatePhysicsBody (0x0021ce08) sets it with +0x60 = +0x64 = 1.4286 for a brain of type 0, as does Human_MakePlayer (0x00229c40); Human_ReleasePlayer (0x0022a2a8) clears it and puts +0x60 / +0x64 back to 1.0. So a slide brakes the player (Characters) but never an AI, whose control asks for its speed again each update. Confirmed (code); the AI's unchanged speed is confirmed (runtime) on Characters.

Human contacts

What a walking human meets of another human, confirmed (code) at the cited addresses:

  • The shapes. The mover sweeps its first shape, and the candidate's chain is walked from its first shape too (body vtable +0x28; PhysicsHumanBody_GetShapes 0x003420c8 returns +0x30): for a human that chain is the capsule alone, so the bone shapes and the push sphere never block. Capsule against capsule (PhysicsSweep_CapsuleCapsule 0x00343cd8) uses each capsule's own radius (shape +0x40, 0.35 × the class scale, already scaled by PhysicsBody_ApplyHumanScale), without body +0x60 or a second scale: centres meet at the sum of the radii (about 0.69 m for the player and a man of scale 1), and the two vertical ranges must overlap during the move. The level's sphere radius (Characters) plays no part.
  • Who is skipped. A candidate without BLOCKHUMANS (0x4) in its body flags, one sharing the mover's non-zero group, and a disabled capsule. Human owners never refuse (owner vtable +0x110 is 0x004dac30, which returns 1), and the brain is not read, so a scripted or BrDead human is met like any other.
  • Groups. Every link of two (or three) humans takes a fresh number from IPhysics +0x10 (counted 1 to 0xfffe) and writes it to both bodies' +0x3c, so linked humans pass through each other: Grab_Link (0x0022c760), Pair_LinkMount (0x0022c000), the tackle (HitReact_WhileTackling), Grab_Escape, Revive_Start, Uncuff_BeginMash, Pair_AlignForMove and Tandem_AlignThree; each also sets both push weights (attribute 7) to 1e9. The matching ends write 0 (Grab_End, Tackle_End, Tandem_Unlink, Throw_ClearLink, Revive_Finish, the uncuff end hooks), as does Human_StartFall (0x0023dc58). A scene sets one group on the humans it plays (SceneTask_Start 0x0039d870, cleared by SceneTask_End). The ignore lists (below) serve strikes, not the walk.
  • Who loses BLOCKHUMANS. Body flag 0x4 is cleared (with the re-pose flag 0x80000000 set) by Human_KnockOut (0x00230328), Human_StartWounded (0x0022fa78), Human_OnHealthOut (0x002674c0, again after either) and Human_Arrest (0x0022ec18, the cuffs), so walkers pass over a human knocked out, wounded or cuffed; a dead one (state 0x100000000) has his body freed unless airborne. Confirmed (code) at the cited addresses; no other & ~4 in the executable reaches a human's body flags (searched by the mask's li -5).
  • A knockdown keeps it. Human_KnockDown (0x0022f100, state 0x80000, the heavy reactions) writes no body flag: it sets the push weight to 1e9 (attribute 7), so a grounded human still blocks walkers, who slide along him without moving him, from the fall until he stands. Getting up (Human_UpdateDown 0x00256a60 → Human_PlayRise 0x0025e2d8, clip 199 with Human_EndDownState 0x0025e250 as its end callback → Human_GetUp 0x0022f3c8) puts the weight back to 1 and touches no body flag either. Confirmed (code).
  • What gives it back. Two things only, confirmed (code):
    • A new body. Human_CreatePhysicsBody (0x0021ce08) frees the body and takes a fresh one from IPhysics_CreateHumanBody (0x0033cc48), whose flags are 0x8002243f (BLOCKHUMANS included; group 0, so no link survives). Human_Heal (0x0022eb40) calls it after waking (Human_WakeUp) and ending a wound (Human_EndWounded), so the human blocks again from the moment he is healed: a revive at its end (Revive_Finish 0x00274ee0, not Revive_Start), a flash (Flash_Use 0x002842e4), the uncuffing (Uncuff_MashSuccess 0x00260910, Uncuff_WithKey 0x00261264), HuRevive (Human_Revive 0x002377f8), and the level's own heals (Gm_Level_Update 0x00159004, ANGameOver_Toggle). Human_AttachInstance also makes one when the human is set up.
    • Human_Unarrest (0x0022ef58) sets body flags 0x80000004: the uncuff paths, HuSetArrested(false) (0x0023774c) and Human_EndDownState when a cuffed human's rise clip ends.
  • What does not. Human_WakeUp (0x00230598) and Human_EndWounded (0x0022fc00) only re-enable a shape and put the push weight back. So a knocked-out human woken without a heal (HuSetConscious(true), Human_SetConscious 0x00237778, or Human_EndDownState's knocked-out branch) and a wound ended by HuSetWounded(false) or Human_ResetForMode stand up without BLOCKHUMANS, and walkers keep passing through them until something heals or unarrests them. Confirmed (code).
  • Seen at runtime (confirmed (runtime), PCSX2 2.9.94, a copy of slot 6, 2026-10-11, scenario repo:research/traces/scenarios/blockhumans_wake.toml). PoizoCiv, standing, his body flags 0x2223f, was put through six direct calls on the game's thread 40 updates apart, his body pointer and body +0x40 read every update:

    Call Body flags after it BLOCKHUMANS
    Human_KnockOut(him, 0) 0x2203b off
    Human_WakeUp(him, 0) (where HuSetConscious(true) and a knocked-out rise end) 0x2203b, unchanged still off
    Human_Heal(him, 0) 0x2203f on
    Human_StartWounded(him) 0x2203b off
    Human_EndWounded(him) (where HuSetWounded(false) ends) 0x2203b, unchanged still off
    Human_Heal(him, 0) 0x2203f on

The heal's new body took the same pool slot (the pointer did not change). So waking and ending a wound leave him passable, and only the heal gives the flag back, as the code says. The knock-out also took bit 0x200 (not pushable) away, which Human_KnockOut's own writes do not: what cleared it in that update is not traced, and the heal's new body did not have it either (Human_CreatePhysicsBody sets it only when +0x3bf is 0; that byte was not read). - The answer (Human_OnContact 0x00219d50): a charging capsule (shape flag 0x2) or an airborne mover not yet ignoring the other strikes him (Strike_Contact, then ignores him) and slides (1); otherwise 4, 0x20004 in the air. So walking into a human slides along him and pushes him by half of the move into him (code 4 above), times 1 / his push weight k (1, or 1e9 while held: nothing). Body flag 0x200 (not pushable) is set by Human_SetPushable (0x0021d848, HuSetPushable(false); also when a body is made for a human whose +0x3bf is 0, Human_CreatePhysicsBody), and by the punch bag; such a human is slid along but never moved. - The pushed human is told (body vtable +0x54, PhysicsHumanBody_OnStruck 0x00342130 → Human_OnPushContact 0x00219b08): only when the pusher is a player and walks into him nearly head-on (the move's direction · the normal < −0.8). A cop, a civilian, or a human of the pusher's side (friendly, and the pusher at a jog or faster) starts a timer at +0x56c / +0x570 (10 s, or 2.1 s for a friend); a second push after 2 s deals Human_DealDamage(him, 0, pusher, 0, hostile 1, 0) (unfriendly: no damage, the hostile flag set; inferred: he takes it as an attack by the player, not checked at runtime) or asks Human_CanPush (friendly) and clears it. Human_StateUpdate clears the timer once the game time passes +0x56c (0x002405e0), so the second push must come before the deadline: 2-10 s after the first for a stranger, 2.0-2.1 s for a friend. A bum under BumLogic ignores it. Human_CanPush (0x002320a0) refuses when either is in a fight stance, holds any of the held flags 0x2fefefff or any state of 0x7bf9e9f7ff0; otherwise it swaps the two places: each human's transform is set to the other's position with its own rotation (vtable +0x6c), so a player who keeps walking into a free teammate at a jog or faster for 2 s ends up on the other side of him (Combat: teammates). Confirmed (code) at 0x00219b08; the swap was seen at runtime with the timer aged by hand (Combat: teammates).

The mover's own velocity is cut by the slide only when it is a player (Sweeping a body).

The shape-pair tables

Two 5 × 5 tables of functions at IPhysics +0x18 (overlap) and +0x7c (sweep), indexed [moving type][other type] (row × 0x14 + column × 4), and three per-type tables against the level mesh and rays. An empty entry means the pair never touches. Filled by IPhysics_Construct, confirmed (code):

Moving \ other 1 box 2 sphere 3 capsule 4 segment Evidence
1 box overlap 0x00345dd8; sweep 0x003451e8 0x00346680; 0x003454c0 0x00346d18; 0x00345108 none; 0x00345590 (never) confirmed (code)
2 sphere 0x00346db8; 0x003453f8 0x00345d58; 0x003440d0 0x00346f90; 0x00345670 0x00346eb0; 0x00344320 confirmed (code)
3 capsule 0x00346808; 0x00344618 0x00346ed8; 0x00345910 0x00346388; 0x00343cd8 0x00347148; 0x00345d18 (overlap) confirmed (code)
4 segment none; 0x00345588 (returns 0) 0x00346de0; 0x00344538 0x00346fb8; 0x00345d38 (overlap) 0x00346500; 0x00344300 (overlap) confirmed (code)
Per type 1 box 2 sphere 3 capsule 4 segment Evidence
mesh sweep +0xe0 0x00348530 0x00348a50 PhysicsMesh_SweepCapsule 0x00347c08 (one sphere, Characters) none confirmed (code)
mesh overlap +0xf4 0x003473b8 (returns 0) 0x003473c8 0x003473c0 (returns 0) none confirmed (code)
ray +0x108 0x00345b90 none 0x003459f0 none confirmed (code)

Mirrored entries call the other order and swap the contact's halves. A segment's "sweep" against a capsule or a segment is the static overlap, which writes no fraction, so such contacts are sorted by stack leftovers (Unit 11): segments move with their bone and are tested where they are, which is how strikes work (Combat). A segment never touches a box, and no sphere or segment is ray-tested. Strikes go through the sweep table (Human_TestStrikes 0x0033f110 sweeps each strike shape from its last pose to its current one), so of a human's strike shapes only the spheres (hands, head, feet, a weapon's spheres) can reach a car's or an object's box; the forearm, shin and spine segments never do. Confirmed (code) (Who reaches a car, Unit 5).

Mesh tests

The mesh functions read the level's collision mesh through the world manager (+0x40 level object, +4 mesh). Confirmed (code):

  • 0x00347170 starts a query: it clears the mesh's per-triangle "visited" bits (+0x94, one bit per triangle, +0x84 triangles) so a triangle shared by several grid cells is tested once, and turns the world box into the mesh's frame to pick the cells.
  • For each enabled triangle of those cells the test fills a working triangle (corners +0x00/+0x10/+0x20, edges 0x00343a90, edge normals 0x00343b20, unit normal +0x70 by 0x00343c30) and turns a two-sided one to face the shape (0x00343c90; for a box, the box's centre decides the side, and the box sweep stores no plane constant at +0x80, Unit 13).
  • Sphere sweep (0x00348a50): Sweep_SphereTriangle (0x0034ee60); box sweep (0x00348530): Sweep_BoxTriangle (0x0034d5d0); sphere overlap (0x003473c8): SphereTriangle_Penetration (0x0034edd0). Hits become 0xb0-byte mesh contacts, sorted by fraction (0x00347300) and copied out.

Geometry kernels

The pair and mesh functions above call these. Their roles are confirmed (code) by their callers and arguments; the exact algebra is in the units below. A correct implementation of the same test gives the same contacts except where a unit names a quirk: Sweep_BoxTriangle's zero-speed reset gives fractions a correct test does not (Unit 13).

Address Name Test Evidence
0x00349268 Sweep_SphereSphere moving sphere against a sphere (also ObjectRender_Draw) confirmed (code)
0x00349488 / 0x00349560 Ray_ClipSlab / Ray_OrientedBox a ray against an oriented box, slab by slab confirmed (code)
0x003498c0, 0x0034a8d0 Sweep_CircleCircle2D, Sweep_IntervalOverlap the capsule-capsule sweep: horizontal circles, then the vertical ranges over time confirmed (code)
0x00349a60, 0x00349180 Sweep_SphereCapsule, Vec_MakePerpendicularBasis a segment against an upright capsule with hemispherical ends; the kernel has no moving radius (a sphere's is folded into the capsule's, a ray passes the capsule's own, Unit 14) confirmed (code)
0x0034a318, 0x0034a6d0 CapsuleBox_SweepCorner, CapsuleBox_SweepSide the capsule-box sweep, four corners and four sides: a circle against a rectangle in the box's plane, with no vertical test (Unit 12) confirmed (code)
0x0034a2d0, 0x0034a6a0 CapsuleBox_CornerCircle, CapsuleBox_SideInterval the capsule-box overlap confirmed (code)
0x0034ac08, 0x0034acc8, 0x0034ad50 Segment_ClosestParam, Segment_ClosestPoint, Segment_ClosestPoints point-segment and segment-segment closest points confirmed (code)
0x0034af50, 0x0034afd8 Ray_SphereRoot, Ray_CircleRoot the first root of a ray against a sphere (3D) or a circle (2D) confirmed (code)
0x0034bbe8 and 0x0034b040, 0x0034b3b8, 0x0034b600, 0x0034b7b0, 0x0034baf0 Sweep_SphereOrientedBox and its corner, edge, edges, face-edge and frame steps moving sphere against an oriented box (also the camera's tilt over obstacles) confirmed (code)
0x0034c430 Sweep_BoxBox moving oriented box against an oriented box: only whether they meet, no fraction (PhysicsSweep_BoxBox writes fraction 0, Unit 13) confirmed (code)
0x0034d5d0, 0x0034aa60, 0x0034ab28 Sweep_BoxTriangle, BoxTri_ProjectBox, BoxTri_ProjectTriangle moving box against a triangle, axis by axis confirmed (code)
0x0034ee60 (0x0034ee40 calls it) Sweep_SphereTriangle moving sphere against a triangle: the face, then an edge (0x0034e3b8) or a corner's region (0x0034e5d0); what it does with a sphere that already overlaps is on Characters confirmed (code)
0x0034e3b8, 0x0034e5d0, 0x0034dff8, 0x0034d938, 0x0034e120 SphereSweep_Edge, SphereSweep_VertexRegion, SphereSweep_EdgeCylinder, Ray_InfiniteCylinder, Segment_SphereIntersect its edge steps: an edge already within the radius (fraction 0), else the edge as a cylinder; past a corner, one of the corner's two edges confirmed (code)
0x0034e840, 0x0034edd0 PointTriangle_DistanceSq, SphereTriangle_Penetration the distance of a point to a triangle; a sphere's depth into it confirmed (code)

How things actually move

None of this is in the step; it is listed so an implementer knows where each motion lives. Confirmed (code):

  • Humans sweep their body in Human_Move at 30 Hz (Characters). The physics world gives the shape and the sweep; no body pushes another here.
  • World objects integrate in their update: by default Task_Integrate (Tasks), explicit Euler with dt = phase time − time of the last update (0x003a1688), so dt is the wheel interval. Airborne and with a body (0x00395a10), velocity first: vz −= 15.68 × dt, then PhysicsBody_Sweep with that velocity, then the rotation by the angular velocity unless settling. On the update after a human lets go (+0x10c set, holder at +0x11c), the object starts at the holder's +0x5f0 with velocity from the holder's +0x600 and that first sweep uses dt = 1/60 s. +0x10c is set only for a throw released from the aiming state (Objects: throws). The two are the thrower's aim, written every frame of the aiming state by Human_TraceThrowAim (0x0018fee0, Graphics draws its arc): +0x5f0 is the release point, a fixed offset turned by the human's rotation (+0x620) and added to his position (+0x610), and +0x600 the throw velocity (Human_ComputeThrowVelocity) turned the same way. Confirmed (code).
  • While airborne or settling (0x4040000, or body flag 0x800), an object's interval is 2 ticks (0x00395b70): it flies at 30 Hz, like the humans.
  • A thrown object hitting a human is decided in its contact handler, from its own sweep: the hit object is tested as a human (0x00229868) and handled by ThrownObject_HitHumanTest (0x003928d0) and ThrownObject_HitHuman (0x00392b88): damage, reaction and wear are in Objects: throws.

Cars

A car's collision is one body (IPhysics_CreateCarBody) with two boxes from the car pool, built by Car_MakeBodies (0x00387d50) from the type record (0x0057e4c0 + 0x5f0 × type, Cars): box 1 has its half-sizes at record +0x00 and its offset at +0x10, box 2 at +0x20 and +0x30. It sets body flags 0x2211a and clears the car's object flag 0x200. Confirmed (code). The car never sweeps its body: it is moved with the car (pose on the next update) and is what objects and the camera meet. A walking human does not meet it: his layers leave the car's body out, and he meets and climbs the car's shell in the level's mesh (layers, Characters). When a car explodes, box 1 loses 0.25 m of height (Cars). Car_OnLanded (0x00389e08) answers 2 to every contact; on the level it also clears the airborne flag, zeroes the velocity and sends message 0x3f (kind 5) with the point (0, 1.84, 0.02) in the car's frame to the task whose handle is at [0x005971b4] + 0x14: the shared sub_car_damage, which makes a sub_hood_smoke there (Cars: hit effects).

Loose parts (Car_UpdateLoosePart, 0x00387f18, from the car's update for each part flying off, bit in +0x11f4) have no body. Confirmed (code):

  1. The part's lifetime (+0x94, 360 at the start) counts down once per update; below 91 it sends message 0x15 once to the object held in +0x8c; at 0 the part is removed (+0x11f0, and its linked part) and stops flying.
  2. Otherwise, while flying (+0x96 bit 0): vz −= 15.68 × dt (the same gravity as objects), then a box of the part's size (part record +0x10-+0x18, records at type record +0xf0, 0x30 bytes each) at the part's matrix is swept by v × dt (IPhysics_CollideShape, option 0x400).
  3. With no contact the part moves by v × dt. With one it backs off the plane (PhysicsVec_BackOffPlane) and bounces with the restitution at part record +0x20 (PhysicsVec_Bounce). On a floor (normal z > 0.7) its spin stops; on the first floor contact (+0x96 bit 1 not yet set, then set) it starts turning to lie flat by the smallest angle, eased like a settle but with an acceleration of 0.05 (+0x88) from rest (+0x84). Each floor contact is counted (+0x97); the first three, when faster than 2 m/s, play impact sounds at volume 1 / (count + 1). After any contact the velocity is scaled by 0.95.

The car's update runs every tick while a part flies (Task_SetUpdateInterval), so a piece lasts 360 ticks, 6 s (inferred from the interval).

30 Hz or 60 Hz, and online play

What follows is inferred from the code above.

  • Gameplay does not depend on the step. Humans step at 30 Hz, flying objects update at 30 Hz, and hits come from sweeps in those updates. The step only turns landed objects and keeps a broad-phase bound.
  • At a 30 Hz step, the settle must be rescaled to keep its 183 ms: with acceleration 0.08 per step², it ends in 6 steps (200 ms). The bound refresh becomes every 60 steps. Neither changes play.
  • Online, nothing the step holds needs syncing beyond an object's settled state: given the landing normal, a settle is deterministic and cosmetic, so a client can play it locally or take the object's rotation from the server. The state that matters is a flying object's position, velocity and holder, and the humans' state, all outside this step. The physics is the game's own short, fixed sequence of float operations, with no third-party solver to make deterministic.

Reimplementing Physics, RayCast and World

Every function of Physics/ (0x0033c288-0x0034f740) and every function of RayCast/ and World/ that Coney does not cite yet, split into units an implementer can take one at a time, bottom-up. Each unit lists its functions (names and sizes from the shared Ghidra project; "tagged" means docs/progress/functions.toml already lists it), what specifies it, where it goes in Coney and what its tests check. All 19 units are specified exactly, on three pages: geometry kernels (Units 1, 4, 5, 12 and 13), shapes, bodies and moves (2, 3 and 6 to 11) and rays and the world (14 to 19), with their arguments, record offsets, constants, branches, the original's quirks and worked test numbers, all confirmed (code) unless a line says otherwise (read in the disassembly, 2026-10-12). Unit 1's corner contacts and the strike shapes' previous positions are also confirmed (runtime) (Seen at runtime). Where a unit corrected a section of this page, the section was fixed in place; each unit also says what Coney changes.

When this plan was made (2026-10-12, main at f325e018), the tracker had 17 of Physics/'s 183 functions, 12 of RayCast/'s 15 and 33 of World/'s 61. RayCast/'s mesh code (src/raycast/collision_mesh.*) is complete apart from Unit 16, which also changes its ray: it skips excluded triangles before intersecting and returns only a kept hit, so it cannot give the see-through material a clear ray crossed (WorldManager_RayCast); World/'s streaming and drawing are done, and its open functions are lifecycle (Units 17 to 19).

What Coney has instead (the stand-ins these units replace):

  • Units 1 to 3 are done (src/physics/, issue #346): the walking body is the original's swept test and pass loop (Coney's implementation). The airborne body's landing still uses Coney's own segment, not Human_LandingTest's contact (#369).
  • Humans meet humans before walls in a pass (a marked Coney choice); Unit 11 sorts both together (#371).
  • Strikes test only the square's aimed object attack against a car (issue #72); Unit 5 and the strike loop (How Human_TestStrikes builds the sweep) let any punch or kick whose sphere reaches a box hit it.
  • A falling object's move is a ray from its centre (world_objects::LooseObjects); Unit 13 is its box sweep.

Order and dependencies. Units 1, 4, 5, 6, 16 and 17 depend on nothing else here. Then:

Unit Needs Unblocks in Coney
1 Triangle sphere-sweep kernels exact edge and corner contacts for every sphere against the level
2 A shape against the collision mesh 1 the walking sweep's contacts, the corner slide, the airborne push-out
3 Moving a body 2 the pass loop, contact codes, slides, bounces, push-outs, the "could not move" counter
4 Static shape-pair overlaps pick-ups, the push sphere, objects touched by humans
5 A sphere swept against a box strikes on cars (issue #72, step 1) and objects' boxes (issue #300)
6 Shapes posed shapes and bounds for 7 to 14
7 Bodies and the sort-and-sweep world 6 the candidate queries every test starts from
8 Ignore lists and shape pools 7
9 Creating and freeing bodies 7, 8 one body per human, object, car and pane, with the original's flags
10 The human body set 6 to 9 strike shapes on bones, the push sphere
11 Moving shapes against bodies 1, 6, 7 humans and walls in one sorted pass
12 A capsule swept against a box 6 walking into an object's box
13 Box against box and triangle 2, 6 falling and thrown objects' boxes, cars' loose parts
14 Rays against bodies 6, 7 the AI's line tests against bodies
15 The physics world 3, 7, 9 one IPhysics per level, the step and the 64 settle slots
16 RayCast leftovers material names, the world manager's ray
17 Level file and level object the level object's destructor order, the WAD entry size
18 World manager lifecycle 17 one WorldManager owning the level's pools (the budget comes back on unload)
19 Streamed world object lifecycle 18 the two-world visibility marks, a pending atomic release

A unit is done when its functions are tagged in Coney (@orig, functions.toml), its tests pass on synthetic geometry, and the stand-in it replaces is gone or marked.

Floating point. The PS2's FPU and VU0 have no infinities or NaNs: a division by zero gives ±FLT_MAX, and rounding is toward zero (inferred: a hardware fact, not read in these functions). An IEEE implementation matches to about one unit in the last place, so tests compare fractions with a tolerance (1e-5 is ample), and the few edge cases that divide by zero say what the original then does.

Coney's implementation

Coney has Units 1 to 3 (src/physics/) and placed world objects (src/world_objects/), but no IPhysics and no bodies of its own yet. A dropped object's fall, bounces, settle and stop are world_objects::LooseObjects (repo:src/world_objects/loose_objects.h), stepped by the play mode: each 1/30 s step runs the object's update, then two settle ticks, so the settle keeps its 11 ticks. Its move is a ray from the body's centre against the level mesh, as long as the move plus the box's or sphere's reach that way; humans, cars and other objects are not met yet. A thrown object starts the same way with its throw's velocity and spin (World objects).

The world objects in the world have their bodies (world_objects::ObjectBodies, repo:src/world_objects/object_bodies.h): each type's box or sphere at its object's pose, with its PHYFLAG layers, remade each step before the humans move. A walking human's sphere slides along the BLOCKHUMANS ones after the level's walls, as Human_OnContact slides along an object (stand-in: pushed out of the deepest body the move goes into, in the ground plane; the sum of those pushes is then swept through the level's walls as a push-out; the object does not move).

The walking body moves through the original's sweep (Unit 3, physics::sweepBody, repo:src/physics/body_sweep.h) against the level (Unit 2, physics::sweepCapsuleMesh) with the original's kernels (Unit 1): up to three passes, each sweeping the whole slid move from where the body is, the capsule's walking sphere against the walls the move goes into, met at the fraction where it first touches from where the move starts (so a fast move never passes a wall), advanced to 0.01 m short of it along the normal, the move and the velocity slid, the corner slide added on the ground, and the stuck counter (+0x70, Coney's m_blockedUpdates) counted. Human::sweep gives it a per-sweep owner (WalkingOwner, repo:src/human/human.cpp) that answers Human_OnContact's 0x20001 for every level triangle and code 0 for the fences a climb passes; the body has flag 0x400 for a player only, so only a player's slid velocity is written back. While airborne the same sweep takes the move across the ground (no corner slide; a player keeps a quarter of the slid speed, the human contact scale), and the air sphere's push out of the nearest wall (physics::pushOutOfWalls) is added to the pending push for the next move, as Human_PushOutInAir does. What differs, each marked in the code:

  • Coney's choice: the human has no physics body between updates; each sweep builds one from the human's own fields (the pending push is body +0x50, kept with flag 0x1000 since Coney divides it by the push weight when it is added).
  • Coney's reading: the sweep is given the slope-scaled velocity, and the scale is taken off the velocity it writes back (which of the two the original writes back is not on this page).
  • Coney stand-in: a capsule body's query box is its walking sphere's bounds, not the capsule shape's (Unit 6); the visited bits live beside the mesh, not in it.
  • Not yet: the airborne body falls and lands by Coney's own landing segment (Human::moveInAir), not by Human_LandingTest's contact in the sweep (#369; physics::landingContact is written and tested).

The shape-pair code of Unit 4 and Unit 5 is in src/physics/, as functions of posed shapes (physics::Box, Sphere, Capsule, Segment in repo:src/physics/shapes.h) that fill a contact record (physics::Contact, repo:src/physics/contact.h, laid out as the 0xc0-byte record's fields): the static overlaps and their table (repo:src/physics/overlaps.h), the segment and capsule-box kernels (repo:src/physics/segment_kernels.h) and the sphere-box sweep (repo:src/physics/sphere_box_sweep.h), with every quirk the units list kept and pinned by a test. Nothing calls them yet: the strike loop (issue #72, step 1) calls physics::sweepSphereBox with a car's or an object's box (physics::boxAt turns a body's pose and half extents into one), and the overlap query (Unit 7) calls physics::overlapShapes. Coney's choices, each marked in the code:

  • PhysicsOverlap_BoxCapsule writes the capsule-side normal (the box normal negated) into +0x50, which the original leaves holding whatever the stack held.
  • The sphere-box sweep's direction to the box, and the inside case's normal, are (0, 0, 0, 1) for a sphere straight above or below the box centre, where the original normalises a zero vector.
  • The ray roots are computed as C / (D − B), the same root as the original's form, and exact at a tangent where the original divides by zero.
  • PhysicsOverlap_BoxBox keeps the original's wrong matrix (a maintainers' choice in Open questions; no caller pairs two boxes).

Humans meet each other as Human contacts describes (human::sweepCapsules and human::contactPush, repo:src/human/human_contacts.h; Human::meetHumans). The characters' step (human::Humans::update) gives every human the step's list of humans, and a walking human's sweep first meets their upright capsules (0.35 × scale, 2 m tall): the nearest contact stops the capsule there and the rest of the move slides along the other human (code 1), and, unless the mover charges, the one met gets half of the move into him over his push weight (1e9 while in a pair or arrested) as a pending push for his own next move (code 4), unless a script made him unpushable (HuSetPushable(false)). A player's velocity loses its part into the human, as against a wall. Skipped: a human without BLOCKHUMANS (Human::blocksHumans), out of health or out of the world, and the human this one holds or is held by (a grab, tackle, mount or mugging), so a linked pair passes through each other. The flag goes with a knock-out (HuSetConscious(false) or the health running out), a wound and an arrest, and comes back only with a heal (HuRevive, a flash, a freed cuffed human's revive) or the release from an arrest; a knockdown keeps it, as in the original (Human contacts, "Who loses BLOCKHUMANS"). What differs, each marked in the code:

  • Coney's choice: the humans take their part of the move before the walls take theirs, where the original sorts both kinds of contact of a pass by fraction together; a capsule already within reach is met at fraction 0 by a move that goes further in, which matches the original's start case across the ground (Unit 11).
  • Not yet: an airborne body meets no human (a jump's strike and code 1 against a human); a scene's shared group; the pushed human's reaction (Human_OnPushContact's timers and damage).

Open questions

  • Not yet seen at runtime from the units: the capsule sweep's cell-list overrun after a fraction above 1 (Unit 2). (The late and missed corner contacts were looked for at a post, a planter, a kerb and a ledge and not seen: Seen at runtime.)
  • Bits and flags the units read but cannot name: triangle flag bit 3 (0x8), which a sphere sweep skips for a body with flag 0x100 or a query with option 0x8000 (every world object's body starts with 0x100 in 0x80000500, so a flying object's sphere passes those triangles; inferred); how many triangles carry each corner edge in bits 14-15; which states 0x00227f68 selects for the landing segment's raised feet. Body flag 0x1000 is set by PhysicsBody_PushOutOfWalls (0x00347b70); other setters are not traced.
  • Callers not traced: IPhysics_CollideShape's callers Car_UpdateLoosePart, Human_TraceThrowAim and CamPower_PlaceShot (the others are in Unit 11; none seen passes 0x8000); which owner class has type mask 0x200, which makes the capsule sweep skip its vertical test; whether anything but IPhysics_OverlapShape reads the overlap table; whether the camera (the other caller of Sweep_SphereOrientedBox) reads its normals, six of which point the wrong way (The normal tables); how a held weapon's strike sphere reaches Car_OnHit as kind 1 (its hit goes to the held object's contact handler).
  • Names that do not fit (kept, as other engineers gave them): CollTri_ComputeEdgeNormals (0x00343b20) normalises the edges; Ray_InfiniteCylinder (0x0034d938) is a capsule; SphereBox_SweepCorner (0x0034b040) is the face-region step, SphereBox_SweepFaceEdges (0x0034b7b0) the corner-region step and SphereBox_ToBoxSpace (0x0034baf0) goes from the box to the world. The plate comments say what each does.
  • From the implementation of Units 1 to 3 (issue #346): Unit 3's test 13 gives PhysicsVec_Bounce((0, 0, −2), (0, 0, 1), 0.1) as (0, 0, 2.2), but the formula above it (v −= n d (1 + e)) and test 12 (−5 to 0.5) give (0, 0, 0.2); Coney follows the formula. Which velocity PhysicsBody_Sweep writes back to a walking human on a slope: the slope-scaled one Human_Move (0x0023d8c8) hands it, or the unscaled one (Coney takes the scale off). Two guards Coney had to choose: a zero-length edge in Ray_InfiniteCylinder (treated as the sphere at its first end, inferred from 1/0 on the PS2) and a move parallel to an edge's side plane in SphereSweep_VertexRegion (decided by the signs the PS2's ±FLT_MAX crossing would have, inferred).
  • A maintainers' choice: PhysicsOverlap_BoxBox (0x00345dd8) builds its rotation matrix the wrong way round with the centres mixed in, so it misses rotated or distant boxes; no caller pairs two boxes, so reproducing it or writing the correct test changes nothing a player sees (Unit 4).

  • Hangout railing vault (checklist item 46: the vault plays but the player stays put): not explained by the door flags above, which only doors and panes write. The climb's pass-through (materials 30, 31 and 122 ignored while record +0x08 has 0x40, Characters) and the railing triangles' material in level95.lev are where to look next; not traced in this pass. A lead: of the clubhouse's triangles (z between −200 and −185 in level95.lev) only 2 are material 122 and 7 material 31, against 137 of 7 (LAMPOST) and 56 of 3 (STEEL), so a railing built of another material keeps its wall during the vault unless the original passes it some other way.

  • How an object of a type with axis 0 comes to rest: nothing in its contact handler clears airborne, so it would keep bouncing at interval 2 (inferred); the throwable ones (dyn_lawnchair_a, _b) break on their first contact (runtime).