AI gangs, tactics and formations¶
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 brain a gang member runs is on AI humans; the
tactic and goal classes are indexed on AI code index and AI goals.
Purpose¶
A gang is the group a human fights with: the Warriors around the player, each enemy gang of a level, the police and a riot's crowd. The gang decides who its members fight and how (its tactic and formation), makes new members through its spawners while the level allows it, answers the scripts' gang messages, and for the Warriors carries the player's commands and the follow that keeps the crew beside him. The AI brains (AI humans), the crimes and police (Crimes) and every mission's encounters depend on it.
Original structure¶
The gang module is 0x00162518-0x0016e388; its function-by-function index is on
AI code index: the gang module, and the tactic and goal classes are on
AI code index and AI goals. The functions this page's behaviour starts from:
| Address | Name | Role | Evidence |
|---|---|---|---|
0x0016d170 |
Gangs_Update |
Each update: every gang's update, its spawners asked once per gang | confirmed (code) |
0x00166708 |
Gang_CachePlayerInfo |
The players' state for the gang, and whether its spawners may run (the cap) | confirmed (code) |
0x001681a0 |
GangSpawner_Update |
One spawner's update and spawn | confirmed (code) |
0x001672a8 |
GangSpawner_OnHumanGone |
A spawned human gone: the counts taken off | confirmed (code) |
0x001664d8 |
Gang_RemoveMember |
A member leaves the gang | confirmed (code) |
Gang fights¶
How a gang shares out a big fight, team-ups and Rumble's CPU sides: Gang fights.
Data: the gang record¶
32 records of 0xb10 at 0x005e6e30 (0x0016c388(id)). Confirmed (code):
| Offset | Type | Meaning |
|---|---|---|
+0x00 / +0x14 |
vec4 / float | centre and radius of the members' bounds (every 5th update, 0x00166b60) |
+0x18 |
int | in use |
+0x2c |
int | gang kind (1, 5, 0x17, 0x18 special-cased) |
+0x30 |
s16 | id, also its bit in other gangs' masks |
+0x32 |
s16 | alert state (0x00164a28): 1 fighting, 0 calm |
+0x38 / +0x3c |
u32 | enemy / friend masks, a bit per gang id |
+0x40 |
ptr | the current tactic, 0 for none |
+0x44 |
handle | the leader (tactics) |
+0x48 |
handle[16] | members |
+0xd4 |
u8 | suspended: its members' brains skip their update (One update) |
+0xe4 |
u32[] | the Lua handler per event id (interned name) |
+0x1b0 |
table | gang-wide anim substitutions (0x00164178, 0x001642f0) |
GangMakeEnemies(a, b)(0x0016acf0), both ways: clear the friend bit, set the enemy bit (0x001690c8).GangMakeFriends(0x0016ad80) does the reverse. Id −1 is ignored.- Friends (
0x00168f58): the same gang or kind, both kinds in {1,0x17}, a global truce (0x0050cb7cin the future), or the friend bit. GangSetThreatResponse(gang, r)(0x0016b3d0) writes brain+0x21cof the current members only.- Alert state (
0x00164a28), on a change: 1 installs the fight anim substitutions (0x00169508), 0 removes them; every AI member's brain+0x14d= 0 when fighting, 1 when calm. With no tactic,0x00166cf8returns a fighting gang to calm once no member has a fight goal (8 or0x3f) or an active goal0xc.
Behaviour¶
The sections from here to Formations and follow slots describe what a gang does, each naming the original function where its logic lives.
Events and GangSetMsgHandler¶
Human_OnEvent (0x0021d4e8) offers an event, until one consumes it, to the human's own script handlers
(0x00384c38), then Gang_OnEvent (0x00164c20, the brain's gang +0x20c), then Brain_OnEvent
(Events). Confirmed (code).
GangSetMsgHandler(gang, msg, name)(0x0016ab38→0x00164bb8) stores the interned name at+0xe4 + msg × 4.Gang_OnEventcalls that handler only while scripting runs (0x00512b28= 1), through script-system+0x8cwith three values. Event 18 (a human died or was knocked out, Script events) callshandler(member, other, standing), whereotheris the attacker (the null handle when none) andstandingthe members not dead or knocked out (0x00166220); event0x11passes the event's+4, event 2 the headcount; any other id goes through the generic marshaller (0x00384ce0) and is consumed when the Lua function returns true.- Then, when the gang has a tactic, the event goes to the tactic's event slot and its result is returned. With no
members (
0x004de0c8on+0x48) only event0x16with+0x08= 0 (a member left) goes on. A tactic not yet started (+0x04= 0, the flagTactic_Process0x003067d8tests) is first run through the gang's update,0x00166708(gang, 0)(0x00165144-0x00165158), whoseTactic_Processstarts it. So when a script sets a tactic and adds members in the same step, the start's goals are given before the event's: the follow tactic's 22 with 1 then pops them to the base (WarriorFollowTactic_GiveGoalscallsBrain_PopToGoalBaseandBrain_MarkGoalBase) and gives them once again, not twice. Confirmed (code).
The gang update¶
Gangs_Update (0x0016d170, step 4 of Humans_Update), for each gang in use that has
members, confirmed (code):
0x00166708(gang, 1): the player's position and heading cached (1 s) and the time checks.- With a tactic:
Tactic_Process(0x003067d8); a non-zero result fires the tactic's Lua callback (vtable+0x54). - One gang in five per update (a counter
0x0050cbec, 0-4): the bounds, the return to calm, and the neutral rule (0x00169d30: a gang of kind0x17/0x18whose members all have threat response 0 is made neutral with the kind-0 gangs). 0x00306630frees the tactics queued for freeing (up to 32,0x006eb290).- A gang with flag
+0xd3and no living members calls the handler at+0x158with its id and is freed (0x0016a1e8).
Deleting a gang¶
GangDelete(id) (0x00373200 → 0x0016a1e8; ids of 32 and above do nothing) → 0x0016d068 → 0x00164738,
confirmed (code):
- Every gang in use clears this gang's bits from its enemy and friend masks.
- The gang's Lua message handlers are cleared, so the members' removal below calls no script handler.
- Each member still in the gang is destroyed, at once: taken out of the member list (
0x001664d8: clears the leader+0x44when it was the leader, offers event0x16to the gang's tactic), thenHuman_Destroy(h, 0)(Characters: destroying a human). Nothing is deferred: the humans, their held objects (destroyed, not dropped), radar blips, HUD panel and pad (for a player) are gone whenGangDeletereturns. - The tactic, if any, is stopped and freed.
- The record is reset (
0x00164808): not in use, not suspended, members, spawners, timers and handlers cleared, leader the null handle.
A player is no exception. A listed player in the gang is destroyed like the others: its pad is freed and it is
removed from the player list (the other player, if any, becomes player 1); no other human is made the player. Scripts
move the player first with HuChangePlayerGang (Characters: players); level 99 does so at
checkpoint 2 (the hand-over).
GangBrFlush(id) (0x0016ba18) only clears the goals and actions of each current member's brain; it removes and
destroys nothing. Called just before GangDelete in the same script step, as in level 99, it has no visible effect.
Tactics¶
A pool of 30 tactics of 0x90 at 0x006ea1b0 (mask 0x006ea1a0; alloc 0x00306558, free 0x003065b0). Base fields:
+0x00 gang, +0x04 started, +0x08 time limit (−1 none; Process returns 2 once past it), +0x0c Lua callback,
+0x1c vtable ({s16 delta, fn} pairs: +0x0c Start, +0x14 End, +0x24 Process, +0x34 type id, +0x3c a
flag, true for types below 0x12, +0x4c event, +0x54 fire the callback). Confirmed (code). The +0x3c flag has
two readers: Start puts the gang on alert when it is 1, and Brain_AddEnemy sends event 0xb when it is 0 (below).
It does not gate any goal handling; the old reading "members keep their own goals" was wrong.
- Setting one (
Gang_SetTactic,0x00165640) ends and frees the old tactic and stores the new one. It does not start it.Tactic_Start(0x00306690) has one caller,Tactic_Process(0x003067d8), on the gang's first update after the binding. Every binding a script step makes before that update, such as aGoalTagright after theTactic*call, has already run. Start does this:- Makes the time limit absolute.
- Sets the gang's alert state to the
+0x3cflag: 1 (fighting) when it is 1, else 0 (calm), so a tactic of type0x12or above calms its gang (Alert state). Types0x16-0x18and0x21also set gang+0xd1= 0 and+0xd2= 1. - For every member that is not a player (
+0x1b0= −1):Brain_MarkGoalBase(0x0028d8a0) andHuman_DropTarget(0x00226f70, clears the target). Nothing is popped. - Runs the class's Start, then marks the tactic as started.
Tactic_Process returns 2 past the time limit, else the class's Process.
- The goal base (brain +0x2d) is how a tactic keeps the goals a member had. Confirmed (code):
- Brain_MarkGoalBase stores the current top index (+0x2c) as the base, only when the stack is not empty. On
an empty stack the base stays as it was.
- Brain_PopToGoalBase (0x0028d8c0) pops while top > base, then sets the base to −1. A base of −1 pops
everything.
- Brain_ClearGoals (0x0028d910, the script's flush) pops everything.
So a member's goals at Start, including any the script pushed in the same step, stay under the base. A
class's Start that gives goals (for example DefendTactic_GiveGoals) calls PopToGoalBase and then
MarkGoalBase before each push. That pops only what is above the mark, nothing at Start, and pushes the tactic's
goal on top. When it ends, the old goals are back on top. Where AI code says a tactic helper
"flushes" a member, it means this pop-to-base and mark, not Brain_ClearGoals. Confirmed (code, by their callees)
for Defend, Info, ManWeaponPile, Pursue, WarriorScatter, Scout, StandGround, Steal, Taunt and Vandalize. Coney
must not clear the stack at a tactic's start.
Tactic_End (0x00306850, from Gang_StopTactic, Gang_Destroy and the old boss tactic) pops every
non-player member down to his base. That removes the tactic's goals and anything pushed onto him after Start,
and never the goals he had at Start. A member whose stack was empty at Start keeps base −1, so End empties his
stack.
Example (level3, checkpoint 2): the script calls TacticDefend(gang, leader, 2.25) and then GoalTag(leader,
...) in the same step. The TagGoal is pushed first, because Defend's Start waits for the gang's update. Start then
marks it under the leader's base and pops nothing. DefendTactic_GiveGoals (0x00310288) skips the defended
human, so the leader keeps tagging. A member who is down, knocked out or has no AI brain (brain +0x04 = 0) is
also skipped. The other members get FollowAndDefend on top of their own goals. Events 19, and 22 with +0x08 = 1,
run GiveGoals again. Each run pops only the goals above the base and pushes a new FollowAndDefend.
- Firing the callback (0x00306938) calls the Lua function at once with two values: the gang's id (+0x30) and
the code. TacticClear ends and frees it.
- Fights under a tactic: Brain_PushFightGoal (0x0028d190) returns at once when the gang has a tactic, so
GoalFight picks the target but pushes no fight goal; the tactic fights. It also does nothing for a knocked-down
or dead human, or a type-3 brain already holding goal 9; otherwise it pops goals 8 and 0x41, pushes FindEnemy
(0x41), Melee (8) and then the fight goal (0xf) (Starting a fight).
- Brain_AddEnemy (0x0028d538) adds the enemy to +0x164 (16), makes the enemy's brain add this human back
(0x0028ef20, unless both are players), and, when the tactic's +0x3c answers 0, sends it event 0xb with the
enemy.
TacticCrowd (type 0x1b, vtable 0x005437a0; 0x00316450): turns a gang into onlookers. Init (0x0030f4d0):
no time limit, +0x28 the option.
- Start (
0x0030f6b0): each living member is flushed and given one goal: with the option, an idle-in-place goal (0x002caf78); without, a timed goal (0x002b4098, 4-6 s). With the option (cheering), the anims0x256, 599 and 668 are substituted by cheers; without (watching),0x25c. - Process (
0x0030fe78, always 0): every 1-2 s, a watcher with record+0x08clear plays0x25cat 51 %; a cheering crowd sends, every 2 s, the next member in turn into one of three cheer idles (state0x20000000). - Event (
0x00310100):0x10(a member warned of an attack) fires the callback with event 6 and is not used;0x14(violence nearby, its strength at+8, cheering crowds only, above 29) makes idle members look at the fight for 20 s and, at min(strength, 100) %, queue a reaction, and is used (0x0030fa18);0x13and0x16re-seat the crowd. - The reaction (
0x0030fc48(what, on)), run when the periodic switch+0x2ais on orwhatis non-zero: each free member with no actions queues two actions, a speech action (PlaySound,0x002fb868, vtable0x00542d20) saying command0x8f(0x10at rand100 < 50 whenwhatis 0;0xb1whenwhatis 1 andonis 0) after 0-750 ms, then a clip action (0x002fa300, vtable0x00542be0) with the cheer0x256in one of three variants. A cheering crowd's Process calls it with (0, 1) on each tick. TacticTrigger(gang, what, on)(0x00316fa0), crowd tactics only: what 0 sets the periodic switch toon; what 1 runs the reaction at once (0x0030fe58).
The scripted tactics¶
Every Tactic<Name> binding finds the gang (0x0016c388), takes a tactic from the pool, runs the class's
constructor (base fields, the callback name interned by 0x003068f8, then its own fields) and sets it (0x00165640),
so the class's Start runs on the gang's next update. A tactic fires its callback with a code, the event names of
TacticGetString (0 TacRunning ... 18 TacAnimStart), in two
ways: a non-zero Process result (each update; codes such as 7 TacInRange are returned on every update the
condition holds, so the callback runs repeatedly), and calls from the event slot. Confirmed (code) for each class
below unless marked.
Shared behaviour, confirmed (code):
- Leader: gang
+0x44holds the leader's handle;0x00165678returns him while he is alive, not a player, not down (0x00223b70) and not out of the fight (0x00227e60), else another member (0x00165738). - Moving as a group (MoveToFlag, TravelPath, WalkinTall, Wander, Confront): the leader is popped to his goal base
and re-marked (
0x0028d8c0,0x0028d8a0), and the moving goal is pushed above his old goals; every other member that is not a player and not down joins the leader's formation (0x00295f28) and getsGoal_FollowPlayer(distance, member, leader, mode). The slot set: the script'sslotSet, or, when it is −1, set 3 with min(members − 1, 9) slots (0x00295db0,0x00295dd8,0x00296488); the leader's set before is remembered. - Event mapping (the human events of Script events, offered to the gang's tactic
by
Gang_OnEvent): 1 damage → code 5TacDamage; 10 saw the player → 4TacSeePlayer; 11 a member spotted someone → the member's brain notes it (0x0028c0c8) and 3TacSeeEnemy; 16 attacked → 6TacAttacked; 8 arrived at a flag → 8TacArrived(MoveToFlag, WalkinTall). Event 20 (violence nearby) is consumed (result 1); events 2, 17, 18, 19 and 22 for one of the gang's own members make the tactic re-issue its members' goals. Each class handles only some of these (below). - Banter (HanginOut, MoveToFlag, TravelPath, Wander, Idle with
bantertrue): every 3 s, unless commands are locked (game state+0x411), a scene plays (+0x410) or fewer than two members live, two idle members are picked at 51 % each; the first says speech command 20statement(0x002208f0), and when his line ends the second says 21response; then the next pair after 3 s (Speech). - Answering violence (HanginOut, Idle with
respondtrue): on event 20 against a gang member, while the share of members already in goal0x1dHelpRespond is below the gang's percentage (+0xd7,GangSetRespondPercentage), one more free member is sent to fight the attacker (0x002b75b8). - The spot line:
0x00165d40(gang, command)makes one non-player member say a speech command once (gang+0xd2armed, mode0x56f8= 1) when he sees a hostile within his sight range; 22spotby default.
| Binding | Type | Vtable | Members get | Process codes | Events |
|---|---|---|---|---|---|
TacticAttack |
0x00 |
0x00543320 |
melee (Goal_Melee), threat response 2 |
9 when no member has an enemy (checked each 1 s) | 1/11: an idle own member melees; 2 → 13 TacMemberDied; 20: idle members attack the offender |
TacticDefend |
0x02 |
0x00543800 |
FollowAndDefend (53) round the human, pushed on top of each member's goals; the defended human gets nothing and keeps his own; dogs (type 221) AvoidEnemies (32) |
11 when the human is gone or dead; 9 when no member has an enemy (1.5 s) | 2 on the human → 11; 17/18 on the human: members rush to him; 20 near him: his attacker's gang becomes an enemy |
TacticHoldTheLine |
0x04 |
0x00543a40 |
HTLDefense (100) spaced along the line, the rest HTLOffense (102) at flag3 |
9 no enemies; 12 an enemy crossed (1.75 s); 14 after hits hits within window s |
2 on a defender: an attacker takes his spot, 13 |
TacticManWeaponPile |
0x06 |
0x00543b60 |
ManWeaponPile (82) |
none | 1 → 5; 2 → 13; 16 → 6 |
TacticPursue |
0x14 |
0x00543c20 |
Chase (12) after the target gang's leader |
9 target gone or search over; 7 a member sees a target in range | 1 → 5 (not for a player's hit); 16 → 6 |
TacticWalkinTall |
0x15 |
0x005440a0 |
leader MoveToFlag at walk, others follow at 0.75 m |
7 when the nearest enemy gang is within range (1 s) |
1, 8, 16 |
TacticWander |
0x16 |
0x00544100 |
leader Wander (58), others follow at 4 m |
none | 1, 10, 11, 16 |
TacticTravelPath |
0x17 |
0x00543f80 |
leader TravelPath (56), others follow at 1 m |
none | 1, 10, 11, 16 |
TacticHanginOut |
0x18 |
0x00543920 |
HangOut (59) at the flag |
none | 1, 10, 11, 16, 20 |
TacticMoveToFlag |
0x19 |
0x00543bc0 |
leader MoveToFlag, others follow at 3 m |
none | 1, 8, 10, 11, 16 |
TacticVandalize |
0x1c |
0x00544040 |
Destroy (91) |
1 when the zone has nothing left (3 s) | 1, 10, 11, 16; 23 a zone object broken → 16 TacObjectDestroyed |
TacticSteal |
0x1d |
0x00543ec0 |
Steal (92) |
none | 1, 10, 11, 16 |
TacticAvoidEnemies |
0x20 |
0x00543380 |
AvoidEnemies (32) |
none | 1 → 5; 16 → 6 |
TacticUseFlag |
0x21 |
0x00543fe0 |
MoveToUseFlag (4) |
7 once the player came within range and every member left the flag |
1/11/16 alert (no callback) |
TacticConfront |
0x23 |
0x00543740 |
Confront (60) in formation |
9, 1, 7, 2 (below) | 1 → 5; 16 → 6 |
TacticIdle |
0x24 |
0x00543aa0 |
Idle (0) where they stand |
15 TacAnimDone once broken off |
1, 10, 11, 16, 20 |
TacticScout |
0x27 |
0x00543da0 |
Scout (111) or PathScout (112) |
none | 1/11/16: the member fights and calls the gang |
Class details, confirmed (code) unless marked:
- Attack (constructor
0x003075c8): Start (0x00307fe0) gives each member not in aPedReactiongoal threat response 2 (brain+0x21c) and a melee goal, and has the gang sayspot. Process (0x003081a8): every 3 s, members with no goal melee and go toward the farthest busy one; every 7 s, a team-up (Gang fights). - Confront (
0x0030e670): withtargetGang−1 the first member's brain target gang (+0x264) is taken, and without one nothing starts. Start (0x0030ec20) puts the posture anims in the gang's substitution table for anim0x253(four defaults from0x005113a8when none is given; the fifth marks a last, separate anim) and says the spot line (spotLine135shadowswitches it to 136shadow_spot). Process (0x0030eec0), every 250 ms between the two gangs' bounds: no route between the leaders (0x0024e078) → 1 when the other leader is in view, else 9; insideapproachRange+ both radii → 7, and 1 once insidecriticalRange; leaving the approach range again → 2. Every 500 ms, members facing the other leader (within 0.99 of his heading) play a random posture anim (0x0025a3e0). - HanginOut (
0x00311ad8): Start (0x00312348) withoutfullAwarenarrows each member's view (brain+0x12c− 20°) and sight range (+0x130× 0.75), gives each aHangOutgoal round the flag (range,harass) and substitutes anim0x25bwith eight idles (0x00511480). - Idle (
0x003149f0):clearAnimsis passed to each member'sIdlegoal. WithdynIdleset, events 1, 11 and 16 do not fire the callback but end every member'sPlayDynIdlegoal (0x00314fb8), and Process returns 15 once none is left. - TravelPath (
0x0031cbc0): the leader'sTravelPathgoal (0x002e0748) takes the path, mode 1 whenloopis true else 2,reverse,gait,startPointanddelay× 1000 ms. With no path the leader gets aTravelFlagNetgoal (70) from where he stands. Every few seconds a free member within 5 m of the leader may use a usable flag within 5 m (MoveToUseFlag, inferred: props on the way). - UseFlag (
0x0031d878): each second members beyondrangeof the flag walk to it (MoveToFlag, gait 3) and those near it use it (MoveToUseFlag) with sight rangeview; once the nearest player is withinrangeof the flag the tactic stops re-seating, gives each member'sMoveToUseFlaggoal a random 0-1 s delay (0x002dbae8) and returns 7 once every such goal reached state 3 (inferred: they have left the flag). - HoldTheLine (
0x00313f38):flag1-flag2is the line andflag3the side the attackers wait on. The defenders are min(line length in metres, 60 % of the members), spaced evenly; the others stand nearflag3. Code 12 fires when an enemy atflag1's height (±0.5 m) is past the line and farther thandistancefromflag3; 14 when members tookhitshits (event 16 with+4= 1, inferred: thrown objects) withinwindowseconds. - Pursue (
0x00317c40): the members'Chasegoals (0x002b04f0) get the two angles, the gait and the target leader. Process every 150 ms: 9 when the target gang is gone, empty or has no leader, or no route exists and the gangs are not in contact; 7 when a member (with LOS,0x002223e8) is withinrangeof a target, who is added as his enemy. Every 100 ms it reads the members'Chasegoals (0x00318020; whether they still see the target, inferred) and arms the spot line from them; once the search time (searchMs, set when the target is lost) has passed with no chase still running, the tactic ends the search (0x00318130) and returns 9. - Scout (
0x0031a430): Start (0x0031af98) gives each member a scout goal and substitutes anim0x29c. Process (0x0031b030), every 200 ms: members with enemies melee, and the gang's alert state is set when any is fighting. A hit, a sighting or an attack (0x0031a818) makes that member melee and, when the crime rules allow, call his gang (Goal_CallGang, radiusrangeor twice the member's brain+0x140). - Vandalize / Steal: each member's goal takes the zone, the delay and
leaderRange; the vandal brain's+0x28dis set. Vandalize's code 1 comes from0x0039a580(zone)reporting nothing left to break.
Attacking on sight¶
Why an enemy-gang member attacks the player he sees, and what holds him back. Confirmed (code) at the addresses
cited; the level87 facts are from its scripts on the disc.
The chain.
- The scan. Every AI brain, in
Brain_UpdateGoals, runsBrain_MaybeScanEnemies(0x0028fa18): every 2 s near the camera, every 8 s in the fight stance or farther than 30 m from it, at most 5 scans in one update. It lists the members of enemy gangs within the sight range (brain+0x130, 30 m when made;HuSetLOSRange), in front (field of view+0x12c, 110° when made;BrSetFOV) unless within the near radius, and in line of sight (The enemy scan, Sight). The gangs must be enemies first:GangMakeEnemies, or a script form'sEnemies. - A new enemy goes to the scanner's own event handler as event
0xb(11): his script handlers, thenGang_OnEvent, which hands it to the gang's tactic, then his brain (Events and GangSetMsgHandler).Brain_AddEnemysends the same event when the tactic's+0x3cflag is 0 (types0x12and up). - A gang with no tactic: the gang soldier's think (
GangBrain_Thinkstep 3, Gang soldiers) pushes the fight (Brain_PushFightGoal(−1)) as soon as his threat response (+0x21c) is not 0 and his enemy list is not empty. That is the plain "attack on sight". The shared handler's event0xbalso sends a 20 m help call flagged "new enemy" (The shared event handler). - A gang with a tactic:
Brain_PushFightGoalreturns at once, so the think starts no fight (Tactics). The tactic turns the event into a callback code instead: HanginOut, Idle, Wander, TravelPath, MoveToFlag, Vandalize and Steal turn event 11 from one of their own members into code 3TacSeeEnemy(HanginOutTactic_OnEvent0x00312580,IdleTactic_OnEvent0x00315228), event 1 into 5, 16 into 6 and event 10 (Brain_MaybeSpotPlayers) into 4TacSeePlayer. Confront answers its own process codes (1 inside the critical range, or no route and the other leader in view; 7 inside the approach range; 2 leaving it; 9 the target gone) and events 1 → 5, 16 → 6 (The scripted tactics). - The script decides. The callback is the Lua
Strategy(gang, code)of the strategy library (global.lua), which calls the gang'sStrategyPreset(gang, code, state). Each preset is a table from the current tactic and the code to the next tactic; a code the table does not name does nothing. So under a tactic, whether a member attacks on sight is the script's choice, made through code 3.
A HanginOut tactic without fullAware narrows each member's sight to the leader's range × 0.75 and his field of view
− 20° (HanginOutTactic_SetAwareness 0x00311c78), so a hanging-out Destroyer spots the player from 22.5 m at most.
level87 (Mission 3: Payback). GangSetup(form) (global.lua) makes every Destroyer gang (kind 19) an enemy
of GangWarriors (the form's Enemies), keeps the threat response of 2 (no form sets SetThreat) and starts the
form's DefaultTactic (HANGINGOUT when none, nothing when Halt is set) with the callback "Strategy". The
presets' rows that matter (code → next tactic; 5 and 6, a hit or an attack warning, give ATTACK in every row):
| Preset | Row (tactic) | 3 TacSeeEnemy |
Other codes |
|---|---|---|---|
StratHunter (the default) |
HANGINGOUT, MOVETOFLAG | CONFRONT | 4 nothing; 8 → HANGINGOUT |
StratHunter |
IDLE | ATTACK | 4 nothing |
StratHunter |
CONFRONT | - | 1 → ATTACK; 2 → PURSUE; 7 nothing; 9 → HANGINGOUT |
StratHunter |
PURSUE / ATTACK | - | 7 → ATTACK, 9 → HANGINGOUT / 9 → PURSUE |
StratAggressive |
any but ATTACK, PURSUE, CONFRONT | CONFRONT | 7 → CONFRONT; others → CONFRONT |
StratRunAround |
WANDERWALK | CONFRONT | CONFRONT as StratHunter's, 9 → WANDERWALK |
StratHomeIn |
HOMEIN | CONFRONT | 8 → HANGINGOUT; CONFRONT 1 → ATTACK, 2 and 9 → HOMEIN |
StratManPile |
HANGINGOUT / MANPILE | MANPILE / - | MANPILE 1 → ATTACK |
StratPassive |
any but ATTACK, CONFRONT | CONFRONT | 7 → CONFRONT; CONFRONT 1 → ATTACK, 2 and 9 → the default |
CONFRONT walks the gang up to the Warriors' leader in formation (TacticConfront(gang, GangWarriors, approach,
critical, mode, ...)) and its code 1 at the critical range turns it into ATTACK (TacticAttack: threat response 2
and a Melee goal for every member). So the usual Mission 3 sequence is: seen → CONFRONT (the walk-up and the spot
line) → ATTACK at 2-3 m; a hit at any point → ATTACK at once.
| Gang (form) | Tactic | Preset | Sight changes | What starts the fight |
|---|---|---|---|---|
EarlyDest (back alleys, EDForm) |
CONFRONT from the start (14 m, 2 m, WalkAggressive) |
Hunter | - | Confront's code 1 at 2 m, no sighting needed |
LiquorDest (chapter 2's drunks) |
IDLE | Hunter | HuSetLOSRange 4, HuBlockLook |
code 3 within 4 m → ATTACK |
Wander (2 min after the drunks die) |
WANDERWALK | RunAround | - | code 3 → CONFRONT → ATTACK |
Chairs (chapter 4, roof chairs) |
HANGINGOUT | Aggressive | narrowed (not FullAware) |
code 3 → CONFRONT → ATTACK |
Roofers (chapter 4) |
IDLE | Hunter | - | code 3 → ATTACK |
HStairs (chapter 4, on the stairs) |
IDLE | Hunter | HuSetLOSRange 1 |
a hit (5, 6), or the player within 1 m |
RoofPursuit (chapter 4) |
HOMEIN (to where the player was) | HomeIn | - | code 3 → CONFRONT → ATTACK |
Event7, FlameTosser (chapter 4) |
none at set-up (Halt) |
ManPile / Hunter | FlameTosser: 1 m, BrDead |
their chapter's own script |
the stoops (SetUpStoop) |
USEFLAG (5 m, view 15 m) | Passive | - | Use-flag's code 7 → CONFRONT (Hold) → ATTACK |
RespondOff (HStairs, Roofers, Chairs, RoofPursuit, FlameTosser) only stops the tactic sending a member to help a
fight it hears about; it does not stop code 3. No form holds a gang passive by its threat response; the passive ones
are held by a short sight (HuSetLOSRange 1 or 4) or by Halt.
Coney status and fix (checked 2026-10-11, repo:src/ai; item 5 of the user's list):
- Coney runs the enemy scan only for members under TacticScout (
ScoutTactic::update→ScanSchedule::maybeScan);Brains::updateandBrain::thinknever scan. So no Destroyer under HANGINGOUT, IDLE, WANDERWALK or HOMEIN ever gets event 11, no code 3 reachesStrategy, and the gang stays in its tactic until a hit (codes 5 and 6) turns it to ATTACK. That is exactly "they see the player and do nothing until he attacks". - Nothing sends event 10 (
kEventSawPlayerhas no sender), so code 4 never fires;level87's presets ignore it, but other levels' may not. - A gang soldier with no tactic never fights an enemy in his list: Coney has no
GangBrain_Thinkstep 3. - The rest of the chain is in place:
codeOfmaps the events to codes,StoryTactic::eventandTactic::fireCallbackcall the Lua callback,Brain::pushFightGoalsrefuses under a tactic, and the scan itself (scanEnemies) sends event0xbto the human's handlers.
Fix, in order:
- Call the scan (
ScanSchedule::maybeScan) for every AI brain in the brain update, asBrain_UpdateGoalsdoes, with the 5-per-update budget and the 2 s / 8 s interval (detail level and fight stance), not only for scouts. - Add
GangBrain_Thinkstep 3 (and the cop's and Warriors' equivalents, Brain types): with threat response, no fight goal and a non-empty enemy list,pushFightGoals. - HanginOut's awareness (× 0.75, − 20° unless
fullAware) at its Start, undone at its End. - Then
Brain_MaybeSpotPlayers(event 10, code 4) for completeness.
Suggested tests (synthetic unless named):
- A kind-19 gang under TacticHanginOut with a recording callback, enemies with the player's gang; the player 15 m in
front in the open: one call with code 3 within 2 s. The player behind him beyond 3 m, beyond 22.5 m, or behind a
wall: no call. With
fullAware: called at 25 m. - The same under TacticIdle with
HuSetLOSRange(h, 4): the player at 5 m → nothing; at 3.5 m → code 3. - A type-2 soldier in a gang with no tactic and the player in sight: a fight goal on the player at his next think; under any tactic: none.
- The scan budget: eight brains due in one update, five scan, the other three on the next.
level87's strategy library with the disc's scripts ([disc]): a HANGINGOUT gang given code 3 is moved to CONFRONT; CONFRONT given code 1 to ATTACK; IDLE given code 3 to ATTACK.- The player's path (
[disc][story]):level87from its checkpoint 2 start, the pad's left stick at about 60 % toward a Destroyer group without pressing an attack; within 10 s a Destroyer has a fight goal on the player and the player takes a hit.
Runtime. Not observed in PCSX2 for this page: no save state reaches Mission 3 yet, and getting there means
playing two missions. A check for later, from such a state: log the tactic callback (0x00306938, the gang id and
the code) while the player walks into a hanging-out group's view; the expected order is 3, then 7 and 1 from
Confront, then the ATTACK tactic's members in Melee.
Runtime, gangs with no tactic (2026-10-12, PCSX2 2.9.94; confirmed (runtime)): at level80 checkpoint 4, two
Destroyer gangs with no tactic and threat response 2 were made enemies of Cochise and Swan by GangMakeEnemies. One
update later every member had scanned (five an update), and within six updates all twelve had FindEnemy and Melee with
Fight or Spectate. After the attack order made them enemies of the Warriors too, Destroyers turned on the player
although he never attacked: five within 21 updates in one run, two within 25 in another and none in 450 in a third, as
the scan lists only the Warriors in a soldier's field of view. That is steps 1 and 3 of the chain above, with nobody hit
first; Coney does neither yet. Of the fix list above, the open PR #394 does item 1 (the scan in every brain); item 2
(the think step that fights a listed enemy) is still missing there. Details, timings and the test:
Scripts.
The boss fights¶
Every boss fight on the disc sets one of the TacticBossScenarioA-H tactics, and each gives the boss an ordinary
fighting goal. None uses the boss goals BossChatter, BossLizzies, BossLuther or BossBigMo (types 0x93-0x96,
AI goals). Those are pushed only by the old Boss tactic's Start (0x003089b0,
type 0x09), which only TacticBoss reaches. No script calls TacticBoss (the bindings scan,
TacticBoss), and GoalBossLizzies is not called either. Confirmed (code:
the only callers of the four pushers and of Tactic_Boss 0x00308870 are those two).
| Level | Binding | Tactic | The boss's goal, by stage |
|---|---|---|---|
level5 (mission 7) |
TacticBossScenarioA |
BossDiegoVargas | below: BigBrawler (Diego); BigThrower then BigBrawler (Vargas) |
level81 (mission 8) |
TacticBossScenarioH |
BossChatterbox | 1 BigLedgeThrower, 2 BigFighterA |
level93 (mission 10) |
TacticBossScenarioG |
BossVirgil | 1 BigLedgeThrower, 2 HideAndSeek, 3 BigFighter |
level31 (mission 11) |
TacticBossScenarioF |
BossBirdie | StationaryShooterB (type 0x8f) |
level55 (mission 17) |
TacticBossScenarioB |
BossLizzies | StationaryShooter (type 0x8d), for class 0xed |
level84 (mission 18) |
TacticBossScenarioE |
BossLuther | StationaryShooterA (type 0x8e) |
level82 (flashback 1) |
TacticBossScenarioD |
BossRoof | Grabber and BigDefender; the survivor AvoidEnemies or BigBull |
level11 (flashback 5) |
TacticBossScenarioC |
BossMoe | BigFighter, its level set to the stage |
The levels come from the bindings scan of the disc's scripts (one level each). The other members of each gang melee (Moe's carry maces, Mace). Confirmed (code) at each tactic's AssignGoal.
The Diego and Vargas fight¶
TacticBossScenarioA (level 5, binding; tactic vtable
0x00543440) gives each Hurricane a goal by character (BossDiegoVargasTactic_AssignGoal 0x00309ea0, again on
gang events 19 and 22). Confirmed (code) at the addresses cited unless marked.
| Who, stage | Goal | Built from the tactic's tables |
|---|---|---|
| Diego (120), any stage | BigBrawler (type 0x84, BigBrawlerGoal_Init 0x002e8288) |
diegoFatigue, diegoDamage, diegoProne (all three), diegoCycles[stage], no flag, no objects |
| Vargas (119), stage 2 | BigThrower (BigThrowerGoal_Init 0x002ed0a8, vtable 0x00542880) |
flags[1], vargasCycles[2], vargasFatigue[2], vargasDamage[2], vargasObjects |
| Vargas, stage 3 | BigBrawler | vargasFatigue, vargasDamage, vargasProne, vargasCycles[3], flags[2], vargasObjects |
| anyone else | StationaryThrower (StationaryThrowerGoal_Init) |
minionObjects |
After pushing a BigBrawler goal the tactic writes the stage into it (BigBrawlerGoal_SetStage 0x002e8840, +0x3f).
What the bytes mean.
- fatigue: seconds. After a run of hits (BigBrawler) or a run of throws (BigThrower) the boss pushes a tired
goal (type
0x90,TiredGoal_Init0x002e7970, vtable0x00542a00) lastingfatigue × 1000ms (BigBrawlerGoal_GetFatigueMs0x002e8818reads entrystage − 1). - damage: percent of his maximum health. The tired goal ends early once he has lost more than
damage% of his maximum since it began (+0x1a= max × damage / 100,+0x18the health at the start;BigBrawlerGoal_GetDamagePercent0x002e8830). - cycles: for the BigThrower, the throws before he tires (
+0x2a; at half of them he taunts once,0x002fb0f0kind 2; after a throw at or past half, a 2 s wait); 0 means he never tires. For the BigBrawler the byte is stored (+0x3d) and never read. - prone: stored (
+0x3a-+0x3c) and never read; no reader was found in the goal's functions or the tactic. - The defaults when a table is missing (
GoalBigBrawlerfrom a script): fatigue 5, 4, 3; damage 30, 20, 10; prone 100.
BigBrawler (0x002e8288; fields +0x10 a flag handle, +0x14 the target's handle, +0x18 state, +0x1c the
re-pick time, +0x20 the chosen attack kind (45 none), +0x24-+0x33 eight object ids, +0x34 fatigue[3], +0x37
damage[3], +0x3a prone[3], +0x3d cycles, +0x3f stage, +0x40/+0x41 the saved brain bytes +0x0b/+0x0c,
+0x42 the next object index, +0x43 the flag step, +0x44 the last attack was kind 12 or 13, +0x45 hits
taken, +0x46 a counter flag). The constructor clears brain +0x11e (he cannot be chased) and, given a ninth
argument, puts that object in his hand (the tactic passes none).
- Start (
0x002e8418): brain+0x21c= 0, may pick up off, field of view 2π,+0x11e= 0, human flags|= 0x223c0, the human's vtable+0xe4with 1e9 (inferred: a hit-react resistance), and the brain bytes+0x0b,+0x0csaved. End (0x002e8530): undoes them (threat response 2, field of view 1.92 rad) and restores the two bytes. - Process (
BigBrawlerGoal_Process0x002e8f78), by state: 0 (stage 1 only) a taunt, line0x57and anim 643 (ANIM_RAGE_START) after 0-750 ms, then 2. 1 pick the best enemy (with the goal's own score hook below); if he is the current target and busy, close to the near range, say a line (PlaySound,0x002fb868) and wait 1 s; else an event 24 broadcast within 30 m (0x00293768), an EngageEnemy goal (taunt0x47) whose run-in distance+0x30is set to 6.25 (2.5 m), the cross + square special (kind 16) on an enemy already within 0.55 × far, else a 31 % taunt, a turn to him, and brain+0x0craised by one for Diego (+0x0btoo, but it stays 0 for both bosses, below); then 2. 2 fight: an object in hand (a throwable for an AI,Human_HeldIsThrowable(A, 0): set 5 or a knife; or held set 4 or 6,Human_HoldsKind4Or6) is thrown: a turn to the nearest enemy (brain+0x164), then a set-command action of command0x10(cross;SetCommandAction_Init(action, B, 0x10, 0), no delay; see the throw below); otherwise a target re-picked every 4 s, an attack kind chosen (Brain_PickAttack) and kept until it can be queued in reach (moving in within its reach), with a 10 % shout while closing. 3 the flag cycle (Vargas, stage 3): walk to the flag (0.5 m), turn toward the camera, play 549ANIM_GHETTO_PICK_UP(the next of the eight objects appears in his hand,0x002e8ee0→0x0024c280, while the tactic has fewer than two objects alive,0x0030a680), step 2 m back along the flag's heading, then 2. - Hits (
BigBrawlerGoal_OnHit0x002e8bc8, from the tactic's event 1): a running boss shouts (line 8); in state 2 a help call (20 m) and one more hit counted; at the sixth hit the tired goal is pushed, the count reset, and with a flag the state becomes 3. - Attack warnings (
BigBrawlerGoal_OnAttackWarning0x002e8858, event0x10): in state 2, unless tired, a grab or tackle he could escape is answered with command 3 (the counter); a player attacker becomes his target; an attacker within 1.5 m, while the boss has 10 % health or more and is free, is shoved off: line 11 or 14 and anim 653ANIM_SPECIAL_ATTACK1_FRONT. - Score hook (
BigBrawlerGoal_AdjustEnemyScore0x002e8620, goal vtable+0x54): −999 beyond his sight range, or when the enemy's task record (human+0xd8) holds a time (+0x2c) 751 ms or more in the past (meaning not traced); in state 2 also beyond 1.15 × far (except Diego in stage 3), else + 4 per metre inside the range, + 15 for a player, + 10 when knocked down; in state 1 + 4 per metre of distance and + 20 for a player not in a hold (0xe0000).
The tired goal (0x90; Start 0x002e7a68, End 0x002e7b30, Process 0x002e7f50): Start saves and clears his
god mode (human flag 0x10), no-reaction (0x800) and unstunnable flags, clears 0x910, sets 0x8000000,
stuns him (Human_Stun) and sets the deadline. Process: line 0x95 once, then line 8 while he stands stunned;
when the deadline passes or the damage limit is reached, his saved god mode back and 0x800 set; then the stun is
ended, and once he is free, god mode on, line 0x96 and anim 643; done on the next update. End restores the saved
flags and clears 0x8000000. So fatigue is how long he stays open and damage how much of his health the
players may
take in that window.
The health caps and the break (BossDiegoVargasTactic_CheckHealth 0x0030a150, each tactic update):
- Diego, stage 1 at 66 % health or below (stage 2: 33 %): unless the tired goal is on top, his health is put back
to 67 % (34 %): he cannot go lower while not tired. While tired and not yet breaking, the break starts
(
TiredGoal_StartBreak0x002e7c18): stun ended, fight stance off, flags0x800and god mode0x10set, anim 671ANIM_SPECIAL_IDLEchained to 672 (0x0025a3e0's special case: held flag0x20000, state code 18), blend 0.3 s, line0x22,+0x21= 1; health set to exactly 66 % (33 %). While held flag0x20000lasts the tactic's Process returns 18 (TacAnimStart) to the callback. - The break is done (
TiredGoal_IsBreakDone0x002e7e68) once his state code is 18 with no held flags left, and 2 s more have passed; the tactic then returns 1 (TacFinished). Until then, outside state 18 and not busy, god mode is cleared again. - Vargas, stage 3: at 66 % (first break) and 33 % (second, tactic
+0x69= 1), the same start; when done,TiredGoal_EndBreak(0x002e7da8) plays 673ANIM_SPECIAL_IDLE_END(blend 0.3 s), clears0x10and0x800, and the break count+0x69rises. Stage 3 reports 1 once neither boss is standing (0x0030a458).
The clips 671-673. They are missing_anim_filler only in the generic anim set (anim ids).
The two bosses' own anim sets carry them, and a character's set answers an id before the generic one
(CharacterInstance_GetAnim 0x00175080: an override slot or gang clip, else the character data's clip). No level 5
script loads or overrides them (level5*.lua set no ANIM_SPECIAL_IDLE with HuUseAnyAnim). From the disc's anim
headers (the lookup confirmed (code); the names from the disc):
| Anim id | Diego (hurr_de) |
Vargas (hurr_va) |
|---|---|---|
671 ANIM_SPECIAL_IDLE |
hurr_de_collapse_c |
var_tired_enter |
672 ANIM_SPECIAL_IDLE_START |
hurr_de_collapse_b |
var_tired_cycle |
673 ANIM_SPECIAL_IDLE_END |
hurr_de_collapse_e |
var_tired_exit |
The same sets give Diego 643 hurr_de_rage and 653 hurr_de_360_attack, and Vargas 549
hurr_va_pickup_large_obj, 643 var_rage1 and 653 var_clearing. Any other character in a tired goal would play
the filler.
BigThrower (Vargas, stage 2; Process 0x002ed718): state 0 a taunt (line 0x11, anim 643); 1 walk to the flag (0.5
m) or tire (above); 2 turn toward the camera and play 549 (ghetto_pickup; for others than Vargas the pickup clip is
overridden with the name at 0x00567a88), the next object appears in his hand (0x002ed498); 3 aim: turn within 45°,
line of sight (six misses drop the target), then 4; 4 throw (BigThrowerGoal_HoldDistance, then a set-command action of
command 0x10 with a 100 ms start delay) and back to 1 when the hand is empty. Start (0x002ed168) and End
(0x002ed260) as the BigBrawler's, plus that override.
The throw¶
Command 0x10 (cross) reaches the dispatcher like a pad press. With a held object of set 4, 5 or 6 it goes to
Player_CrossWithWeapon (0x002880d8, Combat). Confirmed (code). For an AI (brain type not 0)
the clip is thrown at his current target: the human's target handle (Human_GetTargetHandle 0x00226e20,
+0xc8, refreshed from the brain's target). He turns to it over the time to the clip's first event, then
Human_StartMeleeThrow (0x00261e78). A player instead searches (Player_PickThrowTarget, 10 m for set 4, 20 m
for 5 and 6). The clip by held set and gait:
| Held set | At a jog, run or sprint | At a walk, outside the stance | Standing or in the stance |
|---|---|---|---|
| 4 (overhead) | 507 | 506 | 505 |
| 6 (ghetto) | 553 | 552 | 551 |
| 5 (one hand) | 472 | 471 | 467 |
Set 5 first tries the bottle smash (473 / 475) on a human in reach. Vargas's objects (the names in VargasObjTypes
in level5_chapter3.lua: dyn_vargas_bookcase_a, dyn_vargas_sofa, dyn_vargas_stove_a, dyn_vargas_washer_a,
dyn_fridge_c, dyn_porcelain_weap_b, dyn_porcelain_weap_c) are all overhead_weapon
(Objects), set 4, so he throws 505-507 (the generic clips; his set has none). The hit
deals the object's CfgObj +0x58 (Combat moves). In BigThrower state 4 the goal has just
validated the brain's enemy (brain +0x124), inferred to be the same human. In the BigBrawler the throw also goes to
his target, which need not be the nearest enemy he turned to first.
The BossDiego program¶
No script calls GoalBossDiego. The Diego fight uses TacticBossScenarioA (Diego and
Vargas), so this goal is never built in play. Confirmed (code) for the interpreter;
confirmed (disc), per AI bindings, that no script uses it.
The binding's arguments (Goal_BossDiego 0x002a1aa8, BossDiegoGoal_Start 0x002a1b50):
- The variant
+0x10:- variant 0 runs the program at
0x00510e28, keeps his weapon, resets the inventory and gives one revive; - variant 1 runs
0x00510d28with 3500 health and a 1 s start delay.
- variant 0 runs the program at
- Three flag handles
+0x14-+0x1c. - The rest of Start: melee ranges 1 / 2 m, steering off, threat and damage response 0, and the game flag
+0x430set.
The program is a list of (op, argument) s16 pairs. BossDiegoScript_Next (0x0029fa28) reads them:
- a negative op jumps back by that many pairs;
- op
0x97jumps to label n; - op
0x96is label n; - op 100 is "fetch the next pair". Every op below returns to 100 when it is done.
+0x34 is the op's timer and +0x2c a counter.
| Op | Does |
|---|---|
| 101 | calm: threat and damage response 0, no pick-ups |
| 105 | fight-ready: pick-ups on, responses 2, a PathBlocker goal (10) |
| 110 | the attack weight override = table arg of 0x00510c40 (45 bytes each) |
| 115 | fight player 1 (game +0x228): responses 2, target, Goal_Melee(arg s) |
| 116 | the target (or player 1). Beyond 2 m: calm, normal mode, and run to him (gait 5, radius 1.0 or 1.5 m, 50/50), then op 117. Within 2 m: Goal_Melee(3-6 s). |
| 117 / 118 | When idle, the counter rises. At 2 the counter resets and op 125 runs with a random 5-9 s. Otherwise responses 2 and Goal_Melee(3-6 s). |
| 120 / 121 | target player 1 / the current target |
| 125 → 126 → 127 | calm, untargeted and stunned for arg s; then unstunned, normal mode and no-react; once idle, responses 2 |
| 130 | god mode = (arg ≠ 0) |
| 140 / 141 | Alternate every 10 s between Goal_Melee(10 s) and Goal_AvoidEnemies(20 m, 30 m). |
| 160-163 | Calm, no pick-ups, then walk to flag arg (0-3 from the goal's handles, 4 the named flag at 0x00562ba8). The gait is 2, 4, 4 or 3; the radius 1 m, or 5 m for 162. |
| 180 → 181 | clear actions; after 200 ms call the Lua function named at 0x00562bb8 when it exists |
| 192 / 199 | wait for the timer |
| 193 | 50 %: jump to label arg |
| 194 | anim slot 4 = clip 0x198 (arg 0) or 0x203 (1) |
| 195 / 196 / 197 | A Lua call, then clear, unstunned and no-react, and jump to label 2, 3 or 1. Op 196 also makes the human at 0x006e9404 pop to his goal 0x48 and run op 197. |
| 198 | wait arg s |
| 201 | with an object in hand, press cross at the target (a throw); wait arg s |
| 202 | after the timer, count +0x3c + 1 |
| 203 | responses 2 |
| 204 | a porcelain weapon in hand: one of 19 names at 0x00510bc8 (arg 0) or of 2 at 0x00510bc0 (arg 1) |
| 205 | look at the target; wait arg s |
| 210 | the player's gang avoids him (arg 1, BossDiego_GangAvoid) or stops (0) |
| 211 | press cross (arg 0), cross + square (1) or circle + cross (2, 3), then calm |
| 212 | arg 0: auto-escape, double damage, no-react, untouchable, flags 0x8000100; arg 1: auto-escape and untouchable only |
Not needed for any level: Coney can leave GoalBossDiego out until a script calls it.
Brain bytes +0x0b and +0x0c¶
+0x0b, turn boost (Brain_SetTurnBoost0x0028cdf8, 56 callers; forced 0 for Diego and Vargas, classes0x77,0x78, and for a class whose+0x11bis 6 or 7): an AI human's turn limit per update (Human_GetTurnRateForGait0x002212d0, throughHuman_MaxTurn) is the gait's AI word × (b + 1) for b ≥ 0, or ÷ (1 − b) for b < 0; a pad-controlled human ignores it. Above 0 it also swaps the AI's fight-stance walk (anim slot 14) from 372 to 380, the player's combat walk (0x00243848). EngageEnemy raises it by one for its run-in.GoalRunCarrotRun: Start saves the byte (RunCarrotGoal_Start0x002e1320), End restores it, and every fourth updateRunCarrotGoal_UpdateSpeedsets it to the saved value + 0 (no boost), + 2 (a speed boost up to 0.5) or + 3 (above 0.5): a runner being caught turns 3 or 4 times as fast as normal, so he keeps to the path at speed. Confirmed (code).+0x0c(Brain_SetStartBoost0x0028ce60; forced 0 for a class whose+0x11bis 6 or 7): when above 0, an AI that starts moving from rest gets state code 8 instead of 6 (0x00243848; what 8 changes is not traced).
The Warriors' pick-ups¶
WarriorBrain_Think (0x003052f0), every sixth think, while the brain may pick up (+0x265), holds nothing, and its
gang kind's pick-up factor (0x001644f8) is not 0: with chance 25 × factor %, when nobody is close around it
(0x0029eaa0: none of its eight sectors has flag bit 1 or 2 set, that is no human
within 1.5 m (Neighbour sectors); this corrects an earlier reading as a goal check),
the nearest pickable object within 1.5 m (0x0029d5f0) is fetched (0x002faf98). When the leader is free, the gang's
+0x32 is 1, a member is under attack and the Warrior's own enemy list is empty, the search's filter mask is 0x30000
instead of 0x10000 and its last flag 1; the radius stays 1.5 m. CfgWarriorWeapons(false) (game state +0x5704
= 0) refuses an object whose type class (+0x87) is 4, a weapon; other objects are still taken. A Warrior holding a
weapon of kind 4 or 6 with no enemies drops it unless its gang's tactic is type 0x26. Confirmed (code).
Spawners¶
A gang has four spawners of 0x130 at gang +0x640 (GangAddSpawner, 0x00166ff8; a fifth is ignored). Their
update, 0x001681a0, runs each in use against its state at +0x52; the states, their names and the scripts'
uses are in Spawner states. Confirmed (code):
| Offset | Meaning |
|---|---|
+0x00 / +0x14 |
position / name |
+0x50 |
in use; cleared once the spawner has made its total (+0x56 against +0x58) |
+0x52 |
state; GangAddSpawner's kind, GangStartSpawner's mode (0x00168cd0 accepts 0-5, 7, 8, 9, 11) |
+0x51 |
a wave is running (a negative +0x5a, step 2) |
+0x5a |
how many of its humans may be alive at once (GangAddSpawner's maxConcurrent, GangSetMaxConcurrent) |
+0x5c |
its humans alive: +1 per spawn, −1 when one's brain is torn down (0x0028c4f8 → 0x001672a8) |
+0x60 / +0x64 |
delay between spawns in ms / the next spawn time: now + delay after each spawn (100 ms in state 6, 0x00168158) |
+0x68 |
the state's value: seconds for 2, metres for 3, 5, 6 and 8 (GangStartSpawner's last argument) |
+0x70 / +0x74 |
a door it opens to let each human out, and how long it stays open |
+0x7c |
state 2's deadline: the value in seconds after GangAddSpawner (only it sets this; GangStartSpawner to 2 does not) |
+0x88 |
a distance from player 1 inside which it waits: 25 m in a gang of kind 0x17, otherwise 0 (none) |
+0x8c |
must be off screen (GangSetSpawnerMustBeOffScreen; GangAddSpawner sets it for kind 11 only) |
+0xac / +0xb4 |
the dispatch queue: the entry being served, then 4 entries of 0x1c (a time, a count at +0x14, bytes) |
- Ready? 0 never; 1, 6, 7, 8 and 10 always; 2 once past its deadline; 3 while player 1 is within the value
(
0x00336d88), 5 while he is farther; 11 while the gang's living members (0x00166158) are fewer than gang+0xb04, and back to 0 once the gang has spawned (+0xb02) its total (+0xb00). A running wave (+0x51) counts as ready in any state, even 0. - Waves: a negative
+0x5a(fromGangAddSpawnerorGangSetMaxConcurrent) of −n means waves of n. With no wave running, a ready spawner waits until+0x5cis 0 (every human of the last wave gone), then sets+0x51; it then spawns while+0x5c< n and clears+0x51on the spawn that makes+0x5creach n. A positive n never sets+0x51and simply spawns while+0x5c< n. State 11 ignores the limit. - Gates, in order; any one failing skips the spawner this update: player 1 at least
+0x88away (when non-zero); when+0x8cis set, no camera sees the spot;+0x5cunder the limit (or state 11); the next spawn time reached. - Dispatch (4 and 9, decided in step 1; neither is ready itself): when a queued entry is due (
0x0016dda8) and a gang slot is free (0x0016d458), a new gangResponder<n>is made: of type 1 for 4, only while the game's count+0x324is under+0x326, of the spawner's own type for 9, which also takes its owner's friend and enemy masks. The state becomes 6 (from 4) or 10 (from 9), and returns once the entry's squad is complete; an entry whose byte+0x17is 3 may also startTactic_RiotCop. - Place the human: 6, 8 and 10 out of the camera's view (below,
0x001673b8); 7 on a pedestrian flag out of view (AI and the world,0x001679e8); the others at the spawner. - Spawn (
0x00167ea8, named<spawner><count>), open the spawner's door, count it.
The level's cap. Confirmed (code) at the cited addresses. The update above runs for a gang only when the
gang's update (Gang_CachePlayerInfo, 0x00166708, called with 1 from Gangs_Update 0x0016d170) finds the game's
count under its cap, asked once per gang before its four spawners run, so one gang's spawners may each make one
human past it in the same update:
| Gang | Count | Cap | Set by |
|---|---|---|---|
police (gang +0x2c = 1) |
game state +0x324 |
+0x326 |
SetCopSpawnMax (0x00299520) |
| any other | game state +0x432 |
+0x434 |
SetSpawnMax (0x00299510) |
- Up: every spawn adds 1 to
+0x432, and a spawn in state 6 also 1 to+0x324(0x001681a0, after the spawn). - Down:
GangSpawner_OnHumanGone(0x001672a8, fromBrain_TearDown0x0028c4f8) takes 1 off+0x432for every spawned human, and 1 off+0x324(and the spawner's+0x5a) when the gang is police and the spawner is in state 4 or 6; a responder spawner (9 or 10) of another gang takes 1 off its+0x5aonly.Gang_RemoveMember(0x001664d8) also takes 1 off+0x432for a human leaving his gang, unless his spawner is in state 4, 6, 9 or 10. When a dispatch's squad is complete (states 6 and 10), the update takes the responder gang's living members off+0x432(and off+0x324in state 6) again. - Reset: the level reset (
GameState_ResetForLevel,0x00418c68) sets both caps to 16 and+0x324to 0;+0x432is zeroed only when the game state is made (GameState_Construct,0x00418588). - A spawner's own humans may still be counted after it has made its total:
+0x50cleared stops its spawning, not the count. - Confirmed (runtime), PCSX2 2.9.129,
level34checkpoint 2 standing still (2026-10-21):+0x432read 20-21 under+0x434= 45 (SetSpawnMax(45)), and+0x326= 16 (SetCopSpawnMaxis not called). With a breakpoint on0x001681a0for a gang not of the police kind, the update ran within one frame for gang kind 0 (called from0x0016698c); with+0x434written to 10 it did not run for 600 frames while the count fell from 21 to 12 as humans went, and ran again within a frame once 45 was written back. With the cap at 14 the count rose from 12 to 14 and stayed there for 1,200 frames.
The type (0x0016d810): the spawner keeps an index at +0x4c into its ten types (+0x24). Each spawn first
adds 1; at 10, or at a slot holding 0, it goes back to 0; the type at the index is used. So the types are taken in
turn, a 0 ending the list. GangAddSpawner does not reset the index, so a new spawner starting from 0
(inferred: the gang record starts zeroed) takes the second type first when there is one. State 6 (a police
dispatch) uses the model cops_vc1 and type 0x103 for an entry whose byte +0x17 is 3 or more, and otherwise the
type in turn with one of the four models at 0x0050cb40 (Random_Int(3), 0-3); the other states use the spawner's
model (+0x20). Confirmed (code).
The human (0x0016d860): a free human (none: nothing spawns), its name, Human_Init(type, gang, position), facing
the spawner's heading (+0x54, degrees), brain +0x210 = the spawner's slot, the spawner's animation (+0x1c)
unless it is nothing, and then the callback (+0x18, interned at GangAddSpawner): looked up by name (slot
+0x4c, so Table.func and Table:func work) and, when it resolves to a function, called with three
arguments: the human's handle (slot +0x5c), the gang's id (gang +0x30, a short, slot +0x64) and the
spawner's name (gang +0x654 + 0x130 × slot, a string, slot +0x7c). Nothing else is set up for it: the engine
writes no table or global first. The callback runs before the caller opens the door, counts the spawn and, for
states 6 and 10, adds the human to its responder gang and gives its dispatch goal. Confirmed (code). level87's
StoopCallBack(human, gang, name) appends the human to tblStoop[gang].humans, a table its own SetUpStoop made
for that gang id before the spawner started; called with the handle alone it indexes tblStoop[nil] and fails.
Off screen¶
With +0x8c set the spawner asks Camera_AnyPlayerCanSeePoint(radius 0.3, distance 0, point) (0x001202e8) about
a sphere of radius 0.3 m centred 1.6 m above the spawner's position (z + 1.6), and skips the update when the
answer is yes. It asks every player's camera (0x005d9150[i] for i below the player count 0x0050b198) and is yes
when any of them sees it. One camera's test, Camera_CanSeePoint(radius, distance, camera, point) (0x00122548):
- Range: the limit is the smaller of the distance and the camera's view distance (camera
+0x58, vtable+0x20c); a distance of 0, as here, means the view distance. Unseen when |point − camera position| − radius exceeds it. - Frustum: unseen when the sphere lies wholly outside any of the camera's six planes
(
Camera_FrustumTestSphere,0x00121fd8, vtable+0x16c: plane i at camera+0x70 + 0x10ias a normal and distance; outside when n · p − d ≤ −radius). - Occlusion: a ray from the camera's position toward the point, as long as the distance less the radius
(less 1e-5), through the level's collision mesh (
CollisionMesh_RayCast, mask 0), skipping the materials 30LOW_FENCE, 2GLASS, 122RAILINGand 107CHAINLINK_NOCLIMB. Any hit: unseen. No hit: seen.
So the sphere is occlusion-tested, by one ray to its centre (the radius only shortens the ray and widens the
range and frustum tests). Confirmed (code) at 0x001685e8-0x00168624 and 0x00122548. The test uses the
spawner's own position whatever the state, also for the states that place their humans elsewhere.
Out of sight¶
0x001673b8 places the humans of states 8, 6 and 10. Confirmed (code):
- Camera: player 1's; with two players, for 6 and 10 the player whose camera is nearer the dispatch entry's
position (kept at
+0xe1for the other states). - Search: from the route node of that player's human, a best-first search over the route graph (
0x00251d28) toward a goal point, which returns the first node either more than 70 m (0x0050cc60) from the camera or more than the spawner's value in metres from it and outside a cone around the camera's forward of half its field of view + 10° (+0x2ac). The first goal is 100 m straight ahead of the camera; up to 16 more tries each turn the forward by a random angle outside that cone and put the goal at 2 × value. No node in 17 tries: no spawn this update. In detail (the search):- Origin: the camera's position (vtable
+0x21c) with its z replaced by the player human's z (position table0x00714b00); every distance below is from this point, in 3D. - Start node: the player human's area (
0x00250708), then its node nearest the human's position that he reaches in a straight line, up to 30 tried by distance (0x00251150→0x00250e98, as for a route's ends in Path planning). No area or no node: no spawn this update, and no try is made. - Cone: the camera's forward (its orientation's y axis: vtable
+0xac, then0x003363b0) with z then set to 0 and not normalised again; half-angle h = (field of view × 0.5 + 10)° in radians. - Try 1: goal = origin + 100 × the forward taken before its z is cleared.
- Tries 2-17: θ =
Random_Float(h, 2π − h)(0x00335420: a uniform draw, the raw random number / 2³² scaled into the range); the flattened forward is turned by θ about the vertical (a quaternion about (0, 0, 1),0x00511740), and goal = origin + 2 × value × that vector. Each try turns the original forward, not the previous try's; a θ is drawn before every search, so 17 draws are made and the first is unused. The cone the search tests keeps the original flattened forward on every try; only the goal moves.
- Origin: the camera's position (vtable
- Second player: with two players, a node that the other camera can see 1.6 m above it (
0x00122548, its distance argument the larger of the value and its view distance capped at 70 m) is refused. - Turf: the node must lie in one of the gang's turf boxes (
Gang_IsPointInTurf,0x001652e8); a gang with no turf takes any. Otherwise no spawn this update.
The human stands on the node found. The function's other branch (a flag near the point, checked for being unseen) is not reached from its one caller.
The search¶
0x00251d28(value, h, startNode, origin, goal, coneAxis), confirmed (code):
- A* over the route nodes (C records of
0x20) from the start node toward the goal: f = g + h at node+0x16, g at+0x18, h at+0x1a, parent at+0x1c(allu16, 1/16 m). h = 16 × the 3D distance from the node to the goal (0x00251820); an edge costs as in Path planning (0x00251890with mask0xff, so theClimbFilterextras apply when it is on). Unlike the route search there is no mask test and no 65000 cap: every edge is taken except one whose D word has bit 31 set (a hazard-avoided link), which is skipped. A node already open is updated only when the new g is lower. - Popping the node with the lowest f (ties: the heap's order) marks it closed (
+0x18=0xabcd) and tests it, the start node included: found when its distance from the origin exceeds 70 m, or exceeds the value and the unit vector origin → node has a dot product with the cone axis below cos h. Found: the goal point is overwritten with the node's position and the search returns 1. - Limits: the open list is a heap of at most 1,000 entries (
0x002531c0), the closed list at most 3,000 nodes (0x002534d0). An empty open list, a full open list on a push or a full closed list ends the search with nothing (0); on leaving, the closed marks are cleared.
Crimes and the police¶
A crime report, 0x0041b8b0(state, pos, type, offender, victim, severity, mode, count) on the game state
0x0051489c, is how scripts and the game raise the crime types. The game state's
crime fields, confirmed (code): +0x270 the crime scene, +0x288 reporting on (ReportCrime), +0x290 the
player's last crime type, +0x294 + type the responders per type (CfgCrimeResponders), +0x2dc the Lua callback
(CfgSetCrimeCallback), +0x32b + type enabled (CfgEnableCrimeType; read only for type 12, by the police brain at
0x00300910 and 0x00301770). The wanted timer a report starts, the crime level and the unused severity are on
Crimes: wanted.
- Nothing while reporting is off. An offender in a gang of kind 1 (police) or
0x17is ignored. - With an offender: every police gang turns hostile to his gang, and his gang to them except for types 7 and 12
(
0x0016c3a8); the callback runs with his gang and the type (0x0041ae60). - The
CrimeSceneflag moves to the position when it changed. - Responders, when
modeis 1:0x0016df68(type, kind, count, ...)queues the crime on the nearest spawner of any gang in state 4 or 6 (Spawners); kind 1 for types 0, 2, 3, 5, 8, 11 and 13, kind 3 for 9; count the type's responders, orcountitself for type 4. Types 6, 7, 10, 12 and 14 send none. For types 0, 2, 3 and 8 the offender's gang also notes the time (gang+0x5f4, a wanted timer, inferred). Type 1 (kind 1) queues only in step 5. - Type 1 acts only when the nearest store front (activity
0xe) lies within 10 m (squared distance under 100). When that store is not yet robbed, it queues its responders with a delay ofCfgBreakAndEnterDelayand no offender (mode1 only), sends message0x12to thestrobenearest the store's flag when that one is within 6 m (squared distance under 36), and switches the store's buyers off; robbed or not, the store is then marked robbed. Details: Crimes: which break-ins call the police. Types 0, 2 and 8 score a statistic for a player offender against a victim of brain kind 1, 4 or 5, once per victim (0x004ed948on0x006fe490; a statistic, inferred). - When the offender is in player 1's gang,
+0x290takes the type unless it holds 7 or 12, and the HUD is told (0x001b2520).
Who reports, confirmed (code): CrimeIsHappening (0x0041b6e0, mode 1), SpawnCustomCrime (0x0041b7a0, type 4),
an alarmed glass pane (World objects) and the code at 0x0021b290, 0x0022d908, 0x002e6668,
0x003961d0 and 0x00394da8 (type 1), 0x00301cf8 (type 2), 0x0041bec8 (type 9), and 0x002a83f8 and
0x002c4360 (a type from their own records). The crime names are a function (0x0041d2c0) with no callers; the
numbers they go with are inferred from those types (4 Custom, 1 BreakAndEnter for the store, 9
PrecinctAttack).
Warrior commands¶
A war chief (a player human whose +0x3ac is 1) orders the crew, his gang, with one of seven Warrior
commands (Commands). The menu (HUD 0x001a6c58, R2 and the right
stick: HUD), WCIssueCommand
and the game itself go through one dispatcher, 0x0041c4e0(state, chief, command, forced, pos, arg), confirmed
(code):
- Nothing while commands are locked (game state
+0x411), the player's menu is locked (+0x42e+ player), the command is disabled for the player (+0x41e+ player × 7 + command,WCEnableCommand; the player is human+0x1b0) or the human is not a war chief. - The command becomes the player's last (
+0x41c+ player). Unforced (the menu): the chief is quiet when a line of his is still playing (his speech handle resolves) and his+0x194is clear. Then the range check (WarChief_CheckCrewInRange,0x0041c338): when the gang's nearest other member is more than 50 m away (squared distance over 2500), the command is refused and, unless quiet, the chief says 18range, or 92range2when any member is cuffed or knocked out (Human_SayCommand(1.0, chief, …, no interrupt)); game state+0x414+ player records the refusal (The out-of-range flag). Within range, giving the current command again (the tactic's+0x18clear) only repeats its line (WarChief_SayCommand,0x0041cc40) and, for 0 under a tactic of type0x12, calls0x003114e0on it. Forced, only defend (2) goes through the range check (quietly). - Otherwise the gang's tactic is cleared and the command's started: 0
0x00310e00(given 9.0), 10x00320530(unforced, the chief also plays clip 0x2a4 or 0x2a8, and the gang's target is set from his), 2Tactic_Defendaround the chief (clip 0x2a3 or 0x2a7), 30x00313400(the standing tactic of type 3, the first mission) or0x003128a0for a gang whose brain+0x2d5is set, 40x00319570(given 75.0), 50x00320b60; 6 starts nothing. - The chief says the command's line (
WarChief_SayCommand,0x0041cc40, below): 0follow(follow_hidewhile hidden in shadow,Human_IsHiddenInShadow0x00228168), 1attack, 2defend, 3hold(holdhide), 4scatter, 5stealwhen the nearest store flag (activity0xe) is closer than 64 (8 m if that is a squared distance, inferred) and its zone still has objects (ObjZone_HasObjects,0x0039a580), elsevandal; 6 none. A forced command says nothing, and neither does a quiet one (step 2).
The out-of-range flag¶
WarChief_CheckCrewInRange(state, chief, quiet, forced) (0x0041c338) keeps one byte per player, game state
+0x414 + the chief's player index (human +0x1b0), that says whether his crew was out of range at the last check.
Confirmed (code) at 0x0041c338 unless marked:
- Who checks. The dispatcher (
0x0041c4e0, calls at0x0041c600and0x0041c738), as step 2 above, and the player's own update,PlayerBrain_Update(0x003035d8, call at0x003037c4): every 40 updates (brain+0x34modulo 40) it calls the check withquietandforcedboth 1, so no line is spoken and the byte simply follows the crew (The player, step 4). - What it writes. Only for a war chief (human
+0x3ac= 1): the gang's nearest other member (Gang_FindNearestOtherMember) within 50 m (squared distance 2500 or less) clears the byte and the check returns 0; farther, it sets the byte to 1, says 18rangeor 92range2unlessquietorforcedis set, and returns 1. A gang with no other member returns 0 and leaves the byte as it was; so does a human who is not a war chief. - Who reads it. An instruction scan of
SLUS_212.15for offset0x414finds two readers:- the Warrior command menu,
WarCommandDisplay_Update(0x001a7e48, the load at0x001a8028): while the menu is open and no command has been issued, a set byte replaces the highlighted command's name with entry 6 of the command-text table (0x006007d8), the out-of-range text fromGSTRING.COMMAND6 (The Warrior command menu). That text's 2.5 s<DISPLAYTIME>is in the script's text, not set by the code (read fromconfig_strings_en.lua). Confirmed (runtime), PCSX2 2.9.94,level80: with the byte held at 1 the open menu showed that text instead of the command's name. - the mission-failure check,
GameState_CheckGameOver(0x004197a8, the load at0x0041996c): when player 1 is cuffed (not knocked out) or holds a flash (item 1) and others could still help, a set byte fails the mission at once instead of waiting for a member to reach him. A knocked-out player 1 without a flash fails at once whatever the byte (The mission failing, case 5).
- the Warrior command menu,
- No other cue. Besides the chief's line (unforced, not quiet) and the menu's text, nothing plays or shows: the check calls no sound or HUD function, and neither reader plays a sound. The refused command itself is not shown as refused: the menu closes after an issue and its text is updated only before one, so the out-of-range text is seen the next time the menu opens while the crew is still out of range (inferred from the order of the updates; confirmed (code) that the text is set only before an issue).
The command tactics, by command (constructor, vtable, type id; confirmed (code)):
| Command | Constructor | Vtable | Type | Members get |
|---|---|---|---|---|
| 0 follow | WarriorFollowTactic_Create 0x00310e00 (init 0x00310e90, distance 9.0) |
0x005438c0 |
0x12 |
FollowPlayer (below) |
| 1 attack | WarriorAttackTactic_Create 0x00320530 (init 0x003205b0, member goals 0x00320618) |
0x00544160 |
0x01 |
FollowAndAttack (0x34, 0x002bbdc0) on the chief; dogs (class 221) AvoidEnemies (4 m, 8, 8) (code) |
| 2 defend | Tactic_Defend round the chief (given 2.25) |
0x00543800 |
0x02 |
FollowAndDefend (table) |
| 3 hold | WarriorHoldTactic_Create 0x00313400 (init 0x00313490, given 1.5), or WarriorHoldTactic2_Create 0x003128a0 (init 0x003128f8) |
0x005439e0 / 0x00543980 |
0x03 / 0x13 |
type 3: HoldPosition at his own spot, radius 1.5 m, and a turn outward; type 0x13: Hide (0x58) at a wall point near the nearest hiding flag (code, hide) |
| 4 scatter | WarriorScatterTactic_Create 0x00319570 (init 0x00319600, given 75.0) |
0x00543d40 |
0x25 |
Scatter (0x37) to one of up to six hiding flags within 75 m (code) |
| 5 steal / wreck | WarriorStealTactic_Create 0x00320b60 (init 0x00320be0) |
0x005441c0 |
0x26 |
WarriorVandalSteal (0x83); dogs AvoidEnemies (7, 12, 12) (code) |
Confirmed (runtime), PCSX2 2.9.129, level 99's street with the level's Warrior Ash and one Warrior spawned into the
crew (PCSX2 recipes): IssueWarriorCommand(c, true) for each command left
the gang's tactic (gang +0x40) with exactly the vtable in the table, and each member's goal stack held the type the
table names: follow 50 (FollowPlayer, a member just added also 51), attack 52, defend 53, hold 54, scatter 55, wreck
131. The dispatcher (0x0041c4e0) keeps two bytes per player: game state +0x41a + player is the command in force
(GameState_StoreWarriorCommand, stored when the tactic starts) and +0x41c + player the last one asked for. Confirmed
(code) at 0x0041c4e0: it returns before storing +0x41c when the command's enabled byte (+0x41e + player × 7 +
command) is clear, and stores it before the range check of an unforced command, so a command refused for range shows
only in +0x41c and a disabled one in neither. Confirmed (runtime): the street's state had all seven enabled bytes
clear; attack asked without WCEnableCommand left the tactic, both bytes and the goals unchanged, and the same call
after it gave attack. Automatic switching (below), confirmed (runtime): with
WCEnableAutomaticSwitching(true) and nobody to fight, attack gave way to follow in six runs of seven, seen within 1 s
of the recipe returning (polled every 0.25 s; the 1.5 s delay is from the code; the wall times logged include the
tools' start-up); the first run, right after the state loaded, kept attack for 19 s (not explained). With
WCEnableAutomaticSwitching(false) attack held for 15 s in each of three runs.
The names follow, attack, hold, scatter and wreck are read from the lines (inferred). The follow, second hold,
scatter and steal tactics are of type 0x12 or above, so they own their members' goals (+0x3c, Tactics).
What each command tactic does, confirmed (code) at the addresses on the code index. Every
one gives each living member that is not a player and not the chief a goal at Start, after flushing him. In a
two-player game a Warrior of player 1's gang more than 10 m from player 1 and nearer the other player gets
FollowAndDefend on that player instead. Once the chief's line ends, a random free member answers (0x003069f0):
attack_resp (132), scatter_resp (159), vandal_resp (140) or, at a store, steal_resp (141); the hold answers
with line 0x85. None of their Process functions ever ends the tactic (they return 0); a new command replaces it.
- Attack (
0x01):FollowAndAttack(0x34) on the chief, whose Process fights the best enemy within 60 m of the chief (its row). Event 19 gives all goals again; 22 with+8= 1 gives that member his goal again. - Hold (
0x03):HoldTactic_GiveMemberGoal(0x00313718) pops each member to his goal base and pushes HoldPosition (0x36) at his own position with radius 1.5 m (the tactic's+0x20);HoldTactic_GiveGoals(0x00313508) then queues a turn to a heading spread 360° / (members − 1) from the chief's, after 0-1 s with enemies about, else 2-4 s. HoldPosition's Process (0x002be818): with no enemy, a 30 % fidget every 3 s inside the radius, a walk back outside it; an enemy not targeting him: turn to him; one targeting him: a fight goal (4000 ms) when in sight and reach from inside the radius, else turn and shuffle. Events 17 (+4= 0), 19 and 22 (+8= 1) hold again; 20 (violence) makes a free member look at the fighter for 3 s and taunt at 10 %. - Hide (
0x13, the hold for a gang whose brain+0x2d5is set): from the hiding flag (activity0x21) nearest the chief, ten 10 m rays 15° apart find wall points; each member gets Hide (0x58) at a free wall point at least 1 m from the chief. Events 1 and 16 (a player attacked while hidden in shadow): when no free member is left, the attack command is dispatched for the chief; another gang's tactic stops. - Scatter (
0x25): up to six hiding flags (activity0x21) within 75 m of the chief and at least 10 m from him, reachable, nearest first; members get Scatter (0x37) to them in turn, at a random navigable point 1-1.5 m from the flag (no flags: Scatter with no flag). Events 1 and 16 on a member at his flag: he moves on to the farthest flag with no player within 5 m, or with none fights (Melee) for 10 s. - Steal / wreck (
0x26): every 2 s the store flag (activity0xe) within 12 m of the chief and reachable is picked again; its zone's objects are clustered (1 m). Members get WarriorVandalSteal (0x83), which steals from the zone's clusters (an object is claimed for 5 s when no enemy stands within 1.5 m of it) or, with no store, wrecks what is near.
The chief's line and the crew's answer¶
Every order the player gives from the menu is heard twice: the chief says the order, then one Warrior answers once
the chief's line has ended. Confirmed (code) at 0x0041c4e0, 0x0041cc40, 0x003069f0, 0x003208e8,
0x003209d0, 0x00319f70, 0x00310768:
- The order (
WarChief_SayCommand(state, chief, quiet, line)): by the player's last command (+0x41a),Human_SayCommand(1.5, chief, line, no callback, interrupt, 0, no target, 1): 0 → 2follow(86follow_hidewhile hidden in shadow), 1 → 1attack, 2 → 3defend, 3 → 4hold(70holdhidewhile hidden), 4 → 158scatter, 5 → 5stealor 6vandal(step 4 above), 6 → nothing. Lines 1-6, 18, 70, 86 and 158 are the ones that free a stream channel first (Sound: speech). Withquietset (a forced dispatch, or the chief already speaking) nothing is said. Game state+0x416+ player is then set to 1 when no line is playing (quiet, or the line found no sound), 0 when it plays. - The gesture (unforced only, step 3): attack plays 676
cmd_wreckemall_combatin the fight stance or 680cmd_wreckemall_neutralout of it; defend 675cmd_watchmyback_combator 679cmd_watchmyback_neutral(Human_PlayActionClip(chief, clip, 2.0, 0)); not while the chief has a held flag of0x2fefefff, a state flag of0x7bf9e9f7ff0or an object of kind 4 or 6 in hand. Follow and hold have onlymissing_anim_fillerclips (677, 678, 681, 682) and play none. - The answer (
Tactic_SayAckLine(tactic, line),0x003069f0): each command tactic's Start notes whether the chief's line is playing (the chief is a player,+0x416+ player is 0 and his speech handle+0x178resolves). Only then, on the first Process after that line has ended, one member answers; with no line, the answer is skipped. The member: with at least two living members, over the 16 slots, each non-player member other than the leader who is not busy (Human_IsBusy) and whose actions are not blocked; the first such is taken and each later one replaces him at 50 %.Human_SayCommand(1.0, member, line, no callback, no interrupt, 1, no target, 1). Lines: follow 131follow_resp, attack 132attack_resp, defend 130defend_resp, hold 133hold_resp, scatter 159scatter_resp, steal 141steal_respat a store, else 140vandal_resp.
So the automatic commands (below), the scripts' WCIssueCommand(…, true) and the level
start are silent: only the menu (and an unforced IssueWarriorCommand) makes the chief and the crew speak.
Hiding in shadow (Player_UpdateHideOrder, 0x0023e7e0, and Human_SetHiddenState, 0x0023e6e8; confirmed
(code)): while player 1 (*0x0051489c + 0x228) is in shadow (human +0x1ac is 0.0), not busy, his gang able to
help, his last command neither hold nor scatter, and no member under attack or his command follow, a timer starts:
500 ms, or under follow the time the nearest hiding member (brain +0x2d5 set) needs to run to him at gait 4, plus
500 ms. When it passes, the last command is kept in 0x0051480c and hold (3) is dispatched forced; with his
brain's +0x2d5 set that is the hide tactic (0x13). Leaving the shadow re-dispatches the kept
command, forced, when the gang's tactic is still the hide. Both are silent.
The default command: follow¶
Follow is the default, and the game issues it itself. Confirmed (code) at the addresses cited:
- Level start.
InitLevel's game-state reset (0x00418c68) sets, for both players, the last command (+0x41a) to 0, all seven commands enabled (+0x41e), the menu unlocked (+0x42e), and the automatic commands on (+0x431= 1, below).Human_MakePlayer(0x00229c40) makes the new player the war chief (+0x3ac= 1) unless another player of his gang already is, makes him his gang's leader (gang+0x44), and then dispatches command 0, forced (0x0022a1a8). So a crew starts every level under the follow tactic as soon as its chief is made a player. - After a scene.
SceneTask_End(0x0039f450) pops each bound human's scene goal when it is of type0x27-0x2a(JoinCinematicis0x29), and, when one of the roles was a player (or the first player's brain+0x2e4is set), unlocks that player's menu and dispatches command 0, forced;SceneTask_Abort(0x0039ec60) does the same for the first player. So the crew leavesGoalJoinCinematicand goes straight back to following. Human_SetWarChief(0x002398b0) re-issues the player's last command (+0x41a) to the new chief's gang.
A forced dispatch still passes the checks of step 1 above (a locked command system, WCLockCommands, or a disabled
command gives nothing) and always rebuilds the tactic.
The follow tactic (fields after the base: +0x20 the follow distance, 9 m; +0x24 the next formation reshuffle;
+0x28 the next banter check; +0x2c the idle timer; +0x30 "the leader is a player"; +0x31 started):
- Start (
0x003111a8) gives the members their goals (WarriorFollowTactic_GiveGoals0x00310f38): the chief's formation takes slot set 9 (0x00295dd8); every member that is not the chief, not a player (human+0x1b0= −1) and not down is popped to his goal base, marked (Brain_PopToGoalBase,Brain_MarkGoalBase) and getsGoalFollowPlayer(9 m, chief, mode 3)(0x002de380, type0x32, vtable0x00541d70). In a two-player game a Warrior of the first player's gang more than 10 m from him whose nearest player is the other one getsFollowAndDefend(0x35,0x002bca20) on that player instead. "Not down" isHuman_IsDownOrDead(0x00227e60) returning 0: the human has its record (+0xd4) and none of the state bits0x180050000(0x100000000dead,0x80000000dying,0x40000knocked out,0x10000tackled; Combat). It does not read the brain's+0x09(BrDead), the health, or knocked down (0x80000): a member made dead to the AI still gets the follow goal. Confirmed (code). The bindingHuIsAliveis the same test. Start also fills the gang's anim group 604 with ten idle clips (0x00169468; End0x00311230undoes it). - Process (
0x00311638): done (1) when the gang has no leader; every 8 s the chief's slot set is re-picked (0x00295db0, one of two); every 1 s (0.25 s while the chief holds something) a banter check: when no scene plays, no member has an enemy and every member has been idle for 25 s, one member says an idle line and looks at the chief for 4 s. Otherwise it returns 0, so the tactic never ends by itself. - Events (
0x00311928): 19 for a member (not the chief) with a goal: flushed and givenFollowPlayermode 1; 11 from a Warrior: the gang's warning line (0x001691a0, by how many are left and whether the spotter is beyond 12 m); 20 (violence nearby): consumed, and a free member near a busy offender looks at him for 3 s, with a 10 % taunt (0x00311250); 22 with argument 1: the goals are given again. - Repeating the follow command (unforced, under type
0x12) calls0x003114e0: each member holdingFollowPlayerwithoutFollowFormation(0x33) also getsFollowFormation(0.75 m, chief, 1).
Adding a member who is already in the gang (GangAddMember → Gang_AddMember, 0x00166308; confirmed (code) at
the cited addresses). There is no early return:
- A human whose brain
+0x20cnames any gang, the same one included, first has his actions, target, counted enemies and brain+0x164list cleared. He then leaves throughGang_RemoveMember(old, h, 1)(0x001664d8): - a spawned human (brain
+0x210≥ 0) takes his spawner's alive count (+0x69cof the spawner record) and the game state's+0x432down by one and gets+0x210= −1, unless the spawner is in state 4, 6, 9 or 10; - the leader is cleared when he was it;
- under a tactic (gang
+0x40) his brain is popped to its goal base; - the gang gets event
0x16with+4= the members left and+8= 0. - He is put back: brain
+0x20c, his attack weights, god mode under an invincible gang, the leader slot for a player war chief (human+0x3ac= 1). - Event
0x16follows with+4= the count before + 1 and+8= 1.
Gang_OnEvent (0x00164c20) hands an event first to a script handler the gang registered for its type (consumed
when that returns 1), then to the running tactic (vtable +0x4c) while the gang is not empty, or for a removal. The
follow tactic answers 22 with +8 = 1 by WarriorFollowTactic_GiveGoals for every non-player member who is
not down and not the leader, not only the one added. Each is popped to his goal base and marked; nothing requires an
empty stack. That is the only way back: Process gives no goals, and GangBrFlush (0x0016ba18) clears the goals
and actions but leaves the tactic at gang +0x40.
So level80's EndStartCam works in the original by its own GangAddMember calls:
GangBrFlush(0), GangBrDead(0, nil), then GangAddMember(0, −1, h) for the player and both Warriors. Each add
re-runs GiveGoals. The player's add also clears and resets the leader before the Warriors' adds; the Warriors' own
adds give everyone the follow again.
GoalFollowPlayer's Process (0x002de7c0): done (2) when the leader is gone. The init (0x002de380) stores
the distance squared (+0x14; 81 for the tactic's 9 m). Each update, confirmed (code):
- While brain
+0x284(the last move's failure) is set, a counter (+0x24) counts up; at 31 or more both are cleared. So after a failed walk the follower waits about a second before it tries again. - Stand or follow. It stands (no new goal) when all of these hold: the leader is within the distance
(9 m; 10 m in fight mode), measured in 3D between the two humans' positions (
Vec_DistSqon the position table0x00714b00, so a leader 2.2 m up counts his height), its own brain+0x2d5equals the leader's (both set or both clear), and it can walk straight to the leader (Human_CanWalkStraightToHuman0x002221e0on the leader's position: the trains' test, then the walkable line from the follower's polygon, mask 0). Otherwise, while the counter is 0, it pushesFollowFormation(0.75 m, leader, turn-with-leader 1, no time limit)(0x002dfba8) and returns; that goal runs on top until it ends (below). The test runs only whileFollowPlayeris the top goal: whileFollowFormationruns above it, nothing stands the follower down early. - With actions queued it returns. Otherwise the fight mode, or the idle behaviour below.
A move left over from FollowFormation (confirmed (code) at the cited addresses). Neither FollowFormation's
End (0x002dfe68, which restores the edge mask +0xf4, the Warrior's flags and the start boost) nor the pop
(Brain_PopGoal 0x0028d7d8: End, free, then Goal_Resume on the new top) clears the actions, so a move still
running when FollowFormation returns 2 keeps running. Meanwhile FollowPlayer does nothing at all: Goal_Resume
(0x0029ee30) only marks it "to resume" while the action count (brain +0x2e) is above 0, and Goal_Process
(0x0029eed8) then returns 0 without calling its Process, every update. Only when the move has ended does
FollowPlayer's Resume (0x002de540: rejoin the formation, leave the fight stance, the next turn in 1-2 s and the
next fidget in 2-3 s) run, and its Process (the stand test above) from the update after. So there is no stand test,
no new FollowFormation and no turn while a left-over move runs. (Pushing a goal, by contrast, clears the actions:
Brain_PushGoal 0x0028d758.)
So a Warrior stands still anywhere within 9 m of the chief as long as a straight walkable line joins them, and only walks when the chief gets farther, turns a corner, or crosses a fence.
Fight mode (FollowPlayerGoal_IsFightMode 0x002de650, given the goal, the leader and the leader's target,
brain +0x124; only a follower whose brain is of type 3, a Warrior, can be in it). Confirmed (code) at
0x002de650; the tests in this order, the first that decides wins:
- The leader has a target: when the target's brain is not of type 3 (not a Warrior), on. When it is a Warrior,
on when the leader is a player whose brain
+0x2e4is set (a Warrior hitting back at a chief who hit him,WarriorBrain_OnHitByChief0x00306190). - Off when any of the 16 attack slots on the follower (brain
+0x1a4) is held by a player. - On when any player's brain
+0x2e4is set (the players at game state+0x224/+0x228). - On when a member of its gang has attackers (
Gang_AnyMemberHasAttackers0x00169658) and its own brain+0x152is not 0, else off.+0x152counts the brains that have put this one in their enemy lists:Brain_CountHostile(0x0028ef20) raises it, called byBrain_AddEnemy(0x0028d538, unless a player adds a player) and by the enemy scan (Brain_ScanEnemies0x0028b358, its call at0x0028b9e8);Brain_UncountHostile(0x0028ef30) lowers it, called by the enemy scan (its call at0x0028b7d0),Brain_UncountEnemy(0x0028d6a0) andBrain_UncountAllEnemies(0x0028ef48). A player's+0x152is raised and lowered the same way and also recounted every 300 updates, which overwrites it (The player).
So the leader's target alone turns it on (test 1): an AI follower is in fight mode whenever his player chief has locked on to anyone but a Warrior, whether or not anyone attacks.
In fight mode (confirmed (code) at 0x002de7c0): the stand distance is 10 m (squared 100) instead of the goal's 9 m,
the field of view (brain +0x12c) becomes 2π, and with no actions queued:
- The target: his own target when it still passes
Brain_ValidateEnemy(0x0028d358); else the human of his own enemy list (brain+0x164) nearest to him, if that human is up and within 20 m of the leader; else the leader's target when it is up: it is used for the steps below even when friendly, and only when it is not friendly (Brain_IsFriendly0x00290230) does it also become his own target (0x0028cfe0;0x002deae0to0x002deb08). With no leader's target, or a down one, none: the target is cleared and it goes on as out of fight mode below, from leaving the stance (the field of view stays 2π). - It enters the fight stance (
0x0022fe80). The range is the far melee range (brain+0x140), or the near one (+0x13c) when the target's actions are blocked or he is busy. - Beyond the range it only turns to the target (
TurnToPointAction0x002fe000, at the human's max turn). Within the range it queuesMoveToHumanAction(MoveMelee, Move-to-human) on him with the band range + 0.5 m to range + 1.0 m and a 3 s limit: since he is already inside the band's near edge, it backs off, walking (gait 2), to keep that distance. - It never attacks and never starts a move toward the target from fight mode: the band move backs off (and
MoveMeleemay side-step, or close in at gait 4 if the target gets beyond the band's far edge while it runs, Move-to-human steps 3-4). A Warrior fights only under the attack command (Automatic commands) or when hit (below).
Confirmed (runtime) at level34 checkpoint 5 (Scripts item 16): Ash and Snow stood
1.14-1.64 m from the standing player, turning to Furies that were 20.5 m (Snow) and 7.3 m (Ash) off when taken as
targets, never walking toward them, and backed off at gait 2 (1.92 m/s) once a Fury came within 3.6 m (Snow) and
4.1 m (Ash). (An earlier version of this page had the turn and the move the other way round, and the target as the
nearest attacker within 20 m of the follower.)
Out of fight mode the goal sets the field of view back to 110° (1.9199 rad) and clears the target, then
leaves the stance, may fetch a pickable object (the roll of the Warriors' pick-ups, every 30
updates), every 2-4 s turns as its mode says, and every 3-6 s, unless its own or the leader's brain +0x2d5 is set,
at 30 % (Warriors only while standing) queues a fidget (PlayFidget, 0x002f9f48). The modes (goal +0x18,
the init's last argument; confirmed (code) at 0x002de7c0):
| Mode | Every 2-4 s | Used by |
|---|---|---|
| 1 | turn to face the way the leader faces (a point one leader-facing ahead of himself) | event 19 of the follow tactic |
| 2 | turn to face the opposite way (watching the rear) | |
| 3 | turn toward the leader when more than 60° off, slowly (turn boost −8) | the follow tactic |
| 4 | look at the leader's target (LookAtAction) when he has one |
|
| other | nothing |
FollowFormation (type 0x33, FollowFormationGoal_Init 0x002dfba8, vtable 0x00541c50; arguments: arrival
distance (0.75 m from the follow code), leader, a turn-with-leader flag, time limit). Confirmed (code):
- Start (
0x002dfc30): saves human+0xf4and the start boost; a Warrior also gets human flags0x100,0x200(restored at End0x002dfe68). Then Resume (0x002dfcd0): joins the leader's formation, takes the follow point; when the leader runs (gait above 3) and the flag is set, turns to the leader's heading. On levels0xe,0x33,0x34and0x52a Warrior's+0xf4gets bit0x100. -
Process (
0x002e0088):- Done (2) when the leader is invalid, the last move failed (brain
+0x284), or he is no longer a follower of the leader's formation. Wait while the front action is of kind 6. Out of the fight stance when the leader is. - Every 6 updates (brain
+0x34a multiple of 6), only while it has no slot, the follow point (the human ahead, step 4) is written into the goal (+0x10) and into the running move (Ai_SetRunningMoveTarget0x0029f790: when the front action is a move,MoveAction_SetTarget, below). With a slot this step does nothing. - Every n updates (n a random 2-7 drawn at init) the leader's gait is noted; "the leader just stopped" = it was above 3 and is now below 3. Unless he just stopped, nothing more while a check time (now + 1000 ms) has not passed and actions are queued.
- The follow point, written to the goal's
+0x10and refreshed only here (once a second, or when the leader just stopped):Formation_GetFollowPoint(0x00295e38) finds the follower's entry (Formation_FollowerIndex0x002962f0, the entry whose handle is his, −1 none) and reads its slot (Formation_GetSlotPoint0x00296358, entry+0x04; it replans first when the formation's "replan" byte+0x274is 1). With a slot (≥ 0) the point is that slot's world point in the current set (Formation_GetMemberPoint0x00296410: the threes16at slot+0x06/+0x08/+0x0ain 1/16 m, w = 1) and it returns 1; radius = the arrival distance (0.75 m). With no slot (−1) the point is the position of the human it queues behind (Formation_GetAhead0x002963b0: replan first when+0x274is 1, then the handle at entry+0x08, and the leader (+0x244) when that handle no longer resolves) and it returns 0; radius 2.5 m, and from here on that human stands in for the leader in steps 6 and 7 (his speed and his+0x2d5). Confirmed (code) at the addresses cited. A slot is given only by a plan, and only a usable slot is given (below), so "a slot" here means a usable one. - Can it get there (
Nav_WalkDistanceTo0x00247a50; Ghidra's old nameNav_StraightDistancewas wrong): the straight distance when a walkable line from the human's nav point reaches the point; otherwise, when both ends have an area and both areas are on the graph (or it is one area), the length of a route planned with the brain's mask (Route_Findbetween the two ends' nearest reachable nodes, thenPath_LengthFrom, the route freed at once). It fails only when the point has no area or no route exists. On failure a counter (+0x2a) is set to 1 and it waits; each update the counter counts up, and on every 15th count, when the human has no nav polygon or no node reachable near the point, a breakable object within 1.5 m is smashed (VandalizeItempushed); at 30 it tests again. A success clears the counter. - The leader (or the human ahead) standing (his speed gives gait 0) and the walk distance below the radius: done (2). With the leader standing, the move's option bit is 0; otherwise 1.
- Beyond 0.2 m (walk distance): with no move at the front of the queue, a new move action to the point
(
MoveAction_InitEx0x002fbae0, which copies the point into the action's+0x10; radius 0.1 m at a slot, else 2.5 m; the speed below as+0x40, never below the walk). With a move already running, only its speed (+0x40,MoveAction_SetSpeed0x002fbbe0) and radius (+0x2c) are written here, not its point. The move plans its own route (Path planning), so a follower crosses fences, climbs and drops by the route's links. A new move by a follower who has attackers (brain+0x1a4) and a clear+0x2d5also turns him to face the point and raises his start boost by one.
The distance
dused for the speed and the radius tests is the walk distance from step 5 (the route's length when the point is round a corner or over a fence), not the straight one. - Done (2) when the leader is invalid, the last move failed (brain
-
The speed (
FollowFormation_PickSpeed0x002dfef8), withdthe distance,vthe leader's speed capped at 1.25 × the sprint speed: within the arrival distance,v. Beyond it,f= 0.1 × (d− arrival distance); whenv< 2 m/s,f+= 4 ×f× max(v− walk speed, 0);f+= 1 whenv> 2 m/s, the leader's+0x2d5is set, or the follower has attackers. Gaitg= run (4) beyond 8 m or with the leader's+0x2d5, jog (3) beyond 4 m, else walk (2). Speed = max(speed ofg,v) ×f, clamped to [walk speed, 1.25 × sprint speed]. So a follower far behind runs, near his slot matches the leader, and slows to a walk on arrival. - What limits a follower's speed and destination (confirmed (code) at the cited addresses unless marked). The
speed's only caps are the clamp above (1.25 × the follower's own sprint clip speed, 12.81 m/s for Vermin and
Rembrandt) and the control's 25 m/s (Characters); the move action may lower it
further at corners (The corner speed) and a climb leg runs at gait 4. Nothing caps it for
a straight move, and the follower really moves that fast: confirmed (runtime), Vermin's and Rembrandt's speed
+0x1acreached 12.807 m/s and held it on a 75 m run, on a route and on straight moves alike. What keeps a follower on the ground is the destination and the line, not the speed:- the slot point is put on the ground when the formation is planned (a ray from 1.9 m above, 5.9 m long, so to 4.0 m below; the point goes 0.2 m above the hit; with no hit it keeps the plan point's height) and is usable only when the leader has a walkable line to it (Formations);
- a follower with no usable slot aims at the human ahead of him, who stands on the ground;
Nav_WalkDistanceTofails when the point has no area (none within 3.6 m below it, Path planning) or no route reaches it, and then no move is made (step 5);- the move goes straight only when the walkable line reaches the point from where the follower stands, else by a route (Path planning), and every 31 updates its current aim must still pass the walkable line or the move ends and the goal plans again (The move action step 5);
- the running move's point follows every plan (above), so it is at most one plan old.
Where a running move's point comes from¶
Confirmed (code) at the addresses cited; confirmed (runtime) below. A move keeps the point it was made with until one of these writes it:
- Every formation plan. At the end of
Formation_Plan(0x002956d0),Formation_RetargetFollowerMoves(0x00295250; Ghidra's old nameFormation_SetFollowerSpeedswas wrong) takes each follower whose top goal isFollowFormation(0x33), or type0x69or0x73, and whose front action is a move (kind 1): the move gets the follower's new follow point (Formation_GetFollowPoint, after the slots were reassigned) and its radius becomes 0.1 m with a slot or 1.0 m without (step 7 puts 2.5 m back on its next pass). A plan runs when the leader has just stopped (his speed+0x1acfalls to 0.005 m/s or below, which sets+0x274), at the plan time (1000 ms after the last plan at gait 2, else 2000 ms), or once the leader is 1 m (3D) from the spot of the last plan. So a follower's destination is never older than one plan: about 1 to 2 s while the leader moves, and at once when he stops or moves a metre. - Every 6 updates without a slot (Process step 2).
MoveAction_SetTarget(0x002fbbe8) ignores a point within 0.0707 m of the old one (distance² ≤ 0.005). Otherwise it stores the point; with a route, when the route's last waypoint has a walkable line to the new point (Nav_IsConnected0x0024dee8), the route is kept; else the move is aborted (MoveAction_Abort0x002fc560, which callsRouteState_Stop0x0029aa20with 1): unless the route state is 1, 2 or 3 (in a jump, drop or fence climb), the route is freed and the move is restarted (its "started" byte+0x06and the abort bit cleared, so its Start runs again on the next update and asks for a straight line or a new route to the new point); in a jump, drop or fence climb the route is kept and the abort bit stays set, so the move ends on its next update (the move's first test is the abort bit; the leg's clips play on) and the goal makes a new one.
Runtime, PCSX2 2.9.94, level80 (the state of the checks below), the crew standing round the player in the
street; the player teleported 20 m away, then, 35 updates later with both followers running at 12.81 m/s on routes,
teleported another 9.7 m: on the next update both moves held the new slot points (Vermin's from (−176.88,
181.12) to (−186.12, 178.44)), while the goals' own +0x10 kept the old ones for another 29 updates until their
once-a-second refresh; at a later plan the two slots swapped and each running move took the other's point at once.
Both arrived, stood and went back to FollowPlayer.
Confirmed (runtime), PCSX2 2.9.94, level80 at the "Let's Go" hint (a state of the scripted-pad and right-stick
patches), Cleon the player, Vermin and Rembrandt on a landing 19 m away and 4.4 m up; the follow command given with R2
and the right stick up, then the player driven by the left stick; one sample per update:
- The run-in. Vermin planned a route down the stairs (route state
+0x04set) and asked for 12.81 m/s, the clamp 1.25 × sprint; his speed climbed through gait 2, 3 and 5. Both stopped 1.8 to 2.4 m from the player. - The stairs (2026-10-20, PCSX2 2.9.129, the same moment from a state of no patches, the order given with R2 and the right stick at 0 for 20 frames, one sample per update). The stairs run straight in plan from the landing (−192.9, 108.3, 5.63) north to the street, with a half landing at z 2.43 for y 114.8 to 118.7; the landing, both flights and a patch of pavement at their foot are one path area (polygon 283, 29 vertices, ground byte 8), the street another (95, ground 2). Both Warriors ran straight down both flights on the ground (z 5.63 → 2.43 → 1.23 by y 121.7) at 12.81 m/s. Each ran to his slot from his first move: Vermin's (−192.1, 128.4) lies in area 283, and the straight line to it crosses one of 283's own edges, so his move made a one-node route (node 793, the stairs' foot, its last node area 283); Rembrandt's (−194.6, 128.6) lies in the street, his line passes from 283 into 95 at the stairs' foot and he made no route. So "a route down the stairs" is Vermin's slot, not the stairs: without a route the straight line already follows them. Coney's crew takes the same flights (z 2.43 at y 115 to 118.7, 1.23 by y 121); it differed in going first to the leader's position (the replan byte, Formations).
- Walking (stick 50 % straight down, the player at 1.63 m/s): both walked at their own walk speed (1.65 and 1.63 m/s), 0.9 to 2.5 m from him: the formula's floor, since f is far below 1 so close.
- Running (stick 100 %, 7.80 m/s): neither moved while within 9 m. Vermin passed 9 m at update 366; Rembrandt started at update 381 (12.2 m) and Vermin at 386 (13.9 m), both asking 12.81 m/s; a turn action of mode 3 was queued in between. When the player stopped, the asked speeds fell to 9.48 and 7.62 m/s (gaits 5 and 4), a jog of 1.94 m/s about 5 m out, then the walk; they stood 1.6 and 2.9 m from him.
- A climb. With the player put on a crate stack 2.2 m high (the
0x80ledge links of nodes 860-865), both planned routes over the ledge: Rembrandt's route state went to 4 (a0x80climb) 1.6 m from the player and he stood on the stack 0.7 m from him; Vermin's route was held (route state+0x16= 1) while Rembrandt had the shared node, then climbed (state 4) on his turn. Two seconds later the formation's replan put Rembrandt's slot on the ground: he dropped off (route state 2) and climbed back up 7 s later. - Why they climb: the stand test fails at the crate stack (2026-10-12, the same state; the follow command given
with R2 held and the right stick at 100 % up for 15 updates, then the player teleported). With the player on the
street at (−193.0, 130.4) both Warriors walked up and stood (
FollowPlayeron top, no actions) 4.5 and 6.3 m from him. The player was then put on the crate stack's top at (−190.9, 132.9, 3.4): on the next update Rembrandt, 7.9 m away in 3D (inside 9 m, both+0x2d5clear), pushedFollowFormation(0x00541c50on top), so the walkable line from the street to the top failed; Vermin (9.7 m) pushed it one update later. Rembrandt's move planned a route (route+0x04set) and climbed by a0x80link (route state 4); he stood on the top 100 updates later, and Vermin climbed after him. Corroboration from the disc's path data (level80_2, counted by script, not committed): the top is its own area (one polygon, 2.15 × 1.40 m, owning nodes 860-865), and the street's outline leaves the stack out; the segment from Rembrandt's spot to the player leaves the street's polygon at t = 0.78 and enters the top's at t = 0.91, 0.98 m apart in plan, far over the 0.02 m the walkable line allows (Path planning), so the line is refused whatever the heights. - Crossing a fence (2026-10-12, the same state; the crew standing south of the kind-8 fence pairs of nodes
396-403 at y ≈ 199.9, the player teleported over it). Both Warriors have human
+0xe0= 7, so flag0x2, fast climbers. In every run both pushedFollowFormationon the update after the teleport (the fence's hole refuses the line), planned routes over the kind-8 pair and climbed (route state 3); no other rule takes them over.- Player 5-7 m away (
FollowFormation_PickSpeedgives the walk, 2.4-2.5 m/s): each walked to the climb's near waypoint and climbed from the walk (gait 2 when the climb started; the early climb needs a gait above 3); the climb held them about 80 updates, then they walked to their slots: about 150 and 210 updates from the teleport to standing. - Player 14 m away (asked 8.2-8.4 m/s): Vermin ran (gait 4, then 5) and started the climb from the run
2.85 m before the far waypoint (the early climb,
Route_TryClimbNear), was over it about 18 updates later and walked on; Rembrandt was slowed to the walk by the corner speed (his move's slow corner speed+0x44= 1.63, the walk) before the near waypoint, so he arrived walking and climbed from standing (78 updates). Neither ever pressed against the fence for the 31 tries of a failed climb. Stick: none; the player was teleported.
- Player 5-7 m away (
Speculative: the levels whose level-table id is 0xe, 0x33, 0x34 or 0x52 (the Warriors' 0x100 mask while
following) are level14, level51, level52 and level82, if the id is the level's number; all four exist on the
disc (as level14_2 and so on). Not checked.
Automatic commands¶
What turns a following crew into a fighting one is the chief's own brain. PlayerBrain_Update (0x003035d8) calls
WarChief_AutoCommand (0x00303988) on every update with the chief's target (brain +0x124). For a war chief, while
game state +0x431 is set (InitLevel sets it; WCEnableAutomaticSwitching(false) clears it, which only the Rumble
arena scripts call: brawl, kinghill, royal, survival and tagbt, read from the compiled scripts; the binding
(0x00374a00) reads an absent argument as true and nil, the scripts' false, as false), it unlocks his menu every 10
updates and then, by his last command (confirmed (code)):
- 0, follow: when he is mugging (
0x100), tagging or in a player mode 2 or 3 (record+0x46; not traced), defend at once. Otherwise, when his target is within his far melee range (+0x140, 5 m) and is not a Warrior hitting back at him (+0x2e5), and he has attackers (+0x1a4), attack after 1.5 s (defend while he grabs from the rear,0x80). Every 20 updates, next to a store flag whose group still has objects, steal (5). - 1, attack and 2, defend: back to follow 1.5 s after no member of the gang has an enemy (
0x00165a20) and his brain+0x2d4is clear (defend also waits until he is free, not cuffed and not mugging). - 5, steal: back to follow when he is more than 15 m from the store flag.
All of these are forced dispatches. So in the original a crew under follow stands round the chief, turns to face his
enemies, and joins the fight with FollowAndAttack 1.5 s after the chief, within 5 m of his target, is attacked.
Being attacked alone is not enough: the attackers count only while the chief has a target of his own in range. Coney
reads the target his fighter keeps (human +0xc8, which Human_SetTarget 0x00226c70 also gives his brain's
+0x124); until 2026-10-20 it stood in his nearest attacker for it, and so attacked for a chief who had none, as at
level80 checkpoint 4, where the Destroyers come for Cleon before the script's attack order (ai::CrewOrders).
Under attack, defend or follow, Brain_PushFightGoal pushes nothing (Tactics): the tactic decides.
Formations and follow slots¶
A leader's formation hangs from human +0x1a4 (0x0021d428, made on first use); brain +0x1a4 is the
attack-slot list. A pool of 42 formations of 0x280 at 0x006ceaf0; Formations_Update (0x00293c68, step 2 of
Humans_Update) runs 0x002956d0 on each in use. Confirmed (code):
| Offset | Meaning |
|---|---|
+0x000 |
4 slot sets × 9 slots × 12 bytes: s16 x, y, z offset, then s16 x, y, z world point, in 1/16 m |
+0x1b0 |
9 followers × {handle, s32 slot (−1 none), the handle it queues behind} |
+0x220 / +0x230 |
the leader's position / forward at the last plan |
+0x244 / +0x248 |
the leader / the next plan time |
+0x24c |
per set × 9: slot usable |
+0x270 / +0x271 / +0x272 / +0x273 |
current set / slot count / followers allowed in slots / followers |
+0x274 |
replan: 1 asks for a plan before the next slot is read |
- Init (
0x00294ad8): 6 slots, 6 allowed, set 0.BrSetNumFollowSlots(leader, n, n2)sets+0x271= n and+0x272= n2 (n when −1);BrSetFollowSlotSetsets+0x270;BrSetFollowSlot(leader, slot, {x, y}, set)stores (x, y, 0) in 1/16 m in that set. Each replans at once. - Joining (
0x00295f28) takes a free follower entry (handle, slot −1, queued behind nobody), adds 1 to+0x273, sets+0x274= 1 and sets the follower's brain+0x212= the formation's index; leaving (0x00296028) sets it to −1 and removes the entry (Formation_RemoveFollowerAt0x00294c98:+0x274= 1,+0x273less 1). Confirmed (code) at the addresses. - The replan byte
+0x274: reading a slot point (Formation_GetSlotPoint0x00296358), whether a slot is usable (Formation_IsSlotUsable0x00296138) or the human a follower queues behind (Formation_GetAhead0x002963b0) first runsFormation_Planwhen it is 1, so a follower who joins takes his slot on the update he joins:FollowFormation's Resume joins and at once reads its follow point, which plans.Formation_Plansets it itself when the leader has just stopped (his speed+0x1acat most 0.005 m/s now, at least 0.005 at the last call, kept at+0x240), and clears it when it plans. Confirmed (code). At runtime (PCSX2 2.9.129,level80at "Let's Go", follow order given with R2 and the right stick up): Vermin's first move already ran to his slot (−192.1, 128.4, 1.4), and Rembrandt's to (−194.6, 128.6, 1.4); Coney, which planned only at the next plan time, sent both to the leader's own position for the first 2 s and stopped them 3.4 m short of him. - Planning (
0x002956d0; nothing while+0x273is 0), when+0x274is set, at the plan time, or once he is 1 m from the planned spot: next plan in 1000 ms at gait 2, else 2000; the plan point is the leader's position, 2 m ahead while he runs (0x00223c10); the slots' world points are worked out (0x00294f38: the offset in 1/16 m, turned by the leader's orientation (0x004dacc0, so x is to his right and y ahead), added to the plan point and dropped to the ground with a ray (WorldManager_RayCast0x0040db88) that starts 1.9 m above it and runs 5.9 m straight down, so it finds ground down to 4.0 m below; on a hit at distance t the point becomes the plan point + (t − 2.1) down, i.e. 0.2 m above the hit; with no hit it keeps the plan point's height; a slot is usable when the leader has a walkable line to its point (Human_CanWalkStraightTo0x002221e0from the leader), up to+0x272of them; no slot is usable at all while the leader is airborne (state0x1c00000000, jumping or falling,0x00227f90) or at detail level+0x3333 or more: every follower then queues behind the human ahead; followers whose human is gone leave (0x00294e98); each usable slot takes its nearest unassigned follower; followers left over queue nearest-first behind the slotted ones (or the leader); up to 3 passes swap pairs whose paths cross.
GoalTrackHuman (vtable 0x00541d10, constructor 0x002df250: +0x10 target, +0x14 distance, +0x18 a retry
counter): Start (0x002df288) joins the target's formation (0x0021d428, 0x00295f28) and turns the fight stance
on, End leaves it. Process (0x002df3c0): no target → done; every 30 updates a 1.5 s head look-at, every 40 the
actions cleared; with no actions, a failed move (brain +0x284) counts up the retry counter; the goal point is its
slot's world point (0x00296358, 0x00296410), or its own position when it has no slot (so it never walks);
beyond distance, once the counter reaches 31 it and +0x284 are reset, and with the counter at 0 a move action to
the point (gait 2, arrival radius distance, option 1, no delay, facing the target); otherwise a turn-to-point when
more than 15° off the target. Confirmed (code); this corrects the binding's "walks to the target
when it has no slot".
Coney's implementation¶
The gang and its update are repo:src/ai/gangs.h, the spawners repo:src/ai/spawners.h, the tactics
repo:src/ai/tactic.h and its tactic_*.h files with the scripted ones in repo:src/ai/story_tactics.h, the
formations repo:src/ai/formations.h, the Warrior commands repo:src/ai/set_command_action.h and the boss goals
repo:src/ai/boss_goals.h. What Coney does differently and what it leaves out is on
AI humans: Coney's implementation; the sections above say where Coney differs on their subject (the
attack on sight, the follow).
Open questions¶
Each open point sits with its subject above (the attack on sight's fix list, the scripted tactics Coney stands in for, the boss program's unused operations); the reimplementation units still to do are on AI reimplementation plan.