From 6a96fb64202e148d3d11b46f0f6bf9a25be0bebc Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Fri, 7 Aug 2026 00:50:54 -0500 Subject: [PATCH] #52 the peer STANDING-LOCK: case 0's fallthrough was intercepted A replicant could not start walking between gait-change records. The port's body case 4 (the task-#64 lockstep twin) is an INSERTION sitting between case 0 and the advance group; in the binary case 4 is a MEMBER of that group (FUN_004a5678 @004a5678: case 2,3,4,5,8,... -- no turn block, no speed exit), so case 0's fallthrough is meant to land on Advance(). The insertion caught it. On a replicant that is not a race but an identity: case 0 arms walk iff standSpeed < bodyTargetSpeed, and the inserted block resets iff standSpeed < bspd -- where bspd IS bodyTargetSpeed for a replicant. Same expression, so arm and reset fire on the same frame, forever, and a peer parked at Standing with a live replicated demand never cycles. bodyCycleSpeed stays 0 while position advances from dead reckoning: the skate. This is the sequel to e91d447 (#82). Before it the replicant branch read the dead local mapper cell (0 forever), the exit never fired, and the fallthrough worked BY ACCIDENT. Fixing the dead cell closed the escape hatch. Fix: case 0 -> goto advance_body_normally, the leg twin's own idiom, restoring the binary's structure without touching the #64/#82 turn logic. BT_NO_BODY_FALLTHRU=1 reverts. Measured (2-node, scratchpad/night13/skatelock.sh): legacy 336 consecutive locked seconds, bspd=39.2324 bts=39.2324 every line fixed 0 locks, every pass master body-Standing samples 52 -> 21 (it locked too, invisibly at mj=0) turn-in-place intact: pivoter body state 4 x9 / leg state 4 x8, in lockstep Diagnostics (both keepers): [skate] now carries bstate= -- the field lines proved "both channels idle" but never named the state, which was the whole answer; [bodySM]/[peergait] under BT_BODY_SM_LOG instrument the arm->reset pair and a moving replicant's body channel. NOT claimed: that this accounts for the night-13 field episodes. That link is inference -- locked + translating IS the skate signature by construction, but no bench caught the two together. bstate= settles it next playtest. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ --- context/locomotion.md | 37 ++++++++ game/reconstructed/mech2.cpp | 52 ++++++++++- game/reconstructed/mechdmg.cpp | 26 +++++- scratchpad/night13/find_dupes.py | 22 +++++ scratchpad/night13/housekeep13.py | 143 +++++++++++++++++++++++++++++ scratchpad/night13/housekeep13b.py | 66 +++++++++++++ scratchpad/night13/list_open.py | 13 +++ scratchpad/night13/show.py | 11 +++ scratchpad/night13/skatelock.sh | 112 ++++++++++++++++++++++ scratchpad/night13/skatelock2.sh | 83 +++++++++++++++++ scratchpad/night13/turnreg.sh | 72 +++++++++++++++ 11 files changed, 635 insertions(+), 2 deletions(-) create mode 100644 scratchpad/night13/find_dupes.py create mode 100644 scratchpad/night13/housekeep13.py create mode 100644 scratchpad/night13/housekeep13b.py create mode 100644 scratchpad/night13/list_open.py create mode 100644 scratchpad/night13/show.py create mode 100644 scratchpad/night13/skatelock.sh create mode 100644 scratchpad/night13/skatelock2.sh create mode 100644 scratchpad/night13/turnreg.sh diff --git a/context/locomotion.md b/context/locomotion.md index 98ca253..0f93e67 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -273,6 +273,43 @@ number the peer Standing case walks on). Verified: arena circle replicant 218×s 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.** +**The #52 SEQUEL: the peer body-channel STANDING-LOCK (root-caused + fixed 2026-08-07) [T2].** +Fixing (2) above closed an ACCIDENTAL escape hatch and the skate came back in a new shape. The +port's body case 4 is an INSERTION (the task-#64 lockstep twin) sitting between case 0 and the +advance group — but in the binary `case 4` is a **member of that advance group** +(`FUN_004a5678` @004a5678: `case 2,3,`**`4`**`,5,8,…`, no turn block, no speed exit) [T1], so +case 0's fallthrough is supposed to land on `Advance()`. The insertion intercepted it. On a +REPLICANT that is fatal and not a race: case 0 arms walk iff `standSpeed < bodyTargetSpeed`, and +the inserted block's exit tests `standSpeed < bspd` where `bspd` **IS** `bodyTargetSpeed` on a +replicant — the *same expression*. Arm and reset therefore fire on the same frame, every frame, +and a peer parked at Standing with a live replicated demand can never start cycling (reverse +likewise: both sides test `< ZeroSpeed`). Before e91d447 the replicant branch read the dead mapper +cell (0 forever) so the exit never fired and the fallthrough worked by accident. **Fix:** case 0 +`goto advance_body_normally` — the leg twin's own idiom (`goto advance_normally`, mech2.cpp) — +restoring the binary's structure without touching the #64/#82 turn logic. `BT_NO_BODY_FALLTHRU=1` +reverts. Measured: legacy 336 consecutive locked seconds with `bspd=39.2324 bts=39.2324` identical +on every line; fixed 0 locks across every pass; the MASTER's body-Standing samples also fell 52→21 +(it was locking too, invisibly — mj=0 writes no joints, and its two tests read *different* cells so +it only stalls in the window where they disagree). Turn-in-place re-verified under the fix (pivoter +reached body state 4 ×9 / leg state 4 ×8 — armed in lockstep). + +**Why a peer must be able to self-arm walking at all** (the load-bearing bit behind +`mech4.cpp` "stand; case 0 walk-begins next tick"): the peer's body state is set directly from +`record->legState` only on **type-3 edges** (`ReadUpdateRecord`), and entering Standing emits one +while *leaving* it does not. So between gait-change records a replicant is REQUIRED to derive +walking itself from the replicated `bodyTargetSpeed`. That is why the lock needs a mech holding a +*steady* demand — a mech whose gait keeps changing keeps getting rescued by records, which is why +free-walking and wall-jammed benches each reproduce only half the symptom. [T2] + +⚠ **The field symptom link is [T3], not T2.** The Standing-lock is proven and proven removed; that +it accounts for the night-13 episodes is inference (a locked peer has `bodyCycleSpeed==0` and never +advances its clip, so locked + translating *is* the `[skate]` signature by construction) — but no +bench caught the two together. The `[skate]` line now carries `bstate=`, so the next playtest +settles it: episodes gone → confirmed; any survivor names its own state. **NB the night-12 +`skatebench` "reproductions" were a DETECTOR ARTIFACT** — the first detector build tested only +`legCycleSpeed==0`, which is normal on a peer (the body channel poses it), so it fired on every +healthy movement phase. Old-format lines (`legCycleSpeed=`, no `bodyCyc=`) are not evidence. + ## 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 93f5bf8..d5d8bce 100644 --- a/game/reconstructed/mech2.cpp +++ b/game/reconstructed/mech2.cpp @@ -1246,6 +1246,10 @@ Scalar Mech::AdvanceBodyAnimation(Scalar time_slice, int loop) { Scalar distance = 0.0f; + // #52 probe (BT_BODY_SM_LOG): case 0 and the inserted turn block below run + // in the SAME invocation, so a plain local proves the arm->reset pair -- + // no cross-frame state, no per-mech bookkeeping. + int armedFromStanding = 0; // In the binary `bodyAnimationState`@0x728 IS `bodyStateAlarm`'s level (one field); // the reconstruction split them, so SetBodyAnimation's `bodyStateAlarm.SetLevel(state)` @@ -1297,7 +1301,35 @@ Scalar } SetBodyAnimation(0x10); } - // FALLTHROUGH + // FALLTHROUGH -- into the ADVANCE GROUP, which is where the binary + // sends it. #52 SKATE ROOT CAUSE (2026-08-07): in FUN_004a5678 case 4 + // is a MEMBER of the advance list (case 2,3,*4*,5,8,...), so a state + // just armed away from Standing lands on Advance(). The port's turn + // block below is an INSERTION (task #64 lockstep twin) and, sitting + // between case 0 and the advance group, it intercepted that fallthrough. + // On a REPLICANT that is fatal and not a race: case 0 arms walk iff + // `standSpeed < bodyTargetSpeed`, and the inserted block's exit tests + // `standSpeed < bspd` where bspd IS bodyTargetSpeed for a replicant -- + // the SAME expression. Arm and reset therefore fire on the same frame, + // every frame, and a peer parked at Standing with a live replicated + // demand can never start cycling (reverse likewise: both sides test + // `< ZeroSpeed`). It cycles again only when a record sets the state + // directly (ReadUpdateRecord, mech.cpp) -- the observed self-recovery. + // Masters escape because their two tests read DIFFERENT cells + // (bodyTargetSpeed = last-sent vs the live mapper speedDemand) and + // because the body channel is mj=0 there, so its stall is invisible. + // Introduced by e91d447 (#82): before it the replicant branch read the + // dead mapper cell (0 forever), so the exit never fired and this + // fallthrough worked BY ACCIDENT. Fixing the dead cell closed the + // accidental escape hatch and the trn-lock skate came back as a + // Standing-lock skate. BT_NO_BODY_FALLTHRU=1 restores the old path. + armedFromStanding = (int)bodyStateAlarm.GetLevel(); + { + static const int s_bodyFallthru = getenv("BT_NO_BODY_FALLTHRU") ? 0 : 1; + if (s_bodyFallthru) + goto advance_body_normally; + } + // FALLTHROUGH (legacy path only) case 4: // TURN-IN-PLACE, LOCKSTEP twin (task #64) // The body channel runs trn in LOCKSTEP with the leg: armed together at @@ -1324,6 +1356,23 @@ Scalar : (bm != 0) ? bm->speedDemand : 0.0f; if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric) { + // #52 probe: when this fires on a state case 0 JUST armed, the + // mech is being pushed straight back to Standing on the same + // frame it tried to leave it -- the Standing-lock. On a + // replicant `bspd` IS the same cell case 0 tested, so the pair + // is unconditional, not a race. + if (armedFromStanding != 0 && getenv("BT_BODY_SM_LOG")) + { + static float s_bsm = 0.0f; s_bsm += time_slice; + if (s_bsm >= 1.0f) { s_bsm = 0.0f; + DEBUG_STREAM << "[bodySM] " << (GetInstance() == ReplicantInstance + ? "REPLICANT " : "master ") + << GetEntityID() << " case0 armed " << armedFromStanding + << " -> turn-block RESET to Standing bspd=" << (float)bspd + << " bts=" << (float)bodyTargetSpeed + << " standSpeed=" << (float)standSpeed + << " (STANDING-LOCK)\n" << std::flush; } + } bodyStateAlarm.SetLevel(0); ForceUpdate(8); distance = 0.0f; @@ -1347,6 +1396,7 @@ Scalar case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b: case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15: case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20: + advance_body_normally: // case 0's fallthrough target (leg twin: advance_normally) distance = bodyAnimation.Advance( // FUN_0042790c(this+0x6bc, ...) time_slice * globalTimeScale * idleStrideScale, loop); bodyCycleSpeed = distance / time_slice; // this+0x6b8 diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index e45fa58..edb89ed 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -1255,26 +1255,50 @@ void + fabsf((float)owner->bodyCycleSpeed); const int movingNoLegs = (step > 0.08f && step < 5.0f // 5+: teleport/warp && cyc < 0.05f) ? 1 : 0; + // #52 A/B probe (BT_BODY_SM_LOG): the POSITIVE half of the Standing-lock + // evidence. The [skate] line only speaks once a lock has ALSO produced + // 90 sustained moving frames; this says every second what a moving peer's + // body channel is actually doing. Locked: bstate=0, bodyCyc=0. Healthy: + // bstate in the walk/run family with a live cycle. + if (step > 0.08f && step < 5.0f && getenv("BT_BODY_SM_LOG")) + { + static float s_pg = 0.0f; s_pg += 1.0f / 30.0f; + if (s_pg >= 1.0f) { s_pg = 0.0f; + DEBUG_STREAM << "[peergait] replicant " << owner->GetEntityID() + << " step=" << step + << " bstate=" << (int)owner->bodyAnimationState + << " bodyCyc=" << (float)owner->bodyCycleSpeed + << " bts=" << (float)owner->bodyTargetSpeed + << (movingNoLegs ? " <-- IDLE CHANNELS" : "") + << "\n" << std::flush; } + } if (movingNoLegs) { if (++skateFrames > 90 && !skateLogged) { skateLogged = 1; + // bstate = the peer's BODY animation state (@0x728), the channel + // that poses a replicant (s_peerLegCh=0). Added 2026-08-07 after + // night 13: the field lines proved "moving with both channels + // idle" but not WHICH state it was idling in, and the answer + // (0 = Standing, pinned) is the whole diagnosis -- see #52. DEBUG_STREAM << "[skate] replicant " << owner->GetEntityID() << " SKATING: " << skateFrames << " frames moving (" << step << " u/frame) with legCyc=" << (float)owner->legCycleSpeed << " bodyCyc=" << (float)owner->bodyCycleSpeed << " bodyTargetSpeed=" << (float)owner->bodyTargetSpeed + << " bstate=" << (int)owner->bodyAnimationState << " destroyed=" << (int)owner->IsMechDestroyed() << " mode=" << (int)owner->MovementMode() << " at (" << px << "," << pz << ")\n" << std::flush; if (BTMatchLogActive()) BTMatchLog("SKATE", "mech=%d:%d frames=%d step=%.3f cyc=%.3f " - "cmdSpd=%.2f destroyed=%d mode=%d x=%.1f z=%.1f", + "cmdSpd=%.2f bstate=%d destroyed=%d mode=%d x=%.1f z=%.1f", BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(), skateFrames, step, cyc, (float)owner->bodyTargetSpeed, + (int)owner->bodyAnimationState, (int)owner->IsMechDestroyed(), (int)owner->MovementMode(), px, pz); } diff --git a/scratchpad/night13/find_dupes.py b/scratchpad/night13/find_dupes.py new file mode 100644 index 0000000..e9223b9 --- /dev/null +++ b/scratchpad/night13/find_dupes.py @@ -0,0 +1,22 @@ +import sys, re +sys.path.insert(0, r"C:\git\bt411\scratchpad\night7") +import gitea + +rows = gitea.all_issues("all") +rows = [r for r in rows if not r.get("pull_request")] +print("TOTAL ISSUES: %d" % len(rows)) +terms = { + "crouch": r"crouch", + "missile-dir": r"missile.*(direction|facing|foot|feet|track)|emitter", + "night-vis": r"night|darkness|visibilit|fog|thermal|infrared|\bIR\b|predator", + "smoke-all": r"smoke", + "layout-save": r"layout|glass_layout|\bsave\b", + "eject-splash": r"eject|panic|suicide|splash", + "torso-yaw": r"torso.*(twist|yaw)|feet.*fac", +} +for k, pat in terms.items(): + print("\n=== %s ===" % k) + rx = re.compile(pat, re.I) + for r in rows: + if rx.search(r["title"]): + print(" #%-4s %-7s %s" % (r["number"], r["state"], r["title"][:110])) diff --git a/scratchpad/night13/housekeep13.py b/scratchpad/night13/housekeep13.py new file mode 100644 index 0000000..8dbc828 --- /dev/null +++ b/scratchpad/night13/housekeep13.py @@ -0,0 +1,143 @@ +"""Night 13 (2026-08-06, build 4.11.817) tracker housekeeping. ASCII only.""" +import sys +sys.path.insert(0, r"C:\git\bt411\scratchpad\night7") +import gitea + +BUG, INV, WIP, HUD, AUDIO, NET, WORLD, AWAIT = 1, 2, 3, 4, 5, 6, 7, 8 +SRC = ("Source: playtest night 13 (2026-08-06, build **4.11.817 (6fcff95+)**), " + "Discord #play-testing. Field logs staged in `scratchpad/night13/` " + "(4 players + the pod cart).\n\n") +new = {} + +# ---------------------------------------------------------------- NEW ISSUES +i = gitea.create( + "REGRESSION (817): BT_GLASS_LAYOUT=save writes a layout file with the MFD and Secondary lines MISSING", + SRC + + "SAURON (Michael), repeatedly: \"the save command is no longer writing to the glass_layout.cfg " + "file since this latest build\" ... \"got my glass panels set, saved and borders off again now\" " + "-> \"and then relaunched and it reset again\" -> \"looks like it rewrote the glass_layout.cfg " + "again\".\n\n" + "**The diagnostic detail:** \"the glass_layout.cfg had all the MFDs and secondary lines missing, " + "but plasma was still there\". So `save` is not failing to write -- it writes a file in which the " + "MFD and Secondary/Radar entries are simply absent, and the Plasma entry survives. On the next " + "launch there is nothing to restore, so every panel resets and the border/bare state is lost too.\n\n" + "**Workaround (confirmed by SAURON):** set `BT_GLASS_LAYOUT=load` and restore a hand-kept backup " + "copy of `glass_layout.cfg` -- \"set to load and used backup copy of CFG file and working aok so far\".\n\n" + "Probably the same root cause: vwe_propwash the same night -- \"my screen order borked again so " + "I'll need the trick to re-align them\".\n\n" + "**Suspect [T4, unverified]:** today's pod-MFD work is the only thing that touched this area. " + "Candidates, in order: (a) `e179c70` / `67a4f09` -- `BT_POD_RGB` and the bare-panel mode changed " + "how panel surfaces are enumerated and added the new `monitor:` and `,bare` line " + "forms, so the WRITER may no longer emit a line for a surface it cannot express (or the panel " + "list it walks is now populated differently); (b) `d213c98` -- panel create/destroy moved out of " + "the PadRIO ctor into `LBE4ControlsManager`, which changes panel LIFETIME: if the save runs after " + "`BTGlassPanels_Destroy()`, the windows are already gone and there is nothing to serialize, which " + "would explain exactly this signature (Plasma is not a glass panel and is destroyed elsewhere, so " + "it alone survives).\n\n" + "Related: #76 (main cockpit + Plasma positions not restored) -- that is the older, much smaller " + "version of this; fix this one first, #76 may be a subset. Desktop glass mode only; the pod cart " + "is unaffected because it runs `BT_GLASS_LAYOUT=load` off a frozen master.", + labels=[BUG, HUD]) +new['layoutsave'] = i['number'] + +i = gitea.create( + "Missiles launch along the LEG/FOOT facing, then curve to the target -- peer POV only", + SRC + + "Oracle: \"the missile thing we observed where missiles are firing in the direction the mech feet " + "are facing\" ... \"and then coming around to track the target\" ... \"**the emitter is following " + "the foot facing**\".\n\n" + "Confirmed by Oracle as **peer POV only** (epilectrik: \"only from peer pov though right\" -> " + "\"yes correct\"): from your own cockpit the launch looks right; on a REPLICANT the launch vector " + "comes off the leg/hip yaw instead of the torso/turret yaw. The homing itself works -- the missile " + "curves onto the target after launch -- so this is the muzzle/emitter TRANSFORM on the replicated " + "mech, not the guidance.\n\n" + "Reads as the replicant's weapon hardpoint being attached to (or composed against) the wrong node " + "in the segment hierarchy -- the leg/root segment rather than the twisted torso. Compare against " + "the master-side emitter, which is correct.\n\n" + "Related: #37 (MadCat torso is BACKWARDS), #70 (torso twist stops working after respawn) -- all " + "three are torso-yaw composition on a replicated model, and may share a cause.", + labels=[BUG, NET]) +new['missiledir'] = i['number'] + +i = gitea.create( + "CROUCH does not toggle: the lamp flashes on every press but the mech stays STANDING", + SRC + + "Reported by Oracle and previously by SAURON. Oracle: \"the crouch button did not toggle to " + "display crouched\" ... \"no state change\" ... \"button flickers sometimes on press, **the light " + "next to it always flashes when pressed**, but state does not change. Remains in stand mode\".\n\n" + "So the INPUT is arriving and the annunciator responds -- the press is seen all the way to the " + "lamp -- but the crouch state itself never changes. That narrows it to the state transition / " + "gait request rather than the button wiring.\n\n" + "**Not to be confused with (and NOT a bug):** throttle-up does not stand you back up. Settled this " + "night by primary source -- Lynx, quoting the original manual: \"Throttle up should not make you " + "stand. Verified in manual. You must push the button again to stand. I agree that throttle up " + "'should' do that, but it's not designed that way.\" Standing requires a second CROUCH press, " + "which makes this bug worse than it looks: with no working toggle there is no designed way out of " + "crouch. Locomotion/CROUCH background: `context/locomotion.md`.", + labels=[BUG]) +new['crouch'] = i['number'] + +i = gitea.create( + "One panel hit makes smoke erupt from MULTIPLE/ALL locations at once (seen on the sensor special panel)", + SRC + + "Oracle: \"the smoke sometimes emitting from multiple locations all at once when a location like " + "the sensor special panel is hit during testing. I saw this several times. It's not just with the " + "special panel, but it might have something to do with reaching a certain damage threshold which " + "is fairly easy to do quickly with a special panel. So you hit the special with an alpha and " + "suddenly the entire target mech emits smoke from all panels.\"\n\n" + "**Open question from the reporter, and the key discriminator:** \"What I'm not sure about is if " + "it's emitting only from previously damaged panels.\" If it is only previously-damaged panels, " + "this is a threshold that re-triggers the effect on every already-damaged zone at once (a " + "retrigger/latch bug). If it is genuinely ALL panels, the emitter is being attached per-mech " + "instead of per-zone. Answer that first -- it picks the fix.\n\n" + "Suspected trigger is a whole-mech damage THRESHOLD being crossed rather than the specific zone, " + "which a special-panel alpha reaches quickly.\n\n" + "Related: #90 (flames/smoke render less often and shorter than the original), #114 (missile impact " + "smoke too thick), #129 (closed: respawned mech emitted the wreck plume) -- same damage-effect " + "emitter family.", + labels=[BUG, INV]) +new['smokeall'] = i['number'] + +i = gitea.create( + "No SPLASH damage from a PANIC/EJECT/suicide death (splash from normal deaths now confirmed working)", + SRC + + "SAURON: \"Splash damage looked good to me, splash worked on mechs that died close, **but no " + "splash on a panic / eject / suicide death**.\"\n\n" + "Filed as the leftover sub-case of #89, which is otherwise verified working this night and closed.\n\n" + "**Authenticity is NOT established** -- do not 'fix' this before deciding what the original did. A " + "panic eject is plausibly a different death path (the pilot leaves; whether the chassis still " + "detonates with an explosion payload is an open era question), so a self-destruct that does no " + "splash could be correct. Decide from the binary's eject/death path first: does the eject route " + "raise the same explosion object as a combat death, and does that object carry the splash payload?\n\n" + "Related: #89, #106 (closed: splash burstCount dropped), #118/#109 (EJECT/PANIC wiring), #134 " + "(closed: panic eject score penalty).", + labels=[INV]) +new['ejectsplash'] = i['number'] + +i = gitea.create( + "ThermalSight / IR ('predator vision'): the VISIBLE half is unimplemented -- and it was never a heat image", + SRC + + "epilectrik: \"IR we solved but I think haven't implemented yet\" -- decode exists, presentation " + "does not, and there was no tracker item for the visible half (#61 covers the dead ToggleLamp " + "handler; #123 is the same shape for the searchlight).\n\n" + "**Primary-source description of what it should look like** -- Oracle, who played the original " + "pods at Lazer Park, this night:\n" + "- \"the IR was not what you would expect ... it was called predator vision, but it just looked " + "more like a **random palette shift**\"\n" + "- \"it was **not a heat image**\"\n" + "- \"which is why we generally called it LSD vision in Lazer Park\"\n" + "- \"IMHO it did nothing to improve visibility. All I found was that you had to look for some " + "movement and go after that\"\n" + "- epilectrik's recollection: it was a **test mode in the TH division hardware** that they " + "switched on, so it was not a specifically designed effect. Oracle: \"hardware optimized\".\n\n" + "**Why this matters for the port [T3]:** do not build a thermal/heat-gradient shader. The target " + "is a palette/colour-table transform on the existing image -- most likely whatever the 1995 " + "hardware path did when its test mode was enabled. Scope it from the decomp (and, if it really is " + "a hardware mode, decide what the honest modern equivalent is) before writing anything.\n\n" + "Related: #61 (ThermalSight ToggleLamp), #123 (searchlight visible half -- the same " + "toggle-works/presentation-deferred split).", + labels=[INV, WORLD]) +new['ir'] = i['number'] + +print() +print("NEW:", new) diff --git a/scratchpad/night13/housekeep13b.py b/scratchpad/night13/housekeep13b.py new file mode 100644 index 0000000..145cdc7 --- /dev/null +++ b/scratchpad/night13/housekeep13b.py @@ -0,0 +1,66 @@ +"""Night 13 housekeeping, part 2: comments + state changes on existing issues.""" +import sys +sys.path.insert(0, r"C:\git\bt411\scratchpad\night7") +import gitea + +N13 = "Playtest night 13 (2026-08-06, build **4.11.817 (6fcff95+)**). " + +# ---- #89 SPLASH: verified working, close ----------------------------------- +gitea.close(89, + N13 + "**VERIFIED WORKING -- closing.**\n\n" + "SAURON: \"Splash damage looked good to me, **splash worked on mechs that died close**\". " + "epilectrik: \"splash damage seems ok?\" -- no dissent from any tester.\n\n" + "Oracle (original-pod player) also gave the era calibration that explains why splash reads as " + "inconsistent rather than absent, and says our current behaviour matches it: \"sometimes you " + "really catch it and sometimes not. It jogged my memory and I vividly recall that **being at " + "different elevations on a hillside sometimes made a real hash of it** -- the effect could be much " + "stronger or weaker\" ... \"it was **not** the mechwarrior 3/4 splash model\". SAURON: \"that seems " + "correct to me\". So an unpredictable, elevation-sensitive splash is AUTHENTIC and should not be " + "'fixed' into a clean radius falloff.\n\n" + "One sub-case survives and is filed separately as **#144**: no splash on a panic/eject/suicide " + "death (authenticity not yet established).") + +# ---- #108 GHOST MECH: recurred --------------------------------------------- +gitea.comment(108, + N13 + "**Still present, and it was the headline problem of the night.** epilectrik: \"**ghost mech " + "biggest surprise**\" when reviewing regressions.\n\n" + "No new detail beyond the existing repro in this issue -- recording the recurrence on 817 so the " + "issue is not read as stale. Field logs for all four players (3 desktop + the pod cart) are staged " + "in `scratchpad/night13/`, including the pod's matchlogs, which is the first time this issue has " + "log coverage from a hardware seat as well. Analysis: `docs/GHOST_MECH_ANALYSIS.md`.") + +# ---- #76 layout restore: superseded by #140 -------------------------------- +gitea.comment(76, + N13 + "**Overtaken by #140.** As of 817 the problem is no longer 'two windows fail to restore' -- " + "`BT_GLASS_LAYOUT=save` now writes a `glass_layout.cfg` with the MFD and Secondary lines missing " + "entirely (Plasma survives), so nothing restores at all. See #140 for the reports and the suspect " + "commits.\n\n" + "Fix #140 first; this issue may turn out to be a subset of it. Re-verify the two windows named " + "here only once saving writes a complete file again.") + +# ---- #123 searchlight / night visibility: era testimony -------------------- +gitea.comment(123, + N13 + "**Primary-source testimony on night visibility** from Oracle, who played the original pods " + "at Lazer Park. This is directly about the deferred half of this issue (the fog swap), and it " + "raises the priority of the fog over the beam:\n\n" + "- \"some maps were not very dark, some were very dark\"\n" + "- \"**the FOG was what really killed visibility**\"\n" + "- \"I do recall basically stumbling into each other point blank at times and **having to use " + "radar to navigate**\"\n" + "- \"it could be very difficult to make a shot in those conditions\"\n" + "- \"it was 90's tech there\"\n\n" + "Takeaway for the implementation: the authentic night experience is driven by FOG DENSITY, not by " + "ambient darkness, and it was severe enough that radar navigation was the norm. The " + "`searchlightfog=` / `nosearchlightfog=` environment swap described above is therefore the " + "high-value half of this work, not the projected beam.\n\n" + "Operator note the same night: general night-visibility tuning is **deferred** until the correct " + "behaviour is known -- epilectrik: \"the night time visibility I might defer for now since we " + "don't really know the correct behavior ... I'll try to retrieve it from the binary again but " + "might be a hardware side thing\". This comment is that missing calibration.") + +# ---- #61 ThermalSight: point at the new visible-half issue ----------------- +gitea.comment(61, + N13 + "Cross-ref: the **visible** half of ThermalSight now has its own issue, **#145**, with " + "primary-source description of what the IR mode actually looked like (a palette shift, explicitly " + "NOT a heat image -- possibly a hardware test mode rather than a designed effect). Worth reading " + "before anyone implements a presentation off the toggle this issue restored.") diff --git a/scratchpad/night13/list_open.py b/scratchpad/night13/list_open.py new file mode 100644 index 0000000..accd808 --- /dev/null +++ b/scratchpad/night13/list_open.py @@ -0,0 +1,13 @@ +import sys +sys.path.insert(0, r"C:\git\bt411\scratchpad\night7") +import gitea + +labels = gitea.call("/labels") +print("LABELS:", ", ".join("%s=%s" % (l["id"], l["name"]) for l in labels)) +print() +rows = [r for r in gitea.all_issues("open") if "pull_request" not in r or not r.get("pull_request")] +rows.sort(key=lambda r: r["number"]) +print("OPEN ISSUES: %d" % len(rows)) +for r in rows: + lab = ",".join(l["name"] for l in r.get("labels", [])) + print("#%-4s [%-22s] %s" % (r["number"], lab, r["title"][:120])) diff --git a/scratchpad/night13/show.py b/scratchpad/night13/show.py new file mode 100644 index 0000000..2531458 --- /dev/null +++ b/scratchpad/night13/show.py @@ -0,0 +1,11 @@ +import sys +sys.path.insert(0, r"C:\git\bt411\scratchpad\night7") +import gitea +for n in sys.argv[1:]: + i = gitea.call("/issues/%s" % n) + print("=" * 78) + print("#%s [%s] %s" % (i["number"], i["state"], i["title"])) + print("labels:", ",".join(l["name"] for l in i.get("labels", []))) + print("-" * 78) + print((i.get("body") or "")[:1800]) + print() diff --git a/scratchpad/night13/skatelock.sh b/scratchpad/night13/skatelock.sh new file mode 100644 index 0000000..318a5bf --- /dev/null +++ b/scratchpad/night13/skatelock.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# ========================================================================= +# #52 STANDING-LOCK bench -- the night-13 field composition. +# +# FIELD COMPOSITION (night 13, 4.11.817): a healthy PEER that pivots at +# stand and then accelerates. The peer's turn-arm block parks its body +# channel at Standing on the walk-demand yield (mech4.cpp:2853, "case 0 +# walk-begins next tick"); the claim under test is that case 0 CANNOT +# walk-begin on a replicant because the port's inserted turn block resets +# the state case 0 just armed, using the SAME expression. +# +# PROVOCATION: B chases A and holds at BT_GOTO_STOP. Every time A walks +# back out of that radius B re-acquires -- goto turns hard at 0.2 throttle +# (mech4.cpp:4108-4113) = TURNING AT SUB-WALK SPEED, which arms the peer's +# body state 4 -- then the heading aligns, throttle goes to 1.0, and the +# walk-demand yield fires. That is the trap, once per re-acquisition. +# +# READ IT ON A: B's replicant lives on A, so A's log carries [bodySM] and +# [skate] for B. Both nodes carry both gates anyway. +# +# MODE=legacy -> BT_NO_BODY_FALLTHRU=1 (pre-fix path; expect the lock) +# MODE=fixed -> default (expect no lock) +# ========================================================================= +set -x +MODE="${1:-fixed}" +DUR="${2:-230}" +. /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 +rm -f sl_${MODE}_a.log sl_${MODE}_b.log sl_${MODE}_relay.log + +bt_assert_player_env +bt_expert_egg MP.EGG SL.EGG +# map=CAVERN on purpose (not the usual grass/day combat default): the lock needs +# a mech holding a STEADY walk demand without its gait changing, and the +# reliable way to get that is a mech pushing into geometry -- throttle up, leg +# SM parked, no gait edges, so no type-3 record ever refreshes the peer. On +# open grass whether the autodriver finds a wall is luck: the first run locked +# for 336 consecutive seconds, the second for 1. Cavern guarantees it, and its +# walls also let the jammed mech SLIDE, which is what turns a lock into a +# visible skate. time=day only so the windows are watchable. +sed -i "s/^map=.*/map=cavern/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SL.EGG + +LEGACY="" +[ "$MODE" = "legacy" ] && LEGACY=1 + +# ROLES (v2, after the pass-1 miss): the mech we need MOVING WHILE LOCKED is +# the one being observed. v1 made the observer autodrive and it walked into +# the arena wall -- 336 locked seconds but ~zero translation, so the lock +# reproduced and the SKATE (which needs 90 sustained MOVING frames) did not. +# v2 puts the chaser on A: BT_GOTO with a tight stop radius keeps A walking at +# a target that keeps moving, so A gets a stop/turn/walk cycle (the lock entry) +# AND continuous travel (the symptom). B observes; B's own wall-bumping is +# irrelevant because we read A's replicant on B's log. +# v3 = back to the v1 roles, which is the rig that ACTUALLY reproduces. +# v2 (chaser observed) gave zero locks, and that is itself the finding: a mech +# whose gait keeps changing keeps emitting type-3 records, and each one sets the +# peer's body state directly (ReadUpdateRecord), so the peer never has to +# self-arm and never meets case 0. The lock needs the opposite -- a peer PARKED +# at Standing while the master holds a STEADY demand, so no refreshing record +# ever comes. A wall-jammed autodriver is exactly that, which is why v1 locked +# for 336 consecutive seconds. Keep it. +# ---- node B (back window): the OBSERVER -- chases, so it stays engaged ------ +( + export BT_GOTO=enemy BT_GOTO_STOP=150 BT_GOTO_LOG=1 + export BT_BODY_SM_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + [ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1 + bt_launch sl_${MODE}_b.log SL.EGG 0x0C -net 1601 +) +sleep 2 +# ---- node A (front window): the OBSERVED mech -- steady demand, held up ----- +( + export BT_AUTODRIVE=0.7 + export BT_BODY_SM_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + [ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1 + bt_launch sl_${MODE}_a.log SL.EGG 0x03 -net 1501 +) +sleep 5 +python ../tools/btconsole.py SL.EGG 127.0.0.1:1501 127.0.0.1:1601 > sl_${MODE}_relay.log 2>&1 & +RELAY=$! +sleep "$DUR" +kill $RELAY 2>/dev/null +bt_kill_ours +sleep 2 +# bt_launch's winpid poll can MISS (documented) -- pass 1 orphaned a node that +# then sat holding a -net port. Passes run strictly one at a time here, so a +# blanket sweep is safe and is the only thing that guarantees a clean slate. +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 + +echo "=================== MODE=$MODE ===================" +echo "--- THE LOCK: [bodySM] arm->reset pairs (1 Hz throttled) ---" +echo -n "on B (observing A, the chaser): "; grep -ac "STANDING-LOCK" sl_${MODE}_b.log +echo -n "on A (observing B): "; grep -ac "STANDING-LOCK" sl_${MODE}_a.log +grep -a "STANDING-LOCK" sl_${MODE}_b.log | sort -u | head -4 +echo +echo "--- THE PEER GAIT: what a MOVING replicant's body channel is doing ---" +echo -n "samples on B: "; grep -ac "peergait" sl_${MODE}_b.log +echo "body states seen while moving (B's view of A):" +grep -a "\[peergait\]" sl_${MODE}_b.log | grep -oaE "bstate=[0-9-]+" | sort | uniq -c | sort -rn | head -10 +echo "idle-channel samples (the skate condition):" +grep -ac "IDLE CHANNELS" sl_${MODE}_b.log +echo +echo "--- THE SYMPTOM: [skate] episodes ---" +echo -n "on B: "; grep -ac "SKATING" sl_${MODE}_b.log +grep -a "\[skate\]" sl_${MODE}_b.log | head -6 +echo -n "on A: "; grep -ac "SKATING" sl_${MODE}_a.log +echo +echo "--- A's drive cycle (arrive/re-acquire churn = lock entries) ---" +grep -a "\[goto\]" sl_${MODE}_a.log | grep -oaE "arr=[01]" | uniq -c | wc -l diff --git a/scratchpad/night13/skatelock2.sh b/scratchpad/night13/skatelock2.sh new file mode 100644 index 0000000..cfbcfee --- /dev/null +++ b/scratchpad/night13/skatelock2.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# ========================================================================= +# #52 STANDING-LOCK -- A/B on the rig that is KNOWN to reproduce. +# +# Composition lifted verbatim from scratchpad/night12/skatebench.sh, which +# produced 6 skate episodes in one 260s run (sk_run.out) and 4 in another. +# A walks continuously (autodrive) and is killed every ~40s by B, so it +# respawns WHILE MOVING -- and that is the field correlation both nights: +# night 12's skating clustered in the deaths-heavy final drop, and all four +# of night 13's episodes were in the last, deaths-heavy session. +# +# My own rigs (skatelock.sh v1/v2/v3) each failed to reproduce the SYMPTOM: +# a wall-jammed mech locks but does not translate, and a freely-walking mech +# transitions gait constantly, so records keep rescuing its peer copy. The +# kill cycle gives both halves at once -- a steady post-respawn walk demand +# with no gait edges, on a mech that is actually moving. +# +# B is the OBSERVER: A's replicant lives on B, so B's log carries [skate], +# [bodySM] and [peergait] for A. +# +# MODE=legacy -> BT_NO_BODY_FALLTHRU=1 (pre-fix; expect episodes) +# MODE=fixed -> default (expect none) +# ========================================================================= +set -x +MODE="${1:-fixed}" +DUR="${2:-260}" +. /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 +rm -f s2_${MODE}_a.log s2_${MODE}_b.log s2_${MODE}_relay.log + +bt_assert_player_env +bt_expert_egg MP.EGG S2.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" S2.EGG + +LEGACY="" +[ "$MODE" = "legacy" ] && LEGACY=1 + +# ---- node B: OBSERVER + killer (back window) ------------------------------ +( + export BT_MP_FORCE_DMG=1 + export BT_BODY_SM_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + [ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1 + bt_launch s2_${MODE}_b.log S2.EGG 0x0C -net 1601 +) +sleep 2 +# ---- node A: the walking victim; autodrive persists across respawns ------- +( + export BT_AUTODRIVE=0.8 + export BT_BODY_SM_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 + [ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1 + bt_launch s2_${MODE}_a.log S2.EGG 0x03 -net 1501 +) +sleep 5 +python ../tools/btconsole.py S2.EGG 127.0.0.1:1501 127.0.0.1:1601 > s2_${MODE}_relay.log 2>&1 & +RELAY=$! +sleep "$DUR" +kill $RELAY 2>/dev/null +bt_kill_ours +sleep 2 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 + +echo "=================== MODE=$MODE ===================" +echo -n "death cycles on A (the provocation actually ran): " +grep -acE "death cycle START" s2_${MODE}_a.log +echo +echo "--- THE SYMPTOM: [skate] episodes on B (A's replicant) ---" +echo -n "SKATING lines: "; grep -ac "SKATING" s2_${MODE}_b.log +grep -a "\[skate\]" s2_${MODE}_b.log | head -14 +echo +echo "--- THE MECHANISM: [bodySM] arm->reset pairs (1 Hz throttled) ---" +echo -n "STANDING-LOCK on B: "; grep -ac "STANDING-LOCK" s2_${MODE}_b.log +grep -a "STANDING-LOCK" s2_${MODE}_b.log | grep -a REPLICANT | sort -u | head -3 +echo +echo "--- THE POSITIVE: moving replicant's body state, 1 Hz ---" +grep -a "\[peergait\]" s2_${MODE}_b.log | grep -oaE "bstate=[0-9-]+" | sort | uniq -c | sort -rn | head -8 +echo -n "samples flagged IDLE CHANNELS: "; grep -ac "IDLE CHANNELS" s2_${MODE}_b.log +echo +echo "--- control: skate on A (B stands still -- expect 0) ---" +grep -ac "SKATING" s2_${MODE}_a.log diff --git a/scratchpad/night13/turnreg.sh b/scratchpad/night13/turnreg.sh new file mode 100644 index 0000000..9bc36c2 --- /dev/null +++ b/scratchpad/night13/turnreg.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# ========================================================================= +# TURN-IN-PLACE regression check for the #52 fallthrough fix. +# +# The fix only changes ONE path: case 0 no longer falls into the port's +# turn block. Entry INTO state 4 is unaffected -- masters arm it via the +# leg twin's lockstep (mech2.cpp:1014) and peers via the #82 block +# (mech4.cpp:2851), and both set the state so the NEXT frame dispatches +# straight to case 4. This bench exercises that claim instead of asserting +# it: force sustained PIVOTING and confirm state 4 is still entered, still +# advances, and still exits -- on both the master and the peer. +# +# PROVOCATION: BT_GOTO with a tiny throttle and a stop radius it can never +# reach -- the mech steers at the enemy forever while creeping below +# standSpeed, which is exactly the trn entry gate (turning + speed in +# [0, standSpeed] + turnCapable). +# ========================================================================= +set -x +MODE="${1:-fixed}" +DUR="${2:-150}" +. /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 +rm -f tr_${MODE}_a.log tr_${MODE}_b.log tr_${MODE}_relay.log + +bt_expert_egg MP.EGG TR.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" TR.EGG + +LEGACY="" +[ "$MODE" = "legacy" ] && LEGACY=1 + +# v2: MUTUAL goto converged instantly -- both mechs faced each other, err~0, +# nothing ever turned (146 samples parked at Standing). A pivot needs a target +# that keeps MOVING, so B autodrives away and A creeps-and-steers after it: +# heading error stays live while A's speed stays under standSpeed, which is the +# trn entry gate. +( export BT_AUTODRIVE=0.6 # B: the moving target + export BT_BODY_SM_LOG=1 BT_MP_LOG=1 + [ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1 + bt_launch tr_${MODE}_b.log TR.EGG 0x0C -net 1601 ) +sleep 2 +( export BT_GOTO=enemy BT_GOTO_STOP=5 BT_GOTO_THR=0.02 BT_GOTO_LOG=1 + export BT_BODY_SM_LOG=1 BT_MP_LOG=1 # A: the pivoter + [ -n "$LEGACY" ] && export BT_NO_BODY_FALLTHRU=1 + bt_launch tr_${MODE}_a.log TR.EGG 0x03 -net 1501 ) +sleep 5 +python ../tools/btconsole.py TR.EGG 127.0.0.1:1501 127.0.0.1:1601 > tr_${MODE}_relay.log 2>&1 & +RELAY=$! +sleep "$DUR" +kill $RELAY 2>/dev/null +bt_kill_ours +sleep 2 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 + +echo "=================== TURN REGRESSION MODE=$MODE ===================" +echo "--- MASTER: does the turn-in-place clip still get entered + advanced? ---" +for n in a b; do + echo "node $n body state=4 samples : $(grep -a '\[gaitSM\] adv=' tr_${MODE}_${n}.log | grep -caE ' state=4 ')" + echo "node $n leg state=4 samples : $(grep -a '\[gaitSM\] adv=' tr_${MODE}_${n}.log | grep -caE ' legState=4')" + echo "node $n full body distribution:" + grep -a "\[gaitSM\] adv=" tr_${MODE}_${n}.log | grep -oaE "state=[0-9]+" | sort | uniq -c | sort -rn | head -6 +done +echo +echo "--- PEER: does a replicant still pivot (body state 4 while moving)? ---" +grep -a "\[peergait\]" tr_${MODE}_b.log | grep -oaE "bstate=[0-9-]+" | sort | uniq -c | sort -rn | head -8 +echo +echo "--- no new lock / no skate ---" +echo -n "STANDING-LOCK a/b: "; echo "$(grep -ac 'STANDING-LOCK' tr_${MODE}_a.log) / $(grep -ac 'STANDING-LOCK' tr_${MODE}_b.log)" +echo -n "SKATING a/b: "; echo "$(grep -ac 'SKATING' tr_${MODE}_a.log) / $(grep -ac 'SKATING' tr_${MODE}_b.log)"