Scenes (in-engine cutscenes)¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). The disc check
coney-tools wad scenes (coney-tools) parsed all 2,765 records of the NTSC-U disc on
2026-10-06 and prints counts and hashes only. Runtime claims come from the warriors_passive trace
(repo:research/traces/scenarios/warriors_passive.toml, recorded in PCSX2 2.9.94 on 2026-10-05,
AI) and from Characters, and from the scene-binding hooks of
Cast at runtime (repo:research/traces/scenarios/scene_cast_state.toml and
repo:research/traces/scenarios/scene_cast_fence.toml, PCSX2 2.9.94, 2026-10-11).
Purpose¶
How the game plays a scripted scene in the engine: the .scn records, how a scene is loaded into one of 12 slots,
how a script binds humans and objects to its roles, what the scene does to them, to the camera, the screen and the
player's control while it plays, how it is skipped, and how it hands back to the game and to Lua. The first mission
needs two: level99's intro l99_c1 (IntroScene) and l99_c5, which comes before the sparring fight.
In one paragraph: a scene is a header record and, when long, a chain of segment records of the same tracks. The
header names roles (humans), objects, at most one camera and some lights, each with a start and an end
pose; each part holds one track per role (an ordinary animation clip, played on the human) and keyed tracks for the
objects, camera and lights, with events (fades, lens changes, captions, sounds, warps). A script preloads the scene by
name, binds humans to roles by index (GoalJoinCinematic) and objects to object slots (SceneAddObject), then plays
it. The scene task runs at 30 updates a second: it waits until everything is loaded, pushes the current camera and
makes its own camera current, letterboxes the screen, suspends the AI, drives every bound thing from its track in the
scene's space, streams the next segment while the current one plays, and lets cross or START skip after 2 s. When all
roles' clips have ended it puts skipped humans at their end poses, pops the camera back with a cut or a blend, removes
the letterbox, gives the player control back and calls the script's end function with the scene id.
Original structure¶
Scene/SceneCache.cpp (0x00351da0-0x00354bb8) holds the scene list, the 12 slots and the bindings' workers;
TaskEngine/SceneTask.cpp (0x0039d278-0x003a13d0) the scene task; the track runner is 0x00355798-0x00356390
(Source map). Names are ours.
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00353460 |
SceneList_Load |
copies the Scene List chunk | confirmed (code) |
0x00353698 / 0x00353778 / 0x00353730 |
find by substring / exact name / id | scene id lookups (Scripts) | confirmed (code) |
0x00353298 |
SceneSlot_Get |
the slot holding an id, else a free one, else one to evict | confirmed (code) |
0x00353af0, 0x003525f8 |
Scene_Request |
ScenePreload's worker: load into a slot, or count one more user |
confirmed (code) |
0x003524a8 / 0x00352430 |
allocate / loaded | memory for the record, then the file request and its completion | confirmed (code) |
0x00352098 |
SceneRecord_Fixup |
turns the header's offsets into pointers, sets state 2 | confirmed (code) |
0x003531c8 |
Scene_CallLoaded |
calls the ScenePreload callback with the scene id |
confirmed (code) |
0x00352c08 / 0x00352a48 / 0x00352788 |
segment request / allocate / fixup | streams the next segment into the other of two buffers | confirmed (code) |
0x00353818 |
Scene_Play |
common worker of the play bindings | confirmed (code) |
0x003a13d0 |
SceneTask_Create |
a scene task with its flags | confirmed (code) |
0x0039ca48 |
SceneTask_Init |
the task's defaults | confirmed (code) |
0x001cae60 |
Captions_WaitTitleCard |
holds the start while a scene's title card shows (Subtitles) | confirmed (code) |
0x0039d3a8 |
SceneTask_Bind |
runners for the camera, objects and lights | confirmed (code) |
0x0039d870 |
SceneTask_Start |
waits for everything, then starts; re-entered until it does | confirmed (code) |
0x0039cbf0 |
SceneTask_Update |
vtable +0x13c, every 2 ticks |
confirmed (code) |
0x003a00b8 |
SceneTask_ClipDone |
a role's clip ended: chain the next segment's clip or leave the scene | confirmed (code) |
0x00353a10 |
Scene_Stop |
SceneStop's worker |
confirmed (code) |
0x003a0a68 / 0x003a0be8 |
end the clips / stop looping | used by a stop and a skip | confirmed (code) |
0x0039f450 |
SceneTask_End |
gives everything back | confirmed (code) |
0x0039ec60 |
SceneTask_Abort |
gives up a scene that did not start in 10 s | confirmed (code) |
0x003a0da8 |
SceneTask_CallEnd |
calls the play binding's end function with the scene id | confirmed (code) |
0x003a0e48 |
SceneTask_HoldObject |
event 73: holds a bound object (an intro card) before the camera for a time (Intro cards) | confirmed (code) |
0x003a0ed8 |
SceneTask_UpdateHeldObject |
each update, after the objects' runners: places the held object before the camera until its time is up | confirmed (code) |
0x00356290, 0x00356308, 0x003560a8, 0x00355ab8, 0x00356188 |
track runner: bind camera / bind object, advance, step keys, apply | confirmed (code) | |
0x00354d98 |
SceneTrack_Events |
the object, camera and light tracks' events | confirmed (code) |
0x00355798 |
SceneTrack_Flush |
a skipped scene's remaining track events, a reduced set (Skipping) | confirmed (code) |
0x002e5300, 0x002e53e0 |
GoalJoinCinematic and its goal (type 0x29) |
binds a human to a role | confirmed (code) |
0x003541a0 |
Scene_BindHuman |
resets the human (Human_ResetForMode(h, 1) at 0x003541d8), then writes it into the role (The reset at the join) |
confirmed (code) |
0x002325e0 |
Human_ResetForMode |
the reset every join runs: pairs, grabs, holds, rage, stun, props (The reset at the join) | confirmed (code) |
0x0016b8d0 |
Gang_ClearBums |
a cinematic's start destroys the bums in player 1's gang (The player's gang) | confirmed (code) |
0x00354178, 0x00353b78 |
Scene_IsPreloadedWrap, Scene_GetState |
SceneIsPreloaded's worker; the state of an id's slot: 0 none, 1 loading, else the record's state (2 = ready). An AI pair goal (0x00306ec0) waits for 2, then plays its two leaders' animation |
confirmed (code) |
0x00353fc8 |
Scene_Unload |
SceneUnload's worker: a ready scene's user count to 0, then frees finished slots |
confirmed (code) |
0x00354710 |
Scene_SetCallback |
SceneSetCallback's worker (also from InitLevel): looks the Lua function up (script system +0xcc) and keeps it at 0x00512af4; an empty name clears it |
confirmed (code) |
0x00354768 |
Scene_UnloadAll |
from UnloadLevel: ends each of the 12 slots' tasks and unloads them |
confirmed (code) |
0x00353c68 / 0x00353d60 |
Scene_PlayCinematic / Scene_PlayFixed |
ScenePlayCinematic / ScenePlayFixedScene: Scene_Play at the origin with no rotation |
confirmed (code) |
0x00353e38 / 0x00353e70 |
Scene_PlayAt / Scene_PlayAtHeading |
ScenePlay's two forms: straight through, or a position plus a rotation built from a heading (0x00335ea0) |
confirmed (code) |
0x00354c00 / 0x00354bb8 |
Scene_InvokeInPlace / Scene_Invoke |
SceneInvokeInPlace / SceneInvoke: Scene_Play with no scene id of its own |
confirmed (code) |
0x00354038 / 0x00354c48 |
Scene_StopWrap / Scene_Terminate |
SceneStop (also from GoalJoinCinematic's goal) / SceneTerminate: Scene_Stop(id, false) |
confirmed (code) |
0x00354378 |
Scene_GetRoleStart |
a role's start position and rotation (role record +0x90 + 0x60 × role, +0x10, +0x20), loading the scene first; optionally fitted to the bound humans (Scene_FitToHumans); out of range gives the identity |
confirmed (code) |
0x002e51a8 |
GoalJoinCinematic_WalkToMark |
the join goal's step: a move action to the role's start, then a turn to it | confirmed (code) |
0x00354c80 |
WarMoveInstance_ctor |
the 0x90-byte scene instance (vtable 0x00544960), two identity transforms |
confirmed (code) |
0x00352fa0 |
SceneSlot_WaitSegment |
while the slot's next segment buffer is busy, services the file system and retries | inferred |
0x00354558 / 0x003546a0 |
SceneCam_Lock / SceneCam_Unlock |
a locked camera (type 4, LockedSceneCam) made the current scene camera (0x003a1558) over the player's camera (its active sub-camera in modes 5-8 pushed); unlock pops it back and frees it. Used by the in-game camera bindings (0x002cb538) |
confirmed (code) |
0x003555d0 |
SceneTrack_ParticleEvent |
event type 33: the effect starts only when its position is inside player 1's view (each of the six planes, 2 m margin), except the effect sub_shk, which always plays |
confirmed (code) |
0x00356028, 0x00356090, 0x003560a0 |
SceneTrack_SetTarget, SceneTrack_SetCamera, SceneTrack_SetObject |
track runner: set the track's target and re-read its keys at the current frame; the camera form sets +8 and +0xc, the object form +8 |
confirmed (code) |
0x00354280 |
Scene_BindObject |
SceneAddObject's worker: writes the object into the slot by index (definition +0x50) and pins its record; refuses an object already in a scene |
confirmed (code) |
0x00299248 |
Goal_StartParlay |
GoalStartParlay's worker: preloads <base>_000 to <base>_004 (formats 0x00561760-0x00561780) and pushes the parlay goal; no script on the disc calls it (Which level) |
confirmed (code) |
0x002cb5d0 |
Brain_PlayScene |
the parlay goal's steps: looks the five parlay scenes up by name and plays them as animations | confirmed (code) |
0x00306c68 |
AddressTactic_Init |
TacticAddress's tactic: looks its scene up by the name the script gives (no script on the disc calls it) |
confirmed (code) |
Data¶
All values little-endian. Offsets inside a record count from its start; the game adds the record's address on load
(0x00352098, 0x00352788). Layout confirmed (code) at those fix-ups and the readers cited; every record on the disc
parses with it (coney-tools wad scenes, Disc check).
The scene list and the slots¶
scene_list.cnk and the id lookups are on Scripts. A record is loaded as
%s.scn (0x00578a08) with the list name, so the 24 names the list cuts to 16 characters are those of records the
game cannot open by that name (inferred: no WAD name hashes to them; the disc check finds them by content).
There are 12 slots of 0x40 bytes at 0x006eba18, confirmed (code) at 0x00353298:
| Offset | Meaning |
|---|---|
+0x00 |
the loaded header record |
+0x04 |
the scene list record (its id at +0) |
+0x0c, +0x10 |
the two segment buffers; +0x14, +0x18 their list records |
+0x24 |
the scene task while it plays |
+0x2c |
flags: 0x1 loaded, 0x2 waiting for memory, 0x4 file requested; the same three per segment buffer in the next nibbles; 0x1000 the scene has segments; 0x2000 restart the chain (looping) |
+0x30 |
s16 current segment buffer (0 or 1), −1 when the chain is done |
+0x34 |
users: 1 when the record arrives, one more per later ScenePreload, one less per unload (Loading) |
+0x38 |
the interned name of the ScenePreload callback |
+0x3c |
the time of the last request (for eviction) |
A request finds the slot already holding the id, else an empty slot, else a slot whose scene has ended (state 8),
else the least recently requested slot in state 2 or 8, which it unloads first (0x00353298). Confirmed (code).
The header record¶
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u32 | the record's size |
+0x04 |
u32 | 0 in the file; the scene id after loading (0x00353698 of the name) |
+0x08 |
char[16] | name |
+0x18 |
char[4] | suffix of the first segment (aa), empty for a one-record scene; non-empty sets slot flag 0x1000 (0x00352430) |
+0x1c |
u16 | runtime state: 2 loaded, 4 starting, 5 playing, 6 stopping, 7 ending, 8 ended |
+0x1e |
u16 | runtime: humans still in the scene |
+0x20 |
u8 | number of human roles |
+0x21 |
u8 | number of objects |
+0x22 |
u8 | 1 when the scene has a camera |
+0x23 |
u8 | number of lights |
+0x30 |
char[84] | a name, the scene's own in 657 of 1,240 headers (l99_c1_01 in l99_c1); no reader found |
+0x84 |
u32 | the whole scene's length in frames at 30 a second: the parts' durations summed, in all 1,238 headers that have tracks (inferred from that match) |
+0x90 / +0x94 / +0x98 / +0x9c |
u32 | offsets of the role definitions (0x60 each), object definitions (0x60), the camera definition (0x70) and the light definitions (0x70) |
+0xa0 / +0xa4 / +0xa8 / +0xac |
u32 | offsets of four tables of track offsets: a clip per role, a track per object, the camera's track, a track per light |
+0xb0 |
u32 | equal to +0x90 in every record; not read |
A role or object definition (0x60): char name[16]; the start pose at +0x10 (position x, y, z, 1, then a
quaternion x, y, z, w at +0x20); the end pose at +0x30 (+0x40 the quaternion); +0x50 the bound human
or object at runtime (0 in the file). A role's name looks like a model name (warrcl, warrrecv, warrashcv, ...)
but is only a label: binding is by index, and no code maps the name to a model (Cast and props).
The camera definition (0x70): the same name and poses, then +0x54 near plane, +0x58 far plane (slot +0x1ac),
+0x60 the world preload's radius, +0x64 field of view in degrees, +0x6c the scene camera at runtime; +0x50,
+0x5c and +0x68 are not read by the code traced. Confirmed (code) at 0x0039d870; l99_c1 has near 0.5, far 75,
radius 500, field of view 65.47°.
A light definition (0x70): name and poses, +0x50 u32 kind (1 → light flags 0x81, 2 → 1, else 0x80),
+0x54 colour r, g, b (0-1), +0x64 cone angle in degrees (halved and turned to radians), +0x68 range (0 means
15), +0x6c the light at runtime; made by the light task manager when the scene starts (0x003a0928). Confirmed
(code). All 21 lights on the disc are fspot01.
The segment record¶
| Offset | Type | Meaning |
|---|---|---|
+0x00 |
u32 | the record's size |
+0x04 |
char[16] | name: the scene's name cut to 15 characters, then the suffix |
+0x14 |
char[4] | the next segment's suffix, empty for the last |
+0x18-+0x1b |
u8 × 4 | counts of role clips, object tracks, camera tracks (0 or 1) and light tracks |
+0x1c-+0x28 |
u32 × 4 | offsets of the four track tables, as the header's +0xa0-+0xac |
A segment has no definitions: it continues the header's roles, objects, camera and lights in the same order. The
game names the next segment strncpy(name, 15) + strncat(suffix, 3) (0x00352c08) and finds it by exact name.
Confirmed (code).
Tracks¶
A role's track is an animation clip: the 80-byte descriptor of chunk 0x02 followed by its keys and events,
exactly as in a character's resource (Animation); the fix-up
gives it the clip vtable 0x00534240. A role clip's events count frames from the scene's start, not the part's:
l99_c1's first-part clips (506 frames) carry events up to frame 2022 of its 2,026, and the segments' clips carry
none. Inferred from the data and the frame offset 0x003a00b8 adds at each segment change.
An object, camera or light track (vtable 0x00544908):
| Offset | Type | Meaning |
|---|---|---|
+0x04 |
f32 | duration in seconds |
+0x08 |
u32 | bytes of keys and events |
+0x0c |
u32 | bytes of position keys |
+0x10 |
u32 | offset of the keys |
+0x14 |
u16 | number of events |
Then position keys (16 bytes: u16 frame, 2 unused, f32 x, y, z), rotation keys (8 bytes: u16 frame,
s16 x, y, z × 2⁻¹⁵, w rebuilt as in clips) filling the rest less the events, and the events (24 bytes: u16
frame, u16 type, arguments) at the end. Frames are absolute (frame × 1/30 s is compared with the time), and
between keys the runner interpolates positions linearly and rotations as quaternions (0x00355ab8). Confirmed (code)
at 0x00355e98, 0x00355ab8. Like the role clips, the header's camera track carries the whole scene's events,
counted from the scene's start: l99_c1's (506 frames of keys) has events up to frame 1990, and its fades and caption
controls fall where the scene needs them only when read that way. Inferred from the data.
Events¶
Object, camera and light tracks (0x00354d98, confirmed (code) for the actions; counts from the disc):
| Type | Count | Action |
|---|---|---|
| 13 | 8 | preloads (at load) and plays a sound by hash (+8) on the sound manager 0x0050aa84 (inferred: dialogue) |
| 14, 71 | 29, 2 | a sound by hash on the track's human or object; on a car, also its horn or engine (0x0038d6d8) |
| 24, 25 | 203, 212 | sends message 0x12 / 0x13 to the track's object; for a simple_object, show / hide (Objects: the Wonder Wheel); other classes not traced |
| 26 | 41,408 | the scene camera's lens: field of view +8 (degrees), near +0xc, far +0x10 |
| 27 | 300 | fade out over +8 seconds (ScreenQueueEffect type 1 on player 1's view); stops the caption |
| 28 | 380 | fade in over +8 seconds (type 0) |
| 29 | 38 | loop point: in a looping scene, the frame to restart from (+4) |
| 30 | 72 | a light's colour (+8), range (+0x14) and cone (+0x10, degrees) |
| 31 | 33 | calls the scene's end function now (also when skipped past, Skipping) |
| 33 | 333 | a particle effect at a position and rotation (s16 values, scaled as clip keys) named by +0x14 with a prefix |
| 41 | 1,422 | caption control: +4 = 0 shows the next caption, 4 or 5 set that kind (4 hides it), 6 sets a flag first (confirmed (code) at 0x00354d98; Movies) |
| 69 | 0 | an object or car action (0x00396048, 0x0038d798) |
| 73 | 23 | intro card: holds the object in slot +4 (s16) before the scene camera for +8 seconds (Intro cards) |
| 74 | 0 | a coloured fade: type in bit 31 of +4, colour in its low 24 bits, +8 seconds |
| 76 | 241 | pad rumble on every player: strength sqrt(+6 × 0.01) × 180 + 75, at most 255 |
Types 9, 10 and 54 also occur (4 events) and have no case (nothing happens). Role clips carry clip events
(Animation) handled by the animation code (0x00101dd8); scene clips use
11 (4,486, footsteps and cloth sounds by name), 21 and 22 (4,125 each, always in pairs) and others. Type 21 sends
message 0x95 with the position at +8 and type 22 message 0x96 with the value at +8 (confirmed (code) at
0x00101dd8); every role clip on the disc starts with a 21/22 pair at frame 0 whose position is the role's start
position and whose value is its start heading in radians (2·atan2(z, w) of the start quaternion), and the last pair
is the end pose. Inferred: they place the human at the role's marks (at the start and at camera cuts). Type 13 at
frame 0 of one clip names the scene (inferred: its dialogue).
Behaviour¶
From Lua: SuperRunScene¶
The scripts never call the play bindings directly for a story scene; they build a table and call the global.lua
helper. Confirmed (code) for global.lua, read as bytecode (functions at its lines 4107, 4156 and 4233):
SuperRunScene(t): unlesst.Animation,HideHud(), then (unlesst.NoClearWanted, and whenGangWarriorsexists)GangClearWanted(GangWarriors)andGangClearResponders(), thenScreenQueueEffect(1, 0)(black at once). Witht.StageLighting,EnterStore({0, 0, 0, 0})andEnableStageLighting(true). Each human oft.Humansthat is notNilHandleand is in gang type 0 is un-arrested and revived if needed (each counted withStatAdd(player, 5, 4, 1)). Witht.Preloaded,t.SceneIdis already an id:tblScene[id] = tandgPlayCutScene(id)at once. OtherwiseScenePreload(t.SceneId, "gPlayCutScene"),tblScene[id] = t. Returns the id.gPlayCutScene(id)(called by the engine with the id once loaded,0x003531c8): unlessSpotON, warrior and enemy spotting off (CfgSetWarriorSpotting(0),CfgSetEnemySpotting(0)); defaultsBars= true,Delay= 0,Speed= 0, andBlendCam= −1 withFadeIn= true unlessBlendCamis given (thenFadeIn= false); withbBackOff,SetBackOff(Humans). Each non-NilHandlehuman i (from 1) joins role i − 1:GoalJoinCinematic(h, id, i − 1, Speed, true)(GoalJoinAnimationwhenAnimationis 1,GoalJoinFixedScenefor anotherAnimationvalue). Each object i joins slot i − 1 (SceneAddObject). ThenScenePlayCinematic(id, Delay, "PreCashTheWorld", Bars, not NoSkip, Looping, Freeze, BlendCam, Final, Chain)(ScenePlayAnimation/ScenePlayFixedScenewith"AnimReturnFunc"for the animation kinds).PreCashTheWorld(id)(the end function): withNumCallBacks> 0, callsReturnFunc(NumCallBacks)and counts down instead. OtherwiseEndBackOffwithbBackOff; if notFinal:HUDShowMissionSummaryText(HudText, 6)withHudText,ReturnFunc(id), spotting back on unlessBlendCamis 0,ExitStore()and stage lighting off withStageLighting, and withFadeInScreenQueueEffect(0, 0.5)(orScheduleFunc("DelayedFadeIn", DelayFadeIn));tblScene[id] = nil. WithFinal: black at once,ReturnFunc(NumCallBacks),tblScene[id] = nil,HUDLaunchMissionComplete(). ThenbSuppressHud = nil. The count-down calls come from the scene's type-31 events (Events), which a skip still fires, one call each, before the final call (Skipping); the helpers never check whether the scene was skipped.
Nothing in these helpers shows the HUD again, and level99's ReturnFuncs do not either: the cinematic end's
letterbox-out does, as its bars finish going out (HUD: who shows the HUD again).
level99's two scenes (level99_combat.lua, Scripts):
| Table | SceneId |
Humans (role order) |
Objects |
ReturnFunc |
|---|---|---|---|---|
IntroScene, built in P1.SetupCombat, run by StartAmbient at checkpoint 1 |
l99_c1 |
Cleon, Vermin, Rembrandt, NilHandle, Generic2, Generic1, Generic3, player2 (Ash) |
dyn_s_spraycan, dyn_rembrandt, dyn_vermin, dyn_intro_test |
P1.StartTraining |
built in P1.SetupWarriors (when bAshIntro is false; else retried in 50 ms) |
l99_c5, Preloaded = false |
Cleon, Vermin, Generic1, Generic2, Generic3, NilHandle, player, player2 if HuIsAPlayer(player2) else NilHandle |
none | P1.SendWarriors (the fight) |
Neither sets Bars, BlendCam, Delay, NoSkip, Looping, Freeze, Final or Chain, so both are cinematic,
letterboxed, skippable, not frozen and end with a cut hidden by the 0.5 s fade-in. freeze defaults to true only
when the argument is absent: the binding's boolean reader (0x00409318) returns false for an explicit nil, and
global.lua always passes ten arguments. Confirmed (code). Their records:
| Scene | Frames | Parts | Roles (index: name) | Objects | Camera |
|---|---|---|---|---|---|
l99_c1 |
2,026 (67.5 s) | header + aa-ae |
0 warrcl, 1 warrve, 2 warrrecv, 3 warrso01, 4 warrso02, 5 warrso03, 6 warrso04, 7 warrashcv |
s_spraycan01, rembrandt, vermin, cleon |
yes |
l99_c5 |
500 (16.7 s) | header + aa |
0 warrcl, 1 warrve, 2 warrash, 3 warrlynx, 4 warrjones, 5 warrmal, 6 warrrecv, 7 warrashcv |
none | yes |
A role bound to NilHandle (role 3 of l99_c1, role 5 of l99_c5) simply does not play.
level99.lua also plays one scene directly: F.RunWonderWheel(id), the ScenePreload("WonderWheel_99", ...)
callback, binds 29 objects (the wheel, its carts and neon signs) to slots 0-28 and calls ScenePlayFixedScene(id, 0,
0, true, false): the wheel turns as a looping object scene, not frozen, with no camera change.
Loading¶
ScenePreload(name, callback) finds the id and requests the slot (0x00353af0): a scene loaded or loading gets one
more user (+0x34) and nothing else; otherwise memory of size + 0x40 is requested from the pool 0x0050cd4c
(0x001886d8, queued until it fits), then the file %s.scn is requested (0x003524a8). On arrival the header is
fixed up (state 2), the slot is flagged loaded, and the callback is called with the scene id (0x003531c8, the task
manager's phase set to 0 for the call). A play binding on a scene not yet loaded requests it and waits for the file
there and then (0x00353020 services the file manager until it arrives). Confirmed (code).
- Users. The arrival sets the user count to 1 (
0x00352430→0x00353158); aScenePreloadon a scene already loaded or loading, in any state (idle, playing, ended), only adds a user and stamps the request time (0x00353158): no callback, no restart. Confirmed (code) at0x00353af0. - Unloading (
0x00351da0, from the end of a play and fromSceneUnload): one user less (0x003531a8, never below 0); at 0 the record and both segment buffers are freed and the slot emptied; otherwise the record stays and its header goes back to state 2 (loaded, idle). Confirmed (code). SceneIsPreloaded(name)is true only for a slot whose header is in state 2 (0x00354b38): false while the file loads and while the scene starts, plays or ends (states 4-8). Confirmed (code).
Segments stream during play (0x00352c08): the first request loads the first segment into buffer 0; each later
one toggles the buffer, frees what it held, and loads the next by name. The update requests the next segment once
every runner has moved onto the current one (+0xf2 runners ≤ +0xf4 switched) and, for a cinematic, unless a
chain skip is in progress. A role clip ending switches to the same role's clip in the newest segment
(0x003a00b8, waiting for the file if it is still in flight); with no segment left, the human leaves the scene.
Confirmed (code).
Playing¶
Scene_Play (0x00353818) takes the id, a position and rotation, the delay, the end function and the flags below,
creates the scene task (0x003a13d0, cancelling game mode 0xf if it is on top), binds its runners (0x0039d3a8),
stores the end function and BlendCam (task +0x94) and calls the start (0x0039d870). Confirmed (code). The task's
flags and how each binding sets them:
| Task | Flag | ScenePlayCinematic |
ScenePlayFixedScene |
ScenePlayAnimation |
|---|---|---|---|---|
+0xed |
cinematic (the binding's bars): letterbox, preload, player hand-over |
bars |
0 | from an argument |
+0xe5 |
skippable | skippable |
not set (a register left over) | from an argument |
+0xeb |
looping | looping |
looping |
from an argument |
+0xef |
freeze | freeze (1 when omitted) |
freeze |
from an argument |
+0xe7 |
final | final |
0 | 0 |
+0xe8 |
chain | chain |
0 | 0 |
+0xea |
placed relative to its humans | 0 | 0 | 1 |
Which of ScenePlayAnimation's arguments lands in which flag is not traced (0x00353f40 passes its registers on
in a different order).
The scene's space is placed by the position and rotation: ScenePlayCinematic and ScenePlayFixedScene pass the
origin and the identity, so the records' coordinates are world coordinates; ScenePlay passes the script's;
ScenePlayAnimation passes none and the scene is fitted to its bound humans (0x003547e8, details not traced).
ScenePlayCinematic with skippable also sets 0x00510174, a flag the screen effects clear and a player check
(0x0027c120) reads. Confirmed (code). The bars argument therefore switches the whole cinematic behaviour, not only
the bars.
Starting¶
The task's update (0x0039cbf0) runs every 2 ticks (30 a second, Task_SetUpdateInterval(2)). While the state is
4 it counts the delay down by one per update and re-enters the start, which returns at the first thing not ready
(state stays 4). Confirmed (code) at 0x0039d870 unless marked:
- Once: keep the position and rotation; the delay becomes
delay × 60updates (inferred: so 2 s per unit); give up after 10,000 ms of game time (GameTimer +0x48), when the update calls the abort (0x0039ec60, which gives everything back as the end does, without the end function being traced). - With a camera, for every player: end screen look 2 if the human has flag
0x80000, and queue screen effect 5 (end the blur pulse) at once. - Mark every bound human in-scene (human flag
0x800000); a human not yet ready waits. - Cinematic only, steps of the global scene state
0x0051489c + 0x410(one per update): 0 → 1 (the bums in player 1's gang destroyed,Gang_ClearBums0x0016b8d0, The player's gang; the player panels hidden,0x001b2380, andHideHud, HUD); 1 → 2: player 1's camera (the camera inside a wrapper of types 5-8) saves its place in the task and moves to the scene camera's start position, and the world around it is preloaded with a 10 s budget and the camera definition's radius, loading<scene>.pakwhen it exists (WorldManager_Preload, Level loading); 3 comes in step 6. - Wait until every bound human's character instance and model are loaded (requesting them), then every bound object.
- Cinematic (or level id
0x3c): wait until the prepared soundtrack's stream is primed, or its pending re-preload has run, stopping the music when it holds both stereo pairs (Sound: scene soundtracks). Cinematic: system music off (0x0041a008(0), game state+0x3f8= 0, Sound; whether it was on is kept in the task's+0xf0, and the end turns it back on only then); state 3; wait one more update; then clear pad flags0x40and0x800on every player, and unlesschain, clear the chain-skip flag (0x0051489c + 0x56e4). - While the delay has not run out, step every bound human toward its start mark (
0x0039d618, 0.1); wait until all arrive or the delay ends. - Wait while the scene's title card shows, if it has one (
0x001cae60(0x00619570), Subtitles). - Start: call the global scene callback (
SceneSetCallback) with no arguments; make or reuse the scene camera (Camera); start the camera, object and light runners at frame 0; for each bound human (Humans); for a cinematic, letterbox in on every player's view (ScreenQueueEffecttype 2 over 1.5 s, at once whenchain); withfreeze, suspend every brain; for a cinematic with roles, stop every human's held sound (0x0021ecd0). State 5.
Humans¶
Binding. GoalJoinCinematic(human, scene, role, gait) pushes goal 0x29 and at once resets the human and writes it
into the role (0x003541a0: the reset always, The reset at the join; the role only if it exists and
the human is in no scene, human +0x280 = −1); a pad-controlled
human's brain is switched off (BrDead(brain, 1)) right away. The goal's Start switches an AI brain off too
(remembering whether it was), pushes a move to the role's start mark at the given gait and a turn to its start
heading; its Process finishes when the scene has. Its End restores an AI brain; its destroy turns a pad brain back on
(BrDead(brain, 0)). Confirmed (code) at 0x002e53e0, 0x002e5480, 0x002e55a0, 0x002e55e8, 0x002e5618.
At the start each bound human that is free (below) and not already in a scene: counted in (+0x1e), +0x280 =
scene id, its brain's actions cleared, its state reset, record +0x08 0x2000 set, its scene transform set to the
scene's position and rotation, message 10 sent to its held object's task; its pick target (+0x33c,
Human_SetObjectTarget 0x00227080) set to the scene object in the slot that the value (+0x04, read by
Clip_GetEventValue 0x00101ce0) of its clip's first type-9 event names, so the clip's own event 9 later takes that
object into the hand (Objects in a hand; l80_c5's cops hold their batons this way,
Scripts); then its clip is pushed as three animation tasks with the end callback
0x003a00b8, looping when the
scene loops, streaming when it has segments, blended in over 0.3 s only for a non-cinematic scene placed on its humans
(and not in game modes 8 and 0xb); the instance is put in mode 3. Confirmed (code) at 0x0039d870.
The state reset is a handful of small resets, not a release, confirmed (code): 0x0023e6e8(h, 0) takes the
human out of shadow (Brain_SetHiddenInShadow) and, for player 1, ends a pending gang command and clears +0x658;
brain +0x2d5 = 0 (0x0028ef00); the motion reset (slot +0x14c, 0x0023f158: velocities and turn zeroed, the
transform re-applied as it is); 0x00227388 sends message 0x15 to the object whose handle is at +0x360 (not
traced) and clears it; the record's state code +0x14 and +0x18 = 0. None of them touches the state bits of a
grab or a mount, and none calls Grab_Release or Human_BreakPair (Combat).
A human in a grab at the start is not taken in (rare: the join has already let a bound human go,
The reset at the join). "Free" (0x002263d8) means: state 0x100000000 clear, none of the grab,
mount and tackle-mount bits 0xcf0 (grabbed 0x10 / 0x20, grabbing 0x40 / 0x80, mounting 0x400, mounted
0x800), and none of +0x08 0xc1e600. Confirmed (code) at 0x0039d870. Step 5 above waits (state 4) only for
the +0x08 bits; a human that is grabbing, grabbed, mounting or mounted is skipped: not counted, not reset, no
clip, its +0x280 left −1, while the scene plays on without it. The grab is not released by the start, on either
human (the partner need not be bound at all). Its join goal still holds a player's pad off, so the grab runs on
until something breaks it: its power running out (Grab_Release, 95 / 94; inferred, Combat),
a script teleporting either human, or, for a skipped scene, the end's placement below (both through
Human_BreakPair, the partner playing its reaction clip). The cinematic's step 0 → 1 (Gang_ClearBums,
0x0016b8d0) destroys the player's gang members whose type byte +0x11b is 6; it does not release grabs either.
l99_c7 and the lesson-6 bum (issue #225). P1.PowerMovesDone, 2500 ms after the extension 59 is counted,
binds l99_c7's 13 roles through SuperRunScene (below): 0 Cleon, 1 Vermin, 2-4 Generic1-3,
5 NilHandle, 6 CombatEnemy.GenWarrior (the lesson's victim, Rudy; role name rudy), 7-10
CombatEnemy2.SecondWave01-04 (bum01-bum04), 11 the player (warrrecv), 12 player2 or NilHandle; objects
the left door of ChA00 and dyn_beerbottle; return P1.SetupSnapAttacks. Before that it clears Rudy's message
handler 1 and flushes his brain (BrFlush). Inferred from the disassembly of level99_combat.lua (names and order
only). In the original the 59 pair is gone by the time the scene starts, so both are free and bound
(confirmed (runtime), PCSX2 2.9.94, two plays of checkpoint 1 with the course of
repo:research/traces/missions/level99_cp1.toml, the pad let go after P1.PowerMovesDone so the scene was not
skipped, hooks pair-break, throw-clear-link, prop-release):
| Update (second play) | What happened |
|---|---|
| 2057 | 59 starts (Rembrandt state 0x1005, Rudy 0x2001, held 0x400000, Rudy's clip 60); P1.PowerMovesDone scheduled |
| 2131 | P1.PowerMovesDone: ScenePreload("l99_c7") |
| 2141 | 59's event 0x31 removes the spray can (Combat) |
| 2142 | ScenePlayCinematic; in the same update Human_ResetForMode (0x002325e0) runs on every human: Human_ReleaseCarriedProps for each, and Human_BreakPair(Rudy) (return 0x00232690), which clears the throw link (Throw_ClearLink(Rembrandt, —) from 0x00258db4): the pair ends there, 85 updates into the 94-update 59 |
| 2143 on | both in the scene: state 0x800000, scene id 2630 at +0x280, clip 388; Rudy stands on role 6's start mark (−282.58, 127.29) |
| about 2540-2730 | Rudy's role clip moves him (−282.6, 127.3) → (−276.6, 122.2): the Warriors drag him off |
In the first play (scene skipped by the course's presses) the same states showed and the end placed Rudy at role 6's
end mark (−273.62, 120.81). Which caller runs Human_ResetForMode at the cinematic's start was not traced (open).
This qualifies the paragraph above: the scene task's own start does not release a pair (confirmed (code) at
0x0039d870), but in this cinematic the pair was broken in the play call's update, before the free test, so a
player's grab or paired move does not survive a SuperRunScene cinematic here; whether every cinematic does this
waits for that caller.
Coney (#225 and #281, implemented): SceneSystem::joinHuman, the bind, resets the human first, before it checks
the scene or the role (SceneHost::resetHumanForJoin(), Human::resetForJoin()), with the steps of
the reset at the join that Coney models: a throw's aim cancelled, any pair broken (Human_BreakPair,
Combat), a wound ended, then HuSetNormalMode(human, true) (a grab, a stun, rage and a fall
ended, the idle, at least 1 health), and the carried prop gone; and every raging human's rage ends at once
(human::endAllRage(), play mode's join reset). A human never bound is not reset. Stand-in: fire, mini-games, overlay
tasks, held kind-0x27 / 0x10 objects, slow motion, a climb's body and a picked object are not in Coney, so those
steps are left out. Rudy then joins role 6 and is moved by his role clip. Disc tests: through lesson 6 into l99_c7
Rudy is in role 6 and moves 11.2 m (tests/platform/disc_level99_power_lesson_test.cpp); the player still holds Rudy
on the update before the play and no human is in a pair as it starts
(tests/platform/disc_level99_scene_grab_test.cpp). Known difference: Coney starts l99_c7 7 updates
earlier than the original (update 78 of 59 against 85), probably because Coney's scene preload is instant where the
original takes 11 updates from ScenePreload to the play, so the reset removes the can before 59's own 0x31.
While it plays the human is driven by the clip's animation and root motion (Animation),
and the clip's 21/22 events place it at its marks (Events). When its clip ends (0x003a00b8): unless
skipping, it leaves the scene (+0x280 = −1), flags 0x2000 and 0x800000 cleared and 0x20000000 set, message
10 sent, and the count drops; the scene goes to state 7. With segments the clip is replaced by the next part's
instead. Confirmed (code).
Confirmed (runtime) in the warriors_passive trace (from the state saved as P1.SetupWarriors starts l99_c5):
Generic1 holds goal 0x29 with scene 2627 (l99_c5) for 481 updates (16.0 s of the scene's 16.7 s; the state was
saved after it began); when it pops, Generic2 stands at (−282.55, 128.97), role 3 warrlynx's end position
(−282.56, 128.97), and the player at role 6's (−286.93, 121.58). After l99_c1, Rembrandt stood at role 2
warrrecv's end position (−289.03, 120.29) (Characters).
The reset at the join¶
Every human bound to a role is reset first, at the join, not at the scene's start. Scene_BindHuman
(0x003541a0) begins with Human_ResetForMode(human, 1) (the call at 0x003541d8), before it loads the scene,
checks the role or checks that the human is in no scene, so the reset happens even when the bind is then refused.
Human_ResetForMode has three callers: Scene_BindHuman, the HuSetNormalMode binding's worker
Human_SetNormalMode (0x0023a210, which passes the script's full flag) and GeneralParlayGoal_Init
(0x002cb348). Scene_BindHuman in turn is called by the six goals that put a human in a scene:
GoalJoinCinematic's constructor (0x002e53e0, the call at 0x002e5464, run when the binding pushes the goal),
GoalJoinAnimation (0x002e5750), GoalJoinFixedScene (0x002e5188), the join-scene goal's Start (0x002e4eb8)
and the two play-animation goals' Starts (0x002e4a10, 0x002e4ba0). Confirmed (code) at those addresses.
So the rule is: every scene of every kind (cinematic, fixed scene, animation) resets each human it binds, once,
when the script binds it; a role left to NilHandle resets nobody, and humans that are not bound are not reset
(except for rage, below). For a SuperRunScene cinematic, gPlayCutScene binds every role and then calls
ScenePlayCinematic in the same Lua call (SuperRunScene), which is why the reset showed at
runtime in the update of the play call (l99_c7, issue #225). The reset is exactly HuSetNormalMode(human, true)
(binding); confirmed (code) at 0x002325e0:
- A human with any hold flag drops its overlay task; every player in rage (
+0xe0bit0x80000) leaves it (Humans_EndAllRage, all players, not only this human); fire put out; a mini-game ended; a throw's aim cancelled;+0x644= 0. - When the human is in any paired or special state (mask
0x7001e193ff0): the pair is broken (Human_BreakPair, Combat), then a mini-game aborted, a special state and a workout ended, a mug (0x100/0x200), a tag (0x2000000), a tackle hold (0x8000000000/0x10000000000), a tackle (0x800/0x400) or a grab (0xc0/0x30, both sides) ended with its partner, and wounded (0x10000), down (0x80000) and stunned (0x100000) ended. - Always: a held object of kind
0x27or0x10dropped, the carried props released (Human_ReleaseCarriedProps), the fight stance left, and the slow motion a player caused turned off. - Then (the join always passes 1): state bit
0x80000000and bits0x3c00000000cleared (ungrounding first when set), the body restored after a climb, an object it was picking (hold flag0x4000) unpinned and let go, its idle clip rebuilt with action 0, and the motion reset (vtable+0x14c). Last, state bit0x20000000is set.
What this means for Humans: a human that is in a grab, a mount or a tackle when the script binds it is let go there and then, its partner too (the partner need not be bound), so it is normally free by the time the start's free test runs. The free test still matters for a human that gets into a pair between its join and the scene's start (the start can wait for loads and for the walk to the marks).
Objects, camera and lights¶
A track runner (0x90 bytes) per object, camera and light holds the track, the target, the time, the scene's
position (+0x40) and rotation (+0x50) and the sampled pose (+0x60, +0x70). Each update it advances by the
frame's time; at the end of a part it returns the time left over and switches to the next part's track (0x003a0400
camera, 0x003a05a8 objects, 0x003a0768 lights); with none left, a looping scene (runner bit 0x4, set at the
start from the task's +0xeb) starts again from the header's track (restarting the segment chain, slot flag
0x2000, or from the loop point of event 29) with the time left over, so the pass repeats seamlessly; any other
scene without roles ends (state 7). It then sets the target's transform to scene rotation × sampled position +
scene position (0x00356188, target vtable +0x6c). Confirmed (code).
The camera (when +0x22 is set): a type-4 camera (Cam_Scene) is made, given the scene's name and the current
camera's view, the current camera is pushed and the scene camera made current with no blend (0x0011ee08); its
lens is the definition's field of view, near and far; each update the track moves it and its own update runs
(vtable +0x134). The details of the push, of CameraMakeActive during a scene and of the pop are on
Camera. A scene without a camera keeps the current camera. Confirmed (code).
Objects bound by SceneAddObject follow their tracks; at the end they are released to the object manager.
Lights are made from their definitions at the start and released at the end. Confirmed (code).
Cast and props: what a role's or object's name means¶
The original never turns a role's name or an object definition's name into a model. A role is drawn as whatever human the script binds to it, and an object slot moves whatever object the script binds; a role or slot nobody binds plays nothing. Confirmed (code):
- Binding is by index only.
Scene_BindHuman(0x003541a0) andScene_BindObject(0x00354280) write the handle's object into definition+0x50of the role or slot given by number, checking only the index against the header's counts (+0x20,+0x21) and that the human or object is in no other scene. - Nothing reads a role's name. Every load of the role table (header
+0x90) in the scene code (SceneRecord_Fixup,Scene_BindHuman,Scene_GetRoleStart,Scene_FitToHumans,SceneTask_StepToMarks,SceneTask_Start,SceneTask_ClipDone,SceneTask_EndClips,SceneTask_StopLooping,SceneTask_End,SceneTask_Abort) reads a pose (+0x10to+0x4c) or the bound human (+0x50), never+0x00; no binding takes a role by name. - One read of an object's name, in the start (
0x0039d870, the compare set up at0x0039e4a0): when a bound object's definition name begins withs_(its first two bytes0x5f73), the object gets message 8 before its track is bound. For asimple_object, the class of every object the scripts bind to ans_slot (44 of 44), message 8 puts it back at its attached pose (simple_object). 775 of the 2,317 object slots on the disc ares_names (s_spraycan01). - What decides the look: a human's model is its character type, the second argument of the
HuCreatethat made it (Characters, character list), so the same role wears different models in different scripts (l11_c1'swarrclis Cleon'swarr_cl_gen,l80_c1's Cleon iswarr_cl). An object's model is its object type: the scripts bindObjects.<key>, which holds theObjSpawn(type, ...)of that type (a type spawned again is keyed<type>01,<type>02), or a door's leaf (GetLeftDoorHandle,GetRightDoorHandle). An intro card is an object type of its own:l99_c1's slotcleonis bound todyn_intro_test, not to the level'sdyn_cleon(Intro cards). - One cast detail at the start (
0x0039d870, around0x0039e7dc): for a cinematic, a bound human of behaviour class0x12,0x14or0xbc(Cowboy's classes) with no hat object at+0x364gets the handle of a free object of its class's hat type (class+0x14c, looked up by name among the object manager's kind-24 objects and taken only if not attached), so a Cowboy who lost his hat may wear it again in the scene (inferred: the hat, Characters: where a hat comes from).
The names are labels (inferred from the disc): the 1,236 differently named headers hold 3,144 role slots under
689 distinct names, in several conventions: a model name without its underscores (warrrecv for warr_re_cv), the
same with a number (warrso01), two-letter initials (sw, ve, cb), a gang and a part (destso01, cops02) or
a first name (sanchez, mercy). Only 68 of the 689 equal a model name with or without its underscores. Object
definitions are named the same way (805 distinct names: props, s_ props, card names such as cleon).
The cast the scripts give (scratch script over the disc's compiled scripts, names and ids only; inferred, since
it is read from the code statically and a script may pick a table by checkpoint). 266 scene tables in the scripts
(the SuperRunScene table: SceneId, Humans, Objects, From Lua) cover 188 scenes. Following
each Humans entry through assignments to a HuCreate gives, of their 1,374 roles: 937 one character type, 112
two or more (the same name made by several HuCreate calls of a level, Warriors.Rembrandt as type 30 or 32 in
level99), 25 the player with no HuCreate in sight, 204 not resolved (globals set from calls the walk cannot
follow, hVermin, Ajax), and 96 not played (15 NilHandle, 81 beyond the list's end). Of the 937, the role name
equals the model without underscores in 142. Their 563 object slots: 346 an object type, 31 a door leaf, 41 not
resolved, 145 not bound. The result is the Cast and Props columns of Scenes and
movies; l99_c1 reads Cleon warr_cl_gen, Vermin warr_ve or warr_ve_gen, Rembrandt
warr_re or warr_re_cv, nobody, three soldiers warr_so_vb2_hr, warr_so_vb1_hr, warr_so_vb3_hr, and Ash
warr_ty, with props dyn_s_spraycan, dyn_rembrandt, dyn_vermin and dyn_intro_test.
A stand-in from the name alone (for a viewer that shows a scene with no script, not the original's behaviour):
taken in order, (a) the model whose name without underscores is the role's (with or without a trailing number), (b)
for one to three letters, warr_ and the first two, (c) the gang code of the first four letters with so in the
name, the gang's first <gang>_so model, (d) the gang's first model. It names a model for 3,029 of the 3,144 slots;
checked against the 850 script-resolved roles it covers, it gives the same person (same <gang>_<who> start) in
458, the right gang in 340 and another in 52. Inferred (measured on the disc).
Cast at runtime¶
Confirmed (runtime), PCSX2 2.9.94, 2026-10-11, with three hooks of repo:research/traces/patches/scene-casts.toml:
scene-bind-human at 0x00354248 (the store of Scene_BindHuman: the scene id, the role index, the role's name,
the human's name +0x80 and character type +0xd0), scene-bind-object at 0x00354340 (the same store in
Scene_BindObject: the slot's name, the object's type index +0x112 and type name through +0xc0) and
scene-find at 0x00353698 (every scene name lookup and its caller). Scenarios scene_cast_state (no input, from
a state passed with --state) and scene_cast_fence (slot 10, the charge through the wooden fence); thirteen
states in level99 and level80, 40 to 90 s each. A state saved during a level load stops the tick count for
longer than pcsx2 record's 5 s stall limit, so those runs called the recorder with a 180 s limit; the command
has no option for it yet.
l99_c1 (scene 2613), from a state saved as checkpoint 1 starts (the level load is still running; the binding
comes at update 5): seven of its eight roles bound:
| Role | Name in the scene | Human bound | Character type | Model |
|---|---|---|---|---|
| 0 | warrcl |
Cleon | 2 | warr_cl_gen |
| 1 | warrve |
Vermin | 27 | warr_ve_gen |
| 2 | warrrecv |
Rembrandt | 32 | warr_re_cv |
| 3 | (-) |
none | ||
| 4 | warrso02 |
Generic2 | 59 | warr_so_vb2_hr |
| 5 | warrso03 |
Generic1 | 58 | warr_so_vb1_hr |
| 6 | warrso04 |
Generic3 | 60 | warr_so_vb3_hr |
| 7 | warrashcv |
Ash | 40 | warr_ty_cv |
and its four object slots: s_spraycan01 ← dyn_s_spraycan, rembrandt ← dyn_rembrandt, vermin ←
dyn_vermin, cleon ← dyn_intro_test. So the model is the bound human's type, never the role's name: warrve
draws warr_ve_gen, warrso02 a _vb2_hr soldier, and a role nobody binds draws nothing. This settles two of
l99_c1's either-or roles (Vermin warr_ve_gen, Rembrandt warr_re_cv) and corrects the static walk for Ash,
who is type 40 (warr_ty_cv), not 38 (warr_ty); the scenes list now says so. A
checkpoint that makes these humans with other types would show other models; only checkpoint 1's start was read.
The subway scenes bind objects only: Coney_Sub2 (scene 5, level99, five runs) and Coney_Sub4 (scene 7,
level80) each get seven SceneAddObject calls from the script, slots 0-2 and 4-6 (s_train_low, s_train_low01
... in Coney_Sub2; s_train_med_ful ×6 in Coney_Sub4) all bound to objects of type dyn_s_train_med_full
(type index 1144), so s_train_low draws a med_full car: the slot name is not the type. Slot 3, train_sound, is
bound to an object with no world-object type (type index 0, no type name at +0xc0); what it is was not traced.
Which level a scene plays in¶
The scene list is global and no level owns a scene. scene_list.cnk is one list for the whole game
(SceneList_Load, 0x00353460), every .scn record is a file of the WAD, and a scene id is its index in that list.
A level uses a scene when its scripts name it, and every name reaches the scene system from a script. Confirmed (code)
at the three name lookups and their callers:
SceneList_FindBySubstring(0x00353698, the first record whose name contains the string) is called byScenePreload(0x00353f88),SceneIsPreloaded(0x00354b38), the fix-up (the record's own name),Goal_StartParlayandBrain_PlayScene(the parlay names<base>_000to<base>_004built from the script'sbase), andAddressTactic_Init(the nameTacticAddresswas given).- The exact-name lookup (
0x00353778) serves only the segment chain, and the lookup by id (0x00353730) onlySceneSlot_Load. The play bindings,GoalJoin*andGoalPlayAnimationtake ids that a script got fromScenePreload. - The one scene record read outside the scene list is a movie's caption timing,
<movie>_sub.scn, whichMovie_Playreads as a file and plays on its own (Movies).
So C++ code names no scene of its own, and GoalStartParlay and TacticAddress are never called by a script on
the disc (both have no call in any compiled chunk), so the parlay families (l14_p1_000 to _004, l5_m5_000 to
_010, ...) and any tactic scene are not reached by name in the shipped game (inferred).
At runtime every lookup came from a script. Confirmed (runtime), PCSX2 2.9.94, 2026-10-11, the scene-find
hook (Cast at runtime) over thirteen runs in level99 and level80: every call of
SceneList_FindBySubstring came from ScenePreload (return 0x00353f9c), each followed a few updates later by
the fix-up finding the loaded record's own name (SceneRecord_Fixup, return 0x003523fc); no other caller. The
names were l99_c1, l99_c9, Coney_Sub1, Coney_Sub2 and Coney_Sub4, each in the level whose scripts hold
it. The subway scenes are among them: level99's script preloads Coney_Sub1 or Coney_Sub2 (its random
binding is called just before, so it picks one) and level80's Coney_Sub4, so a *_Sub* scene is a level
script's like any other, and the ones no script names (Benson_Sub1, Train_Sub2, EConey_Sub1, ...) were not
seen loaded.
No computed name reaches the unnamed families (inferred, from the disc's compiled scripts, names only). A
script could build a scene name by joining strings, so every string constant of every compiled chunk was compared
with the scene names: the only constants that are the start or the end of a scene name without being one are
rumble_ (33 chunks, joined with a level number: rumble_102, above), L1_IN, L86_M,
turn_bd, level95.lua's l, _in and _out (movie and caption names) and level84.lua's _01 to _07
(its debug PlaySceneTest). None is a piece of an l<N>_l1_, _p1_, _s1_, _m2_ or _t<N>_ name, so those
families are reached by neither a literal nor a joined name; with GoalStartParlay and TacticAddress never
called, they are not played in the shipped game.
Where a scene plays is the world of the level whose scripts play it: cinematics and fixed scenes are played at the origin, so the records hold that world's coordinates (Playing). Evidence for each scene, from a scratch pass over the disc (names and counts only):
| Source | Scenes | Evidence |
|---|---|---|
a level's scripts hold the name as a string (any string constant, not only a ScenePreload argument: most go through a SuperRunScene table) |
309 | inferred (the scripts are the only way in, above); one is named by two levels' scripts (Pel_subway: level31, level115) |
the name is a scene marker in a level's Subtitles chunk (the captions come from the current level's .lev only, Movies) |
3 | inferred; 210 markers in 26 levels, and each of the 192 scenes that the scripts also name is marked in the same level; the others are the 11 caption timings below and these 3 |
a movie's caption timing, l<N>_in_sub / l<N>_out_sub |
11 | confirmed (code), Movies |
only a level's debug scene test (level<N>_scenetest.lua) names it |
3 | inferred |
a level number at the end of the name (rumble_102, wonderwheel_81; the Rumble levels' scripts hold the string rumble_) |
7 | inferred |
only the l<N>_ prefix |
663 | speculative, below |
none (shared animation sets such as gen_*, bar_*, craps_*; the subway scenes Benson_Sub1, Train_Sub2, ...; l4_* and l88_*, with no such level) |
244 |
The l<N>_ prefix names the mission, not the world. Of the 299 l<N>_ scenes whose level the scripts or the
subtitles give, 280 play in level N and 19 in the hub, level95 (l3_c1, l5_c1, l14_c6_a, l31_c1, ...):
mission starts and ends played in the clubhouse, at around (−170, 90, −193) and (−200, 130, −698) in the hub's world,
named by level95.lua and captioned in level95.lev. The scene soundtracks agree with the prefix, not the world:
each of the 213 scenes found in the sound stream table (an unnamed WAD entry listing speeches/<folder>/<scene>) sits
in the folder of its prefix (l3_c1 in l3).
So the prefix is a good guess (94 % on the scenes we can check) but wrong for a mission's clubhouse scenes, and the
663 scenes known only by it are marked speculative. A scene's position does not settle it either: the levels' worlds
overlap in coordinates, and placing each script-known scene at the level of its nearest script-known neighbour is
right for only 196 of 321.
The Plays in and Known from columns of Scenes and movies hold this for every scene.
Intro cards¶
When a cinematic introduces a character or a gang, a card with the name and a picture fills the screen for a second
or two (Cleon, Vermin and Rembrandt in l99_c1, the Destroyers in l80_c1). The card is a scene object: the
level script binds it to an object slot like any other (SceneAddObject), its own track keeps it parked about 10 m
underground (l99_c1's cleon at z −0.96 to −11.8 for its whole track, with no show or hide events), and a camera
event 73 lifts it in front of the camera. Confirmed (code) at the addresses below; the cards' names and places from
the disc.
- The event (
SceneTrack_Events,0x003553ac): the scene task is found through the current camera (aCam_Scene's+0x1e4,0x00353298), so only a scene with a camera holds a card;SceneTask_HoldObject(0x003a0e48) gets the slot from+4(lh, compared unsigned with the object count+0x21) and the seconds from+8. When the slot's bound object (object definition+0x50) is set, it stores it at task+0xd0and the end time, the game clock (0x0050b734 + 0x48, ms) plus seconds × 1000, at+0xd4; sets0x00512c44, which stops the room smoke's widgets drawing (OverlayEffect_Tick,0x0019bd24); and places the card at once. - Every update (
SceneTask_Update,0x0039d0fc, after the object runners have set their objects' poses and before the lights),SceneTask_UpdateHeldObject(0x003a0ed8): with+0xd0set and the game clock not past+0xd4(or+0xd40), and the header's camera count+0x22set, the card goes to the camera runner's world pose (task+0x70: scene rotation × sampled position + scene position, the rotations composed) moved along the camera's view axis (its rotation's +y,0x003363b0) by
d = k × aspect / tan(fov / 2)
with the fov the current camera's (+0x44, the scene lens's, without the progressive mode's added 3°), the aspect
the camera's (+0x4c, the game aspect: 1.3333 at 4:3, 1.6667 at 16:9, Graphics),
and k 0.5, or 0.3 when the device's 16:9 flag is on (device vtable +0xd0, +0x45c). The card turns as the
camera (object vtable +0x6c). Once the clock is past the end, +0xd0, +0xd4 and 0x00512c44 are cleared, and
the object's own track puts the card back underground from the next update.
A skip's flush drops event 73 (Skipping). Disc check (scratch script over the scene list): the 23 events are all on camera tracks, one to three a scene, held 1 to 2 s, and every slot names a character or gang card:
| Scene | Frame: card (seconds) |
|---|---|
l99_c1 |
280: cleon (1), 360: vermin (1), 1032: rembrandt (1) |
l80_c1 |
747: destroyers (2) |
l80_c4 / l80_c5 |
270: ajax (1.75) / 181: cowboy (1.75), 264: snow (1.6) |
l80_c6 / l80_c7 |
388: fox (1.5) / 24: cochise (1), 169: swan (1) |
l2_c1_a, l3_c2, l5_c4, l9_mintro, l11_c2 |
orphans, hihats, hurricanes, moonrunners, bopp |
l14_c1, l20_c6, l31_c5, l34_c4, l55_c1, l55_c5, l82_c2_a |
saracens and jsbs, huns, turnbull_a, furies, lizzies, punks, samo |
Skipping¶
Only a skippable scene (+0xe5), confirmed (code) at 0x0039cbf0:
- The task counts 60 updates (2 s) from its creation (
+0xe0) before a button counts. - Then, on any player's pad, cross (
0x0040) or START (0x0800) held (pad bits) skips: the caption is cleared, that player's view goes black at once (ScreenQueueEffecttype 1, 0 s), and the scene is stopped likeSceneStop(id, false). START also sets the chain-skip flag (0x0051489c + 0x56e4), so the next scenes played withchainskip at once without a button. - The stop ends every role's clip, first firing its remaining events (
0x003a0a68,0x00103e90; only when skipped,+0xe4); the scene then ends as below with the skip flag set. The end fires what is left of the object, light and camera tracks too, a reduced set, described next. - A looping scene with a loop point (
+0xec, Ending) is not cut short: the skip's stop only clears the loop, the task retries it each update and the scene plays to the end of its pass. Confirmed (code) at0x0039cbf0,0x00353a10.
The remaining track events on a skip (SceneTrack_Flush, 0x00355798). In the end (0x0039f450), only when
skipped, each bound object's track is flushed (before the object is placed at its end pose), then each light's,
then the camera's (before the camera is popped, and only while the scene camera's field +0x1e0 is 0, the case in
which it is popped at all; that field's meaning was not traced). The flush walks every event not yet passed, in
track order, regardless of its frame, and does only these; confirmed (code) at 0x00355798:
| Type | On a skip |
|---|---|
| 24 | message 0x12 (show) to the object, unless the next event of the track is a 25 (the code means to look further ahead but compares the same event each time) |
| 25 | message 0x13 (hide) |
| 27 | fade out on player 1's view at once (ScreenQueueEffect type 1, 0 s); the caption is not touched |
| 28 | fade in at once (type 0, 0 s) |
| 31 | calls the scene's end function now (0x003a0da8), once per pending event, as in play |
| 74 | the coloured fade, at once |
| 76 | sets each player's rumble strength (min(255, +6 × 25.5), pads with a motor only), which the camera's pop a moment later sets back to 0 |
Every other type is dropped: sounds (13, 14, 71), lens (26), loop point (29), light colour (30), particles (33),
captions (41), object and car actions (69), holding an object before the camera (73). A type-31 event in the flush
finds its scene through the current scene camera (0x00512c7c + 0x850, +0x14, the camera's scene id +0x1e4),
without the in-play fallback to the object's scene slot; the pointer is cleared (0x003a1558(0)) only after the
camera's flush, so every flush of a scene with a camera finds it.
So on a skip the order is: the role clips' remaining events (at the stop); then, in the end, humans placed, each
object's pending events and placement, the lights' and the camera's pending events (the type-31 calls among them,
in frame order), the camera popped, the letterbox out, brains resumed, and the end function called once more
with the scene id (step 7). A scene whose tracks hold n pending type-31 events therefore calls its end function
n + 1 times, the last one after the camera is back. Confirmed (code) at 0x0039f450, 0x00355798; not yet seen
at runtime (no save state reaches level80).
level80's intro, skipped (inferred from the code above and the scripts, read as bytecode). level80_chapter1
preloads l80_c1 at checkpoint 1 and, once it is loaded, runs SuperRunScene with it already loaded, seven humans
(Cleon, Rembrandt, Vermin and four scene-only Destroyers), the clubhouse's two doors and a Destroyers object as
objects, NoClearWanted, NumCallBacks = 3 and its ReturnFunc (an EndIntroScene). That function, given 3, 2
or 1, removes one of the clubhouse's three glass panes (ObjDestroy: the pane vanishes, with no shatter,
Objects) and sets off a molotov explosion at the matching flag (ObjSpawn("dyn_molotv") there and
BreakObjectsInRadius(molotov, 0.5), which makes the molotov break itself:
Script types: the Molotov); given anything
else (the scene id), it closes the doors, removes the scene's Destroyers gang and goes on to the chapter's next setup.
The three type-31 events on the camera track (about 21.7 to 22.1 s) are flushed by a skip, so PreCashTheWorld runs
three times (ReturnFunc(3), (2), (1), NumCallBacks down to 0, each pane and explosion at once), then a
fourth time from step 7, which calls ReturnFunc(id) and queues ScreenQueueEffect(0, 0.5): the screen, black
since the skip, fades in over 0.5 s. Neither global.lua nor the level's scripts look at whether a scene was
skipped: no scene-state query, no skip callback, nothing zeroes NumCallBacks; the engine's flush is what keeps the
count right. (level80.lua also builds a looping l80_c1 table, but only in its SCENETEST debug path.)
Ending¶
State 7 (all clips done, or a part ended with no roles) calls the end (0x0039f450). Confirmed (code):
- Cinematic: scene state
0x410= 0; system music back on when step 6 turned it off. - Each bound human: its sounds stopped; when skipped,
+0x280= −1 and it is placed at its role's end pose (in the scene's space, throughHuman_SetTransform, so a human still in a grab is let go byHuman_BreakPairand its partner plays a reaction clip, Combat); its join goal (types0x27-0x2a) is popped, which gives a player back control (Humans); unbound. - Each object: when skipped, placed at its end pose; then its scene slot (
+0x110) is −1 again, its sound stopped, its body flagged (0x80000000) and added back to the world, and its record unpinned (SceneAddObjectpinned it and refuses an object already in a scene). The object stays where the scene left it; its spawn record's pose is written only when the object is stored (streamed out or its zone turned off), so it keeps theObjSpawnpose until then. The abort (0x0039ec60) does the same, always placing at the end pose. Each light: released. Confirmed (code); at runtime (slot 1, after lesson 9) the three bats' records, no longer live, held poses on the ground near the pen, not their spawn poses 2.16 m below it, so storing wrote the objects' last places (where the player left them, which may not be the scene's end poses). - The camera: popped and made current over
BlendCamseconds, the scene camera released and the cameras updated once with 0.17 s (Camera). - Cinematic: letterbox out on every player's view (type 3, 1.5 s), which shows the HUD again once the bars
are out (HUD); when a player was bound (or player 1's
brain
+0x2e4is set), player 1's command menu unlocked, follow dispatched to his gang, forced and silent, and one gang update run (The player's gang). - With
freeze, every brain is resumed, including any a script had suspended. - State 8; the end function is called with the scene id (
0x003a0da8, only while scripting runs,0x00512b28= 1). Forlevel99this isPreCashTheWorld, which callsReturnFuncand fades in over 0.5 s. - Cinematic and not
final: preload the world around the camera again, loading<scene>_end.pakwhen it exists. - When skipped and
BlendCam> 0: fade in overBlendCamseconds.
The scene soundtrack is stopped by the end only when the scene was skipped, and by the abort only for a
cinematic; an unskipped scene's soundtrack plays on to its own end or until the next scene's preload stops it
(Sound: scene soundtracks). Confirmed (code) at 0x0039f450, 0x0039ec60; confirmed
(runtime) for an unskipped level99 scene. Neither the start nor the end silences the world's sound: a cinematic only
moves the listener to the scene camera and lowers some sounds while +0x410 is set, and the hub's scripts stop the
ambient bed themselves (Sound: world sound during a scene).
The next update sees state 8 with no humans left and frees the task and unloads the slot (0x00353bf0,
0x00351da0): one user less, and with no user left the slot is emptied, so a scene is loaded again for its next
play (Loading). Confirmed (code).
SceneStop(id, force) (0x00353a10; the binding 0x00367e20 reads force with default false): a starting
scene goes straight to state 7; a playing one with roles has its clips ended (state 6 while a role is in a paired
move, retried each update); without roles it goes to state 7 at once, mid-pass. Only a scene whose task has the
loop-point flag (+0xec) and is not forced just stops looping instead (0x003a0be8 clears the runners' loop bit
0x4 and the roles' clip loop) and ends after the current pass. The start sets +0xec = looping and at least one
bound track or role clip has event 29 (loop point, 0x0039d870); a plain looping scene without event 29 therefore
stops at once like any other. Confirmed (code). WonderWheel_100 has no roles and no event 29 (disc: its 29 object
tracks and camera track carry only events 24 and 25), so a non-forced stop ends it at once: at runtime (PCSX2 2.9.94,
slot read over PINE) it went from state 5 to 7 with its camera track at 10.77 s of the 20 s pass.
The player's gang¶
What a cinematic (task +0xed) does to player 1's gang, the Warriors, in order. Only player 1's gang is touched
(game state 0x0051489c + 0x228); a fixed scene or an animation does none of the engine steps below, only the
reset at the join of the humans it binds. Confirmed (code) at the addresses given; not checked at
runtime.
Before the scene (the scripts, SuperRunScene, read as bytecode): unless NoClearWanted, the
Warriors' wanted state is cleared (GangClearWanted(GangWarriors): police neutral to them again, the wanted HUD
reset) and every responding gang dismissed (GangClearResponders(): non-police responders deleted, police stood
down, bindings); each bound human of gang type 0 is un-arrested
and revived. Then each bound Warrior is reset as it joins: his grab, tackle or mug ends, and
every player's rage ends, bound or not.
At the start (SceneTask_Start, 0x0039d870, step 0 → 1 of the global scene state, once per cinematic):
Gang_ClearBums (0x0016b8d0) walks the 16 member slots of player 1's gang (his brain +0x20c, gang +0x48) and
destroys every member whose class byte +0x11b is 6 (Human_Destroy(member, 0)): the bums the player has
taken into his crew. A class-7 bum and every other member stay. Nothing else in the start touches the gang:
members that are not bound keep their brains and their tactic (the follow tactic, usually) through the scene unless
the scene freezes (Starting step 9; SuperRunScene's scenes do not), and they are neither moved nor
hidden. The same Gang_ClearBums is the GangClearBums binding.
At the end (SceneTask_End, 0x0039f450, step 5 of Ending), after the camera is popped and the
letterbox goes out, and before the end function is called:
- Each bound Warrior's join goal is popped (step 2), which gives a player his pad back and an AI Warrior his brain.
- Only when one of the scene's bound humans was a player (human
+0x1b0not −1), or else player 1's brain+0x2e4is set (a Warrior hitting back at his chief, AI): that player's command menu is unlocked (game state+0x42e+ player = 0), and command 0, follow, is dispatched forced for player 1 (WarriorCommand_Dispatch(state, player 1, 0, 1, 0, 1)at0x0039ff5c). A forced dispatch is silent, still refused while commands are locked or follow is disabled, and otherwise clears the gang's tactic and shared target, records follow as the last command and builds a new follow tactic (AI: commands). - In the same case, one gang update is run at once (
0x00166708(gang, 0)at0x0039ff70; Ghidra calls itGang_CachePlayerInfo): with 0 it skips the spawner step, refreshes the cached player position, runs the new tactic once (Tactic_Process, which starts the follow tactic and hands out its goals) and the wanted level's update (AI: the gang update). It is not an "alert clear" as this page used to say. - With
freeze, every brain is resumed (step 6).
A cinematic given up (SceneTask_Abort, 0x0039ec60, after 10 s without starting) does less: fade in at once on
every view, then follow dispatched forced for player 1 (0x0039f3f0) without the menu unlock, the player test or
the gang update, and the HUD shown again.
So after any cinematic in which the player took part, the crew is back under a fresh follow command with no target, whatever the command was before (attack, hold, scatter): a scene resets the player's last command. The wanted state is cleared before the scene (by the scripts) and not restored.
Timing¶
- Scene time is in 1/30 s frames; the task and its runners advance on the 30-a-second update, the roles' clips on the animation clock (Animation). Confirmed (code).
- While the scene camera is current the game clock follows real time without the 40 ms clamp (Boot), so a slow frame is not stretched out. A fixed-step Coney plays the same frames.
SceneLengthreports the first part's duration only (0x003540b8), not+0x84. Confirmed (code).
The scene test (SCENETEST, PlaySceneTest)¶
The level scripts carry the original developers' scene viewer: set the Lua global SCENETEST to a scene's name
and load a level, and instead of its mission the level spawns that scene's cast and plays the scene on a loop. It is
dead in the retail game: global.lua's first statement sets SCENETEST to nil and no script or engine code sets it
again (Debug). Everything here is confirmed (code), read as bytecode from the
disc's chunks (global.lua, the level scripts and the *_scenetest chunks), unless marked; names and counts only.
The switch. A level whose scripts have a scene test reads SCENETEST where it would start its mission
(level99's RunLevel, most others' RunMission or Main call). When it is set:
- Thirteen levels load a separate chunk:
level2,level5,level9,level11,level20,level31,level34,level51,level54,level83,level84,level87andlevel99. The level makes its Warriors (inlevel99,AddWarriors(), Rembrandt asplayerand Ash asplayer2;level31picks one of two Warrior sets by the scene name), thenpreLoadFile("<level>_scenetest", <entry>)andAddCameras(), and skips everything else of its start: no checkpoint chapter, no start-game callback (level99'sStartAmbient), no mission. The entry the chunk defines and the level names isPlaySceneTest, orPlaySceneinlevel9,level54,level83andlevel87, orPLAYSceneTestinlevel20. - Ten levels define the test in the level script itself and call it instead of their mission:
level3,level14,level52,level55,level80,level81,level82,level86,level92andlevel93. Alevel86_scenetestchunk is on the disc too, with the same five names, but nothing loads it. - The hub,
level95, hasLoadSceneTestinstead: for 19 names (the scenes played in the clubhouse, such asl34_c5_aorl51_c11) it sets the hub chapter that holds the scene as the checkpoint, loads that chapter, and its set-up then runs the chapter's own cutscene action whoseSceneIdisSCENETEST(story.RunMissionAction, afterScreenQueueEffect(0, 2)), or, withCfgSceneTables, schedulesDoQuickMapinstead. So the hub plays the scene through its real mission action, once, not looped.
The dispatcher. PlaySceneTest (or its other name) compares SCENETEST with each scene name it knows, in turn,
and calls that scene's play function; a value that matches none (SCENETEST = 1, say) plays nothing and leaves the
level without a mission. Across the 24 levels there are 196 scene names, every one a scene of scene_list.cnk
(checked against the scene list). Two are unreachable: level20's list has "l20_c6 "
with a trailing space, and level5_scenetest defines Playl5t9010 but its dispatcher never calls it.
A play function sets up the scene the way the story does, from the level's own spawners: it calls the same
Add… functions the chapters use for the scene's cast (level99's PlayL99C1 calls AddCombatTeacher and
AddCombatWarriors), sometimes spawns or enables objects (ObjSpawn, ObjEnableZone), builds the
SuperRunScene table with the story's SceneId, Humans and Objects in the story's role order
(level99's l99_c1 table matches IntroScene's), and calls SuperRunScene. What differs from the story:
Looping= true (1 inlevel9,level54,level83andlevel87) in all but a few: the scene plays again and again (Objects, camera and lights: a looping scene restarts its pass, the role clips looping with it).level86's five do not loop. A few in-level tests keep a story return function while looping:level80'sRunIntroSceneforl80_c1(itsEndIntroScenewith three call-backs, Skipping) andlevel93'sRunFinalSceneforl93_c3(EndFinalScene).- Otherwise the end function does nothing: the chunks'
EndScene(id)andSceneFinished()are empty, and many tables set noReturnFuncat all (PreCashTheWorldskips a missing one). Nothing follows the scene. Preloadedis false (level99,level34,level52):SuperRunScenepreloads the scene itself.
What global.lua does differently while SCENETEST is set: gPlayCutScene calls CfgSubtitles(SCENESUBTITLES)
before every play, and SCENESUBTITLES is 1, so subtitles are forced on. global.lua also sets SCENELANGUAGE
= 0 and calls CfgLanguage(SCENELANGUAGE) when SCENETEST is set, but only at its own load, right after setting
SCENETEST to nil, so that call never runs.
How it ends. A looping scene with a loop point (event 29) plays to the end of its pass when skipped; one without ends at once (Skipping, Ending); either way the empty end function runs and the level then sits with no mission (inferred: no script is left to run but the level's ambient set-up).
For a debug entry (inferred): the developers' review path is "load the level with SCENETEST = the scene's
name". A faithful debug entry sets the global after global.lua has run and before the level's start function, as
Debug describes, and lets the level's own test path spawn the cast and play the scene
with the story's bindings. It needs the level script and, for the thirteen chunk levels, preLoadFile of the
*_scenetest chunk; the scene then loops until skipped.
Disc check¶
coney-tools wad scenes (2026-10-06, NTSC-U): scene_list.cnk sha256 f97e241f…190b927e; 2,765 records parsed
(1,240 headers, 1,525 segments; the 24 cut names found by content), none failing; 3,153 roles, 2,352 objects, 512
cameras, 21 lights, 405,373 frames (3 h 45 min); every segment chain resolves; the header's frame count equals its
parts' durations in all but 2 headers that have no tracks. Records sha256 0f465735…c1e979ef.
Subtitles¶
A scene's subtitles are captions of the caption system (0x00619570, which is HUD +0x18d30), the same one the movies
use: their text is the level's Subtitles chunk, found by the scene's name, and they are drawn by Captions_Draw
(Movies: captions has the chunk, the record kinds, the subtitles option and the drawing). What
is particular to scenes, confirmed (code) unless marked:
- Selecting.
SceneTask_Create(0x003a13d0) callsCaptions_SelectScenewith the header's name (+0x08), so a scene's captions are those of the kind-1 record with its name in the current language's section. With no such record the previous selection stays (0x001cad38restores it). - Timing. Captions follow the scene's own track events, type 41 on the camera track (only there on the disc):
+4= 0 shows the next record (Captions_Next,0x001cb190), 4 hides the caption, 5 sets the ordinary kind without changing the text, 6 clears the fade flag below and shows the next. Events fire when the track runner passes their frame (0x00354d98), so captions run on the scene's 30-a-second clock and stop when the game is paused, while the soundtrack streams on its own from event 13 (Sound). A caption has no duration of its own: it stays until the next type-41 event, a type-27 fade-out, or a skip. A scene without a camera shows none. - Fade-out. A type-27 event hides the current caption first when the flag
+0x4cis set (Captions_SelectScenesets it; an event 41 with+4= 6 clears it so that caption outlasts the fade), then sets the flag again. - Skip. The skip clears the caption (
Captions_SetKind(4)at0x0039cbf0, Skipping); the end (0x0039f450) does not touch it. - The option. Ordinary captions (kind 3) show only with the subtitles option on (
W_GameState + 0x438, thePM_Subtitlesscreen, Front end); a kind-2 record always shows. - Position and letterbox. An ordinary caption is centred at x 0.5, y 0.75 of the screen in the default mode, grey
(178, 178, 178, 255), wrapped at 0.7 of the width (Movies). The letterbox's bottom bar covers the bottomlevel × 0.12of the screen (Graphics), so the caption sits above it. The overlay pass (0x00156658) draws the captions after the screen effects and the HUD, so they are on top of the bars. While the letterbox is in or moving (screen effects+0x1e8or+0x1ecnot 0)HUD_Renderdraws no HUD at all (HUD), so nothing else competes with a scene's captions. - Speech in play has no subtitles. The only callers of the caption functions are the scene task, the track runner,
Movie_Playand the title card below;SoundPlayCommand,HuSpeakand the voice lines never reach them (Sound).
The title card¶
A scene whose section starts with a kind-2 record opens with a title card, a place-and-time line (two lines with
<CR>) drawn in the screen's centre at 1.2 times the text size in dark red, shown whatever the subtitles option says.
Captions_WaitTitleCard (0x001cae60), called every start attempt (step 8 of Starting) until it returns
true:
- When the selected scene is not this scene's name, or no record is next: ready.
- If the next record is kind 2, show it (
Captions_Next;Captions_SetKind(2)sets+0x54and+0x58). - With the game not paused: when
+0x54is 0 the card is over: hide the caption (kind 4) and return ready. Otherwise clear+0x54, stamp+0x5cwith the real clock and freeze the game for 5,000 ms (GameTimer_Freeze(timer, 5000, 0), Boot); not ready.
So the card holds the scene for 5 s of frozen game time; the freeze cannot be ended with the button while a kind-2
caption shows (+0x58), and Captions_Draw fades it in over the first 1.5 s and out over the last 1.5 s, all before
the letterbox comes in (step 9). Disc check (scratch script over the 25 levels with a Subtitles chunk, English
sections): 23 of 209 scene sections open with a kind-2 record and none has one later; in level99 it is l99_c1, the
intro.
Disc counts (the same check): 1,422 type-41 events, all on camera tracks (+4 = 0: 1,228; 4: 186; 5: 5; 6: 3). In
202 of the 206 scenes that have both a section and a header, the number of show events (0 and 6) equals the section's
ordinary captions; the other 4 are a movie's caption scene listed twice and three scenes with captions but no events
(one without a camera). 12 scenes have show events but no section; in them a show would take the record after the
previous scene's (inferred from 0x001cad38; l99_c4 is one, not checked at runtime). Following the events, 11 scenes
end with a caption still current (inferred: it stays on screen after the scene until something hides it). An ordinary
caption is current for 8 to 546 frames, median 89 (3 s).
Coney's implementation¶
repo:src/scenes/ is the format and the player, pure and deterministic: scene_record.* decodes header and segment
records (role clips through the animation decoder, keyed tracks, events kept as stored), scene_list.* the scene list,
scene_cache.* the 12 slots (requests, eviction, the callback on arrival, the two segment buffers), scene_player.*
the scene task and the system the bindings work on, and scene_host.h what a scene asks of the game (humans, objects,
camera, lights, screen effects, captions, sounds, rumble, brains), which the play mode implements. scene_disc.* reads
scene_list.cnk and <name>.scn from the WAD. The bindings are repo:src/scripting/scene_bindings.cpp (the scene
bindings above and the three GoalJoin* bindings that bind a role); global.lua's SuperRunScene path runs on them
unchanged. The Python reader for the disc check is repo:python/src/coney_tools/scenes.py.
Disc test ([disc][scenes], counts only): l99_c1 and l99_c5 load with their 5 and 1 segments and play headless
with the roles level99_combat.lua binds: 2,026 and 500 updates playing, every bound human on its start mark (to
0.1 mm) at frame 0 and within 2 cm of its end mark at the end (on it, after a skip), one soundtrack prepared and started
each. [disc][story][audio] plays level99's checkpoint 1 with the sound mixed offline: l99_c1's soundtrack plays on
a real voice for its whole 2,026 frames, and the mix is audible through every one of them. [disc][scenes][audio]
then plays the other ten l99_ scenes with a soundtrack in turn, one to 430 updates after the last cinematic ends
(at least one while the last soundtrack still plays, so the new one is pending): each starts on a real voice and is
heard through its scene.
[disc][story][combat][scenes] plays checkpoint 1 through lesson 6, whose power move still holds a bum as l99_c7
starts (1 human holding, 1 held), and finds no human holding, held or grabbed 90 updates after the scene.
Coney choices where the page is silent:
- A record arrives on the update after
ScenePreload(Coney's reads are synchronous); a play binding reads it at once. - The bindings work on the scene system the binding context holds at each call. Gameplay makes one per level, over
the disc's scene list (read once), before the level script runs, and drops it when the level ends; the front end
makes one the same way for
level100, hosted by its world (Front end). With none (a test)ScenePreloadand the three play bindings are a stand-in that keeps the scripts' scene flow moving: a scene loads and ends at once, its load and end functions called with its id at the scripts' next update. - Coney runs a level's start callback before it loads the level (
StartAmbientbindsl99_c1's roles then), so a host attached later is told of the humans already joined, at their roles' start marks. - The join goal's brain switch is done for player 1 only: his brain is off from the join (
BrDead(brain, 1)) until the scene lets him go, when the goal's destroy turns it on again (BrDead(brain, 0)). This is what giveslevel5's boss fight the pad back in stage 3: the stage-2 callback switches the brain off and relies on the scenel5_c8to turn it on. An AI human's brain is not switched, and aBrFlushthat drops the join goal before the scene ends does not give the pad back. ScreenQueueEffectalso goes to the scenes' host in play: the stage owns player 1's view's fades and letterbox, which the scene events use too (the original's effect managers are the views', Graphics).- A role is driven from its clip alone: root motion (section A turned by the heading, all three axes; the host may
settle the feet on the ground) and the 21/22 marks, which the data shows hold
f32positions and headings. Event 11 is the human's animation sound, as in play (Sound): footsteps, shuffles and cloth during the scene. Clip events other than 11, 13, 21 and 22 are not acted on. - Keyed rotations are slerped; the camera looks along its rotation's +y with +z up (inferred:
l99_c5's first camera key aims +y at the roles). - Event 30's colour is read as bytes r, g, b at
+8(the cone's float is at+0x10); 69 does nothing. - Event 73 holds its intro card as Intro cards says (
heldObjectPose, tested inconey_tests "[scenes]"); the host says whether the game is 16:9 (Coney's play is 4:3 so far). Coney does not stop the room smoke while a card shows. Before this, the cards stayed parked underground and never showed. - A scene with roles but none bound ends when its tracks do; an aborted scene's end function is not called.
- A track's events fire against the scene's frame (the part's start frame plus its time), the header's and the current part's alike.
- A skip flushes the tracks as Skipping says: in the end, each bound object's pending events (then its
end pose), each light's, then the camera's before its pop, only types 24, 25, 27, 28, 31, 74 and 76, fades at once,
and a rumble set back to 0 by the pop; a looping scene with a loop point plays out its pass. Before Coney did this,
level80's skipped intro stayed black for the rest of the level:PreCashTheWorldfades back in only once its threeNumCallBacksare spent, by the camera track's type-31 calls.
The play mode's stage (repo:src/platform/scene_stage.*, repo:src/platform/play_level_scene.cpp) is the
SceneHost the play mode gives the scene system. It draws the scene camera's view (interpolated between steps, cut
when it jumps more than 1 m), the letterbox (two bars 0.12 of the screen high,
Screen effects; the Display/Cutscene letterbox setting leaves them out) and the fades
over the frame. In a level the scene camera is player 1's (Camera): the scene's start pushes the camera
shown, its keys set the scene camera's view and its end pops the camera back over BlendCam seconds (in the sandbox,
with no cameras, the end is a cut). The player and the level's cast (the humans its scripts made) are posed from
their roles' frames and, when let go, stand where the scene left them (a cast human placed as a spawn places it),
placed at the release itself so the end function's own moves (level87's TeleportToFlag) win (Ending);
that placement breaks any pair the human is in, its partner playing its reaction (Combat).
A player or cast human that is grabbing, grabbed, mounting or mounted at the start is left out as Humans
says (the stage's free test, SceneHost::humanFree): not taken in or posed, its grab going on, and at the end placed
only when the scene was skipped. Other bound humans are drawn as puppets of their characters. Sounds go through
the sound engine
(Sound): the soundtrack is prepared on the scene's load (or, pending a stream pair, by a
later sound update), a cinematic's start waits for it, event 13 starts whatever is prepared, and only a skip or a
cinematic given up stops it; otherwise it plays on past the scene's end. The music ducks to 0.75 while a cinematic
runs. The log says scene sound: <hash> prepared (or pending) and scene sound: started; events 14 and 71 play at
the human the scene holds, else unplaced on the effects bus. Coney stand-ins:
captions, particles and rumble are counted; a puppet's model follows its role's name (warrcl is Cleon's warr_cl,
and so on); a cast human keeps running its brain while a scene poses it.
--scene NAME with --play-level plays one scene at once (Running Coney).
In play (--play-level level99, headless): at checkpoint 1 StartAmbient runs SuperRunScene(IntroScene), and
l99_c1 plays through the scripts with its seven bound humans posed on the stage; it ends after 2,026 frames, calls
P1.StartTraining, whose fade-in reaches the stage, and Rembrandt stands at (−289.03, 120.29), as in the runtime
trace above.
Open questions¶
- Which human state the start resets (answered: small resets, Humans; a grab is never released by it).
Messages
0x95/0x96set the human's transform from the scene's space (Human_HandleMessage,0x00245920), which breaks a pair (Combat); the start's message 10 goes to the human's held object, not the human. What the object at human+0x360is (message0x15at the start). - How
ScenePlayAnimationfits the scene to its humans (0x003547e8), and the 0.1 at0x0039d618. - What the camera definition's
+0x50,+0x5c,+0x68and the header's+0x30name are for. - Track events 24/25 (messages
0x12/0x13), and most clip event types besides 9, 11, 21, 22. - Where a scene's captions come from (answered): the level's Subtitles chunk, by the scene's name (Movies).
- Event 30's argument layout (colour at
+8and cone at+0x10overlap as three floats). - How a role's model is decided (answered: by the bound human's character type; the name is a label, Cast and
props, confirmed at runtime for
l99_c1, Cast at runtime). Still open: the 204 roles and 41 props of scripted scenes the static walk does not resolve, and which of two or moreHuCreatetypes a level's checkpoint picks in the other either-or roles (thescene_cast_statescenario reads them from a state saved before each scene starts; onlylevel99andlevel80states exist). What the object bound to the subway scenes'train_soundslot is (no world-object type). - Which level a scene plays in (answered for the 333 scenes a script, a Subtitles chunk or a movie places,
Which level). Open: the 663 known only by their prefix and the 244 with no level; whether the
never-named
l<N>_families are reached by a computed name (answered: no, Which level).