Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 b93f1e903f build ritual: antivirus smoke test before shipping a zip (#190). Windows Defender's ML classifier flagged btl4.exe as Trojan:Win32/Bearfoos.A!ml and DELETED it -- a false positive, and crucially PER-BINARY rather than per-project: of 17 archived builds extracted side by side only 4.11.957 was condemned (622 through 913 all survived), plus that evening's fresh build. Our exe fits the profile that trips these classifiers (unsigned, rebuilt every session so it never accrues reputation, raw sockets, process enumeration, writes beside itself), so each build is a coin flip; and it turns WITHOUT a code change -- the same binary ran clean across benches all evening, then the 8/14 19:34 signature update (1.457.172.0) condemned it hours later, which is why the field was quiet: nobody had launched since. Ritual added: extract the new exe OUTSIDE any Defender exclusion, wait ~60s (deletion is asynchronous -- an immediate Test-Path passes and the file vanishes a minute later), confirm it survives, and if not, submit to Microsoft as a false positive before shipping. Notes that a rename cannot help (content is what is scored; the exe name is constant, only the zip is versioned) and that an exclusion on the repo makes the test silently pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 01:26:39 -05:00
Joe DiPrimaandClaude Opus 5 45750a6fcf SENDLOGS.bat: fix the two field failures + stop it lying about success. Two testers hit DIFFERENT errors, both now reproduced on a rig and fixed.
(1) ConstrainedLanguage mode (locked-down Windows, WDAC/AppLocker) blocks .NET method calls, so [Environment]::GetFolderPath('Desktop') returned nothing -> null DestinationPath -> no zip, and [math]::Round blew up on the size. Desktop is now resolved from OneDrive/USERPROFILE env vars (still honouring a redirected Desktop) and the size via -f formatting. Deeper trap found while testing: Compress-Archive ITSELF cannot run in that mode -- it is a script module that calls .NET internally -- so fixing the path alone would only have moved the failure downstream. The zip is now built with Windows' native tar.exe (immune to language mode, present since Win10 1803, verified to emit a real Explorer-readable zip), with Compress-Archive kept only as a fallback.

(2) A log still held open by a running BattleTech made Compress-Archive abort the ENTIRE zip -- one locked file, no logs at all. Files are now staged with Copy-Item first: an unreadable one is skipped and NAMED, the zip still gets made from the rest, and the tester is told to close the game and re-run to capture the missing one.

(3) The script announced 'Wrote <path>' and 'Attach the BTLOGS zip' unconditionally -- tester 2's screenshot shows it claiming '11 files, 0 MB' for a zip that was never created. Success is now gated on Test-Path, failure exits 1, and the batch prints 'Nothing was created' instead of sending people to hunt for a file that does not exist.

Also adds ABOUT.txt inside the zip (machine, pack time, install folder, exe build date/version) so a report can be tied to a BUILD -- the ambiguity that has cost us repeatedly when a Pass/Fail arrives with no build number.

Verified on four scenarios: normal run, a log locked by another process, a session downgraded to ConstrainedLanguage (reproducing tester 1's exact MethodInvocationNotSupportedInConstrainedLanguage error first), and running from a folder with no content dir. Handout updated: close the game first, and what SKIPPED means.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 01:10:18 -05:00
Joe DiPrimaandClaude Opus 5 277337c3a5 tracker: date undated tester entries from gitea evidence, and record the self-reference trap. 12 entries pinned to real dates by finding where their text is quoted in a ticket body or an earlier comment -- if a report is quoted in a body, it existed by that ticket's creation date, which is how #170's three crash lines pin to 2026-08-12 (its body says FIELD 2026-08-12 and lists those exact times, settling that they are the ORIGINAL occurrences and not a post-fix recurrence). 27 entries that nothing in gitea pins stay marked (undated) rather than guessed. THE TRAP, hit once during this pass: dating an entry from a comment written while harvesting the sheet -- those quote the tester text verbatim, so a naive search dates the report to your own housekeeping instead of the event (a #170 line briefly carried the harvest date). Exclude same-day comments and take the earliest match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 00:50:56 -05:00
Joe DiPrimaandClaude Opus 5 a4986faffc tracker: date every tester entry, and mark unknown-provenance ones (undated) rather than guessing. The build is cut per session, so the date implies the build -- which is what makes a Pass or Fail interpretable at all. Entries carried over from before the convention are marked (undated) = provenance unknown, not fresh evidence: the sheet was seeded from earlier tracking and nobody can now say which rows are original, so stamping them all with the session date would manufacture precision. That is exactly the error that produced the false 'still crashing after the fix' claim on #170 -- its row carries the same three timestamps and the same players as its own 2026-08-12 body, and whether that is a copy-forward or a genuine recurrence is STILL UNRESOLVED. 41 legacy entries relabelled (undated); 17 tester-dated entries left intact; header hover notes on both Tester Comments columns teach the convention in the sheet itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 00:17:18 -05:00
Joe DiPrimaandClaude Opus 5 dba09e44b9 tracker: add the FAILED RETEST status + the explicit post-playtest sweep. A tester Fail on a shipped fix was collapsing into QUEUED, which reads as 'never started' and understates a regression -- it now gets its own red status. Sweep rules made explicit per Test Status on every PATCHED-RETEST row: Pass -> close + move to CLOSED; Fail with detail -> FAILED RETEST + reopen + raise to >=P2 + post the evidence; Fail without detail -> NEED INFO with ONE discriminating question; blank -> stays yellow and rolls forward. Also records the ambiguity underneath all of it: a Pass or Fail carrying no BUILD NUMBER is uninterpretable (a Pass on an older build cannot verify a later fix), which is exactly why #27 and #38 sit at NEED INFO asking for the boot-screen build instead of going straight to red -- so every PATCHED-RETEST comment should stamp the build the fix shipped in. Sheet hover legends on the Test Status and Dev Status headers rewritten to teach both the vocabulary and the sweep to testers directly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:42:44 -05:00
Joe DiPrimaandClaude Opus 5 74534cec9e KB: the playtest tracker sheet is now a first-class topic (context/playtest-tracker.md) -- the testers moved issue tracking off the .txt handout onto a shared Google Sheet on 2026-08-14, making it the primary feedback channel with gitea still the system of record. Documents the two-tab shape (OPEN priority-ordered / CLOSED), the dev-status vocabulary with the colour EMBEDDED IN THE LABEL, the priority tiers, and the Dev-Comments contract (what is wrong / STATUS / NEED FROM YOU, written for a player in under ~55 words with a concrete ask). Captures the working rules that were learned the hard way this session: read the gitea COMMENT THREAD never the body (re-triaging 53 tickets from threads moved a third of the statuses and dropped P1 from 5 to 2); undated tester rows are HISTORICAL and reading them as fresh produced a false 'still crashing after the fix' claim on #170 that had to be retracted across seven tickets; never write DONE on the OPEN tab; merge duplicates to one row per ticket; never use cell fills for status (fills live on cells, labels on text -- every re-sort desyncs them, re-fixed three times before embedding); a leftover FILTER hides rows after a rewrite (skipping row numbers in the gutter = hidden rows, not lost data); pin row heights or autofit makes wrapped rows unreadably tall. Access documented both ways: read via the public CSV export (the Claude Drive connector CANNOT see it -- scoped to files it created, and it has no cell-write capability at all, so sharing does not help), write via an Apps Script bridge whose URL+token stay in local operator memory and NEVER in this repo. Quick Lookup row added; handout demoted to secondary; operator-console cross-linked. checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:38:29 -05:00
Joe DiPrimaandClaude Opus 5 6dc86792ad #160 CLOSED -- the cab's RIO axis dropouts + frozen boots were the FTDI adapter's BUFFER SIZE set too high (Nick, on the cab). An oversized receive buffer batches incoming serial instead of delivering it promptly: analog samples arrive late and in clumps (the field 'encoder dropouts') and the boot handshake can miss its window entirely (the wedge). NO port code was ever at fault -- every software hypothesis in the pod-hardware RIO section (power-rail sag under mission load, board-side wedge, print deadlock) is superseded; what survives from those notes is the diagnostic procedure, not the theories. Recorded in context/pod-hardware.md 9a with the setting path (Device Manager -> COM port -> Port Settings -> Advanced -> lower Receive (Bytes); Latency Timer flagged [T4] as the adjacent standard knob, not confirmed part of the fix) and as an operator-facing troubleshooting row in docs/OPERATOR_GUIDE.md, because a fresh FTDI install on any future cab build comes up with defaults that reproduce it. Ledger lesson: a fault that reproduces on ONE machine and survives every code change is a DRIVER/CONFIG suspect before it is a code suspect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:29:53 -05:00
Joe DiPrimaandClaude Fable 5 91351b94bc #125 FIXED + 2-node benched: peers now see a killed arm WRECK + its gun pod DROP OFF on the FIRST hit. Root pair: (1) the MechDeathHandler Tick transcription carried an INVENTED write -- ApplyDamageGraphicState(descriptor->graphicState) -- where the raw @0042aa2c (part_003.c:10963-93) only PICKS an effect resource (level flag & 4 -> DescriptorForLevel; gstate flag & 8 -> DescriptorForGraphicState(the zone's CURRENT gstate), overwriting the level pick); with the port's pick hardcoded to Destroyed(1) the invention STOMPED every destruction-cascade child's authored Gone(2) back to 1 in the same tick, master-side, BEFORE the record shipped -- the wire itself carried the corruption, so no peer could ever render the pod dropping (receipts: legacy A remade jointrgun to gstate 2 then re-remade it to 1; legacy B received 'dz_rgun gstate 0->1'). Gotcha 36: an added write is worse than a stub -- it actively fights the authentic writers on every node. (2) the destroyed-mesh swap trigger was descriptor-gated (DescriptorForLevel returns NULL past the last threshold; cascade children have gstate edges with NO level edge) -- the binary needed no trigger at all (its renderer re-picked every segment's mesh from zone gstate PER FRAME); the port's built-once render tree now gets the event equivalent: per-zone gstate caches in Tick fire ONE coalesced RemakeEntityRenderables on any edge, which is also the PEER's only trigger for replicated gstate-only beats. Restored authored two-stage story: a directly-killed gun zone hangs its wrecked variant mesh (thrdrgun-family, exterior skeleton; every zone's descriptor table carries lvl=-1 gs=1/gs=2 terminals -- unreachable by level, existing exactly for the gstate picks); a killed ARM swaps to its wrecked variant AND the pod segment vanishes. Effects suppressed on ->Exists edges (the binary's heal writes the cell raw, no flag -> no respawn puff; regression benched 0 heal puffs across a full death/respawn cycle). 2-node benched both ways (night18/mp_zone125.sh + zonedump.sh): fix = peer receives dz_rarm gstate 0->1 + dz_rgun 0->2 on ONE beat, swaps thrdrarm.bgf and HIDES the pod, first-beat remake receipt; legacy = the stomp conviction above. KB: combat-damage.md MechDeathHandler section rewritten to the corrected reading; gotcha 36.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 18:45:12 -05:00
Joe DiPrimaandClaude Fable 5 f597f76a30 #102 FIXED + benched: the Owens run->reverse 'hang' was the port missing the machine's whole HARD-BRAKE system -- master perf @0x4aa158-0x4aa365 (raw part_013.c:3353-95): a forward gait meeting a negative demand latches +0x3f4 (the recon's 'airborneSelect' -- another jump-jet-family misread: it is the BRAKE flag) and swaps the gait slew to the model's SuperStopAcceleration (+0x5bc, ex-'airborneCycleRate'), kicks the gyro with a random-sign Z lurch, judders the cab 0.2 every 0.4s while braking above walk speed (gyroRumbleTimer@0x5c4), ships the flag on the type-8 record, restores the ground rate on release. Content: heavies brake harder than they drive (10->20); the sprinters brake WEAKER -- owens 50->25, blkhawk 30->10 -- the Owens' long coast into reverse is AUTHORED, and the pod communicated it through the seat; the un-ported port coasted with zero feedback = the felt hang. Ported to the mech4 drive block ([brake] receipts under BT_GAIT_TRACE) + the replicant rate select restored in the peer motion path (the binary's IntegrateMotion:298 line -- dead TU in the port, Simulate bypassed). Plus gotcha 35: the binary's SM switch re-reads the state cell after the wind-down-gate/death-latch preamble (one cell: @0x3b0/@0x728 == the alarm level); the recon's split alarm/int pair dispatched the STALE pre-gate state for a frame -- re-synced, both channels. The 'leg in the air until you twist' statue is the MACHINE's own trn turn-stop MID-PIVOT park, authentically INVISIBLE on the pod (no external self-view existed; the chase cam is a port addition) -- bench-reproduced at trn legFrm=10 + twist rescue, documented not-a-bug (Owens reads worst: deepest mid-swing of the 8 chassis, thigh -43deg vs madcat -27). Reverse-input truth [T1]: reverse is the red button 0x3F/LALT held with the throttle OPEN (manual p7); demand = -runSpeed*throttle (NO fwdScale term in reverse); the throttle channel is [0,1] like the pod lever, so brake keys can NEVER produce reverse -> handout education line. Bench rig: BT_KEYSIM/_REV/_TURN drive the REAL lever+detent+SM chain headlessly (scratchpad/night18/gaithang_bench.sh). Benched (owens/grass): LALT flip at full run -> [brake] ENGAGE rate=25 (ground 50) -> 12->15->6->9->0->16->18<->19 back cycles at the authored -14.1 cadence -> clean recovery on release; held brake key -> authored stand park, ZERO brake engagements (authentic: no button, no brake). Side-find filed: hardware-RIO cab reverse is DEAD (AddOrErase stubs; open-questions row). 674 forensic: that build boots GLASS by default -> reverse was exclusively LALT in the field.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 17:55:08 -05:00
Joe DiPrimaandClaude Fable 5 b67cf72f38 radar audit ledger: docs/RADAR_MAP_AUDIT.md distills the 3-agent audit (architecture, authenticity verdicts, the #49 root+fix, deliberate port deviations, stale-note sweep, follow-ups incl. the arena-edge era question and the 45deg-wedge-vs-manual-60 flag); gauges-hud gains the pointer section; the mech.cpp 'SetTargetRange is stubbed' stale comment swept (live since the zoom work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
2026-08-14 16:23:50 -05:00
Joe DiPrimaandClaude Fable 5 c2193e2e80 #49 FIXED + OPERATOR-VERIFIED: the radar draws the static world again -- walls, buttes, hills, ridges, buildings, trees. Root cause (radar/map drift audit, 3-agent, receipts in scratchpad/night17/radar_audit_*.txt): the original filed every pip-bearing Terrain-GRAPH entity into the gauge staticEntities grid via the interest feed (@004cbfa0: pip gate, Mover->moving / Terrain->static; the derivation graph parents UnscalableTerrain 0x2A + CulturalIcon 0x5E + Landmark 0x56 onto Terrain [T0 TERRAIN.cpp:183]), and MapDisplay::DrawStatic (@004c25b4) drew each entity's AUTHORED vector-outline pip (110 type-0x12 GIMs ship: wall1-3, aw01-05 arena walls, buttea-e, hillg/dhillg, sr1e/n/s/w polar ridges, trees, buildings) world-rotated and metre-scaled. The port's RebuildEntityGrid (the shim replacing the dropped interest feed) iterated DYNAMIC entities only -- the 1008 statically-registered UnscalableTerrain instances never reached the grid, so the scope showed no world at all. Fix: a third pass files non-dynamic Terrain-graph entities from HostManager::AllEntityIterator; pipless entities (floors, sky, the wall10k outer fence -- authentically invisible on the shipped radar too) no-op at the pip lookup exactly as on the machine. Receipts: [map-grid] worldStatics census + a capped per-static [map] dump (cls/rid/pip/dsq) symmetric to DrawMoving's. Benched: grass files cls=42 hill rid=2190 pip=1 drawing at 466m beside 313 trees; arena1 files 126 world statics with the aw wall sections (rid 1918/1915) pip=1 drawing and the unpipped walkway/banner props correctly skipped (authored omissions preserved). Operator-verified live on arena1: 'muuuuch better'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
2026-08-14 16:21:44 -05:00
Joe DiPrimaandClaude Fable 5 7b641f9b40 KB: gotcha 34 -- change detection against a shared multi-writer scratch (the #155 previousModeMask class): a change-detector must own its own baseline; audit every manager-scratch read that an authentic high-frequency toggler can starve. Ticket #155 comment 1858 carries the fix + live verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
2026-08-14 14:56:23 -05:00
Joe DiPrimaandClaude Fable 5 caa50ef0f6 #155 FIXED + LIVE-VERIFIED: the dead eng/weapon MFD banks under a lit PANIC lamp were the gauge renderer's change-detection XOR reading the mode MANAGER's previousModeMask -- a shared scratch overwritten by EVERY Add/RemoveModeMask call. The authentic panic-arm chatter (the no-hysteresis eject evaluator, kept for strict fidelity 2026-08-03) writes that scratch per frame, stomping the SetPresetMode page bits out of the XOR before ExecuteForeground samples it: page EXITS died (reactivation requires the change bit; first entries still worked via !alreadyActivatedFlag), always-active gauges (radar/score/coolant) stayed live -- Oracle's exact bank split. Fix: one line -- sample the renderer's OWN previousModeMask member (already ctor-init'd and maintained at the activate/deactivate guard; the manager read looks like lineage drift), making the change mask accumulate everything since the last RENDER frame, correct under any number of mode writers; single-transition frames identical. Diagnosis chain: authenticity cleared in three legs (121 streamed rows zero eject-gated, MechRIOMapper 0x200000 = PANIC+lamp only, engine dispatch = AND); headless injection exonerated the mapping layer under steady arm AND 2063-edge chatter; the OPERATOR's live mouse session produced the receipts that convicted presentation ([mode] preset fired for the stuck exits while the screen held); fix verified live same session -- 8 transitions incl. 4 exits across all 3 MFDs under 2833 flap edges, all rendered. The PANIC flicker itself stays: authentic (incandescent filament was the pod's low-pass; a glass persistence filter remains a design call). Rig hooks kept: BT_FORCE_EJECT_ARM=1/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
2026-08-14 14:55:30 -05:00
Joe DiPrimaandClaude Fable 5 97bdb5cc0c #155 rig: BT_FORCE_EJECT_ARM hook (=1 steady / =2 chatter at the evaluator's rate) -- pins the panic-arm state with zero damage side-effects. Repro results so far, all NEGATIVE through the PadRIO screen-button injection path (BT_BTNTEST): bank presses deliver and pages flip with ModeEject steady ON (mask 0x650430 observed), and with the arm CHATTERING (2063 edges/run, two presses on two MFDs both delivered). Combined with the authenticity verdict (no eject gating anywhere in content/binary/engine), the dead-bank defect lives ABOVE the mapping layer: prime suspects are the glass window's mouse hit-test / panel rebuild under per-frame mode churn, and the HOTAS CONTROLS.MAP route. Next step needs a human at the desktop: BT_FORCE_EJECT_ARM=2 + mouse clicks on the eng/weapon MFD buttons (and the HOTAS bindings), watching [mode] preset receipts -- ten minutes, fully scripted otherwise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
2026-08-14 14:29:25 -05:00
Joe DiPrimaandClaude Fable 5 ccfa875b65 handout correction (#151): my 'expect ~zero on eject' line RETRACTED -- the ticket's own 2026-08-10 bench decode (unread in yesterday's body-only triage, the exact failure the new read-ticket-threads memory records) proves -499 is the machine's arithmetic: suicide (-CalcKillScore ~999) + death cost (-500) from 1000, three ejects benched, never doubled, #162 uninvolved. Entry rewritten as the correction + the restored design call (authentic ~-1500 swing vs house-rule); counts 16 verify / 3 design calls. Tracker #151 comment 1848 carries the retraction; #104 comment 1850 holds the NARC rename (the 360dps receipts say the label may be truthful about the round profile).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
2026-08-14 13:39:23 -05:00
27 changed files with 1188 additions and 99 deletions
+2 -1
View File
@@ -85,7 +85,8 @@ precise than anything you can infer.
| Multiplayer, replication, netcode | `context/multiplayer.md` |
| **The OPERATOR CONSOLE + RELAY** — how to launch it, ports, seats, the round/launch lifecycle, re-arm | `context/operator-console.md` |
| Running a session as sysop (operator-facing how-to) | `docs/OPERATOR_GUIDE.md` |
| The tester-facing OPEN-ISSUES handout (for Discord) | `docs/OPEN_ISSUES_FOR_TESTERS.txt` — regenerate from the tracker + update the header date before each playtest |
| **The PLAYTEST TRACKER SHEET** — the testers' issue board (primary feedback channel since 2026-08-14): tabs, status vocabulary, the Dev-Comments contract, how to read/write it | `context/playtest-tracker.md` |
| The tester-facing OPEN-ISSUES handout (for Discord) | `docs/OPEN_ISSUES_FOR_TESTERS.txt` — SECONDARY since the tracker sheet; regenerate from the tracker + update the header date before each playtest |
| Pod hardware, monitors, RIO, MFD surfaces, input remap (CONTROLS.MAP/XInput) | `context/pod-hardware.md` |
| Glass cockpit desktop dev layer (BT_GLASS/BT_STEAM gates, PadRIO, miniconsole, Steam) | `context/glass-cockpit.md` |
| Steam internet MP (the wire seam, identity tokens, lobby) | `context/steam-networking.md` |
+17
View File
@@ -199,6 +199,23 @@ default (`-DBT_STEAM=ON` is the documented dev-checkout state). `BT_EXPIRE=ON` (
14-day tester window; an expire-OFF zip is renamed `-noexpire` and warns. Verify by extracting the
zip somewhere clean and booting it with no repo present — that is what catches a missing runtime DLL.
**ANTIVIRUS SMOKE TEST — do this before handing a zip to anyone (#190, 2026-08-15).** Windows
Defender's ML classifier flagged `btl4.exe` as `Trojan:Win32/Bearfoos.A!ml` and DELETED it. This is
a false positive, and it is **per-binary, not per-project**: of 17 archived builds extracted side by
side, only **4.11.957** was condemned — 622 through 913 all survived — plus that evening's fresh
build. Our exe fits the profile that trips these classifiers (unsigned, rebuilt every session so it
never accrues reputation, opens raw sockets, enumerates processes, writes beside itself), so any
given build is a coin flip. It also turns without warning: the same binary ran clean across benches
all evening, then a signature update (8/14 19:34, 1.457.172.0) condemned it hours later.
**The check:** extract the new exe to a normal folder that is NOT inside a Defender exclusion (a
Desktop subfolder is realistic), wait ~60 s, and confirm the file still exists. Deletion is
asynchronous — an immediate `Test-Path` can pass and the file vanish a minute later. If it is
condemned, do not ship it: submit to https://www.microsoft.com/en-us/wdsi/filesubmission (category
"software developer / incorrectly detected", 1-3 day turnaround, clears it for EVERY player) and
rebuild. A rename does not help — the classifier scores content, and the exe name is constant
anyway (only the zip is versioned). Long-term fix is code signing.
⚠ A Defender exclusion on the repo makes this test silently pass — test outside it.
## Build ritual — the stale-link flake (bit 4+ times; MANDATORY for bench work)
MSBuild often does NOT relink `btl4.exe` when only static-lib members changed — the bench then
runs a STALE exe and the session burns hours on phantom results. Ritual for every code-change build:
+27 -8
View File
@@ -977,14 +977,33 @@ Full decomp map of the death path. Two tiers:
`ApplyDamageAndMeasure` @4ac07c. Diag: `BT_CRIT_PROBE=<zone>`. Detail: [[open-questions]]
(the resolved entry) + mechdmg.cpp/mechsub.cpp.
- **`MechDeathHandler`** (ctor `FUN_0042a984` + Performance `FUN_0042aa2c`, cached mech+0x850 / `mech[0x214]`) —
the **per-subsystem destroyed-skin + explosion engine**. Each tick walks the damage subsystems
(mech+0x120[mech+0x11c]); as a subsystem's damage crosses a descriptor-table threshold (`FUN_0042a5f4`
detects the crossing, `FUN_0042a664` looks up the state) it applies the `GraphicState` ("Destroyed" = enum 1,
parsed @part_003.c:9099) to the subsystem graphic (`sub+0xd4`) AND dispatches an effect-creation message
(`FUN_0043663c`→`FUN_004364e4` to the effect mgr `app+0x38`). ✅ Fully exported; was a no-op stub
(mech.cpp:221) — now fully RECONSTRUCTED (mechdmg.cpp — ctor @0042a984 / Tick @0042aa2c; wired in the
Mech ctor, mech.cpp:1469) [T2]. Gamedata `DeathSplashDamage`/`Radius`/`Effect` load into a descriptor @ +0x74/78/7c
(`FUN_004a2da8`). Destroyed-skin variant set (`destroyed`, `destroyeds`…`destroyedd`) @part_003.c:5720.
the per-ZONE **effect watcher**. ⚠ **Corrected reading (#125, 2026-08-14 [T1 raw
part_003.c:10963-93]; supersedes the 07-12 note that had it "applying the GraphicState"):**
each tick walks the zones (mech+0x120[mech+0x11c]); two changed-flag branches SELECT an
effect descriptor — level moved (`+0xb8 & 4`) → `DescriptorForLevel` (`FUN_0042a664`; band
crossing `FUN_0042a5f4` also dirties the zone record on a master), gstate moved (`& 8`) →
`DescriptorForGraphicState(the zone's CURRENT gstate @+0x78)` (`FUN_0042a6c4`), the gstate
pick OVERWRITING the level pick — then dispatches the effect message
(`FUN_0043663c`→`FUN_004364e4` to `app+0x38`). **The descriptor picks the EFFECT only —
the binary NEVER writes gstate here.** The gstate writers are the destruction cascade
(`RecurseSegmentTable`: the dying zone → Destroyed(1), children/siblings → Gone(2)),
replication, and Reset. The port's former `ApplyDamageGraphicState(d->graphicState)` was an
INVENTION, and with its pick hardcoded to Destroyed it stomped every cascade child's
authored Gone(2) back to 1 the same tick, master-side, pre-send — the gun pod never dropped
off a killed arm on ANY node (#125). **The authored two-stage arm story (restored):** a
directly-killed GUN zone shows its wrecked variant mesh (`thrdrgun.bgf`-family, EXTERIOR
skeleton set — every zone's table carries `lvl=-1 {fx gs=1}` + `{fx gs=2}` terminals,
unreachable by level, existing for exactly these gstate picks); a killed ARM swaps to its
wrecked variant AND its pod segment VANISHES (Gone). Port mesh coupling: the binary's
renderer re-picked segment meshes from gstate per frame; the port's built-once tree gets the
event equivalent — per-zone gstate caches in Tick fire ONE coalesced `RemakeEntityRenderables`
on any edge (also the peer's only trigger: replicated gstate-only beats have no level edge).
Effects are suppressed on →Exists edges (the binary's heal writes the cell raw, no flag —
no respawn puff). 2-node benched both ways (night18/mp_zone125.sh): peer receives
`dz_rgun gstate 0->2` and hides the pod on the FIRST beat; legacy A/B receipt shows the
stomp (`jointrgun → gstate 2` then re-remade `→ 1`). [T1/T2] Gamedata
`DeathSplashDamage`/`Radius`/`Effect` load into a descriptor @ +0x74/78/7c (`FUN_004a2da8`).
Destroyed-skin variant set (`destroyed`, `destroyeds`…`destroyedd`) @part_003.c:5720.
**NOT EXPORTED (master-perf gap 0x4a9770-0x4ab188 + the un-exported TakeDamage) [T4]:**
- The `movementMode` WRITER (sets 5-8 then 2/9 on death) + the fall-direction (5/6/7/8) selection.
- The `Mech::DeathShutdown` roster loop (RP `VTV::DeathShutdown` analog — loop subsystems calling the
+4
View File
@@ -796,6 +796,10 @@ default-ON (`'0'` disables).
| `BT_INCOMING_LOG` | `[incoming]` missile-alarm receipt (#83): LOCK/clear edges + ~1Hz held lines with distance (mech4.cpp PerformAndWatch latch) |
| `BT_DESIGNATE=enemy` | bench designator (#83 rig): stamps the production target slots (+0x388/+0x37c/+0x38c) with the nearest live mech on frames where the boresight pick found no mech — standoff locks; a real pick still wins; HUD ring untouched |
| `BT_WORLDHIT=0` | disables the missile flight sweep's static-solid-tree half (#175) — restores the heightfield-only world test (A/B lever; default on) |
| `BT_KEYSIM="rate:sec,..."` | scripted key-hold schedule (#102 bench): overrides `gBTInput.leverRate` segment by segment so a headless bench drives the REAL lever integrator + zero detent + gait SM (the forcedThrottle harnesses bypass the lever); after the last segment the lever rests (mech4.cpp, after BTInputPoll) |
| `BT_KEYSIM_REV="t:dur"` | hold the pod's REVERSE button (0x3F) in [t,t+dur) on the BT_KEYSIM clock — drives `gBTReverseHeld`, the chokepoint every rig funnels through (the authentic sign-flip shape) |
| `BT_KEYSIM_TURN="t:dur"` | full-right turn pulse in [t,t+dur) on the same clock (the #102 twist-rescue leg) |
| (receipt) `[brake]` | hard-brake slew engage/release under BT_GAIT_TRACE (#102: forward cycle + negative demand → forwardCycleRate=superStopAcceleration + gyro lurch/judder; mech4.cpp drive block) |
| `BT_CRIT_PROBE=<zone>` | hammer one own-mech zone every 4s (crit-propagation diag, task #2; mech4.cpp) |
| `BT_REPL_LOG` | replicant/MP replication log (mech4.cpp) |
| `BT_GOTO="enemy"\|"x z"` / `BT_GOTO_LOG` | self-driving beeline (to the enemy or a map coordinate) + its log |
+14
View File
@@ -864,3 +864,17 @@ were three different presentation facts:
- Authored no-lamp facts (from the mechalrm item census, [galm-item]): conditions 1 (Damaged)
and 3 (Overheating) have NO lamp items for ANY subsystem; cond 0 (Destroyed) resolves lamps
only for weapon-slot subsystems. A heat alarm never flashes a cockpit button -- authored.
## RADAR: the static world draws again (#49 CLOSED, 2026-08-14) [T1 audit + T2 operator-verified]
Full subsystem audit + fix ledger: `docs/RADAR_MAP_AUDIT.md` (agent reports:
`scratchpad/night17/radar_audit_*.txt`). Headlines: the machine's scope drew every
pip-bearing Terrain-GRAPH entity (walls/buttes/hills/ridges/buildings/trees -- 110
authored type-0x12 vector pips) as dim staticColor outlines under the bright mover
blips; the port's RebuildEntityGrid fed only DYNAMIC entities, so the whole static
world was absent. Fixed with a non-dynamic Terrain-graph pass (GAUGREND.cpp,
c2193e2). AUTHENTIC ABSENCES to defend in the field: the wall10k outer boundary
was invisible on the pods too (no pip -- never draw a boundary ring); floors/sky/
overhead pieces/walkways are unpipped by design; range rings don't exist (baked
tick ring only); the wedge is 45 deg total (hardcoded pi/8 -- the manual's "60
degrees" is print drift); radar shadowing (@004c228c) compiled empty. Receipts:
[map-grid] census + capped [map] static dumps under BT_MAP_LOG.
+55 -1
View File
@@ -31,7 +31,8 @@ to a steady pose that the cycle clips never rebind, so it HOLDS.) Decoded from B
`[hip]` probe [T2]:
- `<pfx>swr` ramps `jointhip` 0 → **8°** (stand→walk), `<pfx>wrl/wrr` 8 → **11.1°** (walk→run),
`<pfx>wsl/wsr` 8 → 0 (walk→stand). The dedicated return clip only exists because the lean
persists. The BLH has NO `jointhip` channel in any clip → it stays level (both views).
persists. The BLH **and the OWENS** have NO `jointhip` channel in any locomotion clip → they
stay level (both views; Owens jc=9 = root + 8 leg hinges, #102 content audit 2026-08-14 [T1]).
- **Two clip sets per mech.** The ctor gate (@part_012.c:10308-10320) binds the **INTERIOR** set
(4-char `'i'` suffix, `LoadLocomotionClipsExt` @004a86c8) to the **local cockpit master** — those
clips animate `jointshakey` (cockpit shake, a BALL joint) and OMIT `jointhip`, so the PILOT's own
@@ -129,6 +130,59 @@ Port: keyboard lever snaps out of the band at rest (mech4.cpp "GAIT DETENT" acco
the glass PadRIO throttle slew applies the SAME at-rest snap (L4PADRIO.cpp, band published in
lever units via the mech4.cpp `gBTGaitDetentLo/Hi` seam — 2026-07-20 glass re-test) [T2].
**REVERSE is a BUTTON, never a lever sweep (2026-08-14, #102 audit) [T1]:** the pod's throttle
lever is 0..800 ADC → [0,1] forward-only; reverse = HOLD the red button on the throttle handle
(RIO unit 0x3F `Throttle1`, keyboard LALT; manual p7 "hold in the red button while
accelerating"). The mapper formula (@004afd10, part_013.c:7318-23) is a flag sign-flip:
forward `demand = runSpeed(0x34c)·throttle·fwdScale(0x5c0)`; reverse
`demand = runSpeed·throttle`**NO forwardThrottleScale in reverse** and no separate reverse
scale (full-lever reverse demands the full run top speed; the back cycle's clip-derived clamps
bound the actual backing pace). Port: PadRIO EmitButton(0x3F) → `gBTReverseHeld`
`reverseThrust` (glass rigs; keyboard-bridge rigs also honor `key_throttle<0`). ⚠ The real
CAB's hardware red button is currently DEAD (streamed button mappings unconsumed — AddOrErase
still a Fail stub; the BTPadRIOActive accommodation excludes hardware RIO) — open-questions.
⚠ A held brake key can NEVER produce reverse: glass clamps the throttle channel to [0,1]; the
DEV keyboard-bridge zero detent latches a held sweep at exactly 0 (taps DO cross — the latch
needs prev>0). Player education: "hold ALT + keep the throttle open", exactly like the pod.
**HARD-BRAKE slew + gyro lurch/judder (#102 root, ported 2026-08-14) [T1 decode / T2 bench]:**
the master perf (@0x4aa158-0x4aa365; part_013.c:3353-95) watches for a FORWARD leg cycle
meeting a NEGATIVE demand (the red-button throw at speed): engage-edge latches `+0x3f4` (the
recon's `airborneSelect`**another jump-jet-family misread; it is the BRAKE flag**, and it
replicates via the type-8 record field so peers pick the rate too), `ForceUpdate(0x100)`,
swaps `forwardCycleRate(0x344)` to `+0x5bc` = the model's **SuperStopAcceleration** (the recon's
"airborneCycleRate"), and kicks the gyro with a random-sign Z torque+impulse (0.4). While
braking above `walkStrideLength`, the gyro takes a vertical 0.2 impulse every 0.4 s
(`gyroRumbleTimer`@0x5c4) — the pod's brake JUDDER. Release-edge restores `+0x5b8` ground
rate. 1.1f = authored-off sentinel; every shipped chassis is LIVE: heavies brake HARDER than
they accelerate (thor/vul/ava/snd 10→20), madcat 20→20, loki 15→15, but **owens 50→25 and
blkhawk 30→10 brake at half/a third of their ground slew** — the light sprinter's long coast
is authored. The pod communicated that coast with the cab shake; the un-ported port coasted
SILENTLY, which is the felt "hang" of #102. Port: mech4.cpp drive block, before the channel
advances; `[brake]` receipts under BT_GAIT_TRACE.
**Gait-SM PARK facts (2026-08-14 bench matrix, night18/gaithang_bench.sh) [T1/T2]:** every
moving state's slew clamps are NONZERO clip-derived speeds (walk floor `standSpeed`, run floor
`reverseSpeedMax`, back-cycle ceiling `gimpSpeedMax` = sbr exit, 5.83 owens), so clips always
reach their finished-callbacks — the machine has no mid-cycle stall by construction, and a
144-clip sweep found NO mid-clip zero/negative root beats anywhere. Standing is the ONLY
rest, it never reposes the skeleton (authentic — the pose stays where the last clip left it;
wind-downs end in the authored stand pose so normal stops look clean), and the trn arm is the
one input-driven rebinder (SetLegAnimation resets the playhead — why "twisting unsticks it").
A trn cut by the turn-stop exit parks MID-PIVOT (authentic; the next input re-drives it) —
**this is the #102 "leg in the air" statue**: bench D reproduced it deterministically (turn
pulse ends mid-trn → parked at legFrm=10, leg raised; any twist/throttle re-drives — Lynx's
exact rescue). The machine had the same state but NOBODY COULD SEE IT: the pod has no
external view of your own mech (the chase camera is a port/dev addition), and peers' mechs
are continuously re-driven by their own pilots. Owens reads worst because its mid-swing
amplitude is the largest of the 8 chassis (thigh 43° vs MadCat 27°). NOT-A-BUG verdict;
the felt "hang" half was the missing brake system above.
⚠ Recon split-cell corollary: the binary's SM switch reads the LIVE alarm cell after the
wind-down-gate/death-latch preamble; the port's split alarm/int pair must RE-SYNC before the
switch or the gate frame dispatches the stale state (fixed 2026-08-14, both channels).
Bench hooks: `BT_KEYSIM="rate:sec,..."` (scripted lever keys through the real
integrator+detent), `BT_KEYSIM_REV="t:dur"` (the 0x3F button), `BT_KEYSIM_TURN="t:dur"`.
## Turn-in-place (trn, state 4) — AUTHENTIC dispatcher decoded (task #64, 2026-07-14)
**The entry dispatcher is NOT in the Ghidra decomp** — it lives in the master-perf gap
`0x4a9b5c0x4ab188` (functions_index.tsv jumps FUN_004a9770→FUN_004ab188; the SOLE reader of
+8
View File
@@ -100,6 +100,14 @@ status vs the pod's 7-display / RGB-splitter / dual-sound-card rig:
3. Serial protocol timing/handshake vs a real RIO board (and the intercom path) — unverified.
4. `-platform pod` deliberately does NOT auto-enable `L4GAUGE` (each surface needs its own
device); the pod bring-up procedure itself is undocumented → the Nick items above.
5. **REVERSE is DEAD on the hardware-RIO cab (#102 audit side-finding, 2026-08-14) [T3
code-read].** The real red button (0x3F) reaches `buttonGroup` but nothing consumes it:
the streamed record [2] route (`Button Throttle1 → attr 6 ReverseThrust@0x124`) dead-ends
on the unreconstructed `MechControlsMapper::AddOrErase` Fail stubs, and the
`BTPadRIOActive()`-gated `gBTReverseHeld` accommodation deliberately excludes hardware
RIO. Fix lane when cab work resumes: either reconstruct AddOrErase (the streamed button
mappings then drive attr 6 themselves — the authentic path) or widen the accommodation's
gate. Decide the three-rig matrix (DEV bridge / glass PadRIO / hardware RIO) explicitly.
## BINARY-COVERAGE AUDIT discoveries (2026-07-13, 6-agent decomp census) [T1 verified-uncited]
New unaccounted functionality no prior list knew (addresses verified absent from game/+context/+docs):
+2 -1
View File
@@ -444,5 +444,6 @@ doing nothing:
- Uses: [[content-archives]] (the EGG is the mission + roster) · [[multiplayer]] (what the relayed
frames mean: master/replicant, dead reckoning)
- Feeds: [[multiplayer]] · [[steam-networking]] (the alternate wire seam) · [[pod-hardware]]
- Feeds: [[multiplayer]] · [[steam-networking]] (the alternate wire seam) · [[pod-hardware]] ·
[[playtest-tracker]] (where the session's findings land afterwards)
- Sysop instructions: `docs/OPERATOR_GUIDE.md`
+144
View File
@@ -0,0 +1,144 @@
---
id: playtest-tracker
title: "The playtest tracker sheet — the tester-facing issue board"
status: living
source_sections: "Established 2026-08-14/15 (session housekeeping); supersedes the .txt handout as the primary feedback channel"
related_topics: [operator-console, test-harness, project-overview]
key_terms: [gitea, tracker, playtest]
open_questions:
- "Should the OPEN tab auto-sync from gitea on a schedule, or stay a manual reconciliation before/after each playtest?"
---
# The playtest tracker sheet
Since **2026-08-14** the testers track issues in a shared Google Sheet instead of the annotated
`docs/OPEN_ISSUES_FOR_TESTERS.txt` handout. It is now the **primary feedback channel**; edited
.txt docs and pasted Discord logs are secondary ([[operator-console]] covers the session tooling
itself). The sheet is the testers' INPUT surface; **gitea remains the system of record.**
## Shape
Two tabs. Sheet ID and the write-bridge credentials live in the operator's local Claude memory
(`feedback-channels`), **never in this repo** — the bridge URL + token together grant write
access to the sheet.
- **OPEN**`# | Priority | Test Status | Dev Status | Dev Comments | Tester Comments`,
ONE row per ticket, sorted by priority then ticket number.
- **CLOSED** — the same minus Priority, ascending by number. Also holds non-ticket records
(completed audits) under a text key like `AUDIT`.
**The split is driven by GITEA STATE, not by the Dev Status column** — the two drift constantly,
and the sheet's own column is the stale one.
## Dev Status vocabulary (dev-owned column)
The colour is **embedded in the label**, not applied as a cell fill:
| Label | Meaning |
|---|---|
| `🟡 PATCHED - RETEST` | code landed, needs a playtest to confirm — the testers' retest list |
| `🔴 FAILED RETEST` | a shipped fix did **not** hold; back on the bench (never collapse this into QUEUED — it understates a regression as "not started") |
| `🟠 NEED INFO` | blocked on testers for a repro, detail, or log |
| `🔵 YOUR CALL` | not a code question; needs their opinion or era memory |
| `⚪ QUEUED` | logged and understood, work not started |
| `🟢 WORKING` | actively being worked right now |
| `✅ DONE` | verified — **only ever appears on CLOSED** |
Priority: **P1** blocks a session / hits everyone · **P2** gameplay correctness confirmed in the
field · **P3** presentation and polish · **P4** era question or opinion call · **P5** latent or
internal with no field impact.
Test Status is TESTER-owned (`Pass` / `Fail` / `Test` = not yet retested / `Rework` / `Clarify`).
A **Fail on a ticket gitea has closed** is the highest-value signal in the whole sheet — it means
a shipped fix did not hold, or the tested build predated it.
## The Dev Comments contract
That column is what a player reads, so it is written for a player, in three lines:
```
<one sentence: what is wrong, or what changed if patched>
STATUS: <where it actually stands>
NEED FROM YOU: <a concrete, specific ask> <- omit for QUEUED
```
Under ~55 words, no addresses, no engine jargon. The ask must name what to capture (which mech,
which map, what time, what you expected vs saw, the boot-screen build number) — never "let us
know if it happens again". If the thread already answers it, do not ask again.
## Working rules (learned the hard way)
- **Read the gitea COMMENT THREAD, never the body alone.** Bodies are original reports and are
routinely superseded, retracted, or already fixed by comment 4. Triaging 53 tickets from
threads moved a third of the statuses and dropped P1 from 5 to 2. See
[[reconstruction-gotchas]] for the general form of this mistake.
- **DATE EVERY TESTER ENTRY: `M/D - ` at the front of each line.** The build is cut per session,
so the date implies the build — which is what makes a Pass or Fail interpretable at all.
Entries carried over from before this convention are marked **`(undated)`**, meaning
*provenance unknown — do not treat as fresh evidence*. That marker is deliberate: the sheet was
seeded from earlier tracking and nobody can now say which rows are original, so stamping them
all with the session date would manufacture precision. Reading undated rows as fresh already
produced a false "still crashing after the fix" claim on #170 that had to be retracted across
seven tickets — #170's row carries the same three timestamps and players as its own 2026-08-12
body, and whether that is a copy-forward or a genuine recurrence is STILL UNRESOLVED.
One entry per line, each with its own stamp; comment cells grow over time and undated text
running together is unreadable within two sessions.
An undated entry CAN often be dated from gitea: if its text is quoted in a ticket body, the
report existed by that ticket's creation date (this is how #170's three crash lines were pinned
to 2026-08-12 — its body says `FIELD 2026-08-12` and lists those exact times).
**SELF-REFERENCE TRAP:** do not date an entry from a comment YOU wrote while harvesting the
sheet — those quote the tester text verbatim, so a naive search dates the report to your own
housekeeping. Exclude same-day comments and take the EARLIEST match. This bit once already: a
#170 line was briefly stamped with the harvest date instead of the event date.
- **Never write `DONE` on OPEN.** A row that is done belongs on CLOSED; a `DONE` on the open page
is a contradiction that confuses everyone reading it.
- **Merge duplicates to one row per ticket** — linking loose reports to existing tickets creates
two rows for the same number otherwise.
- **Never use cell fills for status.** Fills belong to cells, labels to text; every re-sort moves
the text and leaves the paint, so they desync. This was re-fixed three times before the colour
was embedded in the label.
- **A leftover FILTER hides rows after a rewrite** — if rows appear to vanish and the row numbers
in the gutter SKIP, that is hidden rows, not lost data (Data → Remove filter).
- **Row heights**: autofit makes wrapped comment rows hundreds of pixels tall (≈4 visible rows).
Pin to ~63 px.
## The cycle
1. **Before a playtest** — regenerate/refresh: reconcile every row against gitea, clear stale
statuses, make sure each `NEED INFO` carries an ask the testers can act on that night.
2. **After a playtest — THE SWEEP.** Read the sheet, file the unfiled `New` rows as tickets, link
duplicates to existing numbers, post the field evidence onto the tickets, then resolve every
`🟡 PATCHED - RETEST` row by what the tester wrote in Test Status:
| Test Status | Meaning | Action |
|---|---|---|
| `Pass` | fix confirmed | close in gitea, move the row to CLOSED |
| `Fail` + detail | the fix did not hold | `🔴 FAILED RETEST`, reopen in gitea, raise to ≥P2, post their evidence |
| `Fail`, no detail | cannot act yet | `🟠 NEED INFO` with ONE discriminating question |
| blank | nobody tested it | leave `🟡`, roll to the next session |
**A Pass or Fail with no BUILD NUMBER is uninterpretable** — a Pass on an older build cannot
verify a fix that shipped later. Before treating a `Fail` as a regression, establish that the
tested build actually contained the fix; that ambiguity is why #27 and #38 sat at `NEED INFO`
(asking for the boot-screen build) rather than going straight to `🔴`. Stamp "shipped in build
NNN" into every `🟡` Dev Comment so the answer is checkable later.
3. Reconciling ~50 tickets against their threads is a fan-out job — one agent per batch of ~7,
returning `{num, dev_status, priority, dev_comment}`.
## Access
- **Read** — the sheet is public, so the CSV export works with no auth:
`https://docs.google.com/spreadsheets/d/<ID>/export?format=csv&gid=0` (307-redirects to a
`googleusercontent.com` host; follow it). The Claude **Google Drive connector cannot see it**
that connector is scoped to files it created itself, and it has no cell-write capability for any
file, so sharing the sheet to the account does not help.
- **Write** — a Google Apps Script web app bound to the sheet, called with `curl -sL -d` (never
`-X POST`). Actions: `ping`, `tabs`, `get`, `set`, `append`, `note`, `width`, `height`,
`autofit`, `format`. Extending it requires re-pasting the script AND
**Deploy → Manage deployments → pencil → Version: New version** — clicking Deploy alone keeps
the old code live on the same URL. Treat further script edits as expensive: the operator has
said the redeploy loop is not worth repeating for cosmetics.
## Key Relationships
- Feeds: the gitea tracker (system of record) · `docs/OPEN_ISSUES_FOR_TESTERS.txt` (now secondary)
- Uses: [[operator-console]] (session tooling) · [[test-harness]] (what "verified" means here)
+14
View File
@@ -541,6 +541,20 @@ test); (3) swap in one of the spare Prolific adapters as cheap elimination; (4)
port + ghost-COM cleanup regardless. ⚠ My STALL/RECOVER edge detector in 881 keys on
request-age, which the 50 ms poll keeps resetting — it never fires; the `age=` field carries the
real signal (fix the keying to reply-age at the next build).
**✅ RESOLVED 2026-08-15 (#160) — it was the FTDI adapter's BUFFER SIZE, set too high [T2,
Nick on the cab].** Lowering it fixed BOTH halves: the mid-play axis dropouts and the frozen
boots. An oversized receive buffer makes the driver BATCH incoming serial data instead of
delivering it promptly, so RIO analog samples arrive late and in clumps (the "encoder dropouts")
and the boot-time handshake can miss its window outright (the wedge). **No port code was ever at
fault** — every software theory in this section (power rail sag, board-side wedge, print
deadlock) is superseded; the surviving value of those notes is the diagnostic procedure, not the
hypotheses. Set on every future cab build or driver reinstall, since a fresh FTDI install comes
up with defaults that reproduce it: Device Manager → the COM port → Properties → Port Settings →
**Advanced** → lower **Receive (Bytes)**; check **Latency Timer** in the same panel at the same
time (1 ms is the usual value for responsive serial devices) [T4 — the adjacent standard knob,
not confirmed as part of Nick's fix]. Lesson for the ledger: a fault that reproduces on ONE
machine and survives every code change is a DRIVER/CONFIG suspect before it is a code suspect.
**Remote minidump procedure (worked over SSH, no tools install):** 32-bit dump of the wedged pid
via `C:\Windows\SysWOW64\rundll32.exe C:\Windows\SysWOW64\comsvcs.dll, MiniDump <pid>
C:\bt411\w.dmp full` → scp home → local x86 cdb (`where.exe cdb` resolves the bt411-tools copy)
+68
View File
@@ -1106,3 +1106,71 @@ THIS NEVER RUNS *** banner naming the live site (missile.cpp now does). (3) A d
FIXED is a claim, not evidence -- a fix without a FIELD receipt ([incoming], [seqloop], etc.)
is T3 at best. (4) The fix's own bench must fail first: the first #83 bench (solo) produced
zero receipts because solo seats never fill -- proving the rig, then the fix, in that order.
## 34. CHANGE DETECTION AGAINST A SHARED MULTI-WRITER SCRATCH: previousModeMask (#155, 2026-08-14)
The engine ModeManager keeps ONE previousModeMask cell, overwritten by EVERY
Add/Remove/ReplaceModeMask call. Any consumer computing `changed = current ^
manager->GetPreviousModeMask()` only sees the delta of the LAST write -- fine
while exactly one writer touches the mode per frame, silently lossy the moment
a second writer appears. The authentic panic-arm chatter (the no-hysteresis
eject evaluator flapping ModeEject per frame) made the gauge renderer's page
XOR permanently blind to SetPresetMode's page bits: MFD page EXITS died while
the PANIC lamp flickered (entries survived via !alreadyActivatedFlag; the
always-active radar/score/coolant never consult the XOR -- the field's exact
alive/dead split). The renderer ALREADY OWNED a correctly-maintained
previousModeMask member; reading the manager's scratch instead looks like
lineage drift.
**Rules:** (1) a change-detector must own its OWN baseline snapshot, sampled
and advanced at ITS consumption rate -- never a shared scratch another writer
can stomp between samples. (2) When an authentic high-frequency toggler
exists (chatter, flap, blink), audit every `GetPreviousModeMask`-shaped read
it can starve (LAMP.cpp's read survives because lamp flicker IS the intended
output). (3) The live-human receipt loop cracked what two headless rigs
could not: the injection path was clean, so the defect HAD to be presentation
-- receipts proving delivery while the screen disagrees convict the renderer.
## 35. THE SPLIT ALARM/INT CELL DISPATCHES STALE ON PREAMBLE TRANSITIONS (#102, 2026-08-14)
In the binary, a gait SM's "state variable" and its AlarmIndicator LEVEL are
ONE cell (`+0x3b0` == `legStateAlarm.level`, `+0x728` == body's) -- the switch
reads it LIVE, so a preamble transition (the wind-down gate cutting 6-9 ->
Standing, a death latch binding 0x1c-0x1f) dispatches its NEW state the same
frame: with a reverse demand held, the gate frame falls straight into case 0
-> SetLegAnimation(0x10), which REBINDS AND REWINDS the clip -- a parked pose
structurally cannot linger. The recon split the cell into an alarm + a
mirror int synced once at function top; every mid-preamble SetLevel left the
switch dispatching the STALE pre-transition case for one frame -- and any
read of the mirror between preamble steps (the gate's own state test after a
same-frame death latch) judged a dead value.
**Rules:** (1) when a recon splits a binary cell into alarm + mirror, RE-SYNC
the mirror after EVERY preamble step that can write the alarm, not just at
entry -- or read the alarm directly at each decision. (2) The binary's
same-frame fallthrough semantics (gate -> case 0 -> rebind -> advance) are
part of the state machine's correctness, not an optimization: the one-frame
stale dispatch is exactly one frame of "no exit condition tested" -- the
#102 class. (3) Sibling of gotcha 30 (one cell, many port stores) and the
namedClip aliasing class -- grep for `= (int)` alarm syncs when auditing any
split-cell machine.
## 36. AN INVENTED WRITE INSIDE A FAITHFUL TRANSCRIPTION: descriptor-as-authority (#125, 2026-08-14)
The MechDeathHandler tick was transcribed with one line the binary does not
have: `zone->ApplyDamageGraphicState(d->graphicState)` -- the effect
DESCRIPTOR (a lookup result) written back into the zone's graphic state (the
authoritative cell). The raw @0042aa2c (part_003.c:10963-93) uses the
descriptor ONLY to pick an effect resource; the gstate writers are the
cascade, replication, and Reset. With the pick hardcoded to Destroyed(1),
the invented write STOMPED every cascade child's authored Gone(2) back to 1
in the same tick, master-side, before the record shipped -- so the wire
itself carried the corruption and no peer could ever render the authored
state. An added write is worse than a stub: a stub leaves the authentic
writers alone; an invention actively fights them, and it fights them on
every node that runs the transcription.
**Rules:** (1) in a transcription, every WRITE to authoritative state must
have a byte-level citation -- effects/lookups reading state is safe, state
consuming a lookup result is the red flag; (2) convict with a before/after
receipt AROUND the suspect pass (the [BTrender] seg lines caught gstate 2
becoming 1 between two remakes -- one receipt, whole conviction); (3) when a
lookup table carries entries UNREACHABLE by its primary key (the lvl=-1
gs=1/gs=2 terminals), those entries name the binary's OTHER selector -- find
it before assuming the reachable subset is the whole contract.
+17 -11
View File
@@ -7,9 +7,12 @@
=============================================================================
HOW TO REPORT: drop a line in Discord with the issue number and PASS /
FAIL / what you saw. Screenshots and rough times help.
LOGS -- ONE CLICK: double-click SENDLOGS.bat in the game folder after the
session. ONE small zip lands on your Desktop -- attach that. This build
also names every shot fired in the log, so reports can name weapons.
LOGS -- ONE CLICK: CLOSE THE GAME, then double-click SENDLOGS.bat in the
game folder. ONE small zip lands on your Desktop -- attach that. It now
tells you the exact file it wrote; if it says SKIPPED, the game was still
running and holding that log. (Two of you hit errors last time -- both
causes are fixed, and it no longer claims success when it failed.) This
build also names every shot fired in the log, so reports can name weapons.
SOUNDS: use the SOUNDBOARD zip's COPY button to reference an exact sound
(that is how Oracle handed us the missile-alarm sample -- it worked).
FIELD GUIDE: mark up your copy and send the file back. Oracle's return
@@ -72,13 +75,16 @@ A. FIXED THIS BUILD -- PLEASE VERIFY (headliners first)
The Nova spawned showing its REAR laser's range pips in the forward
view. The HUD now commits the forward view at spawn. (Only the pips
were wrong -- rear weapons never actually fired forward.)
#151 (Oracle) Panic eject scored -499
Solved, and the machine agrees with YOUR math: eject = your mech
self-destructs (scored as self-damage, ~-500) + the death cost
(-500) = about -1000, i.e. 1000 points -> roughly ZERO. The extra
-500 you saw was the #162 double-death bug, fixed on testing night.
TEST: repeat the maneuver -- you should land within a few points
of zero, not hundreds below.
#151 (Oracle) Panic eject scored -499 -- CORRECTION: -499 IS the machine
Our line here was wrong (we mis-read our own ticket history; the
correct decode was already on the ticket). Bench-verified: eject
is scored as SUICIDE (about -999, the kill value of your own
mech) PLUS the death cost (-500). 1000 - 999 - 500 = -499
exactly -- the original's own arithmetic, reproduced three times
on the bench, never doubled. If you eject tonight and land at
-499 again, the machine is RIGHT, not broken.
DESIGN CALL (restored): keep the authentic ~-1500 swing, or
house-rule a flat eject cost? Opinions to the thread.
FEEL Three more fixes on the original's clock (same bug class as #173):
the cockpit HIT-SHAKE (was bouncing ~2x too fast -- hits now rock
you at the pod's pace with less rebound), the FOOTSTEP loudness
@@ -200,7 +206,7 @@ G. INTERNAL ENGINEERING BACKLOG (nothing to test)
=============================================================================
End of list. 57 open issues total:
17 verify (3 headliners), 12 reproduce, 6 era questions, 2 design calls,
16 verify (3 headliners), 12 reproduce, 6 era questions, 3 design calls,
10 known gaps, 11 internal -- plus 4 "not bugs" that are the real machine.
Since Wednesday: 15 fixes fielded, 4 tickets closed by evidence alone, and
Oracle's questionnaire + soundboard find settled five more. Your reports
+1
View File
@@ -223,6 +223,7 @@ raise the seat count if you're simply out of seats.
| A player gets **ROSTER FULL** | no free seat in the current round | Re-arm between rounds so the roster re-opens, then let them join |
| Console window stops responding | the relay stalled on a dead connection | Stop Session, Start Session; please keep the log (below) |
| A player says they cannot connect at all | ports, or wrong address | confirm 1500/1501 TCP + 1501 UDP reach this machine |
| **Pod cab only:** stick/throttle/pedals go dead mid-round, or the cab freezes during boot | the USB-serial (FTDI) adapter's **buffer size is too high**, so cockpit data arrives in late clumps | Device Manager → the RIO's COM port → Properties → **Port Settings → Advanced** → lower **Receive (Bytes)**. Re-check after any driver reinstall or new cab build — FTDI defaults bring it back. |
### The one thing to do before restarting a session
+110
View File
@@ -0,0 +1,110 @@
# Radar/Map Display Drift Audit — binary vs port vs content (2026-08-14)
**What this is:** the AI-facing ledger of the radar/map subsystem audit (3-agent,
full reports preserved at `scratchpad/night17/radar_audit_{bin,port,content}.txt`;
~800k tokens of reads). Trigger: #49 (arena walls missing from radar) + the field
report "many map items aren't showing on radar, like map boundaries."
Companion fix: the static world feed, commit `c2193e2` (#49 CLOSED, operator-verified).
## The architecture, as the machine built it [T1 unless noted]
- ONE radar picture for every map/mech: the 480×640 portrait `BTSEC1.PCX` +
`BTSEC1OV.PCX` baked art (tick ring + crosshair — **no range rings exist**),
with the live `map(` widget in the (366,367) scope rect at (58,229). There is
**no per-map radar art and no map page** — the ModeMapping bit is the
weapon-button CONFIG session, not cartography (aliased ModeNonConfig [T0]).
- **Entity membership** (@004cbfa0, the interest feed): an entity reaches the
scope iff (a) a type-0x12 GaugeImageStream pip exists for its resourceID AND
(b) Mover-derived → moving list, else Terrain-GRAPH-derived → static list.
The derivation graph parents UnscalableTerrain (0x2A), CulturalIcon (0x5E)
and Landmark (0x56) onto Terrain [T0 TERRAIN.cpp:183, CULTURAL.cpp:27] even
though their C++ bases differ.
- **Draw order** (@004c22c4): wedge → static pips → moving pips → name labels,
inside a stroke recorder (erase = replay last frame in bg color).
- **DrawStatic** (@004c25b4): each in-capability static's pip drawn with the
entity's FULL transform — walls/buttes/hills/ridges/buildings/trees rendered
as world-rotated, metre-scaled staticColor(3) outlines. 110 authored GIMs
ship (wall1-3, aw01-05, ab*/ar*/bld*, buttea-e, hillg/dhillg 652-byte
contours, sr1e/n/s/w polar rails, tree, dish, …).
- **DrawMoving** (@004c2788): every mover except the operator, color 7, target
boxed (+4px, hotbox color 1). NO self-blip; NO pipless fallback (every
shipped mech has a pip).
- **DrawNames** (@004c28c4): ≤16 callsign labels (64×16 egg rasters via
playerBitmapIndex); the target's label BLINKS at frame rate.
- **Zoom**: 6-step exponential, RadarRange = 250·2^level (250…8000 m scope
width), SLEWED smoothly; SCALE numeric shows it live. Capability radius =
RadarPercent × 4000 m — radar damage shrinks detection (#105/#121 pending).
- **Wedge**: hardcoded π/8 half-angle (45° total); the config's "180" param
only sizes the DEAD shadow table. (Manual says "60° field of vision" —
print-vs-code drift, the code wins for 4.10 [T1 vs manual].)
## Authenticity verdicts the port must respect
1. **The pure map boundary was INVISIBLE on the shipped machine.** Every
outdoor map places one `wall10k` (collision solids only, ±4100 m ring —
no pip, no video model). Players saw a "boundary" only where visible
wall/ridge ENTITIES formed the perimeter (arena aw*, polar sr1* rails, rav
buttes). Do NOT invent a boundary rectangle. (ExistanceBox type-26 extents:
arena ±700, rav ±1000, others ±7000 — available if a design call ever
wants one.)
2. **BuildRadarShadow (@004c228c) compiled EMPTY** — radar occlusion shading
was cut before ship. The port's empty body is faithful.
3. **Authored pip omissions are design**: floors/sky, wall10k, overhead butte
tops (butteeu), walkways/banners, snow banks have NO pips on purpose.
Never force-draw them.
4. **Missile pips are authored dead content**: lrm/srm/strk/nrk GIMs ship, but
Projectile/Missile are Entity-derived in the graph — they fail both class
tests and never listed on the machine either.
5. **Landmark labels + DropZone blips: dormant/absent** (zero Landmark
instances; DropZones carry rid 1).
## The #49 root cause + fix (2026-08-14, c2193e2)
The port dropped the engine's per-entity interest feed and replaced it with
`GaugeRenderer::RebuildEntityGrid` — which iterated DYNAMIC masters/replicants
only. The 1008 statically-registered UnscalableTerrain instances (every wall,
butte, hill, ridge, big building) never entered the static grid; DrawStatic
(fully transcribed and working) had nothing to draw. Dynamic-registering
CulturalIcon props (e.g. grass's 313 trees) were the only statics that ever
showed. Fix: a third RebuildEntityGrid pass files non-dynamic Terrain-graph
entities from `HostManager::AllEntityIterator`; pipless ones no-op at the pip
lookup exactly as on the machine. Receipts: `[map-grid] worldStatics=` census +
capped per-static `[map] static ent/cls/rid/pip/dsq` dump (BT_MAP_LOG).
Benched grass (hill cls=42 rid=2190 pip=1 drawing + trees) and arena1
(126 world statics, aw walls rid 1918/1915 drawing, unpipped props skipped);
operator-verified visually on arena1.
## Known port deviations (deliberate, inventoried)
- Three visible-radius culls (DrawStatic/DrawMoving/DrawNames) drop contacts
between scope radius and capability radius instead of the binary's
draw-and-clip — justified by the shared gauge surface (out-of-port draws
land on neighbors). Revisit only with a real GraphicsView clip.
- Cross-blip fallback for pipless movers (dev content insurance; dead in the
field — every shipped mech has a pip).
- Dead-mover (state 2/9) radar drop: the wreck-blip removal. The binary's
uninteresting-notification presumably did the same [T4 — unverified whether
a pod wreck kept its blip].
- Per-frame O(all-entities) rebuild vs the authentic event-driven feed —
acceptable at shipped populations; restoring the notify feed retires it.
## Stale notes corrected by this audit
- `mech.cpp` "SetTargetRange is stubbed" — FALSE since the zoom work
(btstubs.cpp body + the mapper slew); comment swept 2026-08-14.
- `btl4rdr.cpp` header "Make has no exported caller" — stale; @004c1b4c IS in
the 2026-08-06 re-export.
- `docs/GAUGE_COMPOSITE.md` row 24 "cross-blip stand-in" — stale; pips are the
live path since dd27238.
## Open follow-ups
- ERA QUESTION (for the group): what did the ARENA EDGE look like on the pod's
scope — just the wall outlines, or anything more? (Guards against inventing
a boundary ring; the code says wall outlines only.)
- #105/#121: RadarPercent is pinned 1.0 until sensor damage lands — the
capability-shrink lane is authored and waiting.
- The 45°-wedge vs the manual's "60° field of vision" — flag to testers
comparing against memory; the 4.10 code hardcodes π/8·2.
- Overlay-plane compositing into the dev sec cell (SECTOR/SCALE readouts) —
existing polish item, unchanged.
+55 -1
View File
@@ -3675,6 +3675,49 @@ void
staticEntities.Add(entity);
}
}
// (BT411 #49, 2026-08-14) THE STATIC WORLD FEED -- the missing half of the
// dropped interest feed. The original filed every pip-bearing
// Terrain-GRAPH entity into staticEntities via NotifyOfNewInterestingEntity
// (@004cbfa0: pip-resource gate, then Mover -> moving / Terrain -> static);
// the derivation graph parents UnscalableTerrain (0x2A), CulturalIcon
// (0x5E) and Landmark (0x56) onto Terrain [T0 TERRAIN.cpp:183,
// CULTURAL.cpp:27], so the map streams' walls / arena wall sections /
// buttes / hills / perimeter ridges / buildings / trees (1008
// UnscalableTerrain + 789 CulturalIcon instances across the shipped maps)
// were all radar-visible as authored vector-outline pips (110 type-0x12
// GIMs ship). The dynamic iterators above never see statically-registered
// entities (Host::AddEntity files them into allEntitySocket only), so the
// port's scope drew NO static world at all -- the #49 walls and the field's
// 'map items not showing'. File non-dynamic Terrain-graph entities here;
// pipless ones (floors, sky, the wall10k outer fence -- authentically
// invisible on the shipped radar too) no-op at DrawStatic's pip lookup
// exactly as they did on the machine.
int world_statics = 0;
{
HostManager::AllEntityIterator all_iterator(host_manager);
all_iterator.First();
Entity *entity;
while ((entity = all_iterator.ReadAndNext()) != NULL)
{
if (entity->IsDynamic()) // dynamics already sorted above
continue;
if (entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
{
staticEntities.Add(entity);
++world_statics;
}
}
}
// census receipt (#49 bench): an empty static scope must be
// distinguishable from a culled one. ~1 Hz under BT_MAP_LOG.
if (getenv("BT_MAP_LOG"))
{
static int s_census = 0;
if ((s_census++ % 60) == 0)
DEBUG_STREAM << "[map-grid] worldStatics=" << world_statics
<< "\n" << std::flush;
}
Check_Fpu();
}
@@ -3702,8 +3745,19 @@ void
// like the pod -- and the working SetPresetMode page switching NEEDS the
// authentic gating (the force made every eng screen paint over the shared
// Eng plane, pinning it on the highest screen and ignoring page flips).
// (BT411 #155, 2026-08-14) READ OUR OWN LAST-FRAME MASK, not the mode
// manager's previousModeMask: that cell is a shared scratch overwritten by
// EVERY Add/RemoveModeMask call, and the authentic panic-arm chatter (the
// no-hysteresis eject evaluator) writes it per frame -- stomping the page
// bits out of this XOR before we sample it. Result in the field: MFD page
// EXITS went dead while PANIC flickered (first entries still worked via
// !alreadyActivatedFlag), radar/score/coolant stayed live (always-active).
// The renderer already maintains its own previousModeMask member (updated
// below at the activate/deactivate guard); sampling it here makes the
// change mask accumulate everything since the LAST RENDER FRAME, correct
// under any number of mode writers. Single-transition frames: identical.
ModeMask
previous_mode_mask = application->GetModeManager()->GetPreviousModeMask();
previous_mode_mask = previousModeMask;
ModeMask
change_mode_mask = current_mode_mask ^ previous_mode_mask; // xor tells me what has changed
//-------------------------------------------------------------------
+14
View File
@@ -867,6 +867,20 @@ void
Pip *pip = (pips != NULL)
? pips->LookUpPip(entity->GetResourceID()) // FUN_004688ce(pips+0x58,entity+0x1bc,0)
: NULL;
// (#49 bench receipt) per-static dump, symmetric to DrawMoving's --
// answers "is the list empty, the pip lookup missing, or the cull
// eating it" straight from a field log. Capped; BT_MAP_LOG.
if (getenv("BT_MAP_LOG"))
{
static int s_sd = 0;
if (s_sd++ < 400)
DEBUG_STREAM << "[map] static ent=" << (void *)entity
<< " cls=" << (int)entity->GetClassID()
<< " rid=" << entity->GetResourceID()
<< " pip=" << (pip != NULL ? 1 : 0)
<< " dsq=" << (float)(delta.x*delta.x + delta.z*delta.z)
<< ((s_sd == 400) ? " (cap)" : "") << "\n" << std::flush;
}
if (pip != NULL)
{
AffineMatrix blipXform;
+26 -3
View File
@@ -603,6 +603,26 @@ int
|| (liveGenerators == 0)
|| (coolantFrac < 0.05f) // _DAT_0049fb50
|| (gimped && noviceSim);
// (#155 rig) BT_FORCE_EJECT_ARM: pin the panic-arm state without damage
// side-effects, so the dead-MFD-bank repro isolates the ModeEject
// interaction. =1 steady ON; =2 CHATTER at the evaluator's own rate
// (the authentic no-hysteresis flap, e.g. a coolant fraction hovering at
// the 0.05 threshold). Env-gated bench scaffolding, off by default.
{
static int s_forceArm = -1;
if (s_forceArm < 0)
{
const char *fa = getenv("BT_FORCE_EJECT_ARM");
s_forceArm = (fa != 0) ? atoi(fa) : 0;
}
if (s_forceArm == 1)
ejectPermitted = 1;
else if (s_forceArm == 2)
{
static unsigned s_flap = 0;
ejectPermitted = (int)(++s_flap & 1);
}
}
return ejectPermitted;
}
@@ -1688,9 +1708,12 @@ Mech::Mech(
vertSpeedFiltered = 0.0f;
{ extern void *g_btFootStepAddr; g_btFootStepAddr = &footStep; } // DIAG: watcher-poll tracer target
{ extern void *g_btAccelAddr; g_btAccelAddr = &localAcceleration; } // DIAG: accel poll/change tracer
radarRange = 1000.0f; // radar display scale (SetTargetRange is stubbed;
// 1km default zoom so contacts within ~500m show on
// the radar, vs the config maximum_range=4000 edge)
radarRange = 1000.0f; // radar ZOOM default (level 2 of the 250*2^n ladder;
// scope shows currentScale/2 = 500m radius). The zoom
// buttons slew it live via SetTargetRange (btstubs.cpp
// body + the mapper slew -- the old 'stubbed' note was
// stale, swept 2026-08-14 radar audit). The config's
// 4000 is the CAPABILITY radius, a different axis.
radarLinearPosition = &localOrigin.linearPosition; // map reads the mech's live world position...
radarAngularPosition= &localOrigin.angularPosition; // ...and orientation (pointers into the base origin)
duckState = 0; // not crouching
+19 -1
View File
@@ -923,7 +923,9 @@ Scalar
// run/run-to-walk transition, drop the gait alarm to "standing".
//
{
int state = legAnimationState; // this+0x3b0
// live-cell read (binary: one field @0x3b0; a same-frame death latch
// above must not leave a stale 6-9 here for the gate to stomp)
int state = (int)legStateAlarm.GetLevel();
if (legCycleSpeed <= ZeroSpeed // this+0x348
&& (state == 6 || state == 7 || state == 8 || state == 9))
{
@@ -932,6 +934,16 @@ Scalar
}
}
// SAME-FRAME RE-DISPATCH (#102, 2026-08-14): the binary's switch re-reads
// the state cell AFTER the gate/death-latch preamble (part_012.c:17523
// 'switch(*(param_1 + 0x3b0))' -- +0x3b0 IS the alarm level, one cell), so
// the frame the wind-down gate cuts to Standing dispatches case 0 at once
// and, with a reverse demand held, rebinds the reverse-entry clip the SAME
// frame (SetLegAnimation resets the playhead -- the pose can never linger).
// The recon's split alarm/int pair made the switch read the STALE pre-gate
// state for one frame; re-sync here restores the binary's dispatch.
legAnimationState = (int)legStateAlarm.GetLevel();
switch (legAnimationState) // this+0x3b0
{
case StandingAnimation: // 0
@@ -1268,6 +1280,12 @@ Scalar
}
}
// SAME-FRAME RE-DISPATCH (#102 parity twin): the binary's switch reads the
// live cell +0x728 after the death-latch preamble (FUN_004a5678 has no
// wind-down gate [T1 part_013.c:1-33]); re-sync so a just-latched crash
// clip dispatches this frame, not next.
bodyAnimationState = (int)bodyStateAlarm.GetLevel();
switch (bodyAnimationState) // this+0x728
{
case StandingAnimation: // 0
+144
View File
@@ -3279,6 +3279,15 @@ void
{
if (dt > 0.0001f && dt < 0.5f)
{
// BRAKE-RATE SELECT (#102, 2026-08-14): the binary's replicant ran
// IntegrateMotion (@004ab1c8) per frame, whose first line picks the
// slew from the replicated brake flag (mech4.cpp:298 -- that TU path
// is DEAD in the port, Mech::Simulate bypassed). The flag itself
// already replicates (type-8 record -> airborneSelect, mech.cpp
// ReadUpdateRecord); without this line a peer watched the master
// brake at superStopAcceleration while its own slew stayed at the
// ground rate.
forwardCycleRate = airborneSelect ? airborneCycleRate : groundCycleRate;
// AUTHENTIC PEER HEADING (decomp FUN_004ab188/FUN_00409f58 via FUN_004ab1c8):
// the original replicant integrates its heading INCREMENTALLY from the
// CURRENT pose -- compose an exact rotation of (replicated yaw rate * dt)
@@ -3925,6 +3934,73 @@ void
// gBTInput. Everything below the reads (the lever/stick/
// twist integrators, detents, edge latches) is UNCHANGED.
BTInputPoll(dt);
// BT_KEYSIM (issue #102 bench, 2026-08-14): scripted key-hold
// schedule -- overrides the polled lever/turn channels so a
// headless bench can drive the REAL virtual-controls chain
// (lever integrator + zero detent + gait SM), which the
// forcedThrottle harnesses bypass. Spec: comma-separated
// "rate:seconds" segments consumed in order; rate is written
// to gBTInput.leverRate verbatim (the key-hold rate is ~0.7;
// sign = key direction; 0 = keys at rest). After the last
// segment the lever stays at rest (a bench never mixes real
// keys in). BT_KEYSIM_TURN="start:dur" adds a full-right
// turn pulse in [start, start+dur) on the same clock (the
// twist-rescue leg of the #102 repro).
{
static const char *s_ksSpec = getenv("BT_KEYSIM");
if (s_ksSpec && *s_ksSpec)
{
static float s_ksClock = 0.0f;
s_ksClock += dt;
float segEnd = 0.0f; int applied = 0;
for (const char *p = s_ksSpec; p && *p; )
{
const float rate = (float)atof(p);
const char *colon = strchr(p, ':');
if (!colon) break;
segEnd += (float)atof(colon + 1);
if (s_ksClock < segEnd)
{
gBTInput.leverRate = rate;
applied = 1;
break;
}
const char *comma = strchr(p, ',');
p = comma ? comma + 1 : 0;
}
if (!applied)
gBTInput.leverRate = 0.0f; // schedule exhausted
static const char *s_ktSpec = getenv("BT_KEYSIM_TURN");
if (s_ktSpec && *s_ktSpec)
{
const float t0 = (float)atof(s_ktSpec);
const char *c2 = strchr(s_ktSpec, ':');
const float tdur = c2 ? (float)atof(c2 + 1) : 0.0f;
if (s_ksClock >= t0 && s_ksClock < t0 + tdur)
{
gBTInput.turnActive = 1;
gBTInput.turnTarget = 1.0f;
}
}
// BT_KEYSIM_REV="start:dur": hold the pod's REVERSE
// THRUST button (unit 0x3F, LALT) in [start,start+dur)
// on the same clock -- drives gBTReverseHeld, the one
// chokepoint every rig funnels through (L4PADRIO
// EmitButton -> mechmppr reverseThrust). This is the
// AUTHENTIC reverse shape: throttle stays put, demand
// sign-flips (issue #102 repro).
static const char *s_krSpec = getenv("BT_KEYSIM_REV");
if (s_krSpec && *s_krSpec)
{
extern int gBTReverseHeld;
const float r0 = (float)atof(s_krSpec);
const char *c3 = strchr(s_krSpec, ':');
const float rdur = c3 ? (float)atof(c3 + 1) : 0.0f;
gBTReverseHeld =
(s_ksClock >= r0 && s_ksClock < r0 + rdur) ? 1 : 0;
}
}
}
gBTDrive.keyFwd = gBTInput.leverRate > 0.0001f ? 1 : 0;
gBTDrive.keyBack = gBTInput.leverRate < -0.0001f ? 1 : 0;
gBTDrive.keyLeft = (gBTInput.turnActive && gBTInput.turnTarget < 0.0f) ? 1 : 0;
@@ -5413,6 +5489,74 @@ void
<< " reverseSpeedMax=" << reverseSpeedMax << " target=" << bodyTargetSpeed
<< " split=" << s_realControls << "\n" << std::flush;
}
// HARD-BRAKE SLEW + GYRO LURCH/JUDDER (issue #102, 2026-08-14) --
// FUN_004a9b5c @0x4aa158-0x4aa365 (part_013.c:3353-3395 in the
// 2026-08-06 re-export; disasm scratchpad/night6/gap_4a9770.txt),
// transcribed field for field. When a FORWARD leg cycle meets a
// NEGATIVE demand (the red-button reverse throw at speed), the
// binary latches airborneSelect(+0x3f4) -- ANOTHER jump-jet-family
// misread: it is the BRAKE-ENGAGED flag, and +0x5bc
// ("airborneCycleRate") is the model's SuperStopAcceleration, the
// hard-brake slew -- swaps forwardCycleRate(+0x344) to it, ships
// the flag (ForceUpdate(0x100); the type-8 record field carries it
// so peers pick the brake rate too), and kicks the gyro with a
// random-sign Z lurch. While braking above the walk cap the gyro
// takes a vertical 0.2 impulse every 0.4 s (gyroRumbleTimer@0x5c4,
// zeroed by the clip loader) -- the pod's brake JUDDER, the cab
// shake that told the pilot "braking!" through the long coast.
// -1.1f = authored-off sentinel (no shipped chassis uses it;
// content: Owens brakes at HALF its ground rate 50->25, Blackhawk
// 30->10, the heavies 10->20). Without this block the port
// silently coasted at the ground slew with zero feedback -- the
// felt "hang" of #102.
if (airborneCycleRate != -1.1f)
{
MechControlsMapper *bmppr = MappingMapper();
const Scalar bdmd = (bmppr != 0) ? bmppr->speedDemand : 0.0f;
if (legCycleSpeed > 0.0f && bdmd < 0.0f)
{
if (airborneSelect == 0)
{
airborneSelect = 1; // brake ENGAGE edge
ForceUpdate(0x100);
forwardCycleRate = airborneCycleRate; // 0x344 <- 0x5bc
if (gyroSubsystem != 0)
{
extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar);
extern void GyroApplyDamageTorque (Subsystem *, Scalar, Scalar, Scalar, Scalar);
Scalar r = RandomUnit();
if (RandomUnit() > 0.5f) // _DAT_004ab170
r = -r;
GyroApplyDamageTorque (gyroSubsystem, 0.0f, 0.0f, r, 0.4f);
GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 0.0f, r, 0.4f);
}
if (getenv("BT_GAIT_TRACE"))
DEBUG_STREAM << "[brake] ENGAGE cyc=" << legCycleSpeed
<< " dmd=" << bdmd << " rate=" << airborneCycleRate
<< " (ground " << groundCycleRate << ")\n" << std::flush;
}
}
else if (airborneSelect != 0)
{
airborneSelect = 0; // brake RELEASE edge
ForceUpdate(0x100);
forwardCycleRate = groundCycleRate; // 0x344 <- 0x5b8
if (getenv("BT_GAIT_TRACE"))
DEBUG_STREAM << "[brake] release\n" << std::flush;
}
}
if (airborneSelect != 0 && walkStrideLength < legCycleSpeed
&& gyroSubsystem != 0)
{
if (gyroRumbleTimer > 0.0f)
gyroRumbleTimer -= dt; // +0x5c4 countdown
else
{
extern void GyroApplyDamageImpulse(Subsystem *, Scalar, Scalar, Scalar, Scalar);
GyroApplyDamageImpulse(gyroSubsystem, 0.0f, 1.0f, 0.0f, 0.2f);
gyroRumbleTimer = 0.4f; // the 0.4s judder period
}
}
if (s_realControls)
{
// CHANNEL ROLES (task #49, disasm-corrected [T1]): in the binary
+121 -66
View File
@@ -1106,9 +1106,19 @@ void
damageDescriptors.push_back(d);
}
if (count != 0 && getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[deathfx] zone descriptor table loaded: " << count
<< " entries (first threshold=" << (count > 0 ? damageDescriptors[0].damageLevel : -1.0f)
<< " effect=" << (count > 0 ? damageDescriptors[0].effectResource : -1) << ")\n" << std::flush;
{
// #125: full table (the summary-only line hid the question that
// mattered -- whether a Destroyed-gstate entry exists per zone).
DEBUG_STREAM << "[deathfx] zone " << damageZoneIndex
<< " '" << (const char *)damageZoneName
<< "' descriptors (" << count << "):";
for (int di = 0; di < (int)damageDescriptors.size(); ++di)
DEBUG_STREAM << " {lvl=" << damageDescriptors[di].damageLevel
<< " fx=" << damageDescriptors[di].effectResource
<< " gs=" << damageDescriptors[di].graphicState
<< " dly=" << damageDescriptors[di].timeDelay << "}";
DEBUG_STREAM << "\n" << std::flush;
}
}
//
@@ -1212,6 +1222,15 @@ MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
// per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c).
int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0;
lastLevel.assign(count, 0.0f);
// #125: gstate cache seeded from the zones' CURRENT states (a handler
// built on an already-damaged mech must not fire a spurious remake).
lastGState.assign(count, (int)DamageZone::ExistsGraphicState);
for (int i = 0; i < count; ++i)
{
Mech__DamageZone *z = (mech != 0) ? mech->Zone(i) : 0;
if (z != 0)
lastGState[i] = (int)z->GetGraphicState();
}
}
MechDeathHandler::~MechDeathHandler() // @0042a9f4
@@ -1219,10 +1238,16 @@ MechDeathHandler::~MechDeathHandler() // @0042a9f4
}
//
// @0042aa2c -- each tick, walk the zones; where a zone's damageLevel rose since
// last tick, fire the current damage-band descriptor's explosion (binary +0xb8 &
// 4) and, on destruction, the Destroyed-graphic descriptor's explosion (+0xb8 &
// 8), applying that descriptor's GraphicState (the destroyed skin) to the zone.
// @0042aa2c -- each tick, walk the zones (raw: part_003.c:10963-93). Two
// changed-flag branches select an effect descriptor: level moved (+0xb8 & 4)
// -> DescriptorForLevel; gstate moved (& 8) -> DescriptorForGraphicState(the
// zone's CURRENT gstate), overwriting the level pick. The descriptor picks
// the EFFECT only -- the binary never writes gstate here (#125 corrected the
// earlier reading, which had invented a descriptor->gstate apply). Port
// additions on top: the per-zone level/gstate caches stand in for the
// changed-flags (immune to consumption ordering), and a gstate edge fires
// ONE coalesced RemakeEntity -- the event equivalent of the binary renderer's
// per-frame mesh re-pick (the port render tree is built once).
//
void
MechDeathHandler::Tick() // @0042aa2c
@@ -1417,12 +1442,40 @@ void
}
int n = owner->damageZoneCount;
int needRemake = 0; // #125: one coalesced remake per tick
for (int i = 0; i < n && i < (int)lastLevel.size(); ++i)
{
Mech__DamageZone *zone = owner->Zone(i);
if (zone == 0)
continue;
// #125: GRAPHIC-STATE EDGE watch (all instances) -- the mesh follows
// the zone's gstate, not the descriptor tables. Three writers move a
// gstate with NO level edge in this loop: a replicated record
// (DamageZone::ReadUpdateRecord -- the peer path), the master's
// destruction cascade (RecurseSegmentTable's SetGraphicState(1)/Gone(2)
// marks on the zone + its children/siblings), and the descriptor apply
// below. The binary needed no event -- its renderer re-picked every
// segment's mesh from gstate PER FRAME; the port's built-once render
// tree re-picks on this diff instead. The old trigger (the descriptor
// branch below, level-rise + Destroyed-descriptor gated) missed both
// the cascade children (gstate Gone, level unchanged) and any zone
// whose authored table lacks a gs=1 entry (DescriptorForLevel returns
// NULL past the last threshold) -- the #125 field shape: peers' arm
// intact until a SECOND destroying edge finally fired a remake, whose
// full-segment walk then applied the stale states too.
int gstateEdged = 0; // the flag-8 (& 8) analog, this tick
if (i < (int)lastGState.size())
{
const int g = (int)zone->GetGraphicState();
if (g != lastGState[i])
{
lastGState[i] = g;
gstateEdged = 1;
needRemake = 1;
}
}
Scalar level = zone->GetStructureDamageLevel();
Scalar prev = lastLevel[i];
@@ -1430,47 +1483,64 @@ void
// (ForceUpdate(DamageZoneUpdateModelFlag)). Just refresh the cache: the
// respawn un-wreck no longer rides this edge -- it fires from the
// death-state exit watcher above, which also covers deaths that never
// moved a zone (#94).
// moved a zone (#94). (The gstate cache above saw the heal edge too --
// the coalesced remake restores the segment; no EFFECT fires for it,
// see the Exists suppression below.)
if (level < prev)
{
lastLevel[i] = level;
continue;
}
if (level <= prev) // unchanged -- no new damage
continue;
lastLevel[i] = level;
const int levelRose = (level > prev) ? 1 : 0; // the flag-4 (& 4) analog
if (levelRose)
lastLevel[i] = level;
if (!levelRose && !gstateEdged)
continue; // nothing moved this tick
// AUTHENTIC (FUN_0042aa2c @0042aa2c, the binary's effect-watcher level
// branch): when the rise CROSSES a band-descriptor threshold
// (FUN_0042a5f4) and this node owns the master, raise the entity's
// damage-zone update flag (*(entity+0x18) |= 2, master-gated on
// (entity+0x28 & 0xc) == 0) -- the zone update records ship this
// zone's damageLevel to every observer, so replicant dolls/hull tint
// track the fight at band granularity. This send was MISSING from
// the port (only the graphic-state branch below raised it): masters
// consumed damage correctly while every observer's copy sat at 0.0
// until destruction (the "no discoloration in MP" report, #87).
if (zone->DescriptorCrossed(prev, level)
&& owner->GetInstance() == Entity::MasterInstance)
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
// AUTHENTIC GATE (decomp re-verified 2026-07-12, workflow): the binary
// fires the CURRENT band descriptor's effect whenever the zone's
// damage-CHANGED flag is set (+0xb8 & 4, set by DamageZone::TakeDamage,
// coalesced per tick) -- i.e. EVERY hit on a damaged mech re-puffs its
// band effect. Bands 3/4 are authored ember/bright-FIRE plumes, so a
// nearly-destroyed mech under fire burns continuously (the demo look).
// A level RISE this tick IS the flag analog -- no crossing test. (The
// earlier metronome was SHKWAVE mispacing, not this path; an observer
// node sees a few replication-step puffs per volley -- smoke, fine.)
const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level);
if (level >= 1.0f && prev < 1.0f) // just destroyed -> the Destroyed descriptor
// AUTHENTIC (FUN_0042aa2c raw, part_003.c:10963-10993 -- re-read
// 2026-08-14 for #125; this REPLACES the 07-12 reading, which had
// invented a descriptor->gstate WRITE, see below):
// flag & 4 (level moved): descriptor = DescriptorForLevel(level)
// [FUN_0042a664]; if the rise CROSSED a band threshold
// [FUN_0042a5f4] and this node owns the master, dirty |= 2
// (the zone update record ships) -- every hit on a damaged
// mech re-puffs its band effect (bands 3/4 are authored
// ember/fire plumes: a nearly-dead mech burns continuously).
if (levelRose)
{
const Mech__DamageZone::DamageDescriptor *dd =
zone->DescriptorForGraphicState(DamageZone::DestroyedGraphicState); // enum 1
if (dd != 0)
d = dd;
if (zone->DescriptorCrossed(prev, level)
&& owner->GetInstance() == Entity::MasterInstance)
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
}
// flag & 8 (gstate moved): descriptor = DescriptorForGraphicState(
// the zone's CURRENT graphic state, zone+0x78) [FUN_0042a6c4] --
// OVERWRITING the level pick (the raw stores into the same
// local_c, null included) -- and dirty |= 2 unconditionally on
// a master. The binary NEVER writes gstate here: the descriptor
// chooses the EFFECT only. The gstate writers are the
// destruction cascade (RecurseSegmentTable: self 1, children/
// siblings 2), replication (ReadUpdateRecord) and Reset. The
// port's former `ApplyDamageGraphicState(d->graphicState)` was
// an INVENTION -- and with the pick hardcoded to Destroyed it
// STOMPED every cascade child's authored Gone(2) back to 1 the
// same tick, master-side, before the record shipped: the gun
// pod never dropped off a killed arm on ANY node. Receipt that
// convicted it: `[BTrender] seg 'jointrgun' -> gstate 1` on a
// solo master right after `[cascade] zone 17 DESTROYED` (#125).
const Mech__DamageZone::DamageDescriptor *d =
levelRose ? zone->DescriptorForLevel(level) : 0;
if (gstateEdged)
{
const int g = (int)zone->GetGraphicState();
// Exists(0) suppression: the binary's heal path (Reset) writes the
// gstate cell RAW (no changed-flag), so a heal never fires flag 8
// -- our cache diff sees the ->Exists edge anyway (wanted, it
// drives the mesh restore) but must not fire the gs=0 BAND entry
// as a respawn "damage" puff.
d = (g != (int)DamageZone::ExistsGraphicState)
? zone->DescriptorForGraphicState(g) : 0;
if (owner->GetInstance() == Entity::MasterInstance)
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
}
if (d != 0)
{
@@ -1489,36 +1559,21 @@ void
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
zone->segmentIndex); // segment (world position)
}
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin (graphic state)
// A graphic-state change means the segment's MODEL changed (intact ->
// destroyed variant, keyed by GetVideoObjectName(skl, gstate)). Fire
// the engine's model-rebuild flag so the renderer re-runs
// MakeMechRenderables and loads the destroyed segment geometry -- the
// same mechanism CulturalIcons use to visibly deform on damage
// (CULTURAL.cpp:91; EXPTBL.cpp:549 does this per zone in the engine's
// ExplosionTable). Without this the tree stays as built at spawn (intact).
// The render's "RemakeEntity" state (which consumes this flag) was
// NOT ported -- the mech tree is built once at spawn -- so we drive the
// equivalent directly: set the flag (authentic signal; also flags the
// zone for net damage replication) AND call the render bridge, which
// swaps the wrecked segment mesh onto the already-built tree in place.
// The single-threaded frame loop makes the immediate swap safe.
if (d->graphicState != DamageZone::ExistsGraphicState)
{
// Master-gated like the binary's gstate branch (@0042aa2c:
// (entity+0x28 & 0xc) == 0 guards the |= 2); the mesh swap
// runs on EVERY instance -- an observer node executes this
// same path when the replicated level/gstate lands.
if (owner->GetInstance() == Entity::MasterInstance)
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
BTRemakeMechModel(owner); // RemakeEntity: swap in the destroyed mesh
}
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
<< " -> effect " << d->effectResource << " gstate " << d->graphicState
<< " -> effect " << d->effectResource
<< " (zone gstate " << (int)zone->GetGraphicState() << ")"
<< "\n" << std::flush;
}
}
// #125: ONE remake per tick, after every zone's state has settled --
// RemakeEntityRenderables walks ALL segments and re-picks each by its
// zone's LIVE gstate, so a single call applies the destroying zone and
// its whole cascade (children Gone, siblings) together, and the render
// tree's own segGState diff makes repeats free.
if (needRemake)
BTRemakeMechModel(owner);
}
//#############################################################################
+11
View File
@@ -372,6 +372,17 @@ class MechDeathHandler
private:
Mech *owner; // @0x14
std::vector<Scalar> lastLevel; // @0x10 per-zone last damageLevel cache
// PORT (#125): per-zone last GRAPHIC STATE cache. The binary's
// renderer re-picked every segment's mesh from its zone's gstate PER
// FRAME, so any gstate write (descriptor apply, the destruction
// cascade's child/sibling Gone marks, a replicated record) showed the
// next frame with no event plumbing. The port's built-once render
// tree needs the event: Tick diffs each zone's gstate against this
// cache and fires ONE coalesced RemakeEntity when anything moved --
// the port equivalent of the per-frame re-pick, independent of the
// descriptor tables (whose ForLevel lookup returns NULL past the last
// threshold -- the #125 first-destroying-hit gap).
std::vector<int> lastGState;
// PORT (respawn replication): last-tick MovementMode, so a REPLICANT
// observer can reverse the one-way wreck swap on the DEATH-STATE EXIT
// edge (9/2 -> 0/1, only Mech::Reset ever leaves the death modes). The
+51 -6
View File
@@ -5,15 +5,60 @@ rem Double-click me after a playtest. The zip lands on your DESKTOP as
rem BTLOGS_<machine>_<date>.zip -- attach that one file in Discord.
rem Collects: every session log, matchlog, and launch breadcrumb. Nothing
rem is deleted or modified; this only makes a copy.
rem
rem Field-hardened 2026-08-15 after two testers hit different failures:
rem * ConstrainedLanguage mode (locked-down Windows) blocks .NET calls, so
rem [Environment]::GetFolderPath and [math]::Round are gone -- the Desktop
rem is resolved from environment variables and the size via -f formatting.
rem Compress-Archive ALSO fails there (it is a script module that calls
rem .NET internally), so the zip is made with Windows' native tar.exe and
rem Compress-Archive is only the fallback if tar is missing.
rem * A log still held open by a running BattleTech made Compress-Archive
rem abort the WHOLE zip. Files are now staged with Copy-Item first, so an
rem unreadable one is skipped and named instead of killing the run.
rem * The old script announced success unconditionally. It now only says
rem the zip is ready if the file actually exists.
rem ABOUT.txt (machine, date, folder, exe version) rides inside the zip so we
rem can tell WHICH BUILD a report came from.
rem ==========================================================================
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$ErrorActionPreference = 'SilentlyContinue';" ^
"$d = Get-Date -Format yyyyMMdd_HHmm;" ^
"$out = Join-Path ([Environment]::GetFolderPath('Desktop')) (\"BTLOGS_{0}_{1}.zip\" -f $env:COMPUTERNAME, $d);" ^
"$files = Get-ChildItem -Path 'content\*.log','content\lastrun_*.txt','content\matchlog_*.txt','content\cdb_*.txt' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-36) };" ^
"if (-not $files) { Write-Host 'No logs found -- run me from the BT411 folder (next to the content directory).'; exit 1 }" ^
"Compress-Archive -Force -Path $files.FullName -DestinationPath $out;" ^
"Write-Host ('Wrote ' + $out + ' (' + $files.Count + ' files, ' + [math]::Round((Get-Item $out).Length/1MB,1) + ' MB)');"
"$desk = $null;" ^
"if ($env:OneDrive -and (Test-Path (Join-Path $env:OneDrive 'Desktop'))) { $desk = Join-Path $env:OneDrive 'Desktop' }" ^
"elseif (Test-Path (Join-Path $env:USERPROFILE 'Desktop')) { $desk = Join-Path $env:USERPROFILE 'Desktop' }" ^
"else { $desk = $PWD.Path };" ^
"$out = Join-Path $desk ('BTLOGS_{0}_{1}.zip' -f $env:COMPUTERNAME, $d);" ^
"$files = Get-ChildItem -Path 'content\*.log','content\lastrun_*.txt','content\matchlog_*.txt','content\cdb_*.txt' | Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-36) };" ^
"if (-not $files) { Write-Host ''; Write-Host ' No logs found -- run me from the BT411 folder (the one containing content).' -ForegroundColor Yellow; exit 1 };" ^
"$stage = Join-Path $env:TEMP ('BTLOGS_' + $d);" ^
"Remove-Item -Recurse -Force $stage;" ^
"New-Item -ItemType Directory -Path $stage -Force | Out-Null;" ^
"$kept = 0; $skip = @();" ^
"foreach ($f in $files) { Copy-Item -LiteralPath $f.FullName -Destination $stage -Force; if (Test-Path (Join-Path $stage $f.Name)) { $kept = $kept + 1 } else { $skip += $f.Name } };" ^
"$exe = Get-Item 'btl4.exe';" ^
"$about = Join-Path $stage 'ABOUT.txt';" ^
"Set-Content -Path $about -Value ('machine=' + $env:COMPUTERNAME);" ^
"Add-Content -Path $about -Value ('packed=' + $d);" ^
"Add-Content -Path $about -Value ('folder=' + $PWD.Path);" ^
"if ($exe) { Add-Content -Path $about -Value ('btl4.exe built ' + $exe.LastWriteTime); if ($exe.VersionInfo.FileVersion) { Add-Content -Path $about -Value ('btl4.exe version ' + $exe.VersionInfo.FileVersion) } };" ^
"if ($kept -eq 0) { Write-Host ''; Write-Host ' Could not read any log files -- close BattleTech, then run me again.' -ForegroundColor Red; exit 1 };" ^
"Remove-Item -Force $out;" ^
"Push-Location $stage;" ^
"if (Get-Command tar.exe) { tar.exe -a -c -f $out * } ;" ^
"Pop-Location;" ^
"if (-not (Test-Path $out)) { Compress-Archive -Force -Path (Join-Path $stage '*') -DestinationPath $out };" ^
"Remove-Item -Recurse -Force $stage;" ^
"Write-Host '';" ^
"if (Test-Path $out) { Write-Host (' ZIP READY: ' + $out) -ForegroundColor Green; Write-Host (' ' + $kept + ' files, ' + ('{0:N1}' -f ((Get-Item $out).Length/1MB)) + ' MB'); if ($skip.Count -gt 0) { Write-Host (' SKIPPED (still in use): ' + ($skip -join ', ')) -ForegroundColor Yellow; Write-Host ' Close BattleTech and run me again to include those.' -ForegroundColor Yellow } }" ^
"else { Write-Host ' FAILED -- no zip was created.' -ForegroundColor Red; Write-Host ' Screenshot this window and send it to us.' -ForegroundColor Red; exit 1 }"
if errorlevel 1 goto :failed
echo.
echo Attach the BTLOGS zip from your Desktop in Discord. Thanks!
echo Attach that BTLOGS zip in Discord. Thanks!
goto :done
:failed
echo.
echo Nothing was created -- see the message above.
:done
pause
+82
View File
@@ -32,3 +32,85 @@ received); Elengil's pre-rotation steam_20260812.log; era answers (valve-boost l
450/800 pip, PPC-hit visuals for Cyd's scramble); Cyd PPC-scramble merge = post-Friday
(rebase over palette work + dirty-skip token fix + 2-node bench). NEXT WEEK: BTSimClock
migration per the census checklist (gyro item may land today instead).
LATE-NIGHT TRACKER SWEEP (post-midnight): #151 resolved-by-#162 (eject -499 = self-blast -499
+ death -500 + DUPLICATE death -500; now expects ~0 -- Oracle re-test Friday); #112 CLOSED
(fixed by #171, sweep receipts burst=1 universal); #147 NaN-trap receipt promoted ALWAYS-ON
(65b30d4) so Friday can name the NaN source; #153 CLOSED-AUTHENTIC (binary clamps elevation
only on input [T1 @004b5cf0]; snap-on-next-input = the machine; Recenter is twist-only).
HANDOUT REGEN TOMORROW must add the known-authentic-quirks lines: veteran heat is real in
4.10; Basic-mode elevation snaps on next pitch input (authentic); eject now costs ~all your
points (self-blast + death cost, the manual's own math); plus reword the old 'settles the
HUD horizon' phrasing (Oracle's question).
FRIDAY DAYTIME (car session): full comment-thread sweep DONE (new memory:
read-ticket-threads-not-bodies). #151 CORRECTED (-499 IS authentic: suicide + death cost,
bench-proven on-ticket 08-10; my ~zero claim retracted, handout fixed, design call restored).
#104 rename HELD (the 360dps receipts say the NARC label may be truthful). #113 closed-faithful
+ the authored TURN RATES wired same day (cec6458: model+0x40 deg/s caps the steering; A/B
benched 43/69 CAP receipts vs zero; mad2 'LRM15' = nrk profile). #155 bank-capture half
ANSWERED: the machine never captured any bank (121 streamed rows zero eject-gated [T1];
MechRIOMapper 0x200000 = PANIC+lamp only [T1]; engine dispatch = AND [T0]) -> the dead
eng/weapon MFDs are OUR bug; repro rig designed (BT_BTNTEST + [group-upd] walked/matched
while armed). SUNDAY SLATE: #155 mechanism hunt + fix; #102 forward/reverse detent; #125
zone-edge; #27 clean retest note; #130 retest-likely-fixed.
#155 CLOSED-OUT FRIDAY AFTERNOON (caa50ef + gotcha 34, 7b641f9): the dead banks were the
gauge renderer's change-XOR reading the mode MANAGER's shared previousModeMask scratch (the
authentic panic chatter stomps it per frame, eating SetPresetMode's page bits); one-line fix
(sample the renderer's OWN member); diagnosed via the operator's live mouse session (receipts
proved delivery while the screen held = presentation convicted), fix live-verified same
session (8 transitions / 4 exits / 2833 flap edges). Flicker stays authentic. SUNDAY SLATE
REMAINING: #102 forward/reverse detent, #125 first-destroyed-zone edge; plus tonight's field
returns. Rides the Sunday build with the turn rates.
RADAR AUDIT (Friday evening, 3-agent, 800k tokens): #49 CLOSED operator-verified (c2193e2)
-- the static world feed restored (non-dynamic Terrain-graph pass in RebuildEntityGrid);
walls/buttes/hills/ridges/trees/buildings draw as authored pips on every map, zero new art.
Ledger: docs/RADAR_MAP_AUDIT.md. Authentic absences documented (wall10k boundary invisible
by authorship; unpipped props; no range rings; 45deg wedge). SUNDAY NOTES must carry: the
radar restoration headline + the two not-bugs (boundary, declutter) + the arena-edge era
question. Sunday build now carries: turn rates + #155 banks + #49 radar world.
NIGHT18 (Friday evening, post-compaction): #102 OWENS RUN<->REVERSE. The #102 audit
(3-agent: binary/input/content + bench matrix night18/gaithang_bench.sh) found the port
was missing the machine's whole HARD-BRAKE system: master perf @0x4aa158-0x4aa365 swaps
the gait slew to the model's SuperStopAcceleration when a forward cycle meets a reverse
demand (+0x3f4 "airborneSelect" = the BRAKE latch, another jump-jet-family misread;
+0x5bc "airborneCycleRate" = the brake rate) + gyro random-sign lurch on engage + 0.4s
vertical judder above walk speed (gyroRumbleTimer@0x5c4). Content: heavies brake harder
than they accelerate (10->20), OWENS brakes at HALF (50->25) -- the sprinter's long
coast is authored; the pod communicated it through the cab shake, the port coasted
SILENTLY = the felt hang. PORTED (mech4 drive block, [brake] receipts) + the
same-frame re-dispatch parity fix (gotcha 35: split alarm/int cell dispatched stale on
gate/death-latch frames; both channels re-synced). Bench: ALT sign-flip at full run =
clean chain into reverse w/ brake receipts; held-brake-key = clean authored stand, zero
brake (authentic), twist arms trn from the park (the rescue). REVERSE INPUT TRUTH: red
button 0x3F/LALT sign-flip, demand = -runSpeed*throttle (NO fwdScale); brake keys can
NEVER reverse (throttle [0,1] like the pod lever) -> HANDOUT education line. Side-find:
hardware-RIO cab reverse is DEAD (AddOrErase stubs; open-questions row). KEYSIM bench
hooks added (BT_KEYSIM/_REV/_TURN). 674 worktree forensic pending -> ticket close.
SUNDAY build now carries: turn rates + #155 banks + #49 radar + #102 brake system.
NEXT: #125 first-destroyed-zone edge.
NIGHT18 addendum: 674 forensic RESOLVED without a repro run -- the 674 build boots the
GLASS profile by default (log: '[boot] platform profile: GLASS'), so PadRIO owned
reverseThrust and the BT_FORCE_FLIP negative-throttle path was stomped (rev=0 all run);
i.e. reverse at 674 was exclusively LALT on every rig. The statue itself = the trn
turn-stop MID-PIVOT park (authentic, pod-invisible: no external self-view existed);
bench D reproduced it at trn legFrm=10 + twist rescue. Peer brake-rate select restored
in the replicant motion path (IntegrateMotion:298 is dead code -- Simulate bypassed).
Final build: 20 benign LNK2019s exact.
NIGHT18 (cont): #125 FIXED + 2-node benched. Root pair: (1) the Tick transcription
carried an INVENTED write -- ApplyDamageGraphicState(descriptor) -- the raw @0042aa2c
(part_003.c:10963-93) only PICKS an effect (level flag -> ForLevel; gstate flag ->
ForGraphicState(CURRENT gstate), overwriting); the hardcoded-Destroyed pick stomped
every cascade child's authored Gone(2) -> 1 master-side pre-send: the gun pod NEVER
dropped off a killed arm on any node (gotcha 36). (2) the mesh swap was descriptor-
gated; now per-zone gstate caches fire ONE coalesced RemakeEntity on any edge (the
binary re-picked meshes from gstate per frame). Restored authored story: killed gun
= wrecked thrdrgun mesh hangs; killed arm = wrecked thrdrarm (exterior skel) + pod
VANISHES -- peers see all of it on the FIRST beat ([zone-repl] dz_rgun gstate 0->2 +
peer remake, mp_zone125.sh fix/legacy receipts). Heal edges fire no effects (Exists
suppression; binary Reset writes raw). Respawn regression clean (0 heal puffs).
SUNDAY build now: turn rates + #155 banks + #49 radar + #102 brake + #125 zone edge.
Handout candidates: 'arms now visibly wreck + pods drop off on everyone's screen'.
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# =========================================================================
# ISSUE #102 FORENSICS -- the 4.11.674 build (commit 137c151, the build
# Lynx reported against). Worktree C:/git/bt411-674. No BT_KEYSIM on
# 674, so the ALT demand shape rides the forced harness instead:
# BT_AUTODRIVE=1.0 + BT_FORCE_FLIP=14 (key_throttle sign-flips at t=14 ->
# reverseThrust=1 via the key_throttle<0 term). BT_PLATFORM deliberately
# UNSET: under glass the PadRIO-active accommodation (mechmppr ~1036)
# stomps reverseThrust back to gBTReverseHeld=0 and the flip dies.
# usage: gaithang674.sh <tag> [extra env pairs...]
# =========================================================================
set -x
EXE=/c/git/bt411-674/build/Release/btl4.exe
cd /c/git/bt411-674/content || exit 1
TAG=$1; shift
sed 's/^experience=.*/experience=expert/' MP.EGG > GH674.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=owens/" GH674.EGG
LOG=gh674_${TAG}.log
rm -f "$LOG"
( export BT_START_INSIDE=1 BT_DEV_GAUGES=1 BT_LOG="$LOG" BT_AFFINITY=0x03
export BT_GAIT_TRACE=1 BT_AUTODRIVE=1.0 BT_FORCE_FLIP=14
for kv in "$@"; do export "$kv"; done
"$EXE" -egg GH674.EGG &
echo $! > /tmp/gh674.pid
cat /proc/$!/winpid > /tmp/gh674.winpid 2>/dev/null )
sleep 55
[ -f /tmp/gh674.winpid ] && taskkill //F //PID "$(cat /tmp/gh674.winpid)" > /dev/null 2>&1
rm -f /tmp/gh674.pid /tmp/gh674.winpid
sleep 2
echo "--- $LOG"
grep -ac "gaitSM" "$LOG" || true
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# =========================================================================
# ISSUE #102 -- OWENS RUN->REVERSE GAIT HANG (night18)
# Solo expert Owens, grass/day. BT_KEYSIM drives the REAL virtual-controls
# chain (lever integrator + ZERO DETENT + gait SM) on a schedule -- the
# forcedThrottle harnesses bypass the lever, which is where the suspect
# lives (mech4.cpp zero detent: a held sweep that crosses 0 latches at 0
# until BOTH keys rest -> demand pinned to exactly 0.0 while reverse held).
#
# run A (repro): 3s rest, 10s full fwd (run), 20s HOLD BACK -- the
# Lynx shape. Twist pulse at t=38 (the rescue leg).
# run B (control): same but RELEASE 1.5s then re-press back -- the
# documented detent recovery; reverse must engage.
#
# Receipts: [gaitSM] per frame (legState/legCycle/legFrm/spd), [vctl] 1Hz
# (keys + lever). Park signature: legState=0 with legFrm frozen mid-clip,
# spd=0 while back key held; reverse states (16-19) never entered.
# usage: gaithang_bench.sh <A|B> [extra env pairs...]
# =========================================================================
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
VARIANT=$1; shift
bt_expert_egg MP.EGG GH102.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=owens/" GH102.EGG
LOG=gaithang_${VARIANT}.log
rm -f "$LOG"
if [ "$VARIANT" = "A" ]; then
# S-key sweep shape (keyboard-bridge rigs only; dead channel on glass)
KS="0:3,0.7:10,-0.7:20,0:8"
KT="38:2.5"
KR=""
XTRA=""
DUR=55
elif [ "$VARIANT" = "C" ]; then
# THE AUTHENTIC LALT SHAPE (glass rig = what Lynx flies): full forced
# throttle, REVERSE BUTTON held t=14..34 (gBTReverseHeld -> reverseThrust
# sign-flip), release at 34 = the recovery probe (a Standing-class park
# recovers on the demand flip; a frozen-clip park stays parked).
KS="0:1"
KT=""
KR="14:20"
XTRA="BT_AUTODRIVE=1.0"
DUR=55
elif [ "$VARIANT" = "D" ]; then
# THE GLASS S-KEY SHAPE: demand pinned at EXACTLY 0 after a full run
# (PadRIO throttle clamps [0,1] -- S can never reverse; the keyboard-
# bridge detent pins the same way). BT_KEY_BRIDGE=1 forces the bridge
# so BT_KEYSIM's lever drives the mapper. Twist pulse at t=38 = the
# Lynx rescue. Park signature: legState=0, legFrm frozen != 0, spd=0.
KS="0:3,0.7:10,-0.7:20,0:10"
KT="38:2.5"
KR=""
XTRA="BT_KEY_BRIDGE=1"
DUR=55
else
KS="0:3,0.7:10,-0.7:2,0:1.5,-0.7:15"
KT=""
KR=""
XTRA=""
DUR=45
fi
( export BT_GAIT_TRACE=1 BT_KEY_LOG=1 BT_KEYSIM="$KS"
[ -n "$KT" ] && export BT_KEYSIM_TURN="$KT"
[ -n "$KR" ] && export BT_KEYSIM_REV="$KR"
[ -n "$XTRA" ] && export $XTRA
for kv in "$@"; do export "$kv"; done
bt_launch "$LOG" GH102.EGG 0x03 )
sleep $DUR
bt_kill_ours
sleep 3
taskkill //F //IM btl4.exe > /dev/null 2>&1
echo "--- $LOG"
grep -ac "gaitSM" "$LOG"
grep -a "vctl" "$LOG" | head -40
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# =========================================================================
# ISSUE #125 -- peers' first destroyed-zone graphic (2-node, thor)
# A (master, thor) self-destroys dz_rarm with ONE 120-pt hit at t=25.
# B observes. PASS receipts on B (the peer):
# [zone-repl] zone 10 'dz_rarm' ... gstate 0->1 (arm Destroyed arrives)
# [zone-repl] zone 17 'dz_rgun' ... gstate 0->2 (pod GONE arrives -- the
# pre-fix master STOMPED this to 1 before the record shipped)
# [BTrender] seg 'jointrgun' slot 6 -> gstate 2 (peer hides the pod)
# [BTrender] RemakeEntity: ... entity=<A's mech> (the peer-side remake
# fired on the FIRST beat -- the pre-fix peer had no trigger at all
# when descriptors misfired, and could never see gstate 2)
# usage: mp_zone125.sh <tag>
# =========================================================================
set -x
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
TAG=${1:-fix}
rm -f z125_a_${TAG}.log z125_b_${TAG}.log
sed 's/^experience=.*/experience=expert/' MP.EGG > Z125.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=thor/" Z125.EGG
BT_PLATFORM=glass BT_START_INSIDE=1 BT_DEV_GAUGES=1 \
BT_LOG=z125_b_${TAG}.log BT_AFFINITY=0x0C BT_MP_LOG=1 BT_DEATH_LOG=1 \
../build/Release/btl4.exe -egg Z125.EGG -net 1601 &
sleep 2
BT_PLATFORM=glass BT_START_INSIDE=1 BT_DEV_GAUGES=1 \
BT_LOG=z125_a_${TAG}.log BT_AFFINITY=0x03 BT_MP_LOG=1 BT_DEATH_LOG=1 BT_DMG_LOG=1 \
BT_SELF_DAMAGE=120 BT_SELF_DAMAGE_ZONE=dz_rarm BT_SELF_DAMAGE_DELAY=25 BT_SELF_DAMAGE_TICKS=1 \
../build/Release/btl4.exe -egg Z125.EGG -net 1501 &
sleep 4
python ../tools/btconsole.py Z125.EGG 127.0.0.1:1501 127.0.0.1:1601 &
PC=$!
sleep 75
kill $PC 2>/dev/null
taskkill //F //IM btl4.exe > /dev/null 2>&1
echo "=== A (master) ==="
grep -a "dmghit\|cascade\|RemakeEntity\|seg 'jointr" z125_a_${TAG}.log | head -12
echo "=== B (peer) ==="
grep -a "zone-repl\|RemakeEntity\|seg 'jointr\|deathfx\] zone 1[07] level" z125_b_${TAG}.log | head -16
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# =========================================================================
# ISSUE #125 -- authored zone-descriptor dump (solo, any chassis)
# usage: zonedump.sh <vehicle>
# Prints each zone's [zone-armor] row + the full [deathfx] descriptor
# table (lvl/fx/gs/dly per entry) at load. The #125 question: which zones
# carry a gs=1 (Destroyed) entry -- the ONLY trigger the old remake had.
# =========================================================================
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
VEH=${1:-thor}
bt_expert_egg MP.EGG ZD125.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=$VEH/" ZD125.EGG
LOG=zonedump_${VEH}.log
rm -f "$LOG"
( export BT_DEATH_LOG=1 BT_DMG_LOG=1
bt_launch "$LOG" ZD125.EGG 0x03 )
sleep 30
bt_kill_ours
sleep 2
taskkill //F //IM btl4.exe > /dev/null 2>&1
echo "--- $LOG"
grep -ac "deathfx" "$LOG" || true