#142 crouch: duckState is a THREE-state posture -- the strip is an animation

Operator confirmed on screen: bduck.pcc is a real duck ANIMATION, and stepping
duckState 0->1->2 plays it.  So the attribute is not a flag:

    0 = standing        1 = moving between        2 = crouched

Everything else was already right -- asset, element (OneOfSeveralPixInt
@004c5204), factory registration, L4GAUGE.CFG:5001, and the attribute binding
(new [gauge] receipt confirms 'bduck.pcc' frames=3x1 attr=BOUND).  We were
writing a two-value flag into a three-frame strip, so frame 2 was unreachable
and the cockpit saw a snap: "it lights up and sticks, no animation".

The handler is back to the binary's exact write (duckState = 1, @0049fa00).
That value now MEANS the middle frame, so the press gives immediate visual
feedback and the earlier toggle divergence is retired.

Needed a separate duckRequest cell, which I tried twice to avoid:
  * duckState cannot be both the request and the display.  Settling it to the
    real posture destroys the request, so on the frame the squat clip parked
    the consumer read "crouched + pending" and issued the opposite direction --
    69 transitions from 2 presses, benched, twice.
  * reading the CACHED legAnimationState instead of the alarm made it worse:
    the cache refreshes only at the top of AdvanceLegAnimation, so right after
    SetLegAnimation it still reads the old state.  Read the alarm.
duckRequest is port-only, appended, never read by offset.

Also fixes a silent failure in the gauge factory: the missing-image path used
DebugStream -- the no-op ReconStream (project gotcha) -- so a strip that failed
to load reported NOTHING.  Now DEBUG_STREAM, plus an ungated one-line receipt
per element naming the image, frame grid, port and whether the attribute BOUND
or came back NULL.  That receipt is what proved the element was healthy and
sent me looking at the value instead of the plumbing.

Benched (crouch142.sh, madcat): 2 presses -> exactly 2 transitions,
SQUAT -> parked (settles to 2) then RISE (settles to 0).  Refusal while moving
still holds (posture=0, authentic per Lynx).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
This commit is contained in:
Joe DiPrima
2026-08-07 23:35:37 -05:00
co-authored by Claude Opus 5
parent fcd1a0ca8d
commit 03c4d55672
4 changed files with 95 additions and 48 deletions
+48 -34
View File
@@ -4450,15 +4450,30 @@ void
// where mapPosture is not ready RETRIES next frame instead of
// silently dropping the request, which also retires the old
// "request consumed, posture=N" miss.
const int duckWant = (duckState != 0);
const int duckActual = ((int)legStateAlarm.GetLevel() == 1); // parked in 'sqd'
// Already playing 'sqd'/'squ': the transition owns the channel, so
// do not re-issue it every frame while want != actual.
const int duckInTransit =
(legAnimationState == 2 || legAnimationState == 3);
if (squatCapable != 0 && duckWant != duckActual && !duckInTransit)
// THE POSTURE MACHINE (#142). duckState is the cockpit strip's frame:
// 0 = standing 1 = moving between 2 = crouched
// The handler writes 1 (the binary's exact behaviour) meaning "a move is
// pending", which doubles as the middle frame. Direction comes from the
// parked leg alarm, so no separate request cell is needed:
// parked -> the pending move is a RISE
// !parked -> the pending move is a SQUAT
// While the clip runs we hold 1; when it settles we write 0 or 2.
// Read the ALARM, not the cached legAnimationState member: the cache
// is only refreshed at the top of AdvanceLegAnimation, so in the
// frame right after SetLegAnimation it still reads the OLD state.
// With the cached read, "am I already moving?" answered no on the
// frame after issuing, the consumer re-issued, and the machine
// ping-ponged squat/rise -- 68 transitions from 2 presses, benched.
// SetLegAnimation writes the alarm synchronously, so the alarm is
// true the instant the clip is armed.
const int duckLegLvl = (int)legStateAlarm.GetLevel();
const int duckParked = (duckLegLvl == 1);
const int duckMoving = (duckLegLvl == 2 || duckLegLvl == 3); // 'sqd' / 'squ'
if (duckRequest != 0 && !duckMoving && squatCapable != 0)
{
if (duckWant && mapPosture == 1)
duckRequest = 0; // one shot, whatever happens
if (!duckParked && mapPosture == 1)
{
SetLegAnimation(2); // 'sqd' -- squat down
ForceUpdate(8);
@@ -4467,7 +4482,7 @@ void
if (getenv("BT_DUCK_LOG") || getenv("BT_GAIT_LOG"))
DEBUG_STREAM << "[duck] SQUAT (posture 1 -> leg clip 2)\n" << std::flush;
}
else if (!duckWant && mapPosture == 2)
else if (duckParked && mapPosture == 2)
{
SetLegAnimation(3); // 'squ' -- rise
ForceUpdate(8);
@@ -4478,34 +4493,33 @@ void
}
else
{
// GATE NOT READY -- and the posture selector reads 0 for a
// MOVING mech, which is the authentic rule: Lynx, "when a
// mech STOPS, crouch button lowers its stance". Benched:
// pressing crouch at a walk gives posture=0 and no squat.
//
// So snap the desired posture back to the actual one. An
// earlier revision retried instead, and that was worse than
// what it replaced: a crouch tapped while running QUEUED for
// 41 seconds and would fire the moment the pilot stopped,
// while the cockpit symbol read "crouched" the whole time a
// standing mech walked around. duckState is what the gauge
// strip draws -- it has to tell the truth, and the truth is
// that this mech did not crouch.
if (duckState != duckActual)
{
static int s_duckRefuse = 0;
if ((s_duckRefuse++ % 30) == 0)
DEBUG_STREAM << "[duck] REFUSED (not stopped): posture="
<< mapPosture << " mode=" << MovementMode()
<< " legLvl=" << (int)legStateAlarm.GetLevel()
<< " myo=" << myomerEffectiveness
<< " -- duckState " << duckState << " -> "
<< duckActual << "\n" << std::flush;
duckState = duckActual;
}
// Gate refuses -- posture reads 0 for a MOVING mech, which is the
// authentic rule (Lynx: "when a mech STOPS, crouch button lowers
// its stance"; benched: crouch at a walk gives posture=0). Settle
// the strip back to the truth instead of holding the mid frame or
// queueing the request for the next time the pilot stops.
static int s_duckRefuse = 0;
if ((s_duckRefuse++ % 30) == 0)
DEBUG_STREAM << "[duck] REFUSED (not stopped): posture="
<< mapPosture << " mode=" << MovementMode()
<< " myo=" << myomerEffectiveness << "\n" << std::flush;
}
}
// SETTLE, unconditionally, re-reading the alarm AFTER any issue
// above. This is the part that was missing: duckState stayed 1
// forever, so once a clip completed the block above kept firing and
// flipped direction every frame -- 68 transitions from 2 presses.
// The squat/rise clips finish fast enough that the alarm is already
// back to 0/1 by the next visit, so "am I still moving?" has to be
// asked fresh, and the strip settled whenever the answer is no.
{
const int lvlNow = (int)legStateAlarm.GetLevel();
if (lvlNow != 2 && lvlNow != 3) // not playing 'sqd'/'squ'
duckState = (lvlNow == 1) ? 2 : 0; // crouched : standing
// else: hold 1, the middle frame, while the clip runs.
}
// (3b) AIRBORNE AUTO-RISE -- recovered 2026-08-06 by the #60
// RE-EXPORT (the raw-disasm pass stopped at 0x4aa0af and missed
// this tail). Pseudocode @004a9b5c: