AI moving: gaits, path planning, routes and steering¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). Static reading in Ghidra;
every claim is confirmed (code) at the cited address unless it says otherwise, as on AI humans, which this page
was split from because that page had grown too large. The overview of the brain, its goals and its actions stays on AI
humans.
An AI moves by writing brain fields, never a stick: +0x110 the heading to move along, +0x114 the speed (through
0x0028ab28; 0x0028aac0(brain, 0) stops), +0x90 the point aimed at and +0x118 its radius; the human's control
(Human_UpdateControl) moves it from those: speeds, turn limits, start and turn clips are on
Characters. A gait's speed is Human_SpeedForGait (0x0022ae40): 1.6286 m/s walking,
7.8012 m/s running. Confirmed (runtime): the civilian's per-player stick magnitude stayed 0 through its move and
move-to-human actions; only the attack action wrote a stick (1.0, for throws).
From 8 m behind the player (confirmed (runtime)): a turn on the spot (clip 398), the run start 414, the run 410 at
7.80 m/s, then at 1.29 m a charge (0x20). Closer, the move-to-human action walks in the fight-stance clips 372-380 at
about 2.3 m/s.
Gaits and speeds¶
An AI never writes a stick: a goal or action sets the brain's move speed +0x114, usually from a gait, and the
human's control turns that into velocity (Turning, below for the speed). Confirmed (code) at the
addresses cited.
Brain_SetMoveGait(brain, gait)(0x0028aac0): speed =Human_SpeedForGait(human, gait), 0 for gait 0;Brain_SetMoveSpeed(0x0028ab28) stores any speed directly (FollowFormation, DevilRun, RunCarrot set speeds between gaits).Human_SpeedForGait(0x0022ae40, jump table0x0055bec0) reads the human's anim record (+0xd4) speeds (Speed classes) without the+0x3a4multiplier; a human with no anim record gets 0.Human_GaitForSpeed(0x00221760) maps back: 5 at or above the sprint speed, 4 at or above the run, 3 the jog, 2 the walk, else 0. The move action and the steering use it to tell standing from moving.
| Gait | Name | Record field | Rembrandt (m/s) |
|---|---|---|---|
| 0 | stand | - | 0 |
| 1 | sneak walk | +0x16c |
1.585 |
| 2 | walk | +0x170 |
1.629 |
| 3 | jog | +0x174 |
4.857 |
| 4 | run | +0x178 |
7.801 |
| 5 | sprint | +0x17c |
10.245 |
| (stance) | fight-stance walk | +0x164 |
3.429 (clip 380; 372 for an AI with turn boost ≤ 0, brain bytes) |
The speeds are each class's own clip speeds; Rembrandt's are confirmed (runtime) on Characters.
The gait each goal asks for (the gait argument of its move actions; confirmed (code) at each Process):
| Gait | Goals |
|---|---|
| the script's argument | MoveToFlag (1), MoveToFlagNetFlag (3), MoveToUseFlag (4), MoveToPosition (5), MoveToHuman (6), TravelPath (0x38), Wander (0x3a), DestroyItem, DestroyCar, Patrol (0x73) |
| 2 (walk) | pedestrians and their flags (0x69, PedFlag, AreaWalker, Hooker, Peddler), StandIdle, Mark, Scout, PathScout, Backoff, Ring, StandGround, Confront, CopPatrol, CopInteract, CopperGuardArrested, BigThrower's hold, the walk back of HoldPosition |
| 3 (jog) | ChaseSupport (3 m), GetItem within 3 m of the object, BigThrower, StationaryShooter's return, Hide near the leader |
| 4 (run) | EngageEnemy, GoalRiot, Investigate, Respond, RiotCop, Tag, GrabTarget, Mace, Grabber, BigDefender, LeftTurf (back into the turf), FollowAndAttack / FollowAndDefend beyond reach, the Melee goal's GetItem |
| 5 (sprint) | Scatter, RunFromTrain, CallGang, Hide away from the leader; EngageEnemy after a running target while stamina is above 50 % (Sight) |
| chosen | Shadow: the leader's gait (2 at first, 3 beyond 50 m); AvoidEnemies: 5, or 4 below 50 % power, or the goal's own; FollowFormation and FollowObject: a speed between walk and 1.25 × sprint (follow); Spectate: 2, or 4 when the watched man runs |
Speed control (Human_UpdateControl 0x00243848, the non-pad branch), each update, with v* = brain +0x114:
Brain_UpdateGait; a gait change rebuilds the human's move anims.- The wanted speed
w: wounded andv*> 0.1 → the walk speed; in the fight stance andv*> 0.1 → the stance walk (+0x164), else 0; otherwisev*, at most 25 m/s. Running faster than the run speed with a held object of kind 4 or 6 drops it, except an object of type class 6 whose+0x5ais 0. - Starting (current speed 0,
w> 0): unless the anim set's kind is 3 or 7 or its+0x18is 4, the human's action becomes 6 (a run start) whenw≥ the run speed, 8 instead with the start boost+0x0cabove 0, or 5 (the walk start) whenwis exactly the walk speed; the first update's speed is min(w, 2 m/s). - Accelerating: + 8 m/s² × the step (0.267 m/s per update at 30 Hz), or + 32 m/s² (1.067 m/s per update) while
more than 2 m/s short, never past
w. - Slowing: − 32 m/s² (1.067 m/s per update) down to
w. Diego and Vargas (classes0x77,0x78) running or sprinting withw= 0 stop at once with action 9 (a stop clip). - Far away (detail level
+0x3332 or more, The enemy scan) and no held flag of0x310c0880: velocity =walong brain+0x110, and the heading is set to+0x110at once (no turn limit, no clips). - Otherwise the turn (Turning). Stopped (
w= 0): when the heading error is above 15° and no flag is held, a turn on the spot starts (Human_StartTurnOnSpot, the clip is Human's); within 15° nothing turns. A rear grabber turns at twice the limit and walks.
Turning¶
A human with no pad (per-player +0x1b = 0) is moved by Human_UpdateControl (0x00243848, the default control
Human_SetUpdateControl 0x00227c48 installs), not by the stick code (Human_PlayerLocomotion 0x00240e38). Each
update it turns the human's heading toward brain +0x110 by at most Human_MaxTurn (0x002213d8), linearly:
the eased turn of CfgTurnRate (0x00510308-0x00510310) is read only on the pad path, in the air and while
grabbing, so it does not apply to the AI. While moving faster than a jog, a large heading change may instead start a
turn clip (0x0025c5b8, Human); the switch 0x005104e8 is 1. Confirmed (code).
Human_MaxTurn picks one word of a pair at 0x005101b0 (Movement constants):
the player's word for a pad human whose brain is not dead, the AI word otherwise. In order:
| State | Pair | AI word per update | Boost applies |
|---|---|---|---|
| turn-limited (two human flags) | fixed | 4° or 6° (both kinds) | no |
| wounded | 0x005101b0 × 0.25 |
0.375° | no |
| grabbing, blocking or ducking | 0x005101b0 |
1.5° | no |
| sprinting (gait 5) | 0x005101b8 |
2.5° | yes |
| running (gait 4) | 0x005101c0 |
4° | yes |
| jogging (gait 3) | 0x005101c8 |
6° | yes |
| fight stance | 0x005101d8 (AI word 0x005101dc) |
24° | no |
held flags 0x1000080 |
0x005101b8 × 0.25 |
0.625° | no |
| otherwise (walk, stand) | 0x005101d0 |
12° | yes |
"Boost applies": the gait rows go through Human_GetTurnRateForGait (0x002212d0), which multiplies the AI word by
the turn boost +0x0b (× (b + 1), or ÷ (1 − b) when negative); the other rows do not. At 30
Hz a walking AI turns 360°/s, a running one 120°/s, unboosted. Confirmed (code); the AI words are .data values that
CfgSetTurnRates does not change: it writes only each pair's first (player) word, so its sixth argument is the
player's stance turn (0x005101d8), and an AI's is always 0x005101dc's 24° (Characters).
The rows are tested top down, so a wounded AI turns 0.375° per update even in a fight stance.
The move action¶
Vtable 0x00542f20, confirmed (code) at the addresses cited. MoveAction_Init (0x002fb9e8) takes the radius, the
point, the gait (its speed goes to +0x40), an option bit, a start delay, an object to face (+0x3c, 0 for none) and
a "flag kind 0x12" bit.
- Start (
0x002fc420; Ghidra has no function there): done (2) when0x00223b70holds or the human is within 0.1 m. Otherwise it resets the avoidance state (0x002890a8), writes the point and radius to brain+0x90/+0x118and asks for a route (Path planning): none needed → it steers straight at the point; none possible → done (2). - Update (
0x002fc5c0), each update:- Done when
0x00223b70holds or the abort bit is set; wait while the human is busy or an avoidance wait runs. - Every 30 updates (offset by the human's index), with a route: when the final point is in sight
(
0x002221e0), drop the route and go straight. - With a route: done within the radius; aim at the route's current waypoint (Following).
- Dynamic obstacles (
0x00414188, 4 records of0x150at0x006f3a10): a blocked segment → speed 0 and a 1000 ms wait. - Every 31 updates (counter
+0x4d) the current aim (the route's waypoint, or the destination without a route) must still passHuman_CanWalkStraightTo(0x002221e0), else done (2) with+0x284left at 0, so the goal plans again. Done (2) when the aim is within 1 m in plan, more than 1.5 m away in 3D, the human is not airborne (0x00227f90) and the direction to the aim lies within 20° of straight up or down (a point on a floor above or below). Confirmed (code) at0x002fc854-0x002fc984. - Steering round other humans (below) may replace the aim by a detour point (radius 0.3). Steering is not reached on every update; see "The speed and the legs" below.
- Heading = the direction to the aim, or to the object
+0x3cwhen set; write+0x90,+0x118and+0x110. - Speed: with an object to face, the gait's speed. Otherwise turn on the spot (speed 0) while stopped and
more than 30° off; else the corner speed (below):
+0x0cwhile the squared distance to the aim is at least the braking distance²+0x48, else+0x44, never above the gait's speed. - Stuck (
MoveAction_CheckStuck0x002fc330): while the asked speed is above 0 a counter (+0x4e) counts updates (speed 0 resets it); on the 61st, less than 0.2 m covered since the saved point, measured in 3D (Vec_DistSq, height included), → brain+0x284= 3, done; else the point is saved again and the count restarts. A climb up a slope counts its height. Confirmed (code). An AI walking a heap too steep for floor is not braked by its wall slides and covers about 1.1 m per 60 updates there (Characters), well clear of the test.
- Done when
- The speed and the legs (confirmed (code) at
0x002fc778-0x002fc7a8and0x002fc988-0x002fc998):- Where the speed comes from. Just before the route step, the update takes its speed: the steering override
(
+0x34) while its count+0x32is non-zero, else the action's own+0x40. That one value goes by pointer toRoute_Follow, which may change it, then to the steering (step 6), and as the cap to the corner speed (step 8). - When the override's count drops. Only at the start of the steering call. An update that returns before step 6 does not use one up.
- Route_Follow's result. With a route, any result but 1 ends the update there, before the steering, and is
the action's result. It returns:
- 0 while held at a claimed node within 2 m (Steering step 15);
- 0 on each update of a climb leg (
Route_ClimbLeg: trying, or once the climb starts), and 2 after 31 failures; - 0 or 2 for a jump or drop leg (
Route_StartJumpLeg); - 0 for a charge (
Route_SmashLeg); - 2 for a refused link;
- 0 when
Route_TryClimbNearstarts an early climb.
- So steering never runs on climb, jump or charge legs, nor while held at a node. It runs on walked legs
(kinds 1, 2 and
0x10) and on the walk up to a climb. Once a climb has started (+0x12= 0), the climb itself holds the human (step 1's busy test, inferred).
- Where the speed comes from. Just before the route step, the update takes its speed: the steering override
(
- Brain
+0x284says why a move failed: 1 no polygon or no route, 2 or 4 an edge it cannot take, 3 stuck. - Nothing keeps a move going between actions (confirmed (code)).
Brain_UpdateGoals(0x0028fbb0) starts every think withBrain_StopMove(0x0028ac18): speed+0x1140 (Brain_SetMoveGait(brain, 0)) and heading+0x110= the human's facing; only a move action's Update (step 7 above) writes+0x90,+0x110,+0x118and the speed back, each update. Abort (0x002fc560) sets the abort bit, ends the avoidance (0x00289108) and frees the route (0x0029aa20); it writes none of the move fields, and destroy (vtable+0x24,0x004eef40) is empty. So when a move is replaced (GoalMoveToFlag's Resume,0x002da550:Brain_ClearActions, thenHuman_DropTarget0x00226f70, which only clears the target) the human is asked to stand for every think in which the new action is still counting its start delay:Action_Update(0x002f9e48) takes the time since the brain's previous think (+0x30) off the delay, the first time too, and returns before the class's Update while any is left.GoalMoveToFlag's delay isRandom_IntRange(0, 250)ms, so a draw within one update (about 33 ms) starts the move in the same think with no gap, and a longer one leaves up to 7 thinks at speed 0; the control slows a walk to rest in two updates and restarts it at once at the walking speed (Characters).
The corner speed¶
How fast a routed AI may take the next corners. Confirmed (code) at the addresses cited; not measured.
- When (the move action's speed step): recomputed when the counter
+0x4c, which counts down each update, runs past 0, or when the aim has moved more than 0.07 m (squared 0.005) from the aim saved at+0x20at the last recompute. After a recompute the counter is 10 when the first corner's speed is above 0, else 0, and the aim is saved. So about every 11 updates while moving, and at once when the waypoint changes. - The points (
MoveAction_CollectPoints0x002fbd18, at most 3): the human's position; the steering's detour point when one is running (brain+0xc4); then the route's waypoints from the current one, claiming each (Waypoint claims): a waypoint entered by a kind-4 or0x80link stops the list before it, one entered by a link whose flags are exactly0x10or0x40is the last one listed; then the destination when there is room. - The two corners (
MoveAction_CornerSpeed0x002fc158), with v the human's speed (+0x1ac) and c the move's speed cap:- corner 1 is points 0 → 1: when they are more than 0.35 m apart (squared 0.1225),
+0x0c=MoveAction_TrialSpeed(v, c, points 0 and 1), else+0x0c= c; - corner 2 is points 1 → 2 (only with 3 points, and when more than 0.35 m apart):
+0x44=MoveAction_TrialSpeed(v, +0x0c, points 1 and 2), else c; - the braking distance: when
+0x44<+0x0c, with t = (+0x0c−+0x44) / 32 (the time to shed the difference at 32 m/s², the AI's braking rate), d =+0x0c× t − 16 t², and+0x48= d². Otherwise+0x48= 0 (the second corner's speed is never used).
- corner 1 is points 0 → 1: when they are more than 0.35 m apart (squared 0.1225),
MoveAction_TrialSpeed(v, trial, from, to)(0x002fbef0):- the human's right vector (his facing crossed with up,
0x00511740), andMove_CornerSpeedLimit(0x002fcd90): the radius of the circle tangent to the facing at from through to (Math_CircleRadius2D), the speed whose per-update turn at that radius is the gait's turn step (2R sin(ω/2) × 30), stepping the gait down while a lower gait allows as much; the trial is the smaller of that and trial, but at least the walking speed; - sub-steps per output point k by the current speed v: 4 above 7 m/s, 5 above 5, 6 above 3, else 7; times
(2 × detail + 1); the output points number 10 − detail (the detail level
+0x333); Human_SimulateMove(0x0022aae8) plays the walk toward to one update at a time: the speed moves toward the trial (−32 m/s² × 1/30 down; +8 m/s² × 1/30 up, +24 more while over 2 m/s short; from rest it starts at min(trial, 2)), the heading turns toward to by at most the gait's turn step (Human_GetTurnRateForGait, boost included), and the human moves speed/30 along it; it records a point every k sub-steps and stops early once the heading is within 0.005 rad of the direction to to or to is within one step;- each recorded point must have an area and a clear line to the next (
Nav_FindArea,Nav_LineClearWithin0x0024f718); a failure lowers the trial by 1 + 0.75 × detail and simulates again, until it is at or below the walking speed, which is then returned; the first trial that passes is returned.
- the human's right vector (his facing crossed with up,
So a running AI brakes for a tight corner by the time the arc would leave the path polygons, and starts braking for the second corner at the distance a 32 m/s² stop needs.
The move-to-human action (MoveMelee)¶
Type 2, MoveMelee (vtable 0x005431e0), made by MoveToHumanAction_Init(near, far, action, brain, human, delay,
limit) (0x002fcf50): the fight's footwork, which keeps a distance band from one human. The fight goal's approach,
the reposition, Spectate and Backoff use it. Confirmed (code) at the addresses cited.
Fields: +0x0c the human's handle, +0x10 / +0x14 near² / far², +0x18 the side counter, +0x1c the radial
counter, +0x20 the time limit (made absolute at Start), +0x24 "no stance" (class +0x11b 6 or 7,
Brain_IsKind6Or7 0x002fd128), +0x25 the saved turn boost.
- Start (
0x002fcff0): the limit becomes now + limit; a "no stance" human leaves the fight stance; the turn boost is saved. Abort (0x002fd068, never refused): a "no stance" human turns to face the human; the turn boost is restored. - Update (
0x002fd4e8), each update, withd²the squared distance:- Done (2) while the mover is in a standing reaction, or when the human is no longer up.
- With the radial counter at 0 and the limit not passed:
d²< near² → counter −10 (back off);d²> far² → +10 (close in), and when also within the mover's far melee range (+0x140) the turn boost is raised by one (with conditions on the two facings, the human's state and held objects). - With the side counter at 0 and the limit not passed, the side (
MoveMeleeAction_PickSide0x002fd158): 2 → side counter +10 (circle one way), 6 → −10 (the other way), both clearing the radial counter; 8 → none. - The move direction: radial counter > 0 → toward the human, gait 4 (run; 2 for "no stance"); < 0 → away from him, gait 2; each update the counter steps one toward 0. A side counter adds ±1 × the mover's right vector (gait at least 2) and steps toward 0 likewise.
- Done when
d²lies in the band, the side counter is 0 and |radial counter| < 6. After the time limit, done once both counters are 0. - Brain
+0x110= the heading of the move for a "no stance" human; otherwise the heading to the human (confirmed (code), the store at0x002fd9b4), so he turns to face him at the stance rate while the stance walk strafes him along the move. The aim point+0x90= his position + 2 × the move direction, radius 0; brain+0x11c(strafe) = not "no stance"; the gait goes toBrain_SetMoveGait.
- The side (
0x002fd158, from the mover's sector record, at most 1 s old): keep circling while the sector the mover is heading into is free; else prefer whichever of sectors 2 and 6 (beside the human) is free and less crowded (Sectors_GetCost: flags + 2 × humans); with both free, the side by the human's flag0x10000. When the two sides cost the same: straight in (8), unless the human is grabbed from the rear or grabbing from the rear; then, when the human is a threat, circle toward his back (sector 4, or 0 for a rear grab), raising the turn boost by one when the mover is one of his active attackers.
So a waiting attacker backs off over 10 updates when too close, runs in over 10 when too far, and circles 10 updates at a time to a free side, facing his man.
Path planning¶
The route request (0x0029a8c0), with its state at brain +0xe0 (+0x04 the route, +0x08 the waypoint radius,
+0x0c a state, +0x10 the waypoint index, +0x14 an edge mask). Confirmed (code). It plans over the level's
path data: the polygons are the walkable areas, the C records the graph's
nodes and the D records its edges. A D record on node N naming node M is the step M → N (A* expands
from the destination, and the follower reads the link from the waypoint before on the waypoint's own records,
0x00251070); inferred from both readers. Most links come in pairs, but a jump down is one-way (level99: node 184
on the ground holds a kind-4 link from 395 on a roof 4 m up, and 395 none back).
The edge mask (u16 route state +0x14, brain +0xf4) is 0xff for every human: the route state's
initialiser (0x0029a388) sets it, nothing in the move action or GoalMoveToFlag changes it, and the goals that do
change it put 0xff back in their End. So a scripted move admits every link kind on the disc (1, 2, 4, 8, 0x10,
0x80); only 0x100 is outside it. Confirmed (code) at the writers, and at runtime
(Vermin's fence). The other writers: 0x13 in the Start of the goals at 0x002aafd0 and
0x002c1470, 0xbf (no 0x40) after a failed move in 0x002ab4b8, and | 0x100 for a Warrior (brain type 3) in
GoalHide's Start (HideGoal_StartMask 0x002cc908) and, in FollowFormationGoal_Start (0x002dfe30), for a
Warrior on the levels whose level-table id (+0x04 of the 0x84-byte entry) is 0xe, 0x33, 0x34 or 0x52.
Confirmed (code).
Mask bit 0x100 is not a link kind. No D record on the disc has it (the counts below), so it admits nothing. What
it does is switch off the ClimbFilter extra costs in the edge cost and the 0x80 re-search in step
5: a Warrior hiding, or following on those four levels, takes climbs at their plain distance. Confirmed (code) at
0x00251890 and 0x002511c8; the disc count is a check of the files.
The link kinds on the disc (NTSC-U, all 64 .lev files' path data, counts only, 2026-10-11; "one-way" is a D
record with no record of the same kind back). The meanings are inferred from the geometry and the follower's handlers:
| Kind | Links | One-way | Length in plan | Height change along the step | Meaning |
|---|---|---|---|---|---|
| 1 | 238,422 | almost none | median 3.3 m | ±9.4 m at most, median 0 | walked |
| 2 | 2,126 | 64 | median 0.64 m | −1.5 to +3.2 m | walked (a short joint, often between two areas; no code treats it apart from 1) |
| 4 | 5,890 | 5,372 | median 0.8 m, at most 15.9 m | 91 % go down, median −1.6 m | jump or drop (Jump legs) |
| 8 | 982 | none | always 1.0 m | always 0 | climb over a fence or wall: a pair of nodes 1 m apart, one each side |
0x10 |
746 | none | always 1.4 m | always 0 | a door; every one has the avoid bit set in the file |
0x80 |
4,730 | all | always 0.8 m | median +1.4 m | climb up onto a ledge; its way back is a kind-4 drop |
No link on the disc has 0x40 or 0x100; the charge kind 0x40 is given at run time by a breakable door or pane
(Objects). Polygon s16 +0x02 (summed into the area's first path at load) is non-zero in
1,257 of the 1,754 areas, 1,251 of which own route nodes; 60 areas own nodes but have 0 there, so a request between
two areas refuses them (inferred: "on the graph").
- Find the human's polygon (
Human_GetNavPolygon0x00247958, cached at human+0x1b4with its point at+0x2b0; fallbacks0x002505b0,0x0024e218; see A point in a hole). None →+0x284= 1, fail. - Straight line first: when the walkable-line test (
0x0024fbf8, below) passes from the human's position (+0x2b0, at its own height) to the point with the mask 0, no route is made and the action steers straight (0x0029a8c0, the route request). Confirmed (code). The destination's area is thenNav_FindArea(0x00250708) of the point itself, with no fallback: none →+0x284= 1 and the request returns 2 (the move action ends at once). - Otherwise both ends must be in one polygon, or both polygons must be on the graph (polygon
s16 +0x02≠ 0; inferred meaning). Each end's node is the nearest of its polygon's nodes (the A record's count and first index) that it reaches in a straight line (0x00251150→0x00250e98,0x0024f290), trying up to 30 by distance. -
A* (
Route_AStar0x00251a70), from the destination's node to the start's, so the parent links read in walking order. Confirmed (code):- Before: every node's parent (C
+0x1c) is set to none (0xffff,PathNodes_ResetParents); the open heap and the closed list are emptied. The first node gets g 0, h, f = h and no parent. - The open list is a binary min-heap of node indices keyed on f (C
+0x16), at most 1000 entries (PathHeap_Push; a sift-up moves a parent down only while the new f is strictly smaller, so equal f keep the older entry above). The closed list holds at most 3000 nodes (PathList_Add). Either one full ends the search as a failure. - A step: pop the lowest f, append it to the closed list, save its g and write
0xfabcinto its g (that value marks "closed"). If it is the start's node, the search is over. Otherwise, for each of its D records in file order, skip the neighbour when its g reads0xfabc(closed), when the record's flags (low 16 bits of the word) share nothing with the mask, or when the new g (below) is 65000 or more (0x005105a4; the cap is on the running total, not on the edge). A neighbour already on the heap with a g no larger than the new g is skipped too. Otherwise its g, parent and h (Nav_Heuristic0x002517b0) are written, f = g + h, and it is sifted up (already on the heap) or pushed (a node off the heap is pushed whatever its old g says). - Costs are
u16in 1/16 m. A distance is the 3D distance between the two nodes' positions × 16, truncated to an integer (the heuristic too). - The new g (
Nav_EdgeCost0x00251890) = the expanded node's g + the step's cost, saturating at0xffff(so a saturated total is always over the cap). The step's cost, in this order:- c = 3D distance × 16 (truncated);
- only while
0x0051059c= 1 (the script'sClimbFilter(on),Climb_SetFilter0x002511b8) and the mask lacks0x100, one of: + 200 when the record's flags have 8; else + 320 when they have0x80; else + 80 when they have 4, unless they also have0x40and the avoid bit is set (then nothing); - add 1600 when the record's avoid bit (bit 31) is set;
- add 40 × u − 8 when u, the use count (C
+0x1f) of the expanded node, is not 0 (nothing when it is 0). The expanded node is the one the step enters in walking order (the record lives on it), so this spreads AIs over parallel routes.
Each addition wraps at 16 bits; only the final sum with g saturates. - The end: the search fails on an empty heap. When it reaches the start's node, that node's g is put back, and the chain of parents is counted: a chain of 129 nodes or more fails (a route holds at most 128 node indices:
0x110bytes less the count and the three words at the end). Then, success or failure, every node on the closed list gets g = 0 (PathNodes_ClearOpenMarks), the start's node included. - Before: every node's parent (C
"Fails at 128 nodes" is therefore the route's length, not the open list: a search may visit up to 3000 nodes.
5. Route_Find (0x002511c8) retries a failed search with mask | 0x8c only when the mask (without 0x100) is not
0xff, so never for the default mask. When 0x0051059c ≠ 0, the (possibly widened) mask has 0x80 and lacks
0x100, and the route found steps over a 0x80 link, it searches again without 0x80, with the cap
0x005105a4 set for that search to the f (C +0x16) of the first route's first node and put back to 65000 after.
It then keeps the second route only when its first node's g (C +0x18) is lower than the first route's first
node's g. Confirmed (code). Both g values are read after the second search, and every node on a found chain was
closed and so has g = 0 by then: the comparison is 0 < 0, and the first route (with its 0x80 climb) is always
kept (inferred from the code; not seen at run time).
6. Building (Route_Build 0x002513a8; a pool of 32 routes of 0x110 bytes at 0x006ca250, taken from slot 31
down; +0x00 the count, then up to 128 s16 node indices, +0x104 the start's node, +0x108 the last node,
+0x10c the destination's area). Confirmed (code):
- Leading cut (only when 0x005105a0 = 0; no code writes it, so always): from the start's node, move the
route's first node along the parents while the walkable line (mask 0) from the start point to the next node
passes; it may stop on the destination's node.
- Shortcuts: from each node a in turn, the nodes 2, 3, 4 and 5 ahead are tried, but never the route's last
node; the farthest of them whose D records name a (a link from a into it, of any kind and whatever the mask)
becomes a's next node. It moves on to the new next node and stops when it or the node after it is the last.
- Trailing cut: while more than one node is left, drop the last node when the walkable line passes from the
destination point (in the destination's area) to the node before it. The line runs from the destination,
not toward it.
- Each node's use count (C +0x1f) is raised; freeing the route (0x00251680) lowers it.
The polygons come in areas, an outline and its holes (Path data); the human's
polygon and a node's polygon are an area's first path. Path flag 8 (u16 at polygon +0x48, bit 3) takes a
polygon out of every test below, so an opened door's hole stops cutting the area. Confirmed (code) at the addresses.
- The walkable-line test (
0x0024fbf8, arguments: the start's polygon, the two points, a polygon-flag mask). It reads no collision geometry: only the hazard spheres below and the path polygons. Confirmed (code):- Refused when a hazard sphere blocks the segment (
0x00221f80, below). - It crosses the segment with every edge of the start area's polygons (the outline and its holes, the list from
polygon
+0x20) whoseu16flags at+0x48have neither 8 nor any bit of the mask and whose box meets the segment's. The crossing is in plan (0x0024e938: x and y only, z is interpolated), at segment parameter t in [0, 1], and a crossing at exactly t = 1 is ignored. Edges come from the slab lists when the polygon has them, else all. It keeps the nearest crossing. No such polygon at all → refused. - No crossing: it passes when the end point's area (
0x00250708) is the start's. - A crossing: the end point needs an area. Its polygons are crossed the same way, keeping the farthest crossing, and the line passes only when the two crossing points are within 0.02 m (dist² < 0.0004): the segment leaves one area where it enters the next. Any edge in between, a hole's included, refuses the line.
- Refused when a hazard sphere blocks the segment (
- What blocks a line at a fence is the fence's hole in the path polygons, not its collision. Confirmed
(runtime), from the path data of a
level99state at checkpoint 3 (PCSX2 2.9.94, over the straight line Vermin'sGoalMoveToFlagasks for, (47.49, 42.97) to (46.30, 20.22); both ends lie in the street's area, whose outline is polygon 0): the segment crosses two holes of that area, a 4-vertex hole of flags 7 spanning x 44.14 to 50.61 and y 24.38 to 25.19 (the climbable fence, 6.5 × 0.8 m) at t = 0.78 and 0.82, and the hole of fence door 2 (x 44.93 to 49.50, y 30.79 to 31.56, flags 7) at t = 0.50 and 0.54. The nearest and farthest crossings are metres apart, so the line is refused and a route is made (Vermin's fence). An opened door's hole gets flag 8 (Objects) and drops out; the fence's never does. A test that treats the area as the union of its polygons (a hole counted as walkable) lets the line through. - The hazard spheres (
0x00221f80, confirmed (code)): 64 records of0x40bytes at0x0065ff50,+0x00the centre,+0x10/+0x20its box (min / max),+0x30(r + 0.3)²,+0x34(r + 0.8)²,+0x38a word (0 from the only adder),+0x3cin use; their count is0x00510170, cleared at level start (0x00217f68). The test runs only when the count is non-zero and0x0051057cis non-zero (1 in the ELF's data; no code writes it). A sphere blocks the segment P → Q when its box meets the segment's box (x, y and z), the segment passes within r + 0.8 m of the centre in 3D (0x00336d28, squared distance to the nearest point of the segment), and the segment heads at it: (Q − P)/|Q − P| · (P − C)/|P − C| < −0.707 (within 45°). The points are tested at their own heights; there is no lift above the ground.0x002198b8(r, word, centre)adds one (and marks the route links within r of it to be avoided,0x00253078→0x00252c90: bit 31 of every D record whose edge passes within r + 0.3, counted so overlapping spheres release it only once), and0x002199c0removes one. The only adder is0x003a5a90, called by the fire particle types (0x003c40a0, and0x003c8b28from0x003ca050, Particles) with r random in 0.5 to 0.75 m: an AI's straight line refuses to run into a fire. In the checkpoint 3 state the count was 0, so0x00221f80returned 0 at once. Confirmed (runtime) for the count; that fires are the only source is confirmed (code) from the single call site. - A node in a straight line (
0x0024f290, the end-node search's test): refused when a hazard sphere blocks it (0x00221f80) or when the segment crosses an edge of any of the area's polygons without flag 8 or the mask (0 there). - A point's area (
0x00250708):Nav_FindAreaCandidate(0x00250760) casts one ray through the world's collision (WorldManager_RayCast,*0x005147c4) from 0.4 m above the point, straight down, 4 m long (so it finds ground down to 3.6 m below the point). No hit, or no path data, → no area. Otherwise it walks the area list (0x00510588, then each first path's+0x24) and takes the first area whose first path has itsu16 +0x4aequal to the hit's area byte (hit record+0x2e: the collision triangle's byte+0x09, Collision), whose box widened by 0.35 m holds the point in plan, whoses16 +0x00(vertex count) is 1 or more and whose flags lack 8. Then the inside test0x0024ea60over its polygons, skipping flags 8 and0x10. Confirmed (code). The first triangle the ray meets decides: a point on a walkway finds the walkway's byte, and a point under one finds the ground's unless the walkway is less than 0.4 m above it.
0x00251d28 is a second A* search that stops at the first node beyond a distance outside a cone of
directions, skipping bit-31 edges; its one caller places spawned humans out of sight (The search).
A point in a hole¶
The point-in-a-hole test and its fallbacks are on AI pathing: a point in a hole.
Following a route¶
The route state (brain +0xe0): +0x00 the brain, +0x04 the route, +0x08 the waypoint radius, +0x0c the
state (1 jumping, 2 dropping, 3 climbing, 4 climbing a 0x80 ledge, 5 finished), +0x10 s8 the waypoint index,
+0x11 "move on now", +0x12 "the leg is new", +0x13 the climb's failed tries, +0x14 the edge mask, +0x16
"held at the node", +0x17 the skip counter. Confirmed (code) at the readers below.
Route_Follow (0x0029aa88), called by the move action every update it is not busy (step 3 of
the move action), in order. Confirmed (code):
- State 5 → return 2 (the route is over). Otherwise the state is set back to 0 on every call (so a jump, drop or climb state lasts only until the next call after the human is free again).
- Skip check: unless "move on now" is set, the skip counter counts calls; on the call where it was already 6 (the
7th),
Route_SkipReachable(0x0029b4b8) tests the current waypoint and, when it may be skipped, sets "move on now"; the counter goes back to 0. - Moving on: with "move on now" set,
Route_Advance(0x0029b6d8): "the leg is new" = 1, the flags and both counters cleared, the index + 1, then while waypoints are left, each thatRoute_SkipReachablepasses is skipped too; the first that is not becomes current: unless the leg into it is of kind 4, the human's claims are released (Nav_ReleaseClaims, below); the radius becomes 0.25 m; brain+0x90= its point,+0x118= 0.25. Past the last waypoint the route is freed (RouteState_Release) andRoute_Followreturns 1 (go on straight to the destination). - The legs: in = the flags of the link from the previous waypoint to the current one (0 at index 0), out = from the current one to the next (0 at the last).
- Claims and busy links (only at the detail level
+0x333< 2): claim the current waypoint's node (Nav_ClaimWaypoint, with out); when it is granted, the out link and then the in link must not be busy (kind 4:Route_IsLinkBusy0x0029a3f0; else kind0x80:0x0029a6c8; other kinds are never busy). Refused or busy → "held" = 1 and, when the human is within 2 m (3D) of the waypoint's point, the aim is that point with radius 0, the move is stopped (Brain_StopMove) andRoute_Followreturns 0. Granted and free → "held" = 0. - A new leg (index > 0, "the leg is new" = 1): in has 8 or
0x80→ the climb handler; else in has 4 → the jump handler; else, when the link's avoid bit is set,0x40→ the charge, any other → refused (brain+0x284= 4 for0x10, else 2) and return 2; otherwise "the leg is new" = 0. The climb and jump are tested before the avoid bit, so a climb or jump link with the avoid bit is still climbed or jumped. - When out has 8 or
0x80,Route_TryClimbNear(0x0029b9b0, the fast climber's early climb) decides the result; otherwise 1 (keep walking to the waypoint).
Route_SkipReachable(i) (0x0029b4b8): only when the link into waypoint i (a kind-1 link for i = 0) and the
link out of it both have kind 1 or 2 (flags & 3), and i is not the last waypoint. Then it passes when the human can
walk straight to waypoint i + 1's node (Human_CanWalkStraightTo 0x002221e0, from where he stands). Confirmed
(code). There is a turn test that never refuses: when the leg into i (from i − 1's node to i's) is longer than
5 m (3D), the code compares the cosine between two unit vectors with cos 135° (−0.707), but both vectors are that
same leg (0x0029b56c-0x0029b654 subtract the same two node positions twice), so the cosine is 1 and the skip goes
ahead. Confirmed (code) from the disassembly. A port must not add a turn limit here.
When the straight line is tested (confirmed (code); each call is the walkable-line test of Path planning with the mask 0):
- Asking for a route (
0x0029a8c0): from the human's position to the point; passing means no route at all. - Skipping a waypoint (
0x0029b4b8, every 7th call and when0x0029b6d8moves on): waypoint i + 1 is tried from where the human stands (0x002221e0), and only when the links into waypoint i and out of it are both of kind 1 or 2 (word & 3); the first waypoint counts as reached by a kind-1 link. So a waypoint at either end of a climb, jump, door or charge link is never skipped, and the human walks to it whatever the line test says. - Dropping the route (the move action's update, every 30 updates, The move action): when the
final point passes
0x002221e0, the route is freed and the human goes straight. - Building the route (
0x002513a8, calls at0x00251494and0x002515e8): leading nodes the start reaches, and trailing nodes from which the destination is reached, are dropped.
0x002221e0 first asks the trains: unless brain +0x2e0 equals the current game time (*(0x0050b734) + 0x48),
Trains_IsPathClear must pass (ObjStartTrain); then the
walkable-line test from the human's polygon (0x00247958) and its position in the position table (0x00714b00).
Link kinds. A leg's kind is the D record that steps from the waypoint before to the current one (0x00251070);
the follower reads it on the update after 0x0029b6d8 moves on (route state +0x12 = 1), at waypoint index > 0.
Confirmed (code) at 0x0029ad04-0x0029adb8:
| Leg's kind | What the follower does | Handler |
|---|---|---|
8 or 0x80 |
a climb (8 over a fence or wall, 0x80 up onto a ledge; the kinds on the disc) |
0x0029b848 |
| 4 | a jump (route state 1) or a drop off the edge (state 2), Jump legs | 0x0029baa8 |
any, avoid bit (31) set, 0x40 |
the charge at a breakable door or pane (Objects) | 0x0029bca0 |
| any other, avoid bit set | refused: brain +0x284 = 4 for kind 0x10 (a closed door), else 2; the move ends |
|
1, 2, 0x10 (an open door) |
walked |
The climb (0x0029b848): each update it aims at the waypoint (brain +0x90, heading +0x110, +0x11c = 1),
turns the body to it (0x0021b100) and calls Climb_TryStart (0x002826f0), the player's own climb
(Characters: Climbing): the forward probes must find a climbable face. On success the route
state +0x0c = 3 (kind 8) or 4 (kind 0x80), brain +0x11d = 1, and the clips play; otherwise it runs on at gait 4
(0x0028aac0) and tries again, giving up (move done) after 31 failed updates (counter +0x13, cleared when the
waypoint changes). When the next leg is 8 or 0x80, a human with flag 0x2 (human +0xe0, a fast climber,
HuSetFastClimber) whose gait (human +0x1a8) is above 3 starts the climb early, within 4.5 m (3D) of the next
waypoint (Route_TryClimbNear 0x0029b9b0): on success the state is 3, the route moves on at once
(Route_Advance), brain +0x11d = 1 and "the leg is new" = 0.
How a climb ends (confirmed (code)). A climb that starts sets the brain's arrival flag +0x11d itself. While the
climb clips play, the human is busy and the move action returns at its first test, so nothing else runs. On the first
update after the climb, MoveAction_Update finds +0x11d set (0x002fc674), clears it, and asks the route to move
on (0x0029aa78 sets "move on now"); Route_Follow then puts the state back to 0 and Route_Advance makes the
waypoint after the climb's far node current. The climb's far node (the waypoint the climb leg entered) is passed
without being walked to: the climb itself carried the human over it. The arrival test Brain_CheckMoveArrival
(0x0023f9d0) only ever sets +0x11d (when it is 0 and the front action is a move); only the move action clears it,
and when a steering detour is running the flag ends the detour (0x00288ad0) instead of moving the route on.
Waypoint claims¶
The 40-entry claim table (the nearer of two humans on a node goes first) is on AI pathing: waypoint claims.
Jump legs¶
The jump and drop legs of a route are on AI pathing: jump legs.
Neighbour sectors¶
Each human has a record of who stands round him in eight 45° sectors. The fight's footwork (MoveMelee), the grab and snap tries, the steering and giving way all read it. Confirmed (code) at the addresses cited, except where marked. Code index: Neighbour sectors.
The record: 60 records of 0x48 bytes at 0x006e8318, one per human slot (Sectors_GetOwner 0x0029e470
maps back). Per sector k (0-7), 8 bytes at +8k:
| Offset | Type | Meaning |
|---|---|---|
+0 |
handle | the nearest human in the sector (the null handle when none) |
+4 |
u16 | flags: 1 someone within 1.5 m, 2 someone very close (below), 4 no walkable line out (a wall), 8 a free player near |
+6 |
u8 | 1 while the wall probe has still to run |
+7 |
u8 | how many humans stand in the sector within 1.5 m |
Then +0x40 holds the owner's heading at the last refresh and +0x44 that time (ms). The distance is not kept: it
exists only while the record is rebuilt. Sectors_Construct (0x0029e250) clears the handles. Sectors_Reset
(0x0029e2b0, also called from Brain_Init) zeroes the heading and time and sets every probe byte.
Sector numbering (Sectors_SectorOf 0x0029eb10 → Human_GetSectorOf 0x0029eb38): take the bearing of the
point from the owner minus the owner's heading, wrapped to [0, 2π). Sector k is centred on k × 45°, with
edges at ±22.5° (0.3927 rad), so 0 is straight ahead. Both angles are the game's headings (Vec_HeadingFromTo and
Quat_Heading inside Heading_RelativeTo 0x00386798), which grow from +y toward +x, clockwise seen from above
and toward the human's right (Characters: Step control gives the proof). So the index
rises clockwise: 2 is his right, 4 behind, 6 his left. Confirmed (code). A bearing exactly on an edge goes to the
lower index on the right half (0 to π) and to the higher index on the left half. A port whose headings grow
anticlockwise (the turn angle θ about +z, minus the game's heading) must number the sectors the other way round, or
negate its bearings first; otherwise every "first try s + 1" below leans the wrong way.
Refreshing (Brain_GetSectors(brain, maxAge) 0x0028fe90 → Sectors_Update 0x0029e5d8): the record is
rebuilt when now ≥ +0x44 + maxAge; otherwise it is returned as it is. Callers pass 1000 ms (MoveMelee, the
steering, the fight goal's re-target and grab tries) or 500 ms (giving way). A rebuild:
- Stores the owner's current heading at
+0x40and gives every sector the null handle, count 0 and probe byte 1. - Lists up to 60 humans within 1.5 m of the owner, in every direction, each with his squared distance
(
Humans_FindAhead(1.5, …, mask 1)0x002274a8). Despite its name this is a plain radius search round a point. It takes every human slot whose word at0x00715390has bit 1 set, except the owner. There is no state check, so downed and busy humans count. - Each listed human raises his sector's count by one. The nearest in each sector becomes its handle.
- The flags are rewritten from the nearest human's squared distance d²: d² < 1.5 → 3 (bits 1 and 2,
within 1.22 m); otherwise d² < 2.5 → 1. Every listed human is within 1.5 m (d² ≤ 2.25), so in practice:
- bit 1 means "someone in the sector";
- bit 2 means "someone within 1.22 m";
- an empty sector gets 0, which also clears bit 4.
(The thresholds look like metres compared against a squared distance. What is described here is what the code
does.)
5. Bit 8 is set only when the owner is an AI (human +0x1b0 = −1). It marks a nearby player who is not already
part of this fight (inferred meaning). Each player human is checked (game state +0x228, count +0x224). Bit 8
is set in the player's sector when all of these hold:
- he is not busy (Human_IsBusy);
- he is within 5.5 m (30.25 squared);
- he is not the owner's target (brain +0x124) and not the target of the owner's target;
- his own target is neither the owner nor the owner's target.
6. Sets +0x44 = now.
The wall probe (Sectors_ProbeWall 0x0029e4b0) runs lazily, at most once per sector per rebuild, and only
when a reader needs it (the probe byte is 1):
- The point: 1.5 m from the owner's position at heading
+0x40+ k × 45°, at the owner's height (the direction is flat, inferred fromVec_FromHeading). - The test: bit 4 is set when
Human_CanWalkStraightTo(owner, point, 0)(0x002221e0) fails. This is not a physics ray. It fails in two cases:- a train's path is in the way, unless the owner's brain is already near a train;
- there is no walkable straight line on the navigation mesh (
Nav_IsWalkableLinefrom the owner's polygon, Path planning).
- After the probe the probe byte is 0. Only the next rebuild clears bit 4.
Sectors_ResetHeading(0x0029e2e8, used only by BigThrower) re-arms the probes with a new heading but keeps an old bit 4.
Readers (0x0029e980-0x0029eb10):
| Function | Answer |
|---|---|
Sectors_IsBlocked 0x0029e9c8 |
true at once when bit 2 is set; otherwise probe, then any of bits 2, 4 or 8 (a very close human, a wall or a free player) |
Sectors_IsFree 0x0029ea48 |
none of bits 1, 2 or 8; then probe, and no wall |
Sectors_IsWall 0x0029ea10 |
probe, then bit 4 |
Sectors_GetCost 0x0029e980 |
probe, then flags + 2 × count |
Sectors_AllClear 0x0029eaa0 |
no sector has bit 1 or 2 (no human within 1.5 m at all) |
Sectors_IsHeldBy 0x0029ead8 |
whether the given human is the nearest in a sector |
Sectors_TurnWay 0x0029ec90 |
+1 or −1, the shorter way round from one sector to another |
A point in a sector (Sectors_GetPoint(distance, out, record, k) 0x0029e350): the owner's position plus
distance along his current heading + k × 45° (not the stored +0x40), at his height (inferred, as for the
probe). The steering passes 1 m (Steering round humans, the standing case). The other caller is
Human_DropCarried (0x00232c60).
Giving way (Brain_GiveWayTo 0x00289ed0, reached only through Brain_PushAside 0x0028a248) is the
standing side: Giving way. The steering is not one of its callers: it handles a standing blocker
with a detour (Steering, case 0) and never asks him to move. Confirmed (code) by the cross-references.
The blocker test (Steering_FindBlocker(state, list, n, myPos, myDir, myStep, outFrac) 0x00288cc8) runs over
the list from Humans_FindAhead: every human within min(2 L, 10 m) of the walker, in any direction
(Steering).
- Skip the walker's target (brain
+0x124) and anyone behind him: the dot product of the walker's unit move direction with the offset to the other human is below 0. - Predict the other human's step:
- a player: his facing × 0.75 × his speed
+0x1ac; - an AI: toward his brain's aim point
+0x90, of length min(the distance to it, 0.75 × his brain speed+0x114) (Steering_ClampStep0x00288f40).
- a player: his facing × 0.75 × his speed
- Let r be the relative step: my step − his. A ray from my position along r, of length |r|, is tested against
a disc of radius 0.63 m round him (
Steering_RayHitsHuman0x00289d68; 0.3969 = 0.63²). There is no hit when:- he is farther than |r| + 0.63;
- he lies behind the ray;
- the ray misses the disc.
Otherwise the result is the hit distance t, measured to the disc's edge. 4. The blocker is the human with the smallest t / |r|. That fraction is also returned. So the test sweeps a circle along the relative motion over one step; it is not a corridor or a cone.
Steering round humans¶
Human_SteerAroundHumans (0x00289138) bends a move action's walk round the one human most in the
way. MoveAction_Update (0x002fc5c0) calls it every update with the steering state (brain +0xa0), the move's
speed for this update, its aim point (the next path point, in and out), its destination (action +0x10) and its
arrival radius (action +0x2c). Confirmed (code) from the disassembly of 0x00289138-0x00289d60 (the decompiler
drops several arguments) unless marked; not checked at run time. The names of the cases are ours.
The state (brain +0xa0; helpers on AI code: steering):
| Offset | What |
|---|---|
+0x00 |
the held point: the point stored by the last successful detour, used as the aim between decisions |
+0x10 |
the predicted contact point of the last decision (C, step 10) |
+0x20 |
the brain |
+0x24 |
the human being avoided (non-zero = avoiding; Steering_SetAvoiding also raises the brain's turn boost) |
+0x28 |
u8, updates since the last decision; Steering_Clear sets 255, which forces a decision on the next update; a move's Start seeds it with the human's index (below) |
+0x29 |
u8, slow decisions in a row (step 5); a move's Start seeds it with the human's index too |
+0x2a |
enabled |
+0x2b |
u8, decisions since the detour was taken (step 6) |
+0x32 / +0x34 |
u16 updates left / the speed override (Steering_SetSpeed(speed, n)) |
+0x3c |
the detour's score (below) |
A move's Start (Steering_ResetAvoidance, 0x002890a8, from the move action's Start) clears the held point to
the origin, turns avoiding off, sets the score to −1e9 and +0x2b to 0. It writes the low byte of the human's
index (human +0x92, his slot in the position table) into both +0x28 and +0x29; it is not a node.
Confirmed (code). It forces no decision as such: step 2 compares the old counter with 3 × the detail level, so the first
call decides when the index is at least that (always at detail level 0), and a human with a lower index waits the rest.
So the humans' decisions are staggered by index. The slow counter starts at the index as well, so the first × 10 of
step 5 comes after 16 − index slow decisions, or on the first one for an index of 15 or more.
Speed overrides. Steering_SetSpeed only stores a speed and a count of updates; the count drops by one at the
start of every call. While it is non-zero the move action takes +0x34 as its speed instead of its own (action
+0x40, read at 0x002fc778), and the brain's move speed +0x114 follows from that (Brain_SetMoveSpeed). So an
override acts from the next update on, for n updates.
One call, in order (P my position, s my speed, human +0x1ac):
- Not enabled, or brain
+0xecset (inferred: held in a waypoint queue) → return 0. Human+0x333above 2 → avoidance off, return 0. - Between decisions: the counter
+0x28is read, stored back plus one, and the old value compared with 3 × human+0x333(with+0x333= 0 it decides every update). Below it: when avoiding, aim := held point and return 1, else return 0. - A decision: counter 0, score := −1e9. When avoiding the arrival radius counts as 0, and if s is a standing gait (gait 0, below 0.5 m/s) aim := held point and return 1.
- Within 1 m of the destination (squared distance ≤ 1) → return 0.
- Look-ahead. L = 0.75 × the move's speed (the distance covered in 0.75 s). While that speed is below 4 the
counter
+0x29counts up (stored plus one), and when the new value reaches 16, L is × 10 once and the counter goes to 0; at 4 or more it goes to 0.Humans_FindAhead(0x002274a8) lists up to 60 humans within min(2L, 10) m of P, in every direction, not counting me. - Expiry. When avoiding,
+0x2bcounts decisions. It is read, stored back plus one, and the old value compared: while it is below 4 × trunc(11 − s) (s my human's speed+0x1ac,Float_ToUInt), the walk heads for the held point instead of the aim; otherwiseSteering_Clearends the detour, which sets+0x2bback to 0. OnlySteering_Clear,Steering_ResetAvoidanceandSteering_Resetwrite 0 there.Steering_TryDetourdoes not, so a new detour taken while avoiding keeps the count of the old one. It counts only on decisions while avoiding. Confirmed (code) at0x0028935c-0x0028938c. - My step. D = the unit direction from P to that point, in plan (z = 0); S = D × min(L, its distance − the arrival radius).
- The blocker.
Steering_FindBlocker(0x00288cc8): of the listed humans, skipping my target (brain+0x124) and anyone behind (D · (B − P) < 0), the one whose 0.63 m disc (0x00289d68) a ray from P along the relative step (S minus his predicted step) hits first. His predicted step is his facing × 0.75 × his speed when his brain+0x04is 0 (inferred: a player), elseSteering_ClampStep(0.75)toward his aim (below). It returns him and t = the hit distance ÷ the relative step's length: the fraction of the 0.75 s step at which the two touch (1e9 for none; the ray test). No blocker → when avoiding aim := held point and return 1, else return 0. - When I lead a formation (human
+0x1a4) and he is one of its followers, his own steering gets the gait-0 speed for 5 updates (Steering_SetGait, so he stops); this call goes on. - Keep the old detour? When avoiding and the stored contact point
+0x10is nearer to P than the new one (|t × S|), aim := held point and return 1. Otherwise+0x10:= C = P + t × S, where I would meet him. - Vectors, all in plan and of unit length unless said: B his position; Q his predicted point, B + his facing
(
Quat_AxisY, 1 m) when his brain+0x04is 0, else his brain's aim point+0x90; N = (B − P) normalised, the bearing to him; H = (Q − B) normalised, his heading; E = D × my up axis (Quat_AxisZ), that is (Dy, −Dx): perpendicular to my move, 1 m long. Two dot products choose the case: b = D · N (how straight ahead he is) and h = D · H (how alike our headings are), against cos 30°, cos 50° and cos 45°. - Right of way: I have it when s is above his speed (human
+0x1ac), or equal and my human's address is the lower. - Standing (case 0): his brain
+0x04is 0 and his speed below 0.05; or his brain's move speed+0x114is ≤ 0; or his steering override is active (+0x32≠ 0) with a speed ≤ 0. -
The case otherwise, tested in this order:
b (bearing) h (headings) Case ≥ cos 30° < −cos 45° (coming at me) 1, head-on ≥ cos 30° > cos 45° (same way) 2, overtake, only with right of way; without it return 0 (follow) cos 50° to cos 30° |h| > cos 45° 2 ≥ cos 50° |h| ≤ cos 45° (crossing) 3, crossing < cos 50° ≥ cos 45° (same way, off to the side) none: without right of way the speed is 0.75 × s for 15 updates; return 0 < cos 50° < cos 45° 3 -
Same route node (cases 0-3): when both brains follow a route (the route state at brain
+0xe0:+0x04non-zero for both) and I am held (my route state's+0x16set, below), andRouteState_CurrentNode(0x0029b248) is the same node for both, the speed is 0.75 × his speed for 5 updates; return 1, aim unchanged.Route state
+0x16is the held flag ofRoute_Follow(0x0029aa88). At detail level below 2, each update claims the current waypoint (Nav_ClaimWaypoint). If it holds the claim and the next or the current leg is a jump or run-climb (link bits 4 or0x80), it also checks that the link is free (0x0029a3f0,0x0029a6c8). - 0 back (another human keeps the node, or is on the link):+0x16= 1. Within 2 m of the waypoint he stops there (brain+0x90the waypoint, speed 0,Brain_StopMove), and the follower returns 0. - Non-zero back:+0x16= 0. -RouteState_InitandRoute_Requestclear it, and at detail level 2 or more it is left as it was.So step 15 slows me behind a human at the node I am queued for. Confirmed (code) at
0x0029ab94-0x0029acfcand0x00289864-0x002898d4. That the link checks return 0 for "taken" is inferred from this use. 16. Yielding (cases 1-3): without right of way and with the contact within 3 m (|t × S|² < 9), cases 1 and 3 stop (speed 0 for 1 update) and case 2 matches his speed for 5 updates; return 1, aim unchanged. 17. The detour point X: - Case 0 (he stands): X = B + σE, σ = −1 when E · N ≥ 0, else +1: 1 m from his centre, perpendicular to my move, on the side of him that my line passes. aim := X andSteering_TryDetour(X); on success return 1. Otherwise k =Sectors_SectorOf(me, X): X's sector relative to my own facing. My human's vtable+0xacfills a transform (position+0x00, rotation+0x10), andHeading_RelativeTo(0x00386798) takes the heading from my position to X less my heading (Quat_Headingof my rotation), wrapped to 0-2π. ThenHuman_GetSectorOf(0x0029eb38) maps it to 0-7, sector k centred on k × 45°. k' = (k + 4) mod 8. X' = his position + 1 m along his live facing (his rotation in the position table,0x00714b10) + k' × 45° (Sectors_GetPoint0x0029e350, with his sector record, refreshed when older than 1,000 ms, naming him). So k is measured in my frame and applied in his. No stored heading is used. Confirmed (code) at0x002899a4-0x00289a24.TryDetour(X'), and on success aim := X'. Return 1 either way (the aim stays X when both fail). - Cases 1 and 2: X = C + σρE. ρ = +1 when B is on E's side of my line (Steering_SideSign, E · (B − P) ≥ 0), else −1; σ = +1 when my segment P → aim crosses his B → Q (Segment_Intersect2D,0x00337308), else −1. So 1 m sideways from where we would meet, away from his side, or towards it when our paths cross (he is moving over to the other side). - Case 3: X = B +Steering_ClampStep(0.75 t)+ σE, σ = −1 when H · E ≥ 0, else +1.Steering_ClampStep(f)(0x00288f40) is his unit direction to his aim (his brain+0x90) × min(his brain+0x114× f, his distance to it). So where he will be when we meet, then 1 m to the side he comes from (behind him). 18. Corner check (cases 1-3): when s minusMove_CornerSpeedLimit(s, brain, A, P, X)is more than 1, the speed is 0.75 × s for 1 update; return 1, no detour. Here s is my human's speed+0x1acand A my rotation's x axis (Quat_AxisX, the side axis: forward is local y).Move_CornerSpeedLimit(0x002fcd90), confirmed (code): 1. R =Math_CircleRadius2D(P, X, A)(0x003375d0): the radius of the circle through P and X whose centre lies from P along A, so the circle tangent to my facing at P. With d = X − P in plan, R = |d| / (2 |d̂ · A|). It is 0 when X = P or when d̂ · A = 0, which is X dead ahead or behind. 2. For a gait g, the speed that turn allows is v(g) = 2R · tan(ω(g) / 2) / (1/30). Here ω(g) is the turn per update (Human_GetTurnRateForGait,0x002212d0): 0.0436 rad (2.5°) at gait 5, 0.0698 (4°) at 4, 0.1047 (6°) at 3 and 0.2094 (12°) below, times the AI's turn boost (brain+0x0b: b + 1, or 1 / (1 − b) when negative). 1/30 is0x005102cc. So it is the speed whose per-update chord, turning at that rate, stays on the circle. 3. g₀ =Human_GaitForSpeed(s)(0x0022aeb0: 0 below 0.5 m/s, else the gait whose speed is nearest), and v = v(g₀), not capped by g₀'s speed. When v ≥ s: return s. 4. Otherwise step down. Keep best = v, go to g − 1, stop below gait 1, and compute v' = min(the gait's speedHuman_SpeedForGait, v(g)). Go on while v' ≥ best, taking v' as the new best; stop at the first gait that allows less. 5. Return min(s, best). It never returns 0 as "none fit": the result is the best of the tried gaits, and 0 only when R = 0 (or s ≤ 0).So the check fires when the detour bends harder than my speed allows by more than 1 m/s. 19. Otherwise
Steering_TryDetour(0x00289010): when the human can walk straight to the point (Human_CanWalkStraightTo) andGround_ProbeBelowdoes not return 1.Ground_ProbeBelow(0x00249050) casts a ray from 0.1 m above the point straight down for 2 m, with mask 0, and returns 1 only when it hits a triangle with flag bit 4 (0x10, the result's+0x2c), the bit the ground snap passes on as "under cover" (Collision: triangles). A point with no ground within 2.1 m, or on any other surface first, passes. So it rules out detour points on those 1,052 marked triangles, not drops; what bit 4 marks is inferred. When both tests pass, avoiding := the blocker, score := t and held point := the point; then aim := X. Return 1 either way. Note: in cases 1-3 the point checked and held is the aim the move passed in, not X (a2is the aim at0x00289cfc), so X steers only the deciding update and the human then heads for the held aim until the next decision; case 0 checks and holds its own point. Inferred to be a slip in the original; a faithful port keeps it.
The score +0x3c is t, the fraction of the 0.75 s step at which the two would touch (smaller is sooner). It is
−1e9 at every decision and after Steering_Clear, t once a detour is taken, and 0 after Steering_EndAvoid. Nothing
in this function or its helpers reads it: a new detour is weighed against the held one by the contact points'
distances (step 10). Who reads it is not traced.
The return value: 0 = no steering, the move keeps its own aim; 1 = steering, and the aim may have changed. On 1
the move action recomputes its direction to the aim and uses an arrival radius of 0.3 m (brain +0x118) for
this update instead of its own; either way the aim goes to brain +0x90 (0x002fc9a0-0x002fca98).
Brain_GiveWayTo (0x00289ed0) and Brain_PushAside (0x0028a248) are the standing side of it: an AI standing in
a mover's way steps into a free 45° sector round it (Giving way).
Giving way¶
A standing AI in a mover's way steps aside one sector (45°) with a GiveWay action. Confirmed (code) at
Brain_GiveWayTo (0x00289ed0) unless marked. The step itself, its clips and how it ends are on the Human side:
Characters: Step control.
Entry. Brain_PushAside(mover, stander, point, step) (0x0028a248) is the only way in. The mover's path is the
line through point along step. Its seven callers, confirmed (code) at the call addresses:
| Caller (call) | When | mover, stander | point, step |
|---|---|---|---|
Route_IsJumpLegClear (0x0029a3f0, call 0x0029a66c) |
a route follower about to take a jump leg (link bit 4) | the follower, the first blocker | his position, unit(waypoint i − waypoint i − 1) |
Route_IsClimbLegClear (0x0029a6c8, call 0x0029a86c) |
a route follower about to take a climb leg (link bit 0x80) |
the follower, the first blocker | his position, unit(waypoint i − his position) |
Gang_ClearWayForLeader (0x002942c8, call 0x002945bc) |
the Warriors' follow tactic, on its timer | the gang leader, each member in his way | the leader's position, his forward |
WarriorBrain_Think (0x003052f0, call 0x00306004) |
a Warrior getting out of the way, every 12 thinks | the Warrior, the nearest gang mate | the Warrior's position, his forward |
SaveHumanGoal_Process (0x002b8ad8, call 0x002b9368) |
a rescuer within 1 m of the downed human, touching someone | the rescuer, the human he touches | the goal point, unit(goal point − rescuer) |
Brain_GiveWayTo (0x00289ed0, call 0x0028a100) |
no free sector (below) | the stander, a neighbour | the stander's position, the original step |
Action_GiveWay (0x002fe4b0, call 0x002fe544) |
the script's ActGiveWay, for a stander with fewer than 8 queued actions |
the other human, the stander | the other's position, his forward |
The answer (yes or no) is used only by Brain_GiveWayTo's own recursion; every other caller ignores it.
The two route legs (Route_Follow 0x0029aa88). For a follower with detail level +0x333 < 2 whose claim on the
current waypoint i holds (Nav_ClaimWaypoint), the leg out of i (to i + 1) is tested first, then the leg into
i (from i − 1), each only when its link has bit 4 (Route_IsJumpLegClear(route, leg end, 1)) or bit 0x80
(Route_IsClimbLegClear(route, leg end, 1)). A 0 (blocked, or a refused claim) marks the route +0x16; within 2 m of
waypoint i the follower then holds there at speed 0 (Brain_StopMove) for this update. Both tests return 1 when clear:
- the jump leg looks for humans within 1 m in plan of the segment from waypoint n − 1 to n and within 2 m in
height (
Humans_FindNearSegment(1.0, 2.0, …),0x002278a8); of those, only one within 4 m in plan of the follower and ahead of him (dot with his forward ≥ 0) counts; - the climb leg looks for humans within 0.5 m (3D) of waypoint n (
Humans_FindAhead(0.5, …)), with no distance or ahead test.
The first found who is friendly (Brain_IsFriendly 0x00290230), not cuffed and not knocked out decides: his front
action a MoveTo (type 1) with a speed +0x114 above 0 → blocked, no push (he is moving anyway); otherwise, with the
third argument 1 (always, from Route_Follow) → Brain_PushAside as in the table, and blocked. Others are skipped.
A pushed blocker steps aside on a later update, so the follower waits at least one update.
The leader's way (Gang_ClearWayForLeader(angle, gang)). Its only caller, WarriorFollowTactic_Process
(0x00311638), passes 40° on its timer: due every 1000 ms (250 ms while the leader holds an object), and 5 ms after
a call that pushed someone. Nothing happens unless the gang has a leader whose stored gait +0x1a8 is below 3
(walking, not jogging). Each of the gang's 16 member slots (gang +0x48), other than the leader, is pushed when:
- his top goal is not GetItem (
0x2c), DestroyItem (0x2d) or Tag (0x5a); - he is within 6 m (3D) of the leader and inside the leader's 40° cone (the unit direction to him dotted with the leader's forward ≥ cos 20°), or within 1 m in any direction;
- he has no script handler (
Human_HasScriptHandler0x0021d570: a handler at+0xe8or the prompt flag+0x1b2), his brain's+0x2e5(sparring) is clear, and he is not the leader's target (leader brain+0x124); - his action queue is empty, or its front action is neither a
MoveTo(1) nor aGiveWay(12).
Then his actions are cleared (Brain_ClearActions, result ignored) and Brain_PushAside(leader, him, leader's
position, leader's forward) runs; the function returns 1 when anyone was pushed.
A Warrior making room (WarriorBrain_Think, step 9 under The Warriors). On the thinks where
step 9 runs, after the blocked-ray turn (whether or not that turned him): the nearest member of his gang
(HandleArray16_Nearest on gang +0x48, excluding himself) is pushed when he is not busy, his actions are not
blocked (HumanRecord_AreActionsBlocked), he is within 1 m, he has neither a Scatter (0x37) nor a HoldPosition
(0x36) goal and his action queue is empty. Mover: the Warrior; point: the Warrior's position; step: his forward.
A rescuer's way (SaveHumanGoal_Process, state 1: within 1.1 × the near range +0x140 of the goal point). In the
fight stance with no queued action: farther than 0.75 m from the goal point, an enemy (Brain_PickBestEnemy) is fought
off first; else, farther than 1 m, he moves closer (gait 2, turn boost + 1). Within 1 m,
Human_CapsuleOverlaps(rescuer, downed) names what his capsule touches (inferred: not the downed human).
A human who is not a threat (Brain_IsThreat) has his actions cleared; when that
works, Brain_PushAside(rescuer, him, goal point, unit(goal point − rescuer)) and the rescuer queues a Nothing
action of 1500 ms. When the clear is refused, the rescuer turns to face him and plays anim action 0x21. A threat
becomes the target and is fought off. A world object in the way is handled by the object branch (not a push).
Brain_PushAside checks the stander's brain flags +0xcc, then calls Brain_GiveWayTo:
- bit 1 set: the stander is already giving way, and the answer is yes;
- bit 2 or bit 4 set: the answer is no;
- otherwise it sets bit 4 on both brains for the call, so the recursion below cannot loop, and clears it after.
The tests, all needed, else no:
- The stander's brain type
+0x04is not 0 (not a player's brain), or its "dead" byte+0x09is set. Confirmed (code) at0x00289f1c-0x00289f2c. - Neither is a threat to the other.
Human_IsThreatTo(0x00222a48) must be false both ways. It isBrain_IsThreaton the two brains (Characters: Step control gives the exact test). - The stander is idle under its control.
Human_IsIdleUnderControl(0x00225390) needs these: - the control is
Human_UpdateControlor the step control; - no state bit of
0x7bf9e1f3ff0; - no held flag.
Which way. All angles are relative to the stander's facing f (Quat_Heading of its transform-table rotation)
and wrapped to 0-2π. Human_GetSectorOf (0x0029eb38) gives the sector, 0-7, each 45° wide and centred on k × 45°,
rising clockwise, toward his right (Sector numbering):
- The mover's path is the line through
pointandpoint + step.Line_ClosestPoint(0x00336e08) gives the pointcon that infinite line nearest the stander:a + d · dot(p − a, d), withdthe unit direction. sis the sector of the heading fromcto the stander: straight away from the path.- The sector of the mover's own heading is also computed, and
Sectors_TurnWay(the shorter way round) is called with both. Its result is unused (confirmed (code): the return value is dropped). - The stander's sector record is refreshed when it is older than 500 ms (
Brain_GetSectors(brain, 500), AI code: Neighbour sectors). - Sectors are tried in the order s, s+1, s+2, s−1, s+3, s+4, s−2, modulo 8. That is seven of the eight; s−3 is
never tried. The first free sector wins.
Sectors_IsFree(0x0029ea48) needs no near human (flags0xbclear) and no wall: the sector is probed on first use and bit0x4must stay clear.
A free sector. All queued actions are popped (Brain_ClearActions; when one refuses, the answer is still yes but
nothing is queued). Then a GiveWay action is allocated:
- heading f + sector × 45°;
- the mover as the human to look at;
- a start delay of
Random_Int(125)= 0-124 ms; - the boost flag: set when the mover is a pad player (
+0x1b0≠ −1) whose stored gait+0x1a8is 3 or more, so the stander dashes away from a jogging or running player, and steps for anything slower.
GiveWayAction_Init (0x002fe568) sets brain +0xcc bit 1 while it lives. A distance from the mover to the point
is computed and not used.
No free sector. For the same seven sectors, in order, the human in each sector (when any) is asked to make room:
Brain_PushAside(stander, that human, the stander's own position, step). The first yes ends the search with yes.
So a crowd can ripple outwards, at most one level deep per call because of bit 4. With no yes, the answer is no.
Waypoint queues¶
20 records of 0x90 at 0x006cde30 (mask 0x006ce970), updated after the brains (0x00299778); inferred: queues at
choke edges. 0x00299538 lays up to 6 points one metre apart along the edge (+0x00, 16 bytes each; count +0x88)
between its endpoints +0x60 / +0x64, with 6 humans at +0x68 and their "settled" flags at +0x80. Each update,
humans whose brain +0xec is 1 or 2 keep their point and the others are reassigned (0x00299a08 when there are more
humans than points, else 0x00299b50), then up to 5 passes swap pairs whose paths cross. Confirmed (code).