From e91d447405cec9979e7a3031d2583dc8be1bcb91 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Thu, 30 Jul 2026 18:26:15 -0500 Subject: [PATCH] #82 FINAL LAYER: the peer body-channel trn-lock -- a circling limper skated because the peer's turn-arm had no walk-demand yield and the trn speed exit read a dead replicant mapper cell The master limped clean (218x gimp-cycle records, zero trn) -- a _ReturnAddress trap on SetAnimationState(4) proved ALL peer trn arming came from the port's own mech4 turn-step block, not the type-3 reader. Two dead ends, one root: - mech4 peer arm: gated on !wantsWalk (standSpeed < bodyTargetSpeed), exits trn when walking demand appears -- the leg twin's authentic precedence (part_012.c:12013, the Standing speed test outranks the turn test). - mech2 body case 4: bspd reads bodyTargetSpeed on ReplicantInstance; the local mapper's speedDemand is a dead cell on a peer (reads 0 forever -> no exit). Verified 2-node timeline: arena circle replicant 218x state-24 / 0x state-4 (was 116x trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths. Diagnostics kept: BT_TRNTRAP ra-trap, BT_ANIMIND_CAP, [gimpfeed], [replgimp] extension. KB: locomotion.md trn-lock entry + the dead-mapper-cell lesson. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QRjrTQpJd6u9XyfnUbTB3v --- context/locomotion.md | 20 +++++++ game/reconstructed/mech2.cpp | 96 ++++++++++++++++++++++++++++++-- game/reconstructed/mech4.cpp | 18 +++++- scratchpad/night6/mp4_cross.sh | 2 +- scratchpad/night6/mp4_limp.sh | 14 +++-- scratchpad/night6/mp4_panel.sh | 2 +- scratchpad/night6/mp_gcircle.sh | 25 +++++++++ scratchpad/night6/mp_timeline.sh | 69 +++++++++++++++++++++++ 8 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 scratchpad/night6/mp_gcircle.sh create mode 100644 scratchpad/night6/mp_timeline.sh diff --git a/context/locomotion.md b/context/locomotion.md index 6facc7e..d556211 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -247,6 +247,26 @@ figure, printed under a misleading label since renamed `ggCapL/R`); field @0x52c has NO binary consumer. Advance caps, loader fills, crunch path and crash branch are all verified byte-faithful — the self-damage dispatch was the only gap. Replication detail in [[multiplayer]]. +**The LAST #82 layer: the peer body-channel TRN-LOCK (root-caused + fixed 2026-07-30) [T2].** +After the storm fix, a CIRCLING limper still skated on peers (user repro): post-knockdown recovery +lands the peer at body-stand; the port's peer turn-step arming block (mech4.cpp `PerformAndWatch`, +needed because the peer no longer runs the leg channel that cross-arms trn) saw +`replMppr->turnDemand` (circling → always turning) and armed body state 4 — and NOTHING could exit: +(a) body case 4's speed exit read the LOCAL mapper's `speedDemand`, a **dead cell on a replicant** +(nothing writes it → 0 forever); (b) turning never stops for a circler. Peer pinned in trn +(advance-only churn = leg-lifty skating) while `bodyTargetSpeed` held the replicated demand +(~24 u/s full). Measured via `[replgimp]` (`bts=24.6 standSpeed=6.83 bodyState=4` in 168/171 +samples) and a `_ReturnAddress` trap on `SetAnimationState(4)` (`[trntrap]`, BT_TRNTRAP → linker +map: ALL 60 hits from PerformAndWatch = the port's own arm, NOT the type-3 reader — the master +broadcasts clean gimp states). Fix pair, mirroring the leg twin's authentic precedence +(part_012.c:12013 — the Standing speed test outranks the turn test): (1) mech4 peer arm gated on +`!wantsWalk` (`standSpeed < bodyTargetSpeed || bts < 0`), exit trn when wantsWalk; (2) mech2 body +case 4 `bspd` reads `bodyTargetSpeed` on ReplicantInstance (the replicated demand — the same +number the peer Standing case walks on). Verified: arena circle replicant 218×state-24/0×state-4 +(was 116×trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths. **Lesson: on a +replicant, every `MechControlsMapper` demand cell except those explicitly re-derived +(`turnDemand`) is DEAD — any peer-side state machine judging a local mapper read is judging 0.** + ## Controls (`BT_REAL_CONTROLS`, default-on) `MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand` / `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s diff --git a/game/reconstructed/mech2.cpp b/game/reconstructed/mech2.cpp index 13395ae..2df2665 100644 --- a/game/reconstructed/mech2.cpp +++ b/game/reconstructed/mech2.cpp @@ -90,6 +90,7 @@ // #include +#include // _ReturnAddress -- the #82 trn-armer trap #include // AudioComponent -- the foot-plant step-intensity broadcast #include // AudioSource -- footstep-source identification #include // AudioResource::GetAudioLevelOfDetail @@ -250,11 +251,33 @@ void // StateIndicator::SetState's Verify(state= 0 && state < 0x21) { + // DIAG (BT_TRNTRAP): who arms state 4 on a REPLICANT? Module-relative + // ra (symbolize: tools/symcrash.py) -- the #82 trn-lock armer hunt. + if (state == 4 && getenv("BT_TRNTRAP") + && GetInstance() == ReplicantInstance) + { + static int s_tt = 0; + if (s_tt++ < 60) + { + char ttbuf[96]; + sprintf(ttbuf, "[trntrap] mech=%d ra=btl4+0x%lx", + (int)GetEntityID(), + (unsigned long)_ReturnAddress() + - (unsigned long)GetModuleHandleA(0)); + DEBUG_STREAM << ttbuf << std::endl << std::flush; + } + } // #78 audio-flake diag: print the LIVE indicator address once per mech // so a session's [attrbind] ptr can be checked against it (stale-bind // hypothesis: watchers bound to a recreated mech's dead indicator). if (getenv("BT_AUDIO_SPATIAL")) { static int s_ai=0; - if (s_ai++ < 6 || (state >= 22 && state <= 27 && s_ai < 200)) + // BT_ANIMIND_CAP: the 200-print budget hid every post-1-minute state + // (two #82 investigations mis-read the silence as "never entered") -- + // raise it for state-timeline diagnosis. + static int s_aiCap = -1; + if (s_aiCap < 0) { const char *c = getenv("BT_ANIMIND_CAP"); s_aiCap = (c && *c) ? atoi(c) : 200; } + if (s_ai++ < 6 || (state >= 22 && state <= 27 && s_ai < s_aiCap) + || (s_aiCap > 200 && s_ai < s_aiCap)) DEBUG_STREAM << "[animind] mech=" << (int)GetEntityID() << " &animationState=" << (void*)&animationState << " inst=" << (int)(GetInstance() == ReplicantInstance) @@ -833,8 +856,35 @@ Scalar // alias (an AV); controlsMapper is the typed mirror of roster slot 0. A mech // with no mapper reads demand 0 (idles) -- matching a zeroed binary roster. MechControlsMapper *mppr = MappingMapper(); // roster slot 0 (task #7) + // REPLICANT DEMAND FEED (#82 final root, 2026-07-30): a replicant's LOCAL + // mapper cell is a dead 0 (input never drives it), so every demand + // threshold in this machine failed on peers -- from STAND the walk-begin + // could never fire and the trn state churned on the turn signal forever + // ("lifting legs in an alternating turning fashion"). It never mattered + // while a peer was mid-cycle (clip-end chains keep cycles going, which is + // why straight-line/grass limpers replicated fine) -- it bit the moment a + // KNOCKDOWN recovery dropped the replicant to STAND. The binary replicant + // reads a LIVE demand here (its mapper cell replicates with the subsystem + // records); the port's replicated equivalent is bodyTargetSpeed (@0x6b4 -- + // every update record's speedDemand writes it on RX). [gimpfeed]-probe + // proof: AdvanceLegAnimationGimp never even runs on replicants, so THIS + // driver is the peer's one and only leg machine. Scalar commandedSpeed = - (mppr != 0) ? mppr->speedDemand : 0.0f; + (GetInstance() == ReplicantInstance) ? bodyTargetSpeed + : (mppr != 0) ? mppr->speedDemand : 0.0f; + // DIAG (BT_GIMPFEED): 1 Hz -- the NORMAL driver is the only leg machine a + // replicant runs; print every threshold operand it sees. + if (getenv("BT_GIMPFEED") && GetInstance() == ReplicantInstance) + { + static Scalar s_gfAcc = 0.0f; s_gfAcc += time_slice; + if (s_gfAcc >= 1.0f) { s_gfAcc = 0.0f; + DEBUG_STREAM << "[gimpfeed] cmd=" << commandedSpeed + << " bts=" << bodyTargetSpeed + << " standSpeed=" << standSpeed + << " state=" << (int)legStateAlarm.GetLevel() + << " gl=" << ([](Mech *m){ extern int BTMechGimpLevel(void*); return BTMechGimpLevel(m); })(this) + << std::endl << std::flush; } + } Scalar distance = 0.0f; // binary: legAnimationState@0x3b0 IS legStateAlarm's level (one field; the @@ -1251,7 +1301,15 @@ Scalar // re-enter walk on the same frame. { MechControlsMapper *bm = MappingMapper(); - const Scalar bspd = (bm != 0) ? bm->speedDemand : 0.0f; + // REPLICANT: the local mapper's speedDemand is a dead cell (nothing + // writes it on a peer -- reads 0 forever, so a peer that entered trn + // could NEVER take the speed exit = the #82 trn-lock). The peer's + // commanded speed is the REPLICATED demand (bodyTargetSpeed@0x6b4, + // stamped by every record RX) -- same source the peer Standing case + // walks on, so entry and exit judge the same number. + const Scalar bspd = (GetInstance() == ReplicantInstance) + ? bodyTargetSpeed + : (bm != 0) ? bm->speedDemand : 0.0f; if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric) { bodyStateAlarm.SetLevel(0); @@ -1613,12 +1671,38 @@ Scalar // straight at the speedDemand cell. MechControlsMapper *gimpMppr = MappingMapper(); static Scalar s_nullDemand = 0.0f; // no mapper -> demand 0, writes inert - ReconMotionSource *motionSource = gimpMppr - ? (ReconMotionSource *)&gimpMppr->speedDemand - : (ReconMotionSource *)&s_nullDemand; + // REPLICANT DEMAND FEED (#82 final root, 2026-07-30): a replicant's LOCAL + // mapper demand is a dead 0 (no input ever drives it), so every speed + // threshold in this machine read 0 on peers -- in particular the trn exit + // (standSpeed < commandedSpeed) could NEVER fire, and a gimped replicant + // that staggered through stand->trn was LOCKED in turn-in-place forever + // (the observed "lifting legs in an alternating turning fashion" skate; + // unblinded [animind] timeline: SIX transitions in 3 minutes, ending at + // state 4). The binary's replicant reads a LIVE demand here because the + // mapper's speedDemand cell replicates with the subsystem records; the + // port's replicated equivalent of that same value is bodyTargetSpeed + // (@0x6b4 -- every update record's speedDemand writes it on RX). Feed + // the machine from it on replicants; the gimp caps that write back + // through the source are transient there (the next record rewrites it), + // and masters keep the authentic live mapper cell. + ReconMotionSource *motionSource = + (GetInstance() == ReplicantInstance) + ? (ReconMotionSource *)&bodyTargetSpeed + : gimpMppr + ? (ReconMotionSource *)&gimpMppr->speedDemand + : (ReconMotionSource *)&s_nullDemand; Scalar distance = 0.0f; extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23) int mode = BTMechGimpLevel(this); // binary this+0x40 = gimp level + // DIAG (BT_GIMPFEED): 1 Hz -- what demand does this machine actually see? + if (getenv("BT_GIMPFEED") && GetInstance() == ReplicantInstance) + { + static Scalar s_gfAcc = 0.0f; s_gfAcc += time_slice; + if (s_gfAcc >= 1.0f) { s_gfAcc = 0.0f; + DEBUG_STREAM << "[gimpfeed] repl cmd=" << motionSource->commandedSpeed + << " bts=" << bodyTargetSpeed << " state=" << legAnimationState + << " standSpeed=" << standSpeed << " mode=" << mode << std::endl << std::flush; } + } // RE-SYNC alarm -> state member (see AdvanceBodyAnimationGimp note). legAnimationState = (int)legStateAlarm.GetLevel(); int state = legAnimationState; // this+0x3b0 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 1b46826..4391e87 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2436,10 +2436,19 @@ void const int bs = (int)bodyStateAlarm.GetLevel(); const bool turning = (replMppr->turnDemand > 0.05f || replMppr->turnDemand < -0.05f); - if (turning && bs == 0) // standing + turning -> enter turn + // YIELD TO WALK DEMAND (#82 trn-lock): the master only sits in + // trn when its commanded speed is at stand (the leg Standing + // case's speed test wins first, part_012.c:12013). A circling + // LIMPER keeps full demand (bts~24) while its yaw rate reads as + // turning here -- ungated, this block pinned the peer in body + // case 4 (advance-only, no exit) forever = the skating. Mirror + // the leg twin's precedence: walking demand outranks the pivot. + const bool wantsWalk = (standSpeed < bodyTargetSpeed + || bodyTargetSpeed < 0.0f /*reverse*/); + if (turning && bs == 0 && !wantsWalk) // standing pivot only SetBodyAnimation(4); - else if (!turning && bs == 4) // turn stopped -> back to stand - SetBodyAnimation(0); + else if (bs == 4 && (!turning || wantsWalk)) + SetBodyAnimation(0); // stand; case 0 walk-begins next tick } // GIMP routing (#78): a limping PEER runs the gimp machines too // (the graphicAlarm level replicates with the damage state). @@ -2456,6 +2465,9 @@ void s_ga = 0.0f; DEBUG_STREAM << "[replgimp] ent=" << (int)GetEntityID() << " gl=" << replGl + << " bts=" << bodyTargetSpeed + << " standSpeed=" << standSpeed + << " bodyState=" << (int)bodyStateAlarm.GetLevel() << " sim=" << (int)GetSimulationState() << " hasClips=" << hasGimpClips << " zones:"; for (int zi = 0; zi < damageZoneCount; ++zi) diff --git a/scratchpad/night6/mp4_cross.sh b/scratchpad/night6/mp4_cross.sh index 2cc19da..6a829f0 100644 --- a/scratchpad/night6/mp4_cross.sh +++ b/scratchpad/night6/mp4_cross.sh @@ -51,6 +51,6 @@ PC=$! sleep 480 kill $PC 2>/dev/null -taskkill //F //IM btl4.exe 2>/dev/null +bt_kill_ours rm -f MP4X.EGG echo "=== DONE ===" diff --git a/scratchpad/night6/mp4_limp.sh b/scratchpad/night6/mp4_limp.sh index 8bee0c3..296b83d 100644 --- a/scratchpad/night6/mp4_limp.sh +++ b/scratchpad/night6/mp4_limp.sh @@ -26,7 +26,13 @@ A=/c/git/bt411/scratchpad/night6/runs/$(date +%H%M%S); mkdir -p "$A"; mv matchlo bt_novice_egg MP4L.EGG MP4LN.EGG observer () { # $1=log $2=affinity $3=port - BT_MP_LOG=1 BT_SYNC_LOG=1 BT_REPL_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 \ + # STATIC + fully controllable from frame one. NO autopilot: BT_GOTO's + # handover is broken (after BT_GOTO_RELEASE the mech reads throttle 0 yet + # keeps advancing ~40u/s -- the control-mapper demand freezes instead of + # following the keyboard), so a released mech walks straight past whatever + # it was sent to watch. Map is arena1, the tightest-spawning map measured + # (pairwise 41-1055u vs grass ~1200u); the limper is the WHITE Black Hawk. + BT_MP_LOG=1 BT_SYNC_LOG=1 BT_REPL_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_TLOC_LOG=1 \ bt_launch "$1" MP4LN.EGG "$2" -net "$3" } observer mp4l_d.log 0xC0 1801 @@ -37,7 +43,7 @@ observer mp4l_b.log 0x0C 1601 sleep 1 # the LIMPER: symbolic leg zone, 2 ticks of 60 -> gimp (no kill) ~2s after # alive; slow tight circle from t0 so it never leaves its spawn area. -BT_MP_LOG=1 BT_DMG_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 \ +BT_MP_LOG=1 BT_DMG_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_TLOC_LOG=1 \ BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \ BT_AUTODRIVE=0.4 BT_FORCE_TURN=0.45 \ bt_launch mp4l_a.log MP4LN.EGG 0x03 -net 1501 @@ -46,8 +52,8 @@ python ../tools/btconsole.py MP4LN.EGG 127.0.0.1:1501 127.0.0.1:1601 \ 127.0.0.1:1701 127.0.0.1:1801 & PC=$! -sleep 420 +sleep 660 kill $PC 2>/dev/null -taskkill //F //IM btl4.exe 2>/dev/null +bt_kill_ours rm -f MP4LN.EGG echo "=== DONE ===" diff --git a/scratchpad/night6/mp4_panel.sh b/scratchpad/night6/mp4_panel.sh index 7b50b07..ed73021 100644 --- a/scratchpad/night6/mp4_panel.sh +++ b/scratchpad/night6/mp4_panel.sh @@ -43,6 +43,6 @@ PC=$! sleep 150 kill $PC 2>/dev/null -taskkill //F //IM btl4.exe 2>/dev/null +bt_kill_ours rm -f MP4X.EGG echo "=== DONE ===" diff --git a/scratchpad/night6/mp_gcircle.sh b/scratchpad/night6/mp_gcircle.sh new file mode 100644 index 0000000..1dc494e --- /dev/null +++ b/scratchpad/night6/mp_gcircle.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# g_circle regression only (extracted from mp_gimpAB.sh) +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +sed "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^experience=.*/experience=novice/" MP.EGG > AB.EGG +rm -f ab_g_circle_obs.log ab_g_circle_limp.log +BT_MP_LOG=1 BT_REPL_LOG=1 BT_AUDIO_SPATIAL=1 BT_SYNC_LOG=1 BT_ANIMIND_CAP=100000 \ + bt_launch ab_g_circle_obs.log AB.EGG 0x0C -net 1601 +sleep 2 +BT_FORCE_TURN=0.45 \ +BT_MP_LOG=1 BT_DMG_LOG=1 BT_AUDIO_SPATIAL=1 BT_GROUND_LOG=1 BT_ANIMIND_CAP=100000 \ +BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \ +BT_AUTODRIVE=0.4 \ + bt_launch ab_g_circle_limp.log AB.EGG 0x03 -net 1501 +sleep 4 +python ../tools/btconsole.py AB.EGG 127.0.0.1:1501 127.0.0.1:1601 & +PC=$! +sleep 180 +kill $PC 2>/dev/null +bt_kill_ours +rm -f AB.EGG +echo "=== DONE ===" diff --git a/scratchpad/night6/mp_timeline.sh b/scratchpad/night6/mp_timeline.sh new file mode 100644 index 0000000..1e4f7b1 --- /dev/null +++ b/scratchpad/night6/mp_timeline.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# #82 REPLICANT-LIMP A/B MATRIX: which factor breaks the peer's limp view -- +# TURNING (replicant trn-arming outprioritizes the gimp cycle at gimp speeds) +# or TERRAIN (arena1 ground staggers -> type-5 knockdown broadcasts)? +# +# 4 configs x 2 nodes x ~3min, sequential: +# g_straight grass, limper walks straight (the old mp_skate baseline) +# g_circle grass, limper circles (run-6 shape) +# a_straight arena1, limper walks straight +# a_circle arena1, limper circles (run-13 shape = the user's repro) +# +# VERDICT per config, from logs alone (no eyes needed): +# observer [animind] inst=1 states 22-27 -> replicant LIMPS (good) +# observer trn(4)/knockdown(32) churn -> skating (bad) +# observer type=5 rx count + limper [knock] rows -> the stagger factor +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 + +run_config () { # $1=tag $2=map $3=circle(0|1) + local tag=$1 map=$2 circle=$3 + # headless rig only: a leftover node from the previous config holds the -net + # port and null-runs this one -- sweep before every config + taskkill //F //IM btl4.exe > /dev/null 2>&1 + sleep 2 + sed "s/^map=.*/map=$map/; s/^time=.*/time=day/; s/^experience=.*/experience=novice/" MP.EGG > AB.EGG + rm -f ab_${tag}_obs.log ab_${tag}_limp.log + # observer: static, replicant-diagnostics on + BT_MP_LOG=1 BT_REPL_LOG=1 BT_AUDIO_SPATIAL=1 BT_SYNC_LOG=1 BT_ANIMIND_CAP=100000 BT_GIMPFEED=1 BT_TRNTRAP=1 \ + bt_launch ab_${tag}_obs.log AB.EGG 0x0C -net 1601 + sleep 2 + # limper: symbolic leg gimp ~2s after alive; straight or circling + if [ "$circle" = "1" ]; then + BT_FORCE_TURN=0.45 \ + BT_MP_LOG=1 BT_DMG_LOG=1 BT_AUDIO_SPATIAL=1 BT_GROUND_LOG=1 BT_ANIMIND_CAP=100000 \ + BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \ + BT_AUTODRIVE=0.4 \ + bt_launch ab_${tag}_limp.log AB.EGG 0x03 -net 1501 + else + BT_MP_LOG=1 BT_DMG_LOG=1 BT_AUDIO_SPATIAL=1 BT_GROUND_LOG=1 BT_ANIMIND_CAP=100000 \ + BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \ + BT_AUTODRIVE=0.4 \ + bt_launch ab_${tag}_limp.log AB.EGG 0x03 -net 1501 + fi + sleep 4 + python ../tools/btconsole.py AB.EGG 127.0.0.1:1501 127.0.0.1:1601 & + local PC=$! + sleep 180 + kill $PC 2>/dev/null + bt_kill_ours + sleep 3 +} + + +run_config a_circle arena1 1 +rm -f AB.EGG + +echo "=== A/B VERDICTS ===" +for tag in a_circle; do + obs=ab_${tag}_obs.log + gimp=$(grep -a '\[animind\]' $obs 2>/dev/null | grep 'inst=1' | grep -c 'state=2[2-7] ') + trn=$(grep -a '\[animind\]' $obs 2>/dev/null | grep 'inst=1' | grep -c 'state=4 ') + kd=$(grep -a '\[animind\]' $obs 2>/dev/null | grep 'inst=1' | grep -c 'state=32 ') + t5=$(grep -a '\[mrec-rx\]' $obs 2>/dev/null | grep -c 'type=5 ') + knocks=$(grep -ac '\[knock\]' ab_${tag}_limp.log 2>/dev/null) + gsim=$(grep -a '\[gimp-sim\]' ab_${tag}_limp.log 2>/dev/null | head -1) + echo "VERDICT $tag: replicantGimp=$gimp trn=$trn knockdown=$kd type5rx=$t5 limperKnocks=$knocks ($gsim)" +done +echo "=== DONE ==="