BT410 5.3.90: the gait player is real -- SequenceController::Advance was the Fail() stub under the legs
Asked what was next for the rebuild, went to measure it, and found I had told
the operator the wrong thing one turn earlier.
THE CORRECTION. I said mech2.cpp (the gait state machine) was the next
package. It sits on top of SequenceController's playback methods, and all
three of those -- SelectSequence, Advance, Reset -- were Fail() stubs. Advance
is the function that actually walks a clip's keyframes and writes each
animated joint. mech2 without it would have called straight into a Fail. So
the dependency runs:
Mech::AdvanceLegAnimation mech2.cpp -- the STATE MACHINE, still absent
-> SequenceController::Advance THIS -- keyframes -> joint writes
-> Joint::SetHinge / SetRotation / SetTranslation
-> the joint DCS flush fixed in 5.3.88
5.3.88 fixed the bottom link, this fixes the middle, the top is still missing.
WHAT THIS DOES NOT DO: make the legs move. Nothing calls any of it yet -- no
gait state is ever selected because mech2 is unreconstructed -- so a run looks
identical to yesterday's. This removes a blocker. Saying otherwise would be
easy and wrong.
RECONSTRUCTED (compile-verified, BT 50/50, links clean):
SelectSequence @004277a8 -- find + lock the clip, parse its layout. The
fetch is FindResourceDescription and NOT SearchList: the ID arriving here is
already resolved, and SearchList would treat it as a resource LIST and walk
the clip bytes as IDs.
Advance @0042790c -- snap through every keyframe the new time has passed
(writing authored poses), then interpolate the partial frame. Returns the
forward distance covered, which is what the gait feeds into mech motion.
Reset @004283b8 -- return every animated joint to neutral, so an abandoned
gait doesn't leave the skeleton frozen on a stale frame.
THE PART THAT CANNOT BE SEEKED. The pose block is packed BY JOINT TYPE -- 8
bytes hinge, 12 ball, 24 ball+translation -- so the root-translation table
behind it is not reachable by arithmetic on any stored count. SelectSequence
must walk the entire skeleton summing per-joint sizes to find it. Which also
means the parse depends on the mech's own skeleton agreeing with the clip: an
unresolvable slot returns NULL and contributes 0, keeping the walk honest
rather than drifting keyframeData onto garbage.
TWO CONTRACTS worth recording before mech2 is written against them:
move_joints == 0 is NOT "do nothing" -- it advances the clock and
accumulates distance while leaving the skeleton alone. That is how the body
channel measures a stride without fighting the leg channel over the same
joints.
The finished callback is RE-ENTRANT BY DESIGN. At end of clip it picks the
next state, re-arms this controller through SelectSequence (rewinding it to
frame 0), and advances the carryover itself -- so its return value is the
distance that carryover covered, folded straight into Advance's own return.
mech2's BodyClipFinished has to honour that or the gait double-counts.
NOT CARRIED OVER from the donor: its BT_HIP_LOG diagnostic and audio
footstep-broadcast path, neither of which is 1995 code. footStepThreshold IS
parsed -- the field is real and authored -- but nothing reads it yet.
AND A CAVEAT ON THE 91% I QUOTED: seqctl.cpp is not in the 50-TU BT census at
all (its code sits below the BT address range the census was built from), and
that figure counts a TU as done if the FILE EXISTS -- 18 files still carry 26
Fail() stubs between them. The census understates what "playable" needs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,57 @@
|
|||||||
# include <mech.hpp>
|
# include <mech.hpp>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if !defined(JOINT_HPP)
|
||||||
|
# include <joint.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(APP_HPP)
|
||||||
|
# include <app.hpp>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// Local helpers.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// Slot -> skeleton joint, through the clip's own index map.
|
||||||
|
//
|
||||||
|
Joint *
|
||||||
|
SequenceController::GetAnimatedJoint(int slot) const
|
||||||
|
{
|
||||||
|
if (jointSubsystem == NULL || jointIndices == NULL)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
Verify(slot >= 0 && slot < jointCount);
|
||||||
|
|
||||||
|
int
|
||||||
|
joint_index = jointIndices[slot];
|
||||||
|
if (joint_index < 0 || joint_index >= jointSubsystem->GetJointCount())
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return jointSubsystem->GetJoint(joint_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Component-wise ease between two orientations.
|
||||||
|
//
|
||||||
|
static EulerAngles
|
||||||
|
InterpolateAngles(
|
||||||
|
const EulerAngles &from,
|
||||||
|
const EulerAngles &to,
|
||||||
|
Scalar ratio
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return EulerAngles(
|
||||||
|
Radian((Scalar)from.pitch + ((Scalar)to.pitch - (Scalar)from.pitch) * ratio),
|
||||||
|
Radian((Scalar)from.yaw + ((Scalar)to.yaw - (Scalar)from.yaw) * ratio),
|
||||||
|
Radian((Scalar)from.roll + ((Scalar)to.roll - (Scalar)from.roll) * ratio));
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -65,33 +116,390 @@ void
|
|||||||
SequenceController::~SequenceController()
|
SequenceController::~SequenceController()
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
// Releases the locked clip resource when reconstructed; nothing is locked
|
// Release the clip this controller holds locked.
|
||||||
// while SelectSequence is staged.
|
|
||||||
//
|
//
|
||||||
|
if (clipResource != NULL)
|
||||||
|
{
|
||||||
|
((ResourceDescription *)clipResource)->Unlock();
|
||||||
|
clipResource = NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// Playback -- the per-frame gait engine (clip parse, keyframe interpolation,
|
// The clip resource layout, as SelectSequence parses it. Offsets are the
|
||||||
// joint writes). Exercised only once the mech is ticking; not yet
|
// binary's (@004277a8):
|
||||||
// reconstructed. See SEQCTL.NOTES.md.
|
//
|
||||||
|
// int frameCount hdr[0]
|
||||||
|
// int jointCount hdr[1]
|
||||||
|
// Scalar footStepThreshold hdr[2] -- authored contact height
|
||||||
|
// int jointIndices[jointCount] keyframe slot -> skeleton joint
|
||||||
|
// Scalar frameTimes[frameCount] keyframe timestamps, seconds
|
||||||
|
// <pose> per frame, per joint, PACKED by DOF
|
||||||
|
// Keyframe rootTranslations[frameCount] .stride == the forward step
|
||||||
|
//
|
||||||
|
// The pose block is packed by joint TYPE, so the root-translation table can
|
||||||
|
// only be located by walking the skeleton and summing the per-joint sizes --
|
||||||
|
// there is no count to seek by. PoseSize is that walk's per-entry size.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
|
||||||
|
static int
|
||||||
|
PoseSize(Joint *joint)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Hinge (X/Y/Z) = a Hinge; ball = EulerAngles; ball+translation = both.
|
||||||
|
//
|
||||||
|
if (joint == NULL)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
switch (joint->GetJointType())
|
||||||
|
{
|
||||||
|
case Joint::HingeXJointType:
|
||||||
|
case Joint::HingeYJointType:
|
||||||
|
case Joint::HingeZJointType:
|
||||||
|
return sizeof(Hinge);
|
||||||
|
|
||||||
|
case Joint::BallJointType:
|
||||||
|
return sizeof(EulerAngles);
|
||||||
|
|
||||||
|
case Joint::BallTranslationJointType:
|
||||||
|
return sizeof(EulerAngles) + sizeof(Point3D);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// SelectSequence (@004277a8) -- bind a gait clip. Stores the finished
|
||||||
|
// callback, rewinds playback, releases the previous clip, then finds, locks
|
||||||
|
// and parses the new one.
|
||||||
|
//
|
||||||
|
// The fetch is FindResourceDescription, NOT SearchList: the clip ID arriving
|
||||||
|
// here is already resolved, and SearchList would treat it as a resource LIST
|
||||||
|
// and walk the clip bytes as resource IDs.
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//
|
//
|
||||||
void
|
void
|
||||||
SequenceController::SelectSequence(int, void *, unsigned, unsigned)
|
SequenceController::SelectSequence(
|
||||||
|
int clip_id,
|
||||||
|
void *finished_callback,
|
||||||
|
unsigned callback_arg2,
|
||||||
|
unsigned callback_arg3
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Fail("SequenceController::SelectSequence -- seqctl.cpp not yet reconstructed");
|
Check_Pointer(this);
|
||||||
|
|
||||||
|
finishedCallback = finished_callback;
|
||||||
|
callbackArg2 = callback_arg2;
|
||||||
|
callbackArg3 = callback_arg3;
|
||||||
|
currentTime = 0.0f;
|
||||||
|
currentFrame = 0;
|
||||||
|
|
||||||
|
if (clipResource != NULL)
|
||||||
|
{
|
||||||
|
((ResourceDescription *)clipResource)->Unlock();
|
||||||
|
clipResource = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceFile
|
||||||
|
*resources = application->GetResourceFile();
|
||||||
|
Check_Pointer(resources);
|
||||||
|
|
||||||
|
ResourceDescription
|
||||||
|
*description = resources->FindResourceDescription(clip_id);
|
||||||
|
clipResource = description;
|
||||||
|
if (description == NULL)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// A mech whose skeleton omits this gait keeps an empty controller;
|
||||||
|
// Advance below is guarded on keyframeCount so playback is inert.
|
||||||
|
//
|
||||||
|
keyframeCount = 0;
|
||||||
|
jointCount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
description->Lock();
|
||||||
|
|
||||||
|
int
|
||||||
|
*header = (int *)description->resourceAddress;
|
||||||
|
Check_Pointer(header);
|
||||||
|
|
||||||
|
keyframeCount = header[0];
|
||||||
|
jointCount = header[1];
|
||||||
|
footStepThreshold = (Scalar *)(header + 2);
|
||||||
|
|
||||||
|
int
|
||||||
|
*cursor = header + 3;
|
||||||
|
jointIndices = cursor;
|
||||||
|
cursor += jointCount;
|
||||||
|
keyframeTimes = (Scalar *)cursor;
|
||||||
|
cursor += keyframeCount;
|
||||||
|
keyframeBase = cursor;
|
||||||
|
keyframeCursor = cursor;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Walk the packed pose block to find the root-translation table behind it.
|
||||||
|
//
|
||||||
|
char
|
||||||
|
*pose = (char *)cursor;
|
||||||
|
int
|
||||||
|
frame,
|
||||||
|
slot;
|
||||||
|
for (frame = 0; frame < keyframeCount; ++frame)
|
||||||
|
{
|
||||||
|
for (slot = 0; slot < jointCount; ++slot)
|
||||||
|
{
|
||||||
|
pose += PoseSize(GetAnimatedJoint(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyframeData = (Keyframe *)pose;
|
||||||
|
|
||||||
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// Advance (@0042790c) -- play the clip forward by time_slice and return the
|
||||||
|
// forward distance covered, which is what the gait feeds into the mech's
|
||||||
|
// motion.
|
||||||
|
//
|
||||||
|
// Two phases. First SNAP through every keyframe whose timestamp the new time
|
||||||
|
// has passed, writing each animated joint's authored pose as it goes. Then
|
||||||
|
// INTERPOLATE the remaining partial frame, easing every joint from where it
|
||||||
|
// is toward the next keyframe.
|
||||||
|
//
|
||||||
|
// move_joints == 0 advances the clock and accumulates distance WITHOUT
|
||||||
|
// touching the skeleton -- that is how the body channel measures a stride
|
||||||
|
// without fighting the leg channel for the same joints.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
Scalar
|
Scalar
|
||||||
SequenceController::Advance(Scalar, int)
|
SequenceController::Advance(Scalar time_slice, int move_joints)
|
||||||
{
|
{
|
||||||
Fail("SequenceController::Advance -- seqctl.cpp not yet reconstructed");
|
Check_Pointer(this);
|
||||||
return 0.0f;
|
|
||||||
|
Scalar
|
||||||
|
target_time = currentTime + time_slice,
|
||||||
|
distance = 0.0f;
|
||||||
|
char
|
||||||
|
*cursor = (char *)keyframeCursor;
|
||||||
|
int
|
||||||
|
slot;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Phase 1 -- snap through the keyframes reached this frame.
|
||||||
|
//
|
||||||
|
while (
|
||||||
|
currentFrame < keyframeCount &&
|
||||||
|
keyframeTimes[currentFrame] <= target_time
|
||||||
|
)
|
||||||
|
{
|
||||||
|
for (slot = 0; slot < jointCount; ++slot)
|
||||||
|
{
|
||||||
|
Joint
|
||||||
|
*joint = GetAnimatedJoint(slot);
|
||||||
|
|
||||||
|
switch (joint != NULL ? joint->GetJointType() : Joint::StaticJointType)
|
||||||
|
{
|
||||||
|
case Joint::HingeXJointType:
|
||||||
|
case Joint::HingeYJointType:
|
||||||
|
case Joint::HingeZJointType:
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
joint->SetHinge(*(const Hinge *)cursor);
|
||||||
|
}
|
||||||
|
cursor += sizeof(Hinge);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Joint::BallJointType:
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
joint->SetRotation(*(const EulerAngles *)cursor);
|
||||||
|
}
|
||||||
|
cursor += sizeof(EulerAngles);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Joint::BallTranslationJointType:
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
joint->SetRotation(*(const EulerAngles *)cursor);
|
||||||
|
}
|
||||||
|
cursor += sizeof(EulerAngles);
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
joint->SetTranslation(*(const Point3D *)cursor);
|
||||||
|
}
|
||||||
|
cursor += sizeof(Point3D);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
distance +=
|
||||||
|
(keyframeTimes[currentFrame] - currentTime) *
|
||||||
|
keyframeData[currentFrame].stride;
|
||||||
|
currentTime = keyframeTimes[currentFrame];
|
||||||
|
++currentFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFrame == keyframeCount)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// End of clip. The finished callback picks the next gait state and
|
||||||
|
// RE-ARMS this controller through SelectSequence (which rewinds us to
|
||||||
|
// frame 0), then advances the leftover time itself -- so on return the
|
||||||
|
// controller is already playing the next clip and its distance folds
|
||||||
|
// into ours. A NULL callback simply parks the clip at its end.
|
||||||
|
//
|
||||||
|
if (finishedCallback != NULL && keyframeCount > 0)
|
||||||
|
{
|
||||||
|
Scalar
|
||||||
|
carryover = target_time - keyframeTimes[keyframeCount - 1];
|
||||||
|
|
||||||
|
distance += (*(ClipFinishedCallback)finishedCallback)(
|
||||||
|
owner, callbackArg2, carryover, move_joints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Phase 2 -- the partial frame.
|
||||||
|
//
|
||||||
|
keyframeCursor = cursor;
|
||||||
|
|
||||||
|
Scalar
|
||||||
|
span = keyframeTimes[currentFrame] - currentTime,
|
||||||
|
ratio = (span > 0.0f) ? (target_time - currentTime) / span : 0.0f;
|
||||||
|
|
||||||
|
for (slot = 0; slot < jointCount; ++slot)
|
||||||
|
{
|
||||||
|
Joint
|
||||||
|
*joint = GetAnimatedJoint(slot);
|
||||||
|
|
||||||
|
switch (joint != NULL ? joint->GetJointType() : Joint::StaticJointType)
|
||||||
|
{
|
||||||
|
case Joint::HingeXJointType:
|
||||||
|
case Joint::HingeYJointType:
|
||||||
|
case Joint::HingeZJointType:
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
Scalar
|
||||||
|
from = (Scalar)joint->GetRadians(),
|
||||||
|
to = (Scalar)((const Hinge *)cursor)->rotationAmount;
|
||||||
|
joint->SetRotation(Radian(from + (to - from) * ratio));
|
||||||
|
}
|
||||||
|
cursor += sizeof(Hinge);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Joint::BallJointType:
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
joint->SetRotation(
|
||||||
|
InterpolateAngles(
|
||||||
|
joint->GetEulerAngles(),
|
||||||
|
*(const EulerAngles *)cursor,
|
||||||
|
ratio));
|
||||||
|
}
|
||||||
|
cursor += sizeof(EulerAngles);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Joint::BallTranslationJointType:
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
joint->SetRotation(
|
||||||
|
InterpolateAngles(
|
||||||
|
joint->GetEulerAngles(),
|
||||||
|
*(const EulerAngles *)cursor,
|
||||||
|
ratio));
|
||||||
|
}
|
||||||
|
cursor += sizeof(EulerAngles);
|
||||||
|
if (move_joints)
|
||||||
|
{
|
||||||
|
const Point3D
|
||||||
|
&to = *(const Point3D *)cursor;
|
||||||
|
Point3D
|
||||||
|
from = joint->GetTranslation();
|
||||||
|
Point3D
|
||||||
|
blended(
|
||||||
|
from.x + (to.x - from.x) * ratio,
|
||||||
|
from.y + (to.y - from.y) * ratio,
|
||||||
|
from.z + (to.z - from.z) * ratio);
|
||||||
|
joint->SetTranslation(blended);
|
||||||
|
}
|
||||||
|
cursor += sizeof(Point3D);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
distance += (target_time - currentTime) * keyframeData[currentFrame].stride;
|
||||||
|
currentTime = target_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
Check_Fpu();
|
||||||
|
return distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
// Reset (@004283b8) -- return every animated joint to its neutral pose. Only
|
||||||
|
// the loop form does anything; the mech calls it when a gait is abandoned
|
||||||
|
// (fall, death, respawn) so the skeleton does not keep the last frame of a
|
||||||
|
// clip that is no longer playing.
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
void
|
void
|
||||||
SequenceController::Reset(int)
|
SequenceController::Reset(int loop)
|
||||||
{
|
{
|
||||||
Fail("SequenceController::Reset -- seqctl.cpp not yet reconstructed");
|
Check_Pointer(this);
|
||||||
|
|
||||||
|
if (loop == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EulerAngles
|
||||||
|
neutral_angles(Radian(0.0f), Radian(0.0f), Radian(0.0f));
|
||||||
|
Point3D
|
||||||
|
neutral_point(0.0f, 0.0f, 0.0f);
|
||||||
|
int
|
||||||
|
slot;
|
||||||
|
|
||||||
|
for (slot = 0; slot < jointCount; ++slot)
|
||||||
|
{
|
||||||
|
Joint
|
||||||
|
*joint = GetAnimatedJoint(slot);
|
||||||
|
if (joint == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (joint->GetJointType())
|
||||||
|
{
|
||||||
|
case Joint::HingeXJointType:
|
||||||
|
case Joint::HingeYJointType:
|
||||||
|
case Joint::HingeZJointType:
|
||||||
|
joint->SetRotation(Radian(0.0f));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Joint::BallJointType:
|
||||||
|
joint->SetRotation(neutral_angles);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Joint::BallTranslationJointType:
|
||||||
|
joint->SetRotation(neutral_angles);
|
||||||
|
joint->SetTranslation(neutral_point);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,18 @@
|
|||||||
|
|
||||||
//##################### Forward Class Declarations #######################
|
//##################### Forward Class Declarations #######################
|
||||||
class Mech;
|
class Mech;
|
||||||
|
class Joint;
|
||||||
class JointSubsystem;
|
class JointSubsystem;
|
||||||
|
|
||||||
|
//
|
||||||
|
// A clip's finished callback: the gait state machine's chance to pick the
|
||||||
|
// next animation. It re-arms the controller and consumes the carryover
|
||||||
|
// time itself, returning the distance that carryover covered -- which is
|
||||||
|
// why Advance can fold its result straight into its own return.
|
||||||
|
//
|
||||||
|
typedef Scalar
|
||||||
|
(*ClipFinishedCallback)(Mech *, unsigned, Scalar, int);
|
||||||
|
|
||||||
//##########################################################################
|
//##########################################################################
|
||||||
//###################### SequenceController ##########################
|
//###################### SequenceController ##########################
|
||||||
//##########################################################################
|
//##########################################################################
|
||||||
@@ -56,6 +66,19 @@
|
|||||||
void
|
void
|
||||||
Reset(int loop);
|
Reset(int loop);
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Skeleton access
|
||||||
|
//
|
||||||
|
private:
|
||||||
|
//
|
||||||
|
// The clip stores its own slot ordering, so a keyframe slot reaches the
|
||||||
|
// skeleton only through the clip's jointIndices map. NULL when the
|
||||||
|
// subsystem is absent or the slot names a joint this mech lacks -- the
|
||||||
|
// playback paths all tolerate that and simply skip the pose entry.
|
||||||
|
//
|
||||||
|
Joint *
|
||||||
|
GetAnimatedJoint(int slot) const;
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Clip data (parsed by SelectSequence; keyframe accessors read by the gait)
|
// Clip data (parsed by SelectSequence; keyframe accessors read by the gait)
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# SEQCTL.HPP / .CPP — reconstruction notes
|
# SEQCTL.HPP / .CPP — reconstruction notes
|
||||||
|
|
||||||
**Status: struct + ctor/dtor + Init RECONSTRUCTED (compile-verified); the
|
**Status: FULLY RECONSTRUCTED (compile-verified 2026-08-02). The three
|
||||||
per-frame playback methods (SelectSequence / Advance / Reset) staged. Helper
|
per-frame playback methods -- SelectSequence / Advance / Reset -- are now real;
|
||||||
2/3 of the Mech embedded helpers ([[MECH-LAYOUT]] step 1).**
|
no `Fail()` stub remains in this TU.**
|
||||||
|
|
||||||
`SequenceController` — the BT keyframe-animation player embedded in the Mech as
|
`SequenceController` — the BT keyframe-animation player embedded in the Mech as
|
||||||
`legAnimation` (@0x65c) and `bodyAnimation` (@0x6bc). Plays a gait clip: walks
|
`legAnimation` (@0x65c) and `bodyAnimation` (@0x6bc). Plays a gait clip: walks
|
||||||
@@ -39,3 +39,74 @@ self-consistently.
|
|||||||
BT/SEQCTL.HPP + SEQCTL.CPP. Not in the authentic BT.MAK member list, so the
|
BT/SEQCTL.HPP + SEQCTL.CPP. Not in the authentic BT.MAK member list, so the
|
||||||
build appends seqctl.obj to bt.lib via the extra-objs fallback (order is
|
build appends seqctl.obj to bt.lib via the extra-objs fallback (order is
|
||||||
immaterial — no static init).
|
immaterial — no static init).
|
||||||
|
|
||||||
|
## Playback reconstructed (2026-08-02)
|
||||||
|
|
||||||
|
`Advance` is **the thing that actually writes mech joints** — the reason the
|
||||||
|
legs never moved is that this was a `Fail()` stub, not anything in the render
|
||||||
|
path. It is worth being precise about the dependency, because it reverses an
|
||||||
|
earlier claim of mine:
|
||||||
|
|
||||||
|
Mech::AdvanceLegAnimation (mech2.cpp -- the gait STATE MACHINE, still absent)
|
||||||
|
-> SequenceController::Advance (THIS -- walks keyframes, writes joints)
|
||||||
|
-> Joint::SetHinge / SetRotation / SetTranslation
|
||||||
|
-> the render side's joint DCS flush (BT410 5.3.88)
|
||||||
|
|
||||||
|
5.3.88 fixed the bottom link (hinges now flush as full matrices the renderer
|
||||||
|
applies) and this fixes the middle one. The top link is still missing, so
|
||||||
|
**nothing calls any of this yet** — mech2.cpp is unreconstructed and no gait
|
||||||
|
state is ever selected. This commit removes a blocker; it does not make legs
|
||||||
|
move, and a run will look identical.
|
||||||
|
|
||||||
|
### The clip layout, and the one part that can't be seeked
|
||||||
|
|
||||||
|
```
|
||||||
|
int frameCount hdr[0]
|
||||||
|
int jointCount hdr[1]
|
||||||
|
Scalar footStepThreshold hdr[2] authored contact height
|
||||||
|
int jointIndices[jointCount] slot -> skeleton joint
|
||||||
|
Scalar frameTimes[frameCount] keyframe timestamps
|
||||||
|
<pose> per frame, per joint, PACKED BY DOF
|
||||||
|
Keyframe rootTranslations[frameCount] .stride == the forward step
|
||||||
|
```
|
||||||
|
|
||||||
|
The pose block is packed by joint TYPE — 8 bytes for a hinge, 12 for a ball,
|
||||||
|
24 for ball+translation — so the root-translation table behind it cannot be
|
||||||
|
reached by arithmetic on any stored count. `SelectSequence` has to walk the
|
||||||
|
whole skeleton summing per-joint sizes to find it. That walk is `PoseSize`.
|
||||||
|
|
||||||
|
A consequence worth recording: the parse depends on the MECH'S OWN skeleton
|
||||||
|
agreeing with the clip's. If `jointIndices` named a joint the mech lacks, the
|
||||||
|
size walk would drift and `keyframeData` would point at garbage. Slots that
|
||||||
|
don't resolve return NULL and contribute 0, which keeps the walk honest.
|
||||||
|
|
||||||
|
### Two things in Advance that are easy to get wrong
|
||||||
|
|
||||||
|
**`move_joints == 0` is not "do nothing".** It advances the clock and
|
||||||
|
accumulates distance while leaving the skeleton alone. That is how the body
|
||||||
|
channel measures a stride without fighting the leg channel over the same
|
||||||
|
joints — both channels play clips, only one drives the pose.
|
||||||
|
|
||||||
|
**The finished callback is re-entrant by design.** At end of clip it picks the
|
||||||
|
next gait state, re-arms THIS controller via `SelectSequence` (rewinding it to
|
||||||
|
frame 0), and advances the carryover itself — so by the time it returns, the
|
||||||
|
controller is already playing the next clip and the callback's return value is
|
||||||
|
the distance that carryover covered. `Advance` folds it straight into its own
|
||||||
|
return. Reconstructing mech2's `BodyClipFinished` will need to honour that
|
||||||
|
contract exactly or the gait will double-count distance.
|
||||||
|
|
||||||
|
### Deliberately not carried over from the donor
|
||||||
|
|
||||||
|
BT411's `Advance` carries a `BT_HIP_LOG` diagnostic and an audio
|
||||||
|
footstep-broadcast path. Neither is 1995 code — the log is the port's own
|
||||||
|
debugging and the audio belongs to its `AudioComponent` work — so neither is
|
||||||
|
here. `footStepThreshold` IS parsed, because the field is real and in the
|
||||||
|
authored resource; nothing reads it yet.
|
||||||
|
|
||||||
|
### Not measured by the manifest
|
||||||
|
|
||||||
|
`seqctl.cpp` is **not in the 50-TU BT census** — its code sits in the
|
||||||
|
0x4277a8-0x428xxx range, below the BT range the census was built from. So the
|
||||||
|
"91% of the census reconstructed" figure never counted this file at all, and
|
||||||
|
the census understates what the gait needs. Worth remembering before quoting
|
||||||
|
that percentage as progress toward playable.
|
||||||
|
|||||||
Reference in New Issue
Block a user