Workouts (the hangout's gym)¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). Script claims come from the
compiled bytecode of level95.lua (worksetup), level95_workout.lua (workout) and global.lua (the
Unlockables list). Runtime claims were made in PCSX2 2.9.94 (2026-10-11) on a copy of quick-save slot 1 with the
scripted-pad, call-brains, workout-rep, workout-end, tutorial-callback and attack-score patches
(patches), switched to level95 checkpoint 2 by calling SetCheckPoint's worker
0x0041abc0(2) and MenuLoadLevel's worker 0x00160d78("level95") as on AI; the player (Cleon
at that checkpoint) was put beside each station by writing his transform, and triangle, cross and square were pressed
through the patched pad bytes, one update per press. Coney claims were made with the release build of 2026-10-10.
Purpose¶
The clubhouse gym of level95 has four stations. Three are cycle games (chin-ups, push-ups, sit-ups): the player
pumps cross to keep a loop clip going, and each loop is a repetition. The fourth, the heavy bag, is a combo
game: the game calls out a short combo, the player lands it on the bag, against a clock. Finishing a set raises the
station's rank (0-10), kept as an unlockable; ranks 5 and 10 at all four stations unlock the power and stamina
upgrades. This page is what an implementer needs to rebuild all four, with the reason Coney's start does nothing today
(Coney) and the fix list and test plan (Notes for implementers).
The stations¶
worksetup.workFlag is the station's index 1-4, the same index into fStartWorkout (where the prompt is),
fWorkout (where the player works out) and fWatchWork (where a Warrior using the station is sent). The flags are
made in level95.lua's set-up; positions in metres, z up, heading in the flag's degrees. Confirmed (code), the
positions also seen in Coney's event log.
| Index | Station | fStartWorkout |
fWorkout |
fWatchWork |
Game | Unlock offset |
|---|---|---|---|---|---|---|
| 1 | Heavy Bag | (-174.8, 106, -197.5), 93 | (-175.5, 106, -197.4), 93 | (-182.3, 100, -197.5), 62 | combo | 11 |
| 2 | Chin Ups | (-181, 106.4, -197.5), 2 | (-181, 105.7, -197.5), 2 | (-181.9, 111, -197.5), 112 | cycle | 31 |
| 3 | Push Ups | (-174, 109.6, -197.4), 182 | (-174, 109, -197.4), 182 | (-174.1, 101.1, -197.5), 2 | cycle | 41 |
| 4 | Sit Ups | (-177.2, 109.5, -197.4), 182 | (-177.2, 108.7, -197.4), 182 | (-179.5, 99.5, -197.5), 22 | cycle | 51 |
The gym is on the clubhouse's lower floor (z ≈ -197.6), below the main room (z ≈ -194.4). The bag itself is a human,
HeavyBaga (character type 460, gang HeavyBag), made at fStartWorkout[1] by level95.lua's AddHeavyBag;
worksetup.ConfigHeavyBag makes it brain-dead, unpushable, ungrabbable, ungroundable, unstunnable, in god mode, with
no money and its look blocked. While no player uses the bag, the Warrior at fWorkout[1] spars with it through
GoalBoxer (Boxer). Confirmed (code).
Each station's set-up (workout.Setup, by workFlag), confirmed (code):
| Station | CfgWorkoutParams gain / decay / hold |
repTable (ranks 1-10) |
timeTable s (ranks 1-10) |
Clips (enter, exit, slow, mid, fast) |
|---|---|---|---|---|
| 1 bag | none | 10 10 10 10 10 15 15 15 15 15 | 26 24 22 20 18 34 32 30 28 26 | none (the player's own attacks) |
| 2 chin-ups | 0.04 0.04 0.03 / 0.03 ×3 / 3 2 2 | 5 5 10 10 10 15 15 15 20 20 | 0 12 22 20 18 33 30 27 41 36 | chinups_enter, _exit, _slow, _mid, _fast |
| 3 push-ups | 0.02 0.04 0.04 / 0.03 ×3 / 3 2 3 | as chin-ups | 0 12 20 18 16 29 25 21 31 26 | push_up_enter, push_up_e, push_up_slow, _mid, _fast |
| 4 sit-ups | 0.1 0.06 0.03 / 0.03 ×3 / 3 2 3 | as chin-ups | as chin-ups | situps_enter, _exit, _slow, _mid, _fast |
A time of 0 means no clock (rank 1 of the cycle games). Every station call passes a fourth argument of 1.8 to
CfgWorkoutParams (Parameters). The three cycle stations also make no camera of their own beyond
workout.cam; the bag makes three (The heavy bag). Each station has one locked camera workout.cam
(positions in Camera).
Starting a workout¶
- The prompt.
worksetup.EnableWorkouts(true)gives eachfStartWorkoutflag a message-0 handler with a hint (SetMsgHandlerEx):worksetup.StartWorkoutwhen the station's rank can be raised, elseworksetup.NoWorkout(which does nothing); the hint shows the station's name andRank: n/10(Ranks). The context action is triangle within the action distance, whichvbWorkout_01's enter and leave handlers set to 1.8 m and back to 1.1 m (CfgActionDistance(1, ...)). Confirmed (code); confirmed (runtime): the hint shows beside each station. worksetup.StartWorkout(flag, human), for a player only:workFlag= the flag's index; clears the flag's handler withSetMsgHandler(flag, 0, nil), which also unregisters the flag's context record (Script_SetMsgHandler,0x00386298, calls the object's+0x12cfor message 0 whatever the callback; a flag's isFlag_UnregisterContext,0x00415d90), so the station's prompt and hint leave the screen at once;HuDropWeapon; when a Warrior occupiesfWorkout[k], swaps him tofWatchWork[k](on difficulty 3 he also drops the bat he was given); the player says voice line 201 (workout);DeactivateWarchief; the player's message 1 (damage) goes toworksetup.PlayerTakesDamage;fWorkout[k]'s message 8 (arrival) toworksetup.DoWorkout;GoalMoveToFlag(player, fWorkout[k], 2, 0, 0, 0.5, 0, true, false)(walk, radius 0.5 m, face the flag's heading);ScheduleFunc("worksetup.TimeOut", 5000); loadslevel95_workoutwithworkout.Setupas its callback (or callsworkout.Setupwhen it is loaded). Confirmed (code).- Arrival.
worksetup.DoWorkoutclears the player's damage handler and the arrival handler and callsworksetup.RetryWorkout, which callsworkout.StartWorkoutonce the file is loaded (else retries every 250 ms). If the player has not arrived after 5 s,worksetup.TimeOutquits (workout.ForceQuit, orActivateWarchiefbefore the file loaded); damage on the way quits throughworksetup.EarlyOut. Confirmed (code). At runtime the walk took 12 updates (action code 3) and the workout's first update came 34 updates after the triangle press. workout.StartWorkout: flushesworksetup.TimeOut;HuSetPushable(player, false); the player's damage message 1 toworkout.PlayerTakesDamage(which times the workout out);rankPassed=worksetup.BuildWorkoutMessage(type, false)(the rank being attempted), capped atcheckpoint; givesfWorkout[type]a message-0 handler withSetMsgHandlerEx(fWorkout[type], 0, f, nil, hint): no prompt, only the second text (the station's name, rank and the "press triangle to quit" line), its functionfworkout.ForceQuitfor the bag andworkout.NoHandler(nothing) for the cycle games (Triangle quits the bag); cuts toworkout.cam(curCam= 1);TeleportToFlag(player, fWorkout[type], -1)(the player stands exactly on the flag, facing its heading); calls the station's start function (CalloutCombos01for the bag,StartCycleGamefor the others). Confirmed (code).
The cycle games¶
The script side¶
StartCycleGame:HuSetWorkoutCallbacks("workout.Started", "workout.DoRep", "workout.CycleQuit");HuWorkout(player, player, enter, exit, slow, mid, fast)with the station's five clips;ScheduleFunc( "workout.StartTimeOut", 1500), which stops the workout and quits ifworkout.startedis still unset. Confirmed (code).Started(h)(the engine's start callback): for a human other than the player,HuStopWorkout(h); for the player:started= true,ActivateWarchief,StartHUD(HUD),HUDTurnOnActionCycleAnim(4, 0.1, 6, 79, 79)(the blinking cross icon, HUD),HuLockPadMovement(player, true),HuSetWorkoutBlend(player, 1.5). Confirmed (code).DoRep(h)(each repetition):reps+ 1;SoundPlay2D("vags/misc/menu_spray"); the counter panel's value; whenreps≥repTable[rankPassed]:UM_Unlock(Level, 1, unlockOffset + rankPassed - 1)(whenrankPassed - 1≤checkpoint),DelayedWinandworkoutDoneset,fWorkout[type]'s message-0 handler cleared,HuStopWorkout(player). Confirmed (code); confirmed (runtime): five repetitions at chin-ups, push-ups and sit-ups rank 1 each ended the workout on the fifth (workout-repandworkout-endhooks), and after the chin-up set the unlock bit of record 83 (95, 1, 31, data 32) cleared at0x006fe8f8(word 2, bit 19).CycleQuit(h)(the engine's end callback):HUDTurnOffActionCycleAnim; after a won setDelayQuit1.5 s later (ForceQuit, then the rank objectives redrawn), elseForceQuitat once. Confirmed (code).
The engine side¶
HuWorkout (0x00238bd8) sets the human's anim overrides for slots 691-695 (0x2b3-0x2b7) to the five clips,
its object target to the second argument and its action code to 0x1b. HuStopWorkout (0x00238d30) sets action
0x1c while working out, or 0 while still in 0x1b. HuSetWorkoutBlend (0x00238cd8) writes the effort +0x670
of a human that is working out. Confirmed (code).
Human_UpdateWorkout (0x00255540, state code 0x1b). Confirmed (code) unless marked:
- Start (action
0x1b). With an object target withinAttackTable_GetFarRange(h, 691)(0x00254508: the human's attack table entry for the clip,max(short +8 × 0.001, float +4 × 1.25)) and all five slots 691-695 holding clips: three tasks are taken; a fade-out (0.1333 s) is pushed; a gait blend (type 12, Gait blend) over slots 693-697 with speed 16 units/s, flags0x201(the target is not rounded) andWorkout_CallRep(0x00234768) as its wrap callback; a chained task plays the enter clip 691 and then the gait blend; push sphere off; held flag0x2000; action back to 0; effort+0x670= 1.0, hold counter+0x674= 6;Human_AlignToObject(0, h, target, 691, side 0);Human_StartWorkout(0x0022d348: state flag0x20000000000, then the start callback). Otherwise the action goes back to 0 and nothing starts. The scripts pass the player as his own target, so the distance is 0 and the reach test always passes; the position is the oneTeleportToFlaggave (inferred). - Each update while working out, pad-controlled (player record
+0x1bset), with v the gait blend's current value (+0x38) and the levelL = floor(v + 0.5)(0-2) indexing the three tables:- The timing factor f is 1, unless
0x005106acis not 1 and the lower clip's time t is at most half its duration d, when f =0x005106ac× (1 − t / (d / 2)). The scripts always make0x005106ac1 (Parameters), so f is always 1. - Pump: when the player's command is
0x12(cross newly pressed, Combat), the hold counter =hold[L]. Then, if the counter is not 0, effort +=gain[L]/ f, capped at 1. Without a pump, a non-zero counter is first decremented and, if still not 0, adds the same gain. So one press adds the gain onhold[L]updates. - Decay, on an update that adds nothing: effort −=
decay[L]× 0.3 when 3 × effort < 1, elsedecay[L]× f; never below 0. - Blend and rate: the task's rate is set to 1. When 3 × effort < 1 the blend target is 0, and once v is 0
the rate becomes 3 × effort + 2 ×
decay[L]: the slow clip slows down. Otherwise the target is 3 × effort − 1 (0-2): slow (693) at 0, mid (694) at 1, fast (695) at 2. Slots 696 and 697 are never reached. - End: on command 10 (triangle newly pressed), or when v is 0 and the rate set the update before is at
most 3 ×
decay[L](effort worn down to aboutdecay/ 3), the action becomes0x1c.
- The timing factor f is 1, unless
- AI-controlled (record
+0x1bclear): the effort is whateverHuSetWorkoutBlendwrote; below 1 the target is 0 and, at v = 0, the rate is effort + 3 ×decay[0]; from 1 the target is effort − 1. No pumps, no decay, no end of its own. - Action
0x1c:Human_EndWorkout(0x0022d380: clears the state flag, stops the gait blend if it is on top, calls the end callback), then the exit clip 692 (Human_PlayActionClip(h, 692, 0x2000, 0)).
Repetitions¶
Workout_CallRep is the gait blend's wrap callback, so a repetition is each time the
leading cursor of the blend wraps (Gait blend): the length of a repetition is
one cycle of the blend's shared normalised time, which runs at the leading clip's pace each update
(below). Confirmed (code); confirmed (runtime):
| Station | Slow / mid / fast clip (s, read from the cursors) | Repetition, pumping every 4 updates | Every 6 updates (chin-ups) |
|---|---|---|---|
| Chin-ups | 2.167 / 1.667 / 1.000 | 37 updates (1.23 s), effort 0.94-1.0, v 1.8-2.0 | 62-65 updates (≈2.1 s), effort ≈0.55, v ≈0.6-0.8 |
| Push-ups | 2.000 / 1.500 / 0.933 | 30-31 updates (1.0 s), effort 1.0, v 2.0 | - |
| Sit-ups | not reached / 1.833 / 1.200 | 39-40 updates (1.3 s), effort 0.97-1.0 | - |
What sets the length¶
Answered 2026-10-11. A repetition is not one clip's duration: the blend's shared normalised
time advances by 1/30 s of whichever clip leads that update, and the pump rhythm moves v off 2.0, so the slower
mid clip leads on some updates. Confirmed (code) at 0x0010a5b8 and confirmed (runtime), PCSX2 2.9.94, re-reading
the per-update cursor logs of the runs above (the lower cursor's time and clip every update, pumping every 4
updates). Every update's step in normalised time was exactly 1/(30 × d) of one of the two clips, d its duration:
the anim step is 1/30 s and the task rate 1.0 throughout, so the earlier "0.03 s per update" fit was a coincidence.
- Why v leaves 2.0. The target is 3 × effort − 1 (the engine side), and the effort drops 0.03 on the update the hold counter runs out, so v follows it to 1.91 (and to 1.82 at chin-ups) one update later and comes back on the pump. The blend's speed (16 units/s) covers those steps in one update.
- Which clip leads (Gait blend): on the update v drops from 2.0 to 1.91 the pair changes to mid + fast and the mid clip leads, because the 0.875 test uses the value and pair from before the move (fraction 0); on 1.82 → 1.91 (rising, old fraction 0.82) the mid clip leads too; on 1.91 → 1.82 and 1.91 → 2.0 the fast clip leads.
- The lengths that follow. Push-ups, per 4 updates: three fast steps and one mid, 3/28 + 1/45 of a cycle, so a repetition takes 30.9 updates (measured 30-31; the fast clip alone would take 28). Sit-ups: 3/36 + 1/55, 39.4 updates (measured 39-40; 36 alone). Chin-ups: two fast and two mid steps, 2/30 + 2/50, 37.5 updates (measured 37; 30 alone). With the wrap rule (a cursor wraps when its rounded frame reaches the clip's last, carrying the overshoot plus half a frame) the counts alternate, 30 / 31 and 39 / 40.
So a reimplementation that picks the leader from the new value, or keeps v at 2.0 while pumping, runs the fast clip alone and its repetitions come out about 0.9 × as long as the original's (28 / 30.9).
The effort at runtime (confirmed (runtime)): Started writes 1.5, which the first loop update caps to 1.0 (the
hold counter of 6 is still running); the enter clip (≈24 updates at chin-ups) changes nothing. Without pumps the
effort falls 0.03 per update to 1/3, then 0.009 per update, the rate falling with it (0.963, 0.855, ... 0.099), and
the workout ends 63 updates (2.1 s) after the loop starts, with no repetition (push-ups). A pump shows in the hold
counter on the next update.
Parameters¶
CfgWorkoutParams(gain, decay, hold, flag) (lua_CfgWorkoutParams, 0x0035c5a0 → Cfg_SetWorkoutParams
0x00234530) copies three floats to 0x00510690 (gain), three to 0x005106a0 (decay), three integers to
0x005106b0 (hold) and writes the tables back into the Lua tables. The fourth argument goes through
Lua_GetOptionalBoolArg (0x00409318: absent → 0, nil → 0, else 1 when the number is not 0), so the 1.8 the scripts
pass becomes 1.0 at 0x005106ac and the timing factor never applies. Confirmed (code); confirmed (runtime): the
decay was exactly 0.03 per update whatever the clip time. HuSetWorkoutParams (0x002345d8) fills three values at
0x00510680 that Human_UpdateWorkout does not read.
The heavy bag¶
No HuWorkout: the player fights the bag human with his own attacks. Confirmed (code) unless marked:
workout.Setup(station 1) setsHUDSetTutorialCallback("workout.lCombos"), so every hit player 1 lands or has blocked callsworkout.lCombos(animId)(Tutorial callback).CalloutCombos01:ActivateWarchief,StartHUD,landed= 0,CfgPlayerCombatWalkOnly(1),HuLockPadMovement(player, true), the stopwatch (HUD), thenCalloutCombos02.CalloutCombos02pickscombo=random(1, 6); if it equals the previous one, the next (6 wraps to 1). It announces the combo (HUDSetAnnounceMsg(5, LEVEL95.CS_n, true): the buttons drawn in the middle of the screen) and sets its list and a position of 1:
| Combo | Hits (anim ids) | Buttons |
|---|---|---|
| 1 | S1 12, SS2 16 |
square, square |
| 2 | S1 12, SS2 16, then SSS3 19 or SSS3_HOLD 20 |
square ×3 |
| 3 | S1 12, SX2 15 |
square, cross |
| 4 | S1 12, SS2 16, then SSX3 17 or SSX3_HOLD 18 |
square, square, cross |
| 5 | X1 11, XX2 13 |
cross, cross |
| 6 | X1 11, XS2 14 |
cross, square |
lCombos(anim): returns at once after a time-out. Picks the camera by the player's octant round the bag (GetOct: by the signs of the player's x and y less the bag's and which is larger): octants 1-2cam, 4cam2, 6-7cam3, 3, 5 and 8 keep the current one. A hit equal to the list's entry at the position moves the position on; any other hit resets it to 1 (and is not tested again as a first hit). For combos 2 and 4 the third hit may be either of the last two entries. A completed list:landed+ 1,menu_spray, the counter panel,Check01, position back to 1.Check01: whenlanded≥repTable[rankPassed]: tutorial callback off, announcement cleared,DelayedWin, the quit handler cleared,Quit01500 ms later (UM_Unlock(Level, 1, unlockOffset + rankPassed - 1), objectives redrawn,ForceQuit). Otherwise the next combo is called out 100 ms later.- The clock:
W_SetStopWatch(timeTable[rank] × 1000, 0, "workout.WorkoutTimeOut"), shown with a label. At 0, withoutDelayedWin:TimedOut, the quit handler cleared, and for the bagWorkoutTimeOut2600 ms later (ForceQuit); for a cycle gameHuStopWorkout(player). Damage to the player does the same. - Triangle quits through
fWorkout[1]'s message-0 handler (workout.ForceQuit): below.
Confirmed (runtime): with triangle pressed beside the bag the game started (locked camera, the clock counting down from
0:26, the counter 0/10, the called-out buttons over the bag); square pressed every 9 updates gave the tutorial
callback anim ids 12, 16, then 19 or 20, about every 11 updates, three per chain; with the combo called out being
X1, XS2, no combo was landed.
Triangle quits the bag¶
SetMsgHandlerEx with a nil prompt does not clear the context record when the second text is set: the binding
(Script_SetMsgHandlerEx, 0x00386168) clears it (+0x12c) only when the callback is nil, or when both the prompt
and the second text are nil or empty; otherwise it registers it (+0x124, Flag_RegisterContext 0x00415d40, once
per flag) with the prompt nil and the hint at +0x14 (ContextActions_Register, 0x00417ca0, kind 1). So
fWorkout[1] gets a kind-1 record with no prompt. Every update the player's record is picked as for any other
(Crimes: triangle): he stands on the flag (the reach was raised to 1.8 m by the gym's volume
box), so it passes. Then:
- The HUD (
HUD_Update,0x001af010): a record with a second text queues it in the hint box at priority 0 when it changes, and the prompt widget takes the record's+0x10, which is null, soHUD_ClearActionPromptruns: no prompt over the bag, the quit hint in the hint box (bottom left: the station's name,Rank: n/10and the quit line). Confirmed (code), and at runtime. - Triangle reaches
ContextAction_Use(0x0024d530), which for kind 1 sends the flag message 0 (Flag_OnMessage,0x00416038→ScriptHandler_Deliver), and the handler callsworkout.ForceQuit. The press is taken in the fight stance with pad movement locked (no attack or block runs first). Confirmed (code), and at runtime.
Confirmed (runtime, PCSX2 2.9.94, level95 checkpoint 2, the player put beside the bag's start flag, triangle tapped
over the scripted-pad patch): the start flag's record left the kind-1 list within 2 updates of the press (the other
three stations' records stayed); 45 updates after the press the player's current record (+0x660) was the
fWorkout[1] flag's, prompt 0, hint set; triangle tapped 120 updates after the start gave the start flag's record
(prompt set) back 3 updates later, with the follow camera, no clock and the start prompt and hint on screen, well
before the 26 s clock ran out.
The bag as a target¶
- The player's target. After the first square at the bag, the player's target handle (
+0xc8) is the bag's (its handle, human+0x90); before it,NilHandle. His health rings show under him while he fights the bag; the bag shows none. Confirmed (runtime). - What a strike meets. The bag (
IPhysics_CreatePunchBagBody,0x0033cb08; the body set at IPhysics+0x4f2a0) has one segment on bone 3 and a capsule; its human bone shapes are off. Read at runtime with the bag at rest (origin at height −197.567): the segment's offset (+0x10) is (0, 0, 0), so its start is bone 3's position, 2.15 m above the bag's origin; its end is 1.5 m along bone 3's local x axis, which at rest points straight down (end 0.65 m above the origin, 0.05 m off the vertical); radius 0.35. The capsule (no bone, offset 0) stands on the bag's origin with radius 0.35 and height 1.787 m (+0x44; a human capsule's height is bone 6's height + 0.2 × scale, recomputed every update byPhysicsCapsule_Pose,0x00343518). Confirmed (code) at0x00343810, values confirmed (runtime). Details: Physics. - Who is warned. A punch at the bag warns the bag alone (Combat: attack warnings): nobody else in the gym hears it.
Boxer¶
A Warrior at the bag spars with it through GoalBoxer (Boxer goal): its brain's attack
weights +0x208 point at the table 0x00511120 (45 bytes, one per attack kind): kinds 1, 3, 7,
24, 31, 39 and 42 weigh 5 and kind 5 (SS2) weighs 10; every other kind 0. Confirmed (code). That AI goal is all the
table serves: the player's workout does not read it.
HUD¶
Confirmed (code), the layout confirmed (runtime) at the bag:
- The counter:
HUDGetNewPH(2, LEVEL95.WLABEL[type])(a counter panel of kind 2, label thenvalue/max, HUD), its maximumrepTable[rank]and its value the repetitions or combos landed. - The clock when
timeTable[rank]> 0 (not rank 1 of the cycle games): the stopwatch with a label, in its minutes form (0:26) at the top right, right-aligned, with the counter panel under it: the placeStopWatchHud_ApplyVideoModegives, (1.0, 0.24), not the HUD table's (0.5, 0.135) (HUD). Confirmed (runtime, default video mode): the clock's right edge stood at 0.95 of the picture's width and its middle at 0.26 of its height. - The health rings under the player while he fights the bag (The bag as a target).
- The cross icon for the cycle games:
HUDTurnOnActionCycleAnim(4, 0.1, 6, 79, 79), the cross button blinking where the action prompt stands, fromStartedtoCycleQuit. - The called-out combo for the bag: the centred announcement.
- No effort meter. Nothing on the HUD shows the effort: the player sees it as the loop clip's speed (slow, mid, fast, then slowing down).
- The prompt beside the station: the station's name over the station, and in the hint box a line of text and
Rank: n/10; while working out, no prompt and the station's quit hint in the hint box (Triangle quits the bag). The start prompt is gone from the press on (step 2 of Starting a workout).
Ranks and what they give¶
worksetup.BuildWorkoutMessage(type, menu) (both in level95.lua and level95_clubhouse.lua) counts the station's
unlocked records of type 10 from its offset (11, 31, 41, 51): r = the number of consecutive data ids offset,
offset + 1, ... unlocked (at most 10), capped at checkpoint + 1. The data id offset itself is unlocked from the
start (records with level 0), so r ≥ 1 is the rank being attempted and r − 1 the ranks passed (Rank: r−1/10).
When r = checkpoint + 1 the station is tired for this chapter (NoWorkout, the rank in grey); rank 10 with the
station's last record (74, 72, 73, 71 for stations 1-4) unlocked shows 10/10; ranks 5 and 10 show in the bonus
colour. Confirmed (code).
Passing rank r unlocks item offset + r − 1 of level 95, group 1, whose record (type 10, global.lua) has data
offset + r (rank 10's items 20, 40, 50, 60 give data 74, 72, 73, 71). So the hub's chapter limits the rank: at
checkpoint c, ranks up to c. The rank is kept as an unlock and saved with the others
(Unlockables). No engine code reads type-10 records (every caller of
Unlocks_IsDataUnlocked, 0x00424130, asks for another type): the ranks give nothing by themselves. Confirmed (code).
The rewards (workout.CheckUnlockables, run by every CleanUp), confirmed (code):
- All four stations past rank 10 (data 71-74) and type 6 data 12 not yet unlocked:
UM_Unlock(95, 1, 10), which unlocks type 6 data 12, the stamina maximum × 1.2 (Unlockables), announced withLEVEL95.FEATEXP[20]. - All four past rank 5 (data 16, 36, 46, 56) and data 11 not yet unlocked:
UM_Unlock(95, 1, 9), type 6 data 11, the power meter's maximum × 1.1, announced withFEATEXP[15]. worksetup.DisplayUnlockedshowsFEATEXP[1], then 2.5 s later the reward's text.
The end¶
workout.ForceQuit(h): CfgPlayerCombatWalkOnly(0), HUDReleasePH, the quit handler cleared,
HuLockPadMovement(player, false), DeactivateWarchief, ForceShowPlayerHud(0, false), HidePlayerHud,
CameraMakeActive(MainCam, 0), the stopwatch stopped and hidden, then CleanUp: the player's damage handler cleared,
HuSetPushable(player, true), the tutorial callback off, CheckUnlockables, StartTimeOut flushed, the workout
cameras deleted, the announcement cleared, worksetup.FinishWorkout (the station's prompt rebuilt with the new rank,
ActivateWarchief) and workout = nil. Confirmed (code). At runtime the workout-end hook was called from
0x00255c98 (the 0x1c branch of Human_UpdateWorkout) on the update of the fifth repetition.
Coney¶
Why triangle at the bag starts nothing (confirmed (runtime), Coney release build of 2026-10-10, --play-level
level95 --checkpoint 2, the player put beside the bag after the opening tutorial and triangle tapped, event log):
the context action works (worksetup.StartWorkout(277, player) runs and the hint shows), and so do the loading and
workout.Setup. What fails is step 2's GoalMoveToFlag(player, fWorkout[1], ..., faceFlag = true): the player
never turns (heading unchanged for 150 updates), so MoveToFlagGoal::process (src/ai/move_to_flag_goal.cpp) keeps
queueing the face-the-flag turn, never arrives and never sends message 8; worksetup.DoWorkout never runs, and
5 s later worksetup.TimeOut quits (workout.ForceQuit, the prompt rebuilt). HuWorkout and the bag's
CalloutCombos01 are never reached. The same call with faceFlag = false (run from the debug menu's Lua console)
sent message 8 on the next update. In the original the player walks there and turns (action code 3, 12 updates).
Every cycle station fails the same way.
The cause, found and fixed (Coney, 2026-10-11): DeactivateWarchief sets the player's brain dead, so the pad is
given up and the goal runs, but the player stands in the fight stance locked on a foe (the bag, or any fighter near),
and Coney's fight-stance walk (Human::combatWalk) turned every human it holds to face the lock target each update,
whatever the brain asked. The turn action was overridden on every update; once it gave up, the goal queued another.
In the original a human no pad drives (per-player +0x1b = 0) is moved by Human_UpdateControl, which turns it
toward the brain's heading: by the fight stance's AI word (24° an update) when asked to move, with a turn on the spot
when stopped more than 15° off, not at all within 15° (AI: Speed control and Turning). Coney now
runs a player whose pad a script took through the AI's stance control (Human::aiCombatWalk, the turn on the spot at
the standing step, as for every AI, a stand-in for the turn clips; AI: Facing the target), so
the goal's turn steers and it arrives (repo:tests/ai/move_action_test.cpp, and on the disc
repo:tests/platform/disc_hub_workout_test.cpp). The headings were not the cause, but they were mirrored: a
flag's degrees turn clockwise seen from above (Flags), so fWorkout[1]'s 93 faces +x, at the bag,
and Coney turned the player to face away from it until the flags' headings were negated (2026-10-20).
The cause, found and fixed (Coney, 2026-10-11): DeactivateWarchief sets the player's brain dead, so the pad is
given up and the goal runs, but the player stands in the fight stance locked on a foe (the bag, or any fighter near),
and Coney's fight-stance walk (Human::combatWalk) turned every human it holds to face the lock target each update,
whatever the brain asked. The turn action was overridden on every update; once it gave up, the goal queued another.
In the original a human no pad drives (per-player +0x1b = 0) is moved by Human_UpdateControl, which turns it
toward the brain's heading: by the fight stance's AI word (24° an update) when asked to move, with a turn on the spot
when stopped more than 15° off, not at all within 15° (AI: Speed control and Turning). Coney now
runs a player whose pad a script took through the AI's stance control (Human::aiCombatWalk, the turn on the spot at
the standing step, as for every AI, a stand-in for the turn clips; AI: Facing the target), so
the goal's turn steers and it arrives (repo:tests/ai/move_action_test.cpp, and on the disc
repo:tests/platform/disc_hub_workout_test.cpp). The headings were not the cause, but they were mirrored: a
flag's degrees turn clockwise seen from above (Flags), so fWorkout[1]'s 93 faces +x, at the bag,
and Coney turned the player to face away from it until the flags' headings were negated (2026-10-20).
The cycle games in Coney (2026-10-11): HuWorkout follows The engine side. A workout begins once all
five clips load (the level's clip source, by the names the script gives) and, for the player who is his own target,
at any distance; the enter clip plays, then a gait blend over the slow, mid and fast clips at 16 units a second whose
target and rate human::WorkoutEffort sets each update from the station's CfgWorkoutParams tables (the pumps, the
hold counter, the 0.03 decay and its 0.3 share below a third, the end on triangle or when worn down). Each wrap of the
blend's leading clip calls the repetition callback; the end clears the flag, calls the end callback and plays the exit
clip. repo:tests/human/workout_test.cpp checks the rules (the run-down ends 58-68 updates into the loop, the
runtime's 63) and repo:tests/platform/disc_hub_workout_test.cpp plays each cycle station through the pad: five
repetitions at a press every 4 updates, each gap within 3 updates of the runtime's or a little below (Coney's 33-34
at the chin-ups, 28 at the push-ups and 36 at the sit-ups against the runtime's 37, 30-31 and 39-40: an open question
below), the rank's unlock and the prompt back with Rank: 1/10; at the chin-ups also a
run-down with no repetition ending 58-68 updates into the loop, and triangle ending the loop on the update it is
pressed. Stand-ins and choices:
- Another human's equipment is reached within 1.5 m (the start clip's attack-table reach is not read).
- The blend starts at value 0.
Human_AlignToObjectturns a human only toward another target.- Coney applies a script's teleport of player 1 at the end of the frame, after the workout may have begun, so every spawn of a human that is working out keeps its clips.
- The update runs from the loop's first update on, whether or not the loop is on top:
Human_EndWorkoutstops the blend only if it is on top, so a clip played over it (a hit reaction, a script's clip) leaves the workout running; meanwhile v is the loop's last value and only the effort, the pumps and the end run, so triangle still ends it (inferred: the code that skips the update during the enter clip is not on this page). - Not done: the start's push sphere off (Coney's bodies push nothing aside yet) and the held flag
0x2000on the start's chain and on the exit clip (Human_PlayActionClip(h, 692, 0x2000, 0); in Coney's task flags0x2000is the duck's counter phase, so the bit's meaning here needs reading first); the exit clip fades in at the combat fade.
Notes for implementers¶
Per station:
- All: make
GoalMoveToFlagwithfaceFlagarrive for the player (done, Coney); keepTeleportToFlagexact;HuWorkout's target is the player himself, so drop the 1.5 m reach stand-in (the reach test passes at 0).CfgWorkoutParams' fourth argument is a flag (non-zero → 1.0), so the timing factor is always 1. - Chin-ups, push-ups, sit-ups (done, Coney;
Human_UpdateWorkoutas in The engine side): play the enter clip 691 then a gait blend over 693-695 (speed 16 units/s, unrounded target 3 × effort − 1, or 0 below an effort of 1/3); effort starts at 1 (the script's 1.5 is capped) with a hold counter of 6; a cross press sets the counter tohold[L]and addsgain[L]on that many updates; otherwise decay 0.03 (× 0.3 below 1/3); the rate is 1, or 3 × effort + 2 × decay once the blend is at 0; end on triangle or when that rate fell to 3 × decay; a repetition is each wrap of the blend's leading clip (call theDoRepcallback then), not a fixed 1.5 s; the exit clip 692 at the end. Use the station's own tables (above). The level index isfloor(v + 0.5)of the blend's value. - Heavy bag: no
HuWorkout; the bag human must take hits so thatHUDSetTutorialCallback's function gets each hit's anim id (already in Coney);CfgPlayerCombatWalkOnly(1)andHuLockPadMovementkeep the player at the bag; the three cameras switch by octant in the script; the stopwatch must callworkout.WorkoutTimeOutat 0. Strike the bag on its segment (bone 3, no offset, along the bone's local x, which hangs down; radius 0.35, length 1.5) and sweep against its capsule (radius 0.35, height 1.787 m from its origin).SetMsgHandlerExmust register a kind-1 record when only the second text is given (that record is the bag's quit), andSetMsgHandler(obj, 0, ...)must unregister the object's record. Warn only the attack's target (Combat). - HUD: the counter panel (kind 2), the stopwatch, the blinking cross icon, the centred combo call-out; no meter.
- Ranks and rewards:
UM_Unlock(95, 1, offset + rank − 1)must reach the unlock records and survive a save; the power (× 1.1) and stamina (× 1.2) upgrades of type 6 data 11 and 12 must be read where the maxima are computed.
Playthrough tests (pad-driven, Testing): from level95 checkpoint 2 on the player's path,
walk to each station with the stick at a partial deflection, then:
- Start: tap triangle beside
fStartWorkout[k]; assertworksetup.StartWorkout, message 8 atfWorkout[k]within 2 s,workout.StartWorkout, and for k = 2-4HuWorkoutand theStartedcallback (the cross icon on); for k = 1CalloutCombos01, the clock at 26 s and the counter at 0/10. - Pump (k = 2-4): tap cross every 4 updates; assert five
DoRepcallbacks, 30-40 updates apart, the counter reaching 5/5,HuStopWorkout, the exit clip, and the rank in the rebuilt prompt one higher (UM_Unlock(95, 1, offset)). - Let it run down (k = 2-4): no presses after the start; assert no
DoRepand the end about 63 updates after the loop starts, the rank unchanged. - Quit: triangle during the loop (k = 2-4) or at the bag; assert
CycleQuit/ForceQuit, the follow camera back, pad movement unlocked, the prompt back. - Bag combos: read the called-out combo from the announcement, press its buttons; assert the counter rising by 1
per landed combo and, at 10 within 26 s,
UM_Unlock(95, 1, 11); and a time-out without a win. - Rewards: with the type-10 records of ranks 5 (or 10) of all four stations unlocked, finish any workout and
assert
UM_Unlock(95, 1, 9)(or 10) and the power (stamina) maximum raised.
Open questions¶
-
Why Coney's repetitions, pumped every 4 updates, come about a tenth sooner than the original's (33-34, 28 and 36 updates against 37, 30-31 and 39-40 at the chin-ups, push-ups and sit-ups). The ratio is close to 0.9, the rate
CfgAnimSpeedsgives clips with range flag0x2000(Animation: Playback rate), so the loop may play at that rate (speculative); a runtime read of the blend task's rate+0x0cduring a workout would settle it. -
The slow clip of the sit-ups (not reached at runtime) and the enter and exit clips' lengths: read them from the clip files.
Human_AlignToObjectwith the human as its own target (a zero direction): what it does to the heading (the player is already on the flag, facing its heading).- Coney's repetitions at 0.9 × the original's (answered, What sets the length): the anim step is
1/30 s and the rate 1.0; the mid clip leads the blend on the updates the pump rhythm takes v below 2.0, which
lengthens the cycle to 30.9 (push-ups), 39.4 (sit-ups) and 37.5 (chin-ups) updates. Confirmed (code) at
0x0010a5b8, confirmed (runtime).