Author SHA1 Message Date
CydandClaude Opus 4.8 ca988db9ed radar: draw the MAP GEOMETRY -- staticEntities was empty on two stacked gates
Playtester DOS-binary comparison: our radar showed blips + wedge but NO map
outlines (the DOS reference draws the whole arena).  MapDisplay::DrawStatic
was always a faithful, working drawer of the rtype-18 GaugeImage outlines
(the same records docs/MAPS.html renders) -- it drew nothing because
staticEntityList was empty, from TWO stacked port regressions; fixing either
alone is insufficient:

1. GaugeRenderer::RebuildEntityGrid (per frame) Clear()ed BOTH grids and
   refilled from the DYNAMIC host iterators only -- the 528 static-flagged
   (0x20C) UnscalableTerrain map placements, which carry most arena outlines
   (arenall: 59 static vs 3 dynamic), were structurally excluded, and the
   wipe also destroyed anything the interest feed contributed.
2. The notify feed's static gate was transcribed as
   Terrain::GetClassDerivations() -- but the shipped content has ZERO Terrain
   records (census: 1008x UnscalableTerrain + 789x CulturalIcon, both
   deriving from Entity in our headers), so the predicate was dead code and
   rejected 100% of map geometry.  @0x4e6ef8 was almost certainly
   UnscalableTerrain's derivation (or the shipped machine's MUNGA derived
   UnscalableTerrain from Terrain).  [T3 on which; the corrected gate is
   what the content requires.]

FIX:
  - staticEntities is now PERSISTENT: filled ONCE per mission inside
    RebuildEntityGrid (AllEntityIterator; keep UnscalableTerrain-derived
    entities carrying a rtype-18 GaugeImage -- covers the static walls AND
    the dynamic CulturalIcon props the 2026-07-12 phantom-red-pip fix routed
    here).  Deterministic whether or not the interest feed delivers.
  - Per-frame RebuildEntityGrid now rebuilds only the MOVING grid (Mover
    filter + dead-mech skip unchanged).
  - Lifecycle: cleared + refill-armed at LinkToEntity (the pod process hosts
    many missions); the gate-corrected remove-notify prunes destroyed
    statics; the gate-corrected add-notify is guarded pre-fill so nothing
    double-adds.
  - BT_RADAR_LOG=1 prints '[radar] static grid filled: N'.

Verified live (glass, DEV.EGG): fill reports 326 map entities; structure
outlines now draw on the radar at the correct zoom; blips + sweep wedge
unregressed.  KB: GAUGE_COMPOSITE row 24 + the Phase-4 staticEntities
deferral resolved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 07:26:15 -05:00
Joe DiPrima 5cde991f56 #174 root-caused + fixed with a negative control: the binary's ONLY re-descend protection is the burst loop's zone-state guard wrapping the WHOLE per-burst application (@0x4a0446, part_012.c:14661 -- crit roll, TakeDamage, score tally, destroyed-flag; RecurseSegmentTable/SendSubsystemDamage have NO internal re-entry protection: no used-marking, no top gate). The port had narrowed the guard to the crit roll -- continued fire re-saturated dead zones (cascade re-fired: night-16 zone 18 x7, each pass re-pushing crit allotment = the ram 'crits worsened with each collision'), and hits on wreckage kept SCORING (binary awards zero -- a quiet farm-the-wreck exploit). Fix: guard restored to binary scope. Bench: BT_ZONE_HAMMER hook (4Hz explicit-zone self-damage) -- fixed build 1 cascade/10 hits/0 post-saturation applications; NEGATIVE CONTROL (fix stashed): 147 cascades. 147 -> 1. Gotcha 31: guard-scope narrowing -- diff the BRACE SCOPE when transcribing decomp ifs; and prove a bench detects the bug before trusting its pass 2026-08-13 02:42:07 -05:00
Joe DiPrima e693b481b3 commit-hygiene sweep: (1) the #170 L4STEAMNET.h contract comment never landed in 2fe58b8 -- the git add used the lowercase filename and the case-mismatch left the tracked (uppercase) path unstaged, silently; (2) the gauges-hud #172 correction was a silent no-op -- a python str.replace whose multi-line needle didn't match printed 'corrected' anyway. Both applied for real now (verified by grep, not by exit code). Note the pattern: two silent tool no-ops in one evening -- verify edits by READING the result, not by the tool's success message 2026-08-13 02:15:45 -05:00
Joe DiPrima 81b0887033 #172 corrected same day (user challenge upheld): the 6px-strip-was-invisible framing RETRACTED -- [lampblink] receipts prove the surround paints the flash at a metronomic 125ms (code-identical loop in 913), and Elengil's layout receipts show the stock surround with the lamp on screen. The real mechanism is EXPECTATION: a generator leak lights the button BY THE RADAR (authentically the only place -- no MFD carries a generator-leak indicator) while testers watch the MFDs. Ships: handout WHERE-TO-LOOK entry (#172) + the refined #136 voice-loop wording; the alert ring is GATED OFF by default (BT_ALERT_RING=1 keeps it available if Friday proves testers miss the lamp even knowing its location); [lampblink] shade-transition receipt added (BT_LAMP_LOG). Era question posted for Oracle: how prominent was the pod's physical generator button light? 2026-08-13 02:14:13 -05:00
Joe DiPrima 8b3d1c8113 #172 root-caused: the leak-lamp chain was NEVER broken -- all 35 night-16 leak events flashed same-frame (Elengil's 'fatal no-flash' lamp flashed 28.4s in the ~6px glass radar-rail strip; the 'delayed' weapon-leak flashes are the AUTHENTIC page gate delivering at eng-page entry; the leak voice is a LOOPING sequence so 5-6 repetitions carried zero edge information -- the testers' clock was the bug's best disguise). Ships: the ALERT BLOOM (glass-only #154-class deviation -- an alternating surround lamp paints a +8px halo UNDER the surfaces, so only the outward bloom shows, respecting the twice-field-broken no-overlay rule) + two always-on receipts closing the observability holes this hunt fell into: [seqloop] START/STOP (looped alarm sequences -- voice-vs-lamp timing now log-readable) and [lamppx] alert ON/OFF (lamp state -> PIXELS, with cell size). Verified live: GeneratorB=0.6 leak -> seqloop START + lamp 0x1b FLASHING + lamppx bloom8 same cluster. #136 refined: edge-started, level-LOOPED, edge-stopped. KB: gauges-hud leak-lamp routing map (generator=rail-only authentic, condenser 0x2c gap authentic, weapon page-gating authentic, cond 1/3 have NO authored lamps) 2026-08-13 01:56:36 -05:00
Joe DiPrima e614924107 one night = ONE log file: the 8MB size roll-over retired (it cost field evidence the moment it existed -- night16: Elengil's incident window sat in an unsent pre-rotation part; testers attach 'the log' = the newest part). The 6am log-day boundary stays. Transport moves to players/SENDLOGS.bat (ships at the dist root): one double-click zips every log/matchlog/lastrun from the last 36h to a Desktop BTLOGS_<machine>_<date>.zip (~10:1 text compression -- a 13MB night = 1.8MB, well under Discord's 10MB free cap, which is what the roll-over was protecting against). Handout HOW TO REPORT updated; mkdist carries the bat 2026-08-13 00:45:21 -05:00
Joe DiPrima fcd592c0f2 #165 ROOT-CAUSED after three failed fixes -- the binary keeps ONE destroyed cell per subsystem (+0x40: fire gate, dial X, wire header, every writer) and the port split it into FOUR stores; the everyday CRIT-LOTTERY kill (MechWeapon::TakeDamage) bypassed the reconstructed base and wrote only the X store, so X'd weapons kept firing while every prior fix aimed at a gate that was never broken (the 4,213-refusal bench drove BT_KILL_SUBSYS, which writes all cells -- it validated readers, not writers). FIX: MechWeapon::TakeDamage chains to MechSubsystem::TakeDamage (@0x4ac0bc semantics); the port-authored FSM-from-damage tail is DELETED (it parked partial-crit weapons at level 1 -- no FSM case, the silent freeze, and with beamFlag armed the 883 hanging beam: #164's upstream writer, caught red-handed by berserker's 20x STUCK-ON receipts); the phantom uninitialized simulationState int is ctor-initialized and kept in lockstep at every destroyed write; always-on [crit] DESTROYED receipt makes crit kills field-visible. BT_CRIT_SUBSYS bench hook drives the REAL writer path; weaponx_bench PASS: kill phase 0 FIRED post / 5,664 refusals / X+gate agree, partial phase keeps firing. Gotcha 30; #174 filed (cascade re-descend) 2026-08-13 00:35:33 -05:00
Joe DiPrima 2fe58b8b89 #170 root-caused + fixed: the #163 SteamAPI_Shutdown was the crash -- at round end every peer's close events are ALWAYS in flight, and Shutdown tears client state down while steamclient's own service thread dispatches them (the field stack: a steamclient worker calling literal NULL, zero btl4 frames, 7 hits / 3 machines / one module-relative stack). BTSteamNet_ShutdownAll is now a QUIESCE: steamActive gated FIRST, linger closes ('round over' not timeouts), one SNS drain, 60ms grace, NO SteamAPI_Shutdown -- and every exit path dies via TerminateProcess (menu-quit no longer returns through the CRT; it uses BTOrderlyDie like everything else, so the 883 detach deadlock stays dead too). Status callback guarded for the dying window. Crash filter now names foreign DLL frames (module+offset via VirtualQuery/GetModuleFileName) so the next foreign-thread crash arrives pre-symbolized. Exit contract: steam-networking.md 2026-08-12 23:23:23 -05:00
Joe DiPrima c3258ee2e1 night-16 field intelligence (build 913, 20260812): tracker housekept -- 6 field-verified closes (#162/#156/#154/#135/#169/#128), #165 FAIL detail + #76 Nanook comment, 4 new tickets (#170 menu-relaunch NULL-callback crash = top defect, #171 ballistic hitscan era question ON HOLD, #172 leak first-flash edge, #173 chicken-walker myomer overheat); KB gains the night-16 section + the spawn-discharge/generator-bias heat note [T3] 2026-08-12 23:05:55 -05:00
24 changed files with 717 additions and 110 deletions
+16
View File
@@ -1328,3 +1328,19 @@ record shipped penalty-off for entire steam nights, #162); (3) the bench for ANY
here is scratchpad/night15/kd_bench.sh -- five HARD invariants (deaths==increments,
cost-per-death, credit-per-kill, zero swallowed, arithmetic witness); its predecessor's
assertions were greps that matched nothing, which is how both halves shipped broken.
## Weapon destroyed-state: the ONE-cell contract (#165 root cause, night-16) [T1]
Binary: subsystem `+0x40` (statusAlarm level) is THE destroyed cell -- all writers (base
TakeDamage @0x4ac0bc zone-saturation tail; cascade descent @0049c9a8; slot-8 pump watchdog),
all readers (Emitter gate @004baa88 `==1` strict; small-dial X @004c7844; techstat; the wire
header). Four alarm cells exist per weapon and are NOT interchangeable: +0x40 status/health,
+0x184 temperature (gate clause `==2`), +0x278 power feed (eng-page X only!), +0x364 weaponAlarm
FSM (level 7 "unavailable" = AMMO weapons only, @004bb9b8/@004bbd04 -- the #30 answer). The
eng-page energy X (@004c8990) draws on `+0x40==1 OR power!=4`: an X over a LOADED emitter that
legally fires its one loaded shot is AUTHENTIC binary behavior (power-fault case). Destroyed
emitters park at weaponAlarm=3 (Loading) via the gate's per-frame refusal -- "recharging under
the X" is authentic. Replication: weapon statusAlarm NEVER crosses the wire; destroyed-ness
reaches peers via ZONE records only; replicant emitters run a gateless Simulate (@004ba8d0) --
fire legality is master-only. Port fix + the four-store split it repaired: [[reconstruction-gotchas]] SS30;
bench: `scratchpad/night16/weaponx_bench.sh` (crit-path kill + partial-crit freeze regression).
+38
View File
@@ -826,3 +826,41 @@ classes (PlayerStatus/pilotList/...) with the config interpreter, which let
more of the authored page furniture parse + draw than before -- the leaking
high-index pixmaps rode in with that. [T3 for this correlation detail; the
leak itself and its fix are T2 live-verified.]
## Coolant-leak LAMP ROUTING (#172, night-16 hunt 2026-08-13) [T1 resolver + T2 field]
The leak lamp chain is INTACT -- every staged leak all night (35 events, 4 machines, incl. the
"fatal" case) asserted its lamp the SAME FRAME as the [techstat] cond-2 SET. The field reports
were three different presentation facts:
- **Generator leak -> the radar-rail generator button ONLY** (kBTGeneratorLamp @0051d070; both
authored item codes 0x80+0x87 collapse to that one lamp, mode 0xffffffff = the ALWAYS-ACTIVE
sentinel, never mode-gated). NO MFD carries any generator-leak indicator -- binary-authentic
(@004cc2fc). In glass surround that rail cell renders at the minimumStrip floor (~6px,
L4RIOBANK.cpp) -- Elengil's fatal lamp FLASHED for 28.4s. ~~"effectively invisible"~~
**CORRECTED SAME DAY (user challenge upheld, bench-verified):** the strip PAINTS AND BLINKS
correctly -- [lampblink] receipts show the painted shade alternating at a metronomic 125ms,
full frame rate, in a loop code-identical to 913's; Elengil's layout receipts show the stock
surround with the lamp on screen (~126px wide x ~6px strip @4Hz). The failure is
EXPECTATION, not visibility: testers watch the MFDs, and no MFD carries a generator-leak
indicator (authentic). Resolution: the handout's WHERE-TO-LOOK entry; the ALERT RING
deviation exists but is GATED OFF (BT_ALERT_RING=1) pending Friday evidence that testers
miss the lamp even knowing its location. Receipts: [lamppx] alert edges, [lampblink] shade
transitions (BT_LAMP_LOG), [seqloop] voice Start/Stop. Era question for Oracle: how
prominent was the pod's physical generator button light?
- **Condenser (loop) leak -> the per-loop button** (@0051d058, 1-based '2F 2E 2D 2B 2A 29';
the 0x2c gap is AUTHENTIC -- 0x2c belongs to the DISTINCT coolingLoop set of the code<0x80
path). Also always-active. No gate, immediate flash.
- **Weapon leak (AFC50 family) -> PAGE-SCOPED lamps only** (quad-select + its eng-page button):
the mode gate (@00444e64 == LAMP.cpp) skips the notify while the page is hidden and the
page-entry force-notify delivers it when the player pages there -- "flash appeared at the
second voice line" = the player paging to engineering after voice loop #1. AUTHENTIC;
documented for testers, not fixed. Coolant-cooled guns leak in PAIRS (weapon bit + serving
condenser bit) -- one page-gated lamp, one always-active loop lamp.
- **The VOICE is a LOOPING sequence** (0E4ACE50, tempo 160): Start on the coolantActive 0->1
edge, self-repeats, Stop on 1->0 (the #136 model REFINED: edge-started, level-looped,
edge-stopped; hysteresis flap = restart). Voice repetitions carry NO edge information --
the field testers used the voice as a clock and mis-dated the lamp. Receipt: [seqloop]
START/STOP, always-on for looped sequences.
- 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.
+51
View File
@@ -1194,3 +1194,54 @@ DATAPOINT (2026-08-11, the #162 retest wave): the kd4/kd8 benches audit this exp
and saw **0 inst=R DMG rows across three 4-node runs and one 8-node melee** (35 deaths total)
on build ~900 -- the behavior did not reproduce on the bench rig; whatever produced the
night-15 field rows needs field conditions (steam wire seam?) the LAN relay path lacks.
## Night-16 playtest intelligence (build 4.11.913, 2026-08-12) [T2 player testimony + log receipts]
Field-verified CLOSED this night: #162 (K/D exact all night, 3 independent confirms), #156
(fade), #154 (gen voice at trip), #135 (leak flashes the right lamp), #169 (lobby layout),
#128 (no ram leaks; ram crits land on gyro/torso progressively, myomer crits unobtainable --
consistent with internals-only, see #103). Logs: `scratchpad/night16/` + Oracle notes.
- **#170 -- post-mission menu-relaunch crash (TOP defect).** `[fe] mission over -- relaunching
the menu` -> `[crash] UNHANDLED EXCEPTION code=0xc0000005 addr=0x0 access=8 target=0x0`.
7 hits / 3 machines / identical module-relative stack, all frames in a Steam-family DLL;
a NULL function-pointer call dispatched by a Steam callback across the mission-teardown ->
menu-relaunch boundary. Intermittent (10 relaunches -> 3 crashes in one log). [T4 hypothesis:
callback left registered across teardown; possibly a #163/#156 teardown-ORDER regression.]
Players report it as "crash while joining lobby" -- it is not the join.
**ROOT-CAUSED + FIXED same night [T2]:** the first #163 fix's `SteamAPI_Shutdown()` inside
`BTSteamNet_ShutdownAll` raced steamclient's service thread (round-end = a burst of peer
closes always in flight). ShutdownAll is now a QUIESCE (no Shutdown; linger closes; seam
gated first) and every exit path dies via TerminateProcess -- see [[steam-networking]]
§THE EXIT CONTRACT. Field verify Friday; the crash filter now names foreign DLL frames.
- **#171 -- ballistic/missile hitscan era question (HOLD until group consensus).** Era witnesses
(Oracle, Lynx, Sauron; Dplotta softer; Draco DISSENTS) recall AFC/Gauss as instant-hit,
no lead, no flight animation, single-panel damage, no splash. 913 field observations: locked
AFC50 misses a perpendicular mover unless led; ballistics AND SRM/Streak/Narc hit beyond the
rangefinder max without limit; energy weapons DO stop damaging at rangefinder max (=> at least
one unmapped range-gating mechanism exists). The projectile flight clocks are [T1] from the
decomp (#168) -- reconcile: hit-resolved-at-trigger with cosmetic projectile? speeds/ranges
off? undiscovered range gate? Pod-hardware ground truth possible end of month.
- **#172 -- leak lamp first-flash delayed/absent.** Elengil: flash only on the SECOND voice line
(x2 spawns, AFC50/Zanin Neko); once ABSENT for an entire fatal leak (5-6 voice repetitions,
no flash on any MFD, died ~9:38PM CT -- the match's only death, receipt findable in night16).
Distinct from the closed #135: this is the SET-edge / first-flash arming path.
- **#173 -- chicken-walker seek-4 myomer overheat** (see [[subsystems]] night-16 note).
- **#165 ROOT-CAUSED + FIXED + BENCHED (2026-08-13, the 4-agent hunt):** gotcha SS30 -- the
binary's ONE destroyed cell was split into four port stores and the crit-lottery kill path
(MechWeapon::TakeDamage) wrote none the gate reads. Fixed (chain to MechSubsystem::TakeDamage,
FSM tail deleted, phantom simulationState initialized + bridged); weaponx_bench PASS both
phases. RESIDUAL UNKNOWNS from the hunt: (i) Elengil's 20:17 ET no-X facet needs ALIA's
PRE-ROTATION steam_20260812.log (ask Elengil -- staged file starts 22:20 ET); (ii) cascade
re-descend re-entrancy (zone 18 x7 re-rolls, own ticket); (iii) incident-(b) "no damage on
target": shooter log computed FULL damage (11.76) -- wire/victim-side loss, needs paired
matchlogs (fleet-wide BT_DMG_LOG + matchlog next night would make this attributable).
- **#172 RESOLVED (2026-08-13, 4-agent hunt): the lamp chain was NEVER broken** -- all 35
staged leak events flashed same-frame. Three presentation facts: generator lamp = the ~6px
radar-rail strip in glass (fatal case: flashed 28.4s unseen -> ALERT BLOOM deviation);
weapon-leak lamps are page-gated AUTHENTICALLY (the "delayed" flash = the player paging to
engineering); the leak voice LOOPS off one edge (testers' clock was wrong). See
[[gauges-hud]] SSCoolant-leak LAMP ROUTING. Receipts armed: [seqloop], [lamppx]. Residual:
Lynx's loop-6 report unstaged (his machine, no log); Elengil's AFC50 pre-rotation log still
wanted for the pair-leak confirmation.
- Nanook lost the coolant MFD glass window, recovered only by config edit -> #76 comment
(in-game layout-reset recovery path).
+50
View File
@@ -1014,3 +1014,53 @@ sampled a cell whose write timing differs from the binary's". Fix shape: run the
transition inline at the destruction edge (mech.cpp death_tail → `UpdateDeathState(0)`), keep the
per-frame caller for maintenance. Verified: kd4_bench 9/9/9/9 + kd8_stress 26/26/26/26, both
SWALLOWED=0 (the receiver dedup tripwire now never fires — no duplicate is ever GENERATED).
## 30. ONE binary cell, FOUR port stores: the destroyed-state split that survived three "fixes" (#165, night-16 2026-08-13)
The binary keeps ONE destroyed cell per subsystem: `+0x40` = the statusAlarm level -- written by
EVERY destruction path (base TakeDamage @0x4ac0bc at private-zone saturation; zone-cascade
descent @0049c9a8; the per-frame watchdog in the slot-8 message pump), read by EVERY consumer
(the Emitter fire gate @004baa88, the small weapon-dial X @004c7844, the techstat scan, the wire
via the Simulation record header). The port split that cell into FOUR stores that never talked:
the private DamageZone.damageLevel (drives the X), the ReconAlarm statusAlarm (gate clause 2 --
written ONLY by the cascade + bench hook), a phantom `int simulationState` (gate clause 1 --
ZERO writers, NEVER ctor-initialized: uninitialized pool memory, MEMBLOCK recycles unzeroed),
and the weaponAlarm FSM cell. `MechWeapon::TakeDamage` then bypassed the reconstructed
`MechSubsystem::TakeDamage` (which HAS the destroyed tail) for the raw engine base -- so the
everyday CRIT-LOTTERY kill saturated the zone (X lit) without ever writing a gate cell: X'd
weapons kept firing. Three prior "fixes" all aimed at the GATE, and the bench (BT_KILL_SUBSYS)
wrote both cells directly -- 4,213 refusals proving a gate that was never broken.
**Sibling defect in the same function:** the port-authored tail drove the weapon FSM from zone
state -- saturation -> level 0 (=Firing!), partial damage -> level 1 (NO case in either FSM:
silent freeze; with beamFlag armed = the 883 hanging beam, #164's upstream writer).
**Rules:** (1) when the binary reads/writes ONE offset, the port must have ONE authority --
alias, don't duplicate; if layout forces a twin field, every writer writes BOTH (grep-audit the
writer census, not the reader census). (2) Never let an override "simplify away" the
reconstructed base -- `MechWeapon::TakeDamage` calling `Subsystem::TakeDamage` instead of
`MechSubsystem::TakeDamage` dropped the invariant silently. (3) A bench hook that writes state
directly validates READERS only; destruction/state benches must drive the REAL writer path
(BT_CRIT_SUBSYS: Damage -> ApplyDamageAndMeasure -> virtual TakeDamage). (4) Every ctor
initializes every reconstructed field -- an unwritten int in pooled memory is a time bomb.
Verified: weaponx_bench P1 (crit kill: 0 FIRED after, 5,664 refusals, X+gate agree) + P2
(partial crit keeps firing -- the freeze is gone). Full mechanism: the night-16 workflow dossier
(4 agents, cross-examined) + docs/GEN_WARNING_AUDIT-style lanes in the #165 ticket thread.
## 31. GUARD-SCOPE NARROWING: a binary guard wrapping a whole block, transcribed onto its first statement (#174, 2026-08-13)
The binary's burst loop guards the ENTIRE per-burst application on zone-not-destroyed
(`if (piVar1[9] != 1) { crit-roll | zone TakeDamage; score tally; destroyed-flag }`,
part_012.c:14661-14677 @0x4a0446) -- a burst landing on a dead zone is a structural no-op:
no re-application, no cascade re-fire, NO SCORE. The port transcribed that guard onto the
crit roll ONLY; the unguarded TakeDamage re-saturated the zone every burst, whose >=1.0 tail
re-ran RecurseSegmentTable (night-16 field: zone 18 x7, zone 9 x4 per mission), each pass
re-pushing crit allotment into surviving subsystems (the ram "crits worsened with each
collision") -- and hits on wreckage kept SCORING (the binary awards zero). Neither
RecurseSegmentTable nor SendSubsystemDamage has any internal re-entry protection in the
binary (verified @0049cad4/@0049c9a8: no `used` write, no top gate) -- the delivery-layer
guard is the ONLY protection, so its scope is load-bearing.
**Rule:** when transcribing a decomp `if` whose body spans multiple statements, diff the
BRACE SCOPE against the port -- a guard attached to the wrong statement compiles, runs, and
passes every reader-side bench. **Bench rule (the #165 lesson, second application in one
week):** prove the bench detects the bug BEFORE trusting its pass -- the negative control
here (fix stashed) scored 147 cascades where the fixed build scores 1.
Bench: BT_ZONE_HAMMER=<zone>[,<amt>] (mech4.cpp) + scratchpad/night16/cascade_bench.sh.
+26
View File
@@ -60,6 +60,32 @@ reset on death), #69 (Sunder V1 → Denkou naming).
token / transport layer. Everything above the seam is the same arcade mesh — the three replication
symptoms this night produced would all reproduce on the relay.
## THE EXIT CONTRACT (#163 -> #170, settled 2026-08-12) [T2 field-proven]
Process-exit topology for every glass/steam process: **flush -> quiesce -> TerminateProcess.**
`BTSteamNet_ShutdownAll` (L4STEAMNET.cpp) is a QUIESCE: gate `steamActive` FIRST (stops
marshal/game-thread seam re-entry), `CloseConnection(..., linger=true)` on every connection
("round over", not a peer-side timeout), close listeners, one `sockets->RunCallbacks()` drain,
`Sleep(60)` grace. **It deliberately never calls `SteamAPI_Shutdown()`, and no exit path may
return through the CRT** (menu-quit included -- it dies via `BTOrderlyDie`, btl4console.cpp).
The two-failure history, because each fix's reason constrains the other:
- **883 (no teardown at all):** bare `ExitProcess` ran DLL_PROCESS_DETACH with live SNS threads
-> intermittent loader-lock deadlock -> "frozen view of how the game ended" + the menu child
hung in `SteamAPI_Init` against the wedged half-dead sibling (#163, night15).
- **913 (full `SteamAPI_Shutdown` before TerminateProcess):** at round end every peer closes
simultaneously, so status events are ALWAYS in flight; Shutdown tears client state down while
steamclient's own service thread dispatches them -> that thread calls literal NULL
(0xc0000005 access=8 target=0x0, BaseThreadInitThunk root, ZERO btl4 frames) in the window
before TerminateProcess (#170, night16: 7 hits / 3 machines / one module-relative stack).
- **The stable point:** TerminateProcess kills all threads atomically with every pointer still
valid (no detach, no dispatch window); the Steam client GCs the session on IPC pipe break --
the standard crashed-game path (883 relaunched all night with no Shutdown ever called). The
child-hang needed a WEDGED sibling, which TerminateProcess makes structurally impossible.
Forensics rider: the crash filter (btl4main.cpp `BTCrashFilter`) now prints foreign frames as
`<module.dll>+0xOFFSET` (VirtualQuery AllocationBase + GetModuleFileName), so a future
foreign-thread crash names its DLL instead of an unresolvable ASLR address.
## Key Relationships
- Rides: [[glass-cockpit]] miniconsole (marshal has a Steam-wire branch) · Extends:
[[multiplayer]] (the arcade mesh, unchanged above the seam)
+15
View File
@@ -338,3 +338,18 @@ forward at the respawn (physical lever / HOTAS — exactly who reported it) had
speed in the spike frames. The calibration facts above (constants byte-exact, dt-normalised
kinetic term) all STAND; the in-life governor (running hot at sustained top speed derates the
myomers) is authentic and remains.
## Spawn-discharge loop heat + per-chassis generator bias (night-16 field, 2026-08-12) [T3 field-observed, mechanism plausible-authentic]
Oracle's explanation for the long-standing "missiles cook off on one side first" reports
(Lynx): at spawn ALL weapons start discharged, and the initial CHARGE of the energy weapons
dumps heat into their loops; generator assignments are biased per chassis (Avatar: BOTH large
lasers on GeneratorA), so that side's loops start hot and a missile launcher sharing the side
cooks off first even if the player fires only missiles. Not yet verified against the authored
data -- verify via BTL4.RES generator/loop assignments before treating as established. [T3]
Related open defect/question: **#173** -- Vulture (and per Oracle ALL large chicken walkers)
cannot run seek-4 myomers without immediate overheat, loop 5 pegged, heat only sheds near
standstill (Elengil; Oracle repro). Investigate myomer heat rate + loop-5 capacity vs authored
data for those chassis; authored-balance vs port-artifact undecided. The myomer drive-heat
FORMULA is verified faithful (#137, above) -- if the rate is right, the question moves to the
authored loop capacity / condenser assignments.
+8
View File
@@ -226,3 +226,11 @@ wrote); affinity masks 2-cores-each up to 4 nodes, 1-each at 8.
- Uses: [[build-and-run]] (parity, env gates, BT_SHOT capture) · [[experience-levels]] (expert vs novice gating)
- Informs: [[reconstruction-method]] (step 4 "verify honestly" — this file is the how)
- Incident sources: [[reconstruction-gotchas]] §23 (pixels), §25 (same-pass latch timing — caught by the 4-node bench), §26 (silent stubs); [[multiplayer]] (replication layers)
## weaponx_bench (night16) -- #165 crit-path destruction
`scratchpad/night16/weaponx_bench.sh`: single node, vulture, BT_AUTOFIRE; P1 kills ERSLaser_1
via BT_CRIT_SUBSYS (the REAL writer path: Damage -> ApplyDamageAndMeasure -> virtual TakeDamage)
and asserts [crit] DESTROYED + [techstat] X + zero named FIRED after + REFUSED>0 + survivor
fires; P2 partial-crits to 0.5 and asserts the weapon KEEPS firing (freeze regression, the #164
upstream writer). LESSON (gotcha SS30): BT_KILL_SUBSYS writes the cells directly and can only
validate READERS -- state benches must drive the real writer path.
+3 -2
View File
@@ -629,7 +629,8 @@ attributes published on Emitter+Myomers — see the AUDIT finding A addendum bel
Byte-exact re-base of the PoweredSubsystem:HeatSink LEAF (Sensor/Emitter/Myomers) → radarPercent heat penalty
(delete the sensor.cpp:287-288 guard — ✅ LANDED: the guard is deleted and the leaf is byte-exact,
sensor.cpp:407-413) + the raw-offset cluster reads (partially locked since — MechWeapon byte-exact);
radar pip/name symbology + staticEntities (task #17, under reconstruction 2026-07-12);
radar pip/name symbology + staticEntities (task #17: pips landed the real L4GaugeImage path;
✅ staticEntities RESOLVED 2026-08-13, branch radar-static-geometry — see row 24);
CycleDisplayMode→ModeManager mask (✅ RESOLVED 2026-07-19, Gitea #6 [T2]: @4d1ae4 relabeled the
NotifyOfDisplayModeChange override (vtbl+0x4C) and made virtual; desktop 'N'/pad RightThumb →
CycleDisplayModeNow; secondary schematic swap dama→crit→heat pixel-verified via
@@ -687,7 +688,7 @@ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr b
| 21 | Eng myomer page: seek-step lamp | Myomers@0x320 INFERRED (ctor @004c8df4 not in the decomp export) | marked in code | T3 | CORRECT (best-effort; flagged) |
| 22 | Eng page: linked heat-sink number (hs+0x1d4 guarded raw) | PrepEngr numeric #2 | marked BEST-EFFORT (guarded) | T3 | CORRECT (guarded; flagged) |
| 23 | sec: radar map (wedge/blips/scale) | Mech Radar* attrs; wedge ← Torso::currentTwist@0x1D8 (Gitea #1) | #1 [T2]; LIVE: blips + wedge render, zoom via SetTargetRange | T2 | CORRECT |
| 24 | sec: radar pip/name symbology | GetVideoObject/LookUpPip/GetNameID | task #17 under reconstruction — cross-blip stand-in | T3 | DEFERRED-FEED |
| 24 | sec: radar pip/name symbology + MAP GEOMETRY | GetVideoObject/LookUpPip/GetNameID; MapDisplay::DrawStatic ← staticEntities | pips: the real L4GaugeImage path landed (task #17). **Map geometry RESOLVED 2026-08-13** (branch radar-static-geometry, playtester DOS comparison): DrawStatic was always faithful but staticEntities was EMPTY — (1) the per-frame RebuildEntityGrid Clear()+dynamic-only refill structurally excluded the 528 static-flagged UnscalableTerrain placements (arenall: 59 static vs 3 dynamic), and (2) the notify gate was transcribed as Terrain, which matches ZERO shipped records (1008 UnscalableTerrain + 789 CulturalIcon derive from Entity — @0x4e6ef8 was almost certainly UnscalableTerrain's derivation). Fix: staticEntities is now persistent, filled once per mission (AllEntityIterator, UnscalableTerrain-derived + rtype-18 pip; `[radar] static grid filled: N` under BT_RADAR_LOG), reset at LinkToEntity, pruned by the gate-corrected remove notify. Verified live: 326 entities filled, arena outlines draw | T2 | CORRECT |
| 25 | sec: headingPointer needle + numeric | YawPitchRoll decomposition (engine-convention port of euler[0]) | LIVE: 089→120 while turning under BT_GOTO | T2 | CORRECT |
| 26 | sec: numericSpeed | LinearSpeed (abs(adv)/dt) | LIVE: 0 parked → 176-182 walking | T2 | CORRECT (units question stays open) |
| 27 | sec: digitalClock MISSION TIME | engine mission clock (format enum arg) | LIVE: 09:19→06:04 counting | T0/T2 | CORRECT |
+18 -5
View File
@@ -5,8 +5,11 @@
build. Section A is the payoff -- please verify.
=============================================================================
HOW TO REPORT: drop a line in Discord with the issue number and PASS /
FAIL / what you saw. Screenshots and rough times help; logs get collected
as usual.
FAIL / what you saw. Screenshots and rough times help.
LOGS -- NEW, ONE CLICK: double-click SENDLOGS.bat in the game folder after
the session. It puts ONE small zip on your Desktop with every log from the
night (session logs, matchlogs, launch breadcrumbs) -- attach that one file.
No more hunting for the right .log, no more missing parts.
=============================================================================
-----------------------------------------------------------------------------
@@ -34,9 +37,19 @@ A. FIXED -- PLEASE VERIFY (headliners first: new fixes fielded THIS build)
TEST: take a leak (ram someone) -- the generator lamp for THAT loop
flashes, and stops when it should.
#136 Leaking SHUT loop responds to component toggles
EXPLAINED AS AUTHENTIC: the alarm SOUND is one-shot per new leak,
the lamp FLASH is continuous while leaking. Confirm the description
matches what you see and we close it.
EXPLAINED AS AUTHENTIC (refined): the leak voice STARTS at the leak,
LOOPS on its own while the leak persists, STOPS when it seals -- so
hearing it again does NOT mean a new leak. The lamp FLASH is
continuous while leaking. Confirm and we close it.
#172 "Coolant leak but no flashing indicator" -- WHERE TO LOOK
The lamp always flashed; it was in a place nobody watches. A
GENERATOR leak flashes the generator button BY THE RADAR (bottom
centre) -- NOT any MFD. That is authentic: no MFD carries a
generator-leak indicator. A WEAPON's leak lamp lives on its
engineering page and flashes when you page there (also authentic).
Loop leaks flash the loop button immediately. TEST Friday: take a
leak, look AT THE RADAR RAIL -- tell us if it still reads as missing
even knowing where to look.
#128 Ramming terrain = instant coolant leak
Ram damage eligibility fixed -- walls and rocks no longer puncture
loops. TEST: bump terrain hard; leaks come from combat, not curbs.
+15
View File
@@ -401,6 +401,17 @@ void
<< " tempo=" << (int)tempo
<< " divPerBeat=" << (int)divisionsPerBeat << std::endl; } }
// #172 field receipt, ALWAYS-ON for LOOPED sequences (the alarm family --
// leak warning, lock beeper; a handful per mission): night-16 proved the
// voice loop is the testers' CLOCK ("it said coolant leak 5-6 times") yet
// no field receipt tied voice to leak edges -- the fatal-case timeline had
// to be reconstructed from matchlog wallclocks. One line per Start/Stop
// EDGE makes voice-vs-lamp timing readable straight from any field log.
// (Looped only: one-shot effects fire constantly and would spam.)
if (isLooped && !isRunning)
DEBUG_STREAM << "[seqloop] START seq=" << (void *)this
<< " tempo=" << (int)tempo << "\n" << std::flush;
Check(this);
//
@@ -438,6 +449,10 @@ void
if (!isRunning)
return;
if (isLooped) // #172 receipt (see StartSequence)
DEBUG_STREAM << "[seqloop] STOP seq=" << (void *)this
<< "\n" << std::flush;
//
// Find next on or off event
//
+55 -12
View File
@@ -2798,6 +2798,7 @@ GaugeRenderer::GaugeRenderer():
{
Test_Tell("GaugeRenderer::GaugeRenderer()\n");
Check_Pointer(this);
staticEntitiesFilled = False; // radar map geometry: filled per mission
int
i;
@@ -3015,6 +3016,13 @@ void
Test_Tell("GaugeRenderer::LinkToEntity(" << entity << ")\n");
// RADAR MAP GEOMETRY (2026-08-13): a new mission link invalidates the
// persistent static grid -- clear it and let the next RebuildEntityGrid
// refill from the new mission's entities (the pod process hosts many
// missions back to back; stale pointers must not cross rounds).
staticEntities.Clear();
staticEntitiesFilled = False;
//
//--------------------------------------------------------------------
// Inform all gauges
@@ -3613,7 +3621,6 @@ void
Check(this);
movingEntities.Clear();
staticEntities.Clear();
if (application == NULL)
{
@@ -3629,13 +3636,18 @@ void
// projectiles that AllEntityIterator would flood the grid with -- iterate the
// dynamic masters (the local + AI/dummy vehicles) plus the dynamic replicants
// (peer vehicles in multiplayer).
// CLASSIFICATION (the phantom-red-pip fix, 2026-07-12): the dynamic
// iterators ALSO surface non-Mover world entities (the map's CulturalIcon
// props, class 0x5E, register dynamic on this port) -- unsorted, they drew
// through the radar's MOVING loop as RED contacts ("multiple pips with one
// player"). Sort by Mover derivation: vehicles into the moving grid (red
// pips), everything else into the static grid (the dim silhouette layer
// DrawStatic draws) -- the follow-up the old note deferred.
//
// STATIC GRID SPLIT (2026-08-13, the missing-radar-map-geometry fix): the
// per-frame rebuild used to Clear() + refill staticEntities from the SAME
// dynamic iterators -- which structurally excluded the map's STATIC-flagged
// UnscalableTerrain placements (528 of the 1071 outlined map records; on the
// arena maps essentially every wall/building), so MapDisplay::DrawStatic had
// nothing to draw and the radar showed no map. staticEntities is now
// PERSISTENT: filled ONCE per mission below (every UnscalableTerrain-derived
// entity carrying a rtype-18 GaugeImage -- static AND dynamic, so the
// CulturalIcon props the 2026-07-12 phantom-red-pip fix routed here still
// arrive), pruned by NotifyOfBecomingUninterestingEntity, reset at
// LinkToEntity. The per-frame loops below rebuild only the MOVING grid.
{
HostManager::DynamicMasterEntityIterator master_iterator(host_manager);
master_iterator.First();
@@ -3655,8 +3667,6 @@ void
if (st != 2 && st != 9)
movingEntities.Add(entity);
}
else
staticEntities.Add(entity);
}
}
{
@@ -3671,10 +3681,43 @@ void
if (st != 2 && st != 9)
movingEntities.Add(entity);
}
else
staticEntities.Add(entity);
}
}
//
// THE ONE-TIME STATIC FILL. Authoritative regardless of the interest
// feed's health: walk EVERY entity once per mission and keep the
// UnscalableTerrain-derived ones that carry a gauge representation (the
// rtype-18 GaugeImage in their model list -- the same records
// docs/MAPS.html renders). The class gate is UnscalableTerrain, NOT
// Terrain: the shipped content has ZERO Terrain records -- its map
// geometry is 1008x UnscalableTerrain + 789x CulturalIcon (a subclass),
// and our headers derive UnscalableTerrain from Entity, so the old
// Terrain gate matched nothing (see the btl4grnd.cpp note on @0x4e6ef8).
// Clear() first so any pre-fill interest-feed adds can't duplicate.
//
if (!staticEntitiesFilled)
{
staticEntities.Clear();
HostManager::AllEntityIterator all_iterator(host_manager);
Entity *entity;
int kept = 0;
while ((entity = all_iterator.ReadAndNext()) != NULL)
{
if (!entity->IsDerivedFrom(*UnscalableTerrain::GetClassDerivations()))
continue;
if (application->GetResourceFile()->SearchList(
entity->GetResourceID(),
ResourceDescription::GaugeImageStreamResourceType) == NULL)
continue;
staticEntities.Add(entity);
++kept;
}
staticEntitiesFilled = True;
if (getenv("BT_RADAR_LOG"))
DEBUG_STREAM << "[radar] static grid filled: " << kept
<< " map entities with gauge outlines\n" << std::flush;
}
Check_Fpu();
}
+5
View File
@@ -673,6 +673,11 @@ protected:
GraphicsPort *graphicsPort[maximumGraphicsPorts];
GaugeEntityList movingEntities;
GaugeEntityArray staticEntities;
// RADAR MAP GEOMETRY (2026-08-13): staticEntities is PERSISTENT, filled
// ONCE per mission by RebuildEntityGrid's authoritative walk (and pruned by
// NotifyOfBecomingUninterestingEntity). Reset at LinkToEntity so a pod
// process that hosts many missions refills per round.
Logical staticEntitiesFilled;
LampManager *lampManager;
GaugeAlarmManager *gaugeAlarmManager;
+38 -5
View File
@@ -155,6 +155,10 @@ static void
static void __cdecl
OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t *status)
{
if (!steamActive)
{
return; // #170: the quiesce window -- the process is dying;
} // no accepts, no table writes, just survive the call
ISteamNetworkingSockets *sockets = SteamNetworkingSockets();
switch (status->m_info.m_eState)
@@ -634,7 +638,8 @@ unsigned long long
}
//
// #163 -- the ORDERED transport shutdown for process exit (2026-08-11).
// #163/#170 -- the ORDERED transport QUIESCE for process exit
// (2026-08-11, corrected 2026-08-12 -- see the #170 block at the tail).
//
// The port never called SteamAPI_Shutdown(): every round-end died via bare
// ExitProcess with live SNS connections and Steam callback threads, and the
@@ -655,6 +660,10 @@ void
{
return;
}
steamActive = 0; // #170 FIRST: gate every seam entry
// (Pump/Send/Recv/Accept) against
// re-entry from the marshal or game
// thread while the table is torn down
ISteamNetworkingSockets *sockets = SteamNetworkingSockets();
for (int i = 0; i < MaxConnections; ++i)
{
@@ -662,7 +671,9 @@ void
&& connections[i].connection != k_HSteamNetConnection_Invalid)
{
sockets->CloseConnection(connections[i].connection, 0,
"round over", false);
"round over", true); // #170 linger: let the close packet
// actually transmit -- peers should
// read "round over", not a timeout
}
connections[i].inUse = 0;
}
@@ -674,7 +685,29 @@ void
listenSockets[p] = k_HSteamListenSocket_Invalid;
}
}
SteamAPI_RunCallbacks(); // let the closes flush client-side
steamActive = 0;
SteamAPI_Shutdown();
sockets->RunCallbacks(); // one SNS drain; our status callback
// stays REGISTERED and this module
// stays LOADED, so any late dispatch
// lands in live (guarded) code
Sleep(60); // bounded grace for the lingered closes
//
// #170 -- DELIBERATELY NO SteamAPI_Shutdown(). The first #163 fix called
// it here, and that OVER-CORRECTED into tonight's field crash: at round
// end every peer closes simultaneously, so ClosedByPeer/ProblemDetected
// status events are ALWAYS in flight when this runs -- and Shutdown tears
// the client state down while steamclient's own service thread is still
// dispatching them. Field signature (night16, 7 hits / 3 machines): a
// steamclient-internal thread (BaseThreadInitThunk root, ZERO btl4
// frames) calling literal NULL (0xc0000005 access=8 target=0x0) in the
// window between Shutdown and TerminateProcess. The contract now
// [T2 field-proven]: quiesce only; EVERY caller must die via
// TerminateProcess (never a CRT return -- that re-opens the DLL-detach
// deadlock this function no longer prevents). TerminateProcess kills
// all threads atomically with every pointer still valid; the Steam
// client GCs the session on IPC pipe break (the standard crashed-game
// path -- build 883 relaunched all night without a single Shutdown
// call). The 883 child-hang-in-SteamAPI_Init needed a WEDGED
// (detach-deadlocked, half-dead) sibling; TerminateProcess makes wedges
// structurally impossible, so the child boots clean without Shutdown.
//
}
+5 -3
View File
@@ -33,9 +33,11 @@ int
void
BTSteamNet_Pump(); // callbacks + rx drain (game thread)
void
BTSteamNet_ShutdownAll(); // #163: ordered transport death for
// process exit -- close every
// connection, then SteamAPI_Shutdown
BTSteamNet_ShutdownAll(); // #163/#170: transport QUIESCE for
// process exit -- gate the seam, close
// every connection (linger), drain.
// NO SteamAPI_Shutdown (#170: NULL-call
// crash); caller MUST TerminateProcess
//
// The wire-seam surface (BTNet* wrappers in L4NET.CPP + the marshal).
+100 -2
View File
@@ -1180,17 +1180,115 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
CkFill(device, 0, L.viewY, L.viewX, L.viewH, band); // left
CkFill(device, L.viewX + L.viewW, L.viewY, L.canvasW - (L.viewX + L.viewW), L.viewH, band); // right
// 2) Button lamp faces (UNDER the surfaces -- only the protruding edge shows).
// 2a) #172 ALERT RING (glass-only deviation, #154-class, user-approved
// 2026-08-13 -- the first cut, a +8px grown-face bloom, read as "the
// button got fat" on the bench and was rejected): an ALERTING lamp in
// a surround strip bank is a ~6px sliver (minimumStrip) -- Elengil's
// fatal GeneratorB leak FLASHED lamp 0x1b for 28.4s and nobody saw it
// (night-16; the pod's physical backlit buttons don't have this
// problem). Draw a thin PULSING OUTLINE 3px outside the lamp's
// border, UNDER everything: the lamp keeps its exact size, neighbors
// repaint their own faces, the surfaces cover the inward arc, and
// only the outward ring survives -- respecting the "no on-top
// overlay" rule (twice field-broken, see the note below step 3).
// The ring blinks WITH the lamp (same brightness sample) and only
// while it genuinely alternates (flash bits set AND levels differ);
// it vanishes on the lamp's own dark half-cycle, so it reads as
// "alarm light" rather than geometry.
// GATED OFF BY DEFAULT (BT_ALERT_RING=1 to enable), 2026-08-13: the
// bench + Elengil's layout receipts proved the lamp paints and blinks
// correctly on screen (see [lampblink]; code-identical loop in 913) --
// the field issue is EXPECTATION (generator leak lights the button by
// the RADAR, authentically; testers watch the MFDs), which the
// handout now states. The ring stays available in case Friday shows
// testers still miss it KNOWING where to look.
unsigned long tick = GetTickCount();
static int s_alertRing = -1;
if (s_alertRing < 0)
{ const char *ar = getenv("BT_ALERT_RING"); s_alertRing = (ar && *ar == '1') ? 1 : 0; }
const int kRingGap = 3; // lamp border -> ring inner edge
const int kRingWidth = 2;
for (int i = 0; s_alertRing && i < L.buttonCount; i++)
{
const BTCockpitBtn &b = L.buttons[i];
int state = CkLampState(b.address);
if ((state & 0x3) != 0
&& (((state >> 2) & 0x3) != ((state >> 4) & 0x3)))
{
int shade = BTLampBrightnessOf(state, tick);
if (shade < 2) continue; // dark half-cycle: no ring
D3DCOLOR fill, border;
CkLampColors(b.colorClass, 3 /*bright*/, b.inert, &fill, &border);
int rx = b.x - kRingGap - kRingWidth;
int ry = b.y - kRingGap - kRingWidth;
int rw = b.w + 2 * (kRingGap + kRingWidth);
int rh = b.h + 2 * (kRingGap + kRingWidth);
// four bars = the ring; the gap between ring and lamp stays band-dark
CkFill(device, rx, ry, rw, kRingWidth, fill); // top
CkFill(device, rx, ry + rh - kRingWidth, rw, kRingWidth, fill); // bottom
CkFill(device, rx, ry, kRingWidth, rh, fill); // left
CkFill(device, rx + rw - kRingWidth, ry, kRingWidth, rh, fill); // right
}
}
// 2b) Button lamp faces (UNDER the surfaces -- only the protruding edge shows).
for (int i = 0; i < L.buttonCount; i++)
{
const BTCockpitBtn &b = L.buttons[i];
int shade = BTLampBrightnessOf(CkLampState(b.address), tick);
int state = CkLampState(b.address);
int shade = BTLampBrightnessOf(state, tick);
if (gCkPressed == b.address || gCkLatched[b.address & 0x7F]) shade = 3;
D3DCOLOR fill, border;
CkLampColors(b.colorClass, shade, b.inert, &fill, &border);
CkFill(device, b.x, b.y, b.w, b.h, border);
CkFill(device, b.x + 1, b.y + 1, b.w - 2, b.h - 2, fill);
// #172 SHADE-TRANSITION receipt (BT_LAMP_LOG only, first 24 blinks):
// proves the surround paint loop actually COMPUTES the alternation --
// the "did the pixels flash" question, answered without a screenshot
// (the PNG-capture rig cratered the frame rate to ~1fps and was
// scrapped). This base fill loop is code-identical to 913's, so its
// verdict applies to the field build.
if (getenv("BT_LAMP_LOG"))
{
static unsigned char s_lastShade[128];
static int s_blinkLog = 0;
int slot2 = b.address & 0x7F;
if ((state & 0x3) != 0
&& (((state >> 2) & 0x3) != ((state >> 4) & 0x3))
&& (unsigned char)shade != s_lastShade[slot2]
&& s_blinkLog < 24)
{
++s_blinkLog;
DEBUG_STREAM << "[lampblink] 0x" << std::hex << b.address
<< std::dec << " shade " << (int)s_lastShade[slot2]
<< "->" << shade << " tick=" << tick
<< "\n" << std::flush;
}
s_lastShade[slot2] = (unsigned char)shade;
}
// #172 paint receipt: one line per ALERT edge per lamp -- the night-16
// forensics could prove the lamp STATE but had zero visibility into
// pixels ("the seam-to-pixels observability hole"). Cheap: fires only
// when a lamp's alternating-status flips.
{
static unsigned char s_wasAlert[128];
int alerting = ((state & 0x3) != 0
&& (((state >> 2) & 0x3) != ((state >> 4) & 0x3))) ? 1 : 0;
int slot = b.address & 0x7F;
if (alerting != s_wasAlert[slot])
{
s_wasAlert[slot] = (unsigned char)alerting;
DEBUG_STREAM << "[lamppx] 0x" << std::hex << b.address
<< (alerting ? " alert ON" : " alert OFF")
<< " cell=" << std::dec << b.w << "x" << b.h
<< (alerting ? "+ring" : "")
<< " at " << b.x << "," << b.y
<< " state=0x" << std::hex << state << std::dec
<< "\n" << std::flush;
}
}
}
// 3) Gauge surfaces on top (green-tinted mono / palette sec). Reuses the
+46 -33
View File
@@ -216,7 +216,28 @@ static LONG WINAPI
if (eip >= base && eip < base + 0x2000000UL)
std::cout << "btl4+0x" << std::hex << (eip - base);
else
std::cout << "0x" << std::hex << eip;
{
//
// #170: name the owning MODULE -- a foreign-DLL stack is
// unresolvable after the fact without its (ASLR'd) base; the
// steam-worker NULL-call needed this and didn't have it.
//
MEMORY_BASIC_INFORMATION fmbi;
char dll_path[MAX_PATH];
if (VirtualQuery((void *)eip, &fmbi, sizeof(fmbi)) != 0
&& fmbi.AllocationBase != NULL
&& GetModuleFileNameA((HMODULE)fmbi.AllocationBase,
dll_path, sizeof(dll_path)) != 0)
{
const char *dll_name = dll_path;
for (const char *s = dll_path; *s; ++s)
if (*s == '\\' || *s == '/') dll_name = s + 1;
std::cout << dll_name << "+0x" << std::hex
<< (eip - (unsigned long)fmbi.AllocationBase);
}
else
std::cout << "0x" << std::hex << eip;
}
MEMORY_BASIC_INFORMATION mbi;
if (ebp == 0 || (ebp & 3) != 0
|| VirtualQuery((void *)ebp, &mbi, sizeof(mbi)) == 0
@@ -710,29 +731,18 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
lt = shifted;
}
}
// SIZE ROLL-OVER -- never deletes anything, but keeps each FILE small
// enough to actually send. A playtest evening concatenates into one
// file (4.3 MB from a single session in scratchpad/night5/), and the
// channel this evidence travels through is a chat attachment. Part 0
// is <stem>_YYYYMMDD.log; once it passes the cap the NEXT launch opens
// <stem>_YYYYMMDD.1.log, and so on. Checked only at open, so one very
// long session can still overrun -- that is deliberate: never split a
// live session across files.
const DWORD part_max = 8UL * 1024UL * 1024UL;
for (int part = 0; part < 1000; ++part)
{
if (part == 0)
wsprintfA(resolvedLog, "%s_%04d%02d%02d.log",
logStem, (int)lt.wYear, (int)lt.wMonth, (int)lt.wDay);
else
wsprintfA(resolvedLog, "%s_%04d%02d%02d.%d.log",
logStem, (int)lt.wYear, (int)lt.wMonth, (int)lt.wDay, part);
WIN32_FILE_ATTRIBUTE_DATA fad;
if (!GetFileAttributesExA(resolvedLog, GetFileExInfoStandard, &fad))
break; // does not exist yet -- use it
if (fad.nFileSizeHigh == 0 && fad.nFileSizeLow < part_max)
break; // still room to append
}
// ONE NIGHT = ONE FILE (the 8 MB size roll-over RETIRED 2026-08-13).
// The roll existed because the evidence channel is a Discord
// attachment (free-tier cap: 10 MB today; 8 MB when this shipped) --
// but multi-part files cost us field evidence the moment they
// existed: testers attach "the log" (the newest part) and part 0
// stays home (night16: Elengil's 20:17 ET incident window was in the
// unsent pre-rotation part). Transport is now SENDLOGS.bat (ships
// in the dist root): zips every log/matchlog/lastrun to one ~10:1
// compressed attachment, so raw file size no longer matters and the
// whole night reads as one uninterrupted file.
wsprintfA(resolvedLog, "%s_%04d%02d%02d.log",
logStem, (int)lt.wYear, (int)lt.wMonth, (int)lt.wDay);
logSelfNamed = 1;
}
}
@@ -1241,17 +1251,20 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
if (BTFrontEnd_Run(&fe_spec) != 0 || fe_spec.mode == BTFeLaunchNone)
{
BTLastrunNote("clean exit: player quit from the menu");
#ifdef BT_STEAM
// #163: the menu keeps the Steam transport up for the lobby;
// returning through the CRT would run DLL detach with live
// Steam threads -- the same intermittent deadlock the
// round-end relaunch hit. Tear it down deliberately first.
//
// #163/#170: the menu keeps the Steam transport up for the
// lobby, and a CRT return would run DLL detach with live
// Steam threads (the 883 wedge). ShutdownAll is now a
// QUIESCE -- it deliberately does NOT stop those threads
// (SteamAPI_Shutdown mid-dispatch was the #170 NULL-call
// crash) -- so this path must die exactly like every other
// exit: flush, quiesce, TerminateProcess. One topology.
//
{
extern void BTSteamNet_ShutdownAll();
BTSteamNet_ShutdownAll();
extern void BTOrderlyDie(void);
BTOrderlyDie(); // never returns
}
#endif
return 0; // quit from the menu
return 0; // unreachable (TerminateProcess)
}
char fe_arguments[192];
char fe_value[64];
+3 -3
View File
@@ -181,9 +181,9 @@ static BOOL CALLBACK
return TRUE;
}
static void
BTOrderlyDie(void)
{
void
BTOrderlyDie(void) // #170: non-static -- the menu-quit
{ // path in btl4main dies through here
{
extern void BTMatchLogClose();
BTMatchLogClose(); // complete receipts on disk (the
+19 -2
View File
@@ -362,7 +362,21 @@ void
{
movingEntities.Add(entity); // FUN_004434f4, this+0x94
}
else if (entity->IsDerivedFrom(*Terrain::GetClassDerivations())) // 0x4e6ef8
//
// GATE CORRECTED 2026-08-13 (the missing-radar-map-geometry fix): this
// was transcribed as Terrain::GetClassDerivations() -- but the shipped
// content has ZERO Terrain records (census: 1008x UnscalableTerrain +
// 789x CulturalIcon, both deriving from Entity in our headers), so the
// Terrain predicate was dead code and rejected 100% of map geometry.
// The binary's @0x4e6ef8 must have been UnscalableTerrain's derivation
// (or the shipped-machine MUNGA derived UnscalableTerrain from
// Terrain). [T3 on which; the corrected gate matches the content.]
// The add is guarded on the one-time fill (GAUGREND RebuildEntityGrid)
// not having run -- post-fill notifications would double-add, and the
// fill's Clear()+walk supersedes any pre-fill adds.
//
else if (!staticEntitiesFilled
&& entity->IsDerivedFrom(*UnscalableTerrain::GetClassDerivations()))
{
staticEntities.Add(entity); // FUN_00443864, this+0xb0
}
@@ -409,7 +423,10 @@ void
{
movingEntities.Remove(entity); // FUN_00443510, this+0x94
}
else if (entity->IsDerivedFrom(*Terrain::GetClassDerivations())) // 0x4e6ef8
// Gate corrected 2026-08-13 to match the ADD side (see the note there):
// UnscalableTerrain, not Terrain. Post-fill this is the prune path for
// a destroyed/expiring static -- it leaves the persistent radar grid.
else if (entity->IsDerivedFrom(*UnscalableTerrain::GetClassDerivations()))
{
staticEntities.Remove(entity); // FUN_00443930, this+0xb0
}
+36 -18
View File
@@ -1233,29 +1233,47 @@ void
{
Mech__DamageZone *zone =
(Mech__DamageZone *)damageZones[zoneIndex]; // this[0x120][idx]
Subsystem *critted = 0;
if (zone->GetDamageZoneState() != DamageZone::BurningState // zone state != 1
&& BTMechCriticalChance(this, zoneIndex)) // @0x4a0164 (roll below)
//
// #174 (2026-08-13): the binary's zone-state guard wraps the WHOLE
// per-burst application -- `if (piVar1[9] != 1) { crit-roll |
// TakeDamage; tally; destroyed-flag }` (part_012.c:14661-14677,
// @0x4a0446) -- a burst landing on an already-destroyed zone is a
// structural NO-OP: no re-application, no cascade re-fire, NO SCORE.
// The port had narrowed the guard to the crit roll only, so
// continued fire into a dead zone re-saturated it every burst ->
// Mech__DamageZone::TakeDamage's >=1.0 tail re-ran
// RecurseSegmentTable (night-16: zone 18 x7, zone 9 x4 in single
// missions), each pass re-pushing the crit allotment into the
// surviving subsystems (the ram "crits worsened with each
// collision") -- and the shooter's damageTally kept scoring hits
// on wreckage the binary scores as ZERO.
//
if (zone->GetDamageZoneState() != DamageZone::BurningState) // zone state != 1
{
critted = zone->CriticalHit(message->damageData); // @0049ccc4
if (critted != 0)
Subsystem *critted = 0;
if (BTMechCriticalChance(this, zoneIndex)) // @0x4a0164 (roll below)
{
damageTally +=
((MechSubsystem *)critted)->CriticalScoreBonus(); // +0x108
if (BTEnvOn("BT_CRIT_LOG", 0))
DEBUG_STREAM << "[critroll] zone=" << zoneIndex
<< " -> " << (critted->GetName() ? critted->GetName() : "?")
<< " subLvl=" << ((MechSubsystem *)critted)->GetSubsystemDamageLevel()
<< "\n" << std::flush;
critted = zone->CriticalHit(message->damageData); // @0049ccc4
if (critted != 0)
{
damageTally +=
((MechSubsystem *)critted)->CriticalScoreBonus(); // +0x108
if (BTEnvOn("BT_CRIT_LOG", 0))
DEBUG_STREAM << "[critroll] zone=" << zoneIndex
<< " -> " << (critted->GetName() ? critted->GetName() : "?")
<< " subLvl=" << ((MechSubsystem *)critted)->GetSubsystemDamageLevel()
<< "\n" << std::flush;
}
}
}
if (critted == 0)
zone->TakeDamage(message->damageData); // zone vtbl+0x18 @0x4a0488
if (critted == 0)
zone->TakeDamage(message->damageData); // zone vtbl+0x18 @0x4a0488
damageTally += message->damageData.damageAmount; // +0x30
if (zone->GetDamageZoneState() == DamageZone::BurningState)
zoneDestroyed = 1;
damageTally += message->damageData.damageAmount; // +0x30
if (zone->GetDamageZoneState() == DamageZone::BurningState)
zoneDestroyed = 1; // set only by the burst that
} // DESTROYS it (binary: inside
// the guard, part_012.c:14674)
if (--burstsLeft == 0)
break;
+98
View File
@@ -7494,6 +7494,104 @@ void
}
}
// #165 BENCH (BT_CRIT_SUBSYS=<name>[,<name>...]): kill named subsystems
// through the REAL crit path -- Damage record -> ApplyDamageAndMeasure ->
// virtual TakeDamage -- NOT the direct cell writes BT_KILL_SUBSYS does.
// This is the path the field takes (the crit lottery), and the path the
// old 4,213-refusal bench never exercised: BT_KILL_SUBSYS wrote
// statusAlarm AND the zone directly, so it validated the fire gate while
// the field's writer (MechWeapon::TakeDamage) silently dropped the
// destroyed-side write. Applies one energy-damage chunk per frame from
// frame 900 until the subsystem's own zone saturates ([crit] receipt
// fires from MechSubsystem::TakeDamage on the edge), then stops.
// Env-gated, viewpoint mech only.
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_CRIT_SUBSYS"))
{
static int s_csFrame = 0;
if (++s_csFrame >= 900) // same settle window as killsub
{
char want_list[256];
strncpy(want_list, getenv("BT_CRIT_SUBSYS"), sizeof(want_list) - 1);
want_list[sizeof(want_list) - 1] = 0;
for (char *want = strtok(want_list, ","); want != 0;
want = strtok(0, ","))
{
// PARTIAL form ("Name=0.5"): stop at that zone level -- the
// freeze-regression phase (a partially-critted weapon must
// keep recycling; the old port tail froze it at alarm 1).
float target = 1.0f;
char *eq = strchr(want, '=');
if (eq != 0)
{
*eq = 0;
target = (float)atof(eq + 1);
}
for (int i = 2; i < GetSubsystemCount(); ++i)
{
Subsystem *s = GetSubsystem(i);
if (s == 0 || s->GetName() == 0) continue;
if (stricmp(s->GetName(), want) != 0) continue;
if (!s->IsDerivedFrom(MechSubsystem::ClassDerivations)) continue;
MechSubsystem *ms = (MechSubsystem *)s;
if (ms->GetSubsystemDamageLevel() >= target) break; // done
Damage d;
d.damageType = Damage::EnergyDamageType;
d.damageAmount = 0.75f; // FINE-GRAINED: a small-laser
// private zone saturates on a
// single 20.0 chunk (first run);
// ~0.04 level/frame ramps let
// the PARTIAL stop actually stop
d.burstCount = 1;
ms->ApplyDamageAndMeasure(d); // FUN_004ac07c -> virtual TakeDamage
if ((s_csFrame % 30) == 0)
DEBUG_STREAM << "[critsub] '" << s->GetName()
<< "' zone=" << (float)ms->GetSubsystemDamageLevel()
<< std::endl << std::flush;
break;
}
}
}
}
// #174 BENCH (BT_ZONE_HAMMER=<zoneIdx>[,<amt>]): 4x/sec, dispatch a
// TakeDamage with an EXPLICIT damageZone at the viewpoint mech itself.
// Pointed at a descend=1 non-vital zone (dz_larm=2 on most chassis),
// the zone saturates, cascades ONCE, the mech survives, and the hammer
// keeps landing on the DEAD zone -- the exact repro of the night-16
// re-descend (zone 18 x7). With the burst-loop guard at binary scope:
// ONE [cascade] for the life and the [dmghit] stream for that zone
// STOPS at saturation (the guard skips application entirely).
if ((Entity *)this == application->GetViewpointEntity()
&& getenv("BT_ZONE_HAMMER"))
{
static int s_zhFrame = 0;
++s_zhFrame;
if (s_zhFrame >= 900 && (s_zhFrame % 15) == 0) // settle, then ~4 Hz
{
int zidx = 2;
float amt = 8.0f;
{
char spec[64];
strncpy(spec, getenv("BT_ZONE_HAMMER"), sizeof(spec) - 1);
spec[sizeof(spec) - 1] = 0;
char *comma = strchr(spec, ',');
if (comma != 0) { *comma = 0; amt = (float)atof(comma + 1); }
if (spec[0]) zidx = atoi(spec);
}
Damage dmg;
dmg.damageType = Damage::ExplosiveDamageType;
dmg.damageAmount = amt;
dmg.burstCount = 1;
dmg.impactPoint = localOrigin.linearPosition;
Entity::TakeDamageMessage td(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(), zidx, dmg);
Dispatch(&td);
}
}
// task #13 scripted verify (BT_VALVE_TEST=1): one MoveValve press at
// frame ~600 -- Condenser1's valveState cycles 1 -> 5, so the flow
// redistribution gives it 5/10 of the total coolant flow ([valve] log
+23
View File
@@ -142,6 +142,11 @@ MechSubsystem::MechSubsystem(
{
refCount = NewRefCount(0x10); // this[0x3d]
this->owner = owner; // this[0x34] -- the owning Mech (canonical)
simulationState = 0; // #165: was NEVER initialized anywhere --
// MemoryBlock pools recycle unzeroed
// (MEMBLOCK.cpp), so stale garbage ==1
// could brick a weapon from spawn and
// draw the seek-graph destroyed bitmap
controlDestination = 0; // this[0x3a] = param_7 (ctor pass-through;
controlMessageID = 0; // this[0x3b] = param_8 weapons override -- task #6)
vitalSubsystem = 0; // this[0x39]
@@ -187,6 +192,8 @@ MechSubsystem::MechSubsystem(
// ctor only set hostEntity; subclasses read
// `owner` widely -> must be set or it is garbage.)
statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0xb, 0)
simulationState = 0; // #165: uninitialized-landmine fix (see
// the sibling ctor)
controlDestination = 0; // this[0x3a]=param_6 (ctor pass-through;
controlMessageID = 0; // this[0x3b]=param_7 weapons override -- task #6)
resource = subsystem_resource; // this[0x3c]
@@ -279,6 +286,7 @@ Scalar MechSubsystem::GetSubsystemDamageLevel() const
void MechSubsystem::ForceCriticalFailure()
{
statusAlarm.SetLevel(1); // +0x2C: 1 = Destroyed
simulationState = 1; // #165 twin (see TakeDamage)
if (printSimulationState) // +0x104 gate
{
PrintState(); // vtable slot 0x34 @4ac8c0
@@ -303,6 +311,7 @@ void MechSubsystem::ForceCriticalFailure()
void MechSubsystem::RespawnRepair()
{
statusAlarm.SetLevel(0); // operational
simulationState = 0; // #165 twin (see TakeDamage)
if (damageZone != 0)
{
((DamageZone *)damageZone)->damageLevel = 0.0f;
@@ -358,6 +367,7 @@ Logical
if (StatusThreshold <= ((DamageZone *)damageZone)->damageLevel) // _DAT_004ac140 (engine view)
{
statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb, 1)
simulationState = 1; // #165 twin (see TakeDamage)
if (printSimulationState != 0) // this[0x41]
{
OnAlarmChanged(); // (*this.vtable+0x34)(this)
@@ -386,6 +396,7 @@ void
((DamageZone *)damageZone)->damageLevel = 1.0f; // dz+0x158 = 1.0 (engine view)
}
statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb, 1)
simulationState = 1; // #165 twin (see TakeDamage)
if (printSimulationState != 0)
{
OnAlarmChanged(); // (*this.vtable+0x34)(this)
@@ -407,6 +418,7 @@ void
((DamageZone *)damageZone)->damageLevel = 0.0f; // dz+0x158 = 0 (engine view)
damageZone->alarm.SetLevel(0); // FUN_0041bbd8(dz+0x10, 0)
statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0xb, 0)
simulationState = 0; // #165 twin (see TakeDamage)
if (printSimulationState != 0)
{
OnAlarmChanged();
@@ -520,7 +532,18 @@ void MechSubsystem::TakeDamage(Damage &damage)
if (dz->damageLevel >= 1.0f) // _DAT_004ac140
{
// #165: named destruction receipt on the EDGE only (damage keeps
// landing on dead subsystems; the write below is idempotent) -- the
// crit-path kill had NO field receipt, which is how an X'd-but-firing
// weapon hid for a month. Always-on: once per subsystem per life.
if (statusAlarm.GetLevel() != 1)
DEBUG_STREAM << "[crit] '" << (GetName() ? GetName() : "?")
<< "' DESTROYED (zone saturated)\n" << std::flush;
statusAlarm.SetLevel(1); // Destroyed
simulationState = 1; // #165: the binary's +0x40 is ONE
// cell; the port split it -- keep
// the twin in lockstep (gate clause
// 1, seek-graph destroyed bitmap)
if (printSimulationState) // +0x104
{
PrintState(); // vtable +0x34 @4ac8c0
+29 -24
View File
@@ -411,34 +411,39 @@ Logical
// @004b96d4 -- slot 9 (TASK #51 RENAME: this body was mislabeled slot-6
// ReadUpdateRecord; @004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage,
// which dispatches on *param==4 == Damage::damageType -- damage semantics).
// The PoweredSubsystem::TakeDamage body (electrical-short handling) is not yet
// reconstructed; chain to the engine Subsystem base, and keep the damage-zone
// alarm tracking this port has shipped with (best-effort; the binary's alarm
// display on weapon damage flows through the unreconstructed @004b0efc chain).
//
// #165 ROOT CAUSE (night-16 hunt, 2026-08-13). The old body here called the
// raw ENGINE Subsystem::TakeDamage (zone accumulation ONLY) and then drove
// weaponAlarm from the zone state -- two defects in one:
// (1) it DROPPED the destroyed-side invariant: the binary keeps ONE
// destroyed cell per subsystem (+0x40 = statusAlarm level) that every
// destruction path writes and the fire gate reads. A weapon killed by
// the CRIT LOTTERY (the everyday field path) saturated its private
// zone (-> the MFD X, [techstat] condition 0) but never wrote the gate
// cell -- the X'd weapon kept firing (Oracle 20260812: ER PPC firing
// indefinitely; the PPC+SL pair firing until the arm CASCADE finally
// wrote the cell). The 4,213-refusal bench passed because
// BT_KILL_SUBSYS writes both cells -- it validated the gate, not the
// writers.
// (2) the weaponAlarm tail wrote the FSM cell BACKWARDS: saturation ->
// level 0 (Firing!), partial damage -> level 1 -- a state with NO case
// in either weapon FSM: the silent freeze (berserker's 20x [beam]
// STUCK-ON, and the state-1+beamFlag combination that drew Conn Man's
// 883 hanging beam -- #164's upstream cause). The binary never drives
// the FSM from damage: a destroyed weapon parks at Loading(3) via the
// fire gate's per-frame refusal (ResetFiringState, emitter.cpp).
// FIX: chain to MechSubsystem::TakeDamage -- the correctly-reconstructed
// binary base (@0x4ac0bc: zone TakeDamage, then damageLevel>=1.0 ->
// statusAlarm=1 + pin + vital-kill) -- and do NOT touch the FSM. The
// binary's electrical-short middle layer (@004b0efc) remains unreconstructed
// (a marked gap, unchanged); what is restored is the destruction invariant.
// MechSubsystem::TakeDamage guards damageZone==0 itself (the 2026-07-29
// Owens NULL-zone field crash stays guarded).
//
void
MechWeapon::TakeDamage(Damage &damage)
{
// FIELD CRASH GUARD (2026-07-29, build 641): the engine base derefs
// damageZone unguarded; an Owens weapon reached here with zone == NULL
// the first night crits could land (#80). See ApplyDamageAndMeasure.
if (this->Subsystem::damageZone != 0)
Subsystem::TakeDamage(damage); // engine base (real chain: @004b0efc)
// Port behavior (kept): a destroyed weapon clears its display alarm;
// otherwise the alarm tracks the resolved damage-zone state.
::DamageZone *dz = this->Subsystem::damageZone;
if (dz != 0
&& dz->GetGraphicState() == DamageZone::DestroyedGraphicState)
{
weaponAlarm.SetLevel(0);
}
else
{
weaponAlarm.SetLevel(
(dz != 0 && dz->damageLevel > 0.0f) ? 1 : 0
);
}
MechSubsystem::TakeDamage(damage); // @0x4ac0bc semantics
}
//
+19
View File
@@ -0,0 +1,19 @@
@echo off
rem ==========================================================================
rem BT411 -- package tonight's logs into ONE Discord-attachable zip.
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 ==========================================================================
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$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)');"
echo.
echo Attach the BTLOGS zip from your Desktop in Discord. Thanks!
pause
+1 -1
View File
@@ -77,7 +77,7 @@ def main():
exes = ["build/Release/btl4.exe"]
bats = ["players/play_solo.bat", "players/join.bat", "players/join_lan.bat",
"players/joyconfig.bat"]
"players/joyconfig.bat", "players/SENDLOGS.bat"]
if steam_on:
bats.append("players/play_steam.bat")
for p in exes + bats: