BT410 5.3.93: the clip loader runs LIVE -- the Mad Cat measures itself, and a 1995 copy-paste bug ships again on purpose
LoadLocomotionClips + ResolveAnimationClip + MeasureClipStride + LoadClipSlot
reconstructed and WIRED into the ctor's GameModel block. animationClips[] is
no longer an empty array: every gait slot resolves by the model's animation
prefix, and the locomotion constants are now MEASURED from the authored clips
instead of asserted as bring-up defaults. First live run, arena mission:
[mech] clips 'mad': standSpeed=5.23 walkStride=18.51 revStride=56.05
revSpeedMax=26.26 gimpSpeedMax=-4.23 gimpStride=-20.26
limpSet=1
That line carries three verifications at once: the prefix printing as text
proves the Mech__ModelResource layout is right at +0x40; the reverse figures
come out NEGATIVE exactly as the transition machines expect; and the mission
ran clean to live driving afterwards (703 log lines, no fault).
DRIVING FEEL CHANGED, deliberately: speedDemand at 0.6 throttle went 14.4 ->
26.9, because the placeholder top speed (30) gave way to the measured 56.05.
The Mad Cat is simply faster than the bring-up guess. Authenticity arriving,
not a regression.
TWO BINARY BEHAVIOURS REPRODUCED ON PURPOSE, both documented at the function:
The speed caps read keyframeData[keyframeCount] -- one entry PAST the last
frame. Fencepost is the binary's own (0x690 + 8 + [0x670]*0xc); whether the
authored table has count+1 entries is unestablished, but the clips were
authored against this read and the measured values are sane.
The reverse-cycle stride is computed from STALE locals. The decomp is
unambiguous: bbr and bbl are both measured into local_8/local_c, and the
divide's second terms come from local_10/local_14 -- still holding the
RUN-LEFT figures. gimpStrideLength = -((bbl + rrl_stale)/(...)). A 1995
copy-paste bug, shipped in every pod for thirty years, reproduced here with
a comment pointing at the wwr/wwl block that shows the intended pattern.
(And an earlier scare resolved: the negation IS in the binary -- the very
next instruction is 0x350 = -0x350. My first decomp window cut one line
short and briefly indicted the donor's minus sign.)
ONE DELIBERATE DIVERGENCE, tagged [T3]: the binary dereferences every resolve
result unguarded -- a model missing a mandatory clip crashes on load. Here a
miss stores NullResourceID (SelectSequence resolves it to an inert controller)
and the dependent measurement is skipped. Keeps the boot alive on unverified
clip sets; revisit when the fleet's models are known-good. Measurement binds
pass a NULL finished-callback (measurement parses, never plays -- the binary's
live pointers can never fire there).
Ctor additionally zero-initializes the whole gait channel -- globalTimeScale
defaulting to 1 specifically, because zero would silence every clip advance --
and fills the clip array with NullResourceID before the loader runs, so the
uninitialized-member class of bug (see 5.3.83) is closed here BEFORE the
consumers arrive.
MECH.HPP: the optional limp set carved out (hasGimpClips + 4 measured limp
figures + gyroRumbleTimer); reservedState 150 -> 140.
Next: the four Advance* entry points -- the last link before the legs move.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,37 @@ Mech::Mech(
|
||||
bodyTargetSpeed = 0.0f;
|
||||
currentBodySpeed = 0.0f;
|
||||
|
||||
//
|
||||
// Gait channel state (mech2.cpp). The measured constants (standSpeed,
|
||||
// gimpSpeedMax, gimpStrideLength and the four limp figures) get their
|
||||
// real values in LoadLocomotionClips below; these defaults keep every
|
||||
// divide in the transition machines finite if a clip set is missing.
|
||||
// globalTimeScale MUST default to 1 -- zero would silence every clip
|
||||
// advance. Unfilled clip slots hold NullResourceID, which SelectSequence
|
||||
// resolves to an empty, inert controller.
|
||||
//
|
||||
legCycleSpeed = 0.0f;
|
||||
bodyCycleSpeed = 0.0f;
|
||||
forwardCycleRate = 1.0f;
|
||||
gimpCycleRate = 1.0f;
|
||||
standSpeed = 1.0f;
|
||||
gimpSpeedMax = 1.0f;
|
||||
gimpStrideLength = -1.0f; // the measured value is negative too
|
||||
globalTimeScale = 1.0f;
|
||||
hasGimpClips = 0;
|
||||
gimpLeftSpeedMax = 1.0f;
|
||||
gimpRightSpeedMax = 1.0f;
|
||||
gimpLeftStrideLength = 1.0f;
|
||||
gimpRightStrideLength = 1.0f;
|
||||
gyroRumbleTimer = 0.0f;
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < AnimationSlotCount; ++i)
|
||||
{
|
||||
animationClips[i] = ResourceDescription::NullResourceID;
|
||||
}
|
||||
}
|
||||
|
||||
eyepointRotation = EulerAngles::Identity;
|
||||
lookPitch = 0.0f;
|
||||
lookYaw = 0.0f;
|
||||
@@ -529,6 +560,42 @@ Mech::Mech(
|
||||
a = model->lookBackAngle;
|
||||
if (a > -360.0f && a < 360.0f) lookBackAngle = a * RAD_PER_DEG;
|
||||
}
|
||||
|
||||
//
|
||||
// The gait clip loader -- resolves every animation clip by
|
||||
// the model's prefix and MEASURES the stride/speed constants
|
||||
// from the clips themselves, replacing the bring-up defaults
|
||||
// above. Guarded on the prefix looking like text because
|
||||
// this ModelResource layout is only partially verified; a
|
||||
// garbage prefix would just probe nonsense names (soft), but
|
||||
// the log line makes a layout miss visible.
|
||||
//
|
||||
if (
|
||||
model->animationPrefix[0] >= 'a' &&
|
||||
model->animationPrefix[0] <= 'z'
|
||||
)
|
||||
{
|
||||
LoadLocomotionClips(model);
|
||||
if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[mech] clips '"
|
||||
<< model->animationPrefix << "': standSpeed="
|
||||
<< standSpeed << " walkStride=" << walkStrideLength
|
||||
<< " revStride=" << reverseStrideLength
|
||||
<< " revSpeedMax=" << reverseSpeedMax
|
||||
<< " gimpSpeedMax=" << gimpSpeedMax
|
||||
<< " gimpStride=" << gimpStrideLength
|
||||
<< " limpSet=" << hasGimpClips
|
||||
<< endl << flush;
|
||||
}
|
||||
}
|
||||
else if (getenv("BT_MECH_LOG"))
|
||||
{
|
||||
DEBUG_STREAM << "[mech] animationPrefix not text ("
|
||||
<< (int)(unsigned char)model->animationPrefix[0]
|
||||
<< ") -- clip loader SKIPPED, layout suspect"
|
||||
<< endl << flush;
|
||||
}
|
||||
}
|
||||
modelDesc->Unlock();
|
||||
}
|
||||
|
||||
@@ -385,6 +385,24 @@
|
||||
Scalar carryover,
|
||||
int move_joints);
|
||||
|
||||
//
|
||||
//--------------------------------------------------------------------
|
||||
// The gait clip loader (mech2.cpp). ResolveAnimationClip maps the
|
||||
// model's animation prefix + a 3-char suffix to a clip resource;
|
||||
// MeasureClipStride binds a loaded slot and integrates its keyframe
|
||||
// strides; LoadLocomotionClips fills animationClips[] and measures
|
||||
// every gait constant from the clips themselves.
|
||||
//--------------------------------------------------------------------
|
||||
//
|
||||
ResourceDescription::ResourceID *
|
||||
ResolveAnimationClip(const char *prefix, const char *suffix);
|
||||
void
|
||||
MeasureClipStride(int slot, Scalar *total, Scalar *last_key);
|
||||
void
|
||||
LoadLocomotionClips(ModelResource *model);
|
||||
int
|
||||
LoadClipSlot(int slot, const char *prefix, const char *suffix);
|
||||
|
||||
//
|
||||
// The respawn heal-and-move (binary @0049fb74): reposition the SAME
|
||||
// entity at the drop-zone origin, kill all motion, clear the death
|
||||
@@ -620,6 +638,22 @@
|
||||
// leaves that slot off the end.
|
||||
//
|
||||
int animationClips[AnimationSlotCount];
|
||||
|
||||
//
|
||||
// The OPTIONAL limp clip set (slots 22-27). LoadLocomotionClips
|
||||
// probes for the "wgl" clip; a model without it leaves hasGimpClips 0
|
||||
// and those slots unfilled, so the limp machine must never be entered
|
||||
// for such a mech. (These four measured figures ARE the limp's --
|
||||
// unlike gimpSpeedMax/gimpStrideLength above, which despite the names
|
||||
// are measured from the REVERSE clips. See MECH2.NOTES.md.)
|
||||
//
|
||||
int hasGimpClips;
|
||||
Scalar gimpLeftSpeedMax;
|
||||
Scalar gimpRightSpeedMax;
|
||||
Scalar gimpLeftStrideLength;
|
||||
Scalar gimpRightStrideLength;
|
||||
Scalar gyroRumbleTimer; // binary mech+0x5c4; the
|
||||
// clip loader zeroes it
|
||||
AverageOf<Scalar> telemetryFilter[5];
|
||||
CString resourceNameA;
|
||||
CString resourceNameB;
|
||||
@@ -719,10 +753,11 @@
|
||||
// reconstructed with named fields.
|
||||
//
|
||||
//
|
||||
// 41 ints carved out for the gait channel above (2 AlarmIndicators = 4,
|
||||
// 8 Scalars, animationClips[0x1d] = 29); 191 -> 150.
|
||||
// 51 ints carved out for the gait channel above (2 AlarmIndicators = 4,
|
||||
// 8 Scalars, animationClips[0x21] = 33, the optional limp set = 6);
|
||||
// 191 -> 140.
|
||||
//
|
||||
int reservedState[150];
|
||||
int reservedState[140];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
# include <mechmppr.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(APP_HPP)
|
||||
# include <app.hpp>
|
||||
#endif
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// A mech walks on two parallel clip channels.
|
||||
@@ -478,3 +482,265 @@ Scalar
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// @004a7f50 -- prefix + suffix -> the clip's resource ID.
|
||||
//
|
||||
// The clip names are the model's 3-char animation prefix with a 3-char gait
|
||||
// suffix appended ("mad" + "wwr" = madwwr), resolved by name over the
|
||||
// animation resources. Returns a pointer to the found description's
|
||||
// resourceID; NULL when the model has no such clip -- which is a REAL case
|
||||
// (the limp set is optional), so callers must tolerate it.
|
||||
//#############################################################################
|
||||
//
|
||||
ResourceDescription::ResourceID *
|
||||
Mech::ResolveAnimationClip(const char *prefix, const char *suffix)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(prefix);
|
||||
Check_Pointer(suffix);
|
||||
|
||||
char
|
||||
clip_name[12];
|
||||
|
||||
strcpy(clip_name, prefix);
|
||||
strcat(clip_name, suffix);
|
||||
|
||||
ResourceDescription
|
||||
*description = application->GetResourceFile()->FindResourceDescription(
|
||||
clip_name,
|
||||
ResourceDescription::AnimationResourceType,
|
||||
ResourceDescription::NullResourceID);
|
||||
|
||||
return (description != NULL) ? &description->resourceID : NULL;
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// @004a8054 -- bind the clip at animationClips[slot] into the leg channel and
|
||||
// integrate its keyframe strides. Returns (via the out parameters) the total
|
||||
// cycle distance and the final keyframe time; the loader divides total by
|
||||
// time to recover a cycle speed.
|
||||
//
|
||||
// The callback is NULL on purpose: measurement only ever PARSES the clip
|
||||
// (SelectSequence), it never plays it, so the finished callback can never
|
||||
// fire. The binary passes a live pointer here; NULL is behaviourally
|
||||
// identical and avoids arming a transition machine mid-load. [T3]
|
||||
//#############################################################################
|
||||
//
|
||||
void
|
||||
Mech::MeasureClipStride(int slot, Scalar *total, Scalar *last_key)
|
||||
{
|
||||
Check(this);
|
||||
Verify(slot >= 0 && slot < AnimationSlotCount);
|
||||
|
||||
legAnimation.SelectSequence(animationClips[slot], NULL, 0, 0);
|
||||
|
||||
*total = 0.0f;
|
||||
*last_key = 0.0f;
|
||||
|
||||
int
|
||||
frame;
|
||||
for (frame = 0; frame < legAnimation.keyframeCount; ++frame)
|
||||
{
|
||||
Scalar
|
||||
frame_time = legAnimation.keyframeTimes[frame];
|
||||
|
||||
*total += (frame_time - *last_key) *
|
||||
legAnimation.keyframeData[frame].stride;
|
||||
*last_key = frame_time;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// @004a80d4 -- resolve and cache every gait clip, measuring the gait
|
||||
// constants from the clips themselves as it goes. This is where standSpeed,
|
||||
// walkStrideLength, reverseSpeedMax, reverseStrideLength, gimpSpeedMax and
|
||||
// gimpStrideLength actually COME FROM -- they are properties of the authored
|
||||
// animations, not authored numbers.
|
||||
//
|
||||
// Two binary behaviours reproduced deliberately; neither is a transcription
|
||||
// slip. See the sidecar before "fixing" either:
|
||||
//
|
||||
// * The speed caps read keyframeData[keyframeCount] -- one entry PAST the
|
||||
// last frame (the binary reads 0x690 + 8 + [0x670]*0xc).
|
||||
//
|
||||
// * The reverse-cycle stride divides the bbl measurement by STALE data:
|
||||
// both bbr and bbl are measured into the same pair, so the divide takes
|
||||
// its second terms from whatever the run cycle left behind. A 1995
|
||||
// copy-paste bug, shipped, and therefore reproduced -- the walk and run
|
||||
// cycles above it show what was obviously intended.
|
||||
//
|
||||
// DIVERGENCE FROM THE BINARY, on purpose: the binary dereferences every
|
||||
// ResolveAnimationClip result unguarded -- a mech whose model lacks a
|
||||
// MANDATORY clip crashes on load. Here a miss stores NullResourceID (which
|
||||
// SelectSequence resolves to an empty, inert controller) and the dependent
|
||||
// measurement is skipped, leaving the bring-up default in place. [T3: keeps
|
||||
// the current boot alive on models whose clip sets have not been verified;
|
||||
// revisit once every fleet mech is known-good.]
|
||||
//#############################################################################
|
||||
//
|
||||
|
||||
//
|
||||
// Resolve one slot: store the clip ID or NullResourceID. Returns whether the
|
||||
// clip exists, so dependent measurements can be skipped on a miss.
|
||||
//
|
||||
int
|
||||
Mech::LoadClipSlot(int slot, const char *prefix, const char *suffix)
|
||||
{
|
||||
ResourceDescription::ResourceID
|
||||
*clip_ID = ResolveAnimationClip(prefix, suffix);
|
||||
|
||||
animationClips[slot] =
|
||||
(clip_ID != NULL) ? *clip_ID : ResourceDescription::NullResourceID;
|
||||
|
||||
return clip_ID != NULL;
|
||||
}
|
||||
|
||||
void
|
||||
Mech::LoadLocomotionClips(ModelResource *model)
|
||||
{
|
||||
Check(this);
|
||||
Check_Pointer(model);
|
||||
|
||||
const char
|
||||
*prefix = model->animationPrefix;
|
||||
//
|
||||
// Zero-initialized because the guarded skips below can reach the reverse
|
||||
// divide with the run pair unmeasured -- a path the (unguarded) binary
|
||||
// does not have, so the stale-pair reproduction must not become an
|
||||
// uninitialized read on top of it.
|
||||
//
|
||||
Scalar
|
||||
total_a = 0.0f, last_a = 0.0f,
|
||||
total_b = 0.0f, last_b = 0.0f;
|
||||
|
||||
gyroRumbleTimer = 0.0f;
|
||||
|
||||
//
|
||||
// Stand -> walk. standSpeed is the clip's final-entry stride.
|
||||
//
|
||||
if (LoadClipSlot(5, prefix, "swr"))
|
||||
{
|
||||
legAnimation.SelectSequence(animationClips[5], NULL, 0, 0);
|
||||
standSpeed =
|
||||
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
||||
}
|
||||
|
||||
//
|
||||
// The forward walk cycle: stride = (s6 + s7) / (d6 + d7).
|
||||
//
|
||||
if (
|
||||
LoadClipSlot(6, prefix, "wwr") &&
|
||||
LoadClipSlot(7, prefix, "wwl")
|
||||
)
|
||||
{
|
||||
MeasureClipStride(6, &total_a, &last_a);
|
||||
MeasureClipStride(7, &total_b, &last_b);
|
||||
walkStrideLength = (total_a + total_b) / (last_a + last_b);
|
||||
}
|
||||
|
||||
LoadClipSlot(8, prefix, "wsr");
|
||||
LoadClipSlot(9, prefix, "wsl");
|
||||
|
||||
//
|
||||
// Walk -> run. reverseSpeedMax is measured from wrr the same way
|
||||
// standSpeed is from swr.
|
||||
//
|
||||
if (LoadClipSlot(10, prefix, "wrr"))
|
||||
{
|
||||
legAnimation.SelectSequence(animationClips[10], NULL, 0, 0);
|
||||
reverseSpeedMax =
|
||||
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
||||
}
|
||||
LoadClipSlot(11, prefix, "wrl");
|
||||
|
||||
//
|
||||
// The run cycle.
|
||||
//
|
||||
if (
|
||||
LoadClipSlot(12, prefix, "rrr") &&
|
||||
LoadClipSlot(13, prefix, "rrl")
|
||||
)
|
||||
{
|
||||
MeasureClipStride(12, &total_a, &last_a);
|
||||
MeasureClipStride(13, &total_b, &last_b);
|
||||
reverseStrideLength = (total_a + total_b) / (last_a + last_b);
|
||||
}
|
||||
|
||||
LoadClipSlot(14, prefix, "rwr");
|
||||
LoadClipSlot(15, prefix, "rwl");
|
||||
|
||||
//
|
||||
// The bump/crash stagger clip, slot 0x20 -- the reason the clip array is
|
||||
// bigger than the state-name table.
|
||||
//
|
||||
LoadClipSlot(0x20, prefix, "bmp");
|
||||
|
||||
//
|
||||
// The reverse set. gimpSpeedMax is measured from the entry clip; the
|
||||
// cycle stride divide below reproduces the binary's stale-pair bug (see
|
||||
// the header comment) and is negated exactly where the binary negates.
|
||||
//
|
||||
if (LoadClipSlot(16, prefix, "sbr"))
|
||||
{
|
||||
legAnimation.SelectSequence(animationClips[16], NULL, 0, 0);
|
||||
gimpSpeedMax =
|
||||
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
||||
}
|
||||
LoadClipSlot(17, prefix, "sbl");
|
||||
LoadClipSlot(20, prefix, "bsr");
|
||||
LoadClipSlot(21, prefix, "bsl");
|
||||
|
||||
if (
|
||||
LoadClipSlot(18, prefix, "bbr") &&
|
||||
LoadClipSlot(19, prefix, "bbl")
|
||||
)
|
||||
{
|
||||
MeasureClipStride(18, &total_a, &last_a);
|
||||
MeasureClipStride(19, &total_a, &last_a); // the binary's stale pair:
|
||||
// total_b/last_b still hold
|
||||
// the run-cycle figures
|
||||
gimpStrideLength = (total_a + total_b) / (last_a + last_b);
|
||||
gimpStrideLength = -gimpStrideLength;
|
||||
}
|
||||
|
||||
//
|
||||
// The OPTIONAL limp set. Probe for wgl; a model without it has no limp
|
||||
// clips at all, and the limp machine must never be entered for it.
|
||||
//
|
||||
hasGimpClips = 0;
|
||||
if (ResolveAnimationClip(prefix, "wgl") != NULL)
|
||||
{
|
||||
hasGimpClips = 1;
|
||||
|
||||
if (LoadClipSlot(22, prefix, "wgl"))
|
||||
{
|
||||
legAnimation.SelectSequence(animationClips[22], NULL, 0, 0);
|
||||
gimpLeftSpeedMax =
|
||||
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
||||
}
|
||||
if (LoadClipSlot(23, prefix, "wgr"))
|
||||
{
|
||||
legAnimation.SelectSequence(animationClips[23], NULL, 0, 0);
|
||||
gimpRightSpeedMax =
|
||||
legAnimation.keyframeData[legAnimation.keyframeCount].stride;
|
||||
}
|
||||
if (LoadClipSlot(24, prefix, "ggr"))
|
||||
{
|
||||
MeasureClipStride(24, &total_a, &last_a);
|
||||
gimpLeftStrideLength = total_a / last_a;
|
||||
}
|
||||
if (LoadClipSlot(25, prefix, "ggl"))
|
||||
{
|
||||
MeasureClipStride(25, &total_a, &last_a);
|
||||
gimpRightStrideLength = total_a / last_a;
|
||||
}
|
||||
LoadClipSlot(26, prefix, "gsl");
|
||||
LoadClipSlot(27, prefix, "gsr");
|
||||
}
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -179,3 +179,59 @@ and `LoadLocomotionClipsExt` (@004a86c8, the 4-char-code variant). Note the
|
||||
manifest attributes all four to **mech2.cpp** while BT411 files them under
|
||||
mech3 — the manifest's attribution comes from the binary's own file tagging,
|
||||
so they belong here.
|
||||
|
||||
## The clip loader is in (2026-08-02) — and it ran live
|
||||
|
||||
`ResolveAnimationClip` / `MeasureClipStride` / `LoadClipSlot` /
|
||||
`LoadLocomotionClips` are reconstructed and WIRED: the ctor's GameModel block
|
||||
calls the loader while the model is locked, replacing the Phase 5.3 bring-up
|
||||
locomotion defaults with values measured from the actual clips. First live
|
||||
run (arena mission, MAD):
|
||||
|
||||
```
|
||||
[mech] clips 'mad': standSpeed=5.23 walkStride=18.51 revStride=56.05
|
||||
revSpeedMax=26.26 gimpSpeedMax=-4.23 gimpStride=-20.26 limpSet=1
|
||||
```
|
||||
|
||||
The 'mad' prefix printing as text is itself evidence the `Mech__ModelResource`
|
||||
layout is right at +0x40. The reverse figures come out negative, as the
|
||||
transition machines expect. **Driving feel changed with this**: speedDemand at
|
||||
0.6 throttle went 14.4 → 26.9, because the placeholder top speed (30) gave way
|
||||
to the measured 56.05. That is authenticity arriving, not a regression.
|
||||
|
||||
### Two binary behaviours reproduced on purpose
|
||||
|
||||
**The speed caps read `keyframeData[keyframeCount]`** — one entry past the
|
||||
last frame (`0x690 + 8 + [0x670]*0xc`). Whether the authored table carries
|
||||
count+1 entries or the read lands on adjacent resource bytes is not yet
|
||||
established; it is what the binary does, the clips were authored against it,
|
||||
and the measured values above look sane.
|
||||
|
||||
**The reverse-cycle stride is computed from STALE data.** The decomp is
|
||||
unambiguous: bbr and bbl are both measured into `local_8/local_c`, then the
|
||||
divide takes its second terms from `local_10/local_14` — still holding the
|
||||
run-left (rrl) figures. `gimpStrideLength = -((bbl + rrl_stale)/(bbl_t +
|
||||
rrl_t_stale))`. A 1995 copy-paste bug, shipped in every pod, reproduced here
|
||||
with a comment. The wwr/wwl and rrr/rrl blocks above it show the intended
|
||||
pattern. (Also settled: the negation IS in the binary — `0x350 = -0x350` on
|
||||
the very next instruction — an earlier decomp window cut just before it and
|
||||
briefly suggested otherwise.)
|
||||
|
||||
### One deliberate divergence
|
||||
|
||||
The binary dereferences every `ResolveAnimationClip` result unguarded — a
|
||||
model missing a mandatory clip crashes on load. Here a miss stores
|
||||
`NullResourceID` (SelectSequence resolves that to an empty, inert controller)
|
||||
and the dependent measurement is skipped, keeping the bring-up default.
|
||||
Tagged [T3] in the source; revisit once every fleet mech's clip set is
|
||||
known-good. The measurement binds also pass a NULL finished-callback where
|
||||
the binary passes live pointers — measurement only parses, never plays, so
|
||||
the callback cannot fire; NULL avoids arming a transition machine mid-load.
|
||||
|
||||
### What "next" looks like now
|
||||
|
||||
The array is filled and every constant is measured. The remaining half of
|
||||
this TU is the four `Advance*` entry points (wired into `Mech::Simulate`) and
|
||||
the two `Gimp*ClipFinished` machines. When `AdvanceLegAnimation` lands, the
|
||||
gait will select clips and SEQCTL will write joints — the first frame where
|
||||
the legs actually move.
|
||||
|
||||
Reference in New Issue
Block a user