Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e29a4d97 | ||
|
|
5cde991f56 | ||
|
|
e693b481b3 | ||
|
|
81b0887033 | ||
|
|
8b3d1c8113 | ||
|
|
e614924107 | ||
|
|
fcd592c0f2 | ||
|
|
2fe58b8b89 | ||
|
|
c3258ee2e1 |
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -217,7 +217,7 @@ yet reconstructed (parse-skipped). The separate window is now the **live viewer*
|
||||
- **Pod MFD port-name reconcile (pod-only):** a ~5-line positional dual-name fallback in `SVGA16::Update`
|
||||
(`UL = GetGraphicsPort("auxUL2"); if(!UL) UL = GetGraphicsPort("Heat"); …`) so BT MFDs reach the pod's real
|
||||
monitors. NOT a CFG rename (the CFG uses BT names pervasively + `MUNGA_L4` is shared with RP's aux names).
|
||||
- **Polish:** overlay-plane compositing over `sec`; a pod-accurate RGB-packing toggle; texture atlas
|
||||
- **Polish:** ~~overlay-plane compositing over `sec`~~ (DONE 2026-08-13, row 28); a pod-accurate RGB-packing toggle; texture atlas
|
||||
(one lock/upload); device-reset recreate hardening for the additional swap chain.
|
||||
|
||||
## Widget reconstruction — the real fix (in progress; mapped by `bt-gauge-widget-recon-map`)
|
||||
@@ -691,7 +691,7 @@ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr b
|
||||
| 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 |
|
||||
| 28 | sec: sectorDisplay (overlay) | localOrigin → Round(−Z·0.01)+500 / Round(X·0.01)+500 | commit 4a4ec68 [BT_SECTOR_LOG live]; NOTE: overlay plane not composited into the dev sec cell (polish item) — data verified by log | T2 | CORRECT |
|
||||
| 28 | sec: sectorDisplay (overlay) | localOrigin → Round(−Z·0.01)+500 / Round(X·0.01)+500 | commit 4a4ec68 [BT_SECTOR_LOG live]; **overlay plane COMPOSITED 2026-08-13** (branch sec-overlay-composite): every sec expand (BTDrawGaugeSurfaces, BTDrawCockpitPanels, SVGA16::Update case 0, glass BlitSurface) now uses sec\|overlay = 0xFF — clut0 already held the arcade DAC's combined table (BuildSecondaryPalette spreads each port over the other's bit combos), so the fix is mask-only; the glass dirty token gained the overlay port so the numerals repaint. Verified live: SECTOR ticks + SCALE:1000 at spawn (1000 = radarRange init; the DOS shot's 4000 is max zoom) + the btsec1ov graticule now draws | T2 | CORRECT |
|
||||
| 29 | sec: schematic ARMOR view (cmArmor/multiArmor, dama.pcc) | zone damageLevel ×100 → adpal ramp | incr.3/4; all-green pristine LIVE | T2 | CORRECT (AUTH-STATIC all-green solo) |
|
||||
| 30 | sec: schematic CRITICAL view (cmCrit) | subsystem simulationState/damage | LIVE this audit: N-cycle shows the full subsystem list (GEN A-D, LOOP 1-6, HUD, SENSORS, GYRO, TORSO, weapons) | T2 | CORRECT |
|
||||
| 31 | sec: schematic HEAT view (cmHeat) | subsystem currentTemperature tint | #6 pixel-verified; re-cycled this audit (mask 0x450421→0x490421→0x510421) | T2 | CORRECT |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -1313,6 +1313,20 @@ static void
|
||||
int mask = port->GetBitMask();
|
||||
int palId = port->paletteID;
|
||||
int tint = w->monoTint;
|
||||
// OVERLAY COMPOSITE (2026-08-13): the radar's low byte is TWO ports -- sec
|
||||
// (0x3F) + overlay (0xC0: the graticule art, the SCALE numeral and the
|
||||
// SECTOR numerals). The arcade DAC indexed the full low byte and clut0
|
||||
// already holds the combined 256-entry table (BuildSecondaryPalette spreads
|
||||
// each port over the other's bit combinations), so the palette-expanded
|
||||
// window composites both planes with just the OR'd mask -- previously the
|
||||
// 0x3F mask left SECTOR:/SCALE: blank while their writers were live.
|
||||
if (tint < 0)
|
||||
{
|
||||
L4GraphicsPort *ov =
|
||||
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort("overlay"));
|
||||
if (ov != NULL && ov->graphicsDisplay == port->graphicsDisplay)
|
||||
mask |= ov->GetBitMask();
|
||||
}
|
||||
static int sMfdPal = -1;
|
||||
if (sMfdPal < 0) sMfdPal = getenv("BT_GLASS_MFD_PAL") ? 1 : 0;
|
||||
if (sMfdPal && tint >= 0)
|
||||
@@ -2110,6 +2124,11 @@ static unsigned long
|
||||
const char *ports[8];
|
||||
int np = 0;
|
||||
ports[np++] = w->portPrimary;
|
||||
if (w->monoTint < 0)
|
||||
ports[np++] = "overlay"; // composited into the palette-expanded
|
||||
// radar (2026-08-13) -- the SECTOR/SCALE
|
||||
// numerals tick on THIS plane, so their
|
||||
// churn must mark the window dirty
|
||||
if (w->portAlt != NULL) ports[np++] = w->portAlt;
|
||||
for (int gi = 0; gi < w->groupCount && np < 7; ++gi)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
+132
-5
@@ -860,6 +860,31 @@ static const float kBTPanelH = 480.0f;
|
||||
// channel-enabled and which is BlankColor; the draw loop below skips the
|
||||
// blanked plane, so the dev cell shows exactly what the pod monitor shows.
|
||||
// The Eng entries share their sibling's cell rect and FOLLOW it in the list.
|
||||
//
|
||||
// OVERLAY COMPOSITE (2026-08-13). The secondary CRT's low byte is authored as
|
||||
// TWO ports -- sec (bits 0x3F, btspal) and overlay (bits 0xC0, btopal,
|
||||
// TransparentZero) carrying the graticule art, the SCALE numeral (RadarRange)
|
||||
// and the SECTOR numerals (sectorDisplay). The arcade DAC indexed the FULL low
|
||||
// byte, blending both planes; every port expand used to mask to sec's 0x3F,
|
||||
// which is why SECTOR:/SCALE: read blank on desktop while their reconstructed
|
||||
// writers were live in the buffer the whole time. clut0 already holds the
|
||||
// DAC's combined 256-entry table (BuildSecondaryPalette spreads each port's
|
||||
// colors across the other's bit combinations), so the composite is exactly
|
||||
// this mask -- no blending code.
|
||||
//
|
||||
static int BTSecCompositeMask(GaugeRenderer *gr, L4GraphicsPort *sec_port)
|
||||
{
|
||||
int mask = sec_port->GetBitMask();
|
||||
if (gr != NULL)
|
||||
{
|
||||
L4GraphicsPort *ov =
|
||||
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort("overlay"));
|
||||
if (ov != NULL && ov->graphicsDisplay == sec_port->graphicsDisplay)
|
||||
mask |= ov->GetBitMask();
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
static const BTGaugeSurfaceDesc kBTGaugeSurfaces[9] =
|
||||
{
|
||||
{ "Heat", 0xFFFF, 0.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
|
||||
@@ -946,7 +971,9 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
|
||||
rot = s_secRot;
|
||||
}
|
||||
svga->DrawDevSurface(
|
||||
device, i, port->GetBitMask(), port->paletteID, d.monoTint,
|
||||
device, i,
|
||||
d.monoTint < 0 ? BTSecCompositeMask(gr, port) : port->GetBitMask(),
|
||||
port->paletteID, d.monoTint,
|
||||
px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph, rot);
|
||||
}
|
||||
|
||||
@@ -1180,17 +1207,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
|
||||
@@ -1224,7 +1349,9 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
|
||||
}
|
||||
int surf = ckSlotOf[i];
|
||||
int useTint = (d.monoTint < 0) ? -1 : tint;
|
||||
svga->DrawDevSurface(device, i, port->GetBitMask(), port->paletteID, useTint,
|
||||
int useMask = (d.monoTint < 0) ? BTSecCompositeMask(gr, port)
|
||||
: port->GetBitMask();
|
||||
svga->DrawDevSurface(device, i, useMask, port->paletteID, useTint,
|
||||
(float)L.surfX[surf], (float)L.surfY[surf], (float)L.surfW[surf], (float)L.surfH[surf], rot);
|
||||
}
|
||||
}
|
||||
@@ -5735,7 +5862,7 @@ Logical SVGA16::Update(Logical forceAll)
|
||||
//Drawing secondary, make sure we got secondary
|
||||
if (secPort != NULL)
|
||||
{
|
||||
secMask = secPort->GetBitMask();
|
||||
secMask = BTSecCompositeMask(renderer, secPort); // sec | overlay (DAC parity)
|
||||
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
|
||||
} else
|
||||
{
|
||||
|
||||
+46
-33
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
+36
-18
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user