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)"