Skip to content

The script system (Lua 4.0)

Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). Runtime observations were made in PCSX2 2.9.94 (2026-10-04) by reading the live Lua state's global table over PINE, and say so. The disc-side survey read the NTSC-U disc's WAD and is reported as names, counts and layouts only.

Purpose

What runs the game's Lua scripts: when the Lua state is made and remade, which libraries and bindings it has, which scripts run in which order and in which state, how C++ calls back into Lua, and what the bindings return to a script. It is what an implementer needs to run enum_preload.lua, the config_preload*.lua scripts, global.lua and a level script (level100.lua for the front end) the way the game does.

In one paragraph: there is one Lua 4.0.1 state, owned by a script-system object. It is made at start-up and remade every time a level is unloaded. It has the standard string, base and math libraries (no io), the tolua support table and 956 game bindings (every one in the script bindings reference). Script errors are silent (_ERRORMESSAGE and _ALERT do nothing). The legal screen runs enum_preload.lua, then config_preload.lua, config_preload2.lua and config_preload3.lua; loading a level runs global.lua and then <level>.lua, all in the same state, so a level script sees the preloads' globals and global.lua's helpers. C++ calls Lua functions by name (dotted, such as Menu.onStart), never by reference.

Original structure

Address Name Role Evidence
0x00512b04 g_ScriptSystem the one script-system object (pointer) confirmed (code)
0x00356390 ScriptSystem_Create allocates 0x28 bytes, builds the object; called by Game_InitializeSubsystems (0x00160fd0) and UnloadLevel (0x00160bac) confirmed (code)
0x00356450 ScriptSystem_Destroy deletes the object and clears the pointer; called by UnloadLevel just before the create confirmed (code)
0x003564d8 ScriptSystem::ScriptSystem opens the Lua state and the libraries, registers the bindings confirmed (code)
0x00356610 ScriptSystem::~ScriptSystem closes the state, frees the schedule confirmed (code)
0x0037d420 RegisterBindings the tolua registration of every game binding confirmed (code)
0x00383f38, 0x00384398, 0x00384480, 0x003842c0 CfgChar, CfgObjectGroup, CfgGang, CfgSpeedClass four bindings registered by the constructor itself confirmed (code)
0x00334478 luaV_execute the Lua 4.0 interpreter confirmed (code)
0x00161218 RunPreloadScripts the legal screen's preload lists confirmed (code)

Names are ours. The vtable is at 0x005449b8; its slots are below.

The object

Offset Field Evidence
+0x00 vtable (0x005449b8) confirmed (code)
+0x04 lua_State* confirmed (code)
+0x08-+0x14 the schedule: a vector of 0x18-byte entries kept as a min-heap on the due time confirmed (code)
+0x1c set when the last name looked up had a : (a method: the table is pushed as self) confirmed (code)
+0x20 the per-frame update function's name (interned string), or 0 confirmed (code)
+0x24 the time of the next forced garbage collection (now + 2000 ms) confirmed (code)

Vtable slots

The function behind each slot, by the byte offset of its function word (each slot is an adjust word and a function word; callers read the function at this offset and the adjust word 4 bytes before it, so the update slot +0x14 has its adjust word at +0x10, the offset Boot once gave).

Slot Function What it does Evidence
+0x14 0x003566d8 update(dt): runs the scheduled calls whose time has come (below), then the update function with dt × 1000 (ms) as its argument, then a garbage collection every 2 s confirmed (code)
+0x1c 0x00356990 collect garbage now (lua_setgcthreshold(L, 0)), and push the next forced collection 2 s away confirmed (code)
+0x24 0x003569d8 level entry: run global.lua, then <name>.lua confirmed (code)
+0x2c 0x00356a78 run a string (lua_dobuffer) confirmed (code)
+0x34 0x00356af8 run a file by WAD name: load it through the file manager, run it as a buffer named after the file, collect garbage confirmed (code)
+0x3c 0x00356ad0 run a buffer confirmed (code)
+0x44 0x00356c58 run each file of a NULL-terminated list through +0x34 confirmed (code)
+0x4c 0x00356e08 find a function by name and push it: the name is split at . and : (at most 32 characters per part); a : also pushes the table as self confirmed (code)
+0x54-+0x84 0x00356f70, 0x00356fc8, 0x00357028, 0x00357060, 0x00357098, 0x003570f0, 0x00357138 push an argument: unsigned, object handle (as its index, 0x00390068), short, int, unsigned, string, VolumeBox usertype confirmed (code)
+0x8c 0x00357188 call with n arguments (plus one for self) confirmed (code)
+0x94 / +0x9c / +0xa4 0x003571b8 / 0x003572e8 / 0x00357430 schedule a call of a named function after a delay in ms, with 0, 1 or 2 number arguments confirmed (code)
+0xac 0x00357588 flush scheduled calls: all, or those whose name matches confirmed (code)
+0xb4 / +0xbc 0x003578d8 / 0x004f1db0 set / get the update function's name confirmed (code)
+0xc4 0x00357910 pop a number confirmed (code)
+0xcc 0x00357958 intern a name (luaS_new + fix), so a stored callback name is never collected confirmed (code)
+0xd4, +0xdc 0x00357988, 0x00357bb8 do nothing (the "mode switch" around the preloads is empty in this build) confirmed (code)
+0xe4 0x00357bc0 returns 0 confirmed (code)

Two empty functions are called on the way: 0x00357ba0 by the collection slot +0x1c and 0x00383f30 by the destructor (confirmed (code); leftovers of a debug build, inferred).

Data

Libraries

Library Opened by Contents Evidence
string 0x00331a58 12 names: strlen strsub strlower strupper strchar strrep ascii strbyte format strfind gsub strformat (strformat is format, ascii is strbyte) confirmed (code)
base 0x00326548 the 33 Lua 4.0 base functions, _VERSION = "Lua 4.0.1", and the 4.0 compatibility names foreachvar nextvar rawgetglobal rawsetglobal confirmed (code); compat names confirmed (runtime)
math 0x0032c628 23 functions, PI, and the pow tag method for ^ confirmed (code)
tolua 0x0037d420 the table tolua, the classes M_Vector4 (x y z w) and M_Quat (i j k r), the variables NilHandle and NilSoundHandle confirmed (code)

There is no io library. dofile and dostring exist (base library) but the scripts use the binding doFile. random is a game binding (registered after the math library, so it replaces math's): random(a, b) returns a whole number in [a, b], both ends included, from the game's own generator (0x00386488 → 0x003353f0): the next entry of a fixed table of 1,024 32-bit numbers (0x005117e0) taken modulo b − a + 1 (unsigned) and added to a. The generator's state is only the table index at 0x006eb880, advanced by 1 (masked to 10 bits) on every draw, zeroed by a static initialiser (0x00335608) and never seeded; many C++ callers draw from the same index. Confirmed (code). Details: World flags.

Runtime check: at the boot movies the global table holds exactly 1,034 entries: the 956 bindings and 78 library and tolua names. confirmed (runtime).

Bindings

RegisterBindings (0x0037d420) makes 962 function registrations under 952 names: ten names are registered twice (tolua overloads; the second registration wins in Lua 4, so those names have the second function): CfgScrFx, FlagNetTraverse, GoalManWeaponPile, GoalPathBlocker, HUDSetInstArrowLocation, QueueMotionBlurEffect, ScenePlay, SetLight, SoundLoopMusicTrack, SoundPlayMusicTrack. With the constructor's four that makes 956. confirmed (code); the count confirmed (runtime).

The first registrations are reached only from the second: each second wrapper checks its argument types and, when they do not match, calls the first (tolua's overload fallback). Confirmed (code); each form's arguments are in the binding's overloads entry.

Name First wrapper Second wrapper Evidence
CfgScrFx 0x0036ae60 0x0036b020 confirmed (code)
FlagNetTraverse 0x0037a8d0 0x0037a968 confirmed (code)
GoalManWeaponPile 0x00361a50 0x00361b80 confirmed (code)
GoalPathBlocker 0x00362250 0x00362360 confirmed (code)
HUDSetInstArrowLocation 0x0036fec8 0x0036ff40 confirmed (code)
QueueMotionBlurEffect 0x0037be18 0x0037be90 confirmed (code)
ScenePlay 0x00367810 0x00367a20 confirmed (code)
SetLight 0x0037bfb8 0x0037c348 confirmed (code)
SoundLoopMusicTrack 0x003712d8 0x00371348 confirmed (code)
SoundPlayMusicTrack 0x003711d0 0x00371230 confirmed (code)

The tolua types and variables (confirmed (code)): 0x00357bc8 declares the usertypes M_Vector4, M_Quat, WorldPath and SoundHandle. The fields have one getter and one setter each, at offsets 0, 4, 8 and 0xc: M_Vector4's x y z w (0x00357c28/0x00357c90, 0x00357d28/0x00357d90, 0x00357e28/0x00357e90, 0x00357f28/0x00357f90) and M_Quat's i j k r (0x00358028/0x00358090, 0x00358128/0x00358190, 0x00358228/0x00358290, 0x00358328/0x00358390). The global NilHandle is the number at 0x006ebd30 (get 0x0036d418, set 0x0036d478) and NilSoundHandle the SoundHandle at 0x00598690 (get 0x0036d4e0, set 0x0036d530). A script can assign either.

Workers of the script and object bindings (each called only by its binding's wrapper unless the row says otherwise; confirmed (code)):

Address Name Binding: what it does Evidence
0x00357a68 Script_PreloadFile preLoadFile: the path is the name itself when it starts ~/ (stripped) or the device says so (0x001458c0), else the current level's directory (level record 0x0051489c + 0x14e8 + 0x84 × index) plus the name; requests the file with the completion 0x00356d00 and the interned callback confirmed (code)
0x00386370 Script_CollectGarbage gc: slot +0x1c confirmed (code)
0x003863a0 Script_RegisterUpdate RegisterUpdate: slot +0xb4 confirmed (code)
0x003863d8 / 0x00386410 Script_ScheduleFunc / Script_ScheduleFuncArg1 ScheduleFunc / ScheduleFuncArg1: slots +0x94 / +0x9c confirmed (code)
0x00386450 Script_FlushScheduledFuncs FlushScheduledFuncs: slot +0xac confirmed (code)
0x003864b0 GameTimer_GetMilliseconds GetGameTime confirmed (code)
0x00386308 Script_SetObjZoneMsgHandler SetObjZoneMsgHandler: ObjZone_SetMsgHandler on the object-zone manager (task manager +0x840) confirmed (code)
0x00386340 Script_SetGeneralCarMsgHandler SetGeneralCarMsgHandler (Cars) confirmed (code)
0x00385950 Object_GetTypeBits GetRTTI confirmed (code)
0x00385918 Object_GetHitpoints GetHitpoints confirmed (code)
0x003859a0 Obj_GetName GetName: the object's name (its vtable +0x14), a fixed default string for a bad handle confirmed (code)
0x003859f0 Obj_GetTypeName GetObjectName confirmed (code)
0x00385a50 Obj_GetPosition GetPosition: a human's position, else the object's, else zero, in one static vector (0x006ebd20) that the next call overwrites confirmed (code)
0x00385b08 AreaEffect_Spawn SpawnAreaEffect (also 0x002a05a4) confirmed (code)
0x00385b90 IsHumanHandle IsAHuman confirmed (code)
0x00385c90 Obj_SetAxisAngle OrientObject confirmed (code)
0x00385ea8 Obj_TestDistance TestDistance(d, a, b): true when a and b are strictly nearer than d; false for a bad handle confirmed (code)
0x00385f60 / 0x00386010 WalkingDistance / Obj_PathExists WalkingDistance / PathValid confirmed (code)
0x003858b8 ScriptHandlers_ClearAll no binding: clears all 26 message handlers of a handler component (slot +0xc with none, messages 0-25); from a gang's and a human's teardown (0x0016ac10, 0x00233f48) confirmed (code)
0x003864e0 Script_StrDup no binding: a heap copy of a string, used by the GUI confirmed (code)
Family (name prefix) Count What for
Hu 174 one human (character): state, flags, speech, animation
Cfg 117 configuration, called by the config scripts and levels
Goal / Act / Br / Tactic 67 / 7 / 32 / 40 AI goals, actions, brains, gang tactics
HUD 61 the in-game HUD, radar, objectives
Gang 57 gangs
Sound, Snd, ambient sounds and emitters, sound slots 51 sound and music
Cam 41 cameras
UM_, Stat, Inv, SSMC_ 37 unlockables, statistics, inventory, save sequences
Scene 15 cutscenes
other 257 world objects (Obj, Car, doors, flags, paths, particles, weather), scheduling, pad and message handlers, level flow, menus, lighting

Counts are by name prefix (a name goes in the first row that matches); 956 in all. confirmed (code). Every binding, with its arguments, result, effect, evidence and usage counts, is in the script bindings reference, generated from research/bindings/.

Survey (disc, counts only): the 467 compiled Lua chunks in the WAD reference 802 of the 956 bindings by their global names; 154 are never used by any script. The scripts on the boot-to-front-end path (enum_preload.lua, the three config_preload*.lua, config_strings_en.lua, global.lua, level100.lua) use 266 distinct bindings, at least 37 of them for their result. The first mission's four scripts (level99.lua and its _combat, _lesson1 and _lesson2 scripts) use 175; the two paths together need 366. An earlier count of 792, 256 and 171 traced calls through the stack and missed calls made inside for loops (CfgHUDMessage in config_preload2.lua, for one); counting references does not. Per-binding counts are in the reference.

Argument and result conventions

Every binding takes its arguments with the tolua helpers below and checks no types: a missing argument reads as the default (0, false or NULL), a wrong type as whatever Lua's conversion gives.

Helper Address Reads / pushes Evidence
number 0x004091c8 argument n as a number, default if n is past the top confirmed (code)
boolean 0x00409318 nil → false, else number ≠ 0 confirmed (code)
string 0x00409220 argument n as a string confirmed (code)
table field 0x004093a0 / 0x00409438 t[i] as a number / string confirmed (code)
push number 0x004094e8 a number confirmed (code)
push boolean 0x00409588 nil for false, the number 1 for true (Lua 4.0 has no booleans) confirmed (code)
push string 0x00409508 a string, nil for NULL confirmed (code)
push usertype 0x00409538 a tolua object (M_Vector4 and others), nil for NULL confirmed (code)
write back 0x004095d8 / 0x00409648 stores a number / string into a table argument (bindings that fill a table they were given) confirmed (code)

The scripts follow the same convention: global.lua sets true = 1 and false = nil before anything else.

How a binding is called

Every call from a script into a C binding goes through Lua 4.0's callCclosure (0x00328a30, (L, closure, base)): the closure's C function is its word +0x0 and its upvalue count the half-word +0xe; the arguments are the stack slots from base, 0x10 bytes each (the type tag at +0x0: 1 nil, 2 number, 3 string, 4 table, 5 function; the value at +0x8, a double or a pointer, a string's characters at +0x10 of its TString). It pushes the upvalues, calls the function with L and returns the results' base. Confirmed (code), and (runtime) by the event-call hook (Differential playthroughs), which logs the function and the first two slots there: slots past the call's arguments hold whatever the stack held, so a reader trims them by the binding's arity. The engine's calls the other way, into a script function by name, all look the name up first with FindFunction (0x00356e08, slot +0x4c above; a1 the dotted name), where the event-callback hook logs them.

Bindings the front end and the script system depend on

The front end needs the results of GetPlatform, isRelease, GetLanguage, GetCurrentLevelIndex, GetLevelId, GetDifficulty, GetProfileDifficulty, GetCheckPoint, UM_IsLevelComplete, ToInt, ScenePreload, GetPTank, ObjSpawn and CameraCreateLocked (the last four return handles); a script runs other scripts and schedules calls through doFile, preLoadFile, ScheduleFunc, ScheduleFuncArg1, FlushScheduledFuncs, gc, PadSetHandler and ShowProfileManager. Their arguments, results and addresses are in the script bindings reference. Two facts matter for the order of things: doFile runs its script synchronously through slot +0x34, and preLoadFile loads asynchronously and runs the script, then calls the callback, when the file arrives (completion routine 0x00356d00). ScheduleFuncArg1 takes (name, arg, ms): the number comes before the delay (slot +0x9c, 0x003572e8, adds its last argument to the game time). confirmed (code).

Scheduled calls

An entry of the schedule (0x18 bytes): due time (ms on the game timer 0x0050b734 + 0x48), the function's interned name, then two (flag, number) pairs for up to two arguments. update pops every entry whose time has come, looks the name up (slot +0x4c) and calls it with its arguments; a name that does not resolve to a function is dropped silently. confirmed (code).

The mission stopwatch

One countdown (or count-up) timer for the whole game, the object at *0x0051504c, set by W_SetStopWatch (0x004235f0) and run by W_StartStopWatch (0x00423648 → 0x004233a8). Confirmed (code):

Offset Type Field
+0x00 u32 game-timer reading at the last step
+0x08 / +0x0c s32 current time / target time, ms
+0x10 float rate the elapsed time is multiplied by: 1.0 from the constructor (0x00423358); other writers not traced
+0x14 u32 running (W_StartStopWatch's argument; the update clears it at the target)
+0x18 / +0x1c u32 / char* shown on the HUD / the label drawn before the time; read by the HUD's stopwatch text (StopWatchHud_UpdateMinutes, 0x001cd5e8, and 0x001cd748; HUD); written by W_ShowStopWatch (0x00423670 → 0x004235e0)
+0x20 char[32] the callback's name, kept as text (empty for none)
+0x40 / +0x48 s32 warning window (ms) / time of the last warning beep; W_SetStopWatch sets 0 / 1000

Each frame of play (mode 1 step 1, 0x004233f8), while running and the game timer has advanced by d ms: the current time moves d × rate toward the target (down when the target is below it). On reaching or passing the target it is clamped there, the watch stops, and the callback is found by name (slot +0x4c, dotted names work) and called with no arguments. While counting down within the warning window, a beep (sound 0x0058b9f0) plays at most once a second. Confirmed (code) at 0x004233f8. The HUD's display of it (W_ShowStopWatch) belongs to the HUD.

Coney (src/warriors/stop_watch.h, stepped by src/gamemodes/player_frame.h before the scripts' frame): the rate is 1 and the warning beep is reported but plays no sound yet.

Message handlers

Game objects (humans, flags, boxes, doors, props) talk to scripts through messages: a record whose +0x20 is the message number and +0x24 the object it is about, with +0x00, +0x04 and +0x11 as extra values. SetMsgHandler (0x00386298 → 0x003860b8) gives the object a handler component (vtable 0x00544b98, made on demand by 0x003848c8) and stores the interned callback name in its slot for that number: 26 slots, +0x0c + 4 × message. Message 0 also keeps a prompt pointer at +0x78 (SetMsgHandlerEx). The component's +0x74 is the repeat period in ms (1000 by default, 0x00384a10; set by slot +0x44). Confirmed (code).

The components come from a pool of 100 (0x80 bytes each, a free list, pointer 0x00512b24), made by InitLevel (0x00384688) and freed by UnloadLevel (0x00384840); a car or prop that is destroyed gives its component back (0x00384968: clear it, detach it from the object, destroy it, push it on the free list). Its methods: set a slot (0x00384aa0, message 0 also stores the prompt at +0x78; 0x00384b30, message 0 stores its value at +0x7c instead), get a slot (0x00384bc0), "has any handler" (0x00384bd0, any of the 26), the two message-0 values (0x00384c00, 0x00384c18) and set the owner +0x08 (0x00384c30). Confirmed (code).

Delivery (0x00384c38): a message reaches Lua only when the object has a name in that slot, the component is attached, and the level-end state (W_GameState + 0x14c) is 0: once a mission is won, failed or left, triggers stop calling scripts. The marshaller (0x00384ce0, only while scripting runs, 0x00512b28 = 1) pushes the arguments by message number and calls the function; some numbers ask for one result, and a true result means the message was consumed. Confirmed (code); the meanings in the last column are from the Script events list and the senders below.

Message Callback arguments Result asked
0 (self, subject): the interaction (triangle), the subject the human yes: true consumes the press
1 (self, other, n): other is the record's +0x04, n the byte +0x11 no
2 (self, other or NilHandle) from +0x00 no
3, 4, 5 (self, subject): entered, left, still inside (below) no
6 (subject, n, other or NilHandle), n from +0x00 no
7 (subject, other), other from +0x00 no
0xb (self, other) no
8 not marshalled here; a flag's own handler (0x00416038) calls (flag, human) when a human arrives at it (AI: GoalMoveToFlag) no
9 (n), the short at W_GameState + 0x412 yes
10, 0xd (self) no
0xc (self) yes
0xe (self, other, n), n from +0x04 yes
0x10 (subject, other, n), n from +0x04 yes
0xf (subject, other) yes
0x11 (subject, other or NilHandle, n), n from +0x04 no
0x12, 0x13 (subject, other or NilHandle): died or knocked out / revived; other is the attacker no
0x19 (self, other or NilHandle, n, flag): n the signed short +0x04, flag the number 1 or 0 (+0x06 ≠ 0, not a boolean); a car's explosion sends (car, NilHandle, -1, 1) (Cars) no

self is the object the handler belongs to; subject is the record's +0x24; other is +0x00 unless the row says otherwise. A gang's handlers (GangSetMsgHandler) use their own marshalling for 18, 0x11 and 2 (AI: gang events).

World objects (props, weapons, pick-ups; the shared vtable 0x005453a0) take a message through their +0x44 (0x00392520): it first offers it to the object zone's handler (ObjZone_DispatchMessage, 0x003982a0, whose result is ignored), then to the handler component at object +0xfc (its +0x5c is 0x00384c38, the delivery above), and returns that result. Confirmed (code).

The interaction prompt. SetMsgHandlerEx (0x00386168) with message 0 and a non-empty prompt also calls the object's +0x124; a world object's (0x00391c98) registers, when it has none yet (+0x130), a kind-1 context record holding the prompt (+0x10) and the second text (+0x14), reach 1.1 m (Crimes: context records). The record is registered too when the prompt is nil or empty but the second text is not: it then has no prompt, and the HUD shows only the second text, in the hint box (the heavy bag's quit, Workouts). A nil callback, or a prompt and second text both nil or empty, calls +0x12c (0x00391ce8 → 0x00417df8; a flag's Flag_UnregisterContext 0x00415d90), which unregisters the record and clears any player's current record (+0x660) that pointed at it. Plain SetMsgHandler with message 0 (Script_SetMsgHandler, 0x00386298) calls the same +0x12c before setting the slot, whatever the callback, so it always removes the object's prompt. Confirmed (code), and at runtime at the gym (worksetup.StartWorkout's SetMsgHandler(fStartWorkout[k], 0, nil) took the station's record off the list within 2 updates). Triangle by the object then sends it message 0 with the human as the subject (Crimes: triangle, step 4); a true result ends the press, anything else lets the game's own pick-up go on. Confirmed (code), and at runtime (slot 1, a bat 1 m ahead of the player given a handler by a call): the call registered a kind-1 record, the player's +0x660 took it the next update, and triangle delivered message 0 (three calls in the one press, from steps 4 and 5) before the bat's pick-up clip started.

Trigger boxes and spheres

Most of the first mission's progress is driven by message 3 on volume boxes. Confirmed (code):

  • A volume box (AddVolumeBox, kind 0: vtable 0x00545df8, pools on Tasks) keeps the handles of up to 60 occupants (+0x70, cleared to NilHandle by 0x004151c0) and its own handler component at +0x160. Every box's update runs once every fifth frame, round-robin by handle (VolumeBoxes_UpdateAll, 0x00412ca0, from mode 1's step 6; the same pass marks in +0x69 whether player 1's camera is inside a box that asks for it, +0x6a). The update (0x00415378), while enabled (byte +0x68), collects the humans within its bounding sphere (centre +0x30, radius +0x40; 0x002274a8, at most 60), skips the dead (0x00227eb0), and tests each:
    • inside and new: added to the occupants, message 3 (entered);
    • inside and already an occupant: message 5, at most once per repeat period (next time at +0x1e0);
    • an occupant no longer inside, or dead: removed, message 4 (left).
    • an occupant whose handle no longer resolves (0x00390268): skipped, with no message 4, and its entry stays. "Dead" here (0x00227eb0) is no brain or state flag 0x100000000; a human taken out by HuDelete (flag 0x200000000) is neither, so while it still resolves it leaves like anyone else (message 4). When the deleted human stops resolving is not traced.
  • A kind-2 box is a player trigger (vtable 0x00545cb8, made by 0x004133d0; update 0x004134f8): the same messages 3, 5 and 4 with the same inside test and repeat period (next time at +0xf8), but its only candidates are the two players' humans (the handles at 0x0051489c + 0x228 and +0x22c) and it keeps just those two occupants (+0xf0). An AI human never sets one off, however it moves. The chase levels mark their camera and hint zones with kind 2 (level2: 5 boxes, level3: 29, among them vbStartRail, which hands the player back his pad in level3's chase). Confirmed (code). Kind-3 boxes (turf) have no trigger update traced. Coney: VolumeBoxes::update() runs kind-2 boxes over the players' humans only (a player's brain, alive or brain-dead).)
  • Disabling a box (EnableVolumeBox(box, false), 0x004152e0) empties its occupant list without sending message 4, so a human still inside when it is enabled again gets a fresh message 3.
  • Teleporting (TeleportToFlag, 0x00385db0) only sets the object's position (and calls a human's slot +0x14c); it does not touch any box, so a teleported occupant gets message 4 on the box's next update. Messages still reach Lua only while W_GameState + 0x14c is 0 (delivery).
  • Inside (0x00412a18): within the bounding sphere, between the box's lowest and highest z (+0x18, +0x28), and inside the four corners rotated about the centre by the 2 × 2 matrix at +0x48-+0x54 (x' = m00 dx + m01 dy, y' = m10 dx + m11 dy, RotateVolumeBox).
  • Message 6, damage done from inside a box (VolumeBoxes_SendDamageMessage, 0x00413018): when a human damages something, every enabled box (handles 0x26c-0x2eb) that has a Lua handler for message 6 and whose inside test holds for the human's position (not the object's) is sent message 6; then humans within 30 m are alerted (0x00293768). The Lua handler gets (human, box, object), the object NilHandle when there is none (marshaller case 6, 0x00384ce0). Senders: a strike landing (Strike_Contact, 0x0021b290, the object hit), a thrown object hitting something (0x00393538, 0x003939a8), an object breaking on a human (0x00392b88, no object) and a car hit (0x0038bea0). Confirmed (code). level34's riot meter and level3's gallery count on it.
    • Only kind-0 boxes get it: the box's kind (slot +0x64: 0 volume, 2 player, 3 turf) must be 0, and the box enabled (+0x68). Every such box that contains the human gets it, not just one. Confirmed (code).
    • What object is, by sender (confirmed (code) unless marked): a strike on a world object, a glass pane or any other non-human, non-car target (Strike_Contact): the struck object, but only if it was still intact before the blow (both its damage counters +0x10d, +0x10e non-zero, or −1 for unbreakable), so the blow that breaks a newsstand or a store item is counted and blows on its wreck are not; a strike on a human sends nothing. A car hit (Car_OnHit): the car, for every strike by a human, for a thrown object's first contact that damages a part, and for a molotov. A thrown object (0x003939a8): the thrown object itself, when it is broken by the impact, with the thrower as the human (inferred). An object breaking on a human it hit (ThrownObject_HitHuman, 0x00392b88): no object (NilHandle). Glass broken by an explosion or BreakGlassInRadius sends nothing.
    • A thrown object (WorldObject_OnContact, 0x00394050, calling WorldObject_OnImpact, 0x003939a8, at 0x003945a8 / 0x003945ec): the human passed is the object's holder (WorldObject_GetHolder, 0x003951d8) or, once it has left the hand, the human who threw it (object +0x11c); the message is sent only when that human is set and the object is broken by the contact (damage counter +0x10d or +0x10e at 0). So the box test is on the thrower's position at the moment the object breaks, not on where it lands, and the Lua handler's third argument is the broken object, whose own position a script may test. No noise event is involved (AI_ReportNoise is a separate call). Confirmed (code).
    • level80 checkpoint 3 (CopsDistracted, level80) relies on this: vVandalBox (corner −265, 262.2, 1; size 34.3 × 19 × 6, so z 1-7) contains the roof spot the thrower is walked to (fThrowPlayer1, −243.8, 268.2, 5.8), so every bottle he breaks sends message 6; the handler then accepts it only when the bottle is inside vBottleBox (corner −264.7, 274.7, 1.1; size 8.2 × 6.5 × 3.5, around fBottleTarget −259.6, 278, 1.3) and otherwise sends the two cops searching again. Inferred from the positions (corner read as the minimum corner).
  • A trigger sphere (TriggerSphereCfg, 0x00414bc0) is a 0x184-byte record from a pool of 100 (0x006f3f50, slots 0x006fd6e0) attached to an object's handler component: +0x170 the object, +0x174 the radius, +0x17c the mode, +0x180 armed, +0x164 its message-5 period (1000 from 0x00414480). TriggerSphereCfg's last argument goes to the object's handler component (+0x74); whether that reaches the sphere's period is not traced. The pool is updated round-robin, each sphere every fifth frame (0x00414398, from mode 1's step 6), with the same enter / still-inside / leave rules as a box (0x004146e0), the messages going to the object (so SetMsgHandler(dealer, 3, ...) hears a human come within the radius). Mode 0 tests distance only; mode 1 adds a test between the sphere's centre and the human (0x0024dee8, inferred: line of sight); mode 2 the same from raised points (0x0024df40). The first mission's one sphere is the dealer's: radius 4, mode 2, interval 500.
  • Arming and disarming a sphere (TriggerSphereEnable, 0x00414ae0). Arming an object with no sphere creates one with the defaults of 0x00414480: radius 0 (so it accepts nobody until a radius is set), mode 1, period 1000 ms, empty inside list. Disarming (0x004144d0) only empties the 60-handle inside list (+0x00, filled with 0xff) and clears +0x180, without message 4; the sphere, its settings and the object's handlers stay. The pool slot is freed when the handler component is cleared (0x00384a50). confirmed (code).

The other box and sphere functions. Confirmed (code):

Address Name What it does Evidence
0x00412888 Box_Destruct the box base's destructor (vtable 0x00545c48): unregisters the handle (+0x44) confirmed (code)
0x004128e8 Box_SetName the name at +0x58, at most 15 characters confirmed (code)
0x00412910 Box_ComputeBounds the bounding sphere: centre +0x30 the midpoint of the corners +0x10 and +0x20, radius +0x40 half the diagonal confirmed (code)
0x004129c0 Box_ContainsObject the inside test on an object's position confirmed (code)
0x00412bf8 / 0x00412c40 VolumeBox_Enable / VolumeBox_SetRotation EnableVolumeBox (the box's slot +0x5c) / RotateVolumeBox (the matrix +0x48-+0x54) confirmed (code)
0x00413198 VolumeBox_ContainsObject IsInVolumeBox: both handles resolve and the object is inside confirmed (code)
0x00413398 / 0x00413448 / 0x004134a0 PlayerBox_DestroyPool / PlayerBox_Destruct / PlayerBox_HasOccupant the kind-2 pool (0x00514814); the destructor (vtable 0x00545cb8, clears the handler component +0x70); whether a human is one of its two occupants confirmed (code)
0x00414ed0 / 0x00414f08 / 0x00414f40 TurfBox_DestroyPool / TurfBox_Construct / TurfBox_Destruct kind 3 (vtable 0x00545d88, pool 0x00514834) confirmed (code)
0x004150e0 / 0x00415288 / 0x00415320 VolumeBox_DestroyPool / VolumeBox_Destruct / VolumeBox_HasOccupant kind 0 (pool 0x0051483c); the destructor clears the handler component +0x160; whether a handle is in the occupants +0x70 confirmed (code)
0x00415118 VolumeBox_FindByName the first box (handles 0x26c-0x2eb) with that name, else NilHandle confirmed (code)
0x00413208 empty confirmed (code)
0x00414438, 0x00414cc8 / 0x00414d28 TriggerSphere_Construct, its static initialiser and stub 60 inside handles set to -1, the handler vtable at +0xf0; all 100 built at boot confirmed (code)
0x004142a0 / 0x004142d0 / 0x00414338 TriggerSpheres_Reset / TriggerSphere_Alloc / TriggerSphere_Free clear the 100 slots; take the first free; free one (the sphere being updated, 0x00514824, is only marked at 0x00514828) confirmed (code)
0x00414548 / 0x00414688 TriggerSphere_Accepts / TriggerSphere_AcceptsObject within the radius of the object, then mode 1's test (0x0024dee8) or mode 2's clear ray (Ray_IsClear); on a point or an object confirmed (code)
0x004144f8 TriggerSphere_HasOccupant whether a handle is in the inside list confirmed (code)
0x00414a28 TriggerSphere_SetRadius TriggerSphereSetRadius: makes the sphere when the object has none, then +0x174 confirmed (code)

Behaviour

Life of the Lua state

  1. Start-up. Game_InitializeSubsystems creates the script system (0x00160fd0). The constructor opens a Lua state with a stack of 0x400 slots (0x0032f698), opens the string, base and math libraries, registers the tolua bindings and the four Cfg functions, and replaces _ERRORMESSAGE and _ALERT with functions that return without doing anything. A script error therefore stops that chunk (or that call) and leaves no trace. confirmed (code).
  2. Legal screen (mode 5 Enter, 0x00161218). Runs two lists through slot +0x44: 0x00512b08 = enum_preload.lua, then 0x00512b10 = config_preload.lua, config_preload2.lua, config_preload3.lua. The calls of slot +0xdc around them do nothing. confirmed (code).
  3. enum_preload.lua defines MATERIAL and SA and calls nothing.
  4. config_preload.lua sets up sound: material and animation sound slots, music info (bindings only).
  5. config_preload2.lua defines PHYS, ANIM, OBJECT, AXIS, PHYFLAG, Platform = GetPlatform(), LanguageExt, then runs doFile("config_strings_" .. LanguageExt[GetLanguage()]) and configures characters, hats, gangs, weapons, HUD messages and sounds through Cfg* bindings.
  6. config_preload3.lua defines and runs CfgObjectsAtoC ... CfgObjectsTtoZ (1,279 CfgObj calls) and CfgLevels, which builds levelNames (111 records) and calls CfgLevelName for each, then sets its temporary globals (levelNames, LT_*, LOCKED, UNLOCKED, SUBWAY_*, GSTRING, CfgLevels) back to nil. Runtime check: 1,034 globals before the preloads, 1,059 after. confirmed (runtime).
  7. A level loads (InitLevel, also for the front-end level level100). The level flow calls slot +0x24 with the level's name: it runs global.lua, then <name>.lua ("%s.lua"). The PC-style branch (../lua/global.lua, ../levels/%s/%s.lua) is dead in this build because 0x001458c0 returns 1. confirmed (code). Runtime check: 1,252 globals at the front end, including the preloads' (Platform = 1, LanguageExt, PHYFLAG ...), global.lua's and level100.lua's (Menu, WonderWheelAnim). confirmed (runtime).
  8. A level unloads (UnloadLevel, 0x001607b8): the script system is destroyed and created again (0x00356450, 0x00356390), so the next level starts from the bindings alone. The preloads do not run again for it (they belong to the legal screen) (inferred from the call sites; what a level that needs PHYS or GSTRING does after an unload is an open question).

Calling Lua from C++

The game calls a script function in three steps: find it by name (slot +0x4c), push the arguments (slots +0x54-+0x84), call (slot +0x8c). Callbacks given to bindings (ShowProfileManager("Menu.fadeToRMI", "Menu.startGame"), ScheduleFunc("Menu.launchRMI", 500), PadSetHandler, SetMsgHandler) are kept as interned names and looked up when they fire, so a script can replace the function behind a name at any time. confirmed (code).

The language files and Platform

config_preload2.lua sets Platform = GetPlatform() (1 on the PS2) before it runs config_strings_<lang>.lua. About twenty entries of each language file are written as if Platform == 2 then A else B end; the PS2 takes B: for example 0x76 is "PRESS THE START BUTTON" (not "PRESS START") and 0x1f names triangle as "back" (not circle). The branches are inferred from the disassembly; the PS2 wording is confirmed (runtime) in PCSX2. GSTRING.HUD entries are stored with explicit indices (GSTRING.HUD[118] = ...), so the disassembly gives each global string id's text.

global.lua

It runs before every level script and gives the levels their shared helpers. Its main chunk (9,438 instructions, 154 functions) sets true/false, the Rumble mode numbers (RM_*), the colour table CL, builds the five languages' GSTRING/LABEL tables and keeps the one for GetLanguage() (then sets strings and strings2 to nil), picks the sound matrix (SndLoadMatrix("armies") for levels 60-64, else "sound"), sets MenuTrack (music/in_the_city once level 84 is complete), and defines the helpers (ChangeCam, TagInfo, NoScenes, ObjectiveAdd, ...). It ends with CfgAmbient() and SetupLevelInventory(). inferred (disassembly of the chunk).

The objective helper (ObjectiveSetup)

Most story objectives "go to this place" are made by ObjectiveSetup(t) (global.lua function 132) with a table of fields. Inferred from the disassembly of global.lua (functions 132-146; names, fields and values only).

  • ObjectiveSetup(t, noAdd) stores t under a new index and, unless noAdd, runs ObjectiveAdd: the text (String) on objective line 0, or line 1 when Bonus is true (HUDSetObjective(line, String, 0, Silent), then states 1 and 0 again); default Radius 2 m, RemoveTime 4000 ms, GTDistance 15 m. When there is a Flag or a Volume: without a Volume, a trigger sphere on the flag (TriggerSphereCfg(Flag, true, Radius, true, 100)) and the handler ObjectiveEnter for messages 3 and 5 on the flag; with a Volume, the same handler on the volume box (no sphere). Then the flag becomes a radar mission objective (texture 27, or 28 when bMission) and, unless NoObject, a marker object (dyn_w_goto, or dyn_w_mission with bMission) is spawned at the flag and shown. HUDType adds a counter panel.
  • ObjectiveEnter(box, human) (messages 3 and 5) finds the objective whose Volume is the sender, else whose Flag is (ObjectiveGetIndex, function 146: the message's first argument is compared with the Lua value of Volume/Flag, so the box or flag handle the engine passes must be the one the script holds). It goes on only when human is a player (HuIsAPlayer), or, with CheckWarriors, any human of gang type 0. Then it clears messages 3 and 5 on the sender and:
    • with GoodToGo: hides the marker, sets message 4 to ObjectiveExit, remembers the human (GTGHuman) and runs ObjectiveGoodToGo (below);
    • with bShowHide: hides the marker and sets message 4 to ObjectiveExit only;
    • otherwise: CallBack(index) (CallBack(index, human) with Player2Callback), then ObjectiveComplete unless NoComplete. So an objective without GoodToGo completes on the first player who enters, alone.
  • ObjectiveGoodToGo(box) (function 145), the "wait for the crew" test, every 500 ms (ScheduleFuncArg1) while the player stays: it passes when GTGHuman is within Radius (2 m by default) of the Flag (TestDistance) and every human in the Lua table Warriors is within GTDistance (15 m) of GTGHuman (GetDistanceTweenHumans), not arrested and alive; with GTGUseObject instead, every Warrior must be inside the box. With PoliceWanted, a wanted Warriors gang (GangIsWanted(GangWarriors, true)) fails it too. On a pass: the hint is flushed if it was shown, CallBack runs and ObjectiveComplete. On a fail: the hint GSTRING.GTGPOLICE (wanted) or, once, GSTRING.GOODTOGO at priority 1 (HUDSetTutorialText(text, 1)); with NoComplete the callback gets false once (and true once on a later pass, without completing); then it checks again 500 ms later. A dead or arrested Warrior still in Warriors blocks it.
  • ObjectiveExit(box, human) (message 4, only after GoodToGo or bShowHide): when the leaving human is the player and player 2 is not still inside (or player 2 leaves with player 1 outside), it stops the 500 ms check, shows the marker again, flushes the GOODTOGO hint if shown, and sets messages 3 and 5 back to ObjectiveEnter.
  • ObjectiveComplete(index): flushes the hint (HUDFlushTutorialText(1)), marks the text done (HUDSetObjective(0, text, 2, true); state 1 with OBJRemove; a bonus on line 1, state 2), releases the counter panel, removes the radar objective and the marker, clears messages 3 and 4 on the flag and the volume, UM_Unlock(Level, 1, UnlockID) when set, and forgets the objective.

Uses: level80's four "go to" objectives ({String, Flag, Volume, CallBack}, no GoodToGo: the player alone entering the volume completes them, level80); level5 checkpoint 1's bar ({Flag = fObjective01, Volume = volObjective01, Player2Callback, CallBack = C1.OpenBar, GoodToGo = true}): the player must stand within 2 m of fObjective01 with all four Warriors within 15 m of him, alive and not arrested.

level100.lua (the front end)

The main chunk, in order (inferred from the disassembly; what the result looks like is on Front end):

  1. TagInfo.NoTag() and NoScenes() (helpers from global.lua that remove the tag and scene helpers for this level).
  2. CfgSetDatabaseSizes(48, 34, {2, 1, 2, 3}).
  3. ChangeCam(CameraCreateLocked("Black", {0, 2, -10}, 40, 180, 0, 0, 0.5, 200), 0): a locked camera (fov 40°, heading 180°, near 0.5, far 200, clamped to 150 by the binding), made active at once (ChangeCam is CameraMakeActive then CameraReset). It sees nothing: the wheel is about 550 m away.
  4. Menu = {}, Menu.movies = {[1] = "TRAILER", [2] = "L1_IN"}, Menu.movieID = 0.
  5. Two sprite batches, L100_RM_PTank1 = GetPTank(786437, {0, 0, 0}, 1, 9000, 256) and L100_RM_PTank2 = GetPTank(1835027, ...): sprite words 0x000c0005 and 0x001c0013, sheet-table records 12 and 28, the Rumble menu's sheets (Front end); level95_clubhouse.lua makes the same two before its menus.
  6. The Menu functions (below), AddObjects, and the table WonderWheelAnim (sceneName = "WonderWheel_100", callbacks preload = "WonderWheelAnim:startScene" and finished = "WonderWheelAnim:finishedScene", the latter never defined nor passed) with enable, start and startScene.
  7. if not Objects then AddObjects() end: 29 ObjSpawn calls, all with the rotation {0, 0, 0.707107, -0.707107} (−90° about z): dyn_s_neon_a-d and dyn_s_wwheel_a (tint 0x474542FF) at (515.51, −68.88, −188.65), and the carts dyn_s_wwcart_simple_a, _b, _c with _01-_07 copies (tint 0x888888FF) in three parked rows at z −208.59 (x 522.95-544.82; y −73.15, −65, −69.19); the scene moves them onto the wheel.

WonderWheelAnim:start puts the 29 handles in scene-slot order (a-carts 0-7, b-carts 8-15, the wheel 16, c-carts 17-24, neons 25-28) and marks itself playing, then, when SceneIsPreloaded(name) (loaded and idle), keeps GetSceneID(name) (which no binding or script defines) as the id and calls startScene itself; otherwise keeps the id ScenePreload(name, "WonderWheelAnim:startScene") returns. It does nothing while disabled. startScene(id) adds the 29 objects (SceneAddObject) and calls ScenePlayCinematic(id, 0, nil, false, false, true, false): no delay, no end callback, no letterbox, not skippable, looping, the world not frozen; reached while disabled (a load arriving after enable(false)), it instead unloads the scene when it is loaded and idle. enable(on) keeps the flag, always loops music/wonderwheel_132b, then on true calls start unless already playing, and on false, when playing, clears the playing mark and calls SceneStop(sceneId) without force (so false), which ends this scene at once (Front end: stop and restart). The lights come from global.lua's matrix entry for level 100 (Front end).

The Menu functions (inferred from the disassembly):

Function Does
onStart startScene, ShowProfileManager("Menu.fadeToRMI", "Menu.startGame"), fade in over 1.5 s
onFinish Menu = nil, release the two particle tanks
fadeToRMI fade out (0.7 s), ScheduleFunc("Menu.launchRMI", 500)
launchRMI loop MenuTrack, ShowRumbleModeInterface("Menu.cancelRumbleMode", "Menu.startRumbleMode", 1)
startRumbleMode(n) SetCheckPoint(1), stop the music, stopScene, MenuLoadLevel("level" .. n)
cancelRumbleMode loop music/wonderwheel_132b, ScheduleFunc("Menu.fadeIn", 100)
startGame stop the music, stopScene, runNextMission(1) (a global.lua helper)
playMovie(id) Menu.movieID = id, stop music, fade out, ScheduleFunc("Menu.playMoviePostFade", 500)
playMoviePostFade stopScene, PlayMovie(Menu.movies[Menu.movieID]) (1 TRAILER, 2 L1_IN), ScheduleFunc("Menu.movieFinished", 500)
movieFinished startScene, fade in
startScene / stopScene WonderWheelAnim:enable(true / false)
reloadProfiles fade out (1.0 s), ScheduleFunc("Menu.reloadProfilesPostFade", 700), which calls SSMC_StartLoadSequence
deleteProfile instant black (ScreenQueueEffect(1, 0)), SSMC_StartDeleteSequence

ScreenQueueEffect(type, seconds) with type 0 fades in and 1 fades out (Front end for the timing and what a fade blocks).

runNextMission (story progress)

global.lua's runNextMission(arg) (function 61 of the main chunk) chooses the story's next level. Inferred from the disassembly:

  1. It builds a table from the last completed level to the next mission's {checkpoint, level}. In order: 0 → 99; 99 → 80; 80 → 87; 87 → 95 (checkpoint 1); 34 → 95 (2); 2 → 95 (3); 3 → 95 (4); 5 → 95 (5); 81 → 95 (6); 86 → 93; 93 → 95 (7); 31 → 95 (8); 14 → 95 (9); 9 → 95 (10); 51 → 52; 52 → 54; 54 → 55; 55 → 84; 84 → 95 (11); 64 → 95 (12). Entries without a checkpoint in this list use 1. Level 95 is the hub the story returns to between missions, entered at a different checkpoint each time (inferred from the pattern).
  2. The last level is the global LastLevel (set to 0 by global.lua's main chunk, so 0 after every level load). When it is 0 or nil, findLastMission() takes the first of 64, 84, 55, 54, 52, 51, 9, 14, 31, 93, 86, 81, 5, 3, 2, 34, 87, 80, 99 for which UM_IsLevelComplete is true, else 0.
  3. If the table has an entry: SetCheckPoint(checkpoint), SoundStopMusicTrack(), MenuLoadLevel("level" .. level), and when arg is 1, HUDLaunchMissionComplete(4). Otherwise it calls HUDLaunchMisssionComplete(3) (three s), a name no binding has, so that call fails and, since errors are silent, ends the function. That this branch is a bug in the shipped script is inferred.

So a new game starts level99 at checkpoint 1 (Front end).

Who calls it. The front end's Menu.startGame calls runNextMission(1) for a new or loaded story game. After a mission, the mission-complete mode (0xb, MissionComplete_Launch 0x0015d420 pushes it) calls the Lua function UnlockAndLoad from its Enter (0x0015cf70, the name at 0x0054f528; confirmed (code)), a global.lua helper (function 59) that calls MissionCompleteUnlocks(), releases the players' assets (LiquidizeAssets) and calls runNextMission(1) (inferred from the disassembly). The failure and pause menus call runNextMission(0) (0x00155408, 0x00155648, 0x001557f8: the name at 0x0054e798, one number argument 0 or 1; confirmed (code)).

The story order, from this table and the hub's fRunMission (The hub); inferred from the disassembly, and it matches the level records' +0x0c (order 1-18 in Levels). Every level is entered at checkpoint 1 except the hub, whose checkpoint picks its chapter and its next mission:

Mission Level Loaded by
1 level99 a new game: Menu.startGame's runNextMission(1) with nothing complete
2 level80 runNextMission after 99
3 level87 runNextMission after 80
hub level95 checkpoint 1 runNextMission after 87
4 level34 the hub, checkpoint 1
hub level95 checkpoint 2 runNextMission after 34
5 level2 the hub, checkpoint 2
hub level95 checkpoint 3 runNextMission after 2
6 level3 the hub, checkpoint 3
hub level95 checkpoint 4 runNextMission after 3
7 level5 the hub, checkpoint 4
hub level95 checkpoint 5 runNextMission after 5
8 level81 the hub, checkpoint 5
hub level95 checkpoint 6 runNextMission after 81
9 level86 the hub, checkpoint 6
10 level93 runNextMission after 86
hub level95 checkpoint 7 runNextMission after 93
11 level31 the hub, checkpoint 7
hub level95 checkpoint 8 runNextMission after 31
12 level14 the hub, checkpoint 8
hub level95 checkpoint 9 runNextMission after 14
13 level9 the hub, checkpoint 9
hub level95 checkpoint 10 runNextMission after 9
14 level51 the hub, checkpoint 10
15 level52 runNextMission after 51
16 level54 runNextMission after 52
17 level55 runNextMission after 54
18 level84 runNextMission after 55
hub level95 checkpoint 11 runNextMission after 84; the hub offers no next mission

Outside this chain, the hub's trophies load the five flashback missions and the Armies of the Night levels (The hub), and level64, their last, returns to the hub at checkpoint 12. Since MissionCompleteUnlocks ends with LastLevel = findLastMission() (inferred from the disassembly of global.lua function 60), the level runNextMission looks up after any completion is the latest story mission complete, not the level just played: finishing a flashback, or a story mission replayed from the hub's mission menu, returns to the hub at the story's current checkpoint. Once level64 is complete it is the first findLastMission finds, so from then on every completion leads to hub checkpoint 12 (inferred, not observed). Which bindings each of these levels needs: Story coverage.

Missions coverage

Which script bindings the story needs after level99 is generated from the disc by coney-tools natives missions onto Story coverage: for every later level (missions 2-18, the hub, the flashbacks and Armies of the Night) the bindings it can call, the ones it is the first to call, how far each is researched and whether Coney implements it. A level's bindings are those its chunks (levelNN.lua and its levelNN_*.lua chapters, without the _scenetest and string files) name, plus those of the global.lua helpers they reach and the engine's two callbacks, the same reach rule as natives mission1 (an upper bound). Where each family's behaviour is described:

Family Behaviour
character Characters, Combat
world World objects, World flags
level Level loading
ai: goals and actions (Goal*, Act*) AI
gang AI
hud HUD
config Config bindings
sound Sound
effects Particles, The streamed world
camera Camera
ai: tactics (Tactic*) AI
ai: brains and the rest (Br*, ...) AI
scene Scenes
script this page
util, debug, input this page

The hub (level95)

level95 is the Warriors' clubhouse and the streets of Coney around it, where the story returns between missions: its functions set up a clubhouse (SetupClubhouseEnvironment, AddClubhouseFlagsBoxesPaths) and Coney (SetupConeyEnvironment), and it loads level95_clubhouse.lua (the clubhouse: the Warchief, the Warriors and their girls at their spots, workouts, the trophies that start flashbacks, the mission and Rumble menus) and level95_coney.lua (preLoadFile with env.FinishClubhouseLua / env.FinishLoadConeyLua). Inferred from the disassembly. Its Main loads level95_chapter<checkpoint>.lua (12 files, names recovered by their CRC) with the callback RunLevel; each chapter sets the numbers of Warriors and girls, the radio track, the Warchief's type (WarchiefTable) and the chapter's mission actions (MissionAction: red circles, cut-scenes, MA_MISSION errands, ending in MA_LOADLEVEL). MA_LOADLEVEL calls story.LoadLevel(fRunMission[checkpoint].level) (level95.lua function 55), which stops the music, banks a live player's assets (LiquidizeAssets(player, true, true, true, true)), sets checkpoint 1, starts a save (SSMC_StartSaveSequence()) and calls MenuLoadLevel("level" .. level) (inferred from the disassembly):

Hub checkpoint 1 2 3 4 5 6 7 8 9 10 11 12
Next mission 34 2 3 5 81 86 31 14 9 51 none none

Flashbacks: the clubhouse's trophies (flashback.PlayMission, action MA_LOADFBLEVEL) load FBMission[k].level: level82, level92, level83, level20, level11, and level60 once those five are complete (its req). Levels 60-64 are the Armies of the Night bonus game (SndLoadMatrix("armies"), the _a models): each one's last checkpoint script loads the next (level60_mothers → level61, ... level63_moonrunners → level64). The mission menu itself (menu.BeginChosenLevel(level, checkpoint) in level95_clubhouse.lua) clears the save data, calls SetCheckPoint(checkpoint), starts a save and MenuLoadLevel("level" .. level). Inferred from the disassembly.

Not the hideout: level2_clubhouse.lua is checkpoint 3 of story mission 5 (level2), a clubhouse full of Orphans (orph_* models named Clubhouse1 ...): another gang's (inferred).

Cameras from Lua

CameraCreateFollow(name, target) is a global.lua helper (function 15), not a binding: it calls CamSetupFollow(name, target) and then CfgFollowCamera(FOLLOWCAM_MINDIST, FOLLOWCAM_MAXDIST, FOLLOWCAM_DEFAULTDIST, FOLLOWCAM_DEFAULTANGLE, FOLLOWCAM_FOV, FOLLOWCAM_NEARPLANE, {FOLLOWCAM_OFFSET_X, _Y, _Z}, FOLLOWCAM_SLOWMO) and returns the camera's handle. global.lua sets these globals to 3, 6.6, 4.8, 13 (degrees), 65, 0.1, (0, 0, 1.4) and 0.2; CameraNormal() applies them again to MainCam. Inferred from the disassembly; the values are confirmed (runtime) in the camera object (Camera).

Scenes and movies

  • The scene list. Boot step 13 (0x003535e8, Boot) loads scene_list.cnk, and 0x00353460 copies its Scene List chunk (type 0x43): a u32 count, then 24-byte records {u32 id, u32 size, char name[16]}, to 0x006eba10, the count to 0x00512aec. Confirmed (code). On the NTSC-U disc there are 2,765 records, each id its own index, one per .scn record: 1,240 scene headers (24 of them under names cut to 16 characters) and 1,525 segments of long scenes. The records, the slots and playback are on Scenes.
  • ScenePreload(name) (0x00353f88) takes the id 0x00353698 finds: the first record whose name contains name (0x00435d30 is a case-sensitive strstr), or 0 when none does; then 0x00353af0 loads it into a free slot of the 12 unless it is loaded or loading already. Confirmed (code). The scene id is global, not per level. Each of the 187 names the scripts pass finds its own record on the NTSC-U disc (no shorter name hits an earlier record). Lookups by exact name (0x00353778, strcmp) and by id (0x00353730) exist too, and C++ code calls 0x00353698 itself (the AI, 0x002cb5d0), so not every scene is preloaded by a script.
  • Movies. PlayMovie(name, flag) → Movie_Play (0x0042a938) opens PSS\<name>.BIK (format 0x0058bfe0), confirmed (code). Who names them: the boot (LOGO, PLOGO, L1_IN), a level record's intro L%d_IN (0x00550160) and outro L%d_OUT (0x0054ed20) (Level loading), and the front end's Menu.movies (TRAILER, L1_IN). The disc's PSS folder holds exactly those 16 movies. How a movie plays: Movies.

Both are listed in Scenes and movies.

level99.lua (the first mission)

The first story level's script, inferred from the disassembly. Its main chunk defines helpers, calls AddFlagsBoxesPaths() and RegisterObjects(), sets the animation ids it uses (ANIM_*) and a Buttons table, and calls Main():

  • Main: checkpoint = GetCheckPoint(), SetFogColor(0.05, 0.05, 0.02), ReportCrime(0), ShowHud(0), RestoreHud(); a table tMission of three checkpoints, each {create the Warriors, start function, script}: 1 = {AddWarriors2, "Checkpoint1", "level99_combat"}, 2 = {AddWarriors, "Checkpoint2", "level99_lesson1"}, 3 = {AddWarriors1, "Checkpoint3", "level99_lesson2"}; then RunLevel().
  • RunLevel, in order: CfgSetStatValue(0, 0, 1000); F.PreloadAnims() (six SetDynamicAnimation calls naming .anm clips); Warriors, GangWarriors = tMission[checkpoint][1](); player = Warriors.Rembrandt, player2 = Warriors.Ash; preLoadFile(script, startFunction) (the checkpoint's script, which runs the start function when it has loaded); AddCameras(); SetStartGameCallback("StartAmbient"); HuSetDemiGodMode(player, true, 0.25) and the same for player2; CfgPlayerMugging(false); extra set-up for checkpoints 2 and 3; EnableCommand(player, 38, 0) and EnableCommand(player, 37, 0) (two player commands off for the tutorial).
  • AddWarriors2 (checkpoint 1): GangCreate(0, "Warriors2", 0, 0), then HuCreate("Rembrandt", 32, {-284.4, 120.4, 0.3}, 0, "warr_sw", 1, gang) (player 1) and HuCreate("Ash", 40, {-285.5, 125.1, -8.5}, 235, "warr_sw", 2, gang); returns the table of humans and the gang.
  • AddCameras: Cameras.follow = CameraCreateFollow("follow", player), MainCam = Cameras.follow, CameraMakeActive(Cameras.follow, 0), CameraReset(Cameras.follow), then AddCameras = nil.
  • StartAmbient (run by InitLevel once the level is ready): at checkpoint 1, SuperRunScene(IntroScene) (the in-engine intro, defined in level99_combat.lua); at checkpoint 3, the ambient loop vags/ambient/city/distant_traffic_loop.
  • Checkpoint1 (level99_combat.lua) calls P1.SetupCombat: the tutorial's sections (P1.SetupCam, SetupBasicAttacks, ...), HUDSetObjective, HUDTurnOffRadar and the training enemies (AddCombatEnemy).

The player therefore exists and the follow camera is active before the first frame of mode 1; the intro scene takes the camera over and gives it back (Camera).

Chapters and checkpoints. The three chapter scripts run one after another in the same level, without a reload: the last step of each calls SetCheckPoint(n + 1) and preLoadFile on the next chapter's script with its set-up function (P1.Cleanup → checkpoint 2, level99_lesson1, P2.SetupLesson1; P2.NavigationSection → checkpoint 3, level99_lesson2, P3.SetupLesson2). Inferred from the disassembly. SetCheckPoint (0x0041ce98) stores the number (W_GameState + 0x33a) and takes a checkpoint copy of what a restart needs, confirmed (code): the inventories (W_GameState + 0x484, 0x7e8 bytes, copied to +0x7ec, 0x0041e0b8), the stats (0x006fe490, 0x180 bytes, copied after themselves, 0x00422c60), and the object manager's 35-word list (0x00715780 → 0x007156f0, the source cleared) with the checkpoint and the level index (−1 unless the level is level34, 0x00397e88). The checkpoint picks the chapter only when the level is entered (Main, above); RunLevel also has a branch that preloads level99_scenetest (PlaySceneTest) when the debug SCENETEST is set: the chunk is on the disc and plays one of eight l99_ scenes on a loop (Scenes: the scene test).

Triggers in this mission (counted from the scripts): 16 volume boxes, whose message 3 starts most steps (vMark01 → P1.FirstGlow, vClimb → P3.MoveToClimb, ...); message 8 on four flags (an AI teacher arrived); messages 1, 3, 4 and 16 on the dealer, whose trigger sphere gives 3 and 4; 18 on a civilian and, through GangSetMsgHandler, on a gang; 2 on two breakable fences; one 50-second stopwatch (P1.SendWarriors → P1.TimesUp); about 60 scheduled calls; the tutorial's attack callback (HUDSetTutorialCallback, HUD), animation callbacks (Characters) and pad handlers (PadSetHandlerEx). How each is delivered: Message handlers, Trigger boxes and spheres, The mission stopwatch, Scheduled calls. No script of the mission launches a failure: the players are demi-gods (below) and the only failure the engine raises by itself is a player falling out of the world (Level loading).

Demi-god mode. HuSetDemiGodMode(player, true, 0.25) sets flag 0x20000000000 and stores 0.25 in one global (0x0051024c) shared by every human. When a hit would take such a human's health below that fraction of its maximum, its health is set to the fraction × maximum and flag 0x10 (god mode) is set, so it takes no more damage until a script clears it (Human_ApplyPendingDamage 0x00265f70, and 0x00256f28, which sets 0x10 once health is at or below the fraction). Confirmed (code); Combat has the rest of the damage order.

How the mission ends (the Lua side inferred from the disassembly, the C++ side confirmed (code)):

  1. P3.UseFlash (the player uses the flash on the dealer) sets the last objective and schedules P3.MissionOver, which runs SuperRunScene with a scene table whose Final is true and ReturnFunc is P3.MissionCompleted.
  2. When that cinematic ends, its end callback, global.lua's PreCashTheWorld(sceneId), sees Final: it queues screen effect 1, calls the ReturnFunc (which restores the default follow slots and re-enables command 1), clears the scene's table entry and calls HUDLaunchMissionComplete() with no argument. (Without Final it would show the scene's HudText, call the ReturnFunc, and switch spotting, the stage lighting and the fade back.) How the scene system plays and ends the cinematic: Scenes and movies.
  3. MissionComplete_Launch(0) (0x0015d420) finds mode 1 on top, pushes mode 0xb and sets the level-change flag 0x0050c754 = 1 (read by mode 1's Exit, Leaving gameplay). Mode 1 is suspended, so its level-end countdown does not run; W_GameState + 0x14c stays 0 until MenuLoadLevel.
  4. Mode 0xb's Enter stills every live human and calls UnlockAndLoad: MissionCompleteUnlocks() unlocks the level (UM_Unlock(Level, 0, 0)) and one more record chosen from the players' scores (StatGetScore); LiquidizeAssets adjusts each player's inventory (money, revives, spray-can charges, keys, item 5) and resets their stats (StatResetPlayer); and runNextMission(1) finds 99 complete and loads level80 at checkpoint 1 (SetCheckPoint(1), SoundStopMusicTrack(), MenuLoadLevel("level80"), which sets +0x14c = 3 and mode 8's next level), then HUDLaunchMissionComplete(4), which only stores kind 4 since mode 0xb is on top.
  5. Mode 0xb's Update (0x0015d160) runs one world frame, sees the kind (4, none of 1-3 it acts on), pops itself, calls the save system's slot +0xb4, pops mode 1 (now on top: its Exit unloads level99), banks both players' money and asks for the autosave (0x00155308, mode 6). Mode 8 then loads level80 (Front end describes the same modes on the new-game path).

The level-end countdown (mode 1 +0x28, W_GameState + 0x14c = 1 or 2) is therefore not used by this ending; it serves MissionComplete (2, which no shipped script calls) and failures (1). Messages stop reaching Lua as soon as +0x14c is not 0 (Message handlers).

These calls reach HuCreate, GangCreate, CamSetupFollow, CfgFollowCamera, CameraMakeActive, CameraReset, SetStartGameCallback, SetCheckPoint, SetDynamicAnimation, HuSetDemiGodMode, EnableCommand, CfgPlayerMugging, ShowHud, RestoreHud, ReportCrime, MenuLoadLevel and HUDLaunchMissionComplete, among the bindings the first mission can call; Mission 1 coverage lists them all with how far each is researched and whether Coney implements it (ShowHud, for one, does nothing in this build).

The combat tutorial's lessons

level99_combat.lua runs twelve sections in order from a table, each started by the previous one's end (directly, or scheduled 1-2.5 s later). A lesson that teaches a move sets HUDSetTutorialCallback and counts the anim ids the callback passes, which is the attacker's id when the victim applies the damage, so only moves that hurt someone count. Inferred from the disassembly of the script (ids and counts only); the moves themselves are confirmed where linked.

# Section Waits for (anim ids, in order) Move Researched
1 camera, intro flags and scenes Scenes
2 basic attacks 12; 11; 51 or 53; 55; 219 or 221; 223 square, cross, grab strikes, mount strikes Combat, mount
3 targeting the PadSetHandlerEx handler L1 Input
4 light, then heavy combos 12, 16, then 19 or 20; 12, 15; 11, 13; 12, 16, then 17 or 18 chains Attacks
5 strong attacks 653 or 655; then 82 or 84 cross + square; circle + cross Specials, Strong grapple
6 power moves 57; then 59 in a front grab, cross + square, then square or cross in its window Power strike
7 snaps (after the scene l99_c7; Rudy drinks at the fence, the second wave surrounds the player) three hits of 25, 27 or 29 square, standing, with the stick past 0.95 more than 45° off the facing, at a bum within 2 m there Attacks, the second wave
8 throws two hits of 147, 149, 151 or 153; then zone 1 is enabled circle with the stick in a grab Throws
9 weapons (after the scene l99_c8, three bats on the ground) the pick-up (message 0 on a bat, with a prompt); then 34; 36; then two more of 34 or 36 (four counted hits) triangle at a bat; square, cross A bat in hand, the prompt
10 rage (2 s after lesson 9) the meter set to half and locked, then filled by hits; the full callback; L1 + R1 (every other command off); 3.5 s into rage, 645 or 647; 3.5 s later, 65 or 233 rage, the rage special, the extended power move in rage (grab or mount) Rage; 65 and 233 inferred
11 finish the second wave the gang's message 18 with none standing any AI
12 the Warriors (after the scene l99_c5) the 50-second stopwatch; a hint the first time the player holds a grab (anim callback on 82 / 84) any Stopwatch, anim callbacks

Then the scene l99_c6 and P1.Cleanup (checkpoint 2). Coney's disc test checks lessons 1-10, driven by mission 1's play-through pad script (repo:tests/platform/disc_level99_course_test.cpp). The pad script is frame-locked, so lessons 11-12 (the second wave and checkpoint 2) wait for an adaptive driver: any change to the AI's movement moves the bums. Points an implementer needs:

  • 6: from the rear hold the hit is scored with the spin's id 80, not 57, so only a front grab passes the first step. The script gives no spray can: Rembrandt's 59 makes and removes its own (dyn_s_spraycan, clip events 0x30 / 0x31) and stops its spray sound (event 0x45), Combat. The scene l99_c7 that follows binds the victim, CombatEnemy.GenWarrior (Rudy), to role 6 rudy; the pair is broken when the cinematic is played (Scenes).
  • 10: the rage special is cross + square while raging (645 / 647); the last step wants the extension of the rage power strike in a grab (63 → 65) or of the rage power strike in the mount (231 → 233); both use the + 2 of Player_UpdatePowerMove (confirmed (code) for the mount too: the mount's power strike puts the mounter in state 0x1000, which the dispatcher sends there). What arms P1.RageMoves (the step order):
    1. P1.WeaponsDone clears the tutorial callback and starts the section 2000 ms later.
    2. P1.SetupRageMode makes rage gains possible again (HuSetPreventRage(false)). It sets the meter to 0.5 and locks it, and it registers the rage handlers: enter P1.EnterRageMode, exit P1.ExitRageMode, full P1.CheckRage, 20000 and 5000 ms. It arms no tutorial callback.
    3. The meter fills only from the player's own hits. The lock stops the drain but not Human_AddRage (Rage), so the player must earn the other half (39 of class 6's 78) by hurting someone.
    4. When it is full, Human_AddRage calls the full handler once, P1.CheckRage(player, true). That shows the next text, drops his weapon, turns every command off and turns command 31 (L1 + R1) back on.
    5. Pressing L1 + R1 starts rage, which calls P1.EnterRageMode(player, …). That turns every button back on except commands 1 and 40, sets the rage protections, and schedules P1.EnableMoveCheck for 3500 ms later.
    6. P1.EnableMoveCheck is what calls HUDSetTutorialCallback("P1.RageMoves").
    7. In P1.RageMoves, step 1 (645 / 647) clears the callback, schedules P1.EnableMoveCheck 3500 ms later again, and moves to step 2. Step 2 (65 / 233) calls P1.RageDone. Any other id is ignored.

So RageMoves arms 2 s + the fill + the L1 + R1 press + 3.5 s after the fourth bat hit. In the passing run that was 631 frames (4260 → 4891). An L1 + R1 pressed before the meter is full, or before command 31 is the only one on, does not start rage, and nothing re-arms it until the meter is full. Inferred from the disassembly of the script (main/46-main/52); the game side is confirmed where linked. - 9: the scene's return function schedules the setup 50 ms later. The setup gives each of the three bats a message-0 handler with the lesson's prompt, places the players in the pen, sets up the fence's enemies and starts a check that runs every 250 ms. The handler, on the first bat taken, removes the handler and prompt from all three, arms the tutorial callback and returns nothing, so the game's own pick-up puts the bat in hand (A bat in hand); no binding does it. The check asks HuGetHeldObject: holding, it shows the lesson's current text once; empty-handed after holding, it shows a "pick it up again" text; it stops rescheduling once the lesson is done. A dropped bat has no prompt any more, but triangle's plain search still takes it (bats are pickable). Inferred from the disassembly of the script; the game side is confirmed where linked.

The steps (P1.Weapons, main/45). It keeps wAnims = {34 ANIM_BAT_COMBO_S1, 36 ANIM_BAT_COMBO_X1}, weaponCount = 1 and multiCount = 1. Each call is one hurting hit: 1. While multiCount is 1, only wAnims[weaponCount] counts. Step 1 wants 34 and step 2 wants 36; anything else, a 36 before the first 34 included, is ignored, not a reset. Step 1 → weaponCount 2, CombatEnemy.GenWarrior's line l99_t1_052 and the next text. Step 2 → 3, the next text, and multiCount = 2. 2. While multiCount is 2, any 34 or 36 adds one and says l99_t1_053. At weaponCount 5, so on the fourth counted hit in all, it sets weaponDone and calls P1.WeaponsDone.

There is no re-arm delay between steps: the callback stays armed throughout, and the next counted hit can come on the next frame. Lesson 10's 3.5 s gaps are its own (above). So 34, 36, 34, 36 ends the lesson on the fourth hit. - 7: P1.StartSnaps (5 s after the set-up) arms P1.Snaps with ANIM_SNAP_RIGHT_01, _LEFT_01 and _BACK_01 (25, 27, 29). Any of the three counts, in any order and on any bum; any other id (an S1, 12) is ignored. The first two update the text and give Rudy a line; the third calls P1.SnapsDone. Confirmed (runtime): snaps 27, 29 and 25 that landed on the bums each reached the callback once, with that id. Coney passes it with the disc (the second wave, the snap). - 2, 6, 8: a plain grab scores nothing; strikes, throws and power moves are scored with their own ids when they start, since a grab move applies its damage on its first update.

The hints in order (runtime reference)

The texts the hint box showed over one play of checkpoint 1 on the original (2026-10-07, the copy l99-tutorial of lesson 1 with TT_20a already up, driven over PINE as Coney's CourseDriver plays it), by LEVEL99 key. "Shown" is the moment HintBox_Update takes the text off its queue (0x001cdf7c), which can be later than the HUDSetTutorialText call: a text with <DISPLAYTIME n> holds the box for n ms first. Updates count from the copy's first; the gaps are where the driver was stopped and the game idled, so only differences within one stretch mean anything. The hooks are lua-exec, c-call, c-call-args, game-hint and hint-show of repo:research/traces/patches/. Confirmed (runtime).

Update Lesson Shown (and what the script set)
0 1 TT_20a (marker 01 shown)
164-165 1 marker 01 entered: dyn_w_mission02 destroyed, TT_20c, dyn_w_mission03 shown
280 1 marker 03 entered: dyn_w_mission03 destroyed, the text cleared twice; then the intro scene (per the lesson table)
497-866 2 callback P1.BasicAttacks; TT_22a, TT_22b, TT_19a (the grab step), TT_22a, TT_22b, TT_19c (the mount step), TT_22a, TT_22b
884-929 2 callback cleared; TT_19b
1009-1239 3 TT_26a (<DISPLAYTIME 3000>), then TT_26b 91 updates later (set at 1009, shown at 1100); pad handler P1.Target at 1129, cleared at 1239
1357-1726 4 callback P1.LightCombos: TT_23a, TT_23b; TT_23e with callback P1.HeavyCombos, TT_23f, TT_23d; callback cleared
1756-1916 5 TT_24a with callback P1.Power, TT_24b; callback cleared at 1916
1916-2278 6 TT_24d with callback P1.PowerMove; TT_24e after the first power strike; callback cleared at 2278
2508 7 dyn_beerbottle destroyed, TT_25a; then the scene l99_c7 (per the lesson table)
2659-2857 7 TT_25c with callback P1.Snaps, shown again after each counted snap
2993-3188 8 callback P1.Throws; TT_25e (shown again after each counted throw); callback cleared at 3188
3390 9 TT_25h (take a bat), after the scene l99_c8 (per the lesson table)
gap
19654-19903 9 the bat taken: callback P1.Weapons; TT_25i (16 updates later), TT_25j, TT_25k, TT_25k; callback cleared
19963 10 TT_28a (60 updates after lesson 9's end)
20745 10 TT_28b (the meter full, P1.CheckRage)
gap
35827-36588 10 TT_28e (<DISPLAYTIME 3000>), TT_28f 93 updates later, callback P1.RageMoves at 35933; TT_28h and the callback cleared at 36156, TT_28d 91 updates later, callback P1.RageMoves at 36261; cleared at 36588
36648 11 TT_30
38396 12 TT_40
40057 SetCheckPoint(2) and the two HUDSetObjective(2, …) lines

Points a playthrough test needs:

  • The lesson texts are the ones in the table above that are not TT_19*. They come in this order on every play.
  • TT_19a, TT_19b, TT_19c and TT_19e are the grab hints: tap circle to grab, L2 to let go, hold circle for the mount, circle to pick up a downed enemy. The lesson handlers show them on a step that needs a grab. The same texts also show whenever the player's state calls for them. In lesson 6, a circle at a bum lying on the ground showed TT_19e and went into the pick-up, where the power strike does not start. So how often they show depends on the driver: assert the lesson texts in order, and allow the TT_19* texts between them.
  • The state hints (SetupStateCallbacks, UpdateStateCallbacks, PlayerState and StateSwitch in level99.lua; below) decide which of the two a step shows: the grab or pick-up hint when the player is idle or mounted, the lesson text while he holds someone.
  • Repeats: TT_25c, TT_25e and TT_25k are shown again for each counted move. A test that checks the order should merge consecutive repeats.
  • The <DISPLAYTIME> texts (TT_26a, TT_28e, TT_28h) hold the box for about 90 updates. The next text set with them waits in the queue that long.

The state hints, and what lesson 6 counts

The research question "P1.PowerMove: TT_19a or TT_24e?" and user checklist item 33's hint order. Inferred from the disassembly of level99.lua (main/29-main/34) and level99_combat.lua (main/30, main/31): names, ids and the order of calls only.

The mechanism. SetupStateCallbacks(idle, mount, frontGrab, rearGrab) registers the anim callback PlayerState on five of the player's clips: ANIM_FIGHT_IDLE, ANIM_NORMAL_IDLE, ANIM_MOUNTING_IDLE (210), ANIM_GRAB_HOLD_FROM_FRONT (82) and ANIM_GRAB_HOLD_FROM_REAR (84) (anim callbacks). It stores the four texts in globals and calls StateSwitch(STATE) at once. UpdateStateCallbacks stores four new texts and calls StateSwitch(STATE); RemoveStateCallbacks stops the switching (DISABLED) and clears the text; DisableStateCallbacks deletes the five callbacks.

PlayerState(h, anim), when one of those clips starts: either idle → STATE = IDLE, 210 → MOUNT, 82 → FGRAB, 84 → RGRAB. It first saves the old state, sets the new one, and only when the two differ (and the switching is on) calls StateSwitch(STATE) with the new state. StateSwitch clears the hint and shows the text stored for that state. So the text always describes the state the player is now in, never the one he left.

Lesson 6 (P1.SetupPowerMoves, then the callback P1.PowerMove):

idle mounted front hold rear hold
set-up TT_19a (tap circle to grab) TT_19e (circle to pick up) TT_24d (the power move) TT_24d
after the first power strike TT_19a TT_19e TT_24e (the extended power move) TT_24e

P1.PowerMove(anim) keeps tPower3 = {ANIM_GRAB_POWER_01_STRIKE_01, ANIM_GRAB_POWER_01_STRIKE_02} = {57, 59}. Step 1 counts only 57: Rembrandt's line l99_t1_017 and UpdateStateCallbacks(TT_19a, TT_19e, TT_24e, TT_24e). Step 2 counts only 59: BrDead(player, true), RemoveStateCallbacks, the callback cleared, P1.PowerMovesDone 2500 ms later. Any other id is ignored.

So the answer is both, by state: TT_19a is the idle text all through the lesson, TT_24e the holding text after the first power strike. The lesson counts only the grab's power strike from the front hold (57, then its extension 59): the standing cross + square (653, lesson 5's special), the mount's power strike (225) and a power strike from the rear hold (scored as 80, the power strike) never count. The texts that read like a standing move are only ever shown while the player holds someone, and a player who lets go is told to grab.

What the user saw in Coney (item 33, steps 1-7): standing he got the power-move text; grabbing, the grab hint; mounted, the power-move text; picked up again, the pick-up hint; let go, the power-move text; grabbed, the grab hint; after 57 standing, TT_24e. Every one is the text of the state he had just left. Coney runs the script itself (src/scripting/anim_callbacks.h), so the lag is in what reaches PlayerState: an anim callback fired one clip late, for the clip that just ended, or the five ids not fired when a chain or an end hook starts them (82 after the connect, 210 after 118, the idle after a paired clip). Not traced in Coney. The original's run in the hints in order shows TT_24d at 1916 because lesson 5 ends in the strong grapple's hold (82), so STATE is FGRAB when lesson 6 sets up.

Coney fixes (item 33 (a) and (b); the hint box itself is on HUD):

  1. Fire the anim callbacks on the update a clip starts, with that clip's id, whether it was started directly, by a chain or by an end hook (anim callbacks); 82, 84, 210 and both idles must each reach PlayerState once per start. A unit test: grab (82 fires), mount (210 fires), get off (an idle fires), in that order, with nothing stale.
  2. SetupStateCallbacks and UpdateStateCallbacks show the text for the current STATE at once (the state kept from the earlier lessons, not reset to idle).
  3. The lesson-6 callback counts 57 then 59 only; Coney's standing cross + square must not reach it as 57.
  4. Playthrough test (level99 cp1): in lesson 6, standing shows TT_19a; front hold shows TT_24d; after 57, back in the hold or holding, TT_24e; mounted TT_19e; and only 57 → 59 from a front hold advances.

Hint timing, lessons 5-8

Every hint call, shown hint and callback of lessons 5-8 over two plays of the original (2026-10-11, PCSX2 2.9.94, the same course and state as above, 3,600 updates each), by update from P1.SetupPowerAttacks, the call that starts lesson 5. Updates are tick counts halved, so two events in one update are exact; where the runs differ the second run's update follows a slash. "set" is a HUDSetTutorialText call, "shown" the 0x001cdf7c show, "anim" an animation callback and "tutorial" the tutorial callback with its anim id. Method and hooks: The queue at runtime. Confirmed (runtime).

Update Lesson What ran Hint calls Shown, in that update
0 4 → 5 tutorial 17, P1.HeavyCombos (lesson 4 done); P1.SetupPowerAttacks nil nothing
29 5 scheduled from the set-up nil, TT_24a TT_24a
52 5 tutorial 653, P1.Power step 1 nil, TT_24b TT_24b
157 5 the strong grapple connects: anim 82, PlayerState nil, TT_19b TT_19b
191 5 → 6 the hold plays, its hit scored: tutorial 82, P1.Power step 2 nil, nil, TT_24d TT_24d
195 6 the course's next press mounts: anim 210, PlayerState nil, TT_19e TT_19e
270 / 269, 298 / 297 6 tutorial 223 (mount strikes), not counted
347 6 a grab connects: anim 82, PlayerState nil, TT_24d TT_24d
392 / 393 6 the power strike: anim 358 (its follow-on idle), PlayerState; then tutorial 57, P1.PowerMove step 1 nil, TT_19a; nil, TT_19a TT_19a twice
420 / 421 6 tutorial 59, P1.PowerMove step 2, P1.PowerMovesDone nil nothing
603 7 the scene's end (PreCashTheWorld) nil, TT_25a TT_25a
752 7 ClearMessage (scheduled 5,000 ms after TT_25a); P1.StartSnaps nil; nil, TT_25c, nil, TT_25c TT_25c twice
761 / 762 7 tutorial 11 (a cross), not counted
907; 953 / 908 7 tutorial 25 or 27, P1.Snaps (first and second snap) nil, TT_25c each TT_25c each
1043 / 1042 7 → 8 the third snap: P1.Snaps, P1.SnapsDone nil, TT_19a TT_19a
1091 8 a grab connects: anim 82, PlayerState nil, TT_25e TT_25e
1137 / 1137 8 the throw: anim 358, PlayerState; then tutorial 151, P1.Throws nil, TT_19a; nil, TT_25e TT_19a, then TT_25e
1194 8 a grab connects: anim 82, PlayerState nil, TT_25e TT_25e
1240 / 1241 8 the throw: anim 358, PlayerState; tutorial 151, P1.Throws, P1.ThrowsDone nil, TT_25e; nil TT_25e, then the box empty

(TT_25b and TT_25c, and TT_25d and TT_25e, are the same text; the table names the ones the lesson table above uses.) What a test can rely on:

  • No lateness: every text was shown in the update its HUDSetTutorialText call ran (0 updates late), because a nil cleared the box first and the box then showed the new text at once (Queueing). The only late text in lessons 1-8 is TT_26b (lesson 3), queued behind TT_26a's <DISPLAYTIME 3000> and shown 92 updates after it. So a playthrough test can assert that each lesson text shows in the update its callback ran, and that the box is never more than one text behind.
  • PlayerState and the lesson callbacks never share an update in lesson 5: the grab's anim callback runs at the connect, 34 updates before the hit P1.Power counts (157 and 191 above, both runs). They do share one in lessons 6 and 8, where the follow-on idle 358 is built when the strike or throw starts; there PlayerState runs first and the lesson's text is the one left showing.
  • The last text of an update is what the player sees. In lesson 5 the course's mount at 195 replaced TT_24d after 4 updates; a player who stays in the hold keeps it. Grab hints (TT_19*) between lesson texts depend on the driver, as above.
  • TT_24e (the extended power move) did not show in these runs: P1.PowerMove's step 1 set TT_19a (grab again) instead, where the 2026-10-07 run above showed TT_24e. The state hints explain it: the follow-on idle 358 had already set STATE to idle when step 1 ran, and the idle text is TT_19a; a player still in the hold gets TT_24e. Inferred.

Checkpoints 2 and 3

level99_lesson1 (P2, checkpoint 2, the street) and level99_lesson2 (P3, checkpoint 3, the run to the dealer), in order. Each step is started by the trigger in its row; the mechanics are on the linked pages. Inferred from the disassembly of the scripts (names, ids and values only).

Step Started by What the script sets up Waits for Mechanics
2.1 set-up P2.SetupLesson1 the Warriors remade for the checkpoint, flash command 40 off, both players at 35 % health and demi-god 0.25, scene l99_c2 the scene's end Scenes
2.2 store P2.EndStreetScn Vermin's talk prompt (message 0), CfgInventoryCallback(P2.UpdateLootCount), vInsideStore message 3 three items of item 10 Breakables, inventory callback
2.3 car P2.SetupCars, 3 s after the third item CarMakeGoodAsNew and CarSpawnRadio on the three cars, the locked camera VerminCar, Vermin's line; then CfgSetSteroTheftHandler(P2.CarRadioStolen) the first stereo a player steals (radioNeeded 2 is never read) Cars: windows, the theft, context kind 3
2.4 mugging P2.SetupPeds, 3 s later PoizoCiv sent to the phone flag wPhone01 (dead brain, GoalMoveToUseFlag), the bums (GoalBumLogic, GoalMoveToUseFlag), HuSetMugCallback(P2.PedMugged) on both players, SetInterrogateParam (below), message 18 on PoizoCiv (P2.MugPedDied: a hint only); after 3 s FlagNetTraverse(PoizoCiv, 1, 0) walks him off the callback with success true; then all-zero SetInterrogateParam Mugging, who and the callback, AI
2.5 Vermin P2.SeeVermin, 5 s later the camera VerminWait, ObjShow(dyn_objective_w), Vermin a radar objective (texture 27), command 38 on after 2.5 s vFenceSection message 3 → SetCheckPoint(3), level99_lesson2 HUD
3.1 set-up P3.SetupLesson2 the dealer FlashDealer with GoalDealer(dealer, 0, 17, 0, 0, false) and messages 1 and 16 (P3.DealerHit), Vermin (god mode, unpushable, ungrabbable, fast climber), scene l99_c3 the scene's end GoalDealer
3.2 fences P3.EndChapterScene DoorOpenDegree(WXL00, 100), command 40 off and 38 on, Vermin (secondary camera target) to fVerminFencePoizo vFenceJump 3, the flag's 8, then vFenceJump2 / 3 texts Climbs, AI fence climb
3.3 climb vClimb 3 (P3.MoveToClimb) Vermin to fStop2; P3.GetBack(1) 10 s later flashes his radar icon while the player is lost fStop2 8 → the camera ClimbPoizo, the players put at fClimbWarp; vFirstClimb / vDoneClimb 3 Characters
3.4 rooftops vReachRooftop / 2 3 (P3.PoizoJump) ObjShow(dyn_w_mission), the camera JumpCam, the players put at fRoofTeleport, radar target fRadarJump vReachWindow 3 (both players with two, below), then Vermin's run to fStop3, his line, vVerminJumps 3 Jumps, Moving into a pane
3.5 wooden fences vAlmostDone 3 (P3.WoodenFences) the players put at fBreakFencePoizo (a held object destroyed), two locked cameras, message 2 on dyn_door_fence02 and 03 either fence's message 2 (P3.FenceBroken) Barriers
3.6 dealer P3.DealerPoizo, 0.5 s later CfgSetGlobalTimeToLive(1000), money raised to at least $20, the dealer's icon dyn_flashdeal, TriggerSphereCfg(dealer, true, 4, 2, 500) with messages 3 / 4, the scene l99_c9 preloaded message 3 on the dealer → P3.CheckForFlash every 0.5 s until InvNumberOf(1) ≥ 1 Buying, trigger spheres
3.7 flash P3.FlashSpeak, 3 s after the buy CfgSetGlobalTimeToLive(10000), command 40 on, PadSetHandlerEx(P3.UseFlash) command 40 (d-pad right) → P3.MissionOver 1.5 s later The flash, How the mission ends

Points an implementer needs:

  • 2.4: SetInterrogateParam(160, 75, 255, 5000, 2500, 20000, 40, 60, 20000, 0, 0) makes the mugging 5 s on target with 20 s off target allowed (Mugging). The mug callback runs for every end of a mugging; the lesson ignores a false one.
  • 3.6: a player hitting the dealer (message 1 or 16) fades out, removes his gang and makes him again after 2 s, the players put back at fHitDealer. Triangle in the dealer's 1.75 m buys (Buying): $20 for one flash, refused at 3 carried; a dirty dealer would take the money and give nothing.
  • 3.7: the last step waits only for the command; the flash itself heals the player when he is hurt (he is at 35 % from 2.1) and does nothing at full health without upgrade (6, 8).
  • 3.4: the jump is the player's own (Jumping) and the script never touches the window: the two type-11 panes in it break when the jumping player reaches them (Moving into a pane). P3.JumpPoizoDone arms vReachWindow (corner 43.2, −4.3, 4.1, size 4 × 6 × 4, just past the panes at x 47.4) with P3.ReachWindow, which counts only players. With one player, his entering removes the handler and runs P3.Player2Jumps. With two, each player who enters is set brain-dead (BrDead, so he gives up the pad, AI) and counted; the second removes the handler and runs P3.Player2Jumps (player 2 arriving first sets bWindowWait; if he then drops out, player 1 still has to enter). P3.Player2Jumps marks the objective done, flushes the Warriors' brains and sets the gang dead (the player is Warriors.Rembrandt, so he too gives up the pad), gives each player GoalAddressPerson towards Vermin, turns the follow camera off with Vermin as its secondary target, destroys the marker dyn_w_mission and sends Vermin to fStop[3] (P3.VerminInWindow on arrival). A jump enters the box in the air (at about x 47.2), so the pad is lost mid-jump and the jump finishes on its own. Confirmed (runtime) in the jump of Moving into a pane, one player, the stick held at 100 % throughout: he landed with 435, the jump end for a centred stick (436 follows a held one, Characters), slid to a stop 0.57 m on, then turned on the spot about 164° (heading 88° to −108.6°, clip 398) in 14 updates and stood idle. If player 2 drops out while player 1 waits at the window, level99.lua's drop-out handler runs P3.Player2Jumps itself.
  • 3.5: a barrier's message 2 is its removal, not its hit: the class update (0x003b3220, every 60 ticks) reports a broken barrier and WorldObject_Remove (0x00391c10) sends message 2, up to 1 s after the breaking hit (Barriers, confirmed (code) and (runtime) there).

Player 2, Vermin's runs and the fence (user checklist 41-44)

What the original does where Coney was seen to differ (user checklist items 41-44, 2026-10-08). The script facts are read from the disassembly of level99.lua, level99_lesson1.lua and level99_lesson2.lua (names, ids and values only) and are inferred unless marked; the engine facts are cited.

Item 41: Ash in a one-player game. Ash exists in every checkpoint, one-player games included, and is kept out of sight; he never joins unless a second pad does.

  • Each checkpoint's Warriors function (AddWarriors2 for 1, AddWarriors for 2, AddWarriors1 for 3) makes Rembrandt with HuCreate(..., player 1, ...) and Ash with HuCreate(..., player 2, ...) at z −8.5, 8.8 m below the street ((−285.5, 125.1), (66.2, 52.7), (68, 52.7)); the drop-out flags fAshTeleport1 / 2 are at z −8.5 too. HuCreate's ground snap is a 2.5 m ray down from 1 m above the given point (Characters: creation), which finds nothing that deep, so Ash stays there. Confirmed (code) for the snap; the positions are the script's.
  • HuCreate's player argument only makes a player when it is 1; 2 is stored as the hand-over priority +0x1b1 and Ash is an ordinary AI human, so HuIsAPlayer(Ash) (player slot +0x1b0 not −1) is false until a second player joins. Confirmed (code) at 0x00233d60, 0x00235758.
  • Checkpoint 2's set-up (level99_lesson1.lua, run after RunLevel names player2 = Warriors.Ash): when HuIsAPlayer(player2) is false it removes his radar blip (HUDDeleteRadarObject) and suspends his brain (BrSuspend(player2, true)); either way it registers F.Player2Join with SetPlayer2Callback (a global.lua helper the multiplayer callback InventoryPlayer2 calls on). The scene l99_c2 binds Ash as its fourth human only when HuIsAPlayer(player2), else NilHandle. Every later step that moves player 2 (fStoreFront2, fStreetCenter2, fRoofTeleport2, fBreakFencePoizo2, fHitDealer2, ...) is guarded by the same test.
  • Joining. Only GameState_SyncPlayers (0x0041a460) makes player 2: when joining is allowed (+0x56e8, CfgMultiplayerJoin) and a second pad comes in, it takes the player gang's next human by hand-over priority (Gang_PickNextPlayer; Ash's 2), calls Human_MakePlayer and then the multiplayer callback with (Ash, true) (GameState_CallMultiplayerCallback, 0x0041b148). Confirmed (code). A second pad joins on START: the level mode's update (0x00158728) calls GameState_SyncPlayers(pad) for a pad with START held.
  • Leaving. A pulled pad does not release player 2: the pad callback Pads_Init installs (Player_UnbindPad, 0x0022a980, passed at 0x00149630) only unbinds that pad from the HUD and raises the controller error message (Gm_Error_SetPadMessage). Player 2 drops out through the pause menu's Player 2 quit (PauseMenu_QuitPlayerTwo, 0x001df3a8): it clears the two-player flag and calls GameState_SyncPlayers(-1), whose release branch runs only with that flag clear and calls Human_ReleasePlayer and the callback with (Ash, false). Confirmed (code). F.Player2Join(h, true) resumes his brain, adds his blip and puts him beside Rembrandt (HuTeleportNearHuman); with false it suspends him again and sends him back to fAshTeleport2 (z −8.5).

Coney fixes (item 41):

  1. HuIsAPlayer must be false for a human made with player argument 2 (or anything but 1) until a second player joins. Coney's answer for a human the scripts made before the level's humans exist (ScriptedHumans::status, repo:src/ai/scripted_humans.cpp) is playerIndex > 0, which makes Ash a player during the set-ups that run then; it should be true only for the human that became player 1.
  2. Keep Ash where HuCreate puts him: no snap that lifts him from z −8.5 onto the street (the original's ray only looks 1.5 m below the point), and his suspended brain must not walk him anywhere.
  3. A player 2 is made only by a second pad joining (with CfgMultiplayerJoin allowing it), then the callback runs with (Ash, true); never by the level set-up.
  4. Test: the one-player Story path into checkpoint 2 (from the l99_c1 wheel scene's hand-over, and a checkpoint-2 start) leaves Ash at z −8.5 with his brain suspended, off the radar and out of l99_c2; with a second pad joined, F.Player2Join puts him beside Rembrandt.

Status (2026-10-20): fix 1 was the cause, on a direct start only. There P2.SetupLesson1 runs while the level's humans are still being made, and Coney answered HuIsAPlayer(Ash) true from his player argument 2, so the set-up kept him on the radar and awake and cast him in l99_c2, which left him on the street at (46.4, 46.5). Now only argument 1 answers as a player. The hand-over from checkpoint 1 already asked his live brain and was right. Fix 2 needed nothing: Coney leaves him at (66.2, 52.7, −8.5), as the original keeps Ash at fAshTeleport1 (−287.80, 124.80, −8.53) on the checkpoint 1 state, standing and not falling (confirmed (runtime), PCSX2, 2026-10-20). Fix 3 does not arise: Coney has no second pad. Test: repo:tests/platform/disc_level99_player2_test.cpp, both ways into checkpoint 2.

Item 42: Vermin runs to both jumps. After the player's window jump, P3.Player2Jumps sends Vermin from fVerminWait (62.8, −3.3, 4.3) to fStop[3] (41.3, 2.8, 4.3) with GoalMoveToFlag(Vermin, fStop[3], 4, 0, 0, 0.3, 0, true): gait 4, run, arrival 0.3 m, then the flag's heading (127°). That leg crosses the window (the panes at x 47.4) at the same height, so on the route it is a jump leg. P3.FinishRun sends him on to fStop[4] (41.5, 19.4, 4.2) with gait 4 again, through vVerminJumps. The only walk order in the checkpoint after the fences is fStop[6] (gait 2, item 43). The gait ids are those of GoalMoveToFlag (2 walk, 3 jog, 4 run, 5 sprint, bindings).

Nothing on the way slows him for the jump, confirmed (code):

  • The move's corner speed (MoveAction_CornerSpeed, 0x002fc158) simulates only the points that MoveAction_CollectPoints (0x002fbd18) gathers, and that stops at the first link with bit 0x4 or 0x80 (a jump or drop link): the take-off point is the last point considered and nothing past the window is, so the jump itself is not a corner (Moving: the move action).
  • An AI starts a jump at any gait, from a run too, with no run-up test (Human_BeginJump, 0x0023db48), and the landing here is level with the take-off, so it is a jump, not the slowing drop (Jumps and drops).

At runtime (2026-10-11, PCSX2, confirmed (runtime); scenario vermin_window_jump in repo:research/traces/scenarios/, the user's slot 9 copied, hook event-call). The player runs at the window with the stick at 100 % straight ahead and taps triangle 59 updates in, as in glass_jump, lands at x 44.5, then stands still. Vermin's human +0x1a8 (gait), +0x1ac (speed), position and top goal were read every update:

  • First leg. GoalMoveToFlag (Vermin, fStop[3]) was called 80 updates in, as the player came down. The top goal was MoveToFlag (vtable 0x00542130). From 82 updates in Vermin went gait 2 → 3 (87) → 4 (95), and from about 101 updates in held 7.80 m/s, the run speed, with clip 410 (ANIM_MOVEMENT_RUN). Gait 4 and 7.80 m/s held on every update up to the take-off at 135, at (50.74, −2.83), 3.3 m before the panes at x 47.4. Then came gait 5 and the jump clip 434: 0.318 m an update across the ground (9.55 m/s), 3.5 m/s up at the start, 0.523 m/s less each update. He crossed x 47.4 in the air (z 4.58-4.53, 0.4 m above the take-off) and landed at 153 at x 45.01 (clip 435), then walked on and stopped at fStop[3] (41.46, 2.63) about 220 updates in.
  • Second leg. GoalMoveToFlag again at 300 (P3.FinishRun), gait 4 from 328 at 7.80 m/s up to the take-off at 346 (y 8.95). The jump went 9.40 m/s along y with 5.0 m/s up; he landed at 369 (y 16.15) and stopped at fStop[4] (41.50, 19.44).
  • So the gait is 4 (run) right up to the take-off on both legs, with no slowing before the window; the gait-5 reading during the jump is the jump's own speed (above 9.02 m/s, the gait-5 bound for Rembrandt's speeds, confirmed (code) at 0x0022aeb0, Speed classes), not a sprint order.

Coney fixes (item 42):

  1. Map GoalMoveToFlag's gait 4 to the run gait and keep it through a route's jump leg: the move's speed is the gait's speed up to the take-off point.
  2. Leave the jump out of the corner-speed look-ahead: collect corner points only up to the first link with bit 0x4 or 0x80, so the turn after the landing does not brake the run-up.
  3. Test (level99 checkpoint 3): from P3.Player2Jumps, Vermin's gait is Run (and his speed the run speed) for the updates before he leaves the ground at the window, and again on the fStop[4] leg.

Status: fix 2 is built (RouteFollower::cornerPoints); with it the gait 4 order runs him at 7.80 m/s up to both take-offs, where before the corner after each landing held him at 2.80 m/s. Test: repo:tests/platform/disc_level99_vermin_fence_test.cpp, which also checks his fence climb in step 3.2 (issue #232).

Item 43: where Vermin faces in the fence lesson. P3.WoodenFences (the player entering vAlmostDone) flushes Vermin and teleports him to fStop[5] (29.5, 6.4, 0.3) with heading −1, so the flag's heading, 273°; the game's heading grows clockwise from +y and faces (sin h, cos h) (Characters), so he faces (−1.00, 0.05), −x, towards the locked camera VerminFencePoizo (25.282, 6.984, 2.012), not towards the fences. He then speaks and plays point_left.anm (GoalPlayDynAnimation); facing −x his left is −y, where the two fences stand (dyn_door_fence02 at (23.6, −6.67), 03 at (27.98, −6.67)), so the gesture points at them. Its callback P3.ShowFence cuts to BreakFencePoizo and, 3 s later, P3.BreakFence gives control back (CamSetFollowAngle(-10)), arms both fences' message 2 and sends Vermin walking (gait 2) to fStop[6] (21.4, −4.1, 0.3) with the face-flag on: there he turns to its 139°, facing (0.66, −0.75), straight at dyn_door_fence02 (2.2 m east and 2.6 m south of the flag). The player is put at fBreakFencePoizo (26.1, 3.8), heading 184°, facing the fences.

Coney fixes (item 43):

  1. TeleportToFlag(h, flag, -1) turns the human to the flag's own heading in the game's clockwise sense (heading 273° faces −x); check that Coney's flag headings are not taken anticlockwise (273° would then face +x, away from the camera, and point_left would point north, away from the fences).
  2. After P3.BreakFence, Vermin walks to fStop[6] and turns to 139° on arrival (faceFlag).
  3. Test (level99 checkpoint 3): during the point, Vermin's heading is 273° ± 5° (the game's sense); after his walk, 139° ± 15° at fStop[6].

Status (2026-10-20): fix 1 was the cause. Coney took every flag's heading, and Teleport's and TeleportToFlag's, in HuCreate's anticlockwise sense, so Vermin faced +x, away from the camera, and every goal that turns a human to a flag turned him the mirror way; the sense is now confirmed (code) and (runtime) on Flags, and Coney negates those headings. Fix 2 needed nothing more. Test: repo:tests/platform/disc_level99_fence_lesson_test.cpp (Vermin within 5° of 273 after the teleport, the player within 5° of fBreakFencePoizo's 184, Vermin within 15° of 139 at fStop[6]).

Item 44: what waits for the fence. Nothing in 3.5 fades the screen. P3.WoodenFences letterboxes in (ScreenQueueEffect(2, 1)) and P3.BreakFence letterboxes out (ScreenQueueEffect(3, 1)) before the player can move; from then on only the fences' message 2 goes on: P3.FenceBroken (either fence) clears the hint, Vermin's blip and icon, sets the global time-to-live to 1000 ms and the player's money to at least $20, and schedules P3.DealerPoizo 0.5 s later. P3.DealerPoizo cuts (CameraMakeActive(DealerPoizo, 0), no fade) to the dealer's locked camera, teleports the players to fHitDealer / 2 and Vermin to fDealer, shows TT_9 and arms the dealer's trigger sphere. Message 2 itself comes only from the fence's removal, 0-29 updates after the breaking hit (Barriers); the charge's start, its run-up and a hit that leaves hitpoints send nothing. So the order is: the hit that breaks a fence (one charge, 10 hitpoints, kind 2) → the fence removed and message 2 → 0.5 s → the cut and the dealer set-up.

Item 44 at runtime: the gap from the hit to the dealer camera (issue #233). Confirmed (runtime), PCSX2 2.9.94, slot 10, scenario fence_dealer (the charge_fence input: L2 held, stick 100 % straight ahead, cross 3 m before the fence; hooks barrier-hit, object-remove, event-call, event-callback; field set screen: player 1's current camera 0x005d9150, view 1's fade level and letterbox level at *0x005fdeb8 +0x1d8 / +0x1ec), two runs, with window captures of the second:

Moment Run 1 (update, tick) Run 2 (update, tick)
the breaking hit (0x003b2180, 10 hitpoints); the three boards removed 29, 0xad00 29, 0xacd0
the player stopped at the fence (speed 0) 31 31
the fence removed (WorldObject_Remove 0x00391c10), P3.FenceBroken in the same update 56, 0xad37 (+27.5) 50, 0xacfb (+21.5)
P3.DealerPoizo called (ScheduleFunc("P3.DealerPoizo", 500) at the removal) 70 64
first sample on the dealer camera, the player at fHitDealer (18.5, −16.3), heading 178° 71 65
hit → cut 42 updates (1.4 s) 36 updates (1.2 s)

So the page was right, with one correction: the removal waits for the fence's next 60-tick update, which is 0 to 29 updates after the hit depending on the fence's phase (five runs so far: 3, 10, 17, 21.5 and 27.5), not 3-17. The removal to the cut is fixed: P3.DealerPoizo runs 14 updates after P3.FenceBroken (500 ms on the game clock) and the dealer camera is on screen in the 15th update. The original's hit-to-cut gap is therefore 15 to 44 updates (0.5-1.5 s), about 1 s on average; it is never "at once". In the screen effects nothing moves: the fade and letterbox levels stay 0 and player 1's current camera stays the follow camera from the charge until the cut, which is a plain switch (CamEnable(3, …), CameraCreateLocked("DealerPoizo", …), then CameraMakeActive(<it>, 0)) in the same update as the teleports and TT_9. What the player sees in the gap (captures of run 2, inferred from those frames): the fence is gone at the hit (hidden, its three boards and dust thrown), the player stands at the gap where the charge stopped him and then plays a short recovery animation (clip 388 from about 17 updates after the hit), the follow camera stays put, the fence lesson's hint stays until P3.FenceBroken clears it, and the next frame is the dealer camera. The user's "almost at once" is not borne out; a short phase (3 updates) gives an 18-update (0.6 s) gap, which may be what was seen. Ruled out: no ScreenQueueEffect, HideHud or camera binding between the hit and P3.DealerPoizo.

Coney fixes (item 44):

  1. Find what fades when the charge starts: no binding call of the checkpoint does (the event log of the run shows every ScreenQueueEffect), so the fade comes from Coney's own code on the charge (a scene or camera transition, a respawn or reset fade, the player falling) and must go.
  2. Send a barrier's message 2 only from its removal after it has broken (the 60-tick class update), never on the charge's start or on a hit that leaves hitpoints. The fence is hidden at the hit itself, not at the removal.
  3. P3.DealerPoizo runs 0.5 s (14 updates) after P3.FenceBroken and cuts to the dealer camera without a fade; the follow camera stays current, with no letterbox, until then.
  4. If Coney's gap is longer than 44 updates, the extra time is Coney's own: check where the barrier's 60-tick countdown is restarted (it must keep its phase from spawn, never restart at the hit) and that ScheduleFunc's 500 ms is counted in game time. Coney, measured (issue #233, repo:tests/platform/disc_level99_fence_dealer_test.cpp, four charges at 98 % stick with L2, cross 3 m out, the start moved to meet the fence's phase at both ends): the hit-to-cut gap was already inside the original's window, 16 to 43 updates (removal 0-29 after the hit, P3.DealerPoizo 14 after it, the cut in that update), no fade and no screen effect, the charge stopped 1-2 updates after the hit and the fence hidden in the hit's own update. What stayed on screen was the three boards the break throws: Coney left them standing in the gap until the cut, while the original removes them in the hit's update (Barriers); fix 2's last sentence holds for them too, and they now go on the tick after the hit.
  5. Test (level99 checkpoint 3.5, the wooden fence): with the hit at update H, the fence is removed and P3.FenceBroken runs at an update R with H ≤ R ≤ H + 30; the dealer camera is current and player 1 at fHitDealer at R + 14 or R + 15, so the cut lands within H + 14 … H + 45; between H and the cut no ScreenQueueEffect is called and the fade and letterbox levels stay 0.
  6. A charge from a stand (issue #106, the user's "the screen fades as soon as the charge starts"). The guess was that the charge's attack warning reaches the flash dealer, whose message 16 runs P3.DealerHit and fades out. In the original the charge warns no one (confirmed (runtime), PCSX2-MCP 2.9.129, slot 10 with the charge_fence input and a hook on Human_WarnOfAttack: 0 calls over the charge, fade and letterbox 0, the hit at update 29, P3.FenceBroken at 45 and P3.DealerPoizo 14 later, as in fix 3); inferred from the code, its search finds no one between 3.29 and 4.11 m ahead of the charge, and the dealer stands about 13 m off behind the fence. Coney, as a disc test (repo:tests/platform/disc_level99_fence_dealer_test.cpp, both fences, through the game session), with the straight charge and with a player who first walks up to 4.8 m short of the fence and stands 2 s (inside the dealer's 17 m, so he turns to face him): no P3.DealerHit, no screen effect and no fade before the break, on main as it stands (2026-10-21). The fade the user saw on the build of 2026-10-08 does not reproduce.

level80.lua (mission 2)

Story mission 2, loaded by runNextMission after level99: Cleon (player 1) and the Warriors after the Destroyers torch the clubhouse. Read from the disassembly of level80.lua and its four chapter scripts (names, ids and values only); bindings on Story coverage. Inferred unless marked.

  • The main chunk sets CHAPTER = GetCheckPoint(), the chapter table (1-4 level80_chapter1 to _chapter4) and the Warriors set-up per checkpoint (PlayerGang), and runs Main (unless the debug SCENETEST is set). Main: LoadState = GetCheckPoint(), fog colour, the ambient sound boxes, the cop-car lights, the checkpoint's Warriors (player = Cleon, player2 = Vermin), the follow camera, RestoreHud, the radar, HUDRemoveAllGoalText, HUDSetObjective(2, LEVEL80.MP_1, 0, true) (the mission line), RunMission (preLoadFile(chapter, "Setup")), and PreloadSubway 2 s later: the passing-train scene Coney_Sub3 / Coney_Sub4 replays through SuperRunScene every 30 s for the whole level (a scene event a test should ignore).
  • NextMission runs the chapter's Cleanup, adds 1 to CHAPTER, SetCheckPoint(CHAPTER) and loads the next chapter in place, as in level3. TutorialEnable(on): on locks player 1's stick (HuLockPadMovement), Vermin brain-dead, all player commands off but 1 and 2; off undoes it.
  • Objectives are made with the objective helper, none with GoodToGo: the player alone entering the volume completes each. Warrior-command lessons each set one WCSetCallback and wait for that command from player 1 (R2 menu, HUD).
  • No scripted failure: no chunk calls HUDLaunchMissionFailed; the mission ends with the final scene's Final = true (how a mission ends).
Step Started by Hints, objectives, scenes (in order) Waits for Ends with
1.1 intro Setup scene l80_c1 (preloaded, RunIntroScene; three callbacks break club windows with molotovs) the scene's end Setup2
1.2 Let's Go Setup2 (EndIntroScene) Warriors teleported, Warrior command 3 issued, commands locked, Cleon's stick locked; Ajax and his girl made; locked camera StartCam1; 1.5 s: TT_1; 2 s: scene l80_make_out (Ajax, played with ScenePlayCinematic, looping); 5 s after TT_1: text cleared, TT_4, command 0 enabled, WCSetCallback("CommandIssued") command 0 (follow, R2 up) from player 1 text cleared; Vermin walks to fC1Vermin2
1.3 crew down CommandIssued none all three Warriors inside vWarriorsTrigger (message 3, counted with IsInsideBox) TutorialEnable(false), a walk-in camera StartCam2; 5 s: EndStartCam
1.4 to Ajax EndStartCam follow camera back, objective MS_C1_1 to fHangAjax (volume vAjaxTrigger) a player in vAjaxTrigger fade, l80_make_out ended, scene l80_c4; at its end Ajax joins, SetCheckPoint(2)
2.1 to Snow and Cowboy Setup (chapter 2) commands locked to 0 (issued); Snow, Cowboy and four cops (all god mode); objective MS_C2_1 to fSnowCowboyTrigger (sphere 2 m); vSnowCowboyTrigger (message 3, a player) sets the cops on Snow a player within 2 m of fSnowCowboyTrigger fade, scene l80_c5 (the arrest)
2.2 uncuff EndSnowCowboyScene Snow and Cowboy arrested at their flags (HuSetArrested), message 17 handlers, the Destroyer-cop and alley-bum events; locked cameras FenceCam, MoveCam (5 s), then the follow camera; 1 s later TT_11; 5 s later text cleared, objective MS_C2_2 (state 0, not silent) message 17 (freed, uncuffing) on both MS_C2_2 done (state 2); 1 s: SetCheckPoint(3)
3.1 to Fox Setup (chapter 3) Fox hiding (god mode) and two searching cops; objective MS_C3_1 to fFoxTrigger (volume vFoxTrigger) a player in vFoxTrigger fade, scene l80_c6
3.2 Hold Up EndFoxScene TutorialEnable(true), only command 3 enabled, TT_5, WCSetCallback("StartPlayers2"); the cops' search lines every 3-10 s command 3 (hold, R2 down) text cleared; objective MS_C3_1a (state 0, silent false, 2000), radar fBottles (texture 28), marker dyn_w_mission01
3.3 throw lesson vThrowTutorialTrigger message 3 (a player) MS_C3_1a state 1; the thrower walked to fThrowPlayer1 (brain-dead), locked camera ThrowCam; 1 s: TT_25; 5 s: text cleared, radar fBottleTarget, marker dyn_w_mission, objective MS_C3_2; 1 s: TT_26 (pick up), then every 200 ms by state: TT_27 (aim: holding, not in throw control), TT_28 (in throw control, not aimed at the marker), TT_29 (aimed: throw); back to TT_26 when the hand is empty. The stick stays locked; only command 10 (pick up), then 7 (aim), then 16 (throw) is enabled at each stage vVandalBox message 6 with the bottle inside vBottleBox (message 6) MS_C3_2 done (state 2), scheduled calls flushed, text cleared, Warrior command 0 issued, control back; fade, scene l80_c6_b
3.4 Wreck'em EndFoxScene2 Fox joins (hunter in hand), TT_7, TutorialEnable(true), only command 5, WCSetCallback("StartVandalize") command 5 (wreck, R2 down-left) text cleared; objective MS_C3_3, the front-door marker (fStoreFront, dyn_w_mission04, hidden while the player stands at it), front door pickable, CfgSetLockPickHandler(... "FrontDoorPicked"); 2 s: command 5 issued
3.5 store FrontDoorPicked (a player picks the front lock) text cleared, marker gone, command 5 issued, vStealTrigger message 7 counts 10 message 7 on vStealTrigger (objects stolen) MS_C3_3 state 2, objective MS_C3_4, the back door pickable with its marker (fStoreBack)
3.6 back door BackDoorPicked (a player picks the back lock) text cleared, MS_C3_4 done (state 2, silent) 0.5 s SetCheckPoint(4)
4.1 to Cochise and Swan Setup (chapter 4) Cochise, Swan and twelve Destroyers (suspended, god mode); objective MS_C4_1 to fCochiseSwan (volume vCochiseSwanTrigger) a player in vCochiseSwanTrigger fade, scene l80_c7
4.2 Attack EndCochiseSwanScene locked camera FightCam1, everyone teleported, Destroyers awake (messages 2 and 18 → DestroyerDied), fight music, TT_8, TutorialEnable(true), only command 1, WCSetCallback("CommandIssued") command 1 (attack, R2 up-left) text cleared, control back, Cochise and Swan join, HUDRemoveAllGoalText, objective MS_C4_2, command 1 issued, the Destroyers lose god mode; 5 s: TANDEM
4.3 watch your back Cleon's message 1 from a gang-type-19 human (hit by a Destroyer) command 2 enabled, text cleared, TT_9, WCSetCallback("EndWatchBackText") any command text cleared
4.4 the end DestroyerDied with no Destroyer of either gang standing MS_C4_2 done (state 2, silent), Cleon's line; at its end a fade, scene l80_c3, then l80_c3_a (the Destroyers' tag shown), then l80_c3_b with Final = true the last scene PreCashTheWorld → HUDLaunchMissionComplete()

Points a playthrough test needs:

  • Steps 4.3 and 4.2's TANDEM depend on timing: TANDEM shows 5 s after the attack order, TT_9 on the first Destroyer hit on Cleon, so their order can swap.
  • TT_26-TT_29 follow the thrower's state every 200 ms and repeat; merge consecutive repeats and allow them to go back (a dropped bottle shows TT_26 again).
  • Entered at checkpoint 3 or 4 (LoadState), the chapter's Setup also locks the commands and fades in; at 3 it sets up the Destroyer-cop and alley-bum events chapter 2 would have.
  • The scenes' ids: most chapter scenes are preloaded with one argument (ScenePreload("l80_c7"), no callback), 2-3 s after the chapter's Setup by a scheduled call, into a global (CochiseSwanScene, ...). That number is the SceneId SuperRunScene later stores as tblScene[SceneId] (global.lua function 125) because the table says Preloaded; a nil id stops the script there ("table index is nil"). So ScenePreload must return the scene's id with or without a callback name (the original returned 1744 for l80_c7), and the preload must have run before the volume is entered. Confirmed (runtime) for the order: Setup at update 54185, the preload 67 updates later.

The fight at checkpoint 4: where it starts and who engages

Play-test finding (2026-10-12): in Coney the Destroyers stand "lined up, rumble-style" when the fight starts, and walking past them does nothing until the player hits one. This section is how the original places and starts them, read from the disassembly of level80.lua and level80_chapter4.lua (names, ids and values only) and the code at the cited addresses, then watched in PCSX2 2.9.94 (below).

Who is made, and where (chapter 4 Setup; confirmed (disc) from the scripts):

  • AddCochiseSwan: gang CochiseSwan (kind 0) with Swan (type 6) and Cochise (type 16). AddDestroyers1 and AddDestroyers2: gangs Destroyers1 and Destroyers2 (kind 19), six soldiers each (types 192, 194, 197, 199). HuCreate puts the twelve in two tight rows of six (y 166.2 and 164.8, 0.6-0.7 m apart, all at 270°). That is only where they wait for the scene, which takes them over at once.
  • Both Destroyer gangs are suspended (GangSuspend), given the turf box vTurfDestroyers (as is CochiseSwan), barred from picking up weapons (BrSetThugWantsWeapon false) and put in god mode with Cochise and Swan. No gang gets a tactic and none a GangSetThreat: the threat response stays 2.
  • The player enters vCochiseSwanTrigger: scene l80_c7 with all twenty-one humans, each in JoinCinematic.

The placement at the scene's end (EndCochiseSwanScene): locked camera FightCam1; the crew flushed, made brain-dead and looking at Swan; then one TeleportToFlag per human, all in the same call, onto flags that AddFlagsBoxesPaths makes:

Who Flags Positions (x, y; z 1.3) Heading
Cochise, Swan fCochise, fSwan (−251.5, 150.4), (−252.1, 151.4) 75°, 50°
Destroyers1 1-6 fDestroyer1-6 (−251.0, 151.6), (−249.7, 150.2), (−251.5, 153.1), (−248.1, 150.2), (−249.0, 151.7), (−249.9, 153.0) 246°
Destroyers2 1-6 fDestroyer7-12 (−250.9, 154.5), (−246.2, 150.3), (−247.1, 151.8), (−248.0, 153.1), (−249.1, 154.5), (−250.3, 155.9) 246°
the seven Warriors fCS<name> about 12 m north-east, round (−238, 164); Cleon on fCSCleon (−238.0, 163.2) 226°-240°

So the twelve stand in a staggered block of about 5 × 6 m, 1.4-1.6 m apart, all facing one way, with Cochise and Swan at its south-west corner facing into it. They are placed all at once; nothing is spawned later and there are no waves. The flags are the original's own, so for one update the block looks just as planned in the original.

What makes it a brawl (the same function, after the teleports): both Destroyer gangs are resumed, then GangMakeEnemies(GangCochiseSwan, GangDestroyers1) and the same for GangDestroyers2. They are not made enemies of GangWarriors. The Warriors stay out of it: Cleon and Vermin get HuSetNoTarget, the stick stays locked (TutorialEnable(true)) and only the attack order is enabled (TT_8). The general AI does the rest, with no further script call:

  1. Gang_MakeEnemies (0x0016acf0) sets the enemy bit both ways through Gang_SetEnemyBit (0x001690c8), whose mask writers (Gang_WriteEnemyMask 0x00168f08, Gang_WriteFriendMask 0x00168f30) stamp the gang's change time +0x34. Each member's Brain_MaybeScanEnemies (0x0028fa18) then scans on its next update, because the gang changed since its last scan (The enemy scan), five brains an update. Confirmed (code).
  2. Each Destroyer lists Cochise and Swan (in range and in sight); Cochise and Swan list the twelve.
  3. With no tactic, a threat response and a non-empty list, GangBrain_Think step 3 (0x00304608) pushes the fight (Brain_PushFightGoal(−1): FindEnemy, Melee) on every Destroyer, and the Warrior think's step 1 does the same for Cochise and Swan, who have Warrior brains (type 3) (Gang soldiers, The Warriors). Melee hands out the attack slots: the soldiers who get one fight (goal Fight), the rest wait in Spectate round the two, and a soldier who strays out of vTurfDestroyers takes a LeftTurf goal back. Confirmed (code) for the steps; their order confirmed (runtime) below.

At runtime (2026-10-12, PCSX2 2.9.94; confirmed (runtime)). The game was entered at checkpoint 4 from a pad-patched level80 state by setting the game state's level (record 2) and checkpoint and reloading. Cleon's transform was written onto fCochiseSwan to enter the trigger, and the scene played out. Once per update, every human's position, gang, brain goal stack, target and enemy count were read over PINE (index +0x92, brain 0x006d53f0 + index × 0x2f0). Updates count from the trigger:

Update What
27 everyone in JoinCinematic (l80_c7 starts)
456 the scene ends; all twenty-one stand exactly on their flags, with no goals and empty enemy lists
457-459 the scans: Cochise and Swan list 12 enemies, each Destroyer 1 or 2 (at most five brains an update)
458-464 all twelve Destroyers have FindEnemy, Melee and then Fight (eight, on Cochise or Swan) or Spectate (four); Cochise and Swan fight a Destroyer each
about 495 the block breaks up: soldiers close in and circle. From then on the twelve mill round the two inside the turf box (over 3,000 sampled updates, x −254 to −244, y 147 to 158), with Fight, Spectate, EngageEnemy, HelpRespond and LeftTurf by turns
until the order no Destroyer lists or targets any Warrior, and Cleon, idle on his flag, is never approached

The order (R2 held, right stick up-left at raw (0x18, 0x30); a full (0, 0) diagonal reads as straight up, the disabled follow, Warrior command menu) was given about 40 s into the brawl (run 1). Two more runs the same day from the same entry (runs 2 and 3, the order about 15 s and 60 s into the brawl) also read, once per update, each human's facing (the transform's quaternion at 0x00714b10 + index × 0x20; the model's forward is its +y axis), enemy list, attack-slot holders (brain +0x1a4, at most +0x1e4, 4 here) and last scan time (+0x160). Run 2 has a gap of about 40 updates over the order itself.

CommandIssued (confirmed (disc), the script), in order: the text and locks cleared; GangAddMember(GangWarriors, -1, hCochise) and the same for hSwan, both appended to the Warriors table; every AI Warrior in that table (Cochise and Swan too) BrFlush, BrDead(false), HuForceLook(false) and GangAddMember(GangWarriors) again; GangMakeEnemies(GangWarriors, GangDestroyers1) and the same for GangDestroyers2; GoalFight(hDestroyer1, player, -1) and GoalFight(hDestroyer2, player, -1); the three turf boxes removed; IssueWarriorCommand(1, true); the Destroyers' god mode off. The callback runs at the end of the dispatcher, after the player's own order has built the attack tactic (WarriorCommand_Dispatch 0x0041c4e0 calls GameState_CallWarriorCommandCallback last; confirmed (code)).

Updates from the order, by run (confirmed (runtime)):

Run 1 Run 2 Run 3
Destroyers targeting the idle Cleon by update 30 5 (the first at update 1) 0 2 (updates 4 and 25)
... by update 450 6 0 6
Destroyers listing one of the seven Warriors, updates 30 / 100 / 250 6 at update 4, at least 7 by 64 2 / 6 / 11 4 / 5 / 11
Destroyers targeting one of the seven, updates 100 / 250 about 7 at update 193 5 / 6 3 / 7

In run 2 the "listing" up to update 60 is only the two GoalFight soldiers, who list Cleon from the call (Brain_AddEnemy). Every run ends in a general melee where the block stood, the crew running in under FollowAndAttack. So nobody has to hit first, but how soon a Destroyer turns on the idle player varies from run to run: it depends on who faces the crew when the scan runs and on the attack slots (below). Run 1's "each lists nine at update 4" held for six of the twelve only.

After the order: who engages whom

Answers to the four questions of the implementer of issues #406 and #73 (2026-10-12).

Cochise and Swan get FollowAndAttack, and walk toward the crew for 2 s only when their list is empty.

  • Gang_AddMember (0x00166308) moving a human out of another gang clears his actions and his target and empties his enemy list (Brain_ClearActions, Brain_SetTarget(0), Brain_UncountAllEnemies, list +0x164 cleared). Confirmed (code); at runtime both lists went from 12 to 0 in the update of the callback (runs 1 and 3).
  • IssueWarriorCommand(1, true) (lua_IssueWarriorCommand 0x003747b8 → GameState_IssueWarriorCommand 0x0041c2d0, the command and forced) goes through the dispatcher forced, which skips the "same command again only repeats the line" branch (Warrior commands, step 2): the attack tactic the player's order built is cleared and a new one made. Its Start calls WarriorAttackTactic_GiveGoals (0x00320618): every living non-player member but the chief, popped to his goal base, gets FollowAndAttack on the chief. Cochise and Swan are members by then, so they get it too. Confirmed (code); FollowAndAttack (vtable 0x005405d0) on both at update 0 or 1 in all three runs.
  • FollowAndAttackGoal_Process (0x002bc198) fights the best enemy (Brain_PickBestEnemy, flags 0xfffffbf9) within 60 m of the chief; with none and the chief more than 8 m away it pushes FollowFormation (0.75 m, 2000 ms). At its first run their lists are still empty (their own scan refills them an update or two later), so both walk toward the crew for 2 s (61 updates in run 3), then fight a Destroyer. Runs 1 and 3: FollowFormation from update 1; run 1 moved them about 6 m, run 3 0.7 m and 3 m (held in the crowd). Run 2: Cochise's list had refilled before the goal's first look and he fought at once (Fight at update 14). Confirmed (runtime).
  • So they never leave the fight. The Destroyers keep them listed and scored (their new gang is the Destroyers' enemy too), and right after the order most Destroyers still target them (below). A Coney where they stay in the block is right on that. What to check: the forced re-issue rebuilds the tactic although command 1 is current, GangAddMember empties the moved human's list, and the empty-list FollowFormation.

GoalFight(Destroyer, Cleon): Melee does not keep the target; the score decides.

  • GoalFight is Brain_StartFight (0x002b2b90) → Brain_Fight(Cleon, −1): Cleon is added to the list and made the target, which releases the soldier's slot on Cochise or Swan (Starting a fight); the stack becomes FindEnemy, Melee, Fight. The Fight goal ends on its first update, Cleon being 12-19 m away (beyond 1.1 × far, 5.5 m, step 4). Melee then picks again with Brain_PickBestEnemy (0x0029f230), as on every run of it (The Melee goal, step 2); Cleon's only extra is the +3 for the previous target. Confirmed (code).
  • With the sight range R = 30 m (brain +0x130, read 30.0 on all twelve), Cleon at 15-19 m scores about 4 × (30 − d) + 20 (a player) + 3 = 67-83, and Cochise or Swan at 1-3 m about 108-116 (the score), unless Brain_CanTakeSlotOn (0x0028dc80) makes them −999. Its first argument is the enemy's brain: it is false when the enemy's holders (+0x1a4) fill his maximum (+0x1e4, 4), the scorer is not one of them and no holder is farther from the enemy than the scorer. Confirmed (code). So a GoalFight soldier goes for Cleon only when Cochise and Swan both have four holders, all nearer to them than he is.
  • Run 3 (confirmed (runtime)): before the order Swan had four holders, one of them Destroyers1.Destroyer1, and Cochise four. GoalFight released Destroyer1's place on Swan (three holders at update 0); at update 1 he had taken it back and targeted Swan, so Cleon was his target for less than one update. Destroyers1.Destroyer2 (in Spectate, holding no slot) ran in on Swan's side and targeted her from update 81. Neither went for Cleon; another soldier of the same gang, Destroyer3, did from update 4, from his own scan. Run 1: Destroyer2 engaged Cleon from update 1 to 137 (EngageEnemy, then Fight), the slots-full case (inferred: the holders were not read in run 1).

The Destroyers' lists after the order: the field of view, not the scan's timing.

  • The gang-changed rescan runs at once: the order's GangMakeEnemies stamps both gangs, and all twelve Destroyers scanned within three updates (run 3: updates 2-3; run 2: the three updates before its first sample), five an update (The enemy scan). Confirmed (runtime).
  • What that scan lists is set by sight: beyond the near radius a member must be within half the field of view of the scanner's facing, and the soldiers have 110° (brain +0x12c read 1.92 rad on all twelve). The crew stood 13-22 m off, inside the 30 m range. A Destroyer facing them listed all seven (run 3: Destroyers1.Destroyer3 and Destroyers2.Destroyer6, every Warrior within 0-48° of their facing); one fighting Cochise or Swan with his back to them listed none (bearings 93-178°). Over 41 scans in runs 2 and 3, every Warrior a scan newly listed was within 55° of the scanner's facing; in 7 of them a Warrior in view was still left out (not traced). The other entries came from GoalFight (Cleon) or were already on the list, which a scan keeps while in range and in line of sight (step 3). Confirmed (runtime); the ground is open, so no line of sight was in question.
  • The rest are listed later, two ways. A Warrior who attacks a Destroyer is added by the hit (run 2: Vermin on Destroyers1.Destroyer4 at update 64, Snow on Destroyers2.Destroyer6 at 76). And the next periodic scan: a fighting soldier waits 2000 ms × 4 = 8 s (run 2: Destroyers1's scans at 60.33 s, then at 68.34 s), others 2 s; by then the crew has run in and stands in front of them (11 of 12 list a Warrior by update 250 in both runs).
  • So a Coney where most Destroyers list only Cochise and Swan after the order is right for the first seconds, if its scan applies the field of view. What must follow is the crew's run-in under FollowAndAttack and the hits and rescans that bring the Warriors into the lists.

HelpRespond's 3 s is a head look; the goal ends with the turn (HelpRespond). Over 866 HelpRespond goals in four level80 traces (run 1 from the scene's end to the order, runs 2 and 3 from 8 s and 30 s before the order), the median lasted 2 updates and the longest 51 (1.7 s); none lasted 3 s. Almost all sat on a Spectate goal (some on a Fight), which carried on afterwards. Confirmed (runtime).

The first updates and the crew's targets: five more runs

Second set of answers for issues #406 and #73 (2026-10-12). Five runs of the original (PCSX2 2.9.94), each from a checkpoint-4 state with the player put on fCochiseSwan (−236.4, 176.6, 4.3) to fire the trigger, every human read once per update over PINE (position, facing, brain goals, target +0x124, enemy list, attack-slot holders +0x1a4, health record +0x144, state word). Runs 1-3 enter the trigger at the same moment and share the opening (the game is deterministic from a state); runs 4 and 5 enter it 4 s and 7 s later and open differently. The order was given 15, 40, 60, 25 and 30 s after the scene. Updates count from the scene's end, U from the order (Cochise's first FollowAndAttack). Confirmed (runtime) unless marked.

A slot refused leaves no target. Brain_SetTarget (0x0028cfe0) stores the nil handle in +0x124 first and writes the new target only when Brain_ClaimAttackSlot (0x0028df30) succeeds; a claim on a full list fails unless the newcomer is nearer than the farthest holder or forced. So a Brain_Fight(side) from Brain_OnViolenceSeen (0x00291960) that finds the side full leaves +0x124 nil, pushes FindEnemy, Melee, Fight anyway, then HelpRespond on top (when none sits above the goal base). Nothing gives him a target or a Spectate goal until HelpRespond ends, except an event (HelpRespond). Then Fight ends at once (no slot, step 2), Melee re-picks and, with every enemy at −999, pushes Spectate. Confirmed (code).

The fight starts within 7-9 updates. The first Destroyer has FindEnemy and Melee at update 1 or 2, the last at update 7 or 9 (runs 1, 4, 5; the earlier run 1 above: all by update 8). Every Destroyer targets Cochise or Swan or has Spectate on his stack by update 7, 7 and 9, and all twelve at once from then. From there to update 30 at most one Destroyer at a time fails it, for one update (run 4, update 22: a soldier just pushed off Swan's slots, his HelpRespond on a targetless Fight lasting one update because he already faced the fight). Later such gaps last longer only when the soldier is stunned (state 0x100000, update 31-52 in run 1: the stack is frozen under the reaction). No HelpRespond is pushed before update 13: the help calls come from the first attacks, by which time every soldier already has Fight or Spectate, so a HelpRespond lands on Spectate (the condition still holds) or, when it lands on a targetless Fight, on a soldier already facing the fight.

After the order: who the crew attack, and what turns a holder. At U+300 (crew Warriors only; Cleon in brackets):

Run 1 Run 2 Run 3 Run 4 Run 5
Destroyers targeting a crew Warrior 9 7 (+2) 7 5 (+1) 5
... listing one of the seven 12 12 12 12 10
slot holders on Cochise or Swan at U who target a Warrior by U+300 5 of 6 6 of 6 5 of 7 4 of 7 4 of 6
crew Warriors who target a holder by U+300 3 5 6 6 5
  • The crew pick by score from their own list; slots on Cochise and Swan play no part. Each crew Warrior's FollowAndAttack picks with Brain_PickBestEnemy (flags 0xfffffbf9) at 9-19 m, within U+0 to U+3 in most cases. The −999 rule asks about the Destroyer's attackers, who are few, so a Destroyer holding a slot on Cochise is as good a pick as a free one. The first pick was a holder for 1-4 of the six per run, and often not the nearest Destroyer: the walk over the list stops at the first valid entry after five slots once a winner exists (empty slots count), so list order matters. Confirmed (code) at 0x0029f230; the picks confirmed (runtime).
  • A holder turns to a Warrior in four ways, counted over the 24 holders that did by U+300 (classified from the trace: goal stack, distances, health): Melee's re-pick by score when his fight goal ends (at its 4 s deadline, or the target beyond 5.5 m) and a Warrior now scores higher (13); the fight goal's re-target (a Warrior within 1.5 m straight ahead, slot forced, at most once a second; 5, often a Warrior running past toward someone else); EngageEnemy's 2 s re-target when the old target is beyond 5 m (2); a hit (event 1 → Brain_SetTarget(hitter, forced) when not busy, GangBrain_OnEvent 0x00304fa8; 2). Two more lost their slot first and then picked a Warrior. Inferred from the traces for the split; each path confirmed (code) at the cited addresses. Free soldiers (Spectate or no slot) take a Warrior or Cleon through Melee's or Spectate's own pick.
  • Cochise and Swan do not go out. From 1,800 health they lost at most 164 by U+300 (lowest 1,636), were knocked down (state 0x80000) 0-2 times each for 12-73 updates, and were never knocked out. A knock-down releases no slot: slot claims prune only dead holders and the knock-out alone releases them (ai.md). Their holders fell from 6-7 at U to 1-6 at U+300, by the holders' own switches.

Coney. Checked 2026-10-12 on the preview build (tip b73ef09d) with --play-level level80 --checkpoint 4 --start -236.4,176.6,4.3,180 --headless --trace-humans, and in the code on main (0a926664) and on PR #394's branch:

  • The set-up matches: the same HuCreate rows and scene, then the twenty-one TeleportToFlag calls put everyone on the same flags with the same headings (makeTeleportToFlag, repo:src/scripting/level_bindings.cpp).
  • Then nothing moves. From the scene's end to update 3,000 the twelve Destroyers, Cochise and Swan stand on their flags with no goal, no target and an empty enemy list: a block of twelve facing one way that never breaks up. That is the "lined up" look. The placement is the original's; what is missing is the brawl that should start one update later.
  • Why, in order:
    1. On main, only the members of a Scout tactic scan (ScoutTactic::update); Brains::update never does, so GangMakeEnemies puts nobody in a list (Attacking on sight, Coney status). PR #394 adds the scan to every brain's update and so fills the lists.
    2. Neither main nor PR #394 has GangBrain_Think step 3 or the Warrior think's step 1 (Brain::think keeps only the tackle meter and the swap prompt), so a listed enemy is fought only when something else calls fight: a hit, an event, a tactic or a script's GoalFight.
    3. After the order: the other ten fight only a Warrior who hits them first (event 1 → Brain_Fight), as the scan's lists then hold only Cochise and Swan (inferred from the code). On this level, that is finding B: "they do not attack unless I hit one first". Whether the two GoalFight Destroyers take Cleon depends on the slots (After the order).

What Coney must do at this fight's start:

  1. Run the enemy scan from every AI brain's update, five brains an update, on the gang-changed trigger (gang +0x34 against the brain's last scan) as well as the interval. PR #394 adds the scan; check there that GangMakeEnemies and GangMakeFriends stamp the gang's change time, so the scan runs on the next update rather than up to 2 s later.
  2. Add the think step that turns a list into a fight: a gang soldier (type 2), cop (type 1) or Warrior (type 3) with a threat response, no fight goal, no gang tactic and a non-empty enemy list pushes FindEnemy and Melee (Brain_PushFightGoal(−1)) on every think.
  3. Keep the placement: the flags are the original's. Do not scatter or delay the twelve.
  4. Melee's slots, with Spectate for the soldiers who get none, and LeftTurf inside vTurfDestroyers (The fight goal, The Melee goal), so the block breaks into a ring.
  5. A test ([disc][story], on the player's path): level80 from checkpoint 4, walked into vCochiseSwanTrigger, the scene skipped. Each Destroyer has pushed FindEnemy and Melee by update 10 of the scene's end; by update 12 all twelve at once target Cochise or Swan or have a Spectate goal, and from then to update 30 at most one fails it at any update (the timeline: 7-9 and at most one, in four runs); within 60 updates at least eight are more than 0.5 m from their flag; no Destroyer targets Cleon before the attack order. After the order, with the player standing still and pressing nothing (eight runs of the original, above and the timeline): Cochise and Swan have FollowAndAttack within 2 updates; by update 300 at least 9 of the 12 Destroyers list one of the seven Warriors (Cleon and the crew), at least 4 target one, at least 3 of the Destroyers that held a slot on Cochise or Swan at the order have targeted a Warrior, and at least 2 crew Warriors have targeted such a holder. Cochise and Swan are never knocked out. How many target the idle player within 30 updates varies (5, 0 and 2), so a test must not require it.
  6. After the order: the forced IssueWarriorCommand(1, true) rebuilds the attack tactic although command 1 is current, and gives Cochise and Swan FollowAndAttack; GangAddMember empties a moved human's enemy list; the enemy scan applies the 110° field of view beyond the near radius (After the order).
  7. The fight's timing and the slots (the timeline): the scan on the gang-changed trigger and the think one step in five (ai.md) put FindEnemy and Melee on every Destroyer within 7-9 updates, before the first attack's help calls. Brain_SetTarget leaves the target nil when the slot claim fails. A standing soldier's turn of more than 15° is one turn clip of about 10 updates, whatever the angle. A crew Warrior's pick ignores whether a Destroyer holds a slot on someone else, and a holder leaves Cochise or Swan through the fight goal's 1.5 m re-target, Melee's re-pick at the fight goal's 4 s deadline, EngageEnemy's re-target and hits.

The order at runtime (reference)

One play of the whole mission on the original (2026-10-08), entered at checkpoint 1 from a level99 state by setting mode 8's level to level80 (record 2) and reloading, with the hooks lua-exec, c-call, c-call-args, game-hint and hint-show of repo:research/traces/patches/ and the call-brains call hook. Scenes were left to play out. Input over PINE as a pad: R2 held with the right stick at full deflection for each order (up, down, down-left, up-left), the left stick at 0.4-0.7 for walks, triangle, L1, cross, square and the L1/R1 mash. Long walks were shortened by teleports (Object_Teleport, 0x00385bb8, through the call hook) to just outside each objective's volume, so nothing on the routes was exercised. Updates count from the first sample; the waits between steps are the driver's, so only the order and the gaps inside one step mean anything. "Shown" is when HintBox_Update takes the text off its queue (0x001cdf7c). Confirmed (runtime).

Update Step Shown or set, in order
416 1.1 HUDSetObjective(2, MP_1, 0); scene l80_c1 (preloaded, played at once)
1859-2058 1.2 Warrior command 3 issued; TT_1 shown; scene l80_make_out (Ajax, looping); TT_4 shown 151 updates after TT_1; WCSetCallback("CommandIssued")
4423 1.3 follow given: callback and text cleared
4647 1.4 objective MS_C1_1 (states 0, 1, 0) and its radar objective, 224 updates after the order (the crew's walk down)
14264-15182 1.4 vAjaxTrigger entered: hint flushed, MS_C1_1 state 2, radar objective removed; scene l80_c4; SetCheckPoint(2) at its end
15182 2.1 command 0 issued; objective MS_C2_1 (0, 1, 0) and its radar objective; l80_c5 preloaded
20437-20467 2.1 within 2 m of fSnowCowboyTrigger: MS_C2_1 state 2; scene l80_c5
21478-21824 2.2 command 0 issued; TT_11 shown 196 updates after; cleared and objective MS_C2_2 (0) 144 updates later
29582 2.2 Snow freed (one triangle, then L1/R1 alternating every 2 updates)
34143-34173 2.2 → 3.1 Cowboy freed: MS_C2_2 state 2; SetCheckPoint(3) 30 updates later; objective MS_C3_1 (0, 1, 0); l80_c6 preloaded
35038-35071 3.1 vFoxTrigger entered: MS_C3_1 state 2; scene l80_c6
35989 3.2 WCSetCallback("StartPlayers2"); TT_5 shown
36655 3.2 hold given: callback and text cleared; objective MS_C3_1a (0) and its radar objective
36891-37101 3.3 vThrowTutorialTrigger entered: MS_C3_1a state 1; TT_25 37 updates later; objective MS_C3_2 (0) 143 updates after that; TT_26 30 updates later
37929 3.3 a bottle in hand: TT_27
38230 3.3 L1 (throw aim): TT_28
47972 3.3 aimed at the marker (the aim turned right and pitched down to 0.139 rad; the aim's target +0x634 set): TT_29
48302-48332 3.3 cross: the bottle broke in vBottleBox, thrown from the roof (message 6): MS_C3_2 state 2, text cleared, command 0 issued; scene l80_c6_b
49202-49207 3.4 command 0 issued; TT_7; WCSetCallback("StartVandalize")
50317-50373 3.4 wreck given: text cleared; objective MS_C3_3 (0) and the front-door radar objective; CfgSetLockPickHandler(nil, nil, "FrontDoorPicked"); command 0, then command 5 two seconds later
52875 3.5 front lock picked (three cross presses 31, 16 and 10 updates apart from the dial's start): text cleared, command 5 issued, marker removed
53230-53236 3.5 the crew's tenth stolen object: MS_C3_3 state 2, objective MS_C3_4 (0), CfgSetLockPickHandler(... "BackDoorPicked"), back-door radar objective
54166-54185 3.6 → 4.1 back lock picked: MS_C3_4 state 2; SetCheckPoint(4) 11 updates later; objective MS_C4_1 (0, 1, 0); l80_c7 preloaded
54609-54638 4.1 vCochiseSwanTrigger entered: MS_C4_1 state 2; scene l80_c7
55063-55069 4.2 command 0 issued; TT_8; WCSetCallback("CommandIssued")
62056-62204 4.2 attack given: text cleared, HUDRemoveAllGoalText, objective MS_C4_2 (0), command 1 issued; TANDEM shown 148 updates later (<DISPLAYTIME 6000>)
63277-63363 4.4 the last Destroyer down (all twelve beaten, 1,200 updates of square at the nearest): MS_C4_2 state 2; scene l80_c3 86 updates later
63726-64643 4.4 scene l80_c3_a, then l80_c3_b (Final)
65829-65831 end HUDLaunchMissionComplete(); UM_Unlock(80, …); level87 loads at checkpoint 1

Points a playthrough test needs:

  • Hints in order: TT_1, TT_4, TT_11, TT_5, TT_25, TT_26, TT_27, TT_28, TT_29, TT_7, TT_8, TANDEM. TT_26-TT_29 follow the thrower's state (a test that drops the bottle sees TT_26 again).
  • Not seen in this run: TT_9 (Cleon's message 1 from a gang-type-19 attacker; he was not hit), and no game hint (HintBox_QueueGameHint) at all, neither the uncuff's hint 19 nor the lock pick's 0x14: their shown counts in this save's profile are not known, so a fresh profile may show them (Uncuffing, Lock picking). level87's first game hint (id 22) did show in the same run.
  • The passing-train scene (Coney_Sub3/Coney_Sub4, ScenePlayFixedScene) played every ~30 s throughout; it is not a mission scene.
  • The stolen objects (step 3.5) come from the crew under the wreck order alone; the player only picked the lock. The store is the pharmacy: its store front storePharmacy01 (activity 14, group 24) stands about 2 m inside the front door, so the player waiting there keeps it within the steal tactic's 12 m, and its stock is the 23 owned objects of zone 24 (Owned items; disc check, counts only). What counts is message 7, a take of an owned object (Taking owned items): wrecking the store counts for nothing (a strike or a break sends message 6, and the step waits on message 7 alone), while the player taking stock himself with triangle runs the same Item_TakeOwned and so should count (inferred from the code path; not seen at runtime).

The arrest scene and the opening fire (user checklist 28, 36)

What the original does where Coney was seen to differ (user checklist items 28 and 36, 2026-10-08). Script facts from the disassembly of level80.lua, level80_chapter1.lua and level80_chapter2.lua (names, ids and values only) are inferred; scene data is read from the disc's l80_c5 records (numbers only); engine facts are cited.

Item 36: who is in l80_c5 and what happens to the batons and cuffs.

  • Every actor is an in-world human, none is scene-only. level80.lua's AddSnowCowboy makes Snow (type 34) and Cowboy (19) by the gate, AddSnowCowboyCops four cops (Cop1 type 253, Cop2 254, Cop4 253, Cop3 256) in their own gang. Chapter 2's Setup makes them all god-mode, gives Snow and Cowboy auto-escape, and spawns two scene props, dyn_cowboy and dyn_snow. A player in vSnowCowboyTrigger sets the cops on Snow (GangMakeEnemies, GangEngageEnemy), so a fight is going on when the objective is reached.
  • Before the scene (SnowCowboySceneTriggered, then RunSnowCowboyScene 1 s later): the gate opens, the Warriors are suspended for the fade (DeactivateWarriors, undone before the scene), DestroyCopWeapons destroys whatever each cop holds (HuGetHeldObject then ObjDestroy, so their own weapons never reach the scene), and the six scene humans' shadows go off (HuShadow(h, false)).
  • The binding. SuperRunScene gets ten humans, bound to roles 0-9 in order by GoalJoinCinematic (global.lua's gPlayCutScene): Cleon, Rembrandt, Vermin, Ajax, Cowboy, Snow, Cop1, Cop2, Cop3, Cop4, against the roles cl, re, ve, aj, warrcb, warrsn, cops01-04; and ten objects bound to object slots 0-9 by SceneAddObject: dyn_cowboy, dyn_snow, dyn_baton, dyn_baton01-03, dyn_cuffs_c, dyn_cuffs_c01-03 (the header's slot names are cowboy, snow, baton, baton01-03, cuffs_c04, cuffs_c05, cuffs_c01, cuffs_c02). The batons and cuffs are not spawned by the chapter's scripts; they are the level's own objects (in its Objects table).
  • The props go into hands by clip events. When the scene starts each bound human's pick target (+0x33c, Human_SetObjectTarget 0x00227080) is set to the scene object in the slot named by the value (+0x04) of the first type-9 event of its role clip (Clip_GetEventValue 0x00101ce0, from the scene start 0x0039d870); confirmed (code). The event then fires in the clip and the human takes that object into bone 25 (message 0x89, take 0x1b, no grip slide in a scene, Objects in a hand). In l80_c5 (disc, frames from the scene's start, 1,010 in all):
Role Human Events So
6 cops01 Cop1 9 → slot 2, frame 0 holds dyn_baton from the start
7 cops02 Cop2 9 → slot 5, frame 0 holds dyn_baton03
8 cops03 Cop3 9 → slot 4, frame 0 holds dyn_baton02
9 cops04 Cop4 9 → slot 3, frame 0; 10 at frame 279 holds dyn_baton01, drops it at 9.3 s
4 warrcb Cowboy 9 → slot 7 at 404; 0x36 (value 6, bone 19) at 462 takes cuffs cuffs_c05 in the right hand, then cuffs_c04 in the left
5 warrsn Snow 0x36 (value 9, bone 19) and 9 → slot 8, both at 443 takes cuffs_c02 and cuffs_c01

Event 10 in a scene is a drop (0x00257f38: a physics body, falls from the hand, and a removal time of the global time-to-live, 30 s from config_preload2.lua; Objects in a hand). dyn_baton01's track ends on the floor (−194.51, 233.02, 1.25), the other three batons' at hand height beside their cops' end marks, agreeing with this. Which object a 0x36 takes is not traced (the start sets only the first type-9 target); inferred from the values that it is the slot named. - After the scene (EndSnowCowboyScene): ObjDestroy on dyn_cowboy, dyn_snow, dyn_warr_cb and the four cuffs (a held object is let go first, ObjDestroy); shadows back on; Snow and Cowboy teleported to fArrestedSnow / fArrestedCowboy, arrested (HuSetArrested), made Warriors; then RemoveGang(GangSnowCowboyCops) (GangDelete): the four cops are destroyed at once with their held objects (GangDelete). So the three batons still in a cop's hand go with the cops, and the only baton left is dyn_baton01 on the floor, gone when its removal time runs out (about 30 s after frame 279, some 5-6 s after the scene's 33.7 s end; inferred from the drop's timer). The script never destroys a baton itself. - A human in a grab is skipped. The scene's start leaves out a bound human that is grabbing, grabbed, mounting or mounted: not posed, not counted, its fight going on (Scenes: humans, confirmed (code)). A cop with Snow in a hold at that moment would stay out of the scene in the original too; with auto-escape and god mode on Snow, how often that happens was checked at runtime (below): not once in 60 s of the fight. - At runtime all ten were taken in (2026-10-11, PCSX2, confirmed (runtime)). Level80 checkpoint 2 was entered as in the runtime order and Cleon teleported to (−191, 224) (Object_Teleport, call hook). He walked towards (−195.3, 225.2) with the stick at 0.5, aimed at the point and re-aimed every 2 updates. The cops were set on Snow as he came into vSnowCowboyTrigger. l80_c5's header went to state 4 on the update of ScenePlayCinematic and to state 5 the next. At that first playing update +0x1e was 10, with all 10 roles bound and no one skipped for a grab. One run only, with the scene started a few seconds after the cops engaged. - No one grabs in the gate fight, so no one is left out (a second run, 2026-10-11, PCSX2, confirmed (runtime); checkpoint 2 entered the same way, hooks strike-contact, hit-reaction and grab-start). vSnowCowboyTrigger is a box from (−195.7, 210.8, 1.1) to (−184.2, 218.0, 4.6) (read from the volume object behind the script's handle), across the street south of the gate: a player walking up from the checkpoint start passes it well before he is within 2 m of fSnowCowboyTrigger, and GangMakeEnemies and GangEngageEnemy(cops, Snow) came the update Cleon stepped in at (−190.6, 217.8). (A player teleported to (−191, 224), north of it, starts the scene without ever setting the cops on Snow: the first run here did.) Cleon then stood still for 1,800 updates (60 s) while the cops, Snow and Cowboy fought (strikes both ways, reactions, the Destroyers joining). Sampled every update, none of Snow, Cowboy and the four cops ever had a grab or mount bit (0xcf0 of record +0x14), and Grab_Start was never called. The scene started at Cleon's walk to the flag with all ten humans taken in. So with this AI a cop holding Snow at the scene's start does not happen in practice; the skip is a code path only. - The batons after the scene (same run, the four records of dyn_baton to dyn_baton03 read every 15-20 updates from the scene's end, through Objects and the ObjectTaskManager's records): before the scene none is spawned (the scene's SceneAddObject spawns them); DestroyCopWeapons destroyed one held object per cop at the start. At the scene's end, 3 updates after and at 1 s, only dyn_baton01 was live, lying at its track's end (−194.51, 233.02, 1.25); the other three records were marked removed (+0x24 bit 0x40000) when GangDelete took their cops. dyn_baton01 was removed between 174 and 193 updates (5.8-6.4 s) after the scene's end, so about 30 s after its drop at frame 279, and at 40 s and later no baton was left. dyn_baton04 and 05 (the level's others) never spawned in this stretch.

Coney fixes (item 36):

  1. Find why Snow (role 5) and the cops (roles 6-9) are not posed: log, at l80_c5's start, each role's handle and whether the stage's free test (PlayLevelMode's setFreeTest, repo:src/platform/play_level_scene.cpp) took it. A cop in a pair with Snow is skipped by design; a human Coney does not know as cast (no brain for the handle), a role left unbound by GoalJoinCinematic, or a pair Coney's fight never ends (Snow's auto-escape) are Coney bugs.
  2. Before the scene, DestroyCopWeapons must remove what each cop holds (HuGetHeldObject returning the held weapon).
  3. Act on role-clip events 9, 0x36 and 10 for scene objects: at the start set each human's pick target from its clip's first type-9 value (an object slot), take the object into the event's bone at the event's offset when the event fires (no grip slide), and drop it on a type-10 event (physics body, removal after the global time-to-live). While held, draw the object from the hand, not from its track.
  4. GangDelete destroys each member's held object with it, so RemoveGang(GangSnowCowboyCops) takes the three held batons; ObjDestroy of a held cuff lets go of it first.
  5. Test (level80 checkpoint 2, the Story path): during l80_c5 all ten roles are bound and posed (Snow and the four cops among them) and each cop holds his baton from frame 0; after the scene no cop, no cuff and no held baton remain, and at most dyn_baton01 lies on the floor until its 30 s timer ends.

Item 28: the opening fire. The fire is the molotov explosions' own aftermath, not a scene effect:

  • l80_c1 has no particle events (its tracks carry only lens, caption, fade, intro-card and type-31 events; disc). Its three type-31 events call EndIntroScene with 3, 2 and 1, and each call ObjDestroys one club pane and runs C1.ExplodeAtFlag at fExplosion1 / 2 / 3 (about (−181 to −184, 85 to 87, 5.8 to 6.3)): ObjSpawn("dyn_molotv") there and BreakObjectsInRadius(molotov, 0.5), so the bottle breaks itself into one sub_explode 0.7 m up, whose second stage makes a part_explosion (the Molotov). The level script makes no fire of its own (the looping FireContinued re-explosion every 9 s is in the SCENETEST debug path only).
  • What lasts is what the explosion's six embers leave where they land (Explosions, confirmed (code) at 0x003c3c08, 0x003c3da8): each ember lives 25-40 updates at a random interval of 1-3 ticks (about 0.4-2 s) and drops a sub_flames puff behind it every update; on landing on a surface that is not a wall it makes a sub_burn scorch (interval 180 ticks, four steps: 12.0 s alive at runtime, below, with an AI no-go sphere of 0.5-0.75 m) and a sub_fade_flame on it (60-89 frames at 2-3 ticks, then a fade: about 4-6 s), the first ember's flame with the looping vags/fire/fire_small_loop. Three explosions so leave up to eighteen small fires on the clubhouse's ledges and floor for several seconds, while the flash, fireballs and debris are over in about 2 s.
  • At runtime (2026-10-11, PCSX2, confirmed (runtime); three plays of the unskipped intro, entered at level80 checkpoint 1 as in the runtime order). The hooks ember-init, ember-update, burn-init, burn-update, fade-flame-init and fade-flame-update of repo:research/traces/patches/ logged the three explosions about 650 updates into l80_c1, 5 and 4 updates apart, with six embers each. The tasks lived as follows:
    • Embers: 2-5 updates (3-9 ticks), never their 25-40. Each starts 0.7 m above its flag (z 6.8 at fExplosion1) and falls about 0.6 m onto the sill or floor, so it lands almost at once and ends there.
    • Scorches (sub_burn): 18, one per ember, each 721 ticks (12.0 s) from its creation to its last update. That is five updates: the creation and four steps of 180 ticks.
    • Flames on the embers (sub_fade_flame): 18, 287-361 ticks (4.8-6.0 s) each, on the ember's landing spot.
    • More flames, not from the embers: another 46-53 sub_fade_flame per play. They start 20-60 updates after the explosions, scattered up to about 6 m round the flags at floor height (z 5.7-6.0), and each lives 278-374 ticks (4.6-6.2 s). They come from the explosion's burning debris (below).
    • Their source (a second pass, 2026-10-11, two more plays, hooks ember-init, burn-init, fade-flame-init and debris-flame): each explosion's twenty large sub_debris are made burning (message 0x27, kind 2, Explosions), and one that lands on a floor (its contact normal's z above 0.9) makes a sub_fade_flame where it lies (sub_debris). The two plays made 71 and 62 fade flames: 18 from the embers and 53 and 44 from landed debris, of 60 burning pieces, all between 20 and 63 updates after the first explosion at z 5.6-7.0. Every flame is accounted for.

So fire is still burning near the flags 3 s after the last explosion, the flames are gone by 6.2 s and the scorches at 12.0 s, and nothing is left at 20 s.

Coney fixes (item 28):

  1. Give the explosion's embers the original's life: 25-40 updates of 1-3 ticks each (Coney counts 25-40 ticks, about half), and a sub_flames puff each update. Here they land after 2-5 updates, so what the player sees is the landing: an ember that ends on landing must still leave the scorch and flame of fix 2.
  2. On an ember's landing make the sub_burn (with its no-go sphere) and the sub_fade_flame on it as above, the first ember's flame with its loop sound; Coney's embers end on landing with nothing behind.
  3. Make the explosion's twenty large debris pieces burning (kind 2) and give each that lands on a floor a sub_fade_flame where it lies (sub_debris): about 15-18 more small fires per explosion, scattered a few metres round it, 20-60 updates after it.
  4. Test: after C1.ExplodeAtFlag, six scorches and six flames are made where its embers land, and one flame per burning debris piece that lands on a floor; the flames are gone between 4.6 and 6.2 s and the scorches at 12.0 s, and no fire particle is left 20 s later.

Deferred: runtime: none left; item 36's batons and holds and item 28's extra flames were checked in the second pass above.

level87.lua (mission 3)

Story mission 3, loaded by runNextMission after level80: Rembrandt (player 1) and the crew tag over the Destroyers' burners in East Coney. Read from the compiled level87.lua and its five chapter scripts through coney-tools' Lua walker (names, ids and values only; our own words); bindings on Story coverage. Inferred unless marked. Chapter 5 (the stealth tutorial) is on Stealth, the Destroyers' reactions on AI, the tag itself on Crimes.

  • Each chapter ends with SaveLevelData, SetCheckPoint(N + 1), ChLoader (preLoadFile of the next chapter with "Setup") and the global Cleanup; that order only works when preLoadFile runs the new chunk later, not inside the call (Errors in a fresh state). Tags already sprayed are kept in the Lua save numbers 1-4 (SaveLevelData, RestorePreviousTags), so a checkpoint start restores them.
  • Tag spots. Each chapter's Handlers gives every spot of its list the tag prompt (SetMsgHandlerEx(spot, 0, "SpotGettingTagged", ...)), a dyn_w_mission marker, a radar objective (texture 28) and a sphere that hides the marker while a player stands in it (GlowGoAway, GlowComeBack). SpotGettingTagged asks for paint (InvGetSpraycanCharges): with none, speech command 37 only; otherwise the prompt is taken off the spot, the player made unpushable, event 14 handed to TagFinished, and HuTag called. TagFinished(human, tag, finished): with 0 the prompt goes back (and chapter 1 refills one can); with 1 the marker and radar entry go, the chapter's counter moves (Updater, ObjectiveUpdate) and the next pattern loads (NextTagPattern, a tagN file).
  • Paint and cash. Chapter 1 gives 2 cans (InvSetSpraycanCharges(0, 2)) and tops one up when they run out (ch1.CheckSpray, ch1.BoostSpray); from chapter 2 the player buys paint from the spray sellers (StartFactory, GoalDealer) with mugged cash, and CheckCash (every 60 s) and the reminders nag (hints CASH, TT_41, RM_1).
Step Started by Hints, objectives, scenes (in order) Waits for Ends with
1.1 intro Setup mission line MP_1; scene l87_c1 the scene's end ch1.Start87: 2 cans, l87_drunk_dest preloaded, crew brain-dead, the intro path camera, the crew walk to their marks
1.2 first burner ch1.PlayerGotToFirstMark objective MS_C1_1; Ajax's line and gestures; 3.5 s: TT_2 (again each time the player leaves the sphere) the burner on the raised ledge tagged (TagFinished with 1) 1 s: ch1.FirstTagDone
1.3 to the streets ch1.FirstTagDone objective MS_C1_2 (go-to marker), the gate camera and Ajax's line, then MS_C1_1 done the player in the objective's volume (vbGoVoltron on the way puts the crew back in the gang) ch1.The_REAL_End: SetCheckPoint(2)
2.1 the drunks Setup (chapter 2) scene l87_drunk_dest; three tag spots armed the scene's end ch2.LiquorSceneDone
2.2 two burners ch2.LiquorSceneDone MS_C1_2 done; TT_41; objective MS_C2_1 with a 0/2 counter; the first spray seller flashing on the radar; reminders TT_41 (20 s), RM_1 (60 s) and CASH (60 s, when broke) two of the three spots tagged ch2.Done: secondary objective MS_C2_2 (the last street tag, 0/1) and objective MS_C2_3 (go-to)
2.3 back alleys ch2.Done - the player at MS_C2_3's flag (the third tag is a bonus: UM_Unlock(87, 1, ...)) ch2.Exit: SetCheckPoint(3)
3.1 three burners Setup (chapter 3) objective MS_C3_1 (0/3), five spots armed; SPRAYATTACK; the boozer's 10 m sphere (its callback ch3.BoozerChallenge does not exist); the bums' fight (vbBumScrap) three tagged ch3.Done: objectives MS_C3_2, MS_C3_3
3.2 alleys' exit ch3.Done - the exit objective ch3.Exit: SetCheckPoint(4)
4.1 the street Setup (chapter 4) crew brain-dead; Rembrandt walks to fMeanStreet; the intro path camera (ch4.RunIntroPoizo) and his line; 15 s: RUNATTACK the camera's end ch4.IntroCamDone: objective MS_C4_1, the wanderers
4.2 burners MS_C4_1 the roof tagger (ch4.ApproachRoofTagger), the roof radio and chairs, the porn-stand chat, the stairs' house party, the chair thrower (FlamerSeePlayer), the liquor store's pickable door the chapter's burners tagged objectives MS_C4_2 (go-to) and MS_C4_3 (bonus)
4.3 out MS_C4_2 - the exit ch4.Exit: SetCheckPoint(5)
5 chapter 5 the stealth tutorial, the last tag (MS_C5_3), the penultimate scene and l87_c2 Stealth HUDLaunchMissionFailed(MF_C5_1) when spotted; the final scene ends the mission

The order at runtime (reference)

From one run of the original (2026-10-08, PCSX2 2.9.94): the level80 reference run played on into level87 at checkpoint 1 with the same hooks. The walks were helped by teleports and, in chapter 2, by writing the player's money, so only the order means anything. Updates count from that run's first sample. Confirmed (runtime).

Update Step Shown or set, in order
65831 1.1 HUDSetObjective(2, MP_1); scene l87_c1
66609-66916 1.2 l87_drunk_dest preloaded; the burner's radar objective; objective MS_C1_1; game hint 22 9 updates later; TT_2
77650-84035 1.2 at each spray, game hint 16 70 updates after it starts; an unfinished spray keeps its progress (the next one started at 19, 27, 43 and 72 of 76 points)
84065-84152 1.3 TT_2 cleared; objective MS_C1_2 and its radar flag; MS_C1_1 done 87 updates later
87071-87097 1.3 to 2.1 MS_C1_2 done; SetCheckPoint(2); scene l87_drunk_dest; three radar objectives
87349 2.2 TT_41; objective MS_C2_1 with its 0/2 counter; then TT_41 every 600 updates, RM_1 about every 1,820 and CASH while broke
95142 2.2 game hint 24 (the player knocked down)
103047-115832 2.2 the counter at 1/2, then 2/2; secondary objective MS_C2_2 (0/1) and objective MS_C2_3
116494-116510 2.3 the third tag: MS_C2_2 done, UM_Unlock(87, 1, ...)
116852-116871 2.3 to 3.1 SetCheckPoint(3); MS_C2_3 done; EConey_Sub5 preloaded; objective MS_C3_1 (0/3); five radar objectives
117333 3.1 SPRAYATTACK, then RM_1 about every 1,820 updates (the run stopped here)

The game hints. Confirmed (code) at the addresses:

  • Hint 16 is queued by Tag_StartSpray (0x0022e610) when the game's hints are on (W_GameState + 0x56e2), the level is not an Armies stage, hint 16 is unlocked, a two-bit counter (game state bits 19-20) is below 2 and the level's number is 87. Tag_End (0x0022e848) withdraws it and, after a finished tag, adds 1 to the counter: it shows at every spray until two tags are finished, in level87 only.
  • Hint 22 is queued once (game state flag 0x10000000) by Radar_Update (0x001c5210) when a mission-objective blip (kind 10) is more than 1.0 m above or below the player and |dx| + |dy| is under 20 m (0x0050e9ac, 0x0050e9b8).
  • Hints 24 and 25 are queued by Human_KnockDown (0x0022f100) when a player goes down: 24 while a two-bit counter (game state bits 30-31) is below 2, then 25 at 2; each adds 1 to the counter. Both need the hints on, no Armies stage, and 0x006092b8 at 0.

Coney's run and the fix list

Coney at f325e018 (release build, the planner fixes of #271 included), started with --play-level level87 --checkpoint N --pad-pipe and played by a scratch course over the observation (markers, events and Coney's own log lines), the left stick at 25-90 %, climbs made by pushing at 90 % and tapping triangle along the level's path nodes.

  • Checkpoint 1 plays to SetCheckPoint(2) in 2,773 updates (the ledge climb, the stick game finished on the second spray, the go-to objective), and chapter 2 then sets up and plays its scene; its first spray is cut short (fix 2).
  • Checkpoint 2 (a direct start): no paint; mugging the passers-by near the start paid $0 or $1 eight times and $14 once in 8,900 updates (fix 3, withdrawn: the $1 amounts fit the bums, who hold $1-$3, and the $14 a civilian; inferred, the victims are not logged).
  • Checkpoint 3: the order matches the script and the reference: objective MS_C3_1 (0/3) and the five spots at the start, SPRAYATTACK once EConey_Sub5 has loaded (update 354; the reference's 481 after SetCheckPoint(3) include the original's load). The boozer's sphere calls the missing ch3.BoozerChallenge at once, set off by the second early Destroyer standing beside him. No paint: three muggings of spawned humans in 6,000 updates paid $0 each, so no spot could be sprayed (fix 4; fix 3 is withdrawn, the spawned humans hold the money their types roll).
  • Checkpoint 4: Poizo's intro path camera, MS_C4_1 (0/3) at the camera's end (update 335) and RUNATTACK 18 updates later, as the script orders. The course reached none of the three burners: the street one needs paint, and on the level's path links the two roof spots are reached only through door links (kind 0x10; for the roof at (442.6, 112.5, 6.5), the bar door dyn_door_bar_bani number 11 at (457.1, 127.1), where the player stood pushing for 2,000 updates) or a jump link (kind 4) 4.6 m up. No link joins the street to chapter 5's start: the way out is a scripted door (Doors.ChA17, opened by ch4.Exit).
  • Checkpoint 5: the stealth tutorial runs in the script's order up to the kill lesson (the hide box, its hints, the move to the marker, the scout lesson, then the creep hint at update 737); the player hides and sneaks (clips 630, 631, 633). Crept up on the first guard at 25 % stick, the player went into the fight stance 1.5 m from him (clip 380) before any L1, and L1 + square played the normal combo (12, 16, 19); the guard called his gang and the mission failed as spotted (update 975) (fixes 5 and 6).

Fixes, in the order to take them (each one implementer unit):

  1. A spray cut short leaves its spot dead (issue #330). Done: Coney's cut-short path now gives the tagger event 14 with (tag, 0). Played from checkpoint 1 into 2, triangle at chapter 2's first spot called HuTag and Coney logged that the spray was cut short on the next update; no event 14 followed, so TagFinished never put the spot's prompt back, and 30 later presses there did nothing. The original's Tag_UpdatePlayer (0x00273c60) calls Tag_End whenever the action is no longer 0x17 or 0x18 and the spray flag 0x2000000 is not set while the tag still exists, and Tag_End (0x0022e848) always gives the tagger event 0xe with finished = +0x3ba (0 here). Confirmed (code). Fix: Coney's cut-short path delivers event 14 with (tag, 0) as its refusal does. Test: a spray whose intro is cut short runs the script's event-14 handler with 0, and a second press at the spot starts a new spray.
  2. The fight stance cuts the spray's intro short (why chapter 2's first spray ended, issue #331). Done (issue #353): Coney now makes the fix below. Before the press the player was already in the fight stance (the stance walk clips 380 and 384, then the fight idle 358): chapter 2's first spot has a human within 2 m, whom the stance takes as its target; chapter 1's spots have none. HuTag started 334 ANIM_TAGGING_INTRO, and on the next update Coney's stance walk, with the stick at rest, put the fight idle 358 back over it, so the intro was no longer playing and the spray counted as cut short. In the original nothing of the stance runs during a spray: Human_UpdateActions (0x00254e78) calls MiniGame_Update, which for a tagging player runs Tag_UpdatePlayer, and returns at once when that answers 1, which it does from the first update of action 0x17 (when it calls Tag_StartSprayClips) to the spray's end; Player_UpdateActions (with the stance, Player_UpdateSprint 0x0027ce90) and Human_ChooseAnimState come after that return. The movement function moves nothing either: Tag_StartSprayClips (0x00278018) sets held flag 0x400, a busy bit (the locomotion gate), so Human_FightStanceMove (0x00241b90) sets no velocity while 334 plays. Confirmed (code) at the addresses; the cause in Coney inferred from its log (the clip column) and its code. Fix: while a spray runs (intro, loop and restart clips, and the stick game) the player's stance and anim-state choice do not run, and 334 holds 0x400. Test: a player in the fight stance (an enemy 1.5 m away, stick at rest) presses triangle at a spot; 334 then 335 play and the stick game starts.
  3. Spawned humans carry no money: withdrawn (2026-10-13, issue #354). The original rolls the starting money in Human_Init for every human: Spawner_MakeHuman (0x0016d860) calls it as Human_Create does. Confirmed (code). The first reading was that Coney rolls it only in HuCreate, but Coney's spawners make their human through the scripts' own HuCreate (ScriptSpawnerWorld::spawn), so a spawner's human rolls its money and object as a scripted one does, in the same order of draws; the test repo:tests/ai/spawner_money_test.cpp drives a spawner over the scripted brains and checks a class of $5-$20 gives $5-$20 and that the draws equal characters::rollStartingCarry()'s on the same seed. A level87 checkpoint 3 run (repo:tests/platform/disc_spawner_money_test.cpp, 1,500 updates) finds the street's civilians (type 395) holding $7-$15 and the bums (types 272-278, the bums among them) $1-$3. The spray and drug sellers (types 428 and 431) and the Destroyer soldier of type 197 hold $0; the Destroyers of type 194 hold the $5 a script gives them. These are figures seen in a Coney run (the disc test prints them; it asserts only the civilians and the bums), with no evidence grade for the original. The $1 muggings above were plausibly of bums (they hold $1-$3), the $0 ones of a seller or a Destroyer soldier, who hold $0 in Coney (inferred; which human was mugged is not logged), so "$0 is not faithful" is not established. The original's sellers (428, 431) hold $0 and a type 197 soldier $5 in 21 of 101 rolls and $0 otherwise, as Coney's roll gives (level87's sellers and soldiers), so Coney needs no fix here.
  4. A downed human drops no money. The original's Human_OnHealthOut calls Human_DropCarried (0x00232c60) when a human is knocked out or wounded, which leaves a dyn_money pickup of his money (Dropped when down). Chapter 3's two early Destroyers get $5 each (EDConstructor, HuSetMoney), the price of a can (GoalDealer), so beating them is the chapter's first paint (inferred from the script); Coney drops nothing. Fix and test on Crimes.
  5. A hidden player enters the fight stance. The original's stance starts only when the state word lacks 0x200000, the hidden state (Fight stance, step 3, 0x0027ce90), and L1 while hidden picks a target without the lock (The stealth kill). Coney's stance ignores the hidden state, so creeping up on a scout puts the player in the fight walk. Test: a hidden player walking to 1.5 m behind an enemy keeps the stealth walk (633) and has no target until L1.
  6. The stealth kill (Stealth; not in Coney yet): checkpoint 5's kill lesson needs it. Without it the guard is only hit, calls for help and the mission fails.
  7. The game hints 16, 22 and 24/25 above (issue #332): Coney queues only hint 0x15. Add the three triggers with their counters and flags in the game state. Hints 22 and 24/25 serve every level (HUD).
  8. Attacking on sight: the Destroyers of chapters 2-4 do not react to the player until hit (AI: attacking on sight, issue #73).
  9. The tag's look: the mist and the wall tag (fixes 6 and 7 of Crimes, issue #70), and the end clip 336 that removes the can (fix 9 there, issue #407; done for player 1).
  10. The spray sellers after a hit (GoalDealer, items 7 and 15): chapter 2's first seller is the one the hints point at.
  11. A playthrough test per checkpoint asserting the order above, once 1-10 land (issue #333; Testing; issue #135 for the rest of the reference order).

Open questions:

  • Whether a crew Warrior can be grabbed and mugged: the course grabbed humans standing beside the start more than once, and which human it was is not logged.
  • What the original does with ch3.BoozerChallenge, which the script names but never defines; its sphere goes off for any human, the second early Destroyer included, as soon as the chapter starts.
  • How the player reaches checkpoint 4's roof burners in the original: whether the bar door (number 11) is open at the checkpoint's start (OpenDoors(giStart) opens doors by script name, and which number each name has was not traced) or another way up exists that the path links do not show. The earlier note that the planner found no route out of the street is answered by the path data: no link leaves the street for chapter 5, whose way is the scripted door. A runtime walk of checkpoint 4 in the original settles both.
  • What a direct checkpoint 3 or 4 start holds in the original (paint and money come from the save's inventory; the course started with none).

level34.lua (mission 4)

Story mission 4, loaded from the hub's checkpoint 1: the riot. Vermin is player 1, Snow player 2, Ash the third Warrior. Read from the disassembly of level34.lua and its three chapter scripts; the bindings each call reaches are on Story coverage. Inferred unless marked.

  • Main: fog colour, HUD hidden then restored with the radar, SetSpawnMax(45), the start callback StartGame, then the checkpoint table: checkpoints 1, 2 and 3 all run level34_riot (P1.SetupRiot), each with its own Warriors set-up at a different spot of the street; 4 level34_gate / P2.SetupGate; 5 level34_park / P3.SetupPark.
  • RunLevel: at checkpoints 1-3 stores the checkpoint as Lua save float 1 (the riot's section, 1-3), then reads floats 1-5 back (F.LoadSaveData: section, riot done, radios done, loot done, muggings so far); the Warriors; preLoadFile of the chapter; the pedestrian flag net (SetupFlagNet, 34 nodes, as in level2); the follow camera; car colours from a 21-colour table; CarPlaceInTrunkOnDetach on four cars (a crowbar, a bat, item 5, a revival); the anim preloads; the tag spots (TagInfo.SetupTags("Tag", 7, checkpoint)).
  • StartGame (the start callback): at checkpoints 2-4 the riot ambient track, the 3D fog (Start3DFog) with MaxFogParticles(10) and a screen effect; at 4 also ForceCrimeLevel(true) and a crime at the cop search flag (CrimeIsHappening, type 7), so the police are already after the Warriors; at 5 the distant-traffic track; from 3 on the molotov car is already blown up (CarExplode); every light off, then the subway lights (checkpoint 1) or the wall light.
  • The riot's three objectives (objective line 0; each one completed is saved as its target in a Lua float and shown as done by P1.CheckRiotStatus on a reload; partial progress is not saved):
    • Riot meter (a HUD bar, HUDGetNewPH(3, ...), its text in per cent): 200 points. vLevelVandal hears message 6 (a human standing in it damaged something; only a Warrior counts): a newsstand gives 3, another world object (GetRTTI 8) 1, a glass pane (1024) 2; every car message 25 from a Warrior with its flag true (SetGeneralCarMsgHandler) gives 3. The first point ever counts as 1 %. At 35 % a tutorial line.
    • Loot: $350 of money picked up (CfgMoneyCallback; the start counts both players' money at that moment). On a reload with the loot not done, both players' money is set to 0.
    • Car radios: 3 picked up (CfgInventoryCallback, item 11 dyn_carstereo), from the three cars given a radio by CarSpawnRadio.
    • Bonus: 10 pedestrians mugged (HuSetMugCallback on both players, successes only) on line 1 with a counter; the tenth unlocks UM_Unlock(34, 1, 2). The count is saved (float 5) at every checkpoint.
    • All three done (F.CheckObjectives) runs P1.HeadToGate.
  • No scripted failure: none of the four chunks calls HUDLaunchMissionFailed and the script protects no one; the mission fails only in the engine's own ways (a player falling out of the world, Level loading).
Checkpoint Chapter What happens Ends with
1 level34_riot, section 1 Riot extras (GangRiotCivs1, suspended until the intro ends), six roof throwers (GoalStationaryThrower with bottles, speakers, TVs, boxes and drum kits; one hit by a Warrior fights), the dealer factory (flash and paint), riot and wanted gangs with turf boxes, the police spawner, 17 object zones on. The intro scene l34_c1 (the three Warriors and the subway door; callbacks 1 and 2 switch lights); at its end the riot ambient at 0.15, tunnel effects and an objective with a radar marker out of the subway. vIntro (a player) wakes the car flippers and plays the preloaded scene l34_car_flip, after which the loot objective starts and the flippers riot (GoalRiot). vEnterRiot (a Warrior) starts the riot meter, the fog and the full ambient. vStartIntroLooters sets a looter gang wrecking zone 51 (TacticVandalize); two looters talking (trigger sphere 10 m), two TV looters who fall out and fight, a weapon-break tutorial box vSection2 entered by a player: muggings and tags saved, float 1 = 2, SetCheckPoint(2), section 1's encounters removed, section 2 set up. Finishing the riot meter or the loot while still in section 1 also calls SetCheckPoint(2) (the section stays 1)
2 level34_riot, section 2 (Entered at 2: the flipped car replaced by a fresh CarSpawn.) The pharmacy owner with a bat (a line and an attack when a player reaches vNearPanzerStore; Panzers spawn off-screen 2 s later and TacticSteal), Huns wrecking zone 29 at vNearHunStore (a throw tutorial), a hardware owner waving the player in (sphere 5 m) who fights if hit, meat and radio looters (they talk, then leave or fight), a molotov thrower at vNearMolotovCar (GoalThrowObject; the car blows up 1 s after), a Moonrunner tagging. The riot spawners run while a player is in vSection2 (message 3 starts, 4 stops them). vCarPoizo (a Warrior): roof throwers paused, a locked camera, the Warriors' brains off, that Warrior warped to fStereoWarp, Vermin's line about the stereos; at its end the radio objective with its counter and a marker to the section 3 gate, brains back on 1 s later vSection3 or vSection3Gate entered by a player: the police scene l34_c3 (eight riot cops with batons, eight extras, the three Warriors, brain-off and suspended meanwhile), then the gate DblGt01 opens, wanted gangs and cops spawn, a scatter tutorial, ForceCrimeLevel(true) and a crime at fCopSearch1; float 1 = 3, SetCheckPoint(3), section 2 removed
3 level34_riot, section 3 The music-store owner and his men with bats (sphere 7 m: a random line when Warriors or wanted gangs come near), wanted gangs and cops spawning while a player is in vSection3; whichever objectives are not done yet P1.HeadToGate (all three objectives): the riot gangs leave through exits, level cops spawn as enemies of everyone, the turf boxes move to the riot turf, muggings and tags saved, SetCheckPoint(4), preLoadFile("level34_gate")
4 level34_gate Entered fresh: riot extras, wanted gangs, cops, level cops, the factory and the mugging bonus are set up again. Then an objective to fFinalGate (ObjectiveSetup, the global.lua helper), the gate DblGt00 opens for the player, a line, the objective on line 2. At the gate (P2.AtGate): the park scene preloaded, DblGt02 opens, an objective and marker to the Furies vNearFuries entered by a Warrior: every riot gang removed, the factory shut, SetCheckPoint(5), preLoadFile("level34_park")
5 level34_park Eleven Furies, three at the fence and two hurt Rogues (wounded, god mode). The scene l34_c4 (preloaded when continuing: the gate closes, the fog ends). Then the objective and marker to the subway exit, Vermin's lines every 15 s, system music on (tracks 160b_risen2 and the_fight_loop_01). The fence Furies shake the fence (two sound emitters); entering vEnterDiamond or vFuryConfront, or 5 s after the first fence Fury arrives, sets the Furies attacking (TacticAttack) and kills the two Rogues. Beating the Furies is not required vSubwayExit entered by a Warrior: the scene l34_c2 with Final true and the park gangs removed; its end runs PreCashTheWorld, which calls HUDLaunchMissionComplete() (how a mission ends); mode 1's Exit then plays L34_OUT (the level record asks for an outro, Movies)

Engine behaviour this mission needs beyond its bindings: message 6 from vLevelVandal when a human standing in it damages something (the box tests the human's position, not the object's), car message 25 with its flag, the money and inventory callbacks, the mugging callback, gang tactics' callback codes (1, 5, 6, 9) and the police answering a forced crime level (Crimes). Type bit 0x400 is a glass pane (Tasks); which prop counts how often: Breakable props.

The riot's crowd, and what Coney lacks

Why Coney's riot fills the subway the player starts in with humans, and what the mission needs to look like the original's (user checklist item 17). The script facts are inferred from the disassembly of level34.lua and level34_riot.lua, level34_gate.lua and level34_park.lua; the engine facts are confirmed (code) where an address is given.

  • The crowd is one spawner. AddRiotCivs1 (level34.lua) makes gang RiotCivs1 (GangCreate(23, ...)) with one spawner, RiotCivs1, of the civilian spawner type at (−57.1, −53.8, 0). Its arguments that matter: no total (−1), a spawn every 100 ms, up to 10 alive, state 7, value 30 and the spawn callback F.RiotPedSpawn (GangAddSpawner). P1.SetupRiot adds it at checkpoints 1-3 with the gang's hit handler F.RiotPedHit. At checkpoint 1 it suspends the gang (GangSuspend(gang, true)) until the intro scene l34_c1 ends; P1.EndIntroScene then wakes it, with the riot ambience at 0.15 and the tunnel effects. The player is still underground then: the start is (−4.3, −25.1, −6.2) (Level starts). Later handlers set the limit to 3, 6 or 10 (GangSetMaxConcurrent).
  • Where its humans appear. State 7 places each human on a pedestrian flag at least 35 m from the player beside or behind the view, or, ahead, at least the camera's view distance capped at 70 m (so possibly under 35 m), out of the camera's sight and clear of the gang's friends (AI and the world: reinforcements, 0x001679e8). From the subway, the street flags are out of sight behind its ceiling, so the original's rioters appear on the streets around the exit. Coney places state 7 as state 8 with a value of 0, which takes the nearest route node outside the view cone. In the subway that is a node on its tracks, next to the player. This is the subway crowd.
  • Every crowd and police squad of the mission is state 7. The other spawners are added in state 0 at their parked positions and started later with GangStartSpawner(gang, name, 7, value). They are the rival rioters RiotRogues and RiotMoonrunners (value 30, while a player is in section 2) and the wanted gangs CopRogues, CopMoonrunners and CopPanzers, and the police Cops (value 60, from the cop scene l34_c3 on and again in level34_gate). The level police RiotCops (50) start at P1.HeadToGate and in level34_gate. So the same placement decides where every gang and every cop of the riot appears.
  • What each rioter does (F.RiotPedSpawn(human, gang, name)): violence reaction off, threat and investigate responses 0. While bRiotPeds is true, 75 % get GoalRiot(h, 10, 100, 2, 0, 0) (GoalRiot) and 25 % FlagNetTraverse(h, 1, 100) (the 34-node pedestrian net that RunLevel sets up). While it is false, all walk the net. In both cases 3 in 10 get a random object of tCivObjects in hand (HuPlaceItemInHand). bRiotPeds is set true when level34.lua loads. P1.SetupSection1 sets it false for the intro, and P1.StoreObjectives sets it true again once the car flippers' scene has run. So from the loot objective on, most of the crowd riots.

Against missions.yaml. Its notes for level34 record the intro, the loot, the radio and mugging objectives, the props, the rioters' decisions and the riot waves as working, and leave one question: how the smash and loot acts find targets. The play-test shows these are also missing:

What the original does Coney now Page
State 7 places every crowd, gang and police human out of sight on a pedestrian flag placed as state 8 with value 0: at the player's side, on the subway tracks at the start reinforcements
A rioter's smash finds a vandalisable object with a stand point; a loot finds a store flag (activity 14) whose group still has objects nothing found (the question in missions.yaml) GoalRiot
The looters wreck zones 51 and 29 (TacticVandalize), and the Panzers rob the pharmacy (TacticSteal) both kinds are StationTactic: the members stand (repo:src/ai/story_tactics.cpp) AI: tactics
The roof throwers and bar staff throw bottles, speakers, TVs, boxes and drum kits (GoalStationaryThrower) the clip plays, nothing leaves the hand (repo:src/ai/riot_goals.h) GoalStationaryThrower
Police: the Cops and RiotCops spawners (state 7), and at checkpoint 4 a forced crime level with a crime at the cop search flag the spawners place their cops as above; the dispatch states 4 and 9 never spawn (no crimes are routed: checklist item 13) Spawners, Crimes
The Warriors' R2 commands not every command works (checklist item 11) AI: Warrior commands

Coney fixes (item 17).

  1. Build state 7's placement (its fix list). This one change moves the subway crowd onto the streets, and the rival gangs and the police with it.
  2. Check the crowd's tasks once it spawns where it should. F.RiotPedSpawn must receive (handle, gang id, spawner name). FlagNetTraverse must walk the 34-node net, and GoalRiot must roam, decide and leave in the gang's turf (RiotCivs1 has no turf box, so the turf gate always passes).
  3. Give the riot's smash and loot acts their targets: vandalisable world objects with a reachable stand point, and the store flags (activity 14) with objects left in their group (GoalRiot states 1 and 2).
  4. Build TacticVandalize (members wreck the zone's objects; code 1 once nothing is left) and TacticSteal in place of the standing stand-in.
  5. Let GoalStationaryThrower's throws release their object (the thrown-object physics), so the roof throwers and bar staff hit.
  6. Police: once 1 is in, check that Cops appear from the cop scene on and RiotCops from P1.HeadToGate on. The routed crimes (dispatch states 4 and 9) are item 13's work.
  7. A disc playthrough test from checkpoint 1: after P1.EndIntroScene, no RiotCivs1 human appears in the subway while the player is in it, and at least one appears on the street flags once he comes out.

Fix list, checkpoint by checkpoint

What stops level34 from playing start to end in Coney. Found by playing each checkpoint headless on the disc (--play-level level34 --checkpoint N, a waypoint driver over --pad-pipe with the left stick at 60-100 %, 2026-10-12, a release build of main at f325e018) and by reading the scripts above. The fixes are in the order an implementer can take them, and each says what "done" means. Items 1-3 are shared with every mission that uses spawners and link to their subject pages; the rest are this mission's.

Shared, first.

  1. The global spawn cap. Confirmed (code) at 0x00166708 (Gang_CachePlayerInfo, called with 1 from Gangs_Update 0x0016d170): a gang's spawners (GangSpawner_Update, 0x001681a0) run only while the game's spawned count is under its cap. For a police gang (gang +0x2c = 1) that is the dispatch count +0x324 under +0x326 (SetCopSpawnMax); for every other gang it is +0x432 under +0x434 (SetSpawnMax, which level34 sets to 45). Each spawn adds 1 to +0x432 (and to +0x324 in state 6), and GangSpawner_OnHumanGone (0x001672a8) takes it off again (Spawners). Before issue #325 Coney did not read SetSpawnMax, so at checkpoint 3 the riot, wanted-gang and police spawners filled the human pool: in 1,500 updates of standing still, HuCreate was asked for 240 RiotCivs1 humans and about 40 of each cop gang, and once the pool was full most of these calls failed (handle 0) on every update. Done: Coney keeps both caps and counts (Coney's implementation); with SetSpawnMax(n) two gangs' spawners never have more than n spawned humans alive, a removed human frees its place, and the police gangs keep to SetCopSpawnMax the same way (tests/ai/spawners_test.cpp). One gang's own spawners may pass the cap by the spawns of one update, as the original's do.
  2. State 7's placement (its fix list; item 1 of the riot's crowd). It is still a stand-in. At checkpoint 1, eight RiotCivs1 humans appeared on one subway spot (2.2, −66.7, −6.2), all at the same point. At checkpoint 2, 26 appeared on the street beside the player's start (−56, −143). At checkpoint 4, the wanted gangs and cops appeared 3-6 m from the player (−49, −256) and cut his health from 900 to 445 within 1,500 updates. Every crowd and police squad of the mission is state 7, so this placement decides how hard checkpoints 3 and 4 are.
  3. A suspended gang still spawns. Confirmed (code): neither Gangs_Update nor GangSpawner_Update reads gang +0xd4 (GangSuspend), so the original's RiotCivs1 spawner keeps making humans during the intro scene. Those humans only stand, because their brains skip their update (One update). Coney spawns them too, which matches the original; keep it.

Checkpoint 1 (from the subway to the street, the car flippers, then vSection2).

  1. Plays: the intro, the stairs, vEnterRiot, the fence (climbed with triangle) to vIntro, the scene l34_car_flip, P1.StoreObjectives with the loot objective, and the walk towards vSection2. Nothing blocks here once item 2 keeps the crowd out of the subway. The trigger boxes send their message for every human that enters, and the scripts filter by HuIsAPlayer or by gang, so that is harmless.
  2. The looters' tactics (item 4 of the riot's crowd). In Coney, TacticVandalize on zone 51 answered with code 6 (attacked) within 30 updates, and the intro looters left. In the original, they wreck the zone first and answer 1 once it is empty (tactic kinds). Done: the looters break zone 51's objects before they leave, unless they are attacked.

Checkpoint 2 (the stores, the stereo scene, then vSection3Gate).

  1. Plays: vCarPoizo warps the Warrior to fStereoWarp, Vermin says his line, and P1.DoneStereoPoizo gives the radio objective and its marker. The hardware owner, the Hun store, the molotov thrower and F.BlowupCar all run.
  2. What ends checkpoint 2 (settled; it was the driver). The script waits for message 3 from a player on one of two boxes, both handled by P1.EnterSection3: vSection3, set by P1.EnableSections when section 2 is set up, lies south of the shut gate DblGt01 (corner (−111.5, −279.4), size 96.5 × 55.2, so y up to −224.2) and cannot be reached yet; vSection3Gate, set only by P1.DoneStereoPoizo, is the strip in front of the gate: corner (−109.3, −224, −0.3), size 3.6 × 1.8 × 5, so x −109.3 to −105.7 and y −224 to −222.2, beside the gate's hinge at (−109.39, −223.97). P1.EnterSection3 goes on only for a player, while bObjCompleted and bSection3 are false and sLocation is not 3; it clears the box's handler, starts the cop scene (P1.SetupSection3), sets sLocation = 3 and calls SetCheckPoint(3). Confirmed (runtime), PCSX2 2.9.94, a state of level34 checkpoint 2 loaded through mode 8 (0x005e5d90 + 0x20 = 4, W_GameState + 0x33a = 2), the scripted pad at stick magnitude 0.3-0.7 and the script's globals read over PINE: after the stereo scene (RadioStolen set), walking south along x −107.5, bSection3 and bCopScene turned true between y −222.19 and −222.43, the box's edge, and the scene moved the player to (−96.7, −247.2). The earlier Coney run had stopped steering 1.5 m from its waypoint, at y −221.5, outside the box. With the waypoint inside the box and a 0.3 m arrival radius, Coney called SetCheckPoint(3) at (−107.6, −222.2), the same edge (2026-10-12, release build of 68f4e6d3). Nothing to fix; the playthrough driver must walk into the 1.8 m strip.
  3. CarSpawnRadio runs twice on the same three cars at checkpoint 2: once from P1.SetupTier2Encounters and once from P1.EnterSection2. The original does the same, because the second call does not test radioSpawn. Coney must make the second call harmless (no second stereo). Open question: what the binding does to a car that already has a radio.

Checkpoint 3 (the three objectives, then P1.HeadToGate).

  1. This checkpoint depends on item 1: while the pool is full, nothing else can spawn.
  2. The three objectives play together (settled). repo:tests/platform/disc_level34_objectives_test.cpp starts at checkpoint 3 and plays them with the pad: it steals the three stereos, takes store items until $350 is picked up (Taking owned items), and hits cars and street props until the riot meter reaches 200 points (a newsstand gives 3, another world object 1, a pane 2, and car message 25 gives 3). Each objective saves its target in its Lua float, and F.CheckObjectives then runs P1.HeadToGate, which calls SetCheckPoint(4). Confirmed (runtime, Coney). P1.HeadToGate is a Lua-to-Lua call, so a script trace shows it only through the SetCheckPoint(4) it makes.

Checkpoint 4 (to fFinalGate, then vNearFuries).

  1. The way to fFinalGate is a fence climb, and the objective waits for the crew (settled). No gate opens on the way: DblGt00 (opened by P2.SetupGate) is at x −111.9, and DblGt02 (−144.94, −270.82), the gate beside fFinalGate, opens only in P2.AtGate, after the objective. Past DblGt00 the street is a dead-end alley, y −251.5 to −260.75 at x −133, closed by a wall at x −135; its south side at y −260.75 is a chain-link fence, and the yard behind it holds fFinalGate. Confirmed (runtime), PCSX2 2.9.94, a state of checkpoint 4 loaded through mode 8, the player put at (−100, −255) over PINE to skip the street fight: walking west at stick 0.7 he stopped at (−134.96, −260.75), and south at 0.6 at (−131.5, −260.75); triangle there played the fence climb (anim ids 437, 438, 439, ANIM_FENCE_CLIMB_STANDING_01 and its two parts, about 2.4 s) and left him at (−131.5, −262.0), from where the flag is a walk; P2.AtGate ran (ParkScene set) with the other two Warriors beside him. Coney climbs the same fence the same way (the driver taps triangle facing south at (−131.5, −259.3)). What then held Coney is the objective's crew test (ObjectiveGoodToGo: every Warrior within 15 m of the player at the flag): its two AI Warriors stayed near the start (−55, −252) until about update 1,200, came on slowly and climbed after him, so the hint "You can't leave anyone in your Warparty behind!" showed and DoorOpen(DblGt02) came at update 4,307, then SetCheckPoint(5) at vNearFuries at 5,481. How quickly the original's Warriors follow from the start was not measured (the player was moved past the street fight). Done for the driver: climb at (−131.5, −260.75) and wait at the flag until DoorOpen of the second gate.
  2. The player died twice in 6,000 updates, killed by the wanted gangs and cops that spawned beside him (item 2), and the level restarted at checkpoint 4. Once item 2 is in, check that he can reach the gate. (In the run of item 11 he was not killed.)

Checkpoint 5 (the park, the Furies, then vSubwayExit).

  1. Plays: the scene l34_c4, P3.EndParkScene and the fence Furies. P3.FuryTimeout runs 5 s later (MoveFromFence, which kills the two Rogues), and Vermin says a line every 15 s, as the script says.
  2. Coney's Furies hit too early (measured; the cause is open). The original, confirmed (runtime), PCSX2 2.9.94, checkpoint 5 loaded through mode 8, the scene's end taken as the moment SubwayScene is set (P3.EndParkScene, which also schedules P3.FuryTimeout 5 s on through P3.ReachFence), the player left standing at (−207.97, −269.5) with no input, two runs: he turned to a fight stance (anim 359) at update 161, and the first hit came at update 276 and 294, both a knock-down (anim 292 ANIM_REACT_KNOCKDOWN_HIGH_FRONT, 294 ..._HIGH_BACK); then a knock-down about every 200 updates, ground hits between, 90 health a hit, from 1,170 to 26 by update 1,074 (run 1). Coney (release build of 68f4e6d3, --play-level level34 --checkpoint 5 --headless --trace, no input; P3.EndParkScene at step 816): the first hit at update 91 after the scene, before P3.FuryTimeout (update 149) and before any P3.MoveFromFence or P3.EnterDiamond from a Warrior, so some human attacks before the script makes the Furies enemies of the Warriors (GangMakeEnemies and TacticAttack are only in those three functions); the hits were small reactions (anims 272, 275, 280), the first time on the ground at update 368, health 900 to 216 by update 785. Done: no Fury hits a Warrior before P3.FuryTimeout, P3.MoveFromFence or P3.EnterDiamond runs, and the first hit comes about 270-300 updates after the scene for a player standing still. Next step: find which human hits first in Coney and what makes it hostile. Note: the original's player had 1,170 health here and Coney's 900; the original state came from a save with progress, so that may be upgrades, not a difference (not checked). Why the original's hits are 90-health knock-downs (issue #492): every park and fence Fury holds a dyn_bat from l34_c4's start, swings the bat's clips (501 at a run, 36, and 37 or the unarmed 193 / 194, which deal 25, at a man down), and each bat-clip hit is the bat's own contact, dyn_bat's 90 × the holder's class float, with the heavy high reaction 292-295 of the clips' code 0x2a; Coney's Furies have no bats (Combat: a bat's hit).
  3. The route to vSubwayExit (settled). P3.GetToSubway takes message 3 from any Warrior on vSubwayExit (corner (−256.6, −203.2, −12.7), size 2 × 3.8 × 3.5, turned about its centre (−255.6, −201.3) by RotateVolumeBox(0.696, −0.718, 0.718, 0.696)). Confirmed (runtime), the same state, full stick with L2 held (sprint), waypoints (−230, −270), (−245.1, −249), (−252.4, −222), (−248.5, −213.5), (−254.5, −202.2), (−255.6, −201.3): the scene l34_c2 started (the player moved into the subway car at (−113, −55, −207)), at update 494 with no hit in one run and at update 1,022 after hits (health 1,170 to 515) in another. The leg from (−252.4, −222) must go by (−248.5, −213.5): straight on, the player slides along an obstacle near (−250, −214). Without sprint (full stick) the Furies knocked him down near (−237, −258) or (−251, −223) about update 245, and in one run he died before the box. Coney with the same route and L2 held: P3.GetToSubway at update 4,085 and HUDLaunchMissionComplete at 4,501 (the first life, standing still, had died at checkpoint 5). Whether L34_OUT plays after it was not checked.
  4. How the AI Warriors meet the Furies (issue #493): in the original they hold their ground beside the player and let the Furies come; they never walk out to meet them. Confirmed (runtime), PCSX2 2.9.94, 2026-10-13: a state of checkpoint 5 made by loading it through mode 8 from a level99 state (the writes of item 14) and saved as the park's humans appeared, before l34_c4 started; then l34_warriors_meet_furies recorded every human every update with no input (hooks event-call and event-callback; two runs, identical step for step counted from the scene's end, which came at update 717 in one and 716 in the other). The scene's end is the update that runs P3.EndParkScene (its tfence_shake_01 emitter); times are updates after it. Vermin is the player, at (−207.97, −269.50); Ash and Snow are the AI Warriors.

    After the scene's end Ash and Snow
    0 Out of the scene under the follow command (tactic 0x005438c0), FollowPlayer (0x32) their only goal, standing 1.64 m (Ash) and 1.34 m (Snow) from the player
    0-150 No enemies; they stand. Snow turns once (mode 3, toward the player's heading) at +81
    +150 P3.FuryTimeout (P3.MoveFromFence): the Furies' GoalFight on the player and TacticAttack, the Warriors made enemies of the park Furies
    +152 / +178 Targets taken (fight mode was on for both from about +151, when the hostile count reached 2 and the Furies took attack slots): Ash takes FenceFury3 (7.3 m from him) as his target, Snow ParkFury02 (20.5 m); each enters the fight stance (358 / 359) and only turns to his target, again and again as it moves
    +280 Still standing where the scene left them, gait 0, 1.47 m and 1.14 m from the player
    +286 The first park Fury within 5 m of the player
    +299 The first hit, on the player (292, a knock-down). The same update Snow, his target 3.6 m off, starts MoveMelee and backs away from him at gait 2 (1.92 m/s); Ash does the same at +316 (4.1 m)
    +392 The first hit on a Warrior (Ash, 1,800 to 1,710); each Warrior answers his first hit with an attack action (0x00542ce0) while FollowPlayer is still his only goal
    +474 The automatic attack command: tactic 0x00544160, FollowAndAttack (0x34) with Fight (0x0f) on top: only now do they fight

    Until the first hit on a Warrior neither moved more than 2.96 m (Ash) or 4.68 m (Snow) from his spot, and never above gait 2 (walk), all of it backing away. Why, confirmed (code), Fight mode: under FollowPlayer a Warrior in fight mode only turns to a target beyond his melee range and backs off from one inside it (0x002de7c0); fight mode came on here because gang members had attackers (from +151 the Furies held attack slots on the player and on both Warriors) and the Furies had listed the Warrior as an enemy (0x002de650, test 4; the player had no target until +298); Snow's target came 26 updates after Ash's because the one enemy in his list was over 20 m from the player until then (inferred); the Warriors attack only from the attack command, which WarChief_AutoCommand (0x00303988) gives once the player's own target is within his far melee range and he has attackers for 1.5 s without a break (Automatic commands); with the player knocked down and his target lost, that took until +474 (inferred).

    Coney (as issue #493 states, from fix-furies-passive's run): its AI Warriors follow the player under FollowPlayer and run out to the Furies (one at up to gait 4, the other gait 3), so a Warrior is hit first and the player 6 updates later (that run's trace, steps 997 and 1003); in the original the player is hit first (+299) and a Warrior 93 updates later. The fixes, each against the original's behaviour on Fight mode (code in repo:src/ai/crew.cpp, FollowPlayerGoal):

    1. Turn, do not close in. In fight mode, beyond the range (the far melee range, or the near one when the target's actions are blocked or he is busy) queue only a turn to the target; within it queue the move-to-human action with the band range + 0.5 m to range + 1.0 m and a 3 s limit. Coney does the reverse (a move to the target beyond the range).
    2. The band move. Coney's MoveToHumanAction only closes in to a stop distance; it needs the original's near / far band. The spec is MoveMelee on Move-to-human, side circling, its ±10 counters and its done rule (in the band, side counter 0, |radial counter| < 6) included; in short: below near², back away at gait 2; above far², close in at gait 4; done inside the band. Without it item 1 cannot back off.
    3. The target. Keep the current target while it is still a valid enemy; else the nearest human of the follower's own enemy list (to the follower) that is up and within 20 m of the leader; else the leader's target when it is up, used for the turn or the back-off even when friendly but made the follower's own target only when not friendly; with no leader's target or a down one, none. Coney takes the nearest attacker of any crew member within 20 m of the follower.
    4. The fight-mode tests, in the original's order: the leader's target not a Warrior turns it on; a player holding an attack slot on the follower turns it off; any player's brain +0x2e4 turns it on; a member with attackers and a non-zero count of brains holding the follower as an enemy turns it on. Coney has only the last two, and counts only attackers who hold the follower as an enemy. Give the brain the count (raised when another brain adds it as an enemy, lowered when removed) rather than scanning attack slots.
    5. Field of view: 2π in fight mode, 110° (and the target cleared) out of it.
    6. Test ([disc], tests/platform/disc_level34_furies_test.cpp or beside it, checkpoint 5, no input): from the scene's end to the first hit on the player, neither AI Warrior is ever above gait 2 nor more than 5 m from where the scene left him, and his top goal is FollowPlayer; the first hit lands on the player, not a Warrior; neither attacks before he is first hit himself (each answers his first hit with an attack action), and neither has the FollowAndAttack (0x34) or Fight (0x0f) goal before the attack tactic (0x00544160) is queued for him, which comes after the player's first hit.

    The fight-mode stand distance, 10 m instead of the goal's 9 m, is already Coney's (kCrewFightFollowDistance, repo:src/ai/crew.h); keep it.

    Open: who queues the attack action a Warrior plays on his first hit while FollowPlayer is his only goal (the hit event's handler, The Warriors, is the likely source; not traced), and the Warriors' far melee range (+0x140). A new action is queued only when the queue is empty, so the runs bound it only roughly: Snow's last turn was chosen about 5.5-5.7 m from his target and his back-off at 3.6-3.8 m, Ash's back-off at about 4.1-4.3 m, so between about 3.8 m and 5.5 m (inferred). Reading brain +0x140 of a Warrior at runtime settles it.

A playthrough test (Testing) for the mission comes last: one driver per checkpoint that climbs the fences (checkpoint 1's, and checkpoint 4's at (−131.5, −260.75), item 11), walks the flag net (PedNet_*) between the boxes above, steps into vSection3Gate's 1.8 m strip (item 7), waits at fFinalGate for the crew, sprints to vSubwayExit (item 15), and asserts the SetCheckPoint calls 2-5 and HUDLaunchMissionComplete. Waypoint routes that already play checkpoints 2, 4 and 5 in Coney are those of items 7, 11 and 15.

level2.lua (mission 5)

Story mission 5, loaded from the hub's checkpoint 2: the Warriors (six, Cleon player 1 and Fox player 2) against the Orphans. Read from the disassembly of level2.lua and its four chapter scripts; the bindings each call reaches are on Story coverage. Inferred unless marked.

  • Main: fog colour, radar on, SetupCars (colours, radios and trunk items of five parked coupes), SetSpawnMax(25), then the checkpoint table, the same shape as level99's: 1 level2_clinic / P1.SetupClinic, 2 level2_tenement / P2.SetupTenement, 3 level2_clubhouse / P3.SetupClubhouse, 4 level2_junkyard / P4.SetupJunkyard, each with its own Warriors set-up at the chapter's start.
  • RunLevel: the Warriors; preLoadFile of the chapter; the follow camera (AddCameras, then CameraSetClipping(MainCam, 0.1, 90)); the mission's objective; at checkpoints 2-4 only: peds = 1, SetupFlagNet2(), system music on and the subway-intro objects destroyed. Then the anim preloads, the start callback StartAmbient, three bums (GoalBumLogic), ReportCrime(true), CfgSetMaxThrowError(5, 5), the dealer "factory" (StartFactory: a revive dealer and a paint dealer that respawn through gang spawners, all Lua) and the tag spots (TagInfo.SetupTags, a global.lua helper over the tagging bindings).
  • SetupFlagNet2 is a Lua function of level2.lua itself (function 96 of the main chunk), not a binding: it links the 13 flags PedNet_01-13 (made by AddFlagsBoxesPaths) into a pedestrian network with 13 FlagNetAddLink calls (two or three neighbours each), calls AddCivilians (gang 23 "Civilians" with one spawner whose per-spawn callback, global.lua's CivFlagNet, sets each civilian walking the net with FlagNetTraverse) and then sets itself and its twin SetupFlagNet52 (a 28-node net for level52, never called here) to nil, so it runs once. Its other caller is P2.SetupTenement, when peds is still 0 (the player came from checkpoint 1 in the same level). Nothing for the engine to register.
  • StartAmbient: from checkpoint 2 on, the subway loop and LoadBonus (the bonus count from Lua save float 1); a fade-in at checkpoints 3 and 4; the traffic ambient; Warrior command 4 (scatter) off.
  • The bonus: every Orphan killed counts (AnyOrphanDied, also hooked to most Orphan gangs' message 18) toward 40 on a second objective line; the 40th unlocks UM_Unlock(2, 1, 2). Each checkpoint saves the count (SaveBonus, SetLUASaveDataFloat(1, n)) and the tags (TagInfo.SaveTags).
Checkpoint Chapter What happens Ends with
1 level2_clinic Fade out, a locked intro camera, jumping blocked for both players, the scene l2_subexit (out of the subway). Three kind-2 boxes switch the player's camera on the way down to the street: vbIntroCam back to the locked camera, vbLeadRail a rail down the stairs (two points, field of view 50, CamLeadRail(3, 0, nil): trailing 3 m behind, mode 2), vbSideRail a level rail (distance 3.5 m, height 3 m). vEnterStreet (kind 0, a Warrior) kills them, gives the follow camera back and plays l2_c1_a. The Orphans at the dice game run to a fence and fight; five down, the bums open a gate (a path camera); the clinic scene, then 13 Orphans to beat (a backup gang spawns after 4) and a counter tutorial vJesseScene: SetCheckPoint(2), preLoadFile of the tenement
2 level2_tenement SetupFlagNet2 when not run yet; the scene l2_c2_a; Jesse runs a lead chase along a path ahead of the player, in god mode, and is interrogated when caught; wandering, dancing and roof gangs; entering vStartWander01 already preloads the clubhouse chapter (its set-up runs then) Jesse gives up → an objective (ObjectiveSetup) → a cut-away → SetCheckPoint(3), bonus and tags saved
3 level2_clubhouse The Orphans' clubhouse, friendly until vNearClubhouse (or a hit) plays l2_c7; then 15 to beat (backup after 4), a rage-throw tutorial; breaking the two speakers quiets the radio (SetAmbientEmitterVolumeMod, then off) the scene l2_c3 → SetCheckPoint(4), saves, preLoadFile of the junkyard
4 level2_junkyard Sully and the junkyard Orphans (suspended), Sully's car spawned; an objective to the gate whose callback removes the street gangs, shuts the dealer factory, drops molotovs and plays l2_c5; then a backup spawner, Sully shaking a fence and fleeing the player (trigger sphere 3 m / 6 m), a HUD bar for the car (HUDEnableBar, HUDSetBarPercentage); every car message 25 (SetGeneralCarMsgHandler) about Sully's car with its flag true counts one hit, 15 hits empty the bar the final scene l2_c6_b → HUDLaunchMissionComplete()

Engine behaviour this mission needs beyond its bindings: kind-2 boxes (the intro cameras) and the rail camera, mode 0 and the trailing lead. Open: which car events send message 25 with the flag true besides Car_DoExplode (0x0038ab50, flag 1), since the junkyard needs 15 of them on one car.

level3.lua (mission 6)

Story mission 6, loaded from the hub's checkpoint 3: the tag competition in Hi-Hat turf and the rooftop chase. Read from the disassembly of level3.lua and its five chapter scripts; bindings on Story coverage. Inferred unless marked.

  • The main chunk sets CHAPTER = GetCheckPoint(), a chapter table (1 level3_street, 2 level3_comp, 3 level3_balcony, 4 level3_chase, 5 level3_gallery), the Warriors' five tag patterns, and runs Main then RunMission. Main: radar range, objective, fog, ambient sound boxes, the pedestrian flag net (SetupFlagNet, as in level2), the chapter's Warriors (PlayerGang[CHAPTER]), the rival gangs' tag settings; player 1 is Rembrandt at chapters 1-2 and Snow from 3, player 2 Ajax; the follow camera; outdoor mode.
  • RunMission preLoadFiles the chapter with its Setup; NextMission runs the chapter's Cleanup, adds 1 to CHAPTER, SetCheckPoint(CHAPTER) and runs RunMission: every chapter hands over to the next in place. The global Startup is true only for the chapter the level was entered at, so a chapter knows whether it starts the level (its own scene set-up, SetStartGameCallback) or continues from the last.
Checkpoint Chapter What happens Ends with
1 level3_street The clubhouse scene, then the Soho street: three gangs at their spots (GoalGuardFlag, GoalPlayDynIdle, walks started by trigger spheres), a truce (hitting any of them fails the mission, C0.Damage), an objective to the competition entering the gate: the Warriors walk in (brain off, GoalMoveToFlag), two locked cameras, NextMission 3 s later
2 level3_comp The tag competition: the scene l3_c1_a, locked cameras for the tutorial, the Warriors' wall (SetMsgHandlerEx message 0 → HuTag with patterns; message 14 per tag finished), rival painters (GoalTag), HuEnableTagCheer the victory scene → NextMission
3 level3_balcony Chatterbox throws molotovs, bottles and bricks from a balcony (GoalBigLedgeThrower), vans to save, a button and an elevator (HuUseAnyAnim for the button press, HuSetAutoCombat), mimes, a pulley the balcony scene → NextMission
4 level3_chase The rooftop chase, below vbEnterGallery: the jump scene → warp to the gallery, NextMission
5 level3_gallery A 180 s stopwatch (W_SetStopWatch), the gallery's statues and paintings to wreck (a HUD bar; vGallery hears message 6, ChangeCollision on statues that fall), seven tag spots; falling (vDeathGallery) or time running out fails damage ≥ 200: player brain off, fade, the end scene → HUDLaunchMissionComplete()

The rooftop chase (checkpoint 4). Snow is player 1. In order:

  1. Setup: the Hi-Hat pursuers (AddPursuit: five, gang type 20, and a spawner off-screen), each taking 5× damage (HuApplyDamageModifier); the path roofpath2 (eight rooftop flags); a fade to black; Snow tireless; five locked cameras for the jumps and a locked intro camera made current; the rail camera (ch4.SetupRailCam: CamSetupRail("ch4.railCam", player, 78, {0, 0, 2}, 0.05, 150) with two points, (−273.776, 349, 29.971) and (248, 349, 29.971)) and its main framing (ch4.MainRail: settings 9 = 2, 2 = 78, 3 = −8, 4 = 6, 0 = 5.5, 1 = 4, eased over 0.5 s); Warrior commands 0-5 off, radar and command HUD off. Entered at checkpoint 4 the start callback is ch4.HoldIntroPoizo; continuing from checkpoint 3 it runs at once.
  2. ch4.HoldIntroPoizo: switches 3 off and 4 on; every non-player Warrior brain off, short sight, fast climber; BrDead(player, true): the player's brain is off, which also takes his pad away; fade in over 2 s; schedules the mimes' door (0.1 s), ch4.SetupPoizo (3 s) and the pursuit (4 s).
  3. ch4.SetupPoizo: a path camera from the intro camera through three points (the second calls Snow's line); every other Warrior: brain flushed, normal mode, a friendly devil run on roofpath2 against the Warriors' gang (gait 4, pace 4 m, up to 10 m/s, urgency 3) under a GoalMoveToFlag to the first chase flag (gait 5); the player: normal mode and GoalMoveToFlag(player, fRoofChase[1], 4, ...). With his brain off the player's brain still runs the goals a script pushes (Brain_UpdateGoals), so the AI walks Snow from the warp point (−268.5, 353.6, 16.6) toward the first chase flag (−214.7, 364, 20.2) while the pad is ignored.
  4. ch4.StartMimePursuit (4 s): the pursuers run to the first chase flag; two Hi-Hat lines.
  5. ch4.StartRail, message 3 of vbStartRail, a kind-2 box (x −260.5 to −256.1, y 357.4 to 376.1, z 18.3 to 27.6), which only a player can set off: the box disabled; the pursuit spawner started; the objective to the roof's end; the pursuers made enemies and each given a hostile devil run on roofpath2 against the Warriors (gait 5, attack at 1 m, pace 13 m, up to 13 m/s, urgency 0.5); switches 3 on and 4 off; the rail camera made current over 0.5 s; HuLockPad and HuLockPadMovement cleared and BrDead(player, false): from here the stick moves Snow, relative to the rail camera's heading. Player 2 gets the same.
  6. During the chase, all by kind-2 boxes (message 3): vMainRail / vSideRail / vTopRail / vBigRail re-frame the rail with CamModifyRail (side: distance 8, height 2, setting 3 = 2, 4 = 0, field of view 65 or 78; top: height 20; big: distance 10, height 7, field of view 65, setting 9 = 4). vFixCam boxes switch to a locked (box 13: a fixed) camera over a jump; while no second player is alive they also give slow motion 0.2 for 2.5 s (boxes 1-9) or 2 s (box 13, the first time only, HuSetSlowMo) and lock the pad (HuLockPad, HuLockPadMovement; not in boxes 11 and 14, and no more once box 13 has been entered) until ch4.TurnOffSlow or leaving the box (message 4, which also returns to the rail camera). vbJumpWarn boxes show a jump prompt (HUDTurnOnActionCycleAnim) for 1.5 s. Falling into vChaseKill (kind 0) fades and fails the mission (MF_4); a pursuer there dies, a Warrior is warped to the gallery. A fence across the roof: at x 85.18 (y 363.67-371.10, z 15.03-17.62), between two posts of the level mesh that leave no gap, stands a row of three dyn_door_fence barriers (door numbers 4, 6 and 10, 10 hitpoints each, two triangles each). Read from the disc's SpawnDoor placements and mesh in Coney (numbers only, 2026-10-11). No chapter script opens them; as every barrier they are two-sided from their spawn and stop a walker from either side (How a closed door stops a human), so the way on is to break one: a charge or a dive (16) breaks one in a hit, three plain hits do too, and it lets him through at its next update (Barriers). Inferred from the confirmed (code) barrier behaviour; the chase was not played in the original to see which hit a player uses. (Before Coney kept the barriers' spawn-time triangle writes, their one-sided triangles let Snow walk through from the west.) The way across the last roofs, read from the disc's collision mesh (inferred; not checked in the original): the jump from vbJumpWarn_08's roof (z 13.2, edge at x 150.5) lands on a roof at z 10.3 (y 361-369.5). Its south part is closed at x 167.4 (y 361.5-366.7) by a wall 3.6 m tall, material 152 with no climbable flag, so no climb takes it (Characters: climbing). The way on is the steel stairs beside it (material 3, y 367-369.5): two 33° flights with a landing from x 167 to 177.5, a platform at z 16.46 to x 193, then the jump in vbJumpWarn_09 over the gap (x 193.5-200.5) to the gallery's roof (z 14.4) and vbEnterGallery (x 200.8-208.8). Outrunning the pursuers, confirmed (runtime) in PCSX2 (coney-tools trace mission from a level3 checkpoint 4 state, 2026-10-21): a player who walks up to each climb is caught (Snow knocked down near x -97 and knocked out at x -74.5 by about update 2,000), while one who sprints into the climbs reaches vbEnterGallery (SetCheckPoint(5) at update 3,038). Coney's playthrough test drives the second way and reaches the gallery for every --ai-seed from 0 to 7 (Testing).
  7. ch4.EnterGallery (vbEnterGallery, kind 2): objective done, handlers off, the chase gangs removed, tireless and fast-climber cleared, the jump scene, then the Warriors warped to the gallery and NextMission.

So the player has no control from the fade-in until he reaches vbStartRail, by design: the original walks him there with a goal on a brain-dead player. A port in which a kind-2 box never sends message 3, or in which a pushed goal does not move a brain-dead player, leaves the player standing for good. Confirmed (code) for the brain and box behaviour; that Snow's walk crosses vbStartRail is inferred from the positions (not seen at runtime).

Engine behaviour this mission needs beyond its bindings: kind-2 boxes, the rail camera mode 0, GoalDevilRun and message 6 from vGallery when a human standing in it damages something.

level5.lua (mission 7)

Story mission 7: Ajax and Cochise after Sanchez in Hurricane turf, the bar, the chase across the yards and the boss fight with Diego and Vargas. Ajax is player 1, Cochise player 2. Read from the disassembly of level5.lua and its four chapter scripts; bindings on Story coverage. Inferred unless marked.

  • The main chunk sets CHAPTER = GetCheckPoint() and a chapter table (1 level5_chapter1, 2 level5_chapter1a, 3 level5_chapter2, 4 level5_chapter3), then runs Main and RunMission, as in level3: RunMission preLoadFiles the chapter with its Setup; NextMission runs the chapter's Cleanup, sets LastChapter, adds 1 to CHAPTER, SetCheckPoint(CHAPTER), saves the tags and runs RunMission. A chapter's Setup checks LastChapter to tell "continued in place" from "entered at this checkpoint" (then it waits for its start callback and plays its opening scene itself).
  • Main: fog colour, fog distance 0, the tag spots, the radar, the chapter's Warriors (checkpoint 1: Rembrandt, Ajax, Cochise, Snow outside; 2: the same four in the bar; 3 and 4: Ajax and Cochise only), the follow camera, car colours, SetSpawnMax(30). At checkpoint 1 the bum and idle anims are preloaded. The bonus flags ("Car", "Bar") are packed into Lua save float 1 (EncodeU32): written at checkpoint 1, read back from 2 on, and a done bonus is shown done; SetupFlagNet (the pedestrian net) runs from checkpoint 3 (chapter 1 runs it itself).
  • Helpers in level5.lua used by the chapters:
    • StartChase{Human, MaxDistance, FailDialog, Reason}: every 50 ms the distance from the chased human to the nearer player is checked; at MaxDistance (35 when not given) or more a 5 s watch starts, and getting back within range cancels it. When it runs out the player says FailDialog and its end calls HUDLaunchMissionFailed(Reason). EndChase stops it. Several tables pass Distance instead, which StartChase does not read, so those chases use 35.
    • SRun (Sanchez's run): entering volume n of the run's list (a human of gang type 0, a Warrior) disables it, Sanchez says line n, and if path n exists he runs it with GoalRunCarrotRun(..., 5, 1, GangWarriors, 13) (keeping ahead of the Warriors); the next volume is then armed. SRun.WarpSanchez teleports him over a pile (a chair-break sound and a line).
    • SetUpPOIZOCAM{Cam, SayVag, SayVag2, Human, Human2, bActionDialog, Time, Delay, Callback, bSkippable, ...}, the in-game cut-away: spotting off, Warriors deactivated, rage ended on both players (HuSetRageMode(false)) and normal mode, fade, the camera switched, any non-Warrior gang that comes within 5 m of a Warrior suspended (trigger spheres), the listener moved; then one line, two lines one after the other (the second started from the first's end callback), or both through HuActionDialog; skippable with the pad (RegisterPOIZOCallback, a global.lua helper on pad bits 64 and 2048). POIZOCamBack undoes it all and calls Callback(skipped).
    • SetSanchezMode: Sanchez is god-mode, ungrabbable, ungroundable, untargetable, unreachable, does not react and never wants a weapon, so he can only be chased.
    • SwitchNodeStack / SwitchNode: when a listed crate stack, skid or trash can breaks (message 2, or 1 for the last two) the nearby path jump node is turned into a door node (ConvertJumpToDoor at its fNodeSwitch flag), opening the way for the AI.
Checkpoint Chapter What happens Ends with
1 level5_chapter1 Fade out; four Hurricanes at a car (suspended, playing idle clips), the stoop crowds and civilians, zone 1, the car's music (a looping emitter) and a radio in it (CarSpawnRadio; stealing it, CfgSetSteroTheftHandler, stops the music), car parts 17 and 21 removed. Start callback: brains off, the scene l5_c8_b (the Warriors and four civilians); then a line, an objective to fObjective01 (ObjectiveSetup, callback C1.OpenBar), the passing-train sound scene Harlem_Subsound every 30 s. vAdvCombat: a wall-smash tutorial. volCar[1] entered by a Warrior (or a Hurricane hit by one): the Warriors warped, three bums with molotovs (GoalBumLogic), the scene l5_c4; then the Hurricanes confront (CONFRONT tactic), the dealer factory (revive, paint), trunk items, a backup spawner, the car bonus (line 1 in per cent and HUD bar 1: 15 car messages 25 with the flag true on that car, or one whose third argument is −1, complete it, UM_Unlock(5, 1, 2), music off, the "Car" flag saved) the objective reached: a locked camera on the bar's front, the train stopped, the wanted level cleared, the arriving Warrior put at the door, the cut-away with one skippable line; its end runs NextMission → SetCheckPoint(2)
2 level5_chapter1a The bar: Sanchez (SetSanchezMode), the bartender (god mode), two Hurricane gangs (CONFRONT, aggressive), three bar girls; zones 10 and 6. The scene l5_c2 (entered at 2: from the start callback), then room smoke (StartRoomSmoke), the bar's music loop, a line, the fight objective, a rage tutorial after 7 s every Hurricane of both gangs dead (message 18 counts down): 3 s, the scene l5_c5; Rembrandt and Snow deleted, the music tracks set, NextMission → SetCheckPoint(3)
3 level5_chapter2 The chase. The scene l5_t16_000, then Sanchez with a spinning icon and a flashing radar marker, spotting off, the objective, StartChase (33 m, fail line l5_t16_012, reason LEVEL5.MF), his first run. volFightF1[1] (once Sanchez has reached volSanEnd[1]): a door closes, the chase ends, the scene l5_t9_004, four Hurricanes fight, two more 1 s later; "back-timer" boxes restart and stop a chase watch while player 1 is between them. All six down: the scenes l5_t8_000 and l5_chase_poizo2, Sanchez runs to a gate, a new chase (60 m, line l5_t16_013). Then the yards (zone 3): volSanDoor01 arms the second run, volFightPit[1] stops the chase for the scene l5_t9_010 and three pit gangs (MANPILE, HANGINGOUT) 3 s later, volFightPit[2] sends Sanchez on, volSanWarp boxes warp him over piles, volSanGo[1] adds a bum blocker, volSanGo[2] starts the third run and a chase (35 m) volStartHouse entered by a Warrior: the chase ends, NextMission → SetCheckPoint(4)
4 level5_chapter3 The boss fight. Rembrandt and Snow rejoin; Diego with his weapon; lights and music off; zone 5; continuing in place, player 1's health is set to 1170. The scene l5_c6, then a locked opening camera, the boss rail camera (CamSetupRail, two points), TacticBossScenarioA stage 1 with the boss HUD bars (bar 3, two names), Warrior commands limited to 1, 2 and 4 (command 1 issued), Warrior weapons off. The tactic's callback: code 1 ends a stage (stage 2: the scene l5_c7, Vargas fights in god mode; stage 3: the scene l5_c8, two tenants join, a second Vargas breaks through a door 5 s later); code 18 (Diego down) puts the camera on Diego and backs the Warriors off 5 m with rage ended. Diego's bar never shows below 66 % in stage 1 or 33 % in stage 2. Hitting the plaster drops a chandelier (the scene l5_chandelier) stage 3's callback: the tenants leave, Diego suspended; 3 s later (waiting while a player is tagging) the Warriors are stilled, a line, a 2 s fade, the scene l5_c3 with Final true (its callback 1 breaks the glass within 10 m of fSanDead); PreCashTheWorld then calls HUDLaunchMissionComplete() (how a mission ends). The level has no outro movie

Fail states: only the Sanchez chase in checkpoint 3 calls HUDLaunchMissionFailed (a player more than the chase distance from him for 5 s); no one else is protected (Sanchez and the bartender cannot be hurt).

The two binding questions (confirmed (code)):

  • HuSetRageMode(human, true) calls the Lua function whose name CfgRageHandlers stored first (copied to 0x006b6830 by Cfg_SetRageHandlers, 0x00236c58). global.lua registers CfgRageHandlers("SetRageMode", "ClrRageMode", "RageFull", 20000, 5000) at boot, so it is SetRageMode, which sets god mode, unstunnable, reduced and increased reaction, ungrabbable, ungroundable and keep-weapon on the human (and locks rage with the rage cheat on). Human_StartRageMode (0x00236d28) passes the human and a flag (a count at 0x0051489c + 0x268 below 1). Ending rage (Human_EndRageMode, 0x00236fb8) calls the second name, ClrRageMode, which clears the same flags. Rage_ResetConfig (0x00236be0) clears the three names.
  • HuActionDialog's two lines start together, not one after the other: Human_ActionDialog (0x00239788) starts the first speaker's line and then the second's in the same call, each through Human_PlaySpeechCutting (0x0021e698), which plays a mono stream at once (mono streams use channels 5-12, Sound) with no queue between humans. The partner link (human +0x18c) is only read by Human_UpdateSpeech (0x0021e940): while a line plays and the partner is within 10 m it calls Human_StopSpeech (0x0021ec38) on both without force, which stops nothing that is still playing, and clears both links. The callback runs when the second speaker's line ends. That the lines overlap audibly is not checked at runtime. This mission uses it only in cut-aways with bActionDialog; the two-line form without it (SayVag2) does wait for the first line.

Engine behaviour this mission needs beyond its bindings: kind-0 volume boxes for a Warrior, message 8 from a flag when a human reaches it (the banter switches), message 18 on a death, car message 25 with its flag and third argument, ConvertJumpToDoor on a path node, and TacticBossScenarioA's callback codes 1 and 18.

Errors in a fresh state

Two level scripts stopped in Coney's fresh Lua state (scripts read with the disassembly; the bindings' code confirmed):

  • level5.lua at checkpoint 2, "comparing nil with a number" on instruction 19 is in DecodeU32 (a level5.lua helper that turns a number into a table of 30 bits; instruction 19 compares the remaining value with 2 ^ i). The level's start-up calls it, for every checkpoint above 1, as DecodeU32(GetLUASaveDataFloat(1)) to restore the mission's saved progress bits (tblSaveData). Coney has no GetLUASaveDataFloat, so the call returned nothing. The original returns a number: slot 1 of the eight saved script floats at W_GameState + 0x570c (0x0037b850 → 0x0041ad00, slot n at +0x570c + (n − 1) × 4, n read as 16 bits, no bounds check). The game state's constructor (0x00418588) zeroes the eight floats and the saved flag bits at +0x572c, so a level entered at checkpoint 2 without a save gets 0.0 (no bits set). SetLUASaveDataFloat (0x0041acd8) writes the slot; level5.lua itself stores the encoded bits there (EncodeU32, in two of its functions). Confirmed (code) for the bindings and the zeroing; the script side inferred.
  • level102.lua, "indexing nil" on instruction 5 is in the global.lua helper FlagPos: instruction 5 reads .x of what GetFlagPos returned. The arena's ConfigRumble calls AddDummyPlayer, which does HuCreate("Dummah", 352, FlagPos(fP1[1]), ...); fP1 comes from level102_brawl_init.lua's AddFlag calls. Coney has neither AddFlag (so fP1[1] is nil) nor GetFlagPos (so FlagPos indexes nil). The original provides both as bindings: AddFlag returns a handle, GetFlagPos an M_Vector4 with x y z w (World flags). Nothing else is missing before that point: Level is set by global.lua, RumbleInfo and Rumble by the arena's main chunk.
  • GetRumbleModeData (also missing in Coney) fills RM_LuaData with the Rumble menu's 23 choices; with it absent the table keeps the zeros ParseLuaData put there, so Rumble.gameType is 0 (RumbleInfo[0] is "brawl", the right file by chance), Rumble.gangSize is 0 (the "no gangs" path, ShowRules1) and Rumble.gang1[1], the player's type, is nil. A playable arena needs the menu's values (inferred from the disassembly of level102.lua; the values are in Front end).
  • level95, events.ChatEvent: attempt to index a nil value: events.SetupConversationEvent(cluster) adds 1 to events.NumEvents, stores events.ChatTable[NumEvents] = {eventCluster = cluster, lastStatement = 1, lastTalker = NilHandle} and calls ScheduleFuncArg1("events.ChatEvent", events.NumEvents, 5000 + random(1, 5000)); ChatEvent(n) then reads events.ChatTable[n].eventCluster and that cluster's CClusters[c].occupied. The script expects the number first and the delay last (as 0x003572e8 does). Coney's binding takes them the other way round, so it called ChatEvent with 5001-10000 after NumEvents ms, and ChatTable[n] was nil. Confirmed (code) for the binding, the script side from the disassembly. (flags.Cleanup also sets CClusters to nil, so a chat event that fires after it would fail the same way; inferred.)
  • level87, ch3.BoozerChallenge is not a function: the third chapter's table at run time holds Cleanup, KillHandlers, AjaxSaysPissTank, DoNextChapter, BumScrap, OneBum, Done, Preloaded and Exit, but no BoozerChallenge, although a callback names it. Confirmed (runtime) in Coney's state; inferred to be a slip in the original script, which would also find nothing to call. Coney logs the call ("is not a function; not called") and skips it; what the original does with a missing callback is not traced.
  • level87 from checkpoint 1 into 2, "attempt to index a nil value (instruction 19)" after the drunk's scene (l87_drunk_dest): the scene's end runs PreCashTheWorld, which calls the second chapter's return function. That function calls RestorePreviousTags and then ch2.ArrowRadarStuff. Instruction 19 of the return function is the read of .ArrowRadarStuff from the global ch2, which is nil. The cause is the order in the first chapter's end (The_REAL_End): SaveLevelData, SetCheckPoint(2), ChLoader (preLoadFile of chapter 2 with "Setup"), then the global Cleanup. In the original the file is only requested there (Script_PreloadFile, 0x00357a68, hands FileManager_Request the completion routine 0x00356d00; confirmed (code)), so Cleanup is still chapter 1's: it sets ch1, Setup and Cleanup to nil, and chapter 2's chunk and Setup run later, when the file arrives. Coney's preLoadFile runs the chunk and Setup inside the call, so the Cleanup that follows is chapter 2's, which sets ch2, Setup and Cleanup to nil. A direct start at checkpoint 2 never runs chapter 1's end, so ch2 survives. Save float 1 (1024 for tag 10) is not the cause: with a bit set RestorePreviousTags reads master[tagflag[i]].wtag (its own instruction 19), and master exists, since chapter 1's Setup builds it (LoadTagTable) and nothing clears it. The original does not fail here (inferred from the disassembly). A preLoadFile must therefore run its chunk and callback after the calling Lua function has returned, at the earliest.

Notes for implementers

  • Table constructors flush every 62 items, not 64: SETLIST stores its items at A × 62 + 1 onwards (luaV_execute, 0x00334478, the multiplier 0x3e; the largest B on the disc is 62). This is this build's LFIELDS_PER_FLUSH. A VM that uses 64 shifts every list item past the 62nd (in config_preload3.lua's levelNames, every level record after the 62nd). confirmed (code).
  • What a script needs to run without errors on the front-end path: the preloads in one state, then global.lua and level100.lua in that same state, with numbers from GetLanguage, GetPlatform, GetCurrentLevelIndex, GetLevelId and UM_IsLevelComplete (nil for false).

Game-state functions

Functions of the game-state module (0x00417af0-0x00424e50, its range confirmed (code): inventory, statistics, flags, configuration workers) that belong to this page, by address.

Address Name What it does Evidence
0x00418518 / 0x00418520 / 0x00418528 GameState_IsDongleValid / GameState_CreateDongle / GameState_CreateDongleChallengeKey the workers of IsDongleValid (always 1), CreateDongle and CreateDongleChallengeKey (always 0): stubs on the PS2 confirmed (code)
0x0041abc0 / 0x0041abe8 Script_SetCheckPoint / Script_GetCheckPoint the workers of SetCheckPoint (GameState_SetCheckPoint) and GetCheckPoint (+0x33a) confirmed (code)
0x0041abf8 / 0x0041ac88 GameState_SetSaveBit / GameState_GetSaveBit bit n of the saved flag bits at +0x572c (32 per word), set or cleared / tested confirmed (code)
0x0041ad28 / 0x0041ad60 Script_SetLUASaveDataBool / Script_GetLUASaveDataBool the workers of SetLUASaveDataBool and GetLUASaveDataBool: bit n − 1 of the same set confirmed (code)
0x0041af20 / 0x0041af48 Script_SetHatCallback / GameState_SetHatCallback SetHatCallBack: the name at +0x33c confirmed (code)
0x0041af80 GameState_CallHatCallback from Human_HandleMessage: unless +0x410 is set, calls the hat callback with (human, object, the object's type name) confirmed (code)
0x0041cf20 / 0x0041cf40 / 0x0041cf58 GameState_SetFlagByte / GameState_SetFlagBits / GameState_ClearFlagBits the four game-state flag bytes at +0x3e8 + n that GameState_TestFlag(n) reads: write, OR, AND NOT (GameState_SetDetailFlag and GameState_ClearDetailFlag use the last two) confirmed (code)
0x0041d830 / 0x0041d860 GameState_SetDetailFlag / GameState_ClearDetailFlag setDetailFlag(n, bits) / clearDetailFlag(n, bits) on the flag bytes +0x3e8 + n confirmed (code)
0x00423358 StopWatch_Construct the stopwatch (0x50 bytes, from Game_InitializeSubsystems): fields zeroed, rate +0x10 = 1.0, +0x44 from 0x00598690 confirmed (code)
0x004235a8 StopWatch_SetCallbackName copies up to 31 characters of the callback name into +0x20, or empties it for none; from W_SetStopWatch and UnloadLevel confirmed (code)
0x004235e0 StopWatch_SetDisplay sets shown (+0x18) and the label (+0x1c) confirmed (code)
0x00423638 StopWatch_GetTime the stopwatch's current time (+0x08, ms) confirmed (code)
0x00423670 StopWatch_Show W_ShowStopWatch(show, label, warn): shown sets the warning window +0x40 to warn and the next beep +0x48 to warn + 1000; hidden resets them to 0 and 1000; then StopWatch_SetDisplay confirmed (code)

Coney's implementation

The script system and the front end's scripts run in Coney (2026-10-04), written from this page; the Lua 4.0 virtual machine underneath is Front end's.

  • ScriptSystem (src/scripting/script_system.h): one Lua state, made by create() (the libraries in the original's order, string, base, math; then the bindings; then _ERRORMESSAGE and _ALERT as functions that do nothing) and made again by the level flow's unload. runFile runs a WAD script by name, runFiles a list, enterLevel runs global.lua and then <level>.lua, call finds a function by dotted name (Menu.onStart; a : passes the table as self) and calls it, schedule/flushScheduled keep the schedule of named calls with up to two number arguments, and update runs the calls that are due and the update function (with the step in ms). The original's errors leave no trace; Coney writes each to its log (script error: <script>: <message>) and counts it.
  • The libraries (src/scripting/lua_libraries.h): the 33 base functions with the four compatibility names and _VERSION, the string library (12 names with Lua's patterns) and the math library (23 functions, PI), written anew from the public Lua 4.0 library.
  • The bindings (src/scripting/script_bindings.h): one table, by name, saying for each whether it is real (does its job: the getters and script-system bindings of the tables above, CfgLevelName, the five string bindings, ShowProfileManager, MenuLoadLevel, ScreenQueueEffect, HUDLaunchMissionComplete, GetGameTime, HuCreate, which keeps the humans a level script makes, Level loading, and the level bindings of src/scripting/level_bindings.h: the flags, GetPosition, the saved script numbers, SetStartGameCallback, GetRumbleModeData, GetRumbleModeGangName and CfgSetDatabaseSizes, World flags, the AI and gang bindings, which hand their calls to the AI host, AI, the trigger bindings of src/scripting/trigger_bindings.h: SetMsgHandler, SetMsgHandlerEx, AddVolumeBox and RotateVolumeBox, below, the three animation callback bindings, Characters, and the scene bindings with the GoalJoin* goals, which play scenes, Scenes; with no scene system the preload and play bindings fall back to Coney's stand-in, where a scene loads and ends at once), routed (handed to a Coney stand-in that logs it: PlayMovie, the three music bindings, ShowRumbleModeInterface) or a stub (returns its documented default: nothing, a new handle for GetPTank, ObjSpawn and CameraCreateLocked, 0 for InvNumberOf, false for UM_IsTypeDirty). The configuration stubs (the Cfg* bindings, CfgObj, sound, unlockables and commands) keep their arguments (RecordedCalls) for the subsystems that will need them. 97 real, 5 routed and 106 stubs (72 of them recording): every binding the front-end path calls, and what the level scripts need for their starts.
  • Message handlers and triggers (src/scripting/message_handlers.h, src/world_objects/volume_boxes.h): an object's callback per message number (26 slots), delivered with the arguments of the table above; the volume boxes with their turn and the kind-0 trigger update (3, 5 once per 1000 ms, 4) over the scripts' humans, run by gameplay every frame before the scripts' frame. An occupant whose handle no longer resolves is skipped with no 4 and kept; Coney stand-in: a human the scripts made resolves for the whole level, HuDeleted or not (when the original's stops is not traced), so a deleted occupant leaves with 4 and HuGetGang still gives its gang. A human's events reach its own handlers through its brain, a flag arrival is message 8 (AI). Message 6 (VolumeBoxes::sendDamage()): a pane, door or car hit that the object took sends (human, box, object) to every enabled kind-0 box the attacker (a thrown object's thrower) stands in. Coney does not send it for props, has no 30 m alert after it, and updates the boxes every frame instead of the original's every-fifth-frame round-robin. --script-trace (ScriptSystem::traceCalls(), Running Coney) logs every binding call and every call into the scripts by name, Coney's own tool.
  • Trigger spheres (src/world_objects/trigger_spheres.h, TriggerSphereCfg in src/scripting/world_bindings.h): a pool of 100, one per object, each checked every fifth frame (index modulo 5) with the boxes' rules, 3, 5 once per the sphere's 1000 ms and 4 going to the object's handler; gameplay finds the object among the scripts' humans, the flags and the spawn records. Coney's choices: the sphere's own object is never its occupant; the clear-line test of modes 1 and 2 is a collision-mesh ray from the centre (mode 2: raised 1 m) to 1 m above the human's feet; TriggerSphereCfg's interval is kept as the handler's period but not used. TriggerSphereEnable arms a sphere (making one with radius 0, mode 1 and a 1000 ms period when the object has none) or disarms it, forgetting who is inside without message 4; Coney has no handler components, so it acts on any object.
  • Play (src/gamemodes/gameplay_mode.h): the scripts keep running in a level, stepped every frame after the level's step; --play-level LEVEL enters a level the same way, after the preloads and a fresh state (LevelScripts, src/gamemodes/level_start.h). In level99 at checkpoint 1 the tutorial runs from the intro through the two markers (vMark01, vMark03; how the original draws them: Objective markers), the l99_t1 scene and P1.SetupBasicAttacks; the basic-attacks lesson then waits for the tutorial callback (HUDSetTutorialCallback, HUD), which the play mode calls with each hit player 1 strikes. The lessons run through the light and heavy attacks, the grab and its strikes, the mount and its strikes (Combat), the L2 let-go, the targeting lesson (L1 held 2 s, heard through the PadSetHandlerEx handler, which the play mode calls after the level's step with each human's pad command, PadSetHandlerEx), the combos and the strong attack, then stop at the strong grapple (circle + cross), whose research is under way.
  • The level table (src/warriors/level_table.h, GameState): CfgLevelName's records by index, read by GetLevelId and the level flow (record 0 is level100).
  • The front end runs the preloads at the legal screen and global.lua and level100.lua in the same state when the level flow starts the front end, then calls Menu.onStart (Front end).

Coney's choices, where the page is silent or Coney differs:

  • A call of an unset global (a binding Coney does not list; the original registers all 956) is skipped as a no-op returning nothing, and logged once by name, instead of stopping the script.
  • Scheduled calls due at the same time run in the order they were scheduled; a call scheduled during update waits for the next update, even with a delay of 0. There is no garbage collection to force (Coney's VM counts references), and the 32-character limit on the parts of a dotted name is not applied.
  • CfgLevelName's argument order: the scripts pass 18 arguments, the index, four names, the level number, then twelve numbers. Coney maps the index, the level name (+0x14), the second name (+0x24), the world name (+0x39), the fourth name (+0x49) and the level number (+0x04) by the disc's data (record 0 is level100 with number 100; the world name equals the level name in every record with packs), and keeps the twelve numbers in order (inferred).
  • random(a, b): whole numbers in [a, b] from the game's own table, read from the player's executable (World flags), or a deterministic stand-in without it; the math library's random uses another one of Coney's, never the C library's. The trigonometry works in degrees, as stock Lua 4.0 does.
  • tolua, M_Vector4 and M_Quat are empty tables and NilHandle and NilSoundHandle are 0, below the first handle a stub gives out.
  • preLoadFile queues its file: the caller's script goes on, and the file runs, then its callback by name, at the level start's preload (with any files those scripts ask for) or, during play, at the top of the next script frame, as the original runs both later, when the file arrives; level87's first chapter depends on that (Errors in a fresh state).
  • Without an AI host (a level script run on its own) GangCreate returns a new handle and the gang counts 0, and InvNumberOf returns 0 until inventories exist, so the hub's and level5's start functions run to their end.
  • A runtime error's message names the last call of a missing binding skipped before it, the likely cause (...; last skipped call HuTagPattern).
  • GetLUASaveDataFloat and SetLUASaveDataFloat outside slots 1 to 8 read 0 and write nothing (the original does not check).
  • Scene stand-in while no scene system is attached (gameplay and the front end attach one, Scenes): ScenePreload returns a new handle and calls its load function with it at the next script update; the three play bindings return true and call their end function (third argument) with the scene at the next update, so a scene ends at once. With a system, SceneAddObject resolves and pins the object's spawn record and binds it.
  • A message is delivered whatever the level-end state (W_GameState + 0x14c), which Coney does not keep yet; a message that asks for a result counts as taken when its call runs, but for message 0 (the interaction), which is taken when its callback returns anything but nil. SetMsgHandlerEx with message 0 keeps the object's prompt as its kind-1 context record (script::MessageHandlers::setPrompt); the second text is not kept and no prompt is shown.
  • ScheduleFuncArg1(name, n, ms) takes the number before the delay, as the original does; the hub's chat events (events.ChatEvent) run without errors since (coney_tests "[disc][story]" runs level95 for 20 seconds of script frames).

Disc check (NTSC-U, 2026-10-04, counts only): coney_tests "[disc][scripts]" with CONEY_DISC set runs the four preloads, global.lua, level100.lua, Menu.onStart and the menu callbacks (fadeToRMI, launchRMI, cancelRumbleMode, playMovie, startGame, onFinish) in one state: no script error and no call of a missing binding. The preloads add 25 globals and the level entry 193 more, as at run time (1,034 → 1,059 → 1,252); the level table holds 111 records; CfgObj is called 1,371 times; 9,273 configuration calls are recorded; 388 HUD strings are set. Menu.startGame asks for level99 (runNextMission(1)), and after the unload global.lua and level100.lua run again in the fresh state without the preloads, also without errors (a Coney observation for the front end, not a check of the original).

A sphere's radius (2026-10-06): TriggerSphereSetRadius sets an object's sphere radius, first making one with the defaults of 0x00414480, not armed, when it has none (TriggerSpheres::setRadius; Coney counts any object as having a handler component, as for TriggerSphereEnable).

Open questions

  • The first mission: what a restart (failure or pause menu) restores from SetCheckPoint's copies, and who reads the object manager's 35-word list; whether TriggerSphereCfg's interval reaches the sphere's period (+0x164); whether a sphere's own object can be its occupant, and what the clear-line tests 0x0024dee8 and 0x0024df40 test between; who writes the stopwatch's rate (+0x10); what commands 37, 38 and 40 (d-pad down, up, right), which the tutorial switches with EnableCommand, do (Commands).
  • What a level loaded after an unload (a fresh state without the preloads) does when it needs PHYS, MATERIAL or GSTRING: does the level flow run the preloads again, or do the level scripts not need them? (For the front end, global.lua and level100.lua run without errors in Coney's fresh state, and so do level99.lua with level99_combat.lua, level2.lua and level3.lua at the checkpoints tried.) The two errors Coney met are answered below (Errors in a fresh state): both are bindings Coney lacks, not missing preloads or globals.
  • The scene system (SuperRunScene). (Answered on Scenes.)
  • RegisterUpdate. (Answered: preLoadFile's completion routine 0x00356d00 runs the loaded chunk through slot +0x3c, then, when a callback name was given, finds it (slot +0x4c) and calls it with no arguments (slot +0x8c); confirmed (code).) Still open: whether the checkpoint scripts arrive during InitLevel's preload, which services the file manager, or in the first frames of play.
  • CfgLevelName's arguments: which of the twelve numbers after the level number fills which field (+0x08 sections, +0x0c, the three flags at +0x0d, +0x10, +0x6c-+0x80), and why the scripts pass 18 arguments where the writer (0x0041f118) is described with 17 values. The section count does not simply equal the number of <level>_<k>.pak files for any one argument (25 of 63 levels with packs match the seventh).
  • CfgObj count: the page counts 1,279 CfgObj calls in config_preload3.lua; running it calls the binding 1,371 times. Static call sites against calls made (loops, or functions called twice)?
  • Degrees or radians: does this build's math library keep Lua 4.0's degrees (Coney's assumption)?
  • random's generator (answered: a table of 1,024 numbers walked by an unseeded index, results in [a, b] inclusive, Bindings). Still open: whether Coney should reproduce the table (it is data in the executable, so a reimplementation would read it from the player's disc) or keep its own seeded generator.
  • NilHandle and NilSoundHandle: their values.
  • PadSetHandler's arguments: this page gives (pad, button, name), Front end gives (button, player, "function"). Coney implements neither yet: PadSetHandler is a stub that ignores its arguments, since the front-end path needs no Lua pad handler.
  • Names: the @orig tags call the script system's slots ScriptSystem::Update, EnterLevel, RunFile, RunFiles, FindFunction, Call, Schedule, ScheduleArg1, ScheduleArg2, FlushScheduled and SetUpdateFunction, the binding wrapper 0x0036eef8 ShowProfileManager_Binding, 0x0041f118 W_GameState_SetLevelRecord, 0x00160d78 MenuLoadLevel_Choose, 0x0015c7b0 LevelFlow_ChooseLevel and 0x0020a268 PM_Mode::HandleCommand, until the research database names them.
  • Binding arguments the script bindings reference marks as not understood yet: which bit of PadSetHandler's mask is which PS2 button; who sends messages 0, 6, 7 and 9-0x19 and what their extra values mean (the arguments are in Message handlers); the scene-play flags (ScenePlay and its relatives) beyond their global.lua names; most fields of the large Cfg* records (CfgChar, CfgPowerClass), which are written through computed addresses with no reader found yet (CfgWarriorClass's are all traced: Warrior classes). Each would move up from inferred once a reader or a runtime observation is found.
  • Hint texts set twice in one frame (user checklist item 33b). (Answered by the original's runtime trace, The queue at runtime and lessons 5-8, two runs: neither of the two places where Coney's level99 course run queued two texts in one step backs up in the original. Lesson 5's end: PlayerState's animation callback on the strong grapple's hold 82 runs at the connect (update 157, TT_19b shown) and P1.Power sets TT_24d when the hit is scored with 82 (update 191), 34 updates apart in both runs, so the two texts never share an update; confirmed (runtime), the dispatch of 82 from the four-clip mix's constructor (0x00106fe0, Animation callbacks). P1.StartSnaps: its HUDSetTutorialText calls (nil, TT_25c, nil, TT_25c) ran in one tick; each text found the box free and showed within its own call, so the box showed the snap text, cleared it and showed it again, with the cue twice, and nothing stayed queued; confirmed (runtime) at the show 0x001cdf7c, as HintBox_Queue reads (confirmed (code) at 0x001ce484-0x001ce52c). In both runs the queue held no entry past the call that queued it, apart from TT_26b behind a <DISPLAYTIME> hint. What Coney must change for it: Coney's differences.)
  • The stale throw hint (user checklist item 38). (Answered for Coney: the user saw level80's TT_25 (lure the cops with a bottle) stay through step 3.4 until the Wreck'em order. The likely cause was the hint box's backlog: a lesson text queued behind another, then shown after the next clear. The box no longer queues on a nil-then-text set (The queue at runtime), and the bug no longer reproduces on the user's path. repo:tests/platform/disc_level80_story_test.cpp starts the story and jumps to checkpoint 3. It walks into Fox's box and, on foot, up the stairs to the roof, then plays the lesson. The box is empty when the bottle breaks, TT_7 is the next hint and no lesson text comes back. The original was not traced for this; the order is the one in the order at runtime.)