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: vtable0x00545df8, pools on Tasks) keeps the handles of up to 60 occupants (+0x70, cleared toNilHandleby0x004151c0) 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+0x69whether 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 flag0x100000000; a human taken out byHuDelete(flag0x200000000) 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 by0x004133d0; update0x004134f8): 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 at0x0051489c + 0x228and+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 themvbStartRail, which hands the player back his pad inlevel3'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 whileW_GameState + 0x14cis 0 (delivery). - Inside (
0x00412a18): within the bounding sphere, between the box's lowest and highestz(+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 (handles0x26c-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 objectNilHandlewhen 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 andlevel3'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
objectis, 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,+0x10enon-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 orBreakGlassInRadiussends nothing. - A thrown object (
WorldObject_OnContact,0x00394050, callingWorldObject_OnImpact,0x003939a8, at0x003945a8/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+0x10dor+0x10eat 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_ReportNoiseis a separate call). Confirmed (code). level80checkpoint 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 insidevBottleBox(corner −264.7, 274.7, 1.1; size 8.2 × 6.5 × 3.5, aroundfBottleTarget−259.6, 278, 1.3) and otherwise sends the two cops searching again. Inferred from the positions (corner read as the minimum corner).
- Only kind-0 boxes get it: the box's kind (slot
- A trigger sphere (
TriggerSphereCfg,0x00414bc0) is a 0x184-byte record from a pool of 100 (0x006f3f50, slots0x006fd6e0) attached to an object's handler component:+0x170the object,+0x174the radius,+0x17cthe mode,+0x180armed,+0x164its message-5 period (1000 from0x00414480).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 (soSetMsgHandler(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 of0x00414480: 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¶
- Start-up.
Game_InitializeSubsystemscreates 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 fourCfgfunctions, and replaces_ERRORMESSAGEand_ALERTwith functions that return without doing anything. A script error therefore stops that chunk (or that call) and leaves no trace. confirmed (code). - Legal screen (mode 5
Enter,0x00161218). Runs two lists through slot+0x44:0x00512b08=enum_preload.lua, then0x00512b10=config_preload.lua,config_preload2.lua,config_preload3.lua. The calls of slot+0xdcaround them do nothing. confirmed (code). enum_preload.luadefinesMATERIALandSAand calls nothing.config_preload.luasets up sound: material and animation sound slots, music info (bindings only).config_preload2.luadefinesPHYS,ANIM,OBJECT,AXIS,PHYFLAG,Platform = GetPlatform(),LanguageExt, then runsdoFile("config_strings_" .. LanguageExt[GetLanguage()])and configures characters, hats, gangs, weapons, HUD messages and sounds throughCfg*bindings.config_preload3.luadefines and runsCfgObjectsAtoC...CfgObjectsTtoZ(1,279CfgObjcalls) andCfgLevels, which buildslevelNames(111 records) and callsCfgLevelNamefor 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).- A level loads (
InitLevel, also for the front-end levellevel100). The level flow calls slot+0x24with the level's name: it runsglobal.lua, then<name>.lua("%s.lua"). The PC-style branch (../lua/global.lua,../levels/%s/%s.lua) is dead in this build because0x001458c0returns 1. confirmed (code). Runtime check: 1,252 globals at the front end, including the preloads' (Platform= 1,LanguageExt,PHYFLAG...),global.lua's andlevel100.lua's (Menu,WonderWheelAnim). confirmed (runtime). - 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 needsPHYSorGSTRINGdoes 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)storestunder a new index and, unlessnoAdd, runsObjectiveAdd: the text (String) on objective line 0, or line 1 whenBonusis true (HUDSetObjective(line, String, 0, Silent), then states 1 and 0 again); defaultRadius2 m,RemoveTime4000 ms,GTDistance15 m. When there is aFlagor aVolume: without aVolume, a trigger sphere on the flag (TriggerSphereCfg(Flag, true, Radius, true, 100)) and the handlerObjectiveEnterfor messages 3 and 5 on the flag; with aVolume, the same handler on the volume box (no sphere). Then the flag becomes a radar mission objective (texture 27, or 28 whenbMission) and, unlessNoObject, a marker object (dyn_w_goto, ordyn_w_missionwithbMission) is spawned at the flag and shown.HUDTypeadds a counter panel.ObjectiveEnter(box, human)(messages 3 and 5) finds the objective whoseVolumeis the sender, else whoseFlagis (ObjectiveGetIndex, function 146: the message's first argument is compared with the Lua value ofVolume/Flag, so the box or flag handle the engine passes must be the one the script holds). It goes on only whenhumanis a player (HuIsAPlayer), or, withCheckWarriors, 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 toObjectiveExit, remembers the human (GTGHuman) and runsObjectiveGoodToGo(below); - with
bShowHide: hides the marker and sets message 4 toObjectiveExitonly; - otherwise:
CallBack(index)(CallBack(index, human)withPlayer2Callback), thenObjectiveCompleteunlessNoComplete. So an objective withoutGoodToGocompletes on the first player who enters, alone.
- with
ObjectiveGoodToGo(box)(function 145), the "wait for the crew" test, every 500 ms (ScheduleFuncArg1) while the player stays: it passes whenGTGHumanis withinRadius(2 m by default) of theFlag(TestDistance) and every human in the Lua tableWarriorsis withinGTDistance(15 m) ofGTGHuman(GetDistanceTweenHumans), not arrested and alive; withGTGUseObjectinstead, every Warrior must be inside the box. WithPoliceWanted, a wanted Warriors gang (GangIsWanted(GangWarriors, true)) fails it too. On a pass: the hint is flushed if it was shown,CallBackruns andObjectiveComplete. On a fail: the hintGSTRING.GTGPOLICE(wanted) or, once,GSTRING.GOODTOGOat priority 1 (HUDSetTutorialText(text, 1)); withNoCompletethe callback getsfalseonce (andtrueonce on a later pass, without completing); then it checks again 500 ms later. A dead or arrested Warrior still inWarriorsblocks it.ObjectiveExit(box, human)(message 4, only afterGoodToGoorbShowHide): 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 theGOODTOGOhint if shown, and sets messages 3 and 5 back toObjectiveEnter.ObjectiveComplete(index): flushes the hint (HUDFlushTutorialText(1)), marks the text done (HUDSetObjective(0, text, 2, true); state 1 withOBJRemove; 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):
TagInfo.NoTag()andNoScenes()(helpers fromglobal.luathat remove the tag and scene helpers for this level).CfgSetDatabaseSizes(48, 34, {2, 1, 2, 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 (ChangeCamisCameraMakeActivethenCameraReset). It sees nothing: the wheel is about 550 m away.Menu = {},Menu.movies = {[1] = "TRAILER", [2] = "L1_IN"},Menu.movieID = 0.- Two sprite batches,
L100_RM_PTank1 = GetPTank(786437, {0, 0, 0}, 1, 9000, 256)andL100_RM_PTank2 = GetPTank(1835027, ...): sprite words0x000c0005and0x001c0013, sheet-table records 12 and 28, the Rumble menu's sheets (Front end);level95_clubhouse.luamakes the same two before its menus. - The
Menufunctions (below),AddObjects, and the tableWonderWheelAnim(sceneName = "WonderWheel_100", callbackspreload = "WonderWheelAnim:startScene"andfinished = "WonderWheelAnim:finishedScene", the latter never defined nor passed) withenable,startandstartScene. if not Objects then AddObjects() end: 29ObjSpawncalls, all with the rotation{0, 0, 0.707107, -0.707107}(−90° about z):dyn_s_neon_a-danddyn_s_wwheel_a(tint0x474542FF) at (515.51, −68.88, −188.65), and the cartsdyn_s_wwcart_simple_a,_b,_cwith_01-_07copies (tint0x888888FF) 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:
- 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). - The last level is the global
LastLevel(set to 0 byglobal.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 whichUM_IsLevelCompleteis true, else 0. - If the table has an entry:
SetCheckPoint(checkpoint),SoundStopMusicTrack(),MenuLoadLevel("level" .. level), and whenargis 1,HUDLaunchMissionComplete(4). Otherwise it callsHUDLaunchMisssionComplete(3)(threes), 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) loadsscene_list.cnk, and0x00353460copies its Scene List chunk (type0x43): a u32 count, then 24-byte records{u32 id, u32 size, char name[16]}, to0x006eba10, the count to0x00512aec. Confirmed (code). On the NTSC-U disc there are 2,765 records, each id its own index, one per.scnrecord: 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 id0x00353698finds: the first record whose name containsname(0x00435d30is a case-sensitivestrstr), or 0 when none does; then0x00353af0loads 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 calls0x00353698itself (the AI,0x002cb5d0), so not every scene is preloaded by a script.- Movies.
PlayMovie(name, flag)→Movie_Play(0x0042a938) opensPSS\<name>.BIK(format0x0058bfe0), confirmed (code). Who names them: the boot (LOGO,PLOGO,L1_IN), a level record's introL%d_IN(0x00550160) and outroL%d_OUT(0x0054ed20) (Level loading), and the front end'sMenu.movies(TRAILER,L1_IN). The disc'sPSSfolder 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 tabletMissionof three checkpoints, each{create the Warriors, start function, script}: 1 ={AddWarriors2, "Checkpoint1", "level99_combat"}, 2 ={AddWarriors, "Checkpoint2", "level99_lesson1"}, 3 ={AddWarriors1, "Checkpoint3", "level99_lesson2"}; thenRunLevel().RunLevel, in order:CfgSetStatValue(0, 0, 1000);F.PreloadAnims()(sixSetDynamicAnimationcalls naming.anmclips);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 forplayer2;CfgPlayerMugging(false); extra set-up for checkpoints 2 and 3;EnableCommand(player, 38, 0)andEnableCommand(player, 37, 0)(two player commands off for the tutorial).AddWarriors2(checkpoint 1):GangCreate(0, "Warriors2", 0, 0), thenHuCreate("Rembrandt", 32, {-284.4, 120.4, 0.3}, 0, "warr_sw", 1, gang)(player 1) andHuCreate("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), thenAddCameras = nil.StartAmbient(run byInitLevelonce the level is ready): at checkpoint 1,SuperRunScene(IntroScene)(the in-engine intro, defined inlevel99_combat.lua); at checkpoint 3, the ambient loopvags/ambient/city/distant_traffic_loop.Checkpoint1(level99_combat.lua) callsP1.SetupCombat: the tutorial's sections (P1.SetupCam,SetupBasicAttacks, ...),HUDSetObjective,HUDTurnOffRadarand 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)):
P3.UseFlash(the player uses the flash on the dealer) sets the last objective and schedulesP3.MissionOver, which runsSuperRunScenewith a scene table whoseFinalis true andReturnFuncisP3.MissionCompleted.- When that cinematic ends, its end callback,
global.lua'sPreCashTheWorld(sceneId), seesFinal: it queues screen effect 1, calls theReturnFunc(which restores the default follow slots and re-enables command 1), clears the scene's table entry and callsHUDLaunchMissionComplete()with no argument. (WithoutFinalit would show the scene'sHudText, call theReturnFunc, and switch spotting, the stage lighting and the fade back.) How the scene system plays and ends the cinematic: Scenes and movies. MissionComplete_Launch(0)(0x0015d420) finds mode 1 on top, pushes mode 0xb and sets the level-change flag0x0050c754= 1 (read by mode 1'sExit, Leaving gameplay). Mode 1 is suspended, so its level-end countdown does not run;W_GameState + 0x14cstays 0 untilMenuLoadLevel.- Mode 0xb's
Enterstills every live human and callsUnlockAndLoad:MissionCompleteUnlocks()unlocks the level (UM_Unlock(Level, 0, 0)) and one more record chosen from the players' scores (StatGetScore);LiquidizeAssetsadjusts each player's inventory (money, revives, spray-can charges, keys, item 5) and resets their stats (StatResetPlayer); andrunNextMission(1)finds 99 complete and loadslevel80at checkpoint 1 (SetCheckPoint(1),SoundStopMusicTrack(),MenuLoadLevel("level80"), which sets+0x14c= 3 and mode 8's next level), thenHUDLaunchMissionComplete(4), which only stores kind 4 since mode 0xb is on top. - 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: itsExitunloadslevel99), banks both players' money and asks for the autosave (0x00155308, mode 6). Mode 8 then loadslevel80(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 events0x30/0x31) and stops its spray sound (event0x45), Combat. The scenel99_c7that follows binds the victim,CombatEnemy.GenWarrior(Rudy), to role 6rudy; 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 state0x1000, which the dispatcher sends there). What armsP1.RageMoves(the step order):P1.WeaponsDoneclears the tutorial callback and starts the section 2000 ms later.P1.SetupRageModemakes rage gains possible again (HuSetPreventRage(false)). It sets the meter to 0.5 and locks it, and it registers the rage handlers: enterP1.EnterRageMode, exitP1.ExitRageMode, fullP1.CheckRage, 20000 and 5000 ms. It arms no tutorial callback.- 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. - When it is full,
Human_AddRagecalls 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. - 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 schedulesP1.EnableMoveCheckfor 3500 ms later. P1.EnableMoveCheckis what callsHUDSetTutorialCallback("P1.RageMoves").- In
P1.RageMoves, step 1 (645 / 647) clears the callback, schedulesP1.EnableMoveCheck3500 ms later again, and moves to step 2. Step 2 (65 / 233) callsP1.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_19candTT_19eare 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 showedTT_19eand 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 theTT_19*texts between them.- The state hints (
SetupStateCallbacks,UpdateStateCallbacks,PlayerStateandStateSwitchinlevel99.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_25eandTT_25kare 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):
- 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
PlayerStateonce per start. A unit test: grab (82 fires), mount (210 fires), get off (an idle fires), in that order, with nothing stale. SetupStateCallbacksandUpdateStateCallbacksshow the text for the currentSTATEat once (the state kept from the earlier lessons, not reset to idle).- The lesson-6 callback counts 57 then 59 only; Coney's standing cross + square must not reach it as 57.
- Playthrough test (level99 cp1): in lesson 6, standing shows
TT_19a; front hold showsTT_24d; after 57, back in the hold or holding,TT_24e; mountedTT_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
HUDSetTutorialTextcall 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 isTT_26b(lesson 3), queued behindTT_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. PlayerStateand the lesson callbacks never share an update in lesson 5: the grab's anim callback runs at the connect, 34 updates before the hitP1.Powercounts (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; therePlayerStateruns 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_24dafter 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 setTT_19a(grab again) instead, where the 2026-10-07 run above showedTT_24e. The state hints explain it: the follow-on idle 358 had already setSTATEto idle when step 1 ran, and the idle text isTT_19a; a player still in the hold getsTT_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.JumpPoizoDonearmsvReachWindow(corner 43.2, −4.3, 4.1, size 4 × 6 × 4, just past the panes at x 47.4) withP3.ReachWindow, which counts only players. With one player, his entering removes the handler and runsP3.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 runsP3.Player2Jumps(player 2 arriving first setsbWindowWait; if he then drops out, player 1 still has to enter).P3.Player2Jumpsmarks the objective done, flushes the Warriors' brains and sets the gang dead (the player isWarriors.Rembrandt, so he too gives up the pad), gives each playerGoalAddressPersontowards Vermin, turns the follow camera off with Vermin as its secondary target, destroys the markerdyn_w_missionand sends Vermin tofStop[3](P3.VerminInWindowon 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 runsP3.Player2Jumpsitself. - 3.5: a barrier's message 2 is its removal, not its hit: the class update (
0x003b3220, every 60 ticks) reports a broken barrier andWorldObject_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 (
AddWarriors2for 1,AddWarriorsfor 2,AddWarriors1for 3) makes Rembrandt withHuCreate(..., player 1, ...)and Ash withHuCreate(..., 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 flagsfAshTeleport1/2are 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+0x1b1and Ash is an ordinary AI human, soHuIsAPlayer(Ash)(player slot+0x1b0not −1) is false until a second player joins. Confirmed (code) at0x00233d60,0x00235758.- Checkpoint 2's set-up (
level99_lesson1.lua, run afterRunLevelnamesplayer2 = Warriors.Ash): whenHuIsAPlayer(player2)is false it removes his radar blip (HUDDeleteRadarObject) and suspends his brain (BrSuspend(player2, true)); either way it registersF.Player2JoinwithSetPlayer2Callback(aglobal.luahelper the multiplayer callbackInventoryPlayer2calls on). The scenel99_c2binds Ash as its fourth human only whenHuIsAPlayer(player2), elseNilHandle. 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), callsHuman_MakePlayerand then the multiplayer callback with (Ash, true) (GameState_CallMultiplayerCallback,0x0041b148). Confirmed (code). A second pad joins on START: the level mode's update (0x00158728) callsGameState_SyncPlayers(pad)for a pad with START held. - Leaving. A pulled pad does not release player 2: the pad callback
Pads_Initinstalls (Player_UnbindPad,0x0022a980, passed at0x00149630) 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 callsGameState_SyncPlayers(-1), whose release branch runs only with that flag clear and callsHuman_ReleasePlayerand 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 tofAshTeleport2(z −8.5).
Coney fixes (item 41):
HuIsAPlayermust 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) isplayerIndex > 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.- Keep Ash where
HuCreateputs 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. - A player 2 is made only by a second pad joining (with
CfgMultiplayerJoinallowing it), then the callback runs with (Ash, true); never by the level set-up. - Test: the one-player Story path into checkpoint 2 (from the
l99_c1wheel scene's hand-over, and a checkpoint-2 start) leaves Ash at z −8.5 with his brain suspended, off the radar and out ofl99_c2; with a second pad joined,F.Player2Joinputs 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 thatMoveAction_CollectPoints(0x002fbd18) gathers, and that stops at the first link with bit0x4or0x80(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 (vtable0x00542130). 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 atfStop[3](41.46, 2.63) about 220 updates in. - Second leg.
GoalMoveToFlagagain 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 atfStop[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):
- 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. - Leave the jump out of the corner-speed look-ahead: collect corner points only up to the first link with bit
0x4or0x80, so the turn after the landing does not brake the run-up. - 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 thefStop[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):
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, andpoint_leftwould point north, away from the fences).- After
P3.BreakFence, Vermin walks tofStop[6]and turns to 139° on arrival (faceFlag). - 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):
- 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. - 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.
P3.DealerPoizoruns 0.5 s (14 updates) afterP3.FenceBrokenand cuts to the dealer camera without a fade; the follow camera stays current, with no letterbox, until then.- 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.DealerPoizo14 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. - Test (level99 checkpoint 3.5, the wooden fence): with the hit at update H, the fence is removed and
P3.FenceBrokenruns at an update R with H ≤ R ≤ H + 30; the dealer camera is current and player 1 atfHitDealerat R + 14 or R + 15, so the cut lands within H + 14 … H + 45; between H and the cut noScreenQueueEffectis called and the fade and letterbox levels stay 0. - 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.DealerHitand fades out. In the original the charge warns no one (confirmed (runtime), PCSX2-MCP 2.9.129, slot 10 with thecharge_fenceinput and a hook onHuman_WarnOfAttack: 0 calls over the charge, fade and letterbox 0, the hit at update 29,P3.FenceBrokenat 45 andP3.DealerPoizo14 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): noP3.DealerHit, no screen effect and no fade before the break, onmainas 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-4level80_chapter1to_chapter4) and the Warriors set-up per checkpoint (PlayerGang), and runsMain(unless the debugSCENETESTis 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")), andPreloadSubway2 s later: the passing-train sceneConey_Sub3/Coney_Sub4replays throughSuperRunSceneevery 30 s for the whole level (a scene event a test should ignore). NextMissionruns the chapter'sCleanup, adds 1 toCHAPTER,SetCheckPoint(CHAPTER)and loads the next chapter in place, as inlevel3.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 oneWCSetCallbackand wait for that command from player 1 (R2 menu, HUD). - No scripted failure: no chunk calls
HUDLaunchMissionFailed; the mission ends with the final scene'sFinal = 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
TANDEMdepend on timing:TANDEMshows 5 s after the attack order,TT_9on the first Destroyer hit on Cleon, so their order can swap. TT_26-TT_29follow the thrower's state every 200 ms and repeat; merge consecutive repeats and allow them to go back (a dropped bottle showsTT_26again).- Entered at checkpoint 3 or 4 (
LoadState), the chapter'sSetupalso 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'sSetupby a scheduled call, into a global (CochiseSwanScene, ...). That number is theSceneIdSuperRunScenelater stores astblScene[SceneId](global.luafunction 125) because the table saysPreloaded; a nil id stops the script there ("table index is nil"). SoScenePreloadmust return the scene's id with or without a callback name (the original returned 1744 forl80_c7), and the preload must have run before the volume is entered. Confirmed (runtime) for the order:Setupat 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: gangCochiseSwan(kind 0) with Swan (type 6) and Cochise (type 16).AddDestroyers1andAddDestroyers2: gangsDestroyers1andDestroyers2(kind 19), six soldiers each (types 192, 194, 197, 199).HuCreateputs 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 boxvTurfDestroyers(as isCochiseSwan), barred from picking up weapons (BrSetThugWantsWeaponfalse) and put in god mode with Cochise and Swan. No gang gets a tactic and none aGangSetThreat: the threat response stays 2. - The player enters
vCochiseSwanTrigger: scenel80_c7with all twenty-one humans, each inJoinCinematic.
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:
Gang_MakeEnemies(0x0016acf0) sets the enemy bit both ways throughGang_SetEnemyBit(0x001690c8), whose mask writers (Gang_WriteEnemyMask0x00168f08,Gang_WriteFriendMask0x00168f30) stamp the gang's change time+0x34. Each member'sBrain_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).- Each Destroyer lists Cochise and Swan (in range and in sight); Cochise and Swan list the twelve.
- With no tactic, a threat response and a non-empty list,
GangBrain_Thinkstep 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 ofvTurfDestroyerstakes 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+0x164cleared). 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_IssueWarriorCommand0x003747b8→GameState_IssueWarriorCommand0x0041c2d0, 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 callsWarriorAttackTactic_GiveGoals(0x00320618): every living non-player member but the chief, popped to his goal base, getsFollowAndAttackon the chief. Cochise and Swan are members by then, so they get it too. Confirmed (code);FollowAndAttack(vtable0x005405d0) on both at update 0 or 1 in all three runs.FollowAndAttackGoal_Process(0x002bc198) fights the best enemy (Brain_PickBestEnemy, flags0xfffffbf9) 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,
GangAddMemberempties the moved human's list, and the empty-list FollowFormation.
GoalFight(Destroyer, Cleon): Melee does not keep the target; the score decides.
GoalFightisBrain_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 withBrain_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), unlessBrain_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 aGoalFightsoldier 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.GoalFightreleased 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
GangMakeEnemiesstamps 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
+0x12cread 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.Destroyer3andDestroyers2.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 fromGoalFight(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.Destroyer4at update 64, Snow onDestroyers2.Destroyer6at 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
FollowAndAttackand 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
FollowAndAttackpicks withBrain_PickBestEnemy(flags0xfffffbf9) 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) at0x0029f230; 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_OnEvent0x00304fa8; 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
HuCreaterows and scene, then the twenty-oneTeleportToFlagcalls 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:
- On
main, only the members of a Scout tactic scan (ScoutTactic::update);Brains::updatenever does, soGangMakeEnemiesputs nobody in a list (Attacking on sight, Coney status). PR #394 adds the scan to every brain's update and so fills the lists. - Neither
mainnor PR #394 hasGangBrain_Thinkstep 3 or the Warrior think's step 1 (Brain::thinkkeeps only the tackle meter and the swap prompt), so a listed enemy is fought only when something else callsfight: a hit, an event, a tactic or a script'sGoalFight. - 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 twoGoalFightDestroyers take Cleon depends on the slots (After the order).
- On
What Coney must do at this fight's start:
- Run the enemy scan from every AI brain's update, five brains an update, on the gang-changed trigger (gang
+0x34against the brain's last scan) as well as the interval. PR #394 adds the scan; check there thatGangMakeEnemiesandGangMakeFriendsstamp the gang's change time, so the scan runs on the next update rather than up to 2 s later. - 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. - Keep the placement: the flags are the original's. Do not scatter or delay the twelve.
- 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. - A test (
[disc][story], on the player's path): level80 from checkpoint 4, walked intovCochiseSwanTrigger, 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 haveFollowAndAttackwithin 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. - After the order: the forced
IssueWarriorCommand(1, true)rebuilds the attack tactic although command 1 is current, and gives Cochise and SwanFollowAndAttack;GangAddMemberempties a moved human's enemy list; the enemy scan applies the 110° field of view beyond the near radius (After the order). - 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_SetTargetleaves 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_29follow the thrower's state (a test that drops the bottle seesTT_26again). - 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's0x14: 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 sameItem_TakeOwnedand 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'sAddSnowCowboymakes Snow (type 34) and Cowboy (19) by the gate,AddSnowCowboyCopsfour cops (Cop1type 253,Cop2254,Cop4253,Cop3256) in their own gang. Chapter 2'sSetupmakes them all god-mode, gives Snow and Cowboy auto-escape, and spawns two scene props,dyn_cowboyanddyn_snow. A player invSnowCowboyTriggersets the cops on Snow (GangMakeEnemies,GangEngageEnemy), so a fight is going on when the objective is reached. - Before the scene (
SnowCowboySceneTriggered, thenRunSnowCowboyScene1 s later): the gate opens, the Warriors are suspended for the fade (DeactivateWarriors, undone before the scene),DestroyCopWeaponsdestroys whatever each cop holds (HuGetHeldObjectthenObjDestroy, so their own weapons never reach the scene), and the six scene humans' shadows go off (HuShadow(h, false)). - The binding.
SuperRunScenegets ten humans, bound to roles 0-9 in order byGoalJoinCinematic(global.lua'sgPlayCutScene): Cleon, Rembrandt, Vermin, Ajax, Cowboy, Snow, Cop1, Cop2, Cop3, Cop4, against the rolescl,re,ve,aj,warrcb,warrsn,cops01-04; and ten objects bound to object slots 0-9 bySceneAddObject:dyn_cowboy,dyn_snow,dyn_baton,dyn_baton01-03,dyn_cuffs_c,dyn_cuffs_c01-03(the header's slot names arecowboy,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 itsObjectstable). - The props go into hands by clip events. When the scene starts each bound human's pick target (
+0x33c,Human_SetObjectTarget0x00227080) 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_GetEventValue0x00101ce0, from the scene start0x0039d870); confirmed (code). The event then fires in the clip and the human takes that object into bone 25 (message0x89, take0x1b, no grip slide in a scene, Objects in a hand). Inl80_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):
- 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'ssetFreeTest,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 byGoalJoinCinematic, or a pair Coney's fight never ends (Snow's auto-escape) are Coney bugs. - Before the scene,
DestroyCopWeaponsmust remove what each cop holds (HuGetHeldObjectreturning the held weapon). - Act on role-clip events 9,
0x36and 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. GangDeletedestroys each member's held object with it, soRemoveGang(GangSnowCowboyCops)takes the three held batons;ObjDestroyof a held cuff lets go of it first.- Test (level80 checkpoint 2, the Story path): during
l80_c5all 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 mostdyn_baton01lies 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_c1has no particle events (its tracks carry only lens, caption, fade, intro-card and type-31 events; disc). Its three type-31 events callEndIntroScenewith 3, 2 and 1, and each callObjDestroys one club pane and runsC1.ExplodeAtFlagatfExplosion1/2/3(about (−181 to −184, 85 to 87, 5.8 to 6.3)):ObjSpawn("dyn_molotv")there andBreakObjectsInRadius(molotov, 0.5), so the bottle breaks itself into onesub_explode0.7 m up, whose second stage makes apart_explosion(the Molotov). The level script makes no fire of its own (the loopingFireContinuedre-explosion every 9 s is in theSCENETESTdebug 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 asub_flamespuff behind it every update; on landing on a surface that is not a wall it makes asub_burnscorch (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 asub_fade_flameon it (60-89 frames at 2-3 ticks, then a fade: about 4-6 s), the first ember's flame with the loopingvags/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
level80checkpoint 1 as in the runtime order). The hooksember-init,ember-update,burn-init,burn-update,fade-flame-initandfade-flame-updateofrepo:research/traces/patches/logged the three explosions about 650 updates intol80_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_flameper 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-initanddebris-flame): each explosion's twenty largesub_debrisare made burning (message0x27, kind 2, Explosions), and one that lands on a floor (its contact normal's z above 0.9) makes asub_fade_flamewhere 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.
- Embers: 2-5 updates (3-9 ticks), never their 25-40. Each starts 0.7 m above its flag (z 6.8 at
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):
- 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_flamespuff 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. - On an ember's landing make the
sub_burn(with its no-go sphere) and thesub_fade_flameon it as above, the first ember's flame with its loop sound; Coney's embers end on landing with nothing behind. - Make the explosion's twenty large debris pieces burning (kind 2) and give each that lands on a floor a
sub_fade_flamewhere it lies (sub_debris): about 15-18 more small fires per explosion, scattered a few metres round it, 20-60 updates after it. - 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(preLoadFileof the next chapter with"Setup") and the globalCleanup; that order only works whenpreLoadFileruns 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
Handlersgives every spot of its list the tag prompt (SetMsgHandlerEx(spot, 0, "SpotGettingTagged", ...)), adyn_w_missionmarker, a radar objective (texture 28) and a sphere that hides the marker while a player stands in it (GlowGoAway,GlowComeBack).SpotGettingTaggedasks for paint (InvGetSpraycanCharges): with none, speech command 37 only; otherwise the prompt is taken off the spot, the player made unpushable, event 14 handed toTagFinished, andHuTagcalled.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, atagNfile). - 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, andCheckCash(every 60 s) and the reminders nag (hintsCASH,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, inlevel87only. - Hint 22 is queued once (game state flag
0x10000000) byRadar_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, and0x006092b8at 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,SPRAYATTACKonceEConey_Sub5has loaded (update 354; the reference's 481 afterSetCheckPoint(3)include the original's load). The boozer's sphere calls the missingch3.BoozerChallengeat 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) andRUNATTACK18 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 (kind0x10; for the roof at (442.6, 112.5, 6.5), the bar doordyn_door_bar_baninumber 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 bych4.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):
- 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
HuTagand Coney logged that the spray was cut short on the next update; no event 14 followed, soTagFinishednever put the spot's prompt back, and 30 later presses there did nothing. The original'sTag_UpdatePlayer(0x00273c60) callsTag_Endwhenever the action is no longer0x17or0x18and the spray flag0x2000000is not set while the tag still exists, andTag_End(0x0022e848) always gives the tagger event0xewith 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. - 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.
HuTagstarted 334ANIM_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) callsMiniGame_Update, which for a tagging player runsTag_UpdatePlayer, and returns at once when that answers 1, which it does from the first update of action0x17(when it callsTag_StartSprayClips) to the spray's end;Player_UpdateActions(with the stance,Player_UpdateSprint0x0027ce90) andHuman_ChooseAnimStatecome after that return. The movement function moves nothing either:Tag_StartSprayClips(0x00278018) sets held flag0x400, a busy bit (the locomotion gate), soHuman_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 holds0x400. 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. - Spawned humans carry no money: withdrawn (2026-10-13, issue #354). The original rolls the starting
money in
Human_Initfor every human:Spawner_MakeHuman(0x0016d860) calls it asHuman_Createdoes. Confirmed (code). The first reading was that Coney rolls it only inHuCreate, but Coney's spawners make their human through the scripts' ownHuCreate(ScriptSpawnerWorld::spawn), so a spawner's human rolls its money and object as a scripted one does, in the same order of draws; the testrepo:tests/ai/spawner_money_test.cppdrives a spawner over the scripted brains and checks a class of $5-$20 gives $5-$20 and that the draws equalcharacters::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. - A downed human drops no money. The original's
Human_OnHealthOutcallsHuman_DropCarried(0x00232c60) when a human is knocked out or wounded, which leaves adyn_moneypickup 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. - 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. - 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.
- 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). - Attacking on sight: the Destroyers of chapters 2-4 do not react to the player until hit (AI: attacking on sight, issue #73).
- 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).
- The spray sellers after a hit (GoalDealer, items 7 and 15): chapter 2's first seller is the one the hints point at.
- 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 callbackStartGame, then the checkpoint table: checkpoints 1, 2 and 3 all runlevel34_riot(P1.SetupRiot), each with its own Warriors set-up at a different spot of the street; 4level34_gate/P2.SetupGate; 5level34_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;preLoadFileof the chapter; the pedestrian flag net (SetupFlagNet, 34 nodes, as inlevel2); the follow camera; car colours from a 21-colour table;CarPlaceInTrunkOnDetachon 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) withMaxFogParticles(10)and a screen effect; at 4 alsoForceCrimeLevel(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.CheckRiotStatuson a reload; partial progress is not saved):- Riot meter (a HUD bar,
HUDGetNewPH(3, ...), its text in per cent): 200 points.vLevelVandalhears message 6 (a human standing in it damaged something; only a Warrior counts): a newsstand gives 3, another world object (GetRTTI8) 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 11dyn_carstereo), from the three cars given a radio byCarSpawnRadio. - Bonus: 10 pedestrians mugged (
HuSetMugCallbackon both players, successes only) on line 1 with a counter; the tenth unlocksUM_Unlock(34, 1, 2). The count is saved (float 5) at every checkpoint. - All three done (
F.CheckObjectives) runsP1.HeadToGate.
- Riot meter (a HUD bar,
- No scripted failure: none of the four chunks calls
HUDLaunchMissionFailedand 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 gangRiotCivs1(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 callbackF.RiotPedSpawn(GangAddSpawner).P1.SetupRiotadds it at checkpoints 1-3 with the gang's hit handlerF.RiotPedHit. At checkpoint 1 it suspends the gang (GangSuspend(gang, true)) until the intro scenel34_c1ends;P1.EndIntroScenethen 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 riotersRiotRoguesandRiotMoonrunners(value 30, while a player is in section 2) and the wanted gangsCopRogues,CopMoonrunnersandCopPanzers, and the policeCops(value 60, from the cop scenel34_c3on and again inlevel34_gate). The level policeRiotCops(50) start atP1.HeadToGateand inlevel34_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. WhilebRiotPedsis true, 75 % getGoalRiot(h, 10, 100, 2, 0, 0)(GoalRiot) and 25 %FlagNetTraverse(h, 1, 100)(the 34-node pedestrian net thatRunLevelsets up). While it is false, all walk the net. In both cases 3 in 10 get a random object oftCivObjectsin hand (HuPlaceItemInHand).bRiotPedsis set true whenlevel34.lualoads.P1.SetupSection1sets it false for the intro, andP1.StoreObjectivessets 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).
- 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.
- Check the crowd's tasks once it spawns where it should.
F.RiotPedSpawnmust receive (handle, gang id, spawner name).FlagNetTraversemust walk the 34-node net, andGoalRiotmust roam, decide and leave in the gang's turf (RiotCivs1has no turf box, so the turf gate always passes). - 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).
- Build
TacticVandalize(members wreck the zone's objects; code 1 once nothing is left) andTacticStealin place of the standing stand-in. - Let
GoalStationaryThrower's throws release their object (the thrown-object physics), so the roof throwers and bar staff hit. - Police: once 1 is in, check that
Copsappear from the cop scene on andRiotCopsfromP1.HeadToGateon. The routed crimes (dispatch states 4 and 9) are item 13's work. - A disc playthrough test from checkpoint 1: after
P1.EndIntroScene, noRiotCivs1human 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.
- The global spawn cap. Confirmed (code) at
0x00166708(Gang_CachePlayerInfo, called with 1 fromGangs_Update0x0016d170): 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+0x324under+0x326(SetCopSpawnMax); for every other gang it is+0x432under+0x434(SetSpawnMax, whichlevel34sets to 45). Each spawn adds 1 to+0x432(and to+0x324in state 6), andGangSpawner_OnHumanGone(0x001672a8) takes it off again (Spawners). Before issue #325 Coney did not readSetSpawnMax, so at checkpoint 3 the riot, wanted-gang and police spawners filled the human pool: in 1,500 updates of standing still,HuCreatewas asked for 240RiotCivs1humans 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); withSetSpawnMax(n)two gangs' spawners never have more than n spawned humans alive, a removed human frees its place, and the police gangs keep toSetCopSpawnMaxthe 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. - State 7's placement (its fix list; item 1 of
the riot's crowd). It is still a stand-in. At checkpoint 1, eight
RiotCivs1humans 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. - A suspended gang still spawns. Confirmed (code): neither
Gangs_UpdatenorGangSpawner_Updatereads gang+0xd4(GangSuspend), so the original'sRiotCivs1spawner 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).
- Plays: the intro, the stairs,
vEnterRiot, the fence (climbed with triangle) tovIntro, the scenel34_car_flip,P1.StoreObjectiveswith the loot objective, and the walk towardsvSection2. 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 byHuIsAPlayeror by gang, so that is harmless. - The looters' tactics (item 4 of the riot's crowd). In Coney,
TacticVandalizeon 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).
- Plays:
vCarPoizowarps the Warrior tofStereoWarp, Vermin says his line, andP1.DoneStereoPoizogives the radio objective and its marker. The hardware owner, the Hun store, the molotov thrower andF.BlowupCarall run. - 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 byP1.EnableSectionswhen section 2 is set up, lies south of the shut gateDblGt01(corner (−111.5, −279.4), size 96.5 × 55.2, soyup to −224.2) and cannot be reached yet;vSection3Gate, set only byP1.DoneStereoPoizo, is the strip in front of the gate: corner (−109.3, −224, −0.3), size 3.6 × 1.8 × 5, sox−109.3 to −105.7 andy−224 to −222.2, beside the gate's hinge at (−109.39, −223.97).P1.EnterSection3goes on only for a player, whilebObjCompletedandbSection3are false andsLocationis not 3; it clears the box's handler, starts the cop scene (P1.SetupSection3), setssLocation= 3 and callsSetCheckPoint(3). Confirmed (runtime), PCSX2 2.9.94, a state oflevel34checkpoint 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 (RadioStolenset), walking south alongx−107.5,bSection3andbCopSceneturned true betweeny−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, aty−221.5, outside the box. With the waypoint inside the box and a 0.3 m arrival radius, Coney calledSetCheckPoint(3)at (−107.6, −222.2), the same edge (2026-10-12, release build of68f4e6d3). Nothing to fix; the playthrough driver must walk into the 1.8 m strip. CarSpawnRadioruns twice on the same three cars at checkpoint 2: once fromP1.SetupTier2Encountersand once fromP1.EnterSection2. The original does the same, because the second call does not testradioSpawn. 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).
- This checkpoint depends on item 1: while the pool is full, nothing else can spawn.
- The three objectives play together (settled).
repo:tests/platform/disc_level34_objectives_test.cppstarts 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, andF.CheckObjectivesthen runsP1.HeadToGate, which callsSetCheckPoint(4). Confirmed (runtime, Coney).P1.HeadToGateis a Lua-to-Lua call, so a script trace shows it only through theSetCheckPoint(4)it makes.
Checkpoint 4 (to fFinalGate, then vNearFuries).
- The way to
fFinalGateis a fence climb, and the objective waits for the crew (settled). No gate opens on the way:DblGt00(opened byP2.SetupGate) is atx−111.9, andDblGt02(−144.94, −270.82), the gate besidefFinalGate, opens only inP2.AtGate, after the objective. PastDblGt00the street is a dead-end alley,y−251.5 to −260.75 atx−133, closed by a wall atx−135; its south side aty−260.75 is a chain-link fence, and the yard behind it holdsfFinalGate. 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_01and its two parts, about 2.4 s) and left him at (−131.5, −262.0), from where the flag is a walk;P2.AtGateran (ParkSceneset) 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 andDoorOpen(DblGt02)came at update 4,307, thenSetCheckPoint(5)atvNearFuriesat 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 untilDoorOpenof the second gate. - 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).
- Plays: the scene
l34_c4,P3.EndParkSceneand the fence Furies.P3.FuryTimeoutruns 5 s later (MoveFromFence, which kills the two Rogues), and Vermin says a line every 15 s, as the script says. - 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
SubwaySceneis set (P3.EndParkScene, which also schedulesP3.FuryTimeout5 s on throughP3.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 292ANIM_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 of68f4e6d3,--play-level level34 --checkpoint 5 --headless --trace, no input;P3.EndParkSceneat step 816): the first hit at update 91 after the scene, beforeP3.FuryTimeout(update 149) and before anyP3.MoveFromFenceorP3.EnterDiamondfrom a Warrior, so some human attacks before the script makes the Furies enemies of the Warriors (GangMakeEnemiesandTacticAttackare 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 beforeP3.FuryTimeout,P3.MoveFromFenceorP3.EnterDiamondruns, 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 adyn_batfroml34_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' code0x2a; Coney's Furies have no bats (Combat: a bat's hit). - The route to
vSubwayExit(settled).P3.GetToSubwaytakes message 3 from any Warrior onvSubwayExit(corner (−256.6, −203.2, −12.7), size 2 × 3.8 × 3.5, turned about its centre (−255.6, −201.3) byRotateVolumeBox(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 scenel34_c2started (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.GetToSubwayat update 4,085 andHUDLaunchMissionCompleteat 4,501 (the first life, standing still, had died at checkpoint 5). WhetherL34_OUTplays after it was not checked. -
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
level99state (the writes of item 14) and saved as the park's humans appeared, beforel34_c4started; thenl34_warriors_meet_furiesrecorded every human every update with no input (hooksevent-callandevent-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 runsP3.EndParkScene(itstfence_shake_01emitter); 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 player0-150 No enemies; they stand. Snow turns once (mode 3, toward the player's heading) at +81 +150 P3.FuryTimeout(P3.MoveFromFence): the Furies'GoalFighton the player andTacticAttack, 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, SnowParkFury02(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 MoveMeleeand 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) whileFollowPlayeris still his only goal+474 The automatic attack command: tactic 0x00544160,FollowAndAttack(0x34) withFight(0x0f) on top: only now do they fightUntil 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
FollowPlayera 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, whichWarChief_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 underFollowPlayerand 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 inrepo:src/ai/crew.cpp,FollowPlayerGoal):- 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).
- The band move. Coney's
MoveToHumanActiononly closes in to a stop distance; it needs the original's near / far band. The spec isMoveMeleeon 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. - 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.
- 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
+0x2e4turns 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. - Field of view: 2π in fight mode, 110° (and the target cleared) out of it.
- Test (
[disc],tests/platform/disc_level34_furies_test.cppor 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 isFollowPlayer; 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 theFollowAndAttack(0x34) orFight(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
FollowPlayeris 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+0x140of 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 aslevel99's: 1level2_clinic/P1.SetupClinic, 2level2_tenement/P2.SetupTenement, 3level2_clubhouse/P3.SetupClubhouse, 4level2_junkyard/P4.SetupJunkyard, each with its own Warriors set-up at the chapter's start.RunLevel: the Warriors;preLoadFileof the chapter; the follow camera (AddCameras, thenCameraSetClipping(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 callbackStartAmbient, 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, aglobal.luahelper over the tagging bindings).SetupFlagNet2is a Lua function oflevel2.luaitself (function 96 of the main chunk), not a binding: it links the 13 flagsPedNet_01-13(made byAddFlagsBoxesPaths) into a pedestrian network with 13FlagNetAddLinkcalls (two or three neighbours each), callsAddCivilians(gang 23 "Civilians" with one spawner whose per-spawn callback,global.lua'sCivFlagNet, sets each civilian walking the net withFlagNetTraverse) and then sets itself and its twinSetupFlagNet52(a 28-node net forlevel52, never called here) to nil, so it runs once. Its other caller isP2.SetupTenement, whenpedsis 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 andLoadBonus(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 unlocksUM_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 (1level3_street, 2level3_comp, 3level3_balcony, 4level3_chase, 5level3_gallery), the Warriors' five tag patterns, and runsMainthenRunMission.Main: radar range, objective, fog, ambient sound boxes, the pedestrian flag net (SetupFlagNet, as inlevel2), 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. RunMissionpreLoadFiles the chapter with itsSetup;NextMissionruns the chapter'sCleanup, adds 1 toCHAPTER,SetCheckPoint(CHAPTER)and runsRunMission: every chapter hands over to the next in place. The globalStartupis 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:
Setup: the Hi-Hat pursuers (AddPursuit: five, gang type 20, and a spawner off-screen), each taking 5× damage (HuApplyDamageModifier); the pathroofpath2(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 isch4.HoldIntroPoizo; continuing from checkpoint 3 it runs at once.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).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 onroofpath2against the Warriors' gang (gait 4, pace 4 m, up to 10 m/s, urgency 3) under aGoalMoveToFlagto the first chase flag (gait 5); the player: normal mode andGoalMoveToFlag(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.ch4.StartMimePursuit(4 s): the pursuers run to the first chase flag; two Hi-Hat lines.ch4.StartRail, message 3 ofvbStartRail, 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 onroofpath2against 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;HuLockPadandHuLockPadMovementcleared andBrDead(player, false): from here the stick moves Snow, relative to the rail camera's heading. Player 2 gets the same.- During the chase, all by kind-2 boxes (message 3):
vMainRail/vSideRail/vTopRail/vBigRailre-frame the rail withCamModifyRail(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).vFixCamboxes 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) untilch4.TurnOffSlowor leaving the box (message 4, which also returns to the rail camera).vbJumpWarnboxes show a jump prompt (HUDTurnOnActionCycleAnim) for 1.5 s. Falling intovChaseKill(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 threedyn_door_fencebarriers (door numbers 4, 6 and 10, 10 hitpoints each, two triangles each). Read from the disc'sSpawnDoorplacements 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 fromvbJumpWarn_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 invbJumpWarn_09over the gap (x 193.5-200.5) to the gallery's roof (z 14.4) andvbEnterGallery(x 200.8-208.8). Outrunning the pursuers, confirmed (runtime) in PCSX2 (coney-tools trace missionfrom 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 reachesvbEnterGallery(SetCheckPoint(5)at update 3,038). Coney's playthrough test drives the second way and reaches the gallery for every--ai-seedfrom 0 to 7 (Testing). 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 andNextMission.
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 (1level5_chapter1, 2level5_chapter1a, 3level5_chapter2, 4level5_chapter3), then runsMainandRunMission, as inlevel3:RunMissionpreLoadFiles the chapter with itsSetup;NextMissionruns the chapter'sCleanup, setsLastChapter, adds 1 toCHAPTER,SetCheckPoint(CHAPTER), saves the tags and runsRunMission. A chapter'sSetupchecksLastChapterto 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.luaused by the chapters:StartChase{Human, MaxDistance, FailDialog, Reason}: every 50 ms the distance from the chased human to the nearer player is checked; atMaxDistance(35 when not given) or more a 5 s watch starts, and getting back within range cancels it. When it runs out the player saysFailDialogand its end callsHUDLaunchMissionFailed(Reason).EndChasestops it. Several tables passDistanceinstead, whichStartChasedoes 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 withGoalRunCarrotRun(..., 5, 1, GangWarriors, 13)(keeping ahead of the Warriors); the next volume is then armed.SRun.WarpSanchezteleports 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 throughHuActionDialog; skippable with the pad (RegisterPOIZOCallback, aglobal.luahelper on pad bits 64 and 2048).POIZOCamBackundoes it all and callsCallback(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 (ConvertJumpToDoorat itsfNodeSwitchflag), 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 nameCfgRageHandlersstored first (copied to0x006b6830byCfg_SetRageHandlers,0x00236c58).global.luaregistersCfgRageHandlers("SetRageMode", "ClrRageMode", "RageFull", 20000, 5000)at boot, so it isSetRageMode, 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 at0x0051489c + 0x268below 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 throughHuman_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 byHuman_UpdateSpeech(0x0021e940): while a line plays and the partner is within 10 m it callsHuman_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 withbActionDialog; 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.luaat checkpoint 2, "comparing nil with a number" on instruction 19 is inDecodeU32(alevel5.luahelper that turns a number into a table of 30 bits; instruction 19 compares the remaining value with2 ^ i). The level's start-up calls it, for every checkpoint above 1, asDecodeU32(GetLUASaveDataFloat(1))to restore the mission's saved progress bits (tblSaveData). Coney has noGetLUASaveDataFloat, so the call returned nothing. The original returns a number: slot 1 of the eight saved script floats atW_GameState + 0x570c(0x0037b850→0x0041ad00, slotnat+0x570c + (n − 1) × 4,nread 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.luaitself 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 theglobal.luahelperFlagPos: instruction 5 reads.xof whatGetFlagPosreturned. The arena'sConfigRumblecallsAddDummyPlayer, which doesHuCreate("Dummah", 352, FlagPos(fP1[1]), ...);fP1comes fromlevel102_brawl_init.lua'sAddFlagcalls. Coney has neitherAddFlag(sofP1[1]is nil) norGetFlagPos(soFlagPosindexes nil). The original provides both as bindings:AddFlagreturns a handle,GetFlagPosanM_Vector4withx y z w(World flags). Nothing else is missing before that point:Levelis set byglobal.lua,RumbleInfoandRumbleby the arena's main chunk.GetRumbleModeData(also missing in Coney) fillsRM_LuaDatawith the Rumble menu's 23 choices; with it absent the table keeps the zerosParseLuaDataput there, soRumble.gameTypeis 0 (RumbleInfo[0]is"brawl", the right file by chance),Rumble.gangSizeis 0 (the "no gangs" path,ShowRules1) andRumble.gang1[1], the player's type, is nil. A playable arena needs the menu's values (inferred from the disassembly oflevel102.lua; the values are in Front end).level95,events.ChatEvent: attempt to index a nil value:events.SetupConversationEvent(cluster)adds 1 toevents.NumEvents, storesevents.ChatTable[NumEvents] = {eventCluster = cluster, lastStatement = 1, lastTalker = NilHandle}and callsScheduleFuncArg1("events.ChatEvent", events.NumEvents, 5000 + random(1, 5000));ChatEvent(n)then readsevents.ChatTable[n].eventClusterand that cluster'sCClusters[c].occupied. The script expects the number first and the delay last (as0x003572e8does). Coney's binding takes them the other way round, so it calledChatEventwith 5001-10000 afterNumEventsms, andChatTable[n]was nil. Confirmed (code) for the binding, the script side from the disassembly. (flags.Cleanupalso setsCClustersto nil, so a chat event that fires after it would fail the same way; inferred.)level87,ch3.BoozerChallengeis not a function: the third chapter's table at run time holdsCleanup,KillHandlers,AjaxSaysPissTank,DoNextChapter,BumScrap,OneBum,Done,PreloadedandExit, but noBoozerChallenge, 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.level87from checkpoint 1 into 2, "attempt to index a nil value (instruction 19)" after the drunk's scene (l87_drunk_dest): the scene's end runsPreCashTheWorld, which calls the second chapter's return function. That function callsRestorePreviousTagsand thench2.ArrowRadarStuff. Instruction 19 of the return function is the read of.ArrowRadarStufffrom the globalch2, which is nil. The cause is the order in the first chapter's end (The_REAL_End):SaveLevelData,SetCheckPoint(2),ChLoader(preLoadFileof chapter 2 with"Setup"), then the globalCleanup. In the original the file is only requested there (Script_PreloadFile,0x00357a68, handsFileManager_Requestthe completion routine0x00356d00; confirmed (code)), soCleanupis still chapter 1's: it setsch1,SetupandCleanupto nil, and chapter 2's chunk andSetuprun later, when the file arrives. Coney'spreLoadFileruns the chunk andSetupinside the call, so theCleanupthat follows is chapter 2's, which setsch2,SetupandCleanupto nil. A direct start at checkpoint 2 never runs chapter 1's end, soch2survives. Save float 1 (1024 for tag 10) is not the cause: with a bit setRestorePreviousTagsreadsmaster[tagflag[i]].wtag(its own instruction 19), andmasterexists, since chapter 1'sSetupbuilds it (LoadTagTable) and nothing clears it. The original does not fail here (inferred from the disassembly). ApreLoadFilemust 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:
SETLISTstores its items atA × 62 + 1onwards (luaV_execute,0x00334478, the multiplier0x3e; the largestBon the disc is 62). This is this build'sLFIELDS_PER_FLUSH. A VM that uses 64 shifts every list item past the 62nd (inconfig_preload3.lua'slevelNames, 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.luaandlevel100.luain that same state, with numbers fromGetLanguage,GetPlatform,GetCurrentLevelIndex,GetLevelIdandUM_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 bycreate()(the libraries in the original's order, string, base, math; then the bindings; then_ERRORMESSAGEand_ALERTas functions that do nothing) and made again by the level flow's unload.runFileruns a WAD script by name,runFilesa list,enterLevelrunsglobal.luaand then<level>.lua,callfinds a function by dotted name (Menu.onStart; a:passes the table asself) and calls it,schedule/flushScheduledkeep the schedule of named calls with up to two number arguments, andupdateruns 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 ofsrc/scripting/level_bindings.h: the flags,GetPosition, the saved script numbers,SetStartGameCallback,GetRumbleModeData,GetRumbleModeGangNameandCfgSetDatabaseSizes, World flags, the AI and gang bindings, which hand their calls to the AI host, AI, the trigger bindings ofsrc/scripting/trigger_bindings.h:SetMsgHandler,SetMsgHandlerEx,AddVolumeBoxandRotateVolumeBox, below, the three animation callback bindings, Characters, and the scene bindings with theGoalJoin*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 forGetPTank,ObjSpawnandCameraCreateLocked, 0 forInvNumberOf, false forUM_IsTypeDirty). The configuration stubs (theCfg*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 andHuGetGangstill 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,TriggerSphereCfginsrc/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.TriggerSphereEnablearms 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 LEVELenters a level the same way, after the preloads and a fresh state (LevelScripts,src/gamemodes/level_start.h). Inlevel99at checkpoint 1 the tutorial runs from the intro through the two markers (vMark01,vMark03; how the original draws them: Objective markers), thel99_t1scene andP1.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 thePadSetHandlerExhandler, 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 byGetLevelIdand the level flow (record 0 islevel100). - The front end runs the preloads at the legal screen and
global.luaandlevel100.luain the same state when the level flow starts the front end, then callsMenu.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
updatewaits 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 islevel100with 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'srandomuses another one of Coney's, never the C library's. The trigonometry works in degrees, as stock Lua 4.0 does.tolua,M_Vector4andM_Quatare empty tables andNilHandleandNilSoundHandleare 0, below the first handle a stub gives out.preLoadFilequeues 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)
GangCreatereturns a new handle and the gang counts 0, andInvNumberOfreturns 0 until inventories exist, so the hub's andlevel5'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). GetLUASaveDataFloatandSetLUASaveDataFloatoutside 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):
ScenePreloadreturns 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,SceneAddObjectresolves 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.SetMsgHandlerExwith 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]"runslevel95for 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; whetherTriggerSphereCfg's interval reaches the sphere's period (+0x164); whether a sphere's own object can be its occupant, and what the clear-line tests0x0024dee8and0x0024df40test between; who writes the stopwatch's rate (+0x10); what commands 37, 38 and 40 (d-pad down, up, right), which the tutorial switches withEnableCommand, do (Commands). - What a level loaded after an unload (a fresh state without the preloads) does when it needs
PHYS,MATERIALorGSTRING: does the level flow run the preloads again, or do the level scripts not need them? (For the front end,global.luaandlevel100.luarun without errors in Coney's fresh state, and so dolevel99.luawithlevel99_combat.lua,level2.luaandlevel3.luaat 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 routine0x00356d00runs 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 duringInitLevel'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 (+0x08sections,+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>.pakfiles for any one argument (25 of 63 levels with packs match the seventh).CfgObjcount: the page counts 1,279CfgObjcalls inconfig_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.NilHandleandNilSoundHandle: their values.PadSetHandler's arguments: this page gives(pad, button, name), Front end gives(button, player, "function"). Coney implements neither yet:PadSetHandleris a stub that ignores its arguments, since the front-end path needs no Lua pad handler.- Names: the
@origtags call the script system's slotsScriptSystem::Update,EnterLevel,RunFile,RunFiles,FindFunction,Call,Schedule,ScheduleArg1,ScheduleArg2,FlushScheduledandSetUpdateFunction, the binding wrapper0x0036eef8ShowProfileManager_Binding,0x0041f118W_GameState_SetLevelRecord,0x00160d78MenuLoadLevel_Choose,0x0015c7b0LevelFlow_ChooseLeveland0x0020a268PM_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-0x19and what their extra values mean (the arguments are in Message handlers); the scene-play flags (ScenePlayand its relatives) beyond theirglobal.luanames; most fields of the largeCfg*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
level99course 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_19bshown) andP1.PowersetsTT_24dwhen 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: itsHUDSetTutorialTextcalls (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 show0x001cdf7c, asHintBox_Queuereads (confirmed (code) at0x001ce484-0x001ce52c). In both runs the queue held no entry past the call that queued it, apart fromTT_26bbehind 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'sTT_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.cppstarts 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_7is 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.)