Physics units: shapes, bodies and moves¶
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.
Part of Reimplementing Physics, RayCast and World: the exact specifications of a shape against the collision mesh, moving a body, shapes, bodies and the sort-and-sweep world, ignore lists, creating bodies, the human body set and shapes moved against bodies. The plan, the order of the units and their dependencies are on that page; the subsystem itself is Physics.
Unit 2: A shape against the collision mesh¶
What it is. A shape swept against (or overlapping) the level's collision mesh: the cells, the visited bits, the per-triangle rules, the capsule's wall rules and corner slide, the sort, and the contact each hit becomes.
- Coney:
repo:src/physics/mesh_sweep.h(done): the swept test from the start of the move, which replaced the walking body's old push-out (human::nearestWallPush,slideAlongTouchedFaces,edgeSlide). - Tests: Test ideas, built with
src/raycast/collision_builder.h; test 1 is the fast mover that must not tunnel.
10 functions, 7136 bytes (2 tagged).
| Address | Name | Size |
|---|---|---|
0x00347170 |
CollisionMesh_BeginShapeQuery |
396 |
0x00347300 |
MeshContacts_SortByFraction |
184 |
0x0033d718 |
Contact_InsertSorted |
160 |
0x0033d2d8 |
IPhysics_SweepShapeVsMesh |
92 |
0x00347c08 |
PhysicsMesh_SweepCapsule (tagged) |
2344 |
0x00348a50 |
PhysicsMesh_SweepSphere |
1836 |
0x003473c8 |
PhysicsMeshOverlap_Sphere |
1016 |
0x003473b8 |
PhysicsMeshOverlap_Box |
8 |
0x003473c0 |
PhysicsMeshOverlap_Capsule |
8 |
0x003477c0 |
PhysicsBody_PushOutOfWalls (tagged) |
1092 |
Conventions: n·x is a dot product over x, y, z (the VU sums only those three), a × b a cross product, "unit" means
divided by its length. Vectors are 16-byte float[4]; w is noted only where code sets it.
Shared records¶
The working triangle (T, 0xd0 bytes on the caller's stack)¶
Filled by every mesh function for the triangle under test; the helpers below write into it. Confirmed (code) at
0x00343a90, 0x00343b20, 0x00343c30, 0x00343c90.
| Offset | Content | Written by |
|---|---|---|
+0x00, +0x10, +0x20 |
corners v0, v1, v2 (copied whole from the vertex buffer, w = 1.0) |
the caller |
+0x30, +0x40, +0x50 |
edges e0 = v1 − v0, e1 = v2 − v1, e2 = v0 − v2 (w = 1) |
CollTri_ComputeEdges 0x00343a90 |
| same | with its second argument non-zero, the edges are made unit in place | CollTri_ComputeEdgeNormals 0x00343b20 (the name is wrong: it normalises the edges; no edge normals are made) |
+0x60, +0x64, +0x68 |
the three edge lengths (only when normalised) | 0x00343b20 |
+0x70 |
unit normal unit((v1 − v0) × (v2 − v0)) |
CollTri_ComputeNormal 0x00343c30 |
+0x80 |
plane constant d = n·v0 (a scalar in x) |
the caller, just before the kernel |
+0x90 |
a copy of the normal (capsule sweep only) | the caller |
+0xc0 |
the kernel's result record: +0xc0 hit count (cleared to 0 before the call), +0xc4 fraction |
the kernel |
Flip (CollTri_Flip 0x00343c90): swaps corners 1 and 2 and negates the normal (all four lanes). It does not
recompute d or the edges. Confirmed (code). So after a flip, "edge k" means the edge of the swapped corner order.
The mesh contact (0xb0 bytes)¶
Each mesh function collects hits in a stack array of 100 mesh contacts (17,600 bytes) plus a 100-entry pointer
list; nothing checks the count against 100 (confirmed (code) in all four collectors: 0x003481a0, 0x00348ec0,
0x00347674, 0x00347abc). Fields:
| Offset | Capsule sweep | Sphere sweep | Sphere overlap | Push-out |
|---|---|---|---|---|
+0x00 sort key |
fraction t |
fraction t |
depth r − dist |
squared distance |
+0x04 |
pointer to the 10-byte file triangle | same | same | same |
+0x08 |
not written | flags 0x10 / 0x20 / 0x40 (below) |
0 | 0 |
+0x0c |
not written | signed start distance n·c − d (normal before any flip) |
not written | not written |
+0x10 |
0.0 | not written | not written | not written |
+0x14 |
the kernel's region code (below) | not written | not written | not written |
+0x20…+0xaf |
a copy of T+0x00…T+0x8f: corners at +0x20/+0x30/+0x40, edges at +0x50/+0x60/+0x70, lengths +0x80, normal +0x90, d at +0xa0 |
same | same (key and pointer only otherwise) | same |
Confirmed (code) at the cited stores. The edges in the copy are whatever T held at the copy: for both sweeps that
is the un-normalised set (CollTri_ComputeEdges(T, 0) runs again just before the kernel, 0x00348118,
0x00348e18), while the lengths at +0x80 are left from the capsule's earlier normalising call.
Region code (capsule only, the kernel's fifth argument): bit i is set when the sphere's centre, taken at the
point where it meets the plane, lies on the inner side of edge i, i.e. (e_i × n)·p ≤ (e_i × n)·v_i
(0x0034ee60, Unit 1). 7 means inside all three edges (a face hit). Confirmed (code).
Mapping into the 0xc0 contact¶
Every sweep and overlap writes its output straight into a caller-supplied array of 0xc0 contacts (layout in
physics.md #contacts), one per kept mesh contact, in the order given under each function. Fields a mesh function
leaves alone are filled by the caller through Contact_InsertSorted (+0x00 own owner, +0x40 other owner = 0).
Common to the three that write contacts, confirmed (code):
| 0xc0 field | Value |
|---|---|
+0x04 own shape |
the querying shape |
+0x44 other shape |
0 (the level) |
+0x50 normal |
mesh contact +0x90 (unit, turned to the shape when flipped) |
+0x80 fraction |
mesh contact +0x00 (for the overlap this is the depth, not a fraction) |
+0xb4 triangle |
mesh contact +0x04 |
Per function: the sweeps add +0x10 = unit move, +0x90 = contact point = t × move + the shape's position
(shape vtable +0x24, which for the sphere 0x004f1210 and capsule 0x004f1248 returns &shape+0x50; for the
capsule that is the feet, not the touching point; w = 1); the capsule adds +0xa0 slide vector, +0xb0 = 0 or 1
and +0x84 = 0.0; the sphere adds +0xb0 = its mesh +0x08 flags and +0x88 = mesh +0x0c; the overlap writes
+0xb0 = 0 and nothing else.
The contact list and its cap of 50¶
The callers own the list. PhysicsBody_Sweep (0x0033e278) keeps a pool of exactly 50 contacts (sp+0x110 to
sp+0x2690), a 50-pointer list and a count, reset to 0 at the start of each pass (0x0033e4a4).
IPhysics_CollideShape (0x0033e7f0) and IPhysics_OverlapShape (0x0033ebb8) pass the mesh function the room
left, 50 − count (0x0033ea90, 0x0033ee8c). The cap is honoured by the sphere sweep and the sphere overlap and
ignored by the capsule sweep (it never reads that argument). Contact_InsertSorted itself does not check it.
Confirmed (code).
0x00347170 CollisionMesh_BeginShapeQuery¶
Size 0x18c. BeginShapeQuery(&mesh, &cellMin, &cellMax, &worldBox); returns nothing. Confirmed (code) unless marked.
- Mesh. Writes the level's collision mesh (world manager global
0x005147c4→+0x40level object →+4) to*mesh. - Visited bits. Zeroes the mesh's checked bit set (
+0x94),(((count + 7) >> 3) + 15) & 0x7ff0bytes forcount=+0x84, 16 bytes at a time. Every collector then tests and sets bitidx & 7of byteidx >> 3before looking at triangleidx, so a triangle listed in several cells is looked at once per query. The bit set is shared mesh state: two queries cannot be interleaved (inferred: nothing in this unit nests them). - Cells.
worldBoxis{min, max}(two vectors). Each corner is transformed separately by the header's world-to-grid matrix (g = x·r0 + y·r1 + z·r2 + r3, header+0x10…+0x4f), itswset to 0, clamped per axis to[header+0x50, header+0x60](that is[0.5, n − 0.5]on the disc) and truncated toward zero (vftoi0).cellMin/cellMaxreceive the integer cell coordinates (x, y, z). Because the corners are transformed one by one, a negative scale would swap them; every disc matrix is a positive diagonal scale plus a translation (collision.md #header), socellMin ≤ cellMaxholds (inferred from the disc survey). - The cell walk (in each caller, confirmed (code) in all four): x from
cellMin.xtocellMax.xinclusive (outermost), then y, then z (innermost), unsigned compares; cell entry(z·ny + y)·nx + xof the grid (collision.md #grid); an entry of 0, or a list whose count is 0, is skipped; the list is walked in stored order. So triangles are met in (x, y, z, list) order, which only matters through the sort's tie rule below.
0x00347300 MeshContacts_SortByFraction¶
Size 0xb8. SortByFraction(pointerList, meshContacts, count); fills pointerList[0..count−1] with pointers into
the 0xb0 array, ordered by the float at +0x00. Confirmed (code).
Algorithm: insertion sort in arrival order. For contact i, scan the list from the front for the first j whose key
is strictly greater than contact i's; place i there, else append. Equal keys therefore keep arrival order.
Quirk, faithful behaviour (confirmed (code) at 0x00347360-0x00347374): the shift that makes room copies
forwards (list[j+1] ← list[j], then reads the slot it has just written), so instead of moving list[j..i−1] up
by one it writes the old list[j] into every slot j+1..i (and one past). Effect:
list[0]is always the true minimum, and the list stays non-decreasing;- each insertion before the end throws away the entries that were after
jand replaces them with copies of the oldlist[j].
Example: keys arriving 0.5 (A), 0.6 (B), 0.3 (C) end as [C, A, A]; B is lost. A correct sort would give [C, A, B].
Who sees it: only callers that use more than the first entry, i.e. the capsule's corner-slide loop and the contacts
IPhysics_CollideShape passes on (duplicates are inserted twice), and the overlap's choice of contact (below). The
walking sweep (PhysicsBody_Sweep) and the push-out use only the first entry, so for them the sort is exact.
0x0033d718 Contact_InsertSorted¶
Size 0xa0. InsertSorted(phys, contact, list, &count, ownOwner, otherOwner, ownShape, otherShape); returns
nothing. Confirmed (code).
- Writes
+0x00=ownOwner,+0x40=otherOwner,+0x04=ownShape,+0x44=otherShape. - Finds the first list position whose contact has a fraction (
+0x80) strictly greater than the new one's, moves the tail up by one (copying backwards, correctly), and stores the pointer there; else appends. count += 1. No check against 50 (the callers keep to it, above). Ties: the new contact goes after existing contacts of equal fraction.
0x0033d2d8 IPhysics_SweepShapeVsMesh¶
Size 0x5c. Called only by IPhysics_SweepBodyVsMesh (0x0033d340, pass step 2 of physics.md #sweep). Returns
true when the type's mesh sweep returned a non-zero count. Confirmed (code).
It picks the mesh sweep by the shape's type byte (shape +0x30) from IPhysics +0xe0 + 4·type (none → false) and
calls it with:
| Argument | Value |
|---|---|
| out | the pool slot at the current count (pool + count·0xc0) |
| max contacts | 1 |
| world box | the pass's box: the shape's box at the start of the move, grown by the move, not scaled by body +0x60 (Unit 3, step 5.2) |
| body, shape | the body and its first shape |
| move | the pass's move (v × dt) |
| options | 4 when the body's type mask (+0x44) has 0x40 (a human), else 0 ((mask >> 4) & 4) |
minimum n.z (f12) |
−0.65 (0xbf266666) |
maximum n.z (f13) |
0.0 (read only with option 8, never set here) |
0x0033d340 then inserts only the first returned contact (Contact_InsertSorted with owner = body +0x34, other
owner 0, own shape, other shape 0; 0x0033d3ac-0x0033d3d4). So in the walking sweep each pass meets at most one
mesh contact, the nearest. For the capsule this is fine (its first entry is exact, and the slide vector it carries
was built from all of them). The −0.65 threshold and option 4 are ignored by the capsule sweep; for a sphere body
they mean "skip ceilings steeper than n.z < −0.65" for humans only. The landing segment of an airborne human is added
after this call (physics.md #sweep), not here.
0x00347c08 PhysicsMesh_SweepCapsule¶
Size 0x928. Mesh sweep table +0xe0, type 3. Register arguments: out (0xc0 array), max (ignored), worldBox,
body, shape, move (pointer); f12, f13 and the options word are never read (confirmed (code): no use of
a1, t2, f12 or f13 in 0x00347c08-0x0034852c). Returns the number of contacts written, 0 if none. All
steps confirmed (code) at the cited addresses.
The sphere (as characters.md #walls): r = shape+0x40 × body+0x60 × Human_GetScale(body+0x34)
(0x00347c98-0x00347cbc); centre c = shape +0x50 (the feet at the start of the move) with z += r + 0.05
(0.05 = 0x3d4ccccd). The kernel is given the record {c, r}.
Per triangle, in cell-walk order, once per query (visited bit):
- Skip unless flag bit 0 (enabled) is set (
0x00347de0). No type-bit mask, no0x800rule. - Load the corners, compute the unit normal
n. - Wall: skip if
|n.z| > 0.65(0x3f266666,0x00347e60; so exactly 0.65 is a wall). - Side:
s = n·c − n·v0. Ifs < 1e-6(0x358637bd): a one-sided triangle (flag0x2clear) is skipped; a two-sided one is flipped (0x00347ec0-0x00347ee4). A centre exactly on the plane counts as behind. - Into the face: skip if
n·move ≥ −0.001(0xba83126f,0x00347f2c), using the possibly flipped normal. A zero move therefore never yields a contact. - Low: normalise the edges (lengths kept). Take the edge
kwith the largest|unit e_k.z|(first one wins a tie). Skip if|v_{k+1}.z − v_k.z| < 0.25(0x3e800000,0x00347fc4), corners indexed mod 3. - Thin: take the longest edge
m(first one wins a tie). If|unit e_m.z| < 0.8(0x3f4ccccd): withp = n × unit e_m(a unit vector in the plane, across the edge), skip if|p·v_{(m+2) mod 3} − p·v_m| < 0.25, i.e. the third corner is less than 0.25 from the edge's line (0x00348060-0x003480d4). A nearly vertical longest edge skips this test. - Store
d = n·v0, recompute the edges un-normalised, clear the result count and the region, and callSweep_SphereTriangle(result, {c, r}, T, move, ®ion)(0x0034ee60). - Keep the hit when the kernel returns non-zero and
0 ≤ t ≤ 1(t= result+4): append a mesh contact (t, triangle pointer,+0x10= 0,+0x14= region, the copy ofT).
Bug (confirmed (code) at 0x00348170): a hit with t > 1 leaves the cell's loop counter un-incremented while
the list pointer moves on, so that cell's walk reads one u16 past the end of its list and treats it as a triangle
index (subject to the visited bit and the tests). The kernel's face path refuses t > 1, but its edge steps can return
a fraction above 1
(Unit 1), so the overrun is reachable at a triangle's edges and corners
(inferred: Unit
1 cases 12
and 13 return 1 with a fraction above 1; not seen at runtime). An implementation should not
read past the list; note the difference in a comment.
After the walk (none kept → return 0):
- Sort (
0x00347300, with its smear). - Corner slide (
0x00348244-0x00348374). StartS = (0, 0, 0)andflag = 0. For each entry of the sorted list, in order (duplicates included):- skip a face hit (region 7);
- skip if the body's owner (body
+0x34, state flags through owner vtable+0x54) has0x4000000(airborne); k= triangle flags>> 14(bits 14-15, read from the file triangle, not the flipped copy); skip ifk= 3;- skip if region bit
kis set (the centre was inside edgek); - otherwise
t_h = n × (0, 0, 1) = (n.y, −n.x, 0)with the contact's normal; if edgek'sz(mesh contact+0x58 + 16k, the un-normalised edge of the possibly flipped order) is > 0,S −= t_h, elseS += t_h;flag = 1.
S.z = 0;S = unit(S) × max(r, |move|)(0x00348378-0x003483f8). With no corner contactSis zero; the VU's reciprocal square root of 0 gives the largest float and0 × that= 0, soSstays zero on the PS2. In C++ guard the zero length explicitly (inferred: the VU result is the documentedRSQRToverflow behaviour).- Output (
0x00348404-0x003484ec), for each sorted entryiintoout[i]: the common fields,+0x10unit move,+0x90point,+0xa0 = S(the same for all),+0xb0 = flag,+0x84 = 0.0. Returns the count (all of them;maxis not applied).
What the corner slide is. Bits 14-15 name one edge of the triangle (0, 1 or 2; 3 = none). When the swept sphere
meets the triangle beyond that edge (not on its face) and the body is on the ground, S is a horizontal unit
direction along the wall, chosen by the edge's direction: worked through for a vertical wall whose front faces the
viewer, an edge running down is the wall's left end and gives "left", an edge running up the right end and gives
"right", so S always points outward past the flagged end. So it eases a walker round an outside corner instead
of stopping on the edge. The arithmetic is confirmed (code); the purpose is inferred. Which faces carry it on the disc
is not counted here (characters.md #slopes notes gravel faces with 3).
Who consumes it (PhysicsBody_ResolveContacts 0x0033d9d8, confirmed (code)): for a contact whose handler
answered a non-zero code, +0xb0 is read (0x0033e108).
+0xb0 & 1(the corner slide): unless the body is a human in a pair or block (Human_IsInPairOrBlock0x00227fd8) or the body's flags+0x40have0x8000, takeSfrom+0xa0, remove from it the part that points back against the move (Vec_RemoveNormalPart(S, unit move)0x0033d870: only whenS·m < 0) and add it to the pass velocity (the vector the next pass's move is made from asvelocity × dt,PhysicsBody_Sweep'ssp+0x40),0x0033e148-0x0033e178. So the next pass (there are at most three) moves an extraS × dtsideways; the velocity reported back to the owner (sp+0x50) does not get it. In this branch the0x20000contact-scale rule is not applied.- Otherwise
+0xb0 & 2is the other slide (store+0xa0once per move in body+0x50), and0x20000the scale. (physics.md #contacts calls this "bit 2"; it is the value 2, bit 1.)
Since the body sweep inserts only the first contact, it is that contact's +0xa0/+0xb0 that is read; they hold the
S and flag computed from all contacts.
How it differs from Coney's stand-in. The test is swept from the start position (c at the start, t the
fraction of the move where the sphere first touches), the side is decided before the move, and only faces the
move goes into count. A body moving fast enough to cross a wall within one update still meets it at the fraction
where it touches, so there is no tunnelling; a body that starts behind a one-sided wall passes through it.
0x00348a50 PhysicsMesh_SweepSphere¶
Size 0x72c. Mesh sweep table +0xe0, type 2. Arguments: out, max, worldBox, body (may be read for its
flags), shape, move, options, f12 = minimum n.z, f13 = maximum n.z. Returns the number of contacts
written. Confirmed (code) at the cited addresses.
The sphere: centre = shape +0x50 exactly (no raise), radius = shape +0x40 (no body or human scale).
Per triangle (same walk and visited bits; the loop counter is correct here):
- Bit 3 (
0x0008, within the low 12 bits): if set, skip when the body's flags+0x40have0x100or the options have0x8000(0x00348c34-0x00348c54). Meaning of bit 3 and of body flag0x100: not traced. - Skip unless enabled (bit 0 of
flags & 0xfff). - Normal. Option
4: skip ifn.z < min; option8: skip ifn.z > max(0x00348cd4,0x00348cf8). No fixed wall rule, no 0.25 m rules, no corner slide. - Side:
s = n·c − n·v0(kept for+0x0c). Ifs < 1e-6: two-sided → flip; one-sided → skip, unless option0x40, in which case it is kept with mesh flag0x10(the normal is not flipped). - Into the face: if
n·move ≥ −0.001: skip, unless option0x40, which keeps it and ORs0x40whenn·move ≤ 0.001(0x3a83126f, moving along the face) or0x20otherwise (moving away). d = n·v0, edges un-normalised, kernel0x0034ee40(the same kernel without the region output) with{c, r},T,move. Keep it when it returns non-zero and0 ≤ t ≤ 1: mesh contact witht, triangle,+0x08flags,+0x0c=s, copy ofT.
After the walk:
- Sort (with its smear); then
count = min(count, max)(0x00348f6c). - Option
0x20000, one contact per plane (0x00348f84-0x00349044): for each kept entryiin order, scan the entries after it; when|n_i · n_j| > 0.9(0x3f666666) swap entryjwith the last entry and drop the last (count −= 1), then move on toj + 1without testing the entry just swapped intoj. The order after removals is therefore no longer sorted. The cap tomaxhappens first, so this can leave fewer thanmaxeven when more were found. - Output in list order: the common fields,
+0x10unit move,+0x90point,+0xb0= the mesh flags,+0x88= the start distances. No+0xa0slide vector, no+0x84.
Differences from the capsule, in one list: the sphere is the shape's own (no +0.05 raise, no body or human
scale); walls are not singled out (the caller's n.z range does it, if asked); no low/thin rules; no corner slide;
the max argument is honoured; options 0x40 (keep backfaces and receding faces, flagged), 0x8000 and 0x20000
exist; bit-3 triangles can be excluded. Reached through the walking sweep for a type-2 body (max 1, options 4 for a
human type mask else 0, min n.z −0.65) and through IPhysics_CollideShape with the query's own options and n.z
range (below).
The query record of IPhysics_CollideShape / IPhysics_OverlapShape (the fields the mesh path reads,
confirmed (code) at 0x0033e834-0x0033e8dc, 0x0033ea64-0x0033eab4, 0x0033ee48-0x0033eea0):
| Offset | Meaning |
|---|---|
+0x00 |
the body (passed as body; its owner +0x34 becomes contact +0x00) |
+0x04 |
the shape (its type byte +0x30 picks the mesh function) |
+0x08 |
a pointer to a box node (three quadwords copied, 0x0033e84c-0x0033e868), used for the box when option 0x400; the record also has +0x0c, +0x14, +0x1c, +0x20, +0x24 and +0x38 (Unit 11) |
+0x10 |
options: 1 test the mesh, 2 the bodies; the whole word is passed on, so 4, 8, 0x40, 0x8000, 0x20000 reach the mesh function |
+0x18 |
the most contacts to copy out |
+0x28, +0x2c |
minimum and maximum n.z (f12, f13) |
CollideShape inserts every returned contact with Contact_InsertSorted (duplicates from the smear included);
OverlapShape adds the returned value to its count without re-sorting.
0x003473c8 PhysicsMeshOverlap_Sphere¶
Size 0x3f8. Mesh overlap table +0xf4, type 2. Arguments: out, room, worldBox, (an unused fourth), shape,
options, f12 min n.z, f13 max n.z. Confirmed (code).
- Sphere: centre shape
+0x50, radius shape+0x40. - Per triangle (walk, visited bits): enabled (raw bit 0); normal; option
4/8as for the sweep (0x003475f8,0x0034761c). No side test and no flip: one-sided faces are met from behind too, and the normal is the file's. SphereTriangle_Penetration(0x0034edd0): kept when the point-triangle distance² <r², key = depthr − distance.- Sort ascending by depth (shallowest first, with the smear);
n = min(count, room); write the kept entries in reverse, soout[0]is the deepest of thenshallowest; each with+0x04shape,+0x440,+0x50normal,+0x80depth,+0xb00,+0xb4triangle. - Returns 1 when anything was found, 0 otherwise, not the count (
0x0034770c-0x0034777c).IPhysics_OverlapShapeadds the return to its count (0x0033eea8), so onlyout[0]is ever seen. In effect the overlap reports one contact: the deepest penetration when there are no more thanroom(inferred from the two facts above).
0x003473b8 and 0x003473c0: the box and capsule mesh overlaps¶
8 bytes each: return 0. A box or a capsule never overlaps the level mesh; IPhysics_OverlapShape with such a shape
gets no mesh contacts. Confirmed (code).
0x003477c0 PhysicsBody_PushOutOfWalls¶
Size 0x444. PushOutOfWalls(phys, body, sphere) with sphere = {centre, radius at +0x10}; returns nothing.
Caller: Human_PushOutInAir (0x0021a490) only (characters.md #slopes). Confirmed (code).
- Box
centre ± r(AABB_FromSphere0x00341660), cells as above. - Per triangle: enabled; normal; wall
|n.z| ≤ 0.65; siden·centre − n·v0 < 1e-6→ one-sided skipped, two-sided flipped; thendist² = PointTriangle_DistanceSq(centre, T)(closest point on the whole triangle, edges and corners included); kept whendist² < r², keydist². - Sort; take the nearest only (
list[0], exact despite the smear). - Push =
(n.x, n.y, 0) × (r − sqrt(dist²))with that triangle's (possibly flipped) normal: horizontal only,n.zis zeroed first (0x00347b74), so the push is shorter thanr − diston a sloped wall. Added to the body's pending push-out (body+0x50), and body flag0x1000set (0x00347b70), which makesPhysicsBody_Sweepapply the push-out without dividing it by the push weight (0x0033e278, the0x1000test). - The push is along the face normal even when the closest point is on an edge or corner (so its length is
r − dist, not the depth alongn). No contacts are produced; nothing happens with no wall in reach.
Compared with CollisionMesh_SpherePush (collision.md #sphere-push): 0.65 instead of cos 15°, one nearest wall
instead of the average of all, edge and corner distances count, and the push is horizontal and deferred to the next
sweep instead of moving the centre.
Test ideas (synthetic meshes, src/raycast/collision_builder.h)¶
All with one-sided triangles unless stated, flags given as the file stores them, cell size 2 m.
- Wall stops a fast mover (no tunnelling). Wall: two triangles in the plane
x = 0,y−5..5,z0..3, front facing −x. Capsule body,r= 0.5, feet at (−2, 0, 0), move (+10, 0, 0) in one call. Expect 1+ contacts, the first att = (2 − 0.5)/10 = 0.15, normal (−1, 0, 0), point (−0.5, 0, 0). Stand-in today: the body ends beyond. - Starting behind a one-sided wall passes. Same wall, feet at (+1, 0, 0), move (−3, 0, 0): no contact. Set flag
0x2: one contact, normal (+1, 0, 0),t = (1 − 0.5)/3. - Moving away or along. Feet at (−2, 0, 0), move (−1, 0, 0) or (0, 1, 0): no contact (
n·move ≥ −0.001). Zero move: no contact. - Low rule. A vertical face 0.24 m tall (steepest-edge rise 0.24): no contact; 0.26 m: contact. Rise exactly 0.25:
contact (
< 0.25skips). - Thin rule. A vertical sliver: longest edge horizontal 4 m, third corner 0.2 m above it and its steepest edge
rising 0.3 m (so the low rule passes): skipped. Same with the third corner 0.3 m off the line: kept. A triangle
whose longest edge is vertical (
|unit z| ≥ 0.8) is kept however thin. - Wall threshold.
n.z= 0.65 is tested, 0.66 is not (capsule). - Side uses the start. A wall the sphere's centre is 1e-7 in front of (
s < 1e-6) counts as behind: one-sided skipped, two-sided flipped (normal reversed in the contact). - Disabled triangle. Clear bit 0 (via
CollisionMesh_SetEnabledInBox's equivalent): no contact. - Cells and visited bits. A wall spanning four cells is reported once (one contact per triangle). A box whose max is past the grid clamps to the last cell.
- Sort. Keys arriving 0.5, 0.6, 0.3 come out
[0.3, 0.5, 0.5](the smear); keys 0.3, 0.3 keep arrival order.Contact_InsertSortedof 0.2, 0.4, then 0.4 puts the new 0.4 after the old one; of 0.4 then 0.2 gives[0.2, 0.4]. - Corner slide. Vertical triangle
v0 = (0, 0, 0),v1 = (0, 1, 0),v2 = (0, 0, 1)(normal +x; edge 2 runs down the wall'sy = 0end), flagsbit0 | (2 << 14); it passes the low rule (edge 2 rises 1 m) and the thin rule (longest edge 1 not steep,v00.71 m from it). Grounded capsule body,r= 0.5, feet atx> 0.5, moving −x so that it meets the triangle'sy = 0edge fromy < 0(centre path aty = −0.3): one contact, region without bit 2,S = (0, −1, 0) × max(0.5, |move|),+0xb0 = 1. Same with the owner airborne, or bits 14-15 = 3, or a face hit (centre path aty = 0.5):S = 0,+0xb0 = 0. The mirrored end (an up-running flagged edge at the other end) gives+y. - Corner slide consumer. In a full sweep pass, the pass velocity gains
S(after removing any part against the move) and the reported velocity does not; a body with flag0x8000gains nothing. - Sphere sweep options. Same wall, sphere shape:
max= 1 keeps only the nearest of two parallel walls; option0x40with the sphere behind keeps the face with+0xb0 = 0x10; option0x20000with two coplanar triangles hit keeps one; option 4 with min −0.65 drops a ceiling (n.z= −1). - Overlap. A sphere of
r0.5 at 0.3 from one wall and 0.1 from another, room ≥ 2: the call returns 1 andout[0]is the 0.4-deep wall. One-sided wall approached from behind: still found. - Push-out. Sphere
r0.5 at 0.3 m in front of a vertical wall: body+0x50gains0.2 × n, flag0x1000set. Two walls at 0.3 and 0.2: only the 0.2 one pushes (by 0.3). A wall sloped ton.z= 0.6: the push is(n.x, n.y, 0) × (r − dist), shorter thanr − dist. Sphere behind a one-sided wall: no push.
Unit 3: Moving a body: sweep passes and contacts¶
What it is. PhysicsBody_Sweep's up-to-three passes and PhysicsBody_ResolveContacts' contact codes: where the
body is put, what is left of the move, how velocities slide, bounce and are written back, the push-outs, and the
airborne human's landing segment.
- Coney:
repo:src/physics/body_sweep.h(done), whichHuman::sweep(src/human/human.cpp) runs. The loose objects' bounce (src/world_objects/loose_objects.*) is still its own (Unit 13). - Tests: Test ideas, with a fake owner that answers fixed codes.
10 functions, 5148 bytes (4 tagged).
| Address | Name | Size |
|---|---|---|
0x0033e278 |
PhysicsBody_Sweep (tagged) |
1396 |
0x0033d9d8 |
PhysicsBody_ResolveContacts (tagged) |
2208 |
0x0033d7b8 |
PhysicsVec_BackOffPlane |
180 |
0x0033d870 |
Vec_RemoveNormalPart (tagged) |
128 |
0x0033d8f0 |
PhysicsVec_Bounce (tagged) |
228 |
0x0033d340 |
IPhysics_SweepBodyVsMesh |
340 |
0x003414c0 |
AABB_ScaleAboutCentre |
372 |
0x00341840 |
AABB_ExtendByMove |
184 |
0x00341e18 |
PhysicsBody_GetContactScale |
40 |
0x00342288 |
PhysicsHumanBody_GetContactScale |
72 |
Notation: vectors are 4 floats; "xyz" operations leave w alone unless said. a·b is the xyz dot product.
Every "w = 1" below is the code writing 1.0 into w (it uses the VU constant register for it).
Shared records and calls¶
Owner vtable slots. GCC 2.x vtables: each slot is 8 bytes, a 16-bit this adjustment then the function
pointer. The pages so far name a slot by either offset; this table gives both. The functions are the human's
(vtable 0x0053f088). Confirmed (code): the slot offsets at the call sites cited, the human's entries read from
the vtable.
| Slot (adjust / function) | What the sweep uses it for | Human's function | Called at |
|---|---|---|---|
+0x20 / +0x24 |
type mask (bit 0x40 a human) |
0x0033e070, 0x0033e090 |
|
+0x50 / +0x54 |
the owner's flags; for a human the 32-bit object flags word +0x54, bit 0x4000000 airborne |
0x004f4780 |
0x0033d3f4, 0x0033e0bc, 0x00342288 |
+0x70 / +0x74 |
set velocity (owner, &vec4) |
0x0023cf00 |
0x0033e6b0, 0x0033e760 |
+0x98 / +0x9c |
get the cached transform (sret, 32 bytes: position, rotation): the copy the owner took at the start of its update (human +0x10 / +0x20) |
0x004ed860 |
0x0033e3e0 |
+0xa8 / +0xac |
get the transform (sret, 32 bytes) from the authoritative table 0x00714b00 |
0x004ed828 |
0x0033e5ec, 0x0033dd4c |
+0xb0 / +0xb4 |
set the transform (owner, &transform32): position and rotation; the human also refreshes its cached copy while airborne and re-poses its task |
0x0021b048 |
0x0033e668, 0x0033ddac |
+0xd0 / +0xd4 |
attribute (owner, n) → float: 1 weight, 7 push weight, 8 restitution (Attributes) |
0x00221260 |
0x0033e314, 0x0033dbec, 0x0033def4 |
+0xf0 / +0xf4 |
get the physics body (0: none) | 0x0021cf78 (returns human +0x1a0) |
0x0033dac8, 0x0033db70 |
+0xf8 / +0xfc |
contact handler (owner, contact) → answer code |
Human_OnContact 0x00219d50 |
0x0033daec |
+0x118 / +0x11c |
after the move (owner) |
Human_PushOutInAir 0x0021a490 |
0x0033e7b0 |
So the "owner that does not want them" of the current Contacts text is wrong: slot +0xf0
returns the other owner's body, and a contact whose other owner has no body is skipped. Confirmed (code) at
0x0033dac8 (the skip) and 0x0033db70-0x0033db78 (the same call's result read as a body: flags +0x40).
Body vtable slots used here (body vtable at body +0x74): +0x28 / +0x2c first shape (called with extra
arguments (1, 3, 0) or (0, 0, 0) that PhysicsBody_GetShapes 0x00341ae0 ignores: it returns +0x30);
+0x48 / +0x4c contact scale (body, contact); +0x50 / +0x54 "you were pushed" (otherBody, contact,
&push, &move). Shape vtable (shape +0x34): +0x28 / +0x2c pose (shape, owner). Confirmed (code) at
0x0033e404, 0x0033e1d8, 0x0033dcb8, 0x0033e670.
Body fields this unit reads or writes (Bodies): +0x34 owner, +0x40 flags, +0x44
type mask, +0x50 pending push-out (vec4), +0x60 scale (vec3), +0x70 "could not move" counter, +0x74 vtable.
Body flags read here: 0x1 sweep against the bodies too, 0x400 write the slid velocity back, 0x1000 do not
divide the push-out by the push weight, 0x8000 no corner slide, 0x80000000 dirty (set). Confirmed (code) at the
addresses in the functions below.
Contact fields used by this unit (0xc0 bytes, Contacts):
| Offset | Meaning | Read / written here |
|---|---|---|
+0x00 |
own owner | written by the insert; read for attributes and the type mask |
+0x04 |
own shape | re-posed after the body is moved |
+0x40 |
other owner, 0 for the level | |
+0x44 |
other shape, 0 for the level | the generic contact scale tests it |
+0x50 |
unit normal, pointing out of the surface towards the mover | inferred from the code 4 push direction and the mesh sweep's "move goes into the face" test; confirmed (code) that the handlers treat it so |
+0x80 |
fraction f of this pass's move at the contact |
|
+0xa0 |
slide vector (a displacement) | |
+0xb0 |
contact flags: 0x1 add the slide to the move, 0x2 store the slide as the next push-out, 0x80 a human's landing |
|
+0xb4 |
mesh triangle (10-byte record, flags u16 at +0x06), 0 for a body |
The move state (locals of the sweep, one set per call, kept across the passes). Confirmed (code) at
0x0033e3c0-0x0033e418:
| Name | Start value | Meaning |
|---|---|---|
V |
v + push/dt/k |
the move velocity: what the passes move by; slid, bounced, gets corner slides |
v |
the caller's velocity | the real velocity: slid and bounced like V, written back to the owner |
moveFlags |
0 | 0x1 a slide was stored in +0x50 this move; 0x2 write v back; 0x4 a contact scale applies |
scale |
0.0 | the largest contact scale met (with 0x4) |
passes |
0 | passes done, 0 to 3 |
found |
0 | contacts found over all passes (all of them, including skipped and code 0 ones) |
last |
0 | the last resolution's return |
PhysicsBody_Sweep¶
0x0033e278, 0x574 bytes. PhysicsBody_Sweep(float dt, IPhysics* phys, Body* body, const vec4* v, vec4* out);
returns nothing. v is only read; the velocity goes back through the owner. out receives the displacement
(sometimes not written at all, below). Confirmed (code) for every step unless marked.
- Move velocity (
0x0033e2bc-0x0033e364).p = body+0x50 × (1/dt)(xyz;p.wkeeps+0x5c). Unless body flag0x1000is set,p = p × (1 / owner.attribute(7))(0x0033e314; the attribute call happens even when the result is not needed later).V = v + p(xyz,w = 1). The real velocityvstarts as the caller'sv, unchanged. - Clear the push-out (
0x0033e3a0): body+0x50 = (0, 0, 0, 1)(the constant at0x005116c0), always, before the next test. So a push-out is used once, by the next sweep after it was added, even if that sweep does not move. - Too slow to move (
0x0033e39c): if|V|² ≤ 1e-6(0x358637bd), return at once: nothing else happens (no dirty flag, no velocity write,outnot written, no+0x11ccall,+0x70unchanged). - Set up (
0x0033e3ac-0x0033e454): set body flag0x80000000(dirty).start= the owner's cached transform (slot+0x98).shape= the body's first shape (body slot+0x28). If the shape is not enabled (shape flags+0x32bit0x1clear), skip the passes and go to step 6 withpasses = 0,found = 0(0x0033e428): the body then moves freely byV × dt. Elsebox= the shape's world AABB (PhysicsBody_ComputeBounds0x00341638), computed once at the start position and not moved between passes. - The passes (loop at
0x0033e470-0x0033e5bc). Each pass:D = V × dt(xyz;dtis the caller's, never reduced: the address ofdtis handed to the resolution, which never reads it). If|D|² ≤ 1e-6, setpasses = 3and leave the loop (0x0033e4b8,0x0033e694).- Two boxes from
box(AABB + the node fields+0x20):qbox = AABB_ExtendByMove(box, D); andsbox = AABB_ExtendByMove(AABB_ScaleAboutCentre(box, body+0x60), D).sboxis never passed on: both queries getqbox(0x0033e518,0x0033e550), and their box readers read only+0x00-+0x18(0x00347170,0x0033feb0). So body+0x60has no effect on which triangles or bodies are tested here (it does widen the capsule's mesh sphere, Characters). Confirmed (code). count = 0;IPhysics_SweepBodyVsMesh(phys, &qbox, body, shape, contacts, sorted, &count, &D)(below); then, if body flag0x1is set, the body sweep0x0033d498with the same arguments (Unit 11: it adds its contacts to the same sorted list).last = PhysicsBody_ResolveContacts(phys, body, sorted, count, &D, &V, &v, &dt, &moveFlags, &scale);found += count.- If
last ≤ 0, leave the loop. Elsepasses += 1; ifpassesis now 3, leave the loop; else next pass.
The contact array holds 50 contacts (0xc0 each) and the sorted list 50 pointers, fresh for each pass (the count
restarts at 0). Confirmed (code) for the layout (sp+0x110, sp+0x2690).
6. After the passes (0x0033e5c4):
- last < 0: return at once (0x0033e5c4): no final move, no velocity write, out not written, no
+0x11c call, +0x70 unchanged. Whatever the aborting contact's code did (it may have moved the body and
changed velocities, and +0x50) stays.
- (Body or owner null: return; the start already called the owner, so this is not reachable in practice.)
- T = the owner's transform (slot +0xa8), which includes what the resolutions moved.
- Moved when passes < 3 or found == 0 (0x0033e5f4-0x0033e604): T.position += V × dt (xyz,
w = 1), set the transform (slot +0xb0; the rotation is the one just read), re-pose the shape (shape slot
+0x28 with the owner), body +0x70 = 0 (0x0033e68c).
- Stuck otherwise (passes == 3 and at least one contact was found): the body is not moved by the rest
(it stays where the last resolution left it); v.x = v.y = 0 (v.z kept) and the owner's velocity is set to
v (slot +0x70) whatever the body flags (0x0033e698-0x0033e6b0); body +0x70 += 1.
- Contact scale (0x0033e6c4): if moveFlags & 0x4: v.x *= scale, v.y *= scale, and v.z *= scale only
when v.z > 0; the same on V (dead afterwards).
- Write-back (0x0033e734): if body flag 0x400 and moveFlags & 0x2, set the owner's velocity to v.
- *out = T.position − start.position (xyz, w = 1) (0x0033e788). T is the position just set (moved) or
just read (stuck). start is the owner's cached copy, so out is the displacement since the owner's update
began, not only this sweep's (they are the same for a human, who refreshes the copy before moving).
- Call the owner's slot +0x118 (0x0033e7b0).
So the three cases of "how far did it go" are (confirmed (code), the summary inferred):
| Pass result | Where the body ends |
|---|---|
| no contact or every contact answered 0 | start + V × dt |
a contact stopped pass k (k = 1 or 2), and pass k + 1 had no stopping contact |
the advance of each stopping contact along its normal (below), + the slid V × dt in full |
| three passes each stopped, or the slid move fell under 1e-6² | the advances only; v.xy zeroed, counter up |
Note what this means: the move is not shortened by the contact fraction. Each pass sweeps the whole slid
V × dt again from wherever the body now is. A body that slides along a wall therefore travels its normal-direction
advance plus a full slid step in one update. Confirmed (code): dt is written once (0x0033e2f0) and re-read each
pass (0x0033e5c0); the resolution never reads the dt pointer (no use of its fourth register argument).
A body whose move is slower than 0.001 / dt (0.03 m/s at 30 Hz) skips the passes and moves freely by V × dt
with +0x70 = 0 (pass 1's |D|² ≤ 1e-6 with found = 0). Confirmed (code); its effect is inferred.
A head-on hit whose slide leaves V ≈ 0 ends the loop on the next pass's |D|² test with passes = 3 and
found > 0: stuck, so the counter goes up each update a body is pressed straight into a wall. Confirmed (code);
this is what the human's "stuck in the air" rule reads (Characters).
PhysicsBody_ResolveContacts¶
0x0033d9d8, 0x8a0 bytes. int PhysicsBody_ResolveContacts(IPhysics* phys, Body* body, Contact** sorted, int count,
const vec4* D, vec4* V, vec4* v, float* dt /*unused*/, u32* moveFlags, float* scale) (the last two on the stack).
Returns 0, 1 or −1. Confirmed (code) for all of it.
At the start, u = D / |D| (reciprocal square root; D is never zero here). Then the contacts are walked in
sorted order (nearest first; equal fractions keep their insertion order, the insert):
- Skip the contact (go to the next) when (
0x0033da80-0x0033dad4):- it has a triangle (
+0xb4≠ 0) whose flags (+0x06) lack bit0x1(disabled since it was found); or - the moving body has no owner; or
- it has another owner (
+0x40≠ 0) whose body (slot+0xf0) is null.
- it has a triangle (
answer = owner.onContact(contact)(slot+0xf8).code = answer & 0xffff.- Code 0 (
0x0033db04): ifanswer & 0x10000, return 0; ifanswer & 0x40000, return −1; otherwise go on with the next contact. Nothing else:0x20000with code 0 does nothing (the scale rule is in the tail, which code 0 never reaches). - Codes 1 to 4 act (below), then the tail. Codes 5 and above go straight to the tail.
- Tail (below), then: return 0 if
answer & 0x10000; else −1 ifanswer & 0x40000; else 1. So any non-zero code ends the walk: later contacts of the pass are never seen. - All contacts walked (or
count ≤ 0): return 0.
What the returns mean to the sweep: 1 "stopped, slide and try again", 0 "done, move the rest", −1 "abort".
The codes¶
Let n = contact +0x50, f = contact +0x80, and the advance be:
d = f × (D · n). Ifd < −0.01, the body's position (owner slot+0xa8) becomesposition + n × (d + 0.01), set back through slot+0xb0with its rotation unchanged, and the contact's own shape (+0x04) is re-posed (shape slot+0x28with the owner). Otherwise the body is not moved.
Because n points at the mover and d is negative, n × (d + 0.01) moves the body towards the surface, along
the normal only, by |d| − 0.01: it stops 0.01 m short, measured along the normal, of where the swept shape
touched. The part of the move along the surface up to the contact is not applied (the next pass's slid move
covers the surface direction). −0.01 is 0xbc23d70a, 0.01 0x3c23d70a. Confirmed (code) at 0x0033dcdc-0x0033ddc8
(code 1), 0x0033ddec-0x0033dedc (code 2), 0x0033df40-0x0033e02c (code 3). The same formula is the stand-alone
PhysicsVec_BackOffPlane (below), which the resolution does not call. That this 0.01 is the 0.006-0.010 m
clearance measured on Characters is inferred (the walls page measured stops, not the code).
| Code | Effect, in order |
|---|---|
| 1 | the advance; then Vec_RemoveNormalPart(V, n) and Vec_RemoveNormalPart(v, n) |
| 2 | the advance; then e = ownOwner.attribute(8) and PhysicsVec_Bounce(V, n, e); e is asked again and PhysicsVec_Bounce(v, n, e) |
| 3 | the advance; then V = v = (0, 0, 0, 1) |
| 4 | other = otherOwner.body (slot +0xf0). If other.flags & 0x200: exactly code 1. Else: push = n × 0.5 × (D · n) (the whole pass move D, not f × D), push *= w_m / (w_o × k_o) with w_m = own owner's attribute 1, w_o = other owner's attribute 1, k_o = other owner's attribute 7 (asked in that order); other+0x50 += push (xyz, w = 1); call other's body slot +0x50 with (other, contact, &push, &D); then exactly code 1 (the advance and both slides) |
Confirmed (code): code 4 at 0x0033db60-0x0033dcd8, the 0.5 is 0x3f000000 at 0x0033dbac. "Own owner" is
contact +0x00, "other owner" contact +0x40.
The tail¶
Run for every code that is not 0. Confirmed (code) at 0x0033e058-0x0033e204:
- Ask for the velocity write-back:
moveFlags |= 0x2, except when- the contact is with the level (
+0x40= 0) and its triangle has flag0x100(bit 8; 20,231 of the disc's 150,567 triangles, Collision); or - the mover and the other owner are both humans (type mask
0x40, slot+0x20) and both airborne (state0x4000000, slot+0x50).
- the contact is with the level (
(A level contact must carry a triangle: the code reads +0xb4 without a null test.)
2. Contact flag 0x1 (corner slide into the move): unless the mover is a human in any state of 0x18003ff0
(Human_IsInPairOrBlock 0x00227fd8: grab, held, mug, tackle, throw, block...) or the body has flag 0x8000:
s = contact+0xa0, Vec_RemoveNormalPart(s, u) (drops the part of s that goes against the pass's direction),
V += s (xyz, w = 1). Only V, not v. Then go to step 5: the scale rule is skipped (as it is when
the human or 0x8000 test refuses).
3. Else, contact flag 0x2 (slide kept for the next move): s = contact+0xa0, Vec_RemoveNormalPart(s, u);
if moveFlags & 0x1 is clear, set it and store body+0x50 = s (whole vec4). So only the first such contact
of the move is kept, and since the sweep cleared +0x50 before the passes, it survives as the next sweep's
push-out (s / dt / k in the next move velocity).
4. Scale rule (flags 0x1 not set): if answer & 0x20000: moveFlags |= 0x4, and
*scale = max(*scale, body.contactScale(contact)) (body slot +0x48; max.s at 0x0033e1f4). The largest
scale met wins, starting from 0.
5. The return (above, step 5).
Who sets the contact flags: the capsule's mesh sweep writes 0x1 when it built a corner slide (0x00348360,
0x00348438), the capsule-box sweep writes 0x1 for its four corner regions with a unit diagonal slide in +0xa0
(0x00345078, Unit 12), the sphere and box mesh sweeps copy their hit's flags word
(0x003490f0,
0x003489d0; the box's word is always 0, 0x003488e0, so a box mesh contact never carries 0x1 or 0x2), the landing
contact is 0x80 (below). Whether any sweep produces 0x2 is not traced. Confirmed (code) for the stores
cited.
PhysicsVec_BackOffPlane¶
0x0033d7b8, 0xb4 bytes. void PhysicsVec_BackOffPlane(vec4* p, const vec4* move, const vec4* n, float f):
d = (move × f) · n; if d < −0.01, p.xyz += n × (d + 0.01) and p.w = 1; else nothing. The same advance as the
resolution's codes 1-3 (which inline it). Its only caller is Car_UpdateLoosePart (0x003881b4). Confirmed (code).
The current plate comment's "moves the point back" describes it from the moved point's side; from the start
position, as the resolution uses it, it moves the point forward along −n.
Vec_RemoveNormalPart¶
0x0033d870, 0x80 bytes. void Vec_RemoveNormalPart(vec4* v, const vec4* n): d = v · n; if d < 0 (strictly),
v.xyz −= n × d and v.w = 1; else v is unchanged (its w too). n is assumed unit (no normalising).
Confirmed (code).
PhysicsVec_Bounce¶
0x0033d8f0, 0xe4 bytes. void PhysicsVec_Bounce(vec4* v, const vec4* n, float e): d = v · n, computed once.
If d < 0, v.xyz −= n × d. Then, in both cases, v.xyz −= n × (d × e), v.w = 1. Confirmed (code) at
0x0033d924-0x0033d9b4. So:
d < 0(going in):v −= n d (1 + e), the reflection with restitutione(as the page says);d ≥ 0(already going out):v −= n d e: the outgoing normal speed is reduced to(1 − e)of itself. The current page's "whenv·n < 0" misses this half.
IPhysics_SweepBodyVsMesh¶
0x0033d340, 0x154 bytes. void IPhysics_SweepBodyVsMesh(IPhysics* phys, const AABB* box, Body* body, Shape* shape,
Contact* contacts, Contact** sorted, int* count, const vec4* D). Confirmed (code):
- The shape's mesh sweep through
0x0033d2d8(phys, &contacts[*count], box, body, shape, D), which picks the function from IPhysics+0xe0 + 4 × shape.type(shape+0x30, a byte) and passes it, besides those: a 1, the wall threshold −0.65 (0xbf266666), a second float 0, and an option4when the body's type mask has0x40(a human;(mask >> 4) & 4), else 0 (0x0033d2e0-0x0033d320). No function for the type: no contact. It returns 1 when the type's sweep reported any hit. The type's sweep writes its hits, nearest first (it sorts them,0x00347300), into consecutive contacts from&contacts[*count]; only the first is inserted (step 2), so one mesh contact per pass, the nearest. Each written contact has+0x04shape,+0x440,+0x50normal,+0x80fraction,+0x90the shape's position at the contact,+0xa0slide,+0xb0flags,+0xb4triangle (capsule:0x00348410-0x003484e4). The details are in Unit 2. - Insert it (when step 1 found one):
Contact_InsertSorted(phys, &contacts[*count], sorted, count, ownOwner = body+0x34, otherOwner = 0, ownShape = shape, otherShape = 0). - The landing segment, only when the body's type mask has
0x40and the owner's state has0x4000000(airborne) (0x0033d3dc-0x0033d40c):Human_LandingTest(owner, contacts, count, D)(0x0023e408, below) writescontacts[*count]; if it returns 1, set that contact's+0xb0 = 0x80and insert it like step 2. (The current text "when body flag0x40is set" on Characters is the type mask+0x44, not body flag+0x40.)
A mesh contact found in step 1 and a landing contact can both be in one pass's list; the landing one is written after the first is inserted, so they never share a slot. Confirmed (code).
Human_LandingTest¶
0x0023e408, 0x2dc bytes. int Human_LandingTest(Human* h, Contact* contacts, int* count, const vec4* D); returns 1
with a contact written at contacts[*count] (the count is not incremented here), else 0. Confirmed (code):
A= the feet: the position in the transform table (0x00714b00 + 32 × h+0x92).- Raised feet: when (
0x00227f68(h) is true and the state lacks0x1000000000, a long fall) or the human holds with flag0x800(0x00226660):A.z += 0.4 × Human_GetScale(h)(0x3ecccccd), but at mosth+0x560 + 0.7(0x3f333333). B= the upper point, human vtable slot+0x60/+0x64(0x00226a20: the feet ++0x4e8− 0.16 × scale, Characters).A += D(the moved, possibly raised, feet).- The ray goes from the higher of
AandBto the lower: originB, endA, unlessB.z < A.z, then the two swap.L= the distance between them, direction = (end − origin) /L. WorldManager_RayCast(world 0x005147c4, {origin, dir, L}, &hit, 0, 0)(mask 0: every enabled triangle).- No hit, or the hit normal's
z ≤ 0.65(0x3f266666): return 0. - Else: human
+0x1d8= the hit material,+0x230= the hit normal (vec4); and in the contact:+0x04= the body's first shape (bodyh+0x1a0, slot+0x28with(0, 0, 0)),+0x50= the hit normal,+0x44= 0,+0xa0=(0, 0, 0, 1),+0xb4= the hit triangle,+0xb0= 0,+0x84= 0 (the point+0x90is not written), and the fraction, withh0= human vtable slot+0x58/+0x5c(1.0,0x004ed818):f = 0when the hit distance< h0, elsef = (dist − h0) / (L − h0). Return 1.
The fraction is measured on the segment beyond 1.0 m from the origin, not along D: with the origin 0.88 m above
the starting feet, f is (g − 0.12) / (|D.z| − 0.12) for a vertical fall with the feet g above the ground (worked
example in the tests). Confirmed (code) for the formula; the reading is inferred.
Contact_InsertSorted¶
0x0033d718 (called by both queries). (phys, Contact* c, Contact** sorted, int* count, ownOwner, otherOwner,
ownShape, otherShape): writes c+0x00, +0x40, +0x04, +0x44; finds the first entry whose fraction is
strictly greater than c's, shifts it and the rest up one, puts c there (or at the end), *count += 1. So
equal fractions keep their insertion order: the mesh contact before a landing contact before body contacts at the
same fraction. It does not check the 50 limit. Confirmed (code).
AABB_ScaleAboutCentre¶
0x003414c0, 0x174 bytes. void AABB_ScaleAboutCentre(AABB* box, const vec3* s): if s.x, s.y and s.z are
all exactly 1.0, nothing. Else, per axis, c = (min + max) / 2, h = (max − min) / 2 × s, and the new
min = min(c + h, c − h), max = max(c + h, c − h) (so a negative scale still gives a valid box); w = 1.
Confirmed (code). In the sweep its result is unused (above, step 5.2).
AABB_ExtendByMove¶
0x00341840, 0xb8 bytes. void AABB_ExtendByMove(AABB* box, const vec4* d): a = min + d, b = max + d; new
min = min(min, a, b), new max = max(max, a, b), per axis (xyz). Confirmed (code).
PhysicsBody_GetContactScale¶
0x00341e18, 0x28 bytes, body vtable +0x48. float (Body*, const Contact* c): 1.0 when c+0x44 (the other
shape) is non-zero, a body; 0.98 (0x3f7ae148) when it is 0, the level. Confirmed (code).
PhysicsHumanBody_GetContactScale¶
0x00342288, 0x48 bytes, human body vtable +0x48. float (Body*, const Contact*): 0.25 (0x3e800000) when
the owner's state (slot +0x50) has 0x4000000 (airborne), else 1.0; the contact is not read. Confirmed
(code).
Who answers 0x20000 (Contacts): the human (level and objects; 0x20004 against a human in
the air), the world object on a floor (0x20002). So a world object sliding on a floor keeps 0.98 of its horizontal
speed per sweep (and of an upward v.z), written back because world-object bodies carry flag 0x400 (0x80000500,
Bodies). An airborne human's contact scale is 0.25, but it reaches the human only when its
body has 0x400 (a player). Inferred from the code paths above.
Corrections to the current pages¶
Each confirmed (code) at the address given:
- physics.md #contacts, code 1 "moves the body back to the contact": it advances the body along the normal to
0.01 m short of the touch, only when it would go more than 0.01 m, and drops the move's surface-parallel part
(
0x0033dcdc). Codes 2 and 3 do the same, not "moves back". -
contacts,
the next contact is code 0 without0x10000"go on with the next contact (return 0)": it ends the walk and returns 0; going on with¶0x10000/0x40000(0x0033db04,0x0033e1fc). -
contacts,
contact has no flag0x20000"keeps the smallest contact scale": the largest, and only for a non-zero code whose¶0x1(0x0033e1f4,0x0033e17c). -
contacts, "+0xb0 has bit 2 removes the normal part of its slide vector and stores it": flag
per move,0x2stores (once¶moveFlags 0x1); flag0x1adds it to the move velocity instead; in both the part removed is the one against the move's direction, not the contact normal (0x0033e148,0x0033e18c). -
contacts,
+0xf0"owner that does not want them": the other owner's body; skipped when it has none.¶ -
sweep, "still blocked after three, the body stays, its horizontal velocity is zeroed (owner vtable
body stays where the last pass's advance put it, and the zeroed velocity is the slid+0x74)": the¶vwritten whatever the body flags; the same happens when a slide leaves no move (0x0033e4b8). -
sweep step 1, "scaled about its centre by body
discarded; the queries get the unscaled box grown by the move (+0x60... grown to cover the move": the scaled box is computed and¶0x0033e518). -
sweep, "The velocity the slides leave is written back ... only when body flag
stopping contact of the move asked for it (0x400is set": and only when some¶moveFlags 0x2: not a level triangle with flag0x100, not two airborne humans) (0x0033e734). -
sweep / characters.md #ground, "the body's pending push-out (divided by dt)": also divided by the owner's¶
attribute 7 unless body flag0x1000(0x0033e2e4). - characters.md #falling, "run from
0x0033d340when body flag0x40is set": the body's type mask+0x44bit0x40and the owner airborne (0x0033d3dc). PhysicsVec_Bouncealso damps an outgoing normal speed by(1 − e)(above).
Test ideas (synthetic)¶
A fake owner (answers a fixed code per call, attributes 1 = 82, 7 = k, 8 = e; records every set-velocity,
set-transform and after-move call), a fake mesh query that returns a scripted contact per pass (normal, fraction,
a triangle with flags 0x0001 unless said, contact flags), no body query (body flag 0x1 clear). Start position
origin, dt = 1/30, push-out 0, k = 1, body flags 0x400 unless said. The values below follow from the spec
(computed by hand, float rounding aside).
- Head-on wall, code 1.
v = (3, 0, 0), contactn = (−1, 0, 0),f = 0.5on pass 1.D = (0.1, 0, 0),d = −0.05→ position(0.04, 0, 0);V = v = 0; pass 2|D|² = 0→ stuck: velocity set to(0, 0, 0)(stuck branch) and again by the write-back;+0x700 → 1;out = (0.04, 0, 0); one after-move call. - Same, answer
0x10001. Same position(0.04, 0, 0); resolution returns 0 → moved branch (byV × dt = 0);+0x70reset to 0; velocity(0, 0, 0)written once (write-back). Shows0x10000ends the walk without stuck. - Same, answer
0x40001. Position(0.04, 0, 0)(the advance happened); no velocity call,outkeeps its sentinel,+0x70unchanged, no after-move call. - Wall at an angle.
v = (3, 0, 0),n = (−0.6, 0.8, 0),f = 0.5, code 1, pass 2 no contact. Advancen × (−0.03 + 0.01)=(0.012, −0.016, 0);V = v = (1.92, 1.44, 0); pass 2D = (0.064, 0.048, 0), no contact → end(0.076, 0.032, 0);+0x70 = 0; velocity(1.92, 1.44, 0);out = (0.076, 0.032, 0). With body flags 0 (an AI): the same position and no velocity call. - Level triangle with flag
0x100. As test 4 with triangle flags0x0101: same position, no velocity call (write-back not asked). - Corner.
v = (3, 3, 0), pass 1n1 = (−1, 0, 0),f = 0.5; pass 2n2 = (0, −1, 0),f = 0.5; code 1 both. Pass 1:D = (0.1, 0.1, 0),d = −0.05→(0.04, 0, 0),V = (0, 3, 0). Pass 2:D = (0, 0.1, 0)→(0.04, 0.04, 0),V = 0. Pass 3:|D|² = 0→ stuck:+0x70 = 1, velocity(0, 0, 0);out = (0.04, 0.04, 0). - Three-pass limit. As test 4 but the fake returns the same contact (code 1,
f = 0.5) on all passes. Pass 1 as test 4; passes 2 and 3:D · n = 0→ no advance,Vunchanged (d = 0is not< 0). After pass 3 → stuck: position stays(0.012, −0.016, 0)(the slidV × dtis never applied); the velocity written is the slidv = (1.92, 1.44, 0)with x and y zeroed,(0, 0, 0);+0x70 = 1;out = (0.012, −0.016, 0). - Code 0 and
0x20000alone. Every contact answered0x20000: positionD = (0.1, 0, 0), no velocity call,+0x70 = 0, no scale. - Push-out.
v = 0,+0x50 = (0.02, 0, 0), no contacts: position(0.02, 0, 0),+0x50reads(0, 0, 0, 1)afterwards, no velocity call. Withk = 2:(0.01, 0, 0). Withk = 2and body flag0x1000:(0.02, 0, 0). - Too slow.
v = (0.0005, 0, 0),+0x50 = (0.00001, 0, 0):V = (0.0008, 0, 0),|V|² = 6.4e-7→ nothing moves,+0x50cleared, dirty flag not set,outkeeps its sentinel, no after-move call. - Slow but above the cut.
v = (0.02, 0, 0), a contact scripted for pass 1:|V|² = 4e-4 > 1e-6but|D|² ≈ 4.4e-7 ≤ 1e-6→ no query is made (the fake records no call), position(0.000667, 0, 0),+0x70 = 0. - Bounce on a floor (world object). Body type mask 0, flags
0x80000500,e = 0.1,v = (1, 0, −5), floorn = (0, 0, 1),f = 0.4, answer0x20002, pass 2 no contact.D = (0.0333, 0, −0.1667),d = −0.0667→z = −0.0567;v = V = (1, 0, 0.5); pass 2D = (0.0333, 0, 0.0167)→ end(0.0333, 0, −0.04); scale 0.98 (level contact) → velocity written(0.98, 0, 0.49). - Bounce going out.
PhysicsVec_Bounce((0, 0, 2), (0, 0, 1), 0.1)→(0, 0, 1.8);PhysicsVec_Bounce((0, 0, −2), (0, 0, 1), 0.1)→(0, 0, 2.2). - Scale,
v.zrule. Test 4 with answer0x20001, a generic body (type mask 0) and the startingv = (3, 0, −1): the slide leavesv = (1.92, 1.44, −1); the write-back is(1.8816, 1.4112, −1)(v.z < 0kept). Withv = (3, 0, 1):(1.8816, 1.4112, 0.98). The same with a human-body fake airborne (state0x4000000): scale 0.25,(0.48, 0.36, ·); on the ground 1.0. With contacts at 0.98 and 1.0 in one move: 1.0 (the largest). The position is not affected by the scale. - Code 4 push. Mover
v = (3, 0, 0), contact with another owner (body flags 0,+0x50 = 0),n = (−1, 0, 0),f = 0.5, attributes 1 = 82 for both, otherk = 1. Other's+0x50 = (0.05, 0, 0, 1)(0.5 × (−0.1) × n), its slot+0x50called once with that push andD; mover as test 1 (position(0.04, 0, 0)). Otherk = 1e9: push≈ 5e-11. Other body flag0x200: no push, no call, mover as test 1. - Corner slide (contact flag
0x1). Test 1 with contact flags0x1,+0xa0 = (0, 0.5, 0): after code 1,V = (0, 0.5, 0),v = 0; pass 2D = (0, 0.01667, 0), no contact → end(0.04, 0.01667, 0),+0x70 = 0, velocity(0, 0, 0); no scale even with0x20001. With+0xa0 = (−0.5, 0.5, 0): the−0.5along−uis removed first → same result. With body flag0x8000:V = 0→ stuck as test 1. - Stored slide (contact flag
0x2). Test 4 with flags0x2,+0xa0 = (0, 0.05, 0): same position and velocity; afterwards body+0x50 = (0, 0.05, 0, ·); a second flag-0x2contact in pass 2 does not overwrite it. - Landing segment fraction. Human, feet at
z = 0.3, upper point 0.88 m above,D = (0, 0, −0.5), ground planez = 0,n = (0, 0, 1):L = 1.38, hit at 1.18 →f = 0.18 / 0.38 = 0.4737, contact flags0x80; with code 1 the advance isz += 0.4737 × (−0.5) + 0.01→ feet at0.0732. Feet atz = 0.05,D = (0, 0, −0.2): hit at 0.93 < 1.0 →f = 0, no advance. A ground normalz = 0.6: no contact. - Insert order. Fractions 0.5, 0.2, 0.5 inserted in that order → sorted 0.2, first 0.5, second 0.5.
- AABB helpers. Scale
[(0,0,0),(2,2,2)]by(1.5, 1, −1)→[(−0.5, 0, 0), (2.5, 2, 2)]; by(1,1,1)unchanged. Extend[(0,0,0),(1,1,1)]by(2, −1, 0)→[(0, −1, 0), (3, 1, 1)].
Unit 6: Shapes: records, poses and bounds¶
What it is. The four shape records (oriented box, sphere, upright capsule, segment), how each is posed from its
owner's transform and bone every update, how its "previous position" is kept for sweeps (the flag 0x10 rule), and
the world AABB of a posed shape. Specified exactly below; everything confirmed (code) at the cited addresses (read in
the disassembly, 2026-10-12), unless a line says otherwise.
- Specified by: this section; background in Shapes, The human body and Combat: moving strikes.
- Coney: a new
src/physics/shapes.{h,cpp}(posed shapes as values, with a pose function per type that takes the owner's transform and bone transform as inputs);src/human/human.hcapsule height moves here;world_objects/object_bodies.*builds its box and sphere from it. - Tests: Test ideas: each shape posed from a synthetic body and bone transform; the flag
0x10sequence; a rotated box's corners and bound against a brute-force corner bound; the segment bound quirk; the capsule's height rules (0.61, 0.7 and its cap, 0.35 cuffed, never under 0.2 × scale).
21 functions, 5272 bytes (1 tagged). Two names changed in Ghidra on 2026-10-12 because the old ones were wrong:
0x003430e8 was PhysicsBox_GetBounds (it returns the eight corners, not a bound) and 0x00341708 was
AABB_FromSphereMethod (it builds an upright cylinder's box).
| Address | Name | Size |
|---|---|---|
0x00342a30 |
PhysicsShape_Construct |
60 |
0x00342a70 |
PhysicsShape_Destroy |
48 |
0x00342aa0 |
PhysShapeSphere_Construct |
60 |
0x00342ae0 |
PhysicsSphere_Destroy |
40 |
0x00342b08 |
PhysicsSphere_Pose |
512 |
0x00342d18 |
PhysicsBox_Construct |
124 |
0x00342d98 |
PhysicsBox_Destroy |
40 |
0x00342dc0 |
PhysicsBox_SetSize |
52 |
0x00342df8 |
PhysicsBox_Pose |
580 |
0x00343040 |
PhysicsBox_SetMatrix |
168 |
0x003430e8 |
PhysicsBox_GetCorners |
956 |
0x003434a8 |
PhysShapeCapsule_Construct |
56 |
0x003434e0 |
PhysicsCapsule_Destroy |
40 |
0x00343508 |
PhysShapeCapsule_SetSize |
12 |
0x00343518 |
PhysicsCapsule_Pose (tagged) |
636 |
0x003437b0 |
PhysShapeSegment_Construct |
56 |
0x003437e8 |
PhysicsSegment_Destroy |
40 |
0x00343810 |
PhysShapeSegment_Pose |
632 |
0x00341190 |
PhysicsShape_GetBounds |
812 |
0x00341660 |
AABB_FromSphere |
168 |
0x00341708 |
AABB_FromUprightCylinder |
180 |
Notation as in Unit 3: vectors are 4 floats, "xyz" operations leave w alone, "w = 1" is the code
writing 1.0 (the VU constant register). Arguments follow the EE ABI: integers and pointers in a0-a3, floats in
f12, f13, f14.
Transforms and rotations¶
Every pose reads its transforms through owner vtable slots (GCC 2.x 8-byte slots, adjust then function; Unit 3's
table). Each returns a 32-byte transform: position (vec4) then rotation quaternion (x, y, z, w).
| Slot (adjust / function) | Called as | Human's function | World object's (vtable 0x005453a0) |
Used by |
|---|---|---|---|---|
+0xa0 / +0xa4 |
(out, owner), the body transform |
0x0023cd00 |
Obj_GetPoseNow 0x003a1ad8 |
sphere, box |
+0xa8 / +0xac |
(out, owner), the body transform |
0x004ed828 |
Task_GetPose 0x003a1e30 |
capsule, segment |
+0xb8 / +0xbc |
(owner, out, bone), a bone's transform in the body's frame |
Human_GetBoneTransform 0x0023bde8 |
sphere, box, segment | |
+0x20 / +0x24 |
(owner), type mask (0x40 a human) |
capsule |
For a human, 0x0023cd00 and 0x004ed828 are the same code (both copy the entry of the transform table 0x00714b00
indexed by human +0x92), so the two body slots agree. For a world object, +0xa0 is the pose extrapolated to now
(Obj_GetPoseNow, Objects) and +0xa8 the stored pose (Task_GetPose: rotation +0x20, position
+0x10, moved by 0x003a2508's offset when flags +0x54 & 0x600 are clear and slot +0xf0 / +0xf4 returns 0); its
Obj_GetWorldPose (0x003a19b0) is the slot before, +0x98 / +0x9c, which no pose reads. Only spheres and
boxes, which read +0xa0, are built for world objects (Bodies). Confirmed (code): the slots at the
call
sites below, the human's entries read from its vtable 0x0053f088, the world object's from 0x005453a0 (the vtable
ObjectTaskPool_New 0x004f3ef0 stores). An earlier draft read them from 0x00544c00, which is the car's vtable
0x00544c08 minus 8, so its column was one slot off.
Human_GetBoneTransform returns the bone cache entry (model space) with the position multiplied by the human's
scale and the rotation as is (Objects); it writes nothing when the human has no model instance
(+0xd8 = 0), so the caller's buffer keeps what it held. In the sphere, box and segment poses that buffer is the one
the body transform was just read into, so such a human's bone transform is its body transform (confirmed (code):
the same stack slot at 0x00342b4c/0x00342b80, 0x00342e3c/0x00342e78, 0x00343858/0x0034389c; that it
happens in play is not known).
Rotation by a quaternion (R(q)). Two forms appear, and they give the same result for a unit quaternion:
- The sphere's no-bone case rotates the offset directly (
0x00342c5c-0x00342c9c):v' = (w² − |u|²) v + 2 (u·v) u + 2 w (u × v), withu = (x, y, z). No normalisation. - Everything else builds a matrix with
Quat_ToMatrix(0x00336500) and multiplies. Its four stored rows are the images of the axes (row 0 =R e_x, and so on) and row 3 is(0, 0, 0, 1):
| Row | x | y | z | w |
|---|---|---|---|---|
| 0 | 1 − 2(y² + z²) |
2(xy + zw) |
2(xz − yw) |
0 |
| 1 | 2(xy − zw) |
1 − 2(x² + z²) |
2(yz + xw) |
0 |
| 2 | 2(xz + yw) |
2(yz − xw) |
1 − 2(x² + y²) |
0 |
| 3 | 0 | 0 | 0 | 1 |
A vector is transformed as row0·v.x + row1·v.y + row2·v.z + row3·v.w, so M v = R v in xyz and its w is
v.w. Every pose then adds a position in xyz and writes w = 1, so the w never matters.
So a shape's "bone x axis" is row 0 of the bone's matrix, R(qb) e_x.
The shape record and vtable¶
Common head, written by PhysicsShape_Construct (0x00342a30, (Shape* s, u8 type), returns s):
| Offset | Type | Meaning | Construct writes |
|---|---|---|---|
+0x00 |
ptr | next shape of the body | 0 |
+0x10 |
vec4 | local offset | (0, 0, 0, 1) (the constant at 0x005116c0) |
+0x20 |
vec4 | previous position (sphere centre, box position, capsule base, segment start) | not written |
+0x30 |
u8 | type: 1 box, 2 sphere, 3 capsule, 4 segment | type |
+0x31 |
s8 | bone, −1 the body's own frame | −1 |
+0x32 |
u16 | flags: 0x1 enabled, 0x2, 0x4 a strike target, 0x8 allocated, 0x10 reset the previous position on the next pose |
0x1 |
+0x34 |
ptr | vtable | 0x00544870 (the base) |
Shape vtable (both offsets of each slot; the type vtables are 0x005447f0 box, 0x00544830 sphere,
0x005447b0 capsule, 0x00544770 segment; read from the vtables):
| Slot | What | Sphere | Box | Capsule | Segment |
|---|---|---|---|---|---|
+0x08 / +0x0c |
set position (s, const vec4* p): copies p (all four words), previous untouched |
+0x50 (0x004f1200) |
+0x80 (0x004f1220) |
+0x50 (0x004f1238) |
+0x40 (0x004f1250) |
+0x10 / +0x14 |
destructor (s, flags) |
0x00342ae0 |
0x00342d98 |
0x003434e0 |
0x003437e8 |
+0x18 / +0x1c |
enable (s, bool on) (0x004f11c8, shared) |
on: flags \|= 0x11; off: flags &= ~0x1 |
|||
+0x20 / +0x24 |
position pointer (s) → & the field |
+0x50 (0x004f1210) |
+0x80 (0x004f1230) |
+0x50 (0x004f1248) |
+0x40 (0x004f1260) |
+0x28 / +0x2c |
pose (s, owner) |
0x00342b08 |
0x00342df8 |
0x00343518 |
0x00343810 |
+0x30 / +0x34 |
set from a matrix (s, const mat4* m), m = 4 rows, row 3 the position |
0x00342d08: centre = row 3; previous untouched |
PhysicsBox_SetMatrix 0x00343040 |
0x00343798: previous = base, base = row 3 |
0x00343a88: does nothing |
The base vtable's entries for these slots point at 0x0042b2b0 (inferred: the pure-virtual stub) except the enable
(0x004f11c8) and the destructor (0x00342a70). The small entries (0x004f11c8-0x004f1260, 0x00342d08,
0x00343798, 0x00343a88) have no function in Ghidra; they are read as listed (confirmed (code)).
Destructors. PhysicsShape_Destroy (0x00342a70, (s, int flags)) sets the base vtable and frees s
(0x0042c820) when flags & 1. Each type's destructor sets its own vtable and calls it with the same flags.
The flag 0x10 rule¶
Switching a shape on sets flags 0x11 (enabled and reset). The sphere, box and segment poses then do:
- At the start: if
0x10is clear, previous = the current position (the pose before this one). - Pose (below).
- At the end: if
0x10is set, previous = the new position. Then clear0x10, always.
So the first pose after a shape is switched on has previous = current: a zero move, so a strike sphere just switched
on can only hit what it starts inside (Unit 5); every
later pose
keeps the pose before. Switching off clears only 0x1. Confirmed (code) at 0x00342b20/0x00342ccc (sphere),
0x00342e10/0x00343000 (box), 0x0034382c/0x00343a48 (segment), 0x004f11d0 (enable).
The capsule pose never reads or clears 0x10: its previous base is always the base before this pose
(0x0034358c, 0x0034373c). PhysicsBox_SetMatrix also ignores it (previous = the old position, always). A
capsule's 0x10, once set by enabling it, stays set (no reader of it found in this unit).
What the segment keeps as "previous" is its start only; the end has no previous. The sphere keeps its centre, the
box its position (+0x80; not its axes).
Sphere¶
0x60 bytes. PhysShapeSphere_Construct (0x00342aa0, (s)): the head with type 2, then +0x40 radius = 0, vtable
0x00544830; the centre +0x50 is not written. Fields: +0x40 radius (float), +0x50 centre.
PhysicsSphere_Pose¶
0x00342b08, 0x200 bytes. void (Sphere* s, Owner* o).
- Flag
0x10start step (above). (P, q)= owner slot+0xa0(0x00342b4c).- Bone −1 (
0x00342b68→0x00342c5c):centre = P + R(q) offset(the direct quaternion form),w = 1. - Bone ≥ 0:
(b, qb)= owner slot+0xb8with the bone (0x00342b80);centre = P + R(q) (b + R(qb) offset)(two matrix products,0x00342bc4,0x00342c1c),w = 1. - Flag
0x10end step; clear0x10.
Box¶
0x90 bytes. PhysicsBox_Construct (0x00342d18, (b)): the head with type 1, vtable 0x005447f0, and +0x50,
+0x60, +0x70, +0x80 = the four rows of the identity at 0x005117a0-0x005117d0: axes identity, position
(0, 0, 0, 1). The half extents are not written. Fields: +0x40 half extents (x, y, z; w 1.0 once sized),
+0x50 / +0x60 / +0x70 the axes A0, A1, A2 (unit vectors, w 0), +0x80 position (the centre).
PhysicsBox_SetSize (0x00342dc0, (b, float sx, float sy, float sz)): +0x40 = 0.5 sx, +0x44 = 0.5 sy,
+0x48 = 0.5 sz (the constant 0.5 is 0x3f000000), +0x4c = 1.0.
PhysicsBox_Pose¶
0x00342df8, 0x244 bytes. void (Box* b, Owner* o).
- Flag
0x10start step (previous = old+0x80). (P, q)= owner slot+0xa0(0x00342e3c).- Bone −1 (
0x00342f98): axesA0, A1, A2= rows 0-2 ofQuat_ToMatrix(q)(row 3 is written to+0x80and then overwritten);position = P + R(q) offset,w = 1. - Bone ≥ 0:
(b, qb)= owner slot+0xb8(0x00342e78). Axes:Ai = R(q) (row i of R(qb)), i = 0, 1, 2 (the loop at0x00342eb0multiplies all four rows; row 3 stays(0, 0, 0, 1)and is overwritten).position = P + R(q) (b + offset),w = 1. Quirk: the offset is added to the bone's position without the bone's rotation (0x00342f10), unlike the sphere and the segment. No box with a bone is known (inferred: the object and car boxes have bone −1), so it is unlikely to matter. - Flag
0x10end step; clear0x10.
PhysicsBox_SetMatrix¶
0x00343040, 0xa8 bytes; also the box's slot +0x30 / +0x34. void (Box* b, const mat4* m), m = rows
m0-m3, m3 the position. In order: previous +0x20 = the old position +0x80 (always); A0 = m0, A1 = m1,
A2 = m2 (all four words copied); position = m3 + m0·off.x + m1·off.y + m2·off.z (xyz), w = 1. Flags untouched.
Called by Car_UpdateLoosePart (0x0038806c) and, through the slot, by PhysicsBody_Moved (0x00341e40) for a
body's first shape.
PhysicsBox_GetCorners¶
0x003430e8, 0x3bc bytes. void (const Box* b, vec4 out[8]). Corner i (0 to 7):
out[i] = position + sx·hx·A0 + sy·hy·A1 + sz·hz·A2, w = 1, with sx = +1 when i & 1 else −1,
sy = −1 when i & 2 else +1, sz = +1 when i & 4 else −1.
(The code builds the x part for all eight, then adds or subtracts the y and z parts in place: 0x00343100,
0x003431a0-0x00343438, 0x00343448.) The y sign is the odd one out; only the order of the corners depends on it.
Its only caller is the generic bound.
Capsule¶
0x60 bytes. PhysShapeCapsule_Construct (0x003434a8, (c)): the head with type 3, vtable 0x005447b0; radius
and height are not written. Fields: +0x40 radius, +0x44 height, +0x50 base (the bottom centre; the capsule is
the vertical range [base.z, base.z + height], Shapes).
PhysShapeCapsule_SetSize (0x00343508, (c, float height, float radius)): +0x40 = radius (from f13),
+0x44 = height (from f12). Note the order: height first. IPhysics_Construct passes 0.35 (0x3eb33333) as the
radius (0x0033c8e4).
PhysicsCapsule_Pose¶
0x00343518, 0x27c bytes (tagged). void (Capsule* c, Owner* o). The flag 0x10 is not used (above).
Not a human (the owner's type mask, slot +0x20, lacks 0x40; 0x0034354c → 0x00343734): previous = old
base; (P, q) = slot +0xa8; base = P + offset (xyz, not rotated), w = 1. Nothing else: the height stays as
set.
A human (0x00343558). Let s = Human_GetScale (0x0021d020: human +0x65c, or 1.0 when 0x00227d28
holds and the scale is under 1.1), called afresh each time it is used.
(P, q)= slot+0xa8(0x00343570). Previous = old base;base = P(all four words; the offset is not used).(b6, ·)= slot+0xb8with bone 6 (the head;0x003435a8).height = b6.z + 0.2 s(0x3e4ccccd). (b6.zis already scaled, above.) The capsule's top is thereforeP.z + b6.z + 0.2 sin every case below except the cuffed one.- If the byte human
+0x3beis non-zero (0x003435c8):base.z += 0.61 s,height −= 0.61 s(0x3f1c28f6). Go to 6. - Else if the human is jumping (state flag
0x400000000,0x00227f68) and not in a long fall (state flag0x1000000000,0x002265f0) (0x00343614-0x00343634):base.z += 0.7 s,height −= 0.7 s(0x3f333333). Then ifbase.z − h560 > 0.7(strictly;h560= human+0x560, the take-off height; the constant is 0.7 not scaled),base.z = h560 + 0.7andheight = b6.z + 0.2 s + P.z − base.z(the top kept). Go to 6. - Else if the human is cuffed (state flag
0x20000,0x00223b70;0x003436c4):height = 0.35 s(base unchanged, so the top drops). - If
height < 0.2 s(strictly),height = 0.2 s(0x00343710).
A state test reads the 64-bit word at the human's current state record (+0xd4), and is false when there is none
(0x002265f0). The radius is never touched here.
Segment¶
0x70 bytes. PhysShapeSegment_Construct (0x003437b0, (s)): the head with type 4, vtable 0x00544770; nothing
else written. Fields: +0x40 start, +0x50 end, +0x60 radius, +0x64 length.
PhysShapeSegment_Pose¶
0x00343810, 0x278 bytes. void (Segment* s, Owner* o).
- Flag
0x10start step (previous = old start+0x40). (P, q)= slot+0xa8(0x00343858), andM = Quat_ToMatrix(q).- Bone −1 (
0x00343a00):start = P(all four words; the offset is not used),end = P + length · M row 0(the body's x axis),w = 1. - Bone ≥ 0:
(b, qb)= slot+0xb8(0x0034389c),B = Quat_ToMatrix(qb). Locala = b + B offset, locale = a + length · B row 0(the bone's x axis).start = P + M a,end = P + M e,w = 1(0x00343970,0x003439c0). - Flag
0x10end step (previous = the new start); clear0x10.
The radius +0x60 is not read.
PhysicsShape_GetBounds¶
0x00341190, 0x32c bytes. void (unused, vec4* min, vec4* max, const Shape* s): the first argument is not read
(PhysicsBody_ComputeBounds 0x00341638 passes the body, whose +0x00 / +0x10 are min / max;
PhysicsBody_AddShapeBounds 0x003417c0 passes stack boxes and then merges them into the body's). By type
(+0x30):
| Type | min before the sort |
max before the sort |
|---|---|---|
2 sphere (0x003412b0) |
centre − r (xyz; w = centre's) |
centre + r |
3 capsule (0x003411f0) |
(base.x − r, base.y − r, base.z) |
(base.x + r, base.y + r, base.z + height) |
4 segment (0x00341248) |
start − r (r = +0x60) |
end + r |
1 box (0x00341320) |
per axis, the corner with the smallest value | per axis, the corner with the largest value |
| anything else | nothing written, return |
The box's corners come from PhysicsBox_GetCorners; the scan is over corners 1 to 7 against the
best so far with strict < (0x00341348-0x00341404), so the exact rotated-box bound. The box path writes only xyz
of min and max.
The sort (0x0034146c), for all four types: min' = min(min, max) per axis, then max' = max(min', max) per
axis; both keep max's w. The second step can never change max. So:
- sphere, capsule and box bounds come out right (their
min ≤ maxalready, for a radius and height ≥ 0); - a segment whose end is below its start on an axis gets a wrong bound on that axis:
[min(start − r, end + r), end + r], which misses the part fromend + rtostart + r, and is a single value (end + r) whenstart − r > end + r. Confirmed (code). A punch bag's segment points down (Bodies), so its z bound is the single heightend.z + r(inferred: where that bound is used, through the body's bound, is Unit 7's).
The capsule bound has no radius below or above: it is the cylinder's box, matching the upright capsule of the pair tests (Shapes).
AABB_FromSphere and AABB_FromUprightCylinder¶
Both write a 0x20-byte AABB (min at +0x00, max at +0x10) and set both w to 1.0; both sort with the
original min and max (so unlike the generic bound, a negative size still gives a valid box).
AABB_FromSphere(0x00341660,(AABB* out, const vec4* c, float r)):min = c − r,max = c + r(xyz). Its only caller isPhysicsBody_PushOutOfWalls(Unit 2,0x00347808).AABB_FromUprightCylinder(0x00341708,(AABB* out, const vec4* c, float height, float radius); formerlyAABB_FromSphereMethod):min = (c.x − radius, c.y − radius, c.z − height/2),max = (c.x + radius, c.y + radius, c.z + height/2). Callers: the human body's broad bound+0x90, in its update (0x0034252c, height = the capsule's+0x44) and in its moved handler (0x00342878: height 2.0, radius 0.8, centre = the new position + 1.0 m in z) (Unit 10).
Test ideas¶
Values computed from this spec (double precision; compare with 1e-5). qz = 90° about z = (0, 0, 0.70711,
0.70711), qy = 90° about y = (0, 0.70711, 0, 0.70711). Quat_ToMatrix(qz) rows: (0, 1, 0), (−1, 0, 0),
(0, 0, 1); Quat_ToMatrix(qy) rows: (0, 0, −1), (0, 1, 0), (1, 0, 0). Body P = (10, 5, 0), q = qz
unless said; the bone (b, qb) = ((0.2, 0, 1.5), qy).
- Sphere, no bone. Offset
(1, 0, 0.5): centre(10, 6, 0.5, 1)(both rotation forms agree). - Sphere on a bone. Offset
(0.08, 0, 0)(the hand's):R(qb) offset = (0, 0, −0.08), local(0.2, 0, 1.42), centre(10, 5.2, 1.42, 1). - Segment on a bone. Offset
(−0.07, 0, 0.05), length 0.38 (the spine's): local start(0.25, 0, 1.57), local end(0.25, 0, 1.19); start(10, 5.25, 1.57, 1), end(10, 5.25, 1.19, 1). - Segment, no bone. Any offset, length 0.38: start =
P=(10, 5, 0), end(10, 5.38, 0, 1). - Box, no bone. Offset
(1, 0, 0): axes(0, 1, 0),(−1, 0, 0),(0, 0, 1); position(10, 6, 0, 1). - Box on a bone (the quirk). Offset
(0.1, 0, 0): axes(0, 0, −1),(−1, 0, 0),(0, 1, 0); position(10, 5.3, 1.5, 1)(b + offsetunrotated by the bone; a rotated offset would give(10, 5.2, 1.4)). - Flag 0x10 sequence (sphere, no bone, offset 0). Pose at
P = (0, 0, 0)→ centre 0. Enable (flags0x11). Pose atP = (1, 0, 0): previous =(1, 0, 0)= centre (zero move), flags0x01. Pose atP = (2, 0, 0): previous(1, 0, 0), centre(2, 0, 0). Disable then enable again: the next pose again has previous = centre. The same sequence on a capsule (non-human owner) gives previous = the old base every time. - SetMatrix. Rows
(0, 1, 0),(−1, 0, 0),(0, 0, 1), position(5, 5, 0); offset(0, 0, 0.5); old position(1, 1, 1): previous(1, 1, 1), position(5, 5, 0.5, 1). Offset(1, 0, 0): position(5, 6, 0). - Box corners. Centre
(0, 0, 0.5), identity axes, half(2, 1, 0.5): corners 0-7 are(−2, 1, 0),(2, 1, 0),(−2, −1, 0),(2, −1, 0),(−2, 1, 1),(2, 1, 1),(−2, −1, 1),(2, −1, 1). - Rotated box bound. The same box turned 45° about z (
q = (0, 0, 0.38268, 0.92388)): bound(−2.1213, −2.1213, 0)to(2.1213, 2.1213, 1), equal to the brute-force min/max of the eight corners. - Sphere and capsule bounds. Sphere centre
(1, 2, 3), r 0.5:(0.5, 1.5, 2.5)to(1.5, 2.5, 3.5). Capsule base(1, 2, 0), r 0.35, height 1.8:(0.65, 1.65, 0)to(1.35, 2.35, 1.8). - Segment bound quirk. Start
(0, 0, 2.15), end(0, 0, 0.65), r 0.35: x[−0.35, 0.35], y the same, z[1.0, 1.0](the true bound is[0.3, 2.5]). Reversed (start(0, 0, 0.65), end(0, 0, 2.15)): z[0.3, 2.5]. Start(0, 0, 1.0), end(0, 0, 0.8), r 0.35: z[0.65, 1.15](true[0.45, 1.35]). - AABB helpers.
AABB_FromSphere(c = (1, 2, 3), r = 0.5):(0.5, 1.5, 2.5, 1)to(1.5, 2.5, 3.5, 1); with r = −0.5 the same box.AABB_FromUprightCylinder(c = (1, 2, 3), height 2.0, radius 0.8):(0.2, 1.2, 2, 1)to(1.8, 2.8, 4, 1). - Capsule height, human (
P = (0, 0, 10),b6.z = 1.6,s = 1unless said): plain: base z 10, height 1.8 (top 11.8).+0x3beset: base z 10.61, height 1.19. Jumping, take-off+0x560= 9.8: base z 10.7, height 1.1 (10.7 − 9.8 = 0.9 > 0.7 → base z 10.5, height 1.3; top still 11.8). Jumping with take-off 10.2: 10.7 − 10.2 = 0.5, no cap: base z 10.7, height 1.1. Jumping and long fall: plain (1.8). Cuffed: height 0.35, base z 10 (top 10.35).+0x3beand cuffed: 1.19 (the+0x3bebranch wins).b6.z = −0.1: 0.1 → clamped to 0.2. Scale 0.97,b6.z = 1.552(the bone already scaled): height 1.746; cuffed 0.3395; clamp floor 0.194. - Capsule, not a human. Owner at
(3, 4, 5)with rotationqz, offset(1, 0, 0): base(4, 4, 5, 1)(the offset not rotated); height and radius unchanged.
Unit 7: Bodies and the sort-and-sweep world¶
What it is. The body record and its update, the body vector, the list of bodies sorted by AABB min x, the widest-body bound, and the box queries every shape, sweep, strike and ray test starts from. Read in the disassembly on 2026-10-12; every claim is confirmed (code) at the cited address unless it says otherwise.
- Coney:
src/physics/physics_world.{h,cpp}(body vector, x-sorted list,+0x14bound, the two box queries, the overlap query);play_level_modeowns one per level. The queries have no 512 cap of their own: the callers' lists hold 512 and nothing checks it (below); Coney uses a growable list and marks the difference. - Tests: Test ideas: the tie order of insert and reinsert, the backward walk's order and its missing upper x test, the filters, the bound refresh on the 120th tick, the overlap query's option bits.
19 functions, 4400 bytes (0 tagged).
| Address | Name | Size |
|---|---|---|
0x00341158 |
PhysicsNode_Init |
52 |
0x003418f8 |
PhysicsBody_Construct |
120 |
0x00341970 |
PhysicsBody_ConstructForOwner |
140 |
0x00341a00 |
PhysicsBody_Destroy |
104 |
0x00341a68 |
PhysicsBody_SetOwner |
64 |
0x00341aa8 |
PhysicsBody_AddShape |
56 |
0x00341ae0 |
PhysicsBody_GetShapes |
8 |
0x00341638 |
PhysicsBody_ComputeBounds |
36 |
0x003417c0 |
PhysicsBody_AddShapeBounds |
128 |
0x00341c10 |
PhysicsBody_Update |
516 |
0x00341e40 |
PhysicsBody_Moved |
84 |
0x003404a8 |
BodyList_Insert |
304 |
0x003405d8 |
BodyList_Remove |
80 |
0x00340628 |
BodyList_Reinsert |
64 |
0x00340668 |
IPhysics_AddBody |
152 |
0x00340700 |
IPhysics_RemoveBody |
156 |
0x0033feb0 |
IPhysics_QueryBox |
624 |
0x00340120 |
IPhysics_QueryBoxFiltered |
904 |
0x0033ebb8 |
IPhysics_OverlapShape |
808 |
Notation as in Unit 3: vectors are 4 floats, "xyz" leaves w alone, owner and body vtable slots are
named by the offset of their 8-byte entry (the function pointer is 4 bytes further). Arguments follow the EE ABI:
a0-a3, then t0-t3, then 64-bit stack slots at the caller's sp+0x00, +0x08, +0x10.
The records¶
The sort node is the first 0x30 bytes of a body: +0x00 AABB min, +0x10 AABB max, +0x20 index in the body
vector (s32), +0x24 previous and +0x28 next in the x-sorted list. The queries build bare nodes on their stack
for their boxes. The body is the node plus the fields of Bodies (0x80 bytes, vtable
0x00544710).
IPhysics fields used here (IPhysics): +0x08 list head (lowest min x), +0x12 u16 tick
counter,
+0x14 float bound (the widest body's max.x − min.x), +0x18 the overlap table, +0xf4 the mesh overlaps by
shape type, +0x4f450 the body vector (+0x04 begin, +0x08 end, +0x0c capacity end; zeroed by the constructor,
0x0033c5cc-0x0033c5dc, grown on demand by 0x004f0fa8). The constructor sets +0x08 = 0, +0x12 = 0 and
+0x14 = 0.0 (0x0033c86c, 0x0033c2d8, 0x0033c2dc).
Body vtable 0x00544710, read from memory: +0x08 PhysicsBody_Destroy, +0x10 PhysicsBody_Update, +0x18
PhysicsBody_Moved, +0x20 PhysicsBody_AddShape, +0x28 PhysicsBody_GetShapes, +0x30 IsIgnoring, +0x38
StopIgnoring, +0x40 Ignore (Unit 8), +0x48 PhysicsBody_GetContactScale (0x00341e18), +0x50
"you were pushed" (0x004f12a0). The human body's vtable 0x005446b0 replaces +0x08, +0x10, +0x18, +0x28,
+0x30-+0x50 (The human body, Unit 8) and keeps +0x20.
PhysicsNode_Init¶
0x00341158. Node* PhysicsNode_Init(Node* n): min = max = (0, 0, 0, 1) (the constant at 0x005116c0), +0x20 =
−1, +0x24 = +0x28 = 0; returns n.
PhysicsBody_Construct and PhysicsBody_ConstructForOwner¶
0x003418f8. Body* PhysicsBody_Construct(Body* b): vtable 0x00544710, PhysicsNode_Init, +0x30 first shape
= 0, +0x34 owner = 0, +0x38 ignore chain = 0, +0x3c group (s16) = 0, +0x40 flags = 0x80000400 (dirty and
"write the slid velocity back"), +0x44 type mask = 0, +0x50 push-out = (0, 0, 0, 1), +0x60 scale =
(1, 1, 1, 1) (0x005116d0), +0x70 counter = 0. Not written: +0x2c, +0x48, +0x4c.
0x00341970. Body* PhysicsBody_ConstructForOwner(Body* b, Owner* o): the same, but +0x34 = o and, only when
o is not null, +0x44 = the owner's type mask (owner vtable +0x20, 0x003419d8). It writes neither
+0x44 for a null owner nor the scale +0x60 (no store of 0x005116d0): a stack body built with it keeps
whatever the stack held there. Only the camera builds such bodies (Bodies); nothing in this unit
reads
+0x60, and Unit 3 shows the sweep ignores it, so Coney may set 1.0 (a marked difference).
PhysicsBody_Destroy¶
0x00341a00, vtable +0x08. void (Body* b, int flags): resets the vtable pointer; if +0x30 is not null,
deletes that first shape (SafeDelete_PhysicsBodyDestroy 0x004f1148: the shape's destructor, then the heap free
with this file's path 0x005771d0 and line 3309) and sets +0x30 = 0; then frees the body itself when flags & 1.
The pool free functions (Unit 9) clear +0x30 first, so pooled shapes are never deleted (inferred from
IPhysics_FreeObjectBody 0x0033cf00, which does so at 0x0033cf50).
PhysicsBody_SetOwner¶
0x00341a68. void (Body* b, Owner* o): +0x34 = o always; when o is not null, +0x44 = owner vtable +0x20
(the type mask). A null owner leaves +0x44 as it was.
PhysicsBody_AddShape and PhysicsBody_GetShapes¶
0x00341aa8, vtable +0x20. void (Body* b, Shape* s): if +0x30 is null, +0x30 = s; else walks the chain
through shape +0x00 and sets the last one's +0x00 = s. It does not write s+0x00 (the shape constructor
0x00342a30 zeroes it) and does not check whether s is already in the chain.
0x00341ae0, vtable +0x28. Shape* (Body* b, int, int, Owner*): returns +0x30; the three extra arguments the
callers pass ((1, 3, 0), (2, 4, owner)) are ignored. The human body's version answers its capsule chain.
PhysicsBody_ComputeBounds and PhysicsBody_AddShapeBounds¶
0x00341638. void (AABB* box, Shape* s): PhysicsShape_GetBounds(—, &box.min, &box.max, s) (0x00341190,
Unit 6); box is any 0x20-byte AABB, the body's own +0x00 or a stack box.
0x003417c0. void (AABB* box, Shape* s): the shape's bounds into a temporary, then box.min = min(box.min, t.min)
and box.max = max(box.max, t.max) per axis (VU0 vmini.xyz / vmax.xyz), then box.min.w = box.max.w = 1.0.
PhysicsBody_Update¶
0x00341c10, vtable +0x10. void PhysicsBody_Update(Body* b, Owner* o). Called by the owners' updates and by
the step's settle (body vtable +0x10 with the object).
- If
b+0x40 & 0x80000800is 0, return (0x00341c44). - Pose the first shape (shape vtable
+0x28witho; no null check, a body always has a shape),b's AABB = its bounds (PhysicsBody_ComputeBounds(b, first)). For each further shape of the chain: pose it, thenPhysicsBody_AddShapeBounds(b, s). Every shape is posed and bounded, enabled or not. BodyList_Reinsert(the global IPhysics at 0x00597198, b)(below); clear0x80000000.- If
b+0x40 & 0x801is exactly0x801(0x00341ce0), the update sweep:s= body vtable+0x28with(1, 3, 0);p= the shape's world position (shape vtable+0x20);d = p − s+0x20(xyz,w = 1): from its previous world position to its new one.- A stack node:
PhysicsNode_Init, then a copy ofb's min, max and+0x20word, and the box is extended byd(AABB_ExtendByMove, Unit 3). The box is the new AABB grown forward byd, so it covers the new pose and one moredbeyond it, not the path from the old pose (0x00341d34-0x00341d7c). count = 0;PhysicsBody_SweepHumanMask(phys, &box, b, s, contacts, sorted, &count, &d)(0x0033d498, Unit 11); 50 contacts of 0xc0 atsp+0x20, 50 pointers atsp+0x25a0.- If
count > 0:PhysicsBody_ResolveContacts(phys, b, sorted, count, &d, &V, &v, &dtOne, &moveFlags, &scale)(Unit 3) withV = d,vnot initialised (stack),dtOne = 1.0(unused by the resolution),moveFlags = 0,scalenot initialised. The return value,V,vandscaleare dropped; what remains is what the contact codes did to the owner (an advance along the normal, a push-out on the other body).
So a body with 0x801 meets bodies that lie ahead of its new position but can miss one it passed through in
this update (inferred from step 4.2; which bodies carry 0x801 is Unit 9's).
PhysicsBody_Moved¶
0x00341e40, vtable +0x18. void (Body* b): resets the first shape's previous position (shape vtable +0x30),
b's AABB = that shape's bounds only (PhysicsBody_ComputeBounds), then BodyList_Reinsert(0x00597198's IPhysics,
b). Other shapes are neither reset nor bounded, the dirty flag is not touched. The human body has its own
(0x00342828, The human body).
BodyList_Insert¶
0x003404a8. void BodyList_Insert(IPhysics* p, Body* b, Body* hint):
- Bound (
0x003404a8-0x003404c4):w = b.max.x − b.min.x; ifp+0x14 < w(strict),p+0x14 = w. This happens on every insert and so on every reinsert. - Empty list:
head = b,b.prev = b.next = 0; done. start = hint, or the head whenhintis 0.- Backward when
b.min.x < start.min.x(0x003404f0): walkn = start.prev, n.prev, …; at the firstnwithn.min.x < b.min.x(strict), linkbaftern. If the walk runs off the front,bbecomes the head (before the last node visited). Sobgoes before every node of equal min x it passed. - Forward otherwise (
b.min.x ≥ start.min.x,0x00340574): walkn = start.next, n.next, …; at the firstnwithb.min.x < n.min.x(strict), linkbbeforen. If the walk runs off the end,bis the tail. Sobgoes after every node of equal min x it passed, and afterstartitself.
The tie rule is therefore not "stable": a body lands after its equals when it is placed moving forward and before them when moving backward.
BodyList_Remove¶
0x003405d8. Body* BodyList_Remove(IPhysics* p, Body* b): returns b's old previous node (0 for the head).
If b is the head: head = b.next and, if that is not null, its prev = 0. Otherwise b.prev.next = b.next (when
b.prev is set) and b.next.prev = b.prev (when b.next is set). Then b.prev = b.next = 0. There is no
membership check: removing a body that is not in the list and has no links changes nothing and returns 0.
BodyList_Reinsert¶
0x00340628. void (IPhysics* p, Body* b): prev = BodyList_Remove(p, b), then BodyList_Insert(p, b, prev).
Consequences of the hint (confirmed (code); the examples computed from it):
- A body whose min x did not change still moves: from its old previous node it walks forward and lands after
every following node of the same min x. In
A(0) B(1) C(1) E(1) D(2)(min x in brackets), reinsertingBunchanged givesA C E B D. - A body that moves back lands before the equals it passes:
A(0) B(1) C(1) D(2)withDmoved to 0 givesD A B C; moved to 1 it walks forward fromCand staysA B C D. - The old head has no previous node, so it starts from the new head (its old successor).
IPhysics_AddBody and IPhysics_RemoveBody¶
0x00340668. void IPhysics_AddBody(IPhysics* p, Body* b): pushes b at the end of the body vector (growing it
through 0x004f0fa8 when full), b+0x20 = size − 1, then BodyList_Insert(p, b, 0): from the head, so a new body
goes after every body of equal min x. No check that b is already present.
0x00340700. void IPhysics_RemoveBody(IPhysics* p, Body* b):
IPhysics_ForgetIgnoredOwner(p, b)(Unit 8).- Swap-remove from the vector: when the size is 2 or more, the last body
Ltakesb's place (L+0x20 = b+0x20,vector[b+0x20] = L); then the size goes down by one (also when it is 1).b+0x20keeps its old value (not −1). BodyList_Remove(p, b).
So removing a body renumbers the last one; nothing outside these functions reads +0x20 except the queries, which
use it to fetch the body (below).
IPhysics_QueryBox¶
0x0033feb0. int IPhysics_QueryBox(IPhysics* p, Body** out, const AABB* box, Body* start, Owner* ignored, u32
typeMask, u32 flagMask) → the number written to out. Leaf function (no calls).
Keep test for a node n (the same in both walks): B = vector[n+0x20] (the body is fetched through the
vector; for a body in the list this is n itself, so Coney may use the node), then, in this order:
B+0x34 == ignored→ drop. (Withignored= 0, ownerless bodies are dropped and every owned body passes.)B+0x44 & typeMask == 0→ drop. So a body with type mask 0 is never found, andtypeMask0 finds nothing.flagMask != 0andB+0x40 & flagMask == 0→ drop.- y and z, inclusive, on the node's box: keep when
n.min.y ≤ box.max.y,box.min.y ≤ n.max.y, and the same for z. The x tests belong to the walks below.
The walk (0x0033feb4-0x00340118):
- Without
start: go to step 3 from the head. - With
start, backward fromstartitself (startis tested too): for eachn = start, start.prev, …:n.max.x < box.min.x→ skip it, go on ton.prev(no bound test);- else, if
box.min.x − n.min.x > p+0x14(strict) → stop the backward walk; - else apply the keep test and append; go on to
n.prev.
When it stops or runs off the front, continue with step 3 from start.next.
3. Forward from the given node: stop at the end or at the first n with box.max.x < n.min.x; skip n when
n.max.x < box.min.x; else the keep test and append.
What follows from this, confirmed (code) for the branches and worked out from them:
- Order: the backward hits come first, in reverse list order (from
starttowards the head), then the forward hits in list order. Not one list order. - The bound never stops the walk while it is current. The stop is tested only on a node that reaches the box
(
n.max.x ≥ box.min.x), whose own width is then at leastbox.min.x − n.min.x; and+0x14is at least every width as long as each body was inserted after its last box change (whichUpdate,Movedand the human body's update all do). So the backward walk runs to the head; the bound matters only if it is smaller than a body's width (inferred; it changes the cost, not the answer). - No upper x test going backward. A body before
startis kept even when its min x is beyondbox.max.x. With the usual callers the box containsstart's own box, so every body beforestarthasmin.x ≤ start.min.x ≤ box.max.xand nothing changes (inferred). A test can show it with a box placed left ofstart. startis a candidate like any other; the callers drop it by passing its owner asignored(all four do, below).- A
startthat is not in the list (no links, not the head) yields at most itself: the backward walk sees onlystart, the forward walk begins at its nullnext. A query must start from a listed body or from none.
IPhysics_QueryBoxFiltered¶
0x00340120. int IPhysics_QueryBoxFiltered(IPhysics* p, Body* query, Body** out, const AABB* box, Body* start,
Owner* ignored, u32 typeMask, u32 flagMask, u32 excludedFlags, u32 excludedState, int (*keep)(Owner* mine, Owner*
theirs)): a0-a3, t0-t3, then the caller's sp+0x00, +0x08, +0x10 (read at 0x003401d8, 0x003401fc,
0x00340174). The same two walks as IPhysics_QueryBox, with these filters after the first three (owner, type mask,
flag mask) and before y and z (0x003401d8-0x00340224, mirrored at 0x00340388-0x003403d4):
- (4)
B+0x40 & excludedFlags != 0→ drop (0 excludes nothing). - (5)
state= the candidate owner's state flags (owner vtable+0x50, the 64-bit word of Unit 3), called for every candidate that got this far, even whenexcludedStateis 0, with no null check: an ownerless body reaches it unlessignoredis 0 (filter 1 then drops it).state & excludedState != 0→ drop. The mask is a 32-bit load, sign-extended (bit 31 also tests the high word). - (6) When
keepis not null:keep(query+0x34, B+0x34)returns 0 → drop.queryis read only here.
The only caller is IPhysics_CollideShape (0x0033e91c, Unit 11), which passes its query body as both
query and start, the stack box, and these from its query record (0x0033e890-0x0033e8f8):
| Argument | Option bit | Field when set | Value when clear |
|---|---|---|---|
ignored |
0x2000 |
+0x0c |
0 |
typeMask |
0x10 |
+0x14 |
−1 |
flagMask |
0x800 |
+0x1c |
0 |
excludedFlags |
0x1000 |
+0x20 |
0 |
excludedState |
0x10000 |
+0x24 |
0 |
keep |
0x40000 |
+0x38 |
null |
So without option 0x2000 the querying body itself is a candidate (ignored = 0); IPhysics_CollideShape
then drops it only through its own group test and its owner's +0x110 answer. Which callers set which bits, and
which keep functions exist, is Unit 11's (callers not traced here).
IPhysics_OverlapShape¶
0x0033ebb8, 0x328 bytes. int IPhysics_OverlapShape(IPhysics* p, Contact* out, const OverlapQuery* q) → contacts
written to out. The record and the contact it fills are on Unit 4; here, every bit
and limit.
Options read (q+0x10, at 0x0033ebfc, 0x0033ec20, 0x0033ec7c, 0x0033ed20, 0x0033ee5c): exactly
0x1 (the mesh), 0x2 (bodies), 0x10 (type mask +0x14, else −1), 0x80 (skip the same non-zero group),
0x100 (ask the querying owner, vtable +0x110), 0x200 (only shapes with flag 0x1), 0x400 (q+0x08 is a
pointer to the box to use; read with a word load at 0x0033ec10). No other bit is read, and q is not passed
on: the push sphere's 0x2000 (0x2012) and the climb's 0x4 and 0x8 (0xd) have no effect here.
- The box:
*q+0x08with0x400, else the querying shape's bounds into a stack node (PhysicsBody_ComputeBoundsonq+0x04). - Without
0x2: no bodies, go to step 5 with 0 contacts. - Candidates:
n = IPhysics_QueryBox(p, list, box, q+0x00, (q+0x00)+0x34, typeMask, 0)into a 512-entry stack list (sp+0x30-sp+0x82c). The querying body isstart, so it must be in the list (above). - Each candidate
C, in the order the query returned them, whileq+0x18(max) is not 0 (tested before each candidate; no other early stop, not whenmaxor 50 contacts are reached):0x80:C+0x3c(u16) and the querying body's+0x3cboth non-zero and equal → skipC.0x100: querying owner's vtable+0x110with(owner, C+0x34)returns 0 → skipC.- shapes =
C's vtable+0x28with(2, 4, querying owner); null → skip. - Each shape
sof the chain (with0x200, only those with flag0x1):fn = overlap[own.type][s.type](p+0x18 + own.type × 0x14 + s.type × 4); null → next shape. Elseslot = contacts[count](50 × 0xc0 atsp+0x830), and iffn(slot, own, s)is non-zero:slot+0xb4 = 0, thenContact_InsertSorted(p, slot, sorted, &count, querying owner, C+0x34, own, s)(Unit 3). All shapes ofCare tested (several contacts per candidate are possible).
- Copy out
k = min(count, max)contacts, in sorted order, 0xc0 bytes each (0x0033eddc-0x0033ee44). - The mesh: if
k < max(unsigned) and0x1, and the querying shape's type has a mesh overlap (p+0xf4 + type × 4):k += meshOverlap(q+0x28, q+0x2c, out + k, max − k, &box, q+0x00)(floats inf12,f13; thena0-a3;0x0033ee84-0x0033eea8). Returnk.
Limits, none checked (0x0033ec44, 0x0033ed10, 0x0033ed8c): 512 candidates, 50 contact slots, 52 sorted
pointers (sp+0x2db0-sp+0x2e7f). A 51st hit writes its contact over the sorted list, and the insert writes
pointers past it; a 513th candidate writes over the contact slots. Inferred: not reached in play (the four callers
use small boxes and max 1 to 32).
The 512 candidates¶
Neither query has a cap, and neither is told the size of out. Every caller provides 512 entries:
IPhysics_OverlapShape (sp+0x30, 0x800 bytes), IPhysics_CollideShape (sp+0x26a0-sp+0x2e9f),
PhysicsBody_SweepHumanMask (sp+0x000-sp+0x7ff), IPhysics_RayCastBodies (sp+0x20-sp+0x81f); in
Human_TestStrikes the list is at sp+0x130 and the next local at sp+0x990 (room for 536). Confirmed (code) for
the four frames; past 512 the original writes over its own stack (whatever lies after the list), which is undefined
behaviour, not a rule to reproduce. The pools allow 60 + 1 + 416 + 255 + 18 = 750 bodies, so a level-sized box could
pass 512 (inferred; no caller asks for one). Coney: a growable list, and a debug assertion at 512 so a test can see
the original's limit.
The callers' arguments¶
| Caller | Query | start |
ignored |
typeMask |
flagMask |
|---|---|---|---|---|---|
IPhysics_OverlapShape 0x0033ec58 |
box | the query body | its owner | +0x14 or −1 |
0 |
PhysicsBody_SweepHumanMask 0x0033d598 |
box | the moving body | its owner | −1 | the mover's layer mask (Sweeping a body) |
Human_TestStrikes 0x0033f4bc |
box (the human body's broad bound +0x90) |
none | the striker's owner | −1 | 0x18 |
IPhysics_RayCastBodies 0x0033efe8 |
box | none | its t0 argument |
its a3 argument |
0x2000 |
IPhysics_CollideShape 0x0033e91c |
filtered | the query body | table above |
The bound refresh¶
In IPhysics_Step (0x00340940-0x00340994), before the settle slots: +0x12 += 1 (u16); if it is now below 120
(0x78), nothing else. Otherwise +0x12 = 0, +0x14 = 0.0, and for each body of the list from the head, +0x14 =
max(+0x14, max.x − min.x) (strict >). So after construction the refresh runs on the 120th, 240th, … step, and
between refreshes the bound only grows (on inserts). The refresh reads the bodies' current boxes; an empty list
leaves 0.
Test ideas¶
Synthetic bodies with boxes only (y and z [0, 1] unless said), owners as distinct tags, type mask −1, flags 0. The
numbers come from a model of this spec (computed 2026-10-12).
- Insert ties. Add
A(0) B(1) C(1) D(2)(min x; width 0.5) → listA B C D. AddE(1)→A B C E D(after its equals). Bound 0.5. - Reinsert unmoved. From test 1, reinsert
Bwithout changing it →A C E B D. - Reinsert backward.
A(0) B(1) C(1) D(2);Dmoved to[0, 0.5], reinsert →D A B C(before its equalA).Dmoved to[1, 1.5]instead →A B C D. - Remove returns the previous node. In
A B C, removingBreturnsA; removing the headAreturns null and makesBthe head withB.prevnull. - Swap-remove. Add
A, B, C(indices 0, 1, 2); removeA→ vectorC, B,C+0x20 = 0,A+0x20still 0. - Query order. Bodies
W[0, 10] P[1, 2] Q[6, 7] S[8, 9] R[8.5, 9.5] T[11, 12](list in that order, bound 10).QueryBox(box x [8, 9], start S, ignored S)→W, R(backward:Sdropped as ignored,QandPskipped on max x,Wkept; forward:Rkept,Tstops it). With no start →W, R. Box[8, 12], startS,ignorednone →S, W, R, T. - No upper x test backward. Same bodies, box x
[0, 0.2], startS, ignoredS→Q, P, W; a brute-force overlap gives onlyW. - A stale bound stops the walk. Bodies
W[0, 10] Q[6, 7] S[8, 9], bound forced to 1.0, box[8, 9]fromS, ignoredS→ nothing (8 − 0 > 1stops atW). With the true bound 10 →W. - Filters. A body with type mask 0 is never found;
typeMask0 finds nothing;flagMask0x4finds only bodies with0x4;ignored= 0 drops an ownerless body; touching y or z boxes (max.y == box.min.y) are found. - Filtered query. As test 6 with
excludedFlags 0x200onR→W; with a fake state0x4000000onW's owner andexcludedState 0x4000000→R; withkeeprefusingW's owner →R; the state callback is called once per candidate that passed the first four filters, also withexcludedState0. - Bound refresh.
A[0, 4],B[5, 6]→ bound 4; shrinkAto[0, 1]and reinsert → still 4. After step 1: counter 1, bound 4; after step 119: 119, 4; after step 120: counter 0, bound 1; step 121: counter 1, bound 1. - Overlap options. A fake overlap entry that always hits: with
max0, no candidate is tested (fake called 0 times); with options0x2012the result equals options0x12; two shapes on one candidate give two contacts; with0x80and both groups 7 the candidate is skipped, with groups 7 and 0 it is not. - Update sweep box. A sphere body (radius 0.35) whose shape moved from x 0 to x 1 with flags
0x801: the candidate box is x[0.65, 2.35], so a body at x[0.1, 0.4]is not met and one at[2.0, 2.2]is. - Update gate. Flags 0:
Updateposes nothing and does not reinsert; flags0x800: poses and reinserts every call, keeping0x800; flags0x80000000: poses, reinserts and clears it, so the next call does nothing.
Unit 8: Ignore lists and shape pools¶
What it is. The 60 ignore slots a body uses to pass through particular owners, the human body's two extra rules, and the fixed shape pools with their allocators. Read in the disassembly on 2026-10-12; all confirmed (code) at the cited addresses unless marked.
- Coney:
src/physics/physics_world.*: fixed pools with the original's counts and "first free" scans, so running out and reuse behave the same; the ignore slots as one array of 60. - Tests: Test ideas: the 61st
Ignore, slot reuse, pointer matching, the grab partner, the pool limits and the flags a freed shape keeps.
14 functions, 1032 bytes (0 tagged).
| Address | Name | Size |
|---|---|---|
0x00340890 |
IPhysics_FindFreeIgnoreSlot |
68 |
0x0033cd70 |
IPhysics_ForgetIgnoredOwner |
140 |
0x00341ae8 |
PhysicsBody_IsIgnoring |
56 |
0x00341b20 |
PhysicsBody_Ignore |
92 |
0x00341b80 |
PhysicsBody_StopIgnoring |
140 |
0x003421d0 |
PhysicsHumanBody_IsIgnoring |
92 |
0x00342230 |
PhysicsHumanBody_Ignore |
48 |
0x00342260 |
PhysicsHumanBody_StopIgnoring |
36 |
0x0033d1a0 |
IPhysics_AllocObjectSphere |
68 |
0x0033d1e8 |
IPhysics_AllocObjectBox |
68 |
0x0033d230 |
IPhysics_AllocCarBox |
68 |
0x0033d278 |
IPhysics_AllocBigSphere |
80 |
0x0033d2c8 |
IPhysics_FreeShape |
16 |
0x003408d8 |
IPhysics_FindFreeStrikeSphere |
60 |
The ignore slot¶
60 slots of 12 bytes at IPhysics +0xf4e0, zeroed by the constructor (720 bytes, 0x0033c830): +0x00 the body
whose list holds it, +0x04 the ignored owner, +0x08 the next slot of that body's chain. A slot is free when
+0x04 is 0. The chain starts at body +0x38. Owners are matched by pointer equality on the owner object
(the task object a body's +0x34 holds), so ignoring an owner passes through every body that owner has.
IPhysics_FindFreeIgnoreSlot¶
0x00340890. Slot* (IPhysics* p): the lowest-index slot with +0x04 == 0, or null when all 60 are in use. It
does not mark the slot.
PhysicsBody_IsIgnoring¶
0x00341ae8, body vtable +0x30. bool (Body* b, Owner* o): false when o is null; else walks the chain from
+0x38 and answers true at the first slot with +0x04 == o.
PhysicsBody_Ignore¶
0x00341b20, vtable +0x40. void (Body* b, Owner* o): nothing when o is null. Else slot =
IPhysics_FindFreeIgnoreSlot(*0x00597198) (the global IPhysics, not an argument); none → nothing. Else slot+0x04 =
o, slot+0x08 = b+0x38, slot+0x00 = b, b+0x38 = slot: pushed at the front. There is no duplicate check:
ignoring the same owner twice uses two slots.
PhysicsBody_StopIgnoring¶
0x00341b80, vtable +0x38. void (Body* b, Slot* s): the argument is a slot, not an owner.
snot null: ifsis the chain head,b+0x38 = s+0x08; else walk the chain for the slot whose+0x08 == sand set it tos+0x08. Then, found or not,s+0x00 = s+0x04 = s+0x08 = 0(freed).snull: every slot of the chain, from the head:+0x04 = 0,+0x08 = 0,+0x00 = 0;b+0x38ends null.
Only IPhysics_ForgetIgnoredOwner passes a slot; the free functions pass null (Unit 9, for example
0x0033cf3c).
IPhysics_ForgetIgnoredOwner¶
0x0033cd70. void (IPhysics* p, Body* removed): for slot 0 to 59, if slot+0x04 == removed+0x34, call
slot+0x00's vtable +0x38 with that slot (the slot's body, which may be a human body). Called first by
IPhysics_RemoveBody (Unit 7). It frees the slots in other bodies' lists that name the removed
body's owner; the removed body's own list is freed by its free function (StopIgnoring(null)). An ownerless
body would match every free slot, whose body is null: undefined, never done (inferred: every free function runs
while the owner is still set, 0x0033cf0c before 0x0033cf48).
The human body's three¶
Human body vtable 0x005446b0: +0x30 0x003421d0, +0x38 0x00342260, +0x40 0x00342230. The human body's
+0xd4 is a word: non-zero = "ignore the level".
PhysicsHumanBody_IsIgnoring(0x003421d0),(hb, o):onull → returnshb+0xd4(the level). Else, ifois the owner human's grab partner (Human_GetGrabOther0x002269d8onhb+0x34: the other human of the human's grab record, 0 when none) → true. ElsePhysicsBody_IsIgnoring(hb, o).PhysicsHumanBody_Ignore(0x00342230),(hb, o):onull →hb+0xd4 = 1; elsePhysicsBody_Ignore.PhysicsHumanBody_StopIgnoring(0x00342260),(hb, s):snull →hb+0xd4 = 0, thenPhysicsBody_StopIgnoring(hb, null)(all slots too); elsePhysicsBody_StopIgnoring(hb, s).
Who asks IsIgnoring (body vtable +0x30) is not traced here: the static calls are only the human's to the base
(0x00342204); the owners' +0x110 answers are the likely askers (Unit 11). Inferred.
The shape pools¶
Every shape is built once by the IPhysics constructor (flags 0x1, enabled, by PhysicsShape_Construct
0x00342a30; 0x8 clear). The allocators scan from index 0 and take the first shape whose test passes; past the
end they return null. They write only +0x32 (and the big sphere's radius): type, bone, offset and size keep the
previous user's values until the caller sets them.
| Pool | IPhysics offset | Count × size | Allocator | Free when | On allocation |
|---|---|---|---|---|---|
| object spheres | +0x22930 |
416 × 0x60 | IPhysics_AllocObjectSphere 0x0033d1a0 |
flag 0x8 clear |
sets 0xb |
| object boxes | +0x2c530 |
416 × 0x90 | IPhysics_AllocObjectBox 0x0033d1e8 |
flag 0x8 clear |
sets 0xb |
| car boxes | +0x4c720 |
36 × 0x90 | IPhysics_AllocCarBox 0x0033d230 |
flag 0x8 clear |
sets 0xb |
| 2.0 m spheres | +0x4db60 |
30 × 0x60 | IPhysics_AllocBigSphere 0x0033d278 |
flag 0x8 clear |
radius +0x40 = 2.0 (0x40000000), sets 0xb |
| weapon strike spheres | +0xf7b0 |
60 × 0x60 | IPhysics_FindFreeStrikeSphere 0x003408d8 |
flag 0x1 clear |
nothing: the caller enables it |
| ignore slots | +0xf4e0 |
60 × 12 | IPhysics_FindFreeIgnoreSlot 0x00340890 |
+0x04 == 0 |
nothing; Ignore fills it |
The counts are the loop limits 0x1a0, 0x1a0, 0x24, 0x1e, 0x3c, 0x3c (signed compares for the shape
pools). 0xb = enabled 0x1, 0x2, allocated 0x8. The strike spheres are switched off and given 0x2 by the
constructor (0x0033c7e0-0x0033c814), so they start free; a strike sphere stays taken while it is enabled, and the
same one is returned again until its caller enables it.
IPhysics_FreeShape (0x0033d2c8), (p, shape): clears only 0x8. A freed pool shape keeps 0x1 and 0x2
(flags 0x3 after a plain allocate and free), so it still counts as enabled to any code that reaches it through a
stale pointer; the next allocation sets 0xb again. The other pools (the human sets, the world-object, glass and car
bodies, glass boxes) are taken by the create functions of Unit 9 (a body is free when its owner +0x34
is 0, Bodies).
Test ideas¶
- Sixty slots. 60
Ignorecalls with distinct owners on one body take slots 0 to 59 in order; the chain reads 59, 58, …, 0 from+0x38; a 61stIgnorechanges nothing (the chain and all slots unchanged). - Reuse. Free slot 5 (
StopIgnoring(b, slot 5)); the nextIgnoretakes slot 5 and puts it at the chain's front. - Duplicates.
Ignore(o)twice → two slots;IsIgnoring(o)true;ForgetIgnoredOwnerof a body owned byofrees both. - Null owner. Base body:
Ignore(null)takes no slot,IsIgnoring(null)is false. Human body:Ignore(null)sets+0xd4 = 1,IsIgnoring(null)returns 1;StopIgnoring(null)sets it to 0 and frees all its slots. - Stop a slot not in the chain.
StopIgnoring(b, s)withsfrom another body's chain:b's chain unchanged,szeroed (its own chain then still points at it: the original's behaviour, a test of the zeroing only). - Forget. Bodies
X(ownerx) andY,Zboth ignoringxandYignoringz; removingXfrees the twoxslots and keepsY'szslot. - Grab partner. A human body whose owner's grab partner is
g:IsIgnoring(g)true with an empty chain; with no grab, false. - Pools. 416
AllocObjectSpherecalls succeed, the 417th returns null;FreeShapeon the 3rd, then the next allocation returns the 3rd. Flags after allocate0xb, after free0x3, after a second allocate0xb.AllocBigSpheresets radius 2.0 even if the slot held 0.5. 37AllocCarBox→ the 37th is null. - Strike spheres.
FindFreeStrikeSpheretwice without enabling returns the same sphere; after enabling it, the next; with all 60 enabled, null.
Unit 9: Creating and freeing bodies¶
What it is. The pools' create and free functions: which slot is taken, what each kind of body starts with, the human list, and what a free leaves behind. All confirmed (code) at the cited addresses (read in the disassembly, 2026-10-12) unless a line says otherwise.
- Coney:
src/physics/physics_world.*(fixed pools with the original's counts, so running out behaves the same); callers insrc/human/human.cpp(Human_CreatePhysicsBody's rules, below),src/world_objects/object_bodies.*,cars.*,glass.*. - Tests: Test ideas: each kind's starting flags (human
0x8002243f, then0x8002203f/0x8002223ffor an AI; punch bag0x8000243fthen0x8000263f; object0x80000500; car0x80000000, then0x8002211aafter its boxes; pane0x8000007a), shapes and scales; the first free slot is reused (the heal keeps the same body pointer, as seen at runtime); a full pool returns null; the body vector's swap-remove.
12 functions, 1752 bytes (0 tagged).
| Address | Name | Size |
|---|---|---|
0x0033cc48 |
IPhysics_CreateHumanBody |
292 |
0x0033ce00 |
IPhysics_FreeHumanBody |
84 |
0x0033cb08 |
IPhysics_CreatePunchBagBody |
228 |
0x0033cbf0 |
IPhysics_FreePunchBagBody |
84 |
0x0033ce58 |
IPhysics_CreateObjectBody |
168 |
0x0033cf00 |
IPhysics_FreeObjectBody |
100 |
0x0033cf68 |
IPhysics_CreateCarBody |
140 |
0x0033cff8 |
IPhysics_FreeCarBody |
120 |
0x0033d070 |
Physics_CreateBoxBody |
228 |
0x0033d158 |
IPhysics_FreeBoxBody |
68 |
0x003407a0 |
IPhysics_AddHumanBody |
88 |
0x003407f8 |
IPhysics_RemoveHumanBody |
152 |
Notation as in Unit 3: vtable slots are named by their adjust / function offsets (+0x38 / +0x3c);
"enable(shape, on)" is the shape vtable +0x18 / +0x1c slot, 0x004f11c8 for every shape type: on sets flags
0x11 (enabled and reset-previous, Unit 6), off clears 0x1 only.
What the constructor leaves¶
The pools are built once by IPhysics_Construct (0x0033c288); a create function overwrites only some fields, so the
rest is whatever the constructor or the slot's last user left. Confirmed (code) at 0x003418f8 (body),
0x00341e98 (human body), 0x00342a30 (shape head):
- Body (
PhysicsBody_Construct):+0x30first shape 0,+0x34owner 0,+0x38ignore list 0,+0x3cgroup 0,+0x40flags0x80000400,+0x44type mask 0,+0x50push-out (0, 0, 0, 1),+0x60scale (1, 1, 1, 1),+0x700, the sort node cleared (PhysicsNode_Init, index+0x20= −1). - Human body (
PhysicsHumanBody_Construct): the body, then+0x80=+0x84= 0, the broad bound+0x90's node initialised,+0xc0=+0xc4= 0, flags|= 0x3f(so0x8000043f),+0xc8=+0xcc=+0xd0=+0xd4= 0. - Each human body set i (0 to 59) gets the capsule i of
+0x10e30(PhysShapeCapsule_SetSize(2.0, 0.35),0x40000000,0x3eb33333) as its first shape+0x30, and ten bone shapes chained on+0x80in this order (PhysicsHumanBody_AddBoneShape0x003429e8appends at the tail): the five segments 5i to 5i+4 of+0x71a0for bones 3, 24, 18, 32, 29, then the five spheres 5i to 5i+4 of+0x120for bones 6, 25, 19, 33, 30. Their offsets, radii and lengths come from the tables at0x00512810(segment offsets, 16 bytes each),0x005128b0(segment radii),0x005128e0(segment lengths),0x00512860(sphere offsets) and0x005128c8(sphere radii), index k of each table going with the k-th bone of its list; the values are on Combat. Flags: the shape head's0x1, plus0x2, plus0x4for bone 3 and bone 6 (0x7; the others0x3). The capsule's flags are0x1. - The 60 weapon strike spheres (
+0xf7b0) are switched off and get0x2(flags0x2). The 30 push spheres (+0x4db60) are plain spheres (bone −1, offset 0, flags0x1). - The punch bag's set (
+0x4f2a0): capsule+0x4f3f0(2.0, 0.35) as+0x30, one segment+0x4f380as+0x80with offset 0, radius 0.35 (+0x4f3e0), length 1.5 (0x3fc00000), bone 3 and flags set to0x4outright (not enabled, a strike target, not a striker).
Taking a slot¶
Every create function takes (phys, owner) in a0, a1 (the box body also two floats, below) and scans
its pool from index 0 for the first body whose owner +0x34 is 0; a full pool returns 0 (null) and changes
nothing. On the slot it calls PhysicsBody_SetOwner(body, owner) (0x00341a68: +0x34 = owner and, when owner is
not null, +0x44 = the owner's type mask from its vtable +0x20 / +0x24), writes the fields below, and ends with
IPhysics_AddBody (0x00340668), which appends the body to the body vector (+0x4f450; +0x20 = its index, the
size before the append) and inserts it into the x-sorted list (Queries) with its stale AABB:
the
dirty bit makes the next update re-pose and re-sort it.
| Kind | Function | Pool (count, stride) | Writes | Then |
|---|---|---|---|---|
| human | IPhysics_CreateHumanBody 0x0033cc48 |
+0x124b0 (60, 0xe0) |
flags 0x8002243f, scale (1, 1, 1, 1), group 0, +0x70 0, +0xc0 0, +0xc4 0, +0xd0 0; enable(capsule, on) then capsule flags &= ~0x2; every bone shape enable(off) |
IPhysics_AddHumanBody, IPhysics_AddBody, PhysicsBody_ApplyHumanScale (Unit 10) |
| punch bag | IPhysics_CreatePunchBagBody 0x0033cb08 |
the one set +0x4f2a0, no free test |
flags 0x8000243f, scale 1, group 0, +0x70 0, +0xc0 0, +0xc4 0, +0xd0 0; enable(capsule, on), &= ~0x2; enable(segment, off) |
IPhysics_AddHumanBody, IPhysics_AddBody; no scale step |
| world object | IPhysics_CreateObjectBody 0x0033ce58 |
+0x15930 (416, 0x80) |
flags 0x80000500, group 0, +0x70 0, scale 1; no shape (the caller adds one) |
IPhysics_AddBody |
| car | IPhysics_CreateCarBody 0x0033cf68 |
+0x4be20 (18, 0x80) |
flags 0x80000000, group 0, +0x70 0; scale not written; no shape |
IPhysics_AddBody |
| glass pane | Physics_CreateBoxBody 0x0033d070 |
+0x3af30 (255, 0x80) with box i of +0x42eb0 |
flags 0x8000007a, group 0, +0x70 0, scale not written, the box (below) |
IPhysics_AddBody |
The human flags 0x8002243f are dirty, THROWNWEAPONTARGET (0x20000), RAYCASTTARGET (0x2000), 0x400 (write
the slid velocity back, Unit 3) and 0x3f (0x1 sweep against bodies, then the layers 0x2, 0x4,
0x8, 0x10, 0x20); the punch bag's lack 0x20000. Quirks of the creates, confirmed (code) by what they do not
write: none clears the pending push-out +0x50, the ignore list +0x38 or (human) +0x84 and +0xd4; the frees
clear those that matter (Freeing), so a body is only stale if its last free skipped them (the pane, below).
None writes +0x00-+0x1c (the AABB). A car's and a pane's scale is never written by anyone in these units, so it
stays the constructor's 1 (inferred: no other writer of a car or pane body's +0x60 was found).
The box body¶
Physics_CreateBoxBody(phys a0, owner a1, width f12, height f13) (0x0033d070; the name lists its floats first as
the decompiler showed them). For pool index i it uses box i of +0x42eb0 (the two pools advance together):
PhysicsBox_SetSize(width, 0.5, height, box) (0x00342dc0, which halves its three sizes: half-extents
(width / 2, 0.25, height / 2), +0x4c = 1), box flags |= 0x9 (enabled, allocated), body +0x30 = the box.
The only caller is GlassPane_CreateBody (0x0038ed68) with the pane's +0xcc and +0xd4 (width and height,
Objects), which then calls the body's update (vtable +0x10 / +0x14) with the pane, so the box
is posed by the pane's matrix at once.
The human list¶
IPhysics_AddHumanBody(phys, body) (0x003407a0) appends at the tail of the list headed at IPhysics +0x04
(next +0xc8, previous +0xcc; an empty list makes the body the head with both links 0). IPhysics_RemoveHumanBody
(0x003407f8): first the body's StopIgnoring(null) (vtable +0x38 / +0x3c; for a human body
PhysicsHumanBody_StopIgnoring 0x00342260: +0xd4 = 0 and every slot of its ignore list freed), then the unlink
(the head moves to the next, whose previous becomes 0; otherwise previous.next = next and next.previous = previous),
then PhysicsHumanBody_ClearStrikeSpheres (0x00342168, Unit 10), then both links 0. Nothing here
reads the list; who walks it is not traced in these units.
Freeing¶
All take (phys, body). IPhysics_RemoveBody (0x00340700) is common to all: IPhysics_ForgetIgnoredOwner
(0x0033cd70: every ignore slot in any body's list that names this body's owner is freed through that body's
StopIgnoring), then a swap-remove from the body vector (when it holds two or more, the last body moves into
this one's index and takes its +0x20; the removed body's +0x20 is not reset), then BodyList_Remove.
| Function | Steps, in order |
|---|---|
IPhysics_FreeHumanBody 0x0033ce00 / IPhysics_FreePunchBagBody 0x0033cbf0 (identical) |
IPhysics_RemoveHumanBody, IPhysics_RemoveBody, owner 0, PhysicsHumanBody_SetPushSphere(body, 0) (frees the push sphere, Unit 10). Flags, +0xc0 and the bone shapes are left as they are (the next create resets them) |
IPhysics_FreeObjectBody 0x0033cf00 |
IPhysics_RemoveBody; the first shape's 0x8 cleared (not its other flags); StopIgnoring(null); owner 0; +0x30 = 0 |
IPhysics_FreeCarBody 0x0033cff8 |
IPhysics_RemoveBody; each shape unchained in turn (its next 0, its 0x8 cleared, +0x30 = the following one), ending with +0x30 = 0; StopIgnoring(null); owner 0 |
IPhysics_FreeBoxBody 0x0033d158 |
IPhysics_RemoveBody; the box's 0x8 cleared; owner 0. No StopIgnoring and +0x30 kept (the next create of that slot rewrites it with the same box) |
Owner 0 is written with PhysicsBody_SetOwner(body, 0), which leaves the type mask +0x44 as it was.
The callers' rules¶
Human_CreatePhysicsBody (0x0021ce08), confirmed (code): if the human has a body (+0x1a0) it is freed first
(Human_FreePhysicsBody 0x0021cf20: the punch bag's free for class 0x1cc, else the human free; +0x1a0 = 0).
For class 0x1cc (HeavyBag) the punch bag's set, flags |= 0x200. Otherwise a human body; then when the human's
brain +0x04 is 0 (the player, inferred from the 1.4286 scale Characters gives the player) flags
|= 0x400 (already set) and scale +0x60 = (1.4286, 1.4286, 1, 1) (0x3fb6db6e); otherwise flags &= ~0x400;
then, when human byte +0x3bf is 0, flags |= 0x200. Corroboration: the runtime flags of Layers
(humans
0x2203f and 0x2223f, cars 0x2211a, panes 0x7a) are these values without the dirty bit.
Car_MakeBodies (0x00387d50) ORs 0x2211a into its body's flags at 0x00387ea8, so a car's body reads
0x8002211a (not 0x8002231a).
Test ideas¶
A fake owner (type mask 0x40 for a human, 8 for an object, scale attribute s). Values from this spec.
- Human create. Fresh world, owner with scale 1: returns pool slot 0; flags
0x8002243f; capsule flags0x11; bone shapes0x6(bones 3, 6) and0x2(the rest);+0x60= (1, 1, 1, 1);+0x20= 0; human list = [it]; after the scale step the capsule is height 2.0, radius 0.35 and the body is still dirty until its update. - Scale 1.1 (not in a scene): capsule height 2.2, radius 0.385; spine segment offset (−0.077, 0, 0.055), radius
0.198, length 0.418; forearms radius 0.077, length 0.22; shins 0.11, 0.44; head offset (0.055, 0, 0.033), radius
0.165; hands offset (0.088, 0, 0), radius 0.099; feet 0.165 (float32 values to 1e-6). In a scene state the same
human reads scale 1.0 (below 1.1,
Human_GetScale0x0021d020). - Pool limits. 60 human creates succeed, the 61st returns null; 416 objects, 18 cars, 255 panes likewise.
- Reuse. Create A, B, C (slots 0, 1, 2); free B; the next create returns slot 1 (lowest free), not slot 3.
- Swap-remove. Bodies A, B, C in the vector at 0, 1, 2; remove A: C is at index 0 with
+0x20= 0, size 2, A's+0x20still 0. - Player and AI. Player: flags
0x8002243f, scale (1.4286, 1.4286, 1, 1); AI with+0x3bf= 1:0x8002203f; AI with+0x3bf= 0:0x8002223f; punch bag:0x8000263f, segment flags0x4, capsule0x11. - Car. Create:
0x80000000, no shape; after its boxes:0x8002211a. Free: both boxes lose0x8,+0x30= 0. - Pane.
CreateBoxBody(1.2, 2.0): half-extents (0.6, 0.25, 1.0), box flags gain0x9; free clears0x8only. - Free human. A human body with a push sphere, two ignore slots and
+0xd4= 1: after the free, the sphere's0x8is clear, both slots are free,+0xd4= 0, the list no longer holds it, its owner is 0; another body's slot naming its owner is freed too.
Unit 10: The human body set: update, strikes and push sphere¶
What it is. What the human body adds to a body: the scale step, the per-update pose and broad bound, the strike switch, the strike test's gathering (the parts Unit 5 does not cover), the weapon spheres and the push sphere. All confirmed (code) at the cited addresses (disassembly read 2026-10-12) unless a line says otherwise.
- Coney:
src/human/human.*(body update after the move, the push),src/combat/*(Human_TestStrikes: the gather, the line-of-sight ray and the per-candidate loop below, sweeping each enabled strike shape from its last pose through the sweep table: Unit 5 for boxes, Unit 11 for humans),src/platform/play_level_mode.*(strikeObjects()). Issue #72's step 1 (strikes hit cars) needs Unit 5, the gather and the sweep loop, not the whole of this unit. - Tests: Test ideas: bone shapes switch on by bit
1 << ((id − 2) & 31)and every switch empties the struck list;Movedmoves each switched-on sphere to the body's new origin (prev untouched), so the next sweep runs from the origin, not across the teleport; the broad bound's radiusmax(0.8 × scale, reach); the push of 0.25 m × w / (w × k) along the 3-D line, not applied to the target, a player, a held (k = 1e9) or an unpushable one; one strike per candidate body per update.
14 functions, 6168 bytes (1 tagged).
| Address | Name | Size |
|---|---|---|
0x00341e98 |
PhysicsHumanBody_Construct |
104 |
0x00341f00 |
PhysicsHumanBody_Destroy |
40 |
0x00341f28 |
PhysicsBody_ApplyHumanScale |
412 |
0x003420c8 |
PhysicsHumanBody_GetShapes |
8 |
0x003420d0 |
PhysicsHumanBody_SetPushSphere |
92 |
0x00342130 |
PhysicsHumanBody_OnStruck |
28 |
0x00342158 |
PhysicsHumanBody_AddStrikeSphere |
16 |
0x00342168 |
PhysicsHumanBody_ClearStrikeSpheres |
100 |
0x003422d0 |
PhysicsHumanBody_Update |
748 |
0x003425c0 |
PhysicsHumanBody_PushAwayHumans |
616 |
0x00342828 |
PhysicsHumanBody_Moved |
168 |
0x003428d0 |
PhysBody_SetShapeEnabled |
280 |
0x003429e8 |
PhysicsHumanBody_AddBoneShape |
72 |
0x0033f110 |
Human_TestStrikes (tagged) |
3484 |
The record and its vtable¶
Human body vtable 0x005446b0 (read from the binary), slot adjust / function: +0x08 / +0x0c destructor
0x00341f00 (resets the vtable, then the body destructor); +0x10 / +0x14 Update 0x003422d0; +0x18 / +0x1c
Moved 0x00342828; +0x20 / +0x24 AddShape 0x00341aa8 (the body's); +0x28 / +0x2c GetShapes 0x003420c8
(returns +0x30, the capsule, whatever arguments it gets); +0x30 / +0x34 IsIgnoring 0x003421d0; +0x38 /
+0x3c StopIgnoring 0x00342260; +0x40 / +0x44 Ignore 0x00342230; +0x48 / +0x4c GetContactScale
0x00342288 (Unit 3); +0x50 / +0x54 OnStruck 0x00342130 (calls Human_OnPushContact
0x00219b08 with the owner, Human contacts). The struck list of the strike test is this
ignore
list: IsIgnoring answers +0xd4 for a null owner, yes for the owner human's grab partner (Human_GetGrabOther
0x002269d8, the anim record +0xd4's handle +0xc4), else the ordinary list; Ignore(null) sets +0xd4 = 1;
StopIgnoring(null) clears +0xd4 and the whole list.
Fields this unit uses (The human body): +0x30 capsule, +0x34 owner, +0x40 flags (0x4000
skips the update; Human_RestoreBodyAfterClimb 0x00281368 clears it, so a climb sets it, inferred), +0x80 bone
chain, +0x84 push sphere, +0x90 broad bound (two vec4, min and max, w = 1), +0xc0 strike bits, +0xc4
weapon sphere chain, +0xd0 weapon timer (updates left), +0xd4 ignore the level.
PhysicsBody_ApplyHumanScale¶
0x00341f28 (body). s = Human_GetScale(owner) (0x0021d020: human +0x65c, but 1.0 while
Human_IsInSceneState and +0x65c < 1.1). Capsule height +0x44 = Human_GetCapsuleHeight (0x00219890: 2 ×
s), radius +0x40 = Human_GetCapsuleRadius (0x00219860: 0.35 × s). Then it walks the bone chain assuming the
constructor's order (Unit 9), with no null test: the first five shapes as segments (+0x10 =
table offset × s in xyz, w the table's 0; +0x60 radius × s; +0x64 length × s), the next five as spheres
(+0x10 offset × s, +0x40 radius × s). It reads the scale afresh for every field (the same value). Finally it
sets the dirty bit and calls the body's Update with the owner, so the capsule and bound are rebuilt at once.
PhysicsHumanBody_Update¶
0x003422d0 (body a0, owner a1); called at the end of Human_Move (0x0023db20), just before the push
(below), and so before Human_TestStrikes, which 0x0023fea8 runs after the move
(Objects). Let F be the flags read on entry.
- If
F & 0x4000: return (nothing posed). - If
Fis dirty (0x80000000): pose the capsule (shape+0x28/+0x2cwith the owner), body AABB = the capsule's bounds alone (PhysicsBody_ComputeBounds0x00341638), re-sort (BodyList_Reinsert0x00340628), clear the dirty bit in memory.Fkeeps its dirty bit for step 4. - If the weapon timer
+0xd0≠ 0: markFdirty (locally), and pose every weapon sphere of+0xc4with the held object (Human_GetHeldObject0x00226ff0) or, when none, the owner. Else, if+0xc0≠ 0, markFdirty (locally). - If
Fis dirty (so: dirty on entry, or a weapon timer, or any strike bit), rebuild the broad bound+0x90:R= 0.0; if the owner is a human with an anim record (+0xd4) whose Anim Range List pointer (+0x160) is set,R= that list's reach (+0x04) for the record's clip id (+0x20) (AttackTable_GetReach0x002544a0, Animation); the id is not range-checked.c= the capsule's world position (its base+0x50),c.z += capsule height × 0.5.r=Human_GetScale(owner)× 0.8 (0x3f4ccccd).- With weapon spheres (
+0xc4set):r = max(r, |(sphere centre − capsule base) with z = 0| + sphere radius)for the first sphere of the chain only, andRis not used. Without:r = max(r, R). +0x90=AABB_FromUprightCylinder(c, height, r)(0x00341708, Unit 6): x and y in[c ± r], z from the capsule's base to base + height;w= 1.
- Pose every bone shape whose flags have
0x1or0x4(so the spine and head always) with the owner. - If a push sphere is held (
+0x84), pose it with the owner (bone −1, offset 0: its centre is the human's origin).
Numbers (Rembrandt-class reaches from Frame data, scale 1): a standing human, no strike
and not dirty, keeps his last bound; during S1 (reach 0.94) r = 0.94; SX2 1.37; SSX3 1.47; STRAFE_FRONT
1.74; the push 653 1.58; at scale 1.3 and S1, r = 1.04 (0.8 × 1.3 wins). The reach is never scaled. Which clip
the record's +0x20 names during a chain (the attack's id or the clip's) is not traced; inferred the playing id.
PhysicsHumanBody_Moved¶
0x00342828 (body, matrix), vtable +0x18 / +0x1c. Confirmed (code); who calls it (a virtual call) is not traced.
- The body's
Moved(0x00341e40): the capsule's set-from-matrix slot (shape+0x30/+0x34,0x00343798: prev+0x20= base, base = the matrix's translation+0x30), body AABB from the capsule, re-sort. The dirty bit is not touched. +0x90=AABB_FromUprightCylinder(translation + (0, 0, 1.0), height 2.0, radius 0.8): x and y ± 0.8, z from the translation's z to z + 2.0. Not scaled, unlike the update's.- Every bone shape whose flags have
0x1or0x4gets its set-from-matrix slot with the matrix: a sphere's centre becomes the translation (0x00342d08; offset and bone ignored, prev+0x20untouched); a segment does nothing (0x00343a88). Unit 6 gives the slot per type.
So Moved does not reset previous positions. Its effect on strikes: at the next pose a sphere copies its centre
(now the origin) to prev and computes the new one, so the next sweep runs from the human's origin up to the hand,
inside the human, not across the distance moved (inferred from the pose rule, Unit 6).
PhysBody_SetShapeEnabled¶
0x003428d0 (body, id, on), through Human_SetStrikeShape (0x0021c030) from the clip events
(Combat).
- Always first:
StopIgnoring(null)on the body (vtable+0x38/+0x3c): the human body clears+0xd4and frees its whole ignore list. So every strike-on or strike-off event, even one that changes nothing below, empties the struck list, and a body already struck can be struck again in the next window. id < 0: only whenon= 0 and+0xc0≠ 0:+0xc0= 0 and every bone shape enable(off). (id < 0withon≠ 0 does nothing more.)id ≥ 0:bit = 1 << ((id − 2) & 31)(the hardware shift uses the low five bits). On with the bit already set, or off with it clear: return. Else set or clear the bit and enable(shape, on) for the first shape of the chain whose bone byte+0x31equalsid; none: the bit alone changes.
Bits: 3 → 0x2, 6 → 0x10, 18 → 0x10000, 19 → 0x20000, 24 → 0x400000, 25 → 0x800000, 29 → 0x8000000,
30 → 0x10000000, 32 → 0x40000000, 33 → 0x80000000; the shapeless event bones 0 → 0x40000000, 4 → 0x4,
23 → 0x200000, 28 → 0x4000000, 31 → 0x20000000. Bones 0 and 32 share bit 30: an event for bone 0 sets it,
so a later on-event for shin 32 finds it set and does not switch the shin on, and an off-event for either clears
both (confirmed (code) by the arithmetic; whether any clip has both in one window is not checked).
Weapon strike spheres¶
PhysicsHumanBody_AddStrikeSphere(body, sphere) (0x00342158) pushes the sphere at the head of +0xc4
(sphere +0x00 = old head). PhysicsHumanBody_ClearStrikeSpheres (0x00342168) pops them one by one: enable(off),
next 0, head = the following one. Who builds them is on Combat
(Human_BuildWeaponStrikeSpheres 0x00247be8, the timer +0xd0 = event word + 1).
The push sphere¶
PhysicsHumanBody_SetPushSphere(body, on) (0x003420d0; through Human_SetPushSphere 0x0021c058, which does
nothing when the human has no body): on with none held takes the first push sphere whose 0x8 is clear
(IPhysics_AllocBigSphere 0x0033d278: radius +0x40 = 2.0, flags |= 0xb; null when all 30 are taken); off frees
it (IPhysics_FreeShape 0x0033d2c8: clears 0x8 only) and sets +0x84 = 0. Of the 94 call sites of
Human_SetPushSphere, only Grab_Start (0x0026c7a8) and Tackle_Start (0x00270a34) pass 1; every other one
passes 0 (byte search for the call with its argument, confirmed (code)). So the sphere is on from a grab's or a
tackle's start until the next action that switches it off (most clip starters do).
PhysicsHumanBody_PushAwayHumans¶
0x003425c0 (body), called at the end of Human_Move (0x0023db28) right after the body's Update.
target=Human_GetTarget(owner)(0x00226e60). No push sphere (+0x84= 0): return.IPhysics_OverlapShape(How it fills a contact) with the query: body, the push sphere, box pointer 0, the owner, options0x2012(0x2bodies,0x10type mask;0x2000is never read), type mask0x40(humans), at most 32 contacts, thresholds 0. Each human meets it through his capsule (SphereCapsule).- The contacts are walked from the last to the first. For each,
other= contact+0x40. Skip it when it istarget; when its player byte+0x1b0is not −1 (any player); when its body (owner vtable+0xf0) has flag0x200. d= other's position − this human's position, both from the authoritative transforms (0x00714b00+ slot+0x92× 0x20), in 3-D (z included);u = d / |d|(VUrsqrt).push = u × 0.25 × w(this) / (w(other) × k(other))withw= attribute 1 andk= attribute 7 (Attributes; 82 and 82 for humans, so0.25 / k), added in xyz to the other body's push-out+0x50(Vec4_AddTo0x004e6918,w= 1). The other's next sweep moves it (Unit 3).
Two humans at the same point: |d| = 0, the PS2's rsqrt gives the largest float and 0 × max = 0, so no push
(inferred from the hardware; an IEEE port must special-case it rather than add NaNs).
Human_TestStrikes: the gather and the loop¶
0x0033f110 (phys a0, body a1). Unit 5 gives the
per-shape sweep
(prev, cur, the restore, the contact fields, the pose rule); this is the rest, confirmed (code) at the addresses.
- Nothing unless
+0xd0≠ 0 or+0xc0≠ 0.held=Human_GetHeldObject(owner); the weapon handler's owner isheld, or the human when none. - Weapon spheres against the level (only when
held≠ 0,0x0033f218). For each sphere of+0xc4whose type has a mesh sweep (IPhysics+0xe0+ type × 4): a scratch body's box = the shape's bounds grown by its move (AABB_ExtendByMove), then the mesh sweep with max 1, the human body, the shape, the move, options0x800c,n.zlimits −0.65 and 0.65 (Unit 2: walls only, triangles with bit 3 skipped). On a hit: contact+0x00,+0x04,+0x40,+0x44= 0, the shape put back tocur, the held object's handler (vtable+0xf8/+0xfc) with the contact; the rest of the weapon spheres are skipped. - Strike spheres against the level (when
+0xc0≠ 0,0x0033f360): the same for each bone shape with flags0x1and0x2and type 2 (spheres only), options0x804c(adds0x40: one-sided faces kept). On the first hit: the owner fields zeroed as above, the shape restored, then the owner'sStrike_Contact(vtable+0x100/+0x104, the level branch: a particle at+0x90); the rest of this step is skipped. - Gather (
0x0033f4bc):IPhysics_QueryBox(phys, list, &body+0x90, start 0, ignored owner = own owner, type mask −1, flag mask 0x18)(Queries): bodies whose flags have0x8or0x10. A body with onlyCHARGETARGET(0x20) is never gathered here. The ray origino= own authoritative position + (0, 0, 1.0). - Per candidate, in list order:
- Skip: null or ownerless; own group non-zero and equal to its group; own
IsIgnoring(its owner)(vtable+0x30/+0x34: struck already, the grab partner); its type mask has0x8(an object) and its owner's state flags (owner vtable+0x50/+0x54) have0x4000000(airborne: a flying object is not struck). - Line of sight. Target point
t= its first shape's world position (GetShapes with (0, 0, 0), shape+0x20/+0x24),+1.0in z when the candidate is a human (type mask0x40).d = t − o,L = |d|, ray{o, d / L, L}throughWorldManager_RayCast(0x0040db88) with the material exclusion list at0x005771c8and options0x48, or0x8for a human candidate (Objects gives the exclusions). A hit (result+0x10≠ 0): skip the candidate. - Weapon spheres (candidate flags
0x8): for each sphere of+0xc4(no flag test), its move as in Unit 5, against the candidate's shapes: a human's bone chain filtered by flag0x4, otherwise GetShapes(2, 4, own owner). On the first nonzero sweep: contact+0x00=held(or the human),+0x04the sphere,+0x40its owner,+0x44its shape; the handler+0xfc; for a non-human candidate an answer of 5 means "next candidate shape", for a human the answer is not read. A hit with an owner: ownIgnore(its owner)(vtable+0x40/+0x44), and the weapon spheres stop for this candidate. - Strike shapes (
+0xc0≠ 0, candidate flags0x30(0x10or0x20), and own owner vtable+0x108/+0x10cwith its owner nonzero): for each bone shape with0x1and0x2, its move, against: a non-human's GetShapes(2, 4, own owner) chain; a human's bone shapes with flag0x4, or his capsule alone whenAnim_HitsWithCapsule(owner, Human_GetAnimId(owner))(0x00101b88, clip flag0x10000, Combat). The first nonzero sweep: contact as Unit 5,Strike_Contact, thenIgnore(its owner)when it has one; the strike shapes stop for this candidate. - Go on with the next candidate: a hit does not end the test. Each body can be struck once per update (and once per window, by the ignore list), but one swing can strike several bodies in the same update.
- Skip: null or ownerless; own group non-zero and equal to its group; own
- Weapon timer (
0x0033fe50): if+0xd0≠ 0, decrement it; at 0ClearStrikeSpheres; either way set the body's dirty bit.
Test ideas¶
Synthetic bodies; fake owners with scale, reach, type mask and attributes. Values from this spec (float32).
- Switch bits.
SetShapeEnabled(25, on):+0xc0=0x800000, the right-hand sphere flags gain0x11; again: nothing changes but the ignore list is emptied.(0, on)then(32, on):+0xc0=0x40000000, shin 32 stays off.(−1, off):+0xc0= 0, every bone shape off. - Struck list emptied. Ignore(owner X) on a human body, then
SetShapeEnabled(19, off)with bit 19 clear:IsIgnoring(X)is false afterwards. - Broad bound. Capsule base (10, 5, 0), height 2.0, scale 1, strike bit set: reach 1.37 →
+0x90= (8.63, 3.63, 0) to (11.37, 6.37, 2.0); reach 0 → ±0.8; scale 1.3, reach 0.94 → ±1.04. Weapon sphere at (10.6, 5.8, 1.2) radius 0.1 with reach 1.37:r= 1.1 (the reach is ignored). - No refresh. Not dirty,
+0xc0= 0,+0xd0= 0:+0x90is left as it was; flag0x4000: nothing posed. - Moved. Matrix translation (3, 4, 0.5):
+0x90= (2.2, 3.2, 0.5) to (3.8, 4.8, 2.5). An enabled hand sphere at (10.6, 5.0, 1.4) with prev (10.5, 5.0, 1.4) → centre (3, 4, 0.5), prev unchanged; after the next pose with the hand at (3.6, 4.0, 1.9): prev (3, 4, 0.5), move (0.6, 0, 1.4). - Push. This at (0, 0, 0), other at (1, 1, 0.5), k = 1: other's
+0x50+= (0.166667, 0.166667, 0.083333); k = 2: (0.083333, 0.083333, 0.041667); other at (0, 2, 0): (0, 0.25, 0). Not pushed: the target, a player, body flag0x200; k = 1e9: 2.5e-10 (effectively 0). Same position: no push, no NaN. - Push sphere switch. On twice takes one sphere; off frees it (
0x8clear) and+0x84= 0; a 31st on with all 30 taken leaves+0x84= 0. - Ray. Striker at the origin, human candidate's capsule base (3, 4, 0): ray from (0, 0, 1), direction
(0.6, 0.8, 0), length 5.0, options 8; an object whose first shape is at (3, 4, 0.5): direction
(0.597022, 0.796030, −0.099504), length 5.024938, options
0x48. A wall triangle across the ray: no strike. - Several bodies. Two boxes both reached by one hand sweep in one update: both get
Strike_Contact, each once; the next update neither (ignored); after an off-event, both can be struck again. - Airborne object. An object body (type 8) whose owner reports
0x4000000: never struck.
Unit 11: Moving shapes against bodies: spheres, capsules, segments¶
What it is. The sweep table's sphere, capsule and segment pairs (IPhysics +0x7c, The shape-pair
tables), their three kernels, the body pass of a body's sweep (PhysicsBody_SweepHumanMask, which
puts the
bodies' contacts in the same sorted list as the mesh's), and the general shape query IPhysics_CollideShape with every
option bit it reads. All confirmed (code) at the cited addresses, read in the disassembly on 2026-10-12, unless a line
says otherwise; no runtime claims.
- Coney:
src/physics/sweeps.*for the pair functions and kernels;src/human/human_contacts.hmoves ontoPhysicsSweep_CapsuleCapsuleas specified here (four differences from it today: below);PhysicsBody_SweepHumanMaskreplacesHuman::meetHumans, so a pass's human and wall contacts are sorted together by fraction (today a marked Coney choice). - Tests: Test ideas: every number there was computed from this spec (a Python model of it), with the original's quirks pinned: start-of-move normals, swapped heights, unsorted capsule roots, the missed vertical drop onto a capsule, the inward sphere-capsule normal.
16 functions, 8672 bytes (3 tagged).
| Address | Name | Size |
|---|---|---|
0x003440d0 |
PhysicsSweep_SphereSphere |
560 |
0x00344300 |
PhysicsSweep_SegmentSegment |
32 |
0x00344320 |
PhysicsSweep_SphereSegment |
536 |
0x00344538 |
PhysicsSweep_SegmentSphere |
224 |
0x00345670 |
PhysicsSweep_SphereCapsule |
668 |
0x00345910 |
PhysicsSweep_CapsuleSphere |
224 |
0x00345d18 |
PhysicsSweep_CapsuleSegment |
32 |
0x00345d38 |
PhysicsSweep_SegmentCapsule |
32 |
0x00343cd8 |
PhysicsSweep_CapsuleCapsule (tagged) |
1012 |
0x00349268 |
Sweep_SphereSphere |
544 |
0x003498c0 |
Sweep_CircleCircle2D |
416 |
0x0034a8d0 |
Sweep_IntervalOverlap |
396 |
0x00349a60 |
Sweep_SphereCapsule (tagged) |
2160 |
0x00349180 |
Vec_MakePerpendicularBasis |
232 |
0x0033d498 |
PhysicsBody_SweepHumanMask (tagged) |
640 |
0x0033e7f0 |
IPhysics_CollideShape |
964 |
Summary for the implementer¶
- Calling convention of a sweep pair (confirmed (code) at
0x0033d68cand0x0033ea0c, the two callers):bool fn(Contact* c, Body* ownBody, Shape* own, Body* otherBody, Shape* other, const vec4* move)ina0-a3,t0,t1.ownis the moving shape,moveits whole move for this pass (the other shape is at rest). Returns 1 on a hit. OnlyPhysicsSweep_CapsuleCapsulereads a body (the other body's type mask+0x44). - What a hit writes (the caller then writes
+0xb4 = 0andContact_InsertSortedwrites+0x00,+0x04,+0x40,+0x44; Unit 3). The slot is not cleared first: a field not listed holds what the 50-slot stack array held.
| Pair | +0x10 |
+0x50 |
+0x80 |
+0x88 |
+0x90 |
+0xa0 |
+0xb0 |
|---|---|---|---|---|---|---|---|
| sphere-sphere (2, 2) | unit(other − own), start | unit(own − other), start | first touch | own centre + f·move | 0 | ||
| capsule-capsule (3, 3) | −+0x50 |
horizontal unit(own − other) at the start; (0, 0, 1) in the start case | max(circle entry, vertical entry) | own base + f·move | start case only | 0, or 2 in the start case | |
| sphere-capsule (2, 3) | −+0x50 |
unit(capsule mid-height − sphere start): into the capsule | first stored root in [0, 1] | sphere centre + f·move | 0 | ||
| capsule-sphere (3, 2) | halves of (2, 3) with −move swapped | sphere centre − f·move | 0 | ||||
| sphere-segment (2, 4) | unit(move) | −unit(move) | closest approach, not first touch | that distance | sphere centre + f·move | 0 | |
| segment-sphere (4, 2) | halves of (2, 4) with −move swapped | 0 | |||||
| segment-segment, capsule-segment, segment-capsule | nothing written: the static overlaps of Unit 4, move ignored | leftover | leftover |
- Normals are taken at the start of the move, not at the contact, in sphere-sphere and capsule-capsule; a
glancing approach gets the start direction. The sphere-capsule normal points the other way from every other
pair (from the mover into the other shape). Resolved with code 1 or 2 (Unit 3), such a normal gives
no advance (
D · n > 0) and no slide (Vec_RemoveNormalPartacts only onv · n < 0), so the pass returns 1 and the next pass meets the same contact: after three passes the mover is stuck where it started, its horizontal velocity zeroed. Inferred from the two functions; not seen at runtime. It concerns a world object's sphere hitting a human (sphere-capsule) and a walking human meeting aBLOCKHUMANSobject of sphere shape (capsule-sphere). - The capsule is three different shapes in this unit: an upright cylinder
[B.z, B.z + h]against another capsule (with the heights swapped, below); a true capsule whose rounded ends lie inside[B.z, B.z + h]against a sphere (hemisphere centres atB.z + randB.z + h − r); and Unit 4's true capsule reachingrpast both ends against a segment (the overlap, used as the sweep). - Sorting with walls. In
PhysicsBody_Sweepeach pass's list gets, in this order, the nearest mesh contact, the landing contact, then one contact per candidate body (Unit 3, The body pass), all throughContact_InsertSorted(strictly-greater insertion), so humans and walls are resolved nearest first, and at an equal fraction the mesh contact comes first. Any non-zero answer ends the walk (Unit 3), so a human met at 0.3 is resolved and the wall at 0.6 is not seen in that pass. - The capsule-capsule start case ("already within reach at fraction 0"), settled: below.
Shared records and notation¶
Shape fields read (Shapes): sphere +0x40 r, +0x50 C; capsule +0x40 r, +0x44 h, +0x50 B
(+0x58
is B.z); segment +0x40 S, +0x50 E, +0x60 r. a·b is the xyz dot product summed (x·x + y·y) + z·z;
"unit(v)" is v × rsqrt(v·v) on VU0 (a zero vector gives a zero result on the EE, inferred: 0 × FLT_MAX; Coney
should write (0, 0, 0) and say so). 1e-6 is 0x358637bd, −1e-6 0xb58637bd, 1.0 0x3f800000, −1.0 0xbf800000,
0.5 0x3f000000, 0.05 0x3d4ccccd. Vectors written as unit vectors keep w = 1.
The hit record of the two root kernels (a stack record of the caller, count cleared to 0 first): +0x00 count
(int), +0x04 the roots t[i] (floats, at most two used here), +0x10 + 16·i a vector per root, always
(0, 0, 0, 1) from 0x005116c0 (unused). The root filter of PhysicsSweep_SphereSphere (0x00344144) and
PhysicsSweep_SphereCapsule (0x00345730): walk the roots from the last to the first and remove each t with
t < 0 or t > 1 (shifting the rest down); no root left is no hit; otherwise the fraction is t[0], the first
kept in stored order.
Sweep_SphereSphere¶
0x00349268, 0x220 bytes. bool Sweep_SphereSphere(Hits* hits, const {vec4 P; vec4 m}* ray, const {vec4 C; float
R}* sphere). It finds the t with |(C − P) + t·m| = R, that is a centre moving from C by t·m against a fixed
point P (equivalently a ray from P along −m against a sphere at C). Returns 1 when it appended a root:
d = C − P,c = d·d − R²,b = m·d.c ≤ 0(P within R of C, touching included): appendt = 0, return 1.a = m·m; if|a| < 1e-6: return 0.disc = (2b)² − 4·c·a(computed as(2b)·(2b) − (4c)·a).disc > 0:s = √disc; append(−2b − s)/(2a)and(−2b + s)/(2a), the smaller first (a swap if needed).disc == 0: append−2b/(2a).disc < 0: return 0.
The roots are not clamped. Also called by ObjectRender_Draw (Geometry kernels).
PhysicsSweep_SphereSphere¶
0x003440d0, 0x230 bytes, table (2, 2). Calls the kernel with P = the other centre, m = move, C = the
own centre, R = r_own + r_other, then the root filter. On a hit (0x00344214-0x003442cc): +0x10 =
unit(C_other − C_own), +0x50 = −+0x10 = unit(C_own − C_other) (both from the centres at the start), +0x80
= f, +0x90 = C_own + f·move (w = 1), +0xb0 = 0. Consequences: spheres already touching at the start hit at
fraction 0 whatever the move (also a move away, also a zero move); a move with |move|² < 1e-6 (shorter than
1 mm) never hits otherwise.
Sweep_CircleCircle2D¶
0x003498c0, 0x1a0 bytes. bool (float r1 f12, float r2 f13, float* t0 a0, float* t1 a1, const vec4* c1 a2, const
vec4* c2 a3, const vec4* m t0, int flag t1). The caller zeroes the z of c1, c2 and m, so this is the circle of
radius r1 at c1 moving by t·m against the circle r2 at c2:
d = c1 − c2,b = m·d.- If
flag & 1andb ≥ 0(not closing in, a tangent or zero move included): return 0. c = d·d − (r1 + r2)². Ifc ≤ 0(overlapping or touching):*t0 = 0,*t1 = 1.0, return 1.a = m·m,disc = (2b)² − 4·a·c(as(2b)² − a·(4c)).disc > 0: the two roots(−2b ± √disc)/(2a), written so that*t0 ≤ *t1, return 1.disc == 0: both =−2b/(2a), return 1. Else return 0.
With flag 0 and a zero move, step 4 divides 0 by 0 (EE: −FLT_MAX, inferred), which the caller rejects; Coney may
return 0 there.
Sweep_IntervalOverlap¶
0x0034a8d0, 0x18c bytes. int (float* lo a0, float* hi a1, float maxT f12, float v f13, float A0 f14, float A1 f15,
float B0 f16, float B1 f17): the interval [B0, B1] moves by v·t against the fixed [A0, A1]; [lo, hi] comes
in as the allowed times and is narrowed to the times they overlap. Returns 1 when they are separated, 0 when they
overlap:
| Start | v |
Effect |
|---|---|---|
B1 < A0 (B below) |
v ≤ 0 |
return 1 |
v > 0 |
lo = max(lo, (A0 − B1)/v); if maxT < lo return 1; hi = min(hi, (A1 − B0)/v) |
|
A1 < B0 (B above) |
v ≥ 0 |
return 1 |
v < 0 |
lo = max(lo, (A1 − B0)/v); if maxT < lo return 1; hi = min(hi, (A0 − B1)/v) |
|
| overlapping (neither) | v > 0 |
hi = min(hi, (A1 − B0)/v) |
v < 0 |
hi = min(hi, (A0 − B1)/v) |
|
v == 0 |
lo = 0, hi = 1.0, return 0 |
Then return 1 if hi < lo, else 0. Touching counts as overlap (strict < tests). The min/max are written as
"store when strictly smaller/greater". Also used by the box-triangle sweep, where the v == 0 reset matters
(Unit 13). In the capsule sweep it does not: the caller always passes lo = 0, hi =
1, so two
humans moving level (m.z exactly 0) with overlapping ranges get lo = 0 and the fraction is the circles' t0.
PhysicsSweep_CapsuleCapsule¶
0x00343cd8, 0x3f4 bytes, table (3, 3). Own capsule (B1, r1, h1) moves by m; the other (B2, r2, h2) is at rest:
- Circles (
0x00343d20-0x00343db4):c1,c2,m2= B1, B2, m with z = 0.flag= 1, except 0 when the mover goes down (m.z ≤ −1e-6) and starts strictly above (B1.z > B2.z) (0x00343d48-0x00343d90).Sweep_CircleCircle2D(r1, r2, &t0, &t1, c1, c2, m2, flag); 0 → no hit. Then require0 ≤ t0 ≤ 1(else no hit).t1is never used. - Vertical (
0x00343df0-0x00343e2c), skipped when the other body's type mask (+0x44) has0x200:lo = 0,hi = 1,Sweep_IntervalOverlap(&lo, &hi, 1.0, m.z, A0 = B2.z, A1 = B2.z + h1, B0 = B1.z, B1 = B1.z + h2); 1 → no hit. The heights are swapped: the fixed range is the other's base with the mover's height, the moving range the mover's base with the other's height. Equal heights (every human of one scale and state) hide it. - Fraction
f = max(t0, lo)(max.sat0x0034402c;lois 0 when step 2 was skipped). The circle times[t0, t1]and the vertical times[lo, hi]are never intersected: a contact is reported atmax(t0, lo)even when the circles have separated again beforelo(or the ranges beforet0). - Normal source
d = c1 − c2(horizontal, at the start), unless the start case below replaced it. - Write (
0x00343ff0-0x003440a0):n = unit(d);+0x50 = n,+0x10 = −n,+0x80 = f,+0x90 = B1 + f·m(the mover's base at the contact, w = 1),+0xb0= 0 or 2. Return 1.
The start case¶
Taken when t0 < 1e-6 (the circles touch or overlap at the start; the test reads t0, not f), m.z ≤ −1e-6 and
B2.z ≤ B1.z (0x00343e34-0x00343fec): a capsule coming down onto another from above, already within reach
across the ground. Then:
u= unit(c1 − c2), andk = (r1 + r2) − |c1 − c2| + 0.05; if|c1 − c2|² < 1e-6,u= unit(m2) andk = r1 + r2 + 0.05, or, if also|m2|² < 1e-6,u = (0, 1, 0)(0x00511730) with the samek.+0xa0 = u × 0.5k(the slide), contact flags+0xb0 = 2, and the normal source becomes (0, 0, 1, 1) (0x00511740), so+0x50 = (0, 0, 1),+0x10 = (0, 0, −1).
What the resolution then does (inferred from Unit 3): with a non-zero answer, the slide (less its part
against the move's direction) is stored once as the mover's next push-out, so a human dropping onto another's head
stops falling there (the upward normal removes v.z < 0) and is pushed off sideways by half of the overlap plus
0.025 m on its next sweep; code 4 also gives the one below a downward push-out, 0.5·(D·n)·n.
The other start positions, settled (all confirmed (code) through the steps above): within reach and closing in
(flag 1, m2·d < 0) → a hit at fraction 0 (or at lo) with the start normal, no slide; within reach and not
closing in (moving away, along the tangent, or no horizontal move) → no hit; coincident axes with flag 1 → no
hit (b = 0). This is what Coney's sweepCapsules already does across the ground.
Vec_MakePerpendicularBasis¶
0x00349180, 0xe8 bytes. void (vec4* U a0, vec4* V a1, const vec4* W a2), W a unit vector. If |W.x| ≥ |W.y|:
k = 1/√(W.x² + W.z²), U = (−W.z·k, 0, W.x·k); else k = 1/√(W.y² + W.z²), U = (0, W.z·k, −W.y·k). U.w is
not written. V = W × U (VU opmula/opmsub), the whole quadword written, V.w = U.w. For W = (0, 0, 1):
U = (−1, 0, 0), V = (0, −1, 0); for (0, 0, −1): U = (1, 0, 0), V = (0, −1, 0).
Sweep_SphereCapsule¶
0x00349a60, 0x870 bytes. bool (Hits* hits a0, const {vec4 O; vec4 dir}* ray a1, const {vec4 P0; vec4 A; float R}*
cap a2): a ray against the capsule of radius R around the segment P0 → P0 + A. Roots are in units of dir
(unnormalised), appended in the order below, at most two. Returns count != 0 (1 after two roots).
- Frame (
0x00349a98-0x00349c68):L = |A|(VUsqrt),W = A/L;U, VfromVec_MakePerpendicularBasis(W).D = (U·dir, V·dir, W·dir),len = |D|,inv = 1/len,D ×= inv(unit).P = (U·(O − P0), V·(O − P0), W·(O − P0)). - Parallel when
|D.z| ≥ 0.999999(0x3f7fffef= 0.99999899, about 0.08° from the axis) orlen < 1e-6(0x00349c1c-0x00349c90). Withg = dir·Aandq = R² − P.x² − P.y²: g < 0andq ≥ 0: append(P.z + √q)·inv, then(L − P.z + √q)·inv, return 1.g > 0andq ≥ 0: append−(P.z + √q)·inv, then(L − P.z + √q)·inv, return 1.- otherwise return 0.
The rising case (g > 0) gives the entry at the bottom and the exit at the top. The falling case is wrong: its
second root is the entry time negated (the true entry is (P.z − L − √q)·inv), so a sphere moving exactly
down the axis from above the capsule has a negative "entry" (dropped by the filter) and is met only at its exit
below the bottom, or not at all. Confirmed (code) at 0x00349d38-0x00349d5c; the consequence (an object dropped
straight down onto a human's head passes into him) is inferred.
3. Side (0x00349e2c-0x00349f98): a = D.x² + D.y², b = P.x·D.x + P.y·D.y, c = P.x² + P.y² − R²,
disc = b² − a·c. disc < 0: return 0 (the caps are not tried). disc > 0: for s = (−b − √disc)/a, then
(−b + √disc)/a, append s·inv when 0 ≤ P.z + s·D.z ≤ L; two appended → return 1. disc == 0: s = −b/a,
appended under the same test, and then return 1 at once; if not appended, go on.
4. Bottom cap (0x0034a018-0x0034a174): b' = b + P.z·D.z, c' = c + P.z² (the sphere at P0),
disc = b'² − c'. > 0: for s = −b' − √disc, then −b' + √disc, append s·inv when P.z + s·D.z ≤ 0,
returning 1 once two are stored. == 0: s = −b', the same test. < 0: nothing.
5. Top cap (0x0034a178-0x0034a2a8): b'' = b' − D.z·L, c'' = c' + L·(L − 2P.z) (the sphere at P0 + A),
disc = b''² − c''; the same as step 4 with the test P.z + s·D.z ≥ L.
Edge cases: no inside test: a ray starting inside gets its exit (and a negative entry); the caller's filter
keeps the exit when it is within the move. Store order is not time order: an entry through a cap and an exit
through the side are stored exit first, and the caller takes the first kept root. L = 0 (h = 2r in the caller)
divides by zero (EE: W = 0, then len = 0, the parallel branch with g = 0: no hit; inferred).
PhysicsSweep_SphereCapsule¶
0x00345670, 0x29c bytes, table (2, 3). Sphere (C, r_s) moving by m, capsule (B, r_c, h) at rest. Kernel arguments
(0x003456ac-0x00345720): O = C, dir = m, P0 = B + (0, 0, r_c), A = (0, 0, (h − r_c) − r_c),
R = r_c + r_s; then the root filter. On a hit (0x003457fc-0x003458dc): M = B + (0, 0, 0.5h); +0x50 =
unit(M − C) (from the sphere's start centre to the capsule's mid-height, a 3D vector pointing into the
capsule), +0x10 = −+0x50, +0x80 = f, +0x90 = C + f·m, +0xb0 = 0. With h < 2r_c the axis points down and
the shape spans [B.z + h − 2r_c, B.z + 2r_c] (the formula as written; inferred to be unintended).
PhysicsSweep_SphereSegment¶
0x00344320, 0x218 bytes, table (2, 4). Not a first-touch sweep. (s, t) = Segment_ClosestPoints(d1 = m, d2 = E −
S, w = C − S) (Unit 4); v = (C − S) + s·m − t·(E − S); hit
when v·v ≤ (r_s +
r_seg)² (touching counts). Then +0x80 = s (the path parameter of closest approach), +0x88 = |v| (the distance
there, not a depth), +0x90 = C + s·m, +0x10 = unit(m), +0x50 = −unit(m), +0xb0 = 0. A zero move gives s = 0
and a zero normal. Unit 4's kernel quirks (absolute parallel threshold) apply.
The mirrors and the overlap entries¶
PhysicsSweep_SegmentSphere(0x00344538, (4, 2)) andPhysicsSweep_CapsuleSphere(0x00345910, (3, 2)): call (2, 4) or (2, 3) as(c, otherBody, otherShape, ownBody, ownShape, −m)(x, y, z negated,wkept), and on a hit swap the quadwords+0x00..+0x3fwith+0x40..+0x7f.+0x80,+0x90and+0xb0are not swapped:+0x90is the sphere's centre moved by−f·m. After the swap+0x50is still "the other's half"; for (3, 2) it is unit(C_sphere − M), again pointing from the mover into the other.PhysicsSweep_SegmentSegment(0x00344300, (4, 4)),PhysicsSweep_CapsuleSegment(0x00345d18, (3, 4)),PhysicsSweep_SegmentCapsule(0x00345d38, (4, 3)): call the overlap0x00346500,0x00347148,0x00346fb8with(c, own, other)and return its answer; the move is ignored and nothing is written, so a hit's fraction+0x80and flags+0xb0are whatever the slot held. In a strike test (the only caller that moves a segment, Unit 5) the first hit ends that body's shapes and the fraction is not read. Coney should write fraction 0 and flags 0 there and say so.
PhysicsBody_SweepHumanMask: the body pass¶
0x0033d498, 0x280 bytes. void (IPhysics* phys, const AABB* box, Body* body, Shape* shape, Contact* contacts,
Contact** sorted, int* count, const vec4* D), the same arguments as IPhysics_SweepBodyVsMesh (Unit 3);
called by PhysicsBody_Sweep when body flag 0x1 is set and by PhysicsBody_Update (0x00341c10). The name is
another engineer's and is kept; the function is the whole body pass, not only its mask.
- Mask (
0x0033d4d4-0x0033d578):2when the body's type mask+0x44lacks0x40. For a human:4;0x44when the owner's state (slot+0x50) has0x4000000(airborne) or0x00226660(owner, 0x800)is true (holding);| 0x20when the shape's flags+0x32have0x2;| 0x40000when owner+0x1a8(gait) ≥ 4;| 0x80000 << pwhen the owner's byte+0x1b0(player number, signed) is not −1. - Candidates:
IPhysics_QueryBox(phys, list, box, startBody = body, ignoredOwner = body+0x34, typeMask = −1, flagMask = mask)(Queries); list order (x-sorted). - Per candidate, in list order (
0x0033d5ac-0x0033d6e4): skip when both groups+0x3c(u16) are non-zero and equal; ask the mover's owner, slot+0x110(owner, candidateOwner), skip on 0; the chain is the candidate's body slot+0x28called with(2, 4, owner). Walk it: a shape is tried only with flag0x1(enabled) and a non-emptysweep[shape.type][candidate.type]entry; the contact slot iscontacts[*count]. On the first hit:+0xb4 = 0,Contact_InsertSorted(phys, slot, sorted, count, body+0x34, candidate+0x34, shape, candidateShape), and go to the next candidate: at most one contact per body, the first shape of the chain that hits, not the nearest. - No check against the 50-slot array (inferred: never reached).
IPhysics_CollideShape¶
0x0033e7f0, 0x3c4 bytes. int IPhysics_CollideShape(IPhysics* phys, Contact* out, const Query* q, const vec4* move):
four arguments, the move separate from the query. Returns the number of contacts copied to out.
| Query offset | Read when | Meaning |
|---|---|---|
+0x00 |
always | the querying body (its owner becomes contact +0x00; its group and owner filter the candidates) |
+0x04 |
always | the shape swept (its type picks the sweep and mesh functions) |
+0x08 |
option 0x400 |
a pointer to a 0x30-byte box node, copied and used instead of the shape's world bounds |
+0x0c |
option 0x2000 |
an owner whose bodies are skipped (else none) |
+0x10 |
always | options |
+0x14 |
option 0x10 |
type mask the candidates must meet (else −1) |
+0x18 |
always | the most contacts copied out |
+0x1c |
option 0x800 |
body flags a candidate must have at least one of (else no requirement) |
+0x20 |
option 0x1000 |
body flags that exclude a candidate |
+0x24 |
option 0x10000 |
owner state bits (slot +0x50) that exclude a candidate |
+0x28, +0x2c |
option 0x1 |
the mesh sweep's min and max n.z (f12, f13) |
+0x38 |
option 0x40000 |
a callback (ownOwner, candidateOwner); 0 excludes |
Steps (confirmed (code) at the addresses):
- Box (
0x0033e82c-0x0033e87c): a local node (PhysicsNode_Init); the shape's world bounds (PhysicsBody_ComputeBounds0x00341638), or with0x400the node at*(q+0x08); thenAABB_ExtendByMove(box, move). - Bodies, with option
0x2(0x0033e890-0x0033ea60): candidates from0x00340120(the filtered query, specified in Unit 7: start bodyq+0x00, the fields above as its arguments, list order). Per candidate: skip the same non-zero group and an owner refusal (slot+0x110), always (no option bit, unlikeIPhysics_OverlapShape); chain from body slot+0x28with(2, 4, owner); every enabled shape with a sweep entry is tried, and every hit inserted (+0xb4 = 0, thenContact_InsertSorted), so one body can give several contacts. The querying body itself is a candidate unless a filter drops it (inferred from the start-body walk; the callers seen set0x2000with their own owner or sweep a temporary body). - Mesh, with option
0x1(0x0033ea64-0x0033eb10):phys+0xe0 + 4·type; if set, it is called with(contacts + count, max = 50 − count, box, body, shape, move, options = the whole word, f12 = q+0x28, f13 = q+0x2c), and each contact it returns is inserted with other owner 0 and other shape 0. So the mesh's option bits (0x4,0x8,0x40,0x8000,0x20000, Unit 2) come from the same word, and body and mesh contacts are sorted together by fraction; at an equal fraction the body contact, being inserted first, comes first. - Copy (
0x0033eb14-0x0033eb7c):n = min(count, q+0x18)contacts from the sorted list toout(0xc0 each); returnn.
Bits not read here: 0x80, 0x100, 0x200 (the overlap query's) do nothing in this function.
Callers' option words (confirmed (code) at the call sites; the others are not traced):
| Caller | Options | Other fields |
|---|---|---|
Cam_Follow_SphereTest 0x001305a0, CamMug_PlaceShot 0x00138798, CamMug_Update 0x001397bc |
0x12812: bodies, type mask +0x14 = 8 (objects), required flags +0x1c = 4 (BLOCKHUMANS), ignored owner, excluded owner state +0x24 = 0x4000000 (airborne) |
max 1 |
CamMini_TestShot 0x001375d4 |
0x52813 (the above, plus mesh and a callback 0x00136040) |
|
Human_IsWayClearTo 0x0021c1d0, Human_CapsuleOverlaps 0x0021c524 |
0x2803: mesh, bodies, required flags 4, ignored owner (the human) |
max 1 |
Human_IsClearToTarget 0x0021c3e0 |
1: mesh only |
max 1 |
Human_PushOutInAir 0x0021aa14, WorldObject_UpdateBodyContacts 0x00394be8 |
0x2004d: mesh only, sphere-sweep options 0x4, 0x8, 0x40, 0x20000; n.z range 0 to 0.7 |
max 2 (the human) |
Human_HandleMessage 0x0024732c |
0x52: bodies, type mask 0x100408 |
max 2 |
Where Coney differs today¶
human::sweepCapsules (src/human/human_contacts.h) against PhysicsSweep_CapsuleCapsule, each confirmed (code)
on the original's side: (1) Coney's normal is taken at the contact, the original's at the start; (2) Coney tests the
mover's swept height range against the other's, the original intersects times with the heights swapped and takes
max(t0, lo) without checking the circle exit; (3) Coney has no descending start case (normal up, slide, flags 2);
(4) Coney resolves humans before walls, the original sorts both. The start case across the ground already agrees.
Test ideas¶
Values from the spec (doubles; compare with 1e-5). Capsules are (B; r = 0.35, h = 1.8) unless said; spheres
(C; r).
- Sphere-sphere kernel.
P = (2, 0, 0),m = (−4, 0, 0),C = 0,R = 1: roots −0.75, −0.25 (none kept by the filter). Same withm = (4, 0, 0): 0.25, 0.75. - Sphere-sphere head-on. Own
(0; 0.5), other((2, 0, 0); 0.5), move(4, 0, 0): f = 0.25,+0x50 = (−1, 0, 0),+0x10 = (1, 0, 0),+0x90 = (1, 0, 0),+0xb00. - Glancing, start normal. Own at
(0, 0.8, 0): f = 0.35,+0x90 = (1.4, 0.8, 0),+0x50= unit(−2, 0.8, 0) =(−0.92848, 0.37139, 0)(the contact direction would be(−0.6, 0.8, 0)). Own at(0, 1.2, 0): no hit. - Already touching. Own
(0; 0.5), other((0.5, 0, 0); 0.5), move(−1, 0, 0)(away): hit, f = 0. Move(0.0005, 0, 0)from 2 m apart: no hit (|m|² < 1e-6). - Circles. r 0.35 each,
c1 = 0,c2 = (1, 0, 0),m = (0.5, 0, 0), flag 1:(0.6, 3.4).c2 = (0.5, 0, 0),m = (0.1, 0, 0):(0, 1);m = (−0.1, 0, 0): no hit. Flag 0,c2 = (1, 0, 0),m = (−0.5, 0, 0):(−3.4, −0.6)(returned; the caller rejects). - Intervals.
A = [1, 2.8],B = [−0.2, 0.8],v = 0.5: overlap,lo = 0.4,hi = 1.A = [0, 1.8],B = [2.3, 4.1],v = −1:lo = 0.5.A = [1, 2.8],B = [−1, 0.5],v = 0: separated.A = [2, 3.8],B = [−1, 0.8],v = 0.5:lo = 2.4 > 1, separated. Overlapping,v = 0:lo = 0,hi = 1. - Capsules head-on.
B1 = 0,B2 = (1, 0, 0),m = (0.5, 0, 0): f = 0.6,+0x50 = (−1, 0, 0),+0x90 = (0.3, 0, 0),+0xb00.B1 = (0, 0.4, 0): f = 0.85109,+0x50 = (−0.92848, 0.37139, 0)(start normal). - Capsules within reach.
B2 = (0.5, 0, 0):m = (0.1, 0, 0)→ f = 0,+0x50 = (−1, 0, 0);m = (−0.1, 0, 0)→ no hit;m = 0→ no hit. - Vertical miss and the swap.
B1 = (0, 0, 2),B2 = (1, 0, 0),m = (0.5, 0, 0): no hit.B1 = (0, 0, 1.5)withh1 = 1.0, otherh2 = 1.8: no hit (the swap tests[1.5, 3.3]against[0, 1.0]); withh1 = 1.8,h2 = 1.0: a hit at 0.6,+0x90 = (0.3, 0, 1.5). - Rising.
B1 = (0, 0, −2.4),m = (0.5, 0, 1.0),B2 = (1, 0, 0): f = 0.6 (lo= 0.6,t0= 0.6). - Times never intersected.
B1 = (−0.8, 0, −2.4),B2 = 0,m = (3, 0, 1): circles[0.0333, 0.5], verticallo = 0.6: hit at 0.6 with+0x50 = (−1, 0, 0)and+0x90 = (1.0, 0, −1.8), where the axes are 1.0 m apart. - Landing on a head (start case).
B1 = (0.3, 0, 1.7),B2 = 0,m = (0, 0, −0.2): f = 0,+0x50 = (0, 0, 1),+0x10 = (0, 0, −1),+0xa0 = (0.225, 0, 0),+0xb0 = 2,+0x90 = (0.3, 0, 1.7).B1 = (0, 0, 1.7):+0xa0 = (0, 0.375, 0); withm = (0.1, 0, −0.2):(0.375, 0, 0). The same with the other's type mask0x200: identical.B1 = (0.3, 0, 2.0),m = (0, 0, −0.5): f = 0.4 (vertical entry), start case still taken (it readst0= 0),+0x90 = (0.3, 0, 1.8),+0xa0 = (0.225, 0, 0). - Descending, outside reach.
B1 = (1, 0, 2),m = (−0.5, 0, −0.5): f = 0.6,+0x50 = (1, 0, 0), flags 0. - Sphere-segment. Segment
(−1, 0, 0)-(1, 0, 0)r 0.05, sphere((0, −1, 0); 0.1),m = (0, 2, 0): f = 0.5 (closest approach, not the touch at 0.425),+0x88 = 0,+0x10 = (0, 1, 0),+0x50 = (0, −1, 0),+0x90 = 0. Sphere at z 0.1: f = 0.5,+0x88 = 0.1. At z 0.2: no hit. Segment-sphere with the roles swapped andm = (0, −2, 0)moving the segment: the same fraction, halves swapped. - Basis.
W = (0, 1, 0):U = (0, 0, −1),V = (−1, 0, 0).W = (0.6, 0.8, 0):U = (0, 0, −1),V = (−0.8, 0.6, 0). - Sphere-capsule (capsule
B = 0, r 0.35, h 1.8; sphere r 0.1;malong x unless said):- side,
C = (−1, 0, 1),m = (1, 0, 0): roots[0.55, 1.45], f = 0.55,+0x50= unit(1, 0, −0.1) =(0.99504, 0, −0.09950)(into the capsule),+0x90 = (−0.45, 0, 1); C = (−1, 0.3, 1): f = 0.66459;C = (−1, 0, 1.6)(top cap): f = 0.57574;C = (−1, 0, −0.05)(bottom cap): f = 0.79384;C = (−1, 0, 2.0): no hit;- inside,
C = (0, 0, 1),m = (0.2, 0, 0): roots[−2.25, 2.25], no hit;m = (1, 0, 0): roots[−0.45, 0.45], hit at the exit 0.45,+0x50 = (0, 0, −1); - exact vertical drop,
C = (0.1, 0, 3),m = (0, 0, −2): roots[1.54437, −0.55563], no hit (true touch at 0.55);m = (0.01, 0, −2)(tilt 0.29°, not parallel): f = 0.55628; - exact vertical rise,
C = (0.1, 0, −1),m = (0, 0, 2): f = 0.45563 (correct); - store order,
C = (−0.3, 0, −0.5),m = (1.2, 0, 2.4): roots stored[0.625, 0.17089](side exit, then bottom entry), f = 0.625; withm = (0.6, 0, 1.2)the exit is 1.25 and f = 0.34177.
- side,
- Capsule-sphere with the capsule moving: the (2, 3) case with
mnegated;+0x90is the sphere's centre minusf·m. - Overlap entries. Segment-capsule via (4, 3): a hit leaves
+0x80and+0xb0as the slot held (fill the slot with a sentinel and check it survives). - Body pass. A fake world: a human mover (type mask
0x40) with candidates flagged 4,0x44,0x20; masks 4 (walking),0x44(airborne),0x64(charging and airborne),0x40004at gait 4,0x100004for player 1; a non-human mover → 2. A candidate with two capsules that both hit gives one contact (the first in the chain); the same throughIPhysics_CollideShapegives two. - Humans and walls in one pass. Through
PhysicsBody_Sweepwith a fake mesh contact at 0.6 and a capsule met at 0.3, answers 4 for the human: the human's contact is resolved, the wall's is not reached in pass 1. A mesh and a body contact both at 0.5: the mesh's first in the walk; throughIPhysics_CollideShape, the body's first. - CollideShape options.
max1 with two hits returns the nearer; option0x2000naming the mover's own owner removes its self-contact;0x800with+0x1c = 4drops a candidate withoutBLOCKHUMANS;0x10000with0x4000000drops an airborne owner's body; option0x80makes no difference (same-group bodies are always skipped).