6d7946a264e81329564d3e56e2d4a7e1a2d14b6c
214
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ceee71ce85 |
lamp-sweep comment: #45 citation was the scoreboard issue -- cite the field reports
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a338a5db89 | Merge remote-tracking branch 'origin/glass-lamp-latency' | ||
|
|
6180a44c64 |
#91 thor black rectangle: it's the OWN missile pod -- blakskn now material-keyed
Three testers reported a black rectangle swaying with the footsteps in the thor cockpit (Summoner = the thr1 label, same THX canopy -- no isolation). Decode: the inside view is a PER-MECH AUTHORED type-A set, not the cop alone (no fallback in EntitySegment::GetVideoObjectName -- authored data): madcat/vulture/bhk1 = cop; sunder/loki/avatar = +tor; thor = +tor +MSL (the shoulder pod, the reported rectangle); owens = +both legs +tshd. The pod/leg pilot-facing surfaces use the SAME "<pfx>skin:blakskn_dz_*" interior-structure material as the canopy frame, but the unlit frame constant was keyed on the _cop FILENAME -- identical material rendered (0.13,0.12,0.15) on the canopy and pure (0,0,0) on the pod/legs ([matlog]: owx_cop blakskn vcol=FF211F26 vs owx_lule vcol=FF000000). Fix (bgfload.cpp): the frame-constant treatment keys on meshIsCop OR material contains "skin:blakskn_dz_". mechfx:blakskn_mtl (tshd shadow quads) deliberately excluded. Verified: zero pure-black px in the lower view band across walk captures, the pod plate renders frame-toned and blends with the bar at rest (the reported anomaly dissolves), owens legs read as coherent structure, canopy/terrain un-regressed. Diags added: BT_MAT_LOG=<stem> per-batch material routing dump (bgfload), BT_HIDE_INSIDE_SEG=<substr> inside-mesh hide (btl4vid), [view] per-segment inside-roster names. KB: cockpit-view.md exactly-one claim corrected + the #91 section; bench scratchpad/night11/thorrect.sh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f99003cf50 |
Glass cockpit lamps: sweep every frame so lit buttons track the sim under load
Playtesters (all on the glass surround) reported the cockpit lighting sometimes going slow or nonexistent while the 3D view stayed perfectly smooth. The RIO serial/lamp stack is byte-identical to pod-proven Red Planet (diffed L4LAMP/L4RIO/L4SERIAL), so the wiring was fine -- the defect is the glass UPDATE CADENCE: The lit buttons draw every frame (BTDrawCockpitPanels, reading PadRIO::GetLampState), but the lamp STATE sweep that fills that store (LampManager::Update -> AssertNewLampValue -> SetLamp) rides the gauge renderer's FOREGROUND turn, which fires only once per full gauge cycle (foreground->background->copy). The cycle can't reach the next foreground turn until the throttled background gauge sweep drains the ~140-instrument active list -- and that background task starves under MP load (issue #45, "instruments freeze while fps stays healthy"). So the lamp sweep ran ~1x/second: buttons froze / flashes stalled while the view (a separate per-frame foreground render) stayed smooth. Fix: BTGlassSweepLamps() runs the lamp sweep EVERY frame on the dev/glass composite path (L4VB16.cpp, in BTDrawGaugeInset), decoupled from the gauge cycle. It is cheap -- deduped state pushes over ~72 lamps, no raster. The pod never enters BTDrawGaugeInset (real gauge hardware), so its authentic bandwidth-paced serial lamp cadence is untouched. BT_GLASS_LAMP_SWEEP=0 restores the authentic once-per-cycle behaviour. Verified: Release links clean (only the 40 tolerated /FORCE externals); glass surround boots and runs, lamp sweep pushes state (43 [lamp] pushes, buttons lit), ~60 fps, no crash. The under-load win is structural (the sweep is now an unconditional per-frame call); true MP-starvation repro needs multiple nodes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d86b65d7aa |
#118 STRICT: remove the eng-page eject-key hook -- soft keys never pilot-eject
Operator called binary-faithful; the trace agrees. The hook (bfc072b/
|
||
|
|
675fe68cb0 |
#118 correction sweep: the ARCADE's eject was LIVE -- 'dead code in 4.10' was wrong
The absolute-pointer scan missed the E8-relative call; the later byte-scan found it (FUN_004a9b5c+0x10 -- the master performance evaluates eject permission per frame in the shipped binary). Under deadline pressure the disproven 'unfinished/dead code' claim leaked back into three comments and the handoff; swept per the correction mandate. Settled press model, now stated correctly everywhere: pilot eject = the pilot KEYPAD bank while armed (+ the PANIC key reporting through that matrix). The MFD soft keys NEVER pilot-eject in the binary -- every page routes them to authored functions; the flashing engEject cell is the INVITE LAMP. The port's page-gated eng-key eject is a MARKED CONVENIENCE deviation (operator-requested), and the coolant hysteresis is a MARKED smoothing deviation for a degenerate boundary oscillation -- not completions of unfinished code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
78722542fd |
#118: REMOVE the full-face flash overlays -- the MFD corruption, root-caused
The operator's three captures decode as ONE mistake drawn two ways: the cockpit compositor deliberately draws the side buttons as BIG rects tucked UNDER the display surfaces (step 2), with only the protruding edge visible -- the edge IS the port's lamp light (the pod's backlit keys sat beside the CRT; there is no in-display face to light). My on-top "flash overlay" ( |
||
|
|
86a6bbb9cb |
#118: the flash overlay lands in the REAL compositor (L4VB16) + [ckflash] probe
The first visibility fix painted L4GLASSWIN -- a window the operator's single-window layout never shows. The main-window cockpit compositor is BTDrawCockpitPanels (L4VB16): same under-the-surface masking, so the RIO flash was again invisible. The 3b overlay now re-draws flashing lamps (state & 3) above the surfaces there, tick-phased; [ckflash] (BT_LAMP_LOG) logs each animated draw. Verified in-engine: addr 0x27 state 0x37 alternating shade 1 -> 3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5f79dcee3b |
#118: flashing lamps draw ON TOP of the MFD imagery -- the invite is visible
The glass windows paint buttons UNDER the display surface (only a sliver of edge protrudes), so the RIO flash commands were arriving ([lamp] 0xb <- 0x37 FLASHING in every trace) while the operator "never saw it flash" -- the blink was a few masked pixels. Lamps carrying RIO flash bits (state & 3) now re-draw their full face above the surface with the tick-phased shade: the gotoEngineering / engEject invites read like the pod's backlit keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fa3d634ed3 |
#118 crash fix: the eng-eject page gate moves into the game-thread RIO drain
The page-gated eject query ran in the window-click thread (PadRIO EmitButton) and dereferenced the TU-local application global -- NULL there under the /FORCE duplicate-symbol layout -> c0000005 at mode-manager +0x50 the moment the operator pressed the eng eject key (field crash, live.log [crash] record). Moved into LBE4ControlsManager::ProcessRIOEvent's ButtonPressedEvent branch, where mode_mask is already resolved on the game thread; PadRIO now only queues events. Same gate semantics (armed + that bank's ENG-page mode bits); verified no-crash + no-eject on the quad page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bfc072b56f |
#118: the ENG-PAGE EJECT KEY ejects -- page-gated intent completion
The decode that settles it: the eng-page side-key functions align key-for- key with the engEject lamp map (selectGenA-D = 0xF-0xC, EJECT = 0x0B, GEN-MODE = 0xA, COOLING = 0x9, BACK = 0x8; banks 2/3 at 0x27-/0x7-), and the streamed weapon pages route the same key to EjectAmmo -- so 0x0B/0x23/ 0x03 ARE the authored EJECT position. On generator pages the shipped stream leaves the key with NO live route (msg 9, no handler anywhere): eject was UNFINISHED in 4.10 (the evaluator was dead code -- the arcade never armed panic at all). Intent-completion [T3, marked]: while PANIC is armed AND that bank displays an ENGINEERING page (kBTEngModeMask bits -- the same gating the alarm lamps use), the flashing EJECT key fires the pilot-keypad eject. Any other page: authored function untouched (the two field regressions came from skipping the page gate). Verified: armed + QUAD page + 0x0B press -> no eject (gate holds); panic / numpad / Backspace unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
417008a6d4 |
#118 faithful settlement: MFD soft keys NEVER eject -- panic/keypad only
The streamed .CTL dump (BT_CTRLMAP_LOG, 109 records on the madcat) settles
the eject-input question from CONTENT: no 0x200000 record exists -- the MFD
soft keys carry their authored per-page routes unconditionally (elem 0xB on
a weapon eng page = msg 0xB EjectAmmo, the round eject), so they must never
route to pilot-eject. The invite-cell routing (
|
||
|
|
b4b98a64c0 |
#118 fix-the-fix: armed eject routes ONLY the invite cells + Panic
Field-caught by the operator within minutes: the blanket MFD key-space routing (0x00-0x2F) made the DISPLAY navigation key eject the mech mid-page-switch while armed. Narrowed to exactly the guarded PANIC button (0x3D) and the three eng-page EJECT INVITE cells (0x0B/0x23/0x03, the addresses the engEject 0x85 alarm lamp flashes on). Navigation is safe while armed; the flashing cell still punches out (re-verified through the click seam: armed -> press 0x0B -> PUNCH-OUT). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b9430cc5a7 |
#118: the MFD eject cell ejects -- all pilot-keypad-space keys route while armed
The operator's pod flow ("the engineering panel's bottom-left button flashes
EJECT, you push it") is input #3 of 3. The eng-page eject cell resolves to
MFD side-column addresses 0x0B/0x23/0x03 (kBTEngBankTop - 4 per bank), and
the binary's ONLY armed eject binding is the KeyboardPilot group (0x200000
-> mech msg 0x19; exhaustive 0x200000 scan: no other consumer) -- so on the
pod those keys report through the pilot keypad matrix. Desktop mirror: MFD
key-space presses (0x00-0x2F) and Panic (0x3D) also land as pilot-keypad
keys. Outside panic mode the binding is mode-masked dead -- normal MFD
operation untouched; armed, the flashing cell (or any keypad key -- the pod
semantic) punches out.
Verified through the click seam: gens dead -> armed -> press 0x0B ->
PUNCH-OUT.
All three eject inputs now live: Panic button, Backspace/pad LeftThumb,
and the flashing MFD eject cell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
611b1a8bbd |
#118: the PANIC BUTTON ejects -- keypad-bank routing + the 0x17/0x19 id bug
Two finds close the press path the operator field-tested: 1. The pod's guarded PANIC key reports through the PILOT KEYPAD bank -- the binary's ONLY panic press binding is keyboardGroup[KeyboardPilot].Add( mode 0x200000, mech, 0x19) (FUN_004d266c); there is NO buttonGroup[0x3d] consumer (0x3d is the lamp address). The desktop Panic click now ALSO lands as a pilot-keypad key (PadRIO::EmitButton -> EmitKeypad), so the armed-mode binding fires; a healthy press stays a mode-masked no-op, exactly the pod behaviour. 2. KeypressMessageID resolved to 0x17 via enum-chain arithmetic; the binary passes LITERAL 0x19 (two mapper ids are unreconstructed). The armed keypad send arrived at the Mech as msg 0x17 -- no handler, silently swallowed. Pinned to 0x19 (Mech::EjectPilot's id on the OWNER receiver -- the collision is the design). Verified headless through the REAL click seam (BT_BTNTEST=61): generators killed -> panic-arm ON -> screen-click Panic -> [lbe4key] unit 0 mode 0x650421 -> PUNCH-OUT. Also: IsDisabled eject refusals now log; [padkey]/ [lbe4key] seam tracing under BT_PAD_LOG. Still open (cosmetic): the flashing eject slot on the engineering MFD shows no graphic -- the streamed page element's pixmap is unreconstructed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1efe8efc68 |
#118 tails: death score cost + the DeathWithoutHonor console notice
* SpecialCaseDeathPenalty restored to the engine ScenarioRole (SCNROLE.h/ .cpp) -- the 4.10-only role field the 2007 WinTesla source dropped (record slot [1] after KillBonus, fail-hard read; role+0x20; the DefaultRendererRate pattern). Its consumer decoded from the @004c05c4 export gap (@004c07cd, missing from the #52 reconstruction): every death with advancedDamageOn hands the ENGINE base ScoreMessageHandler (@0042da20 == PLAYER.cpp:138, a DIRECT call bypassing the BT type Verifies) a type-1 award of -penalty. Wired verbatim. Shipped content authors the key nowhere -> cost 0 in the field. NUANCE [T4]: the 1995 engine adds at Player+0x1c8, which no BT scoreboard reads (+0x278) -- the pod's cost may never have displayed; our single-cell port shows it. * The eject console notice: FUN_004c198c IS the ConsolePlayerMechDeathWithoutHonorMessage ctor (T0 BTCNSL.CPP survives and is compiled) -- "death without honor" is the punch-out record. BTPlayerEjectBookkeeping now sends it to the console host through the same ConsoleClientID path as the VTVDamaged notify, then latches suppressConsole: the eject notice REPLACES the death notify. Verified two-node: "notice -> console host 1" fires on the ejecting node; the relay-side delivery of client-5 traffic is a pre-existing question shared with the never-observed VTVDamaged sibling (operator-console workstream). * Eject alarm-10 audio: resolved NEGATIVE -- the five Eject*.wav ship in AUDIO/ but BTL4.RES references them nowhere (orphaned like the SQUAT clips); wiring them would be a stand-in. Operator-optional deviation. * RIO 0x38 panic control: deferred with the plumbing documented (the glass Flight-Controls window already renders the 0x38 bank; consumer = read buttonGroup[ButtonPanic] alongside the binding-engine edge) -- pod-hardware-only, untestable on this bench. Two-node verify (fresh-exe rerun after a stale-exe trap): REFUSED healthy, PUNCH-OUT after generator kill, death + wreck swap + smoke + un-wreck warp all replicated on the observer, respawned mech refuses again, console notice fired. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6dbeb3d266 |
wreck smoke dies WITH the wreck; the audio/effect clock is calibrated to the byte-proven 28
USER REPORT: "when you destroy a mech, it continues to smoke for a very long
time after the mech disappears... up to 30secs to a minute."
MEASURED (new [pfx] timed census with per-def particle attribution): the
death plume (psfx 1 DDTHSMK) was re-armed on a 10s cadence as a WORLD effect
(ownerTag=0) -- so neither the burial transition nor the respawn cleanup
(StopAllEntityEffects) could touch it. The last-armed window kept emitting
over the empty spot for up to 10s after the hulk vanished, plus 6+-2s authored
particle lives, and back-to-back windows stretched the visible tail to the
reported range. The re-arm itself is a marked PORT ADDITION [T3]; the binary
fired DDTHSMK once -- its 10s window + particle lives ended right around the
~17s hulk sink. The authored design: the smoke dies WITH the burial.
FIX: the plume is spawned ATTACHED (BTStartPfxAttached, tagged to the dying
entity -- it also rides the sinking hulk now), and the burial transition calls
BTStopEntityPfx alongside the respawn cleanup that already did. Verified over
a 175s run containing FOUR wreck events (the enemy kill + three player bay-
fire deaths): every plume's emission ends at its wreck's burial or respawn,
particles fade within ~8-11s, no orphan emitters remain.
THE COUPLING (user relay of Oracle: "prolonged smoke and explosion INCLUDING
SOUND... same lever?") -- CONFIRMED as a family: the binary's audio layer
times in FRAME COUNTS converted by the engine frame-rate global:
AudioTime::Seconds_To_Frames == fmul [0x52140c]=28.0; fadd 0.5; round
(@0x42c611, @0x42dd86 -- the 0x42xxxx cluster of the ~90 global consumers)
Our engine's DefaultRendererRate said 30: every audio duration, sequence
delay and compression window ran ~7% fast. Calibrated to 28 [T1]. NOT yet
explained by this: a truly SUSTAINED/looping explosion sound (7% is not
"prolonged") -- the effect stop-path trace stays open on #114/#51.
Tooling: BTPfxParticle carries defIndex; [pfx] census (BT_PFX_LOG, 2s) prints
emitters + particles PER EFFECT SLOT with a timestamp; wrecksmoke.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5d6ded73f0 |
#110: cross-pod verified BOTH ways -- the cascade replicates, and peer fire alone drives it
Two two-node benches close the last stated gap ("no two-node run has watched a
peer's view of an arm death"), the second at the user's insistence on the REAL
field composition after the first used the self-damage scalpel:
1. mp_armchain.sh (scalpel): B self-destroys dz_rarm mid-fight. A's screen
receives the whole cascade -- [zone-repl] dz_rarm lvl 0->1 AND the cascaded
dz_rgun lvl 0->1 -- plus the respawn reset coming back clean (1->0). B's
post-cascade AFC pushes turned out to be the RESPAWN RE-ARM, correct
behavior, not a gate failure. (Also confirmed: the self-damage harness
dispatches a real Entity::TakeDamageMessage -- the identical path a peer's
round takes, so the scalpel and the field share every line from the handler
down.)
2. mp_armgrind.sh (the field composition): stand-off at 180, NO self-damage
anywhere, A grinding B with real cross-pod fire for six minutes. On B:
* 25 incremental arm-zone hits from A's weapons via the cylinder tables
(dz_rgun reached 1.0 from pure accumulation -- 15 cascades on rehits)
* dz_rarm finished by B's OWN AFC bay cooking off (AmmoBinAFC100 BAY FIRE
350, half into dz_rarm) -- itself lit by A's fire crits: peer-originated
end to end
* [cascade] zone 9 -> AFC100 + AmmoBinAFC100 + Condenser6 force-failed,
fire gate refusals logged
On A: [zone-repl] dz_rarm 0->1 and dz_rgun 0->1 -- the shooter SEES the arm
and pod die. B symmetrically received A's torso deaths.
Also: DamageZone::ReadUpdateRecord gets a change-only [zone-repl] diagnostic
(BT_MP_LOG) -- the probe that made the peer side measurable; mp_armgrind.sh
opens with the stale-node taskkill the other benches already had (a leftover
node from an aborted launch joined a later lobby as a third instance).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
06adaee523 |
#110: the zone destruction cascade is LIVE -- an arm now takes its gun pod with it
Lynx: "You can destroy an upper arm, and the gun pod is intact and can still fire." Conn Man's audit: three chassis, both arms, reproducible. And the user is right that this family was claimed fixed before: #86 gated fire on the WEAPON's own destruction and was verified by destroying the weapon directly (BT_KILL_SUBSYS) -- the field scenario was the ARM ZONE dying with the weapon subsystem healthy, which that bench never reproduced. Oracle's 693-night "I can still fire destroyed energy weapons" was almost certainly this mechanism. ROOT CAUSE -- a silent stub. Mech__DamageZone::RecurseSegmentTable @0049cad4 was transcribed faithfully... against three local shim types whose iterators unconditionally return NULL: struct SegmentIterator { DZRef *Next() { return 0; } }; struct SegTableX { SegmentRecord *operator[](int) { return 0; } }; The cascade fired on zone death (bench: 72 [cascade] lines), "descended", and touched nothing. Every line read correct; none of it did anything. THE REAL WALK (decomp re-read, FUN_0049cad4): iterate the mech's segment table (mech+0x300, vtbl+0x34 GetNth) to this zone's segment, then destroySiblingsOnDestruction (Wword 0x68): recurse every other zone on the SAME segment (seg+0xD0), setting graphic state 2 (Gone) descendOnDestruction (Wword 0x67): recurse every zone on every CHILD segment (seg+0xE8 -> child+0xD0) -- the arm takes the gun The engine ALREADY HAS both structures: EntitySegment::damageZoneTable @0xD0 / childIndexTable @0xE8 (TableOf is 0x18 -- byte-identical to the decomp offsets), populated at stream time by JMOVER.cpp:373 [T0]. The stub was never necessary. Implemented with the engine iterators via two new read accessors on EntitySegment (SEGMENT.h); the binary's asymmetry is reproduced as-is (the sibling loop checks graphic state != 1, the child loop does not; re-hits on a 1.0 zone re-run the cascade -- idempotent in effect). VERIFIED (ava1, self-damage to one zone, autofire everything): dz_rarm -> [cascade] zone 9 -> zone 17; AFC100 + AmmoBinAFC100 + Condenser6 ForceCriticalFailure'd; "[ammo] AFC100 -> NoAmmo (gate1): destroyed=1"; torso LRM5/SRM2 keep firing (correct) dz_larm -> [cascade] zone 2 -> zone 6; PPC + Condenser4 force-failed; "[emitter] 'PPC' fire REFUSED (destroyed=1)" x4998 -- the emitter gate now logs refusals BY NAME (the FIRED line never named its weapon, which is how #86's verification gap survived) regression smoke: frame time 7.13/7.20ms, 0 asserts, 0 cascades in ordinary combat (they fire only on genuine zone deaths) Authored data (now dumped under BT_DMG_LOG): descend=1 on exactly the four arm zones (ava1: 2/6/9/17), destroySibs=0 everywhere -- so legs/torso behavior is untouched by this change. KB: combat-damage.md cascade section; reconstruction-gotchas #26 (the silent-stub trap: a shim that returns EMPTY converts a reconstruction into fiction that reads correct -- shims must Fail() loudly or log their emptiness). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3354db4bd1 |
#32: RETRACT the retention diagnosis; count what matters (steals + true drops); name the saturating class
The reopen said "the pool fills and never returns a source". The 30s census in
the SAME field logs disproves it: free returns to ~227-230 between bursts and
reuses climbs ~20/s all session. free=0 on the failure line is true by
DEFINITION at the instant of a failed acquire -- the third instance of the
counter-sampling trap (live=256-vs-6, then hsparm greps, now this), read off the
alarm line instead of the trend line.
WHAT THE LOGS ACTUALLY SHOW
* The pool cycles; release-on-stop exists and works (the engine steal loop).
* During firefights CONCURRENT demand exceeds 240 and the priority steal loop
services each new sound by killing an old one -- continuously through
combat (failures spread across every decile of every combat session).
* Idle standing demand is ~13 sources. My "each component holds its SourceSet
to entity teardown, ~20-25 per mech" narrative was wrong.
* The raw ACQUIRE FAILED line count (6.8k-19k per log) is NOISE: the steal
loop retries after every failed attempt, so lines accumulate per EVENT and
most events still play via a steal. True drops were never counted.
CHANGES
1. Census now carries steals= and drops= (drops = the steal loop ran dry and
the sound NEVER played) plus a per-class drop histogram
("[audio] dropped by class: {class 1005 x4v: N} ...") -- all ungated, so
the next field logs are decisive instead of suggestive.
2. The ACQUIRE FAILED print is rate-limited to 1/30s and now names the
requesting class + voice count. 19k-line log spam distorted this triage.
3. BT_AUDIO_SOURCES=<n> now raises the POOL cap too (it previously raised the
AL context budget while the pool stayed at 240, making the field
experiment impossible to run).
MEASURED (9 mechs, missile autofire, 150s)
* cap 240: peak 130 sources, 0 fails -- demand tracks SHOOTER count, not mech
count; one shooter cannot saturate. A 6-shooter lobby pins 240.
* cap 48 (BT_AUDIO_SOURCES=64): saturation reproduced -- census
steals=223 drops=455, histogram names the classes.
* Dominant field requester (requested=4) = class 1005 Static3DPatchSource:
world-placed effect sounds, i.e. EXPLOSIONS. 1001 DirectPatchSource x1v
dominates drops at low cap; 1002 Dynamic3DPatchSource x3v present.
CONSEQUENCE FOR THE FIX ORDER: #84's stale-aim double detonation duplicates
exactly the saturating class on observer nodes. Fix #84 FIRST, then re-read the
field census; only if it still saturates does the budget experiment
(BT_AUDIO_SOURCES with frame time measured) become the play.
KB: the wrong night-9 entry in open-questions.md replaced with the corrected
diagnosis; gotcha candidate noted -- an alarm-line counter is not a trend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5a1a407ed1 |
pre-ship: resolve the new audio diagnostic gates ONCE instead of per frame
RunSequence and AudioIdleWatcher::Execute run per sequence per frame, so the BT_ATTRBIND_LOG probes added while chasing #99 were calling getenv ~2700x/sec on a 15-sequence mech. Small, but this is the same path the source-pooling fix (#32) just bought 10% of frame time in, and an unset diagnostic should cost nothing. Cached in function-static flags. Pre-ship smoke (combat, 2 kills, a respawn, clean teardown): frame time 7.18 / 8.27 / 8.71 ms -- matches the post-pooling 7.79ms baseline 0 asserts, 0 access violations, 0 audio acquireFails Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7c08e8bd03 |
#99: commit the missed audseq.h probe member (case-mismatched path in the previous commit)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e058b6acb7 |
#99: trace the coolant-leak alarm end to end -- it IS implemented; found the real defect
Oracle: "alarm is not sounding despite an active leak" ... "was sounding,
resolved a leak, but there was another".
RETRACTION FIRST. I previously told the user the coolant alarm was "genuinely
unbuilt -- there is no alarm implementation to starve". That was wrong, and it
was asserted without checking. The alarm is fully implemented and the entire
chain works. Traced live, in order:
Condenser6.ReportLeak (= HeatSink::coolantActive) changes 0 -> 1
-> [watchpoll] CHANGE on that exact address
-> [matchfire] val=1 -> ctl 1 (StartAudioControlID)
-> [seqstart] the alarm AudioControlSequence, events=25 looped=1
-> [seqsend] ctl 8/16 (select), 6/1 (volume), 1/0 (START), 2/0 (STOP), ...
a three-part looping alarm: two chirps then an 8s sustained tone.
The authored design, read from BTL4.RES: 19 subsystems per mech each bind TWO
AudioLogicalTriggers to their ReportLeak flag -- match 1 -> Start, match 0 ->
Stop -- and ALL of them drive ONE shared alarm sequence.
SO WHY WAS IT SILENT IN .674? Almost certainly the audio source pool, fixed
after that build in
|
||
|
|
bb7a6abe70 |
#32: correct the stale "no field complaints since" note + record the frame-time A/B
Found while checking whether a player had reported this before: the code already
carried a prior analysis of this exact error which deliberately decided NOT to
act on it, concluding the 2026-07-23 atomic-delete fix (
|
||
|
|
ad9dfade88 |
#32: POOL the OpenAL sources instead of creating and destroying one per sound
Every 4.11.674 player log is saturated with acquisition failures -- 3031, 4657, 5245, 6275, 6571 across five machines. In Lynx's largest session the first failure lands 9.3% in and they continue to 96.8%: once it starts it never recovers for the rest of the match. CAUSE: RequestAudioChannels called alGenSources() per sound event and ReleaseSourceSet called alDeleteSources() on release -- create and destroy per sound. OpenAL sources are a scarce driver resource (OpenAL Soft caps a context at 256) and a combat burst churned straight through the ceiling. The two counters looked contradictory and were the tell: ACQUIRE FAILED always printed live=256 while the 30s census printed live=6. Same global, sampled at different moments -- sources spike to the cap during a burst and drain back between them. Churn, not a steady leak. FIX: generate sources once, up to a cap, and recycle them through a free list. Release scrubs and parks instead of deleting. Steady-state play performs no AL allocation at all. The scrub is load-bearing, not hygiene: a recycled source carries whatever the previous owner set, and the engine sets AL_LOOPING per sound (L4AUDLVL.cpp:327). Hand a looping source to a one-shot and it plays forever -- which is the "sound stuck looping" family (#51, #5). Every reusable property is reset at the single point where a source changes owner. VERIFIED (scratchpad/night8/audiopool.sh + the two-node mp_burst.sh): solo, sustained fire : 0 failures, pooled 152, reuses 21998 two nodes, 4 min : 0 failures on both, pooled 148/149, reuses ~7000 each ⚠ HONEST LIMIT: the bench does NOT reproduce the field failure -- the PRE-fix binary also scores 0 on it (peak 49 live), because solo/2-node combat is not dense enough to reach 256. So this verifies the pool works and allocates nothing in steady state; it does NOT by itself prove the field failures are gone. The five field logs remain the "before". Peak demand is set by how many audio COMPONENTS are alive (each reserves a SourceSet of up to 25 voices and holds them), not by audible sounds: measured high-water 138 solo, 149 two-node. That scales with player count, so the cap is set near the driver ceiling (240) and the pool now logs its high-water mark once per 25-source band -- so the next playtest sizes this from field data instead of a guess. Growth also self-limits: if a driver offers fewer sources than the cap, alGenSources simply fails, growth stops, and the pool recycles what it has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
137c151951 |
#87: mech armour PANELS now darken with damage -- the .DZM material system was loaded and unused
Players: "the actual enemy mech in external view is not showing darkened armor panels". We swapped destroyed LIMB meshes but never darkened a panel. The 1995 game darkens armour through the MATERIALS. MakeMechRenderables (FUN_004cef28) built, per (damage zone, material), a watcher FUN_004573e4(material, &zone->damageLevel, 0.1f) that snapshotted the materials |
||
|
|
ac7c46b87a |
#93: fix the end-of-round PerformAndWatch crash -- the STATIC projectile
pool held dangling entity pointers across round teardown Root cause (disasm-pinned on the shipped 4.11.659 exe): RajelAran's crash (call to 0x65676769 = ASCII 'igge', EBP-walk return btl4+0x2b91a) is the tgt->Dispatch vtable call in BTUpdateProjectiles' NON-MECH impact branch (mech4.cpp:1598; the site matches the disasm literally -- the 0x12/0x64 TakeDamageMessage ctor, the EntityID::Null ternary, the getenv gate after). The trap: the mech branch is guarded by BTIsRegisteredMech(tgt) -- but at round teardown a destroyed mech is DEREGISTERED, so a round still in flight (the pool is a static array that outlives the round; missiles are slow, #84) holding it as p.target now FAILS the mech check and falls into the !BTIsRegisteredMech branch, which treats the freed mech as a cultural icon and dispatches into freed memory. The liveness check itself routed the dangling pointer into the unguarded branch. Freed heap reused by a string -> vtable slot +0x10 read 'igge' -> call 0x65676769. (The EBP walker explains the stack shape: the faulting call pushed its return address on ESP, but the walk reads [EBP+4] = PerformAndWatch's frame.) Fix -- scrub at the source of truth: - BTProjectilesDropEntity(e): every pool entry drops a dying entity from p.target/p.shooter; called from ~Mech and ~CulturalIcon (the only free paths for targetable entities). Flight + impact code is already null-guarded on both fields. - BTProjectilesClearAll(): kills all rounds at RunMissions exit (cross- mission hygiene for the static pool). Verified: 6 short-mission cycles (45s missions with a spawned dummy + autofire, mission expiring mid-combat) -- 0 exceptions, 6/6 clean 'RunMissions returned'. The original crash was a heap-reuse race with no deterministic repro; the scrub eliminates the dangling-pointer class by construction. (Bench note: blind autofire never launches missiles -- the launcher needs a target lock -- so the exact in-flight race was not re-created; the non-regression + the pinned mechanism carry the verdict.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
494f32efb4 |
#45 ROOT CAUSE + FIX: the gauge executive starves in MP -- panels froze at healthy fps
Instruments repaint via the background task pump, which authentically runs
ONLY in the frame's leftover time with a floor of ONE pump per frame; the
gauge renderer is 1 of ~7 round-robin tasks and each turn advances ONE gauge
of ~140 active, with the rate mask advancing once per full sweep. On a busy
MP mission the foreground eats the whole frame budget, the floor becomes the
norm, and the whole instrument stack rotates once in MINUTES at perfect fps:
comms-panel K/D stuck at 0, recharge tickers frozen, while the tallies and
their replication underneath were exactly right. Measured: 4-node bench at
70-90 fps gave the PilotList ~2 Execute turns in six minutes.
Fix, both env-tunable, authentic behavior restorable:
BT_BG_MIN (APPMGR.cpp, default 32, 0=authentic) minimum background
pumps per frame regardless of slack;
BT_GAUGE_BATCH (GAUGREND.cpp, default 32, 1=authentic) gauges advanced per
gauge-renderer turn (a visit is only a rate-mask check
unless the gauge is due).
Verified 4-node self-damage bench: sweeps 0.006/s -> 18-20/s, PilotList ~10
Exec/s, ALL FOUR panels tracked every death live (0->11) within ~1s, bg cost
2-4 ms/frame, respawn ledger clean (40 cycles, 0 swallow / 0 mismatch).
Also: BT_PERF now reports gaugeTurns/sweeps/active alongside bgTasks;
BT_AF_PERIOD now throttles the missile autofire group too (unthrottled spam
trips the documented FailureHeat all-weapons brick, which froze run 1 of the
cross-fire bench).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bf0555ccf1 |
#81: every MP respawn is a cross-machine round trip -- one degraded peer ghosts everyone
Two-node bench with the fixed host:local labels shows each player's drop-zone
request is answered by the OTHER machine's DropZone: A asks, B grants; B asks, A
grants. FindGroup("DropZones") iterates replicants of remotely-mastered zones
too and takes the geometrically closest, so a respawn is
my request -> an arbitrary peer's DropZone -> that peer's reply -> back to me.
With 5 players that is 5 round trips through arbitrary peers, and ONE degraded
peer can strand everybody else's respawn. That finally explains the otherwise
unexplained field datum that one machine stopped processing the death-transition
stream 57% into a match and never recovered (0 explosions/wreck swaps/burials
while four other machines logged 4/4/4) -- a node in that state cannot answer
anyone's respawn. It also explains why solo is 100% reliable (in-process) and
why a healthy 2-node bench passes.
Also fixes the instrumentation before it costs a night: every [dz] line printed
"entity 1" because a player's LOCAL entity id is 1 on every machine -- with five
players the log would have said a respawn stalled but not WHOSE. All [dz] lines
now print host:local (including the usedBy= owner of each busy slot).
Doc: two candidate fixes recorded (prefer a locally-mastered DropZone / make the
reply path tolerant of a deathCount that is ahead of ours), neither to be guessed
at -- the [ghost] DISCARDED line's mismatch direction decides it in one line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ba6756c40a |
drop-zone / respawn instrumentation: make the GHOST MECH visible in field logs
The ghost (a player who dies, never respawns, stays dead-but-driveable and is a
wreck on every peer) traces to a respawn stalling on drop-zone acquisition:
DropZone::AssignDropZoneMessageHandler grants a slot only if IsAvailable(), and
when none is free it reposts to ITSELF every 0.1s at MaxEventPriority and never
replies -- so deathPending is never cleared. This whole subsystem was DARK: not
one drop-zone line in a full night of field logs.
ALWAYS-ON (no env gate -- rare, catastrophic, field-only, same rationale as the
#57/#59 guards; volume bounded and rate-limited):
[dz] POOL -- slots + downTime + proximity radius, once per mission
[dz] GRANTED-- one per respawn, with the WAIT time; tags (GHOST RECOVERED)
when a long-stalled request finally lands (what testers see as
"it fixed itself")
[dz] STALL / GHOST LIKELY -- escalating, per-waiter rate-limited, and it names
WHY each slot is busy (age + last user), which discriminates
cooldown saturation from proximity blocking -- different fixes
[dzreq] -- the REQUESTER half: re-try count for this death, and msgDeath vs
our own deathCount. The engine gates the hunt on those being
equal AND the DropZone resend net is keyed on the same value, so
a mismatch breaks both silently (cf #45, tally replication).
BT_DROPZONE_LOG=1 adds the verbose per-request slot dump for bench work.
First run already overturned a theory: slots=8, so an 8-slot pool cannot be
cooldown-starved by 5 players (max 5 on cooldown at once) -- the shared
dropzone=one in eggmodel.py is NOT the bottleneck. Suspicion moves to the
handshake (lost reply / deathCount mismatch).
Verified with NO env vars set, exactly as a tester runs it: 4 probe lines for a
full death+respawn cycle, all in the normal day log.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e1c15eb806 |
#82 peers see a limping mech SKATING: the mech erased its own gimp cell every frame
The binary's one mech+0x40 IS Simulation::simulationState, which rides EVERY
update record header -- so in 1995 a peer's replicant learned the gimp level on
every packet and its gait limped with no gimp-specific replication anywhere.
The port's Mech::PerformAndWatch wrote SetMovementMode(1) every frame ("ground,
non-death, non-airborne"), which erased the level once it was mirrored in for
the warning voice (#78): the wire carried 1, bystanders walked while sliding at
limp speed, and the voice sequence restarted on every damage event (1->4 edge
per tick) instead of announcing once.
- mech4: that per-frame write now writes the AUTHORITATIVE level
(gimped ? 3/4 : 1) via a new alarm-only bridge BTMechGimpAlarmLevel -- which
deliberately never consults the cell it feeds, so a respawn-cleared alarm
cannot re-latch stale gimp out of it.
- BTMechGimpLevel: falls back to the replicated cell, with ONE-CELL precedence
(a fall/death/limbo state wins, so the normal drivers and their death latch
run -- what the binary's single cell enforced structurally).
- Reverted the #78 record guard: on a replicant the master's records are
authoritative, so pinning 3/4 against them would keep a peer limping through
a respawn. Fixed at the writer instead.
- [simstomp] trap now scoped to the watched mech with a module-relative return
address (symcrash-able) -- that is what named the writer.
- Also learned + recorded: zone damage levels replicate only when the EXPLOSION
TABLE's tier is crossed (peer measured at 0.428 vs master 0.857), so peer-side
damage state must never be inferred from them.
Verified two-node (scratchpad/night6/mp_skate.sh): the observer's replicant gets
sim=4 and its gait runs 23 -> 25 (wgr entry -> ggl limp cycle). Field clue that
cracked it: "after respawning a peer DID see the limp" (epilectrik/SAURON).
KB: locomotion.md + gotcha #25.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1ac425860e |
particles.png recovered (#79, found by cyd) -- particle texture ships at last
content/VIDEO/particles.png (128x128 RGBA): the texture L4PARTICLES has tried to load since the engine was written. Boot-verified: [particles] device objects created (max=8192, texture=loaded). Every particle effect now draws textured -- the authored look; the arcade pods displayed untextured quads because the file never shipped. Comments updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cfa52a28d8 | Merge remote-tracking branch 'origin/glass-cockpit-refit' | ||
|
|
1671b7d4b2 |
the REVERSE DISABLED voice (#78): SimulationState 3/4 IS the trigger -- mirror the gimp level into the engine cell
User + old-timers were right; the earlier "no voice exists" verdict was wrong. The authored mech audio has state watchers on Entity.SimulationState==3/4 (the binary's one-cell mech+0x40) that start sequence notes 29,16,40 -- two klaxon hits then Warnings01 zone 8 (key 40-41) = the "reverse disabled" voice line the testers remember. The port's cell split (graphicAlarm vs engine simulationState, gotcha #23) meant the trigger value never arrived. - mechdmg: mirror gimp 3/4 into SetSimulationState at the leg-half crossing (guarded: never stomps disabled/fall/dead states). Voice verified playing end-to-end on the bench: SetupPatch bank2 patch113 note=40 -> Warnings01_z7.wav. - mech.cpp: BT_GIMP_SAFE_BASE_READ on all seven Simulation::ReadUpdateRecord sites -- gimp is monotonic per life; a record captured pre-gimp must not stomp the cell (the loopback otherwise perpetuates the stale value and restarts the warning on every damage event). - diagnostics (all env-gated): [statefire]/[startreq]/[animind]/[gimp-sim]/ [simstomp]/[indstomp] + StateIndicator::DebugAudioWatcherCount + raised spatial-log caps. These traced the whole chain and PROVED no SetState path stomps the cell. OPEN (follow-up): a RAW writer (bypasses SetState entirely; invisible to the indicator-level trap) resets the cell between damage events -- under the bench's 1 Hz metronome harness it restarted the sequence before the 1.8 s voice note; sporadic real-play damage is unaffected (one edge -> full sequence). Needs a cdb write-watchpoint session; candidates: a recon raw +0x2c-equivalent write or a struct copy spanning it. Also decoded en route: the AnimationState triggers on the limp states play EngineShiftRev01 (the downshift foley) -- working, and NOT the voice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ed844456af |
audio: gated [statefire] diagnostic on AudioStateTrigger (BT_AUDIO_SPATIAL)
Logs animation-range state-trigger fires (trigState>=5, capped 60). Used to prove the #78 limp audio end-to-end: entering the gimp states fires the authored strained-servo component (statefire trigState=23 old=7 new=23, ctl Start) -- the 1995 wounded-leg sound, reachable for the first time now that the limp gait lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aa788bc56d |
noframe: drop the drag-by-surface -- it is a finishing step, not a mode
Author's correction to the shape of yesterday's option. The intended workflow is: place the displays where you want them WITH their frames, quit, then edit glass_layout.cfg to turn the frame off. So a ,noframe window should be pinned, not draggable -- removed the WM_NCHITTEST -> HTCAPTION handler that made the surface a drag handle. The arrangement being finished is the point: with no caption there is nothing to drag it by, which is exactly what you want on a wall of monitors. Delete the flag to get the frame (and the dragging) back. Unchanged and still needed: SaveLayout writes the flag back, because any later drag of a FRAMED window rewrites the whole file and would otherwise strip it. Re-verified live: - Heat MFD comes up with WS_CAPTION clear and answers WM_NCHITTEST on its surface with HTCLIENT (pinned) -- no window reports HTCAPTION any more. - Comm MFD (unflagged) keeps its caption. - A framed window's WM_EXITSIZEMOVE rewrote the file and BOTH ,noframe flags survived, with the new header explaining the arrange-first workflow. - surround / exploded / pod / dev boot and simulate clean. Docs updated to describe the two-stage workflow rather than surface dragging: the file's own header, environ.ini's BT_GLASS_LAYOUT block, context/ glass-cockpit.md and the ledger (which records the removed behaviour so the next reader does not re-add it). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f5f4c6c198 |
glass_layout.cfg: ",noframe" per-line option strips a window's title bar
Append it to a window's line and that window becomes a bare WS_POPUP -- no
caption, no border, just the display and its buttons -- for a multi-monitor
wall where the chrome is only noise:
Heat MFD=1920,0,657,539,noframe
Options are comma-separated after the four numbers and unknown ones are
ignored, so an older build reading a newer file loses the option but never the
line (the bindings.txt grammar rule, applied here).
TWO THINGS THAT WOULD HAVE MADE IT A TRAP, both handled:
- A frameless window has NO TITLE BAR TO DRAG, which is the entire point of
the sticky layout. So a noframe window is dragged BY ITS SURFACE:
WM_NCHITTEST returns HTCAPTION anywhere that is not a button, HTCLIENT over
one. Buttons stay clickable, and because Windows drives the move the drag
still ends in WM_EXITSIZEMOVE -- so it still saves.
- SaveLayout rewrites the WHOLE file, so it writes the flag back. Without
that, the first finished-drag after adding the option would have silently
stripped it.
Ordering: the flag is read BEFORE frame sizing (a quiet LoadLayout pre-pass,
then the normal pass after ComputeLayout), because AdjustWindowRect -- and
therefore the ring placement -- depends on whether a window carries chrome.
The pre-pass is quiet so the restore still logs exactly once.
Verified live: two flagged windows came up with WS_CAPTION clear while the
other five kept it (read back with GetWindowLong(GWL_STYLE)); a click still
dispatched on the frameless radar (CLICK 'Secondary / Radar' addr=0x18); and a
WM_EXITSIZEMOVE save round-tripped both flags back into the rewritten file.
surround / exploded / layout=off / pod / dev all boot and simulate clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
dca2586aa8 |
the Owens crash: a device reset that never waited for the device (#35)
Eight byte-identical field stacks from night 6, all one player, all in an
Owens: ParticleEngine::Destroy +0x11, access=0 target=0x0, from the plain
per-frame render path. Nothing in the stack touches weapons or the Owens.
Conn Man's Surface Pro 9 (Iris Xe, 128 MB shared) is simply the only GPU in
the fleet that ever actually LOSES the D3D9 device -- his two-trigger
missile+laser bursts are what provoke the timeout, not what crashes.
What crashed is our device-loss handling, which was wrong three ways at once,
in two inline copies (the scene Present and the wait-screen Present):
1. On D3DERR_DEVICELOST it called Reset() IMMEDIATELY. Reset on a
still-lost device ALWAYS fails, and V() only logs. There was no
TestCooperativeLevel gate at all.
2. It then ran ParticleEngine::Initialize against the lost device. The
creates fail there and NULL their out-params -- proven, not assumed:
the bench repro faults at target=0x0, not at a dangling address.
3. The next lost frame called ParticleEngine::Destroy again, which
Release()d those NULLs blind. Read of vtable at 0x0. Dead.
So: lost frame 1 tears down and leaves NULLs, lost frame 2 crashes. Two
frames, every time, deterministic -- which is exactly why all 8 field stacks
are byte-identical.
Reproduced before fixing. BT_DEVICELOST_TEST=<frame>,crashrepro runs the
field sequence on the bench; on the unfixed build it died at Destroy +0x11,
access=0 target=0x0, and symbolized to the same four frames as the field
logs. Same shape, same offsets-modulo-hook. That run also proved the
out-param-nulling assumption the whole diagnosis rested on.
The fix -- one shared DPLRenderer::BTResetLostDevice() replacing both inline
copies:
- Destroy() is idempotent and null-safe, and nulls after release.
- Reset() is gated on TestCooperativeLevel() != D3DERR_DEVICELOST; while
the driver still says lost, skip the frame and retry.
- The Reset HRESULT is checked; on failure, log and retry next frame
instead of driving on.
- On success, re-create via the new CreateDeviceObjects(), NOT
Initialize(): Initialize memsets the installed-effects table, so every
reset that DID succeed silently killed all particle effects for the rest
of the mission. The quieter sibling bug, fixed by the same split.
- Initialize checks its HRESULTs and defends MAXPARTICLES<=0; the draw
paths guard the NULL buffer, and ExecuteParticles keeps draining
particles while the engine is dormant so they cannot pile up.
Verified: the crashrepro shape now logs SURVIVED and play continues; three
forced full loss/reset cycles each log "[render] device reset OK"; a plain
run is assert-free.
Found while verifying, worth its own line: VIDEO\particles.png has NEVER
existed -- not in the tree, not in BTL4.RES, not anywhere in git history.
The texture load has failed on every machine since the engine was written,
and every billboard particle ever rendered was untextured quads via
SetTexture(0, NULL). RenderParticles deliberately does NOT gate on the
texture -- that would disable all particles everywhere; untextured IS the
shipped look. Filed separately; a real particle sheet is a content task.
The field verification that counts is Conn Man flying his exact crash
loadout on this build: instead of a dead process he should see at worst a
brief hitch and "[render] device reset OK" in his log. #35 stays open until
that happens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
29c502df83 |
Glass windows remember where you drag them (BT_GLASS_LAYOUT)
The BT_GLASS_PANELS windows self-place in a pod-faithful ring and re-snap
once the main window is up. They carry WS_CAPTION so a dev can drag them,
but the drag never survived the menu->mission->menu relaunch loop.
BT_GLASS_LAYOUT (L4GLASSWIN.cpp) adds opt-in persistence to a cwd-relative
glass_layout.cfg beside bindings.txt (gitignored):
off/0/unset computed ring only, no file I/O (default, pod-faithful)
load restore saved positions on startup (per-window fallback
to computed); never writes
save/adjust restore first, then rewrite on each finished drag
(WM_EXITSIZEMOVE) and on teardown -- the round trip
One "<title>=x,y,w,h" line per window; position restored, size ignored
(frame size is deterministic from content, so an old w,h can't distort a
later geometry change). A restored window is flagged so ComputeLayout's
post-main-window re-snap leaves hand-placed windows alone. Native
analogue of TeslaRel410 pod-launch's per-rig --bridge-pos/--layout args.
Verified [T2 runtime round-trip]: load restored 7 from a seeded cfg;
save wrote all 7 with a window moved to 777,333 (GetWindowRect read the
live window); relaunch in load put that window physically at 777,333
after the re-snap fired. Release links clean (only the 40 tolerated
/FORCE externals).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
8284b8d9a7 |
Glass panels: MFD phosphor green (21FF42) + respect BT_COCKPIT_TINT
The BT_GLASS_PANELS=1 per-display windows tinted the mono MFD surfaces pure WHITE (MfdMonoTint 0x00FFFFFF) -- the cockpit surround already used phosphor green via BT_COCKPIT_TINT (L4VB16 CkTint). New GlassMfdTint(): default the standard green rgb(33,255,66)=0x0021FF42, overridable by the SAME BT_COCKPIT_TINT=RRGGBB env the surround honours (used verbatim as the 0x00RRGGBB ExpandPlaneToBGRA tint -- no R5G6B5 packing, which is the surround's D3D path). Pixel-verified live (BT_GLASS_PANELS=1): all lit interior MFD pixels R33 G255 B66 = 21FF42 by default (was white); BT_COCKPIT_TINT=FF8000 -> R255 G128 B0. Radar keeps its amber palette (monoTint -1, unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1af2dfc769 |
back the audio cap off to opt-in: 7-23 already fixed the complaint, and the cap is a governor
Correcting my own framing in |
||
|
|
7e57816d39 |
audio cut out in firefights because nobody ever asked OpenAL for more than 256 voices
Field report from 2026-07-23 was "audio cutting in and out toward the end of the
match". The night-5 logs say it precisely: 2017 ACQUIRE FAILED lines in a single
match, and every last one of them at live=256.
It is not a leak, which is where I started and where the code comments still
pointed. The source census sits at a healthy 45-60 live, spikes during
firefights, and drains right back down afterwards -- 226 back to 42 in one
window -- so the engine's priority steal loop is doing exactly what it should:
AudioSourceStop/SuspendMaintenance -> ReleaseChannels -> ReleaseSourceSet ->
alDeleteSources, with the live counter following it down. The 2026-07-23 fix
(
|
||
|
|
ef8e449a17 |
#55 steps 0/1/2/4: the 1995 DEATH pass restored, the arg gate made real, and 5 subsystems the respawn sweep never reached
THE HEADLINE: the port's death sweep was a total no-op, and fixing it required
fixing the arg gate in the same commit -- otherwise corpses refill their ammo.
STEP 2 -- the authentic dispatch shape:
* engine/MUNGA/SUBSYSTM.h: Subsystem::DeathShutdown gains the binary's
universal base body { DeathReset(c) } (@004ad10e). It was empty, and NO
port class overrode it, so everything the 1995 game does at death was
skipped entirely.
* mech4.cpp death sweep now passes 0, not 1 (the binary's @0049fe0c arg) --
the wreck shape: alarms/state settle, nothing refills.
* ARG PROPAGATION (mandatory once the sweep runs): AmmoBin::DeathReset is now
arg-gated exactly as @004bd26c -- refill only when arg != 0, ammoAlarm
unconditional. Ignoring the arg was harmless only while the sweep was
dead; with it restored, every corpse would have re-armed. Also forwarded
in PoweredSubsystem / HeatSink / Condenser / HeatableSubsystem / Generator
(each binary body forwards it -- verified addresses in the plan).
STEP 4 -- coverage for 5 classes that had an authentic slot-10 body but no
DeathReset, so the respawn sweep fell through to the empty base:
Torso (this is Gitea #70 -- "loosing torso twist function after a death"),
Gyroscope, Myomers, HUD, Seeker.
STEPS 0/1 -- observability + the David chain:
* Three unconditional matchlog rows: DEAD_NOTIFY (mech + resolved link),
PLAYER_LINK (player <-> vehicle), RESPAWN (mode/alive/zones/subsys/pos).
A respawn was previously INVISIBLE in the matchlog -- night 3's analysis
had to infer them from ammo arithmetic.
* Mech::PlayerLinkMessageHandler + the death dispatch site: when the engine's
one-shot registry lookup misses (no null check, no retry -> the whole
death/respawn cycle silently swallowed), recover the SAME object via the
reverse link the binary's own respawn branch walks (player+0x1FC ==
playerVehicle). Complete-type TU, no raw offsets.
* deathPending cleared in the first-spawn branch (a latch carried in would
permanently kill every later respawn -- the #57 class).
VERIFIED on the 2-pod rig (madcat vs thor, forced kills, 5 death/respawn cycles
per pod): build clean, ZERO new /FORCE unresolved externs from the 5 new
overrides; refill lines appear ONLY immediately before a Mech::Reset and NEVER
between a death and the next respawn (the corpse-refill regression this commit
had to pre-empt); all three probe rows present in both pods' matchlogs; every
DEAD_NOTIFY carried a non-null link.
DELIBERATELY DEFERRED (documented, not forgotten): the mechsub.cpp rename pair
(ResetToInitialState -> GenerateFault, ClearStatus -> the root reset @004ac22c),
deleting HeatableSubsystem::ResetToInitialState, and deleting RespawnRepair.
Those need the HeatableSubsystem vtable-slot-10 pre-flight the plan calls for,
and we have no binary image here to dump the vtable from -- guessing at it
risks the vptr-alias trap. Next session with the decomp shards open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e28dcbc161 |
the #35-class sweep + the games-night load crash: four lifecycle-window null-derefs guarded
THE CLASS (established by Gitea #35's 515 fix, confirmed growing tonight): code the binary could run bare because a pod's mission world always existed, crashing in the port's MP join/teardown/respawn windows where mission, player, or a roster link is briefly NULL. Swept every GetMissionPlayer()/ GetPlayerVehicle()/GetCurrentMission() chain in the tree (23+7 sites). GUARDED (the 515 pattern -- authentic behavior whenever the object exists, skip/degrade + loud log when mid-teardown): 1. DPLRenderer::SortAndReloadNameBitmaps -- runs in the round-STOP path (the end-of-round circle), raw mission+player+entity-manager derefs. 2. DPLRenderer::LoadOrdinalBitmaps -- called from (1); raw GetMissionPlayer()->GetInstance() evaluated on EVERY machine (the camera- director test derefs before it branches). 3. DPLRenderer::LoadNameBitmaps -- same window, raw GetPlayerCount() chain. 4. Reservoir::Reservoir master-gate block -- THE CAPTURED GAMES-NIGHT CRASH ([crash] btl4+0x4990d = this ctor inlined into CreateReservoirSubsystem, AV reading NULL+0x1d0): linkedSinks.Resolve() derefed raw; +0x1d0 is exactly the masterScale FILD of the resolved master heat-sink bank (heatSinkCount @0x1D0 -- the offset the crash named). Observed trigger: a DUPLICATE-PILOT egg (the seat-ghost bug put one identity in two seats) poisoning the ID registry, so Resolve() nulled during viewpoint-entity construction and at least two pods died loading the same egg. Now: loud '[spawn] FATAL-AVOIDED' + degrade to the inactive-copy shape. The duplicate-pilot ROOT CAUSE is the seat-ghost fix (tracked separately); this guard makes the failure survivable and self-identifying either way. CLASSIFIED SAFE, not touched: btl4mppr/btplayer/mechmppr/btl4gau3 chains (guarded or null-tolerant by design); APP.cpp's five Check(player) sites (mission state machine -- player exists by construction, original engine ordering); CAMMGR/CULTURAL GetCurrentMission chains (construction-time). VERIFIED: build clean (0 errors, no new unresolved externs); solo smoke on the guarded exe -- zero FATAL-AVOIDED lines (guards silent on the healthy path), subsystems simulating, no faults. STILL OWED: a full launch->stop rig cycle (exercises the guarded round-stop path live) and the duplicate-pilot-egg repro -- both blocked on port 1500 while the operator's console session is up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
12d6862c1f |
keylight: unship it -- opt-in (BT_KEYLIGHT=1), gone from all player docs
Operator decision: the player base is not on Windows 11, so the RGB keyboard
mirror should not be shipped or promised. The feature is NOT deleted -- Cyd's
implementation stays intact behind the gate for whoever wants it -- it is
simply off by default and invisible:
* PadRIO gate flips from opt-out (BT_KEYLIGHT=0 disables) to OPT-IN
(BT_KEYLIGHT=1 enables). Default boots produce zero keylight lines, and a
stub build and a real build now behave identically for players: nothing.
* Removed from every user-facing artifact: the README paragraph, the
CONTROLS.md section, the CONTROLS.html footer line.
* environ.ini template: the entry stays (it is the discoverable home for the
opt-in) but now says OFF-unless-1, Windows 11 22H2+, and why it is
unshipped.
* mkdist's cut-time stub warning removed -- it existed to protect a README
promise that no longer exists.
* context/glass-cockpit.md records the decision, the opt-in, and the
build-time SDK stub gate in one place.
VERIFIED: default boot logs zero keylight lines; BT_KEYLIGHT=1 engages the
feature (the stub on this machine: its one honest log line + the key map).
All three console suites pass; checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8edce41f88 |
bindings: one-time migration to the board-2 layout -- the two-population fork is gone
Operator decision: "make this version devour their custom settings and re-output
the correct binding file with the updates -- I'd like to eliminate competing
code paths." Before this, the preserved-bindings.txt convention meant upgraders
stayed frozen on whatever layout their file was written under, forever: two
player populations, two sets of true documentation, and every future default
improvement reaching only fresh installs.
THE MIGRATION (PadBindingProfile::MigrateBindingsFile, run once at load):
* A file carrying the new "# bindings-board 2" marker is left alone -- the
check is the first thing that runs, so this is a no-op forever after.
* Otherwise: every row matching the UNION OF EVERY DEFAULT SET EVER SHIPPED
(85 canonical rows, harvested from c2ee729..1cda880 and embedded as a
table; comments stripped, whitespace collapsed, uppercased) is dropped --
those rows were never a player's choice, they were just the old board.
* Rows the player actually authored are CARRIED with their original text and
comments, and WIN over the new defaults: the losing default row is emitted
commented out as "# (yours wins) ...", keyed by the control ("KEY T",
"PAD A", "PADAXIS LX") so key rows only displace key rows.
* The joystick wizard's marker-delimited section is preserved VERBATIM (the
same markers L4JOY rewrites between), so HOTAS players migrate without
re-running the wizard.
* The previous file is saved as bindings.old.txt first. Restoring it over
bindings.txt just re-migrates next boot -- deliberate: the old WORLD is not
restorable, only the player's own rows persist. That is the point.
VERIFIED end-to-end with the real exe (three planted scenarios):
A. old-world file (8 old-default rows + 2 authored rows incl. a rate tweak +
a wizard section): both authored rows carried with comments, both
conflicting new defaults stood down with "(yours wins)", wizard section
byte-preserved, backup written, log line states all counts, and the
migrated file parses with ZERO malformed lines.
B. already-migrated file: byte-identical md5 after a full boot, no migration
line, no backup touched. (First attempt at this check clobbered its own
evidence by running case C first -- re-run properly.)
C. no file: fresh defaults now carry the marker as line 1, no migration.
The operator's real bindings.txt was stashed before testing and restored
untouched; it will migrate on their own next launch, as intended.
Docs flipped to the new reality: the README's "your keys DO NOT change"
upgrade promise is replaced by the migration story (customs + stick kept, old
stock keys replaced, bindings.old.txt escape hatch); CONTROLS.md gains the same
note; context/glass-cockpit.md records the mechanism, the 85-row table's
provenance, and why restoring the backup re-migrates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
150961176c |
merge fix-ups: the four review findings + a build portability gate the merge exposed
Applied on top of the glass-cockpit-refit merge, all from the pre-merge review: 1. README CONTROLS TABLE REWRITTEN for the new default board. It still taught "W/S throttle, A/D turn" while the defaults moved flight to the numpad and handed the letter rows to the MFD banks -- a fresh install would read controls that do not work. The new table teaches the numpad flight block, the panel-on-your-keys groups, F7 for control mode (was M), backtick for the camera (was V), the coolant valves on 1-3/QWE with the detent warning kept, and PgUp/PgDn volume. UPGRADERS ARE TOLD EXPLICITLY that their preserved bindings.txt keeps their old keys, and that deleting it opts into the board. 2. BACKTICK VIEW-TOGGLE GUARDED. The bindings-row-wins rule (KeyHasBinding) covered V and J/K/L but not backtick itself, which polled unconditionally -- and BACKTICK is a nameable, unbound key, so binding it to a button double-dispatched (button + camera). Both halves of the view poll are now guarded. 3. ENVIRON.INI REFUSES ONE-SHOTS. The loader applied ANY key=value line, and BT_JOYCONFIG in the file would defeat the #66 one-shot in a sneaky way: the wizard DELETES the process var after running, so in every relaunched child there is no real env var left to win over the file -- capture wizard at every mission start. BT_JOYCONFIG is now skipped with the reason in the template header, which also warns that test/debug hooks (BT_MP_FORCE_DMG and friends) do not belong in a file that silently persists forever. 4. VOLUME DOCUMENTED EVERYWHERE IT WAS MISSING. CONTROLS.md gains the PgUp/PgDn row (the old "- / = (see below)" cell pointed at a note that never mentioned volume) and the note that the keys moved so they no longer share a key with anything in any layout; the bindings template header reserves PgUp/PgDn informally. 5. L4KEYLIGHT BUILD GATE (found by building the merge on this machine). The C++/WinRT Dynamic Lighting TU does not compile against SDK 10.0.19041 -- its bundled cppwinrt fails inside winrt/base itself (C2039 'wait_for'). CMake now detects an SDK older than 10.0.22000 and builds a dormant stub with the same five-function scalar interface (logs once at Start, identical behaviour otherwise) -- extending the feature's own runtime philosophy ("no Dynamic Lighting -> log once, stay dormant") to build time. Dynamic Lighting is a Windows 11 22H2 feature, so nothing real is lost on an older build machine, and machines with a newer SDK build the real mirror unchanged. VERIFIED on the merged tree (4.11.572): * build clean -- 0 compile errors; the 40 /FORCE-tolerated unresolved externals are byte-identical to every pre-merge build (20 CreateStreamedSubsystem + 20 DefaultData; the earlier claim that all 40 were CSS was shorthand -- corrected here for the record). * solo boot: environ.ini template written + loaded, mode resolver announces the surround, historical bands L276/R276/T223/B336 confirmed, keylight stub logs its dormant line, BT_SHOT capture shows the under-glass button treatment and the corrected hat labels, 0 faults. * PgUp/PgDn volume PROVEN LIVE via injected keys: 5%->10%->15%->10%, saved to volume.cfg. (Side catch: content\volume.cfg had been sitting at 0.00 from an old test -- anyone using this tree had a silent game; reset.) * full relay cycle on the merged exe: 2 pods staged, launch pair, mission, StopMission, round RESET -- all clean. * all three console suites still pass; checkctx CLEAN. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9d536294e4 |
Mouse buttons are bindable; mouse LOOK scoped (not built)
BUTTONS (shipped). Win32 hands mouse buttons out as virtual keys, and the
binding path already stores VKs and polls GetAsyncKeyState -- so this needed no
verb, no parser change and no new machinery, just names:
key MOUSE4 button 0x40 # side buttons
key MOUSE5 button 0x46
key MOUSEMIDDLE button 0x41
MOUSELEFT/MOUSERIGHT are named too, with a warning in the file header and the
docs: they are how the player presses cockpit buttons (left = press, right =
latch), and the poll is focus-guarded but not click-aware, so binding either
ALSO fires on every panel click. The side and middle buttons are the free
ones. Axes work as well (`key MOUSE4 axis Throttle slew + 0.7`).
One guard: the RGB lamp mirror skips mouse VKs when building its map -- a
keyboard lamp array has no mouse buttons to paint (BTPadBindingIsMouseKey).
Verified live: a bindings.txt carrying two mouse rows loads 76 keys (74 + 2),
and a real injected XBUTTON1 press produced `[emitter] FIRED #1`. Clean A/B --
the earlier run whose SendInput failed the struct-size check logged no fire at
all. Shipped commented-out examples in the default profile so the capability
is discoverable.
LOOK (scoped only). docs/MOUSELOOK_PLAN.md. Mouse MOVEMENT is a real feature,
not a table entry, and the hard part is not reading the device:
- The cursor is already spoken for. The glass cockpit's premise is that all
72 buttons are mouse targets; mouse-look wants a captured, hidden, recentred
cursor. Both cannot be true, so this is a MODE question first -- four
models compared, recommending hold-to-look (costs nothing when unbound,
cannot strand a player, behaves the same in surround and exploded layouts).
- The channels are POSITIONAL (JoystickX/Y are -1..1 deflections the mapper
reads per control mode); a mouse gives deltas. Accumulate-into-a-virtual-
stick reuses every existing rule; a rate model would need new mapper
semantics.
- Control mode changes what it MEANS: BASIC steers with the stick, MID/ADV
twist the torso -- so mouse-look steers in one and aims in the other.
Proposed grammar (new row type, so old builds skip it with a warning),
where the code goes, ~250 lines, and a verification plan whose load-bearing
item is "panel clicks still work when a mouselook binding exists but is not
engaged". Also flagged: the pod had NO mouse, so nothing here is
reconstruction -- every choice is design, judged on feel, and it must stay out
of the pod build's path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5f88a4eeb2 |
The keyboard becomes the button board (RP412 bindings port) + a controls page
Ported RP412's bindings design and made it the DEFAULT: the letter and number rows are the MFD button banks laid out WHERE THEY SIT ON THE PANEL, flight moves to the numpad so the board stays free, F1-F12 are the map's two columns, and G/B stay unbound as the physical gap between the lower clusters. Expressed in BT's OWN grammar (slew/deflect/set, the Turn channel) -- bindings.txt is a documented compatibility surface and was not touched. Coverage: 61 of 72 addresses on the keyboard, and the 11 absent ones are exactly those the pod never wired (0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E intercom/door). All 72 stay clickable on the panel. It lands on BT's addresses unreasonably well: 1-4 + QWER are the ENTIRE coolant system (Condensers 1-6, flush, balance), F6/F7 the display and control-mode cycles, F9-F12 Generators A-D, F4 the crouch button reconstructed earlier today. THE DELIBERATE COST. A bound key is removed from the authentic 1995 typed-hotkey channel so it cannot double-dispatch, and this board binds nearly everything -- so 5 (Quad page), z (Eng1), t/y/u/i/o (pilot select) and +/- (target zoom) are given up. Unbind a key to get its 1995 meaning back. Documented in the file header, the markdown and the page. NEW RULE: A BINDINGS ROW WINS OVER A BUILT-IN CONVENIENCE KEY (PadRIO::KeyHasBinding). V and J/K/L are board buttons now, and those polls read GetAsyncKeyState directly -- without this they would have fired BOTH the button and the built-in. View toggle lives on BACKTICK alone. CONTROLS.MAP rewritten to mirror the board so glass/pod/dev still feel identical (the 2026-07-21 settlement): 90 bindings, 0 parse complaints. HAT LABELS CORRECTED [T1]. INPUT_PATH_AUDIT flagged 0x41-0x44 and was right. Settled from the streamed mapping (BT_CTRLMAP_LOG): elem 66 -> subsys 17 attrID 14, i.e. 0x42 is the TORSO subsystem, not a look; 0x44/0x43/0x41 are the mapper's LookLeft/LookRight/LookBehind (attrID 10/11/12). The .RES has no "TORSO CENTER" string -- its names are LookBehind/Down/Forward/Left/Right -- so the audit's wording was loose but its substance correct. Swept L4GLASSWIN, L4PADPANEL and L4VB16. docs/CONTROLS.html: interactive keyboard (hover a key for its address and meaning, MFD clusters outlined), pad diagram, panel map, the under-glass press-target figure, radar placement thumbnails and the environ.ini table -- modelled on RP412's page, rewritten for BT's addresses and hazards. mkdist wraps the fragment in a doctype/charset shell and ships it beside CONTROLS.txt. Verified: bindings.txt regenerates and loads (74 keys, 0 errors); CONTROLS.MAP 90 bindings, 0 errors; 72/72 panel addresses still dispatch; surround / exploded / dock / pod / dev boot and simulate with zero faults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
56d9971b0e |
-fit: pin the backbuffer to the CANVAS, not the client (the display was wrong)
Field report with a screenshot: `btl4.exe -fit` renders tiny MFDs hugging the corners, an over-wide world view, and a horizontally squashed scene. CAUSE. L4VIDEO pins the windowed backbuffer to the CLIENT RECT (Gitea #56, so a device Reset cannot silently re-derive it). That is correct precisely while client == canvas -- true on a normal boot, and FALSE under -fit, which sizes the window to the whole monitor BEFORE the device exists. The backbuffer came out 3440x1440 against a 1452x1059 canvas, and everything downstream disagreed: - BTApplyWorldViewport and BTDrawCockpitPanels lay out from the BACKBUFFER, so the canvas they drew was 3440 wide: bands unchanged, MFDs still 320x240 (hence tiny), view rect 2888x881 instead of 900x500; - the world projection used the aspect of the INTENDED view (1.8) inside a 3.28 viewport, stretching the scene; - Present then scaled that oversized backbuffer into the 1974x1440 letterbox, squashing it horizontally by 0.574; - and every button was hit-tested through gBTCockpitCanvasW/H while being drawn in backbuffer space -- the exact #50 divergence the pinning comment warns about, unnoticed because nothing had clicked under -fit. FIX: when the cockpit surround is up, the backbuffer IS the canvas, whatever the window is doing. Pin to gBTCockpitCanvasW/H and let the letterbox do the scaling -- which is what the rest of the design already assumed. Drag-resize was always right for this reason (the device is created while client == canvas and a later resize does not recreate it); only -fit, which changes the client first, exposed it. MY EARLIER VERIFICATION WAS WRONG. I called -fit good from a screenshot that was centred and letterboxed, without checking that the backbuffer matched the canvas or that a single button could be clicked. Both are checked now. Verified: -fit on 3440x1440 renders correct proportions (captured), and 72/72 buttons dispatch under -fit; surround/exploded/dock/pod/dev all boot and simulate with zero faults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |