4736cba1ca9bd51be8c159b4b106cf5cb0a1b707
250
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
48d47ef806 |
#45 review pass: correct three claims the fix's own comments got wrong
An adversarial review of the change returned NO BLOCKERS but caught three
statements that were wrong or unverifiable. All three are corrections to my own
comments/docs, not behaviour changes -- the verified binary is unchanged.
1. The phantom-kill note had the ORDERING backwards. It claimed the owner's
record "overwrites it on the next record -- the phantom stops being visible".
The engine's event priorities run the other way: an inbound update record is
posted at UpdateEventPriority == MaxEventPriority and drained by
ExecuteBackgroundTask, while the rerouted score message sits at
EntityManagerEventPriority == DefaultEventPriority and is popped later -- so
the correction usually lands FIRST and the phantom AFTER it. The artifact is
BOUNDED, not masked: on the killer's pod only, that victim's KILLS reads +1
until the next record, i.e. <= one 2s heartbeat instead of "until the victim
next scores or dies". Swept in btplayer.cpp, KD_SCOREBOARD_PLAN.md and
RECONCILE.md.
2. The corpus figures were not reproducible, and were mis-tiered. I cited
"125 of 125 SCORE type=2 over 255 node-logs" and "0 of 8800 DMG rows" -- true
when measured, but the corpus is append-only and this fix's OWN verification
runs grew it from ~255 to 425 files, so nobody can re-derive them. Replaced
everywhere with invariant RATIOS that hold at any corpus size, re-measured
here:
* 0 of 18818 DMG rows are inst=R (damage is applied master-side only)
* 439 of 439 DEATH inst=R rows read killer=0:0 killdmg=0.000, against
0 of 225 inst=M rows (sole lastInflictingID writer: mech.cpp:746)
Also re-tagged T1 -> T2: log-corpus field evidence is T2 per the CLAUDE.md
tier table. Lesson recorded in the banner: cite ratios, not counts, for a
corpus your own rigs append to.
3. The recordLength guard's justification was wrong (and the plan's risk 5 with
it). A short legacy record does NOT desync the stream -- both sides advance
by the WRITER's stamped recordLength and no reader asserts a length. The real
hazard is a read PAST THE ALLOCATION: an inbound update message lives in a
per-event heap block sized from messageLength, so on a TRAILING legacy record
the two new fields would be read off the end of it.
Plus: the record struct's own field comment still said "deathCount -- the DEATHS
column", the exact wrong-field belief #45 removed, sitting on the wire struct.
Filed the review's other finding on #60: a SECOND decomp export gap, index
jumping FUN_0049fe80 (ends 0x49ffc8) -> FUN_004a1232, 4714 unexported bytes
containing Mech::TakeDamageMessageHandler @0x4a0230 -- the producer of every
score and death message in the game. Verified independently against
functions_index.tsv before posting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a52207d779 |
#45 scoreboard: reclaim the binary's DEATHS field, add a heartbeat, sweep the false KB claims
Follow-up to
|
||
|
|
4537cb42e5 | KB: link the fourth deferred defect (#65 HandleMessage non-virtual) -- the sed anchor missed on the first pass | ||
|
|
23978bd27a |
KB: link the four deferred defects to their new Gitea reports (#62 AutoConnect / #63 PlayerStatus databinding / #64 damageZone shadow / #65 HandleMessage non-virtual)
Findings from the #46/#47/#48 work were living only in context/open-questions.md. Each is a real defect with player-visible or latent-crash impact, so each now has a tracker item with evidence + fix design; the KB entries point at them by number so the two stay in sync. |
||
|
|
f3179ef020 |
#47 follow-up: restore the DAMAGE TIER to the heat branch -- HeatableSubsystem::GetStatusFlags was a stub returning 0
Found while auditing today's vtable change before release. The binary's
HeatSink::GetStatusFlags @004add30 OPENS with `call 0x4ac144` --
MechSubsystem::GetStatusFlags, the damage tier (damageLevel >= 1.0 -> bit 0
Destroyed; > 0 -> bit 1 Damaged). There is no zero-returning
HeatableSubsystem override anywhere in the image.
Our port routed that call through `HeatableSubsystem::GetStatusFlags()
{ return 0; }` -- a "neutral default" that silently dropped bits 0 and 1 for
the ENTIRE heat branch (every weapon, sensor, emitter, PPC, generator,
powered subsystem). Two live consequences:
* Destroyed / Damaged could never reach MechTech's annunciator scan --
gutting half the shipped mechalrm table (Destroyed -> gotoEngineering +
engCooling + engBusMode) hours after #47 shipped the flash;
* PoweredSubsystem's AutoConnect gate (`GetStatusFlags() == 0`,
powersub.cpp:358) read a DAMAGED subsystem as pristine.
One-line fix: chain the real tier, reproducing @004add30 exactly. The call
site's own comment already said `// FUN_004ac144` -- the implementation had
simply never matched it.
Also corrected a stale comment on MechSubsystem::GetStatusFlags that read the
field as a STRUCTURE level ("1.0 = intact, 0 = dead") -- backwards. The
engine member is damageLevel and ACCUMULATES to 1.0 = destroyed (mechdmg's
crit cascade tests `level >= StructureMax` for exactly that), which is what
makes bits 0/1 line up with TechStatusType Destroyed/Damaged. Behaviour was
always right; the comment was a leftover of the same structureLevel misreading
#46 retired.
VERIFIED (3-pod combat sim, BT_LAMP_LOG): the annunciator is genuinely live --
Jammed x7, AmmoBurning x5, BadPower x20, Overheating x207 (Overheating
correctly maps to NO lamp: the shipped table has no entry for it). The lamp
resolution is per-subsystem as designed -- different weapons flash their own
panel buttons (lamps 0xf/0x5/0xb/0x3 under distinct mode masks), 32 flash
writes across a whole battle, no thrash. Zero crashes, zero plane/OOB
tripwire hits across all three pods.
FILED, deliberately NOT fixed tonight (open-questions.md): vtable slot +0x40
is misattributed. The binary calls it `(this, 0)` in AutoConnect's outer gate
and `(this, candidateGenerator)` in the roster walk -- a one-arg voltage query
(HasVoltage(source)), not GetStatusFlags(). Modelling both as the no-arg
GetStatusFlags makes the outer gate demand "no flags" and the inner "some
flag" with nothing between them, so AutoConnect's loop body can never execute.
Pre-existing before and after this change; it is a live power-bus behaviour
change and deserves its own playtest cycle, not a release-eve patch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1d31af61d7 |
Gitea #48: arm the artifact tripwires BY DEFAULT + surface detections through the matchlog
The user's ask: if the translation-table fix did not quite catch it, will the
next playtest LOG the artifact? Before this commit, no -- BT_PLANE_AUDIT was
opt-in via env var, and playtest machines set none.
Changes (all engine L4VB16):
* BT_PLANE_AUDIT is now DEFAULT-ON, =0 opts out. Armed cost is a couple of
integer compares per DRAW CALL (not per pixel); confirmed zero log noise
on a clean default-env run post-fix.
* A SECOND trap class: out-of-bounds draw STARTS, checked always-on in
buildDestPointer (the funnel every primitive builds its pointer through).
Release builds compile Verify() to nothing, so a wild start previously
computed a silent rogue pointer into (or past) the shared buffer. Now
logged ([plane] OOB) and CLAMPED in-bounds.
* First detection of each class writes a GLITCH matchlog record
(kind=plane / kind=oob with position+color+mask), so the evidence arrives
with the round logs the operator already collects -- unattended.
* Both existing plane-leak trap levels (display primitives + the port-level
pixmap table scan) flipped to the same default-on gate.
Honest coverage note (in gauges-hud.md): an in-buffer EXTENT overrun from a
valid start (a blit walking past the right/bottom edge into later rows of the
SAME plane) is not trapped -- per-pixel walk checks are too hot. The plane
trap still catches any such overrun whose color leaks planes, which is the
class the night-3 artifacts belonged to.
Verified: full clean rebuild -- 40 pre-existing LNK2019 only (the
engine->game BTMatchLog linkage resolved, /FORCE-trap checked); default-env
probe run: 0 [plane] lines, game unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6bb03aed0b |
Gitea #48 ROOT-CAUSED + FIXED: MFD "stray blocks / misplaced lamps" = uninitialized translation-table entries leaking pixels into other displays' bit-planes
The conviction was empirical, not theoretical: a new write-site trap
(BT_PLANE_AUDIT) in the seven L4VB16 drawing primitives logs any draw whose
color carries bits outside its port's plane mask -- the exact cross-display
corruption condition (Replace ORs an unmasked color; Or/Xor ignore the mask
entirely; And clears foreign planes). A quiet solo session produced 15-30
leaks per minute:
[plane] PORT 'sec' pixmap draw at(0,0) idx 217 entry=0xffffff00 mask=0x3f
[plane] LEAK DrawPixelMap8[table] at(639,0) color=0xffffff00 mask=0x3f leak=0xff00
THE DEFECT: L4GraphicsPort::translationTable[256] is never initialized in the
ctor, and BuildSecondaryTranslation fills only the entries its BitWrangler
reaches -- 2^numberOfBits: 64 for the sec plane (mask 0x3F), 4 for the overlay
(0xC0). Entries above that stay heap garbage. Every draw resolves color
through this table, and the PIXMAP path indexes it with raw pixel values
0..255: the 480x640 radar background carries pixel index 217, whose garbage
entry's high bits (0xFF00 = ALL EIGHT MFD planes) were stamped into the shared
640x480 buffer -- invisible on the culprit page (the in-plane bits happened
dark) and visible as bright fragments at the same coordinates on every OTHER
display. That is the operator's screenshot exactly: the same-position blips
on Mfd1+Mfd2 and the Heat-display block.
FIX (engine, both layers):
* zero translationTable in the L4GraphicsPort ctor (plane-neutral default);
* BuildSecondaryTranslation now cycles the in-plane pattern across entries
[2^bits..255] -- high-index art degrades to its (index mod 2^bits) colour
IN-PLANE and can never leak. The 1995 binary ships the SAME 64-entry fill
and survived on 6-bit art discipline; garbage is not a preservable
behaviour, so the cycle-fill is a guarded PORT deviation (documented).
A/B PROOF: same probe, 60s -- 0 leaks after the fix (15-30/min before).
sim3 3-pod regression: zero crashes.
SECOND real defect found + fixed en route: sessions configure the CAMERA seat
first ("cameraInit" -- which includes the MISSION-REVIEW context:
configure(0, sec, 0, 0x00FF, native, rgb, mrpal.pcc), a DirectColor context
whose translation tables legitimately hold full RGB565 values spanning all 16
plane bits), and the viewpoint swap to the mech re-configured WITHOUT tearing
that tree down (btl4app's s_gaugeTreeBuilt latch treats the mech build as the
first). The orphaned review gauges kept executing through stale DirectColor
ports. BTL4GaugeRenderer::ConfigureForModel now overrides (ConfigureForModel
made virtual in L4GREND.h) and tears the prior entity-bound tree down before
every rebuild -- safe on first build, and the review screen rebuilds the same
way when the seat returns to the camera.
Also logged (open-questions): the review-screen PlayerStatus panels read the
compiled player at RAW BINARY OFFSETS (+0x1FC vehicle / +0x1C8 score /
+0x1C4 alive-dead box) -- the databinding trap, dormant until the review
screen runs; bridge before enabling the review.
Ruled out along the way (with evidence): the pilot-list label/erase geometry
(erase box 128x32 covers the 64x16 name rasters), the radar name labels
(view-level ClipImage clips them), PlayerStatus in-game execution ([ps] probe:
never runs in-game), and the PNAME bitmap dimensions (egg generator emits
64x16 exactly).
Why it "started with the comms feature" (#43): that wave registered the new
gauge classes with the config interpreter, letting more of the authored page
furniture parse and draw than before -- the leaking high-index pixmaps rode in
with it. [T3 correlation detail; the leak + fix are T2 live-verified.]
Diagnostics kept (env-gated): BT_PLANE_AUDIT (the leak trap, now a permanent
regression tripwire), BT_PS_LOG, the port-level pixmap identity trap.
KB: gauges-hud.md (#48 section), decomp-reference.md (BT_PLANE_AUDIT),
open-questions.md (PlayerStatus databinding entry). checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6f5a264835 |
Gitea #47 COMPLETE: the ENG-button attention FLASH is live (jam / bay fire) -- MechTech status scan + BTL4GaugeAlarmManager + lamp chain, all from the binary
The pod behaviour Cyd described -- "on a jam or bay fire the display eng button
for the system flashes" -- is authored data + a five-stage chain, now running:
MechTech::TechnicalAssistance (@004ad33c, per frame)
edge-scans every monitored subsystem's GetStatusFlags() 7-bit condition
mask (TechStatusType: Destroyed 0, Damaged 1, CoolantLeaking 2,
Overheating 3, AmmoBurning 4, Jammed 5, BadPower 6)
-> Start/StopEntityAlarmMessage (@00436688 id 7 size 0x20 / @004366b8 id 8
size 0x1C; broadcast @004364e4; port shape: direct
Start/StopEntityAlarmImplementation calls on the gauge renderer)
-> GaugeAlarmManager::Activate(alarmModel) -- alarmModel = MechTech+0x100 =
the 'mechalrm' ModelList (id 83) -> SearchList(83, type 31) -> the baked
GaugeAlarmStream (id 331, 11 items {condition, lampCode}), decoded:
Destroyed -> gotoEngineering + engCooling + engBusMode
CoolantLeaking -> gotoEngineering + engCooling
AmmoBurning -> gotoEngineering + engEject
Jammed -> gotoEngineering + engEject
BadPower -> gotoEngineering + engBusMode
-> BTL4GaugeAlarmManager::ReadGaugeAlarmStreamItem -- THE REAL BODY, from
@004cc2fc + helpers @004cc108/148/1a0/264/27c + tables @0051cf1c..0x51d084
(gotoEngineering 0x80 = the subsystem's QUAD-SELECT bezel button via
lamp[aux]/mode[aux] tables; 0x81+ descend the eng-page bank; Condenser /
Generator specials on CoolantLeaking; <0x80 = the heat-bank fixed map).
btl4galm.cpp's old bodies were admitted fabrications and its provenance
note ("no override body exists in the image") was wrong -- corrected.
-> LampManager::FindLamp (@00444c80) -> Lamp::SetAlertState (@00444e64, a
COUNTER so stacked alarms hold the flash) -> L4Lamp::NotifyOfStateChange
emits RIO flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239) -> the
pod's physical lamps AND the glass panels (PadRIO IS rioPointer there).
FOUR load-bearing defects found and fixed en route -- each independently fatal
to the feature:
1. HeatableSubsystem's Derivation chained Subsystem directly, SKIPPING
MechSubsystem (written before the WAVE-1 re-basing) -- so EVERY subsystem
on both family branches failed IsDerivedFrom(MechSubsystem), which is
exactly MechTech's monitor filter: the status scan watched NOTHING.
2. MechSubsystem::GetStatusFlags was non-virtual (binary: vtable slot 12) --
the scan's MechSubsystem* call bound statically to the base tier and the
weapon bits could never surface.
3. ProjectileWeapon::GetStatusFlags sat in a Ghidra export gap -- raw-disasm
@004bbf88: base | 0x20 (weaponAlarm==5 Jammed) | 0x10 (linked bin
cookOffArmed@0x18C AmmoBurning). The port had "defer to base".
4. Mech::Reset's respawn sweep blanket-cast every roster entry to
MechSubsystem and called RespawnRepair -- wrong for the Subsystem-level
entries (MechTech 0xBDC, SubsystemMessageManager 0xBD3). On MechTech the
recon damageZone slot lands on its subsystemMonitors chain head: NULL
while the scan was broken (fix #1's bug MASKED this one), but the moment
the monitors populated, RespawnRepair virtual-called through a
SubsystemMonitor as if it were a DamageZone -> load-time crash in
StateIndicator::SetState (caught with cdb; call [edx+14h] on code bytes).
The sweep now filters IsDerivedFrom(MechSubsystem) before the cast.
Also: GUID identities pinned -- 0x50f4bc = PoweredSubsystem::ClassDerivations
(via @004b1208 = its TestInstance; btl4gau2's two "Generator" comments were
wrong, swept), 0x50fb60 = Generator's. A shadow-field instance documented for
the deferred de-shadow: MechSubsystem::damageZone re-declares the PUBLIC engine
Subsystem::damageZone (SUBSYSTM.h:159) -- mechtech's naive `sub->damageZone`
read the never-written engine member (0 monitors again); now reads the recon
member via GetDamageZoneProxy(). Logged in open-questions.
Wiring: BTL4GaugeRenderer now constructs + assigns the BTL4GaugeAlarmManager
(the base ctor NULLs it and Activate Check()s it); MechTech's Report*/
StatusMessageSink stubs are real; alarmModel confirmed baked (= 83) at runtime.
VERIFIED LIVE (scratchpad/lampflash.py, BT_LAMP_LOG chain trace):
[techstat] MechTech id 32 monitors 29 subsystems, alarmModel 83
[techstat] LRM15_1 condition 4 SET (alarmModel 83) <- bay fire armed
[galarm] condition 4 code 0x80 -> lamp 0xd mode 0x1 FLASH
[lamp] 0xd <- 0x37 (FLASHING) <- the select button
[galarm] condition 4 code 0x85 -> lamp 0xb mode 0x4 FLASH <- engEject
[techstat] LRM15_1 condition 4 CLEARED <- detonation clears
[techstat] AmmoBinLRM15_1 condition 0 SET <- bin Destroyed
Regressions: baytest PASS (bay fire kills), baypurge PASS (purge extinguishes),
sim3 3-pod brawl PASS with ZERO crashes (6 kills, organic heat-route bay fires,
respawns clean through the new sweep filter).
Env: BT_LAMP_LOG ([techstat]/[galarm]/[lamp]). KB: gauges-hud (the flash
section), decomp-reference (the GaugeAlarm closure + tables + GUIDs),
open-questions (built-note + the de-shadow deferred item), btl4gau2 comment
sweep. checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5ae4410914 |
Gitea #46: ammo bay fire now DETONATES and KILLS (+ #47 fire icon) -- three stacked kill-switches removed, the fuse decoded, and a vptr-alias corruption caught by regression
Field report (night 3): "two ammo bay fires and no death" (Cyd + RajelAran; one
purged, one left burning). Root cause = THREE independent kill-switches stacked
on the same path, all in ammobin.cpp:
1. `GameClock::Now() { return 0; }` -- `cookOffTime < Now()` was `0 < 0`, so
an ARMED bay fire never detonated.
2. `InjectHeat(void*) {}` -- the detonation body was a no-op.
3. The bin's Damage record was never stamped -- 0 damage of type 0 (which the
mech TakeDamage handler drops) even if 1+2 had fired.
THE FUSE (raw disasm, scratchpad/disammo.py): the old "RandomDelay" was a Ghidra
carve artifact -- FUN_004dcd94 is __ftol and the export DROPPED the caller's x87
expression. The real bytes @004bd450: fld 10.0 / fmul [ticksPerSecond] /
fadd 0.5 / __ftol -- a FIXED 10.0-SECOND fuse in clock ticks. New gotcha #19
(reconstruction-gotchas.md) documents the __ftol export blind spot.
THE DAMAGE RECORD: bin+0x1F0..0x21C is a real engine `Damage` (FUN_0041db7c IS
Damage::Damage(), byte-matched to T0 DAMAGE.cpp). The linked ProjectileWeapon's
ctor @004bc3fc stamps it from weapon->damageData @0x3A8 via
owner->roster[0x128][res+0x1C0] -- projweap.cpp's old comment called this "the
bin's HUD display block ... wired in the AmmoBin family"; both halves were wrong
and it was wired nowhere. Now stamped (before MissileLauncher's ctor divides by
missileCount -- a missile bin authentically holds the per-SALVO amount).
THE DETONATION (@004ac274 = MechSubsystem::DistributeCriticalHit -- the old
"HeatableSubsystem::InjectHeat" label was wrong, and the old reconstruction
iterated a stand-in CriticalChain whose First()/Next() returned 0):
statusAlarm pulse Exploding(2)->Destroyed(1) (slot 13 = the printSimulationState
state PRINT @004ac8c0, not an "explosion notify"), own private zone pinned
destroyed, then collect the mech DamageZones whose crit entries plug the bin
(the binary filters plug classID 0x4E = DamageZoneClassID -- VDATA.h idx 78,
cross-checked via idx 28 = AudioStateTrigger), split the amount evenly, and send
the OWNER one full Entity::TakeDamageMessage per zone: inflictingEntity = SELF,
damageZone = the zone index, inflictingSubsystemID = the bin (the message-
manager explosion-bundling key, ENTITY3.h's own NOTE), printing the binary's
exact "ammo explosion damaging <zoneName>" @0050df61.
Port shape: Mech::AmmoExplosionFanOut (mechdmg.cpp) behind a databinding bridge;
guarded deviation: zoneCount==0 warns instead of the binary's unguarded divide.
CriticalChain/CriticalEntry stand-ins DELETED from mechrecon.hpp.
VERIFIED LIVE (BT_BAYTEST hook = message 1, the crit-induced arm channel):
scratchpad/baytest.py : arm -> 10s -> "20 rounds x 35 = 700 (type 2)" ->
"ammo explosion damaging dz_ltorso" -> zone cascade -> mech DESTROYED
(authentic death list).
scratchpad/baypurge.py: arm -> eject-hold purge -> "bay fire EXTINGUISHED
(bin empty)", no detonation.
scratchpad/sim3.py : the HEAT route arms organically in combat (overheated
AFC100), detonates "11 x 25 = 275 (type 1)" split across dz_larm + dz_lgun.
BAYBOOM matchlog record added for MP field forensics.
FIX-OF-THE-FIX (caught by the sim3 regression, would have shipped a crash):
MechSubsystem's ReconDamageZone proxy puts structureLevel at OFFSET 0 -- which
ALIASES THE REAL DamageZone's VTABLE POINTER (the private zone is `new
DamageZone`, mechsub.cpp:154; mechsub.hpp:260 documents the alias). My first
DistributeCriticalHit kept the old body's `damageZone->structureLevel = 1.0f`
and OVERWROTE THE ZONE'S VPTR with 0x3F800000; the respawn sweep's virtual
SetGraphicState (vtable+0xC) then called through it -> AV at 0x3f80000c in
RespawnRepair, one frame after a bay-fire death. ALL EIGHT proxy-view sites in
mechsub.cpp swept to the engine view (((DamageZone*)damageZone)->damageLevel
@0x158) -- including two silently-wrong LIVE readers: GetStatusFlags (vptr as
float -> always "intact") and ApplyDamageAndMeasure (the crit cascade's
measure). Ruled out first by evidence: the weapon->bin stamps were all clean
(six stamps, all classID 0xbcb, logged).
#47 (half 1 -- the FIRE ICON): BallisticWeaponCluster::Execute @004c9a38 reads
bin+0x18C = cookOffArmed into the btefire.pcc TwoState, and while armed computes
(Now - cookOffTime)/ticksPerSecond -- the COOK-OFF COUNTDOWN -- into the numeric
beside it. The old reconstruction misread 0x18C as "the reload state" and
bridged the icon to BTAmmoBinFeeding, so it blinked on every feed and never lit
on a bay fire (RajelAran: "it doesn't"). Now driven by the
BTAmmoBinCookOffArmed/CookOffTime complete-type bridges. [T2 -- the data path
is rig-verified; the pixels await the next live session.]
#47 (half 2 -- the ENG-BUTTON FLASH): fully mapped, deliberately NOT built this
session. The authored data SHIPS (BTL4.RES carries exactly one type-31
GaugeAlarmStream); the chain is alarm SetLevel -> gauge-watcher socket ->
Renderer msg 7 -> GaugeAlarmManager::Activate @00448d00 (T0) -> the BTL4
override @004cc148..@004cc2fc (btl4galm.cpp's provenance note claiming "no
override body exists" is WRONG -- corrected in-file) -> LampManager::FindLamp
@00444c80 -> Lamp::SetAlertState @00444e64 (flash counter) -> the L4 flush
@00474e94 emitting flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239).
Missing: the override bodies, the gauge-watcher sender, the aux-button lamps.
3-piece plan in context/open-questions.md.
Also logged: HandleMessage is vtable slot 8/9 in the binary but NON-virtual
across 10 reconstruction classes (bit the BT_BAYTEST hook; typed call used, gap
documented in open-questions).
KB: combat-damage.md (the full cook-off section), decomp-reference.md (the
cluster addresses + the GaugeAlarm/lamp map + BT_BAYTEST env), gauges-hud.md
(the fire-icon correction), reconstruction-gotchas.md #19 (__ftol),
open-questions.md (2 entries), btl4galm.cpp provenance correction.
checkctx CLEAN. 40 LNK2019 unchanged (the two pre-existing families).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d39227ef39 |
Gitea #51 ROOT CAUSE: LoopAtWill was mapped to "loop forever", arming 224 of 603 samples as endless OpenAL sources
SAURON's "coolant flush sound glitched and perma" is one instance of a systemic
defect. SetupPatch (L4AUDLVL.cpp) decided looping from the SAMPLE flag alone:
AL_LOOPING = (info.loop != ForceStatic)
That armed every non-ForceStatic sample -- 224 of the 603 authored zones,
including unmistakable one-shots -- as an OpenAL source that never ends on its
own. Any such sound whose note-off never arrives plays forever. Measured 1946
LoopAtWill x Transient arming events in one short session.
The sample flag expresses PERMISSION; the SOURCE decides. Three facts [T1]:
* the enum's own comments (L4AUDLVL.h:14-19) -- LoopAtWill = "will play once
OR LOOP AS DESIRED", ForceStatic = "plays only once EVEN IF LOOPED" (a
veto), LoopAlways = "ramp up and then down";
* LoopAtWill is enum value 0, i.e. the DEFAULT an unauthored sample receives
(WTPresets.cpp:37) -- it cannot mean "loop forever";
* LoopAlways, the real always-loop, is used by ZERO shipped samples.
"As desired" is the source's authored AudioRenderType (Transient=one-shot vs
Sustained=held), streamed from AUDIO*.RES (AUDLVL.cpp:28) and already consulted
by the renderer (L4AUDRND.cpp:567, AUDREND.cpp:184). SetupPatch now takes it,
threaded from all three L4AudioSource call sites (Direct/Dynamic3D/Static3D).
New rule: LoopAlways->1, ForceStatic->0, LoopAtWill->the source's render type.
MEASURED before changing behaviour (BT_LOOP_AUDIT=1, scratchpad/loopaudit.py):
LoopAtWill x Transient -> loop=0 (was 1) 1946 events
LoopAtWill x Sustained -> loop=1 unchanged 65 events
ForceStatic x Transient -> loop=0 unchanged 292 events
The engine loops (EngineAccel07_z0..z2, EngineMotor01, EnginePower01) are all
LoopAtWill/Sustained and PRESERVED -- no regression there. All 24 reclassified
samples are genuine one-shots: laser charge/fire/explosion/loaded, missile
loading, engine shift, coolant pressure inc/dec.
A/B PROOF (scratchpad/loopab.py, same session both arms, only BT_LOOP_LEGACY
differs), asking the engine's own BT_AUDIO_DUMP what is still playing with
loop=1 long after every release:
[legacy] EnginePower01, CoolantPresInc03_z2, CoolantPresDcr03_z2
[fixed] EnginePower01
The two stuck coolant sources are the reported symptom. They were eventually
reclaimed when a later trigger reused the source, which is why the bug was
intermittent -- the "perma" case is when nothing else retriggers it. The fix
removes the mechanism.
BT_LOOP_LEGACY=1 restores the old rule for a field A/B without a rebuild. The
layers to listen to are the beam sustains: LaserA/CSustain* are authored
LoopAtWill on a TRANSIENT source, so they now end with the sample instead of
looping until note-off. The render type is T1 authored data; the interpretation
that LoopAtWill defers to it is a strong reading of the enum + defaults rather
than a disassembled statement, and is flagged as such in the KB.
Plausibly bears on #32 (audio cutting in/out late in a match): a stuck looping
source holds its pool slot for the rest of the round.
Rigs: scratchpad/flushsnd.py (flush start/stop pairing), flushsnd2.py (stuck-
source hunt), loopaudit.py (the classification matrix), loopab.py (the A/B).
KB: context/wintesla-port.md audio section, context/decomp-reference.md env
table (BT_LOOP_AUDIT / BT_LOOP_LEGACY / BT_AUDIO_DUMP / BT_AUD_TAIL),
docs/AUDIO_FIDELITY.md F38. checkctx.py CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f3bdb3b85a |
Searchlight + ThermalSight ToggleLamp WIRED (#61) -- and a swapped table attribution ROOT-CAUSED, retracting a false "1995 latent bug"
Both classes' Receiver::MessageHandlerSet were default-constructed blackholes (input-path audit systemic cause #1): entryCount 0, no parent chain, Find() returns NullHandler for every id, Receive drops silently. The new unhandled- message trace printed it on the first press: [btntest] PRESS 0x14 at poll 400 [msg] UNHANDLED: Searchlight has no handler for message id 3 Each class now has a GetMessageHandlers() function-local static chained to PowerWatcher's (which name-resolves through HeatWatcher/MechSubsystem to Receiver's empty ROOT set, so id 3 does NOT collide with HeatSink's ToggleCooling), plus a correctly-typed forwarder -- Receiver::Handler is void(const Message*) while the decomp shape is Logical(Message&), so a cast would be UB that merely happens to work on x86 __thiscall. DefaultData re-pointed off the dead empty sets. MessageArg now reads the NAMED ReceiverDataMessageOf<int>::dataContents with a static_assert locking it to the binary's message+0xC (was a raw offset read -- databinding rule). ROOT CAUSE of a long-standing misattribution: @004b860c is **ThermalSight's** ToggleLamp (table @0x51120C), NOT Searchlight's. The two TUs emit parallel shared-data blocks with identical stride (msg entry, +0x5C attrs, +0x84 Performance triple); what pins each entry to its TU is that "ToggleLamp" is NOT pooled across them -- two copies exist, each emitted immediately before its own class-name string ("Searchlight"@0x51144B, "ThermalSight"@0x511475). [T1: reference/decomp/section_dump.txt:69661-69711] Searchlight's own handler is @004b838c, which sits in a Ghidra EXPORT GAP (#60). RECOVERED by raw disassembly of content/BTL4OPT.EXE (scratchpad/dis838c.py, the EjectAmmo technique) -- and it corrected two things I had inferred wrong: * NO ControlsAllowLights/+0x25C novice gate. Searchlight tests the press alone; that lock is ThermalSight-only. A NOVICE pilot CAN work the lamp. * `or word ptr [this+0x18],1` sits AT the jle target -> the graphics-dirty bit (updateModel == ForceUpdate(), per #59) is raised UNCONDITIONALLY. Consequence: the "ORIGINAL 1995 LATENT BUG -- the searchlight can never light" claim is RETRACTED. It compared Searchlight's Performance (@004b841c, reads requestedOn@0x1E0) against ThermalSight's toggle (0x1DC) -- two different classes -- and so invented a missing 0x1DC->0x1E0 bridge. Every sibling toggles the field its own Performance reads. The searchlight DID light in the arcade, the searchlight->fog swap was NOT inert there, and building PullFogRenderable is FAITHFUL rather than a designer-intent deviation (the 2026-07-13 "left as-is" decision is void; the sim needs no repair). Verified live, real click seam (BT_BTNTEST): 0x14 -> [light] requested ON -> reported lightState 0 -> 1 (lamp lights) 0x12 -> [light] thermal sight requested ON -> thermalActive 0 -> 1 UNHANDLED lines for both classes gone; 40 LNK2019 unchanged (the pre-existing CreateStreamedSubsystem + Entity__SharedData::DefaultData families only). Swept the misattribution out of searchlight.cpp/.hpp, thermalsight.cpp/.hpp, hud.cpp:282 (it had claimed @00511180/@004b838c as HUD's), btplayer.cpp's +0x25C consumer list, context/{decomp-reference,subsystems,rendering,open-questions, pod-hardware,experience-levels}.md, docs/{GLASS_COCKPIT,INPUT_PATH_AUDIT}.md. checkctx.py CLEAN. Still open (documented, not fixed): ThermalSight has no visible IR effect (ToggleGlobalThermalVision is a marked no-op, pvision unported, IsLocallyViewed returns False); ThermalSight publishes "LightState"->0x1D8 where the binary has "LightOn"->0x1D8 and "LightState"->0x1E0; Searchlight's commandedOn@0x1DC has no identified role and measured 21 live, hinting our SubsystemResource +0x28 differs from the binary's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
23229e573e |
Reverse thrust was dead on the desktop (user report): wire the 0x3F throttle-handle button
LALT -> button 0x3F was bound in both binding layers and PadRIO pushed the RIO ButtonPressed event correctly, but nothing ever set the mapper's ReverseThrust: - the authentic route is BTL4.RES 'L4' streamed record [2] (Button Throttle1 0x3F -> attr 6 ReverseThrust@0x124), but our streamed BUTTON mappings are not consumed at all -- MechControlsMapper::AddOrErase is still the unreconstructed Fail stub, so the event never reaches the attribute; - the ONLY writer of reverseThrust was the keyboard bridge's 'key_throttle < 0' test, which is bypassed whenever a RIO is operational (the pad RIO always is, so the bridge is OFF on the desktop) AND is unreachable regardless, because L4PADRIO clamps the Throttle channel to [0,1]. Net: Alt did nothing and the flag was re-zeroed every frame. FIX: publish the 0x3F hold state from PadRIO::EmitButton -- the one chokepoint every desktop source funnels through (keyboard, gamepad, DirectInput joystick, glass-panel clicks via SetScreenButton) -- and apply it in InterpretControls gated on BTPadRIOActive() so real pod hardware is untouched (there the streamed mapping owns the flag). Marked [T3]; delete once streamed button mappings land. Reverse is a HOLD (manual: hold the red throttle button, release = forward). Verified with scratchpad/revtest.py (keybd_event injection, BT_KEY_NOFOCUS): forward = rev=0 dmd +61.501; LALT held = rev=1 dmd -61.501; release edge logged. KB: pod-hardware.md now records the streamed-button-mapping gap + this seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
6c3fca2f67 |
Gitea #38 ROOT CAUSE + FIX: mech paint lost on geometry re-load (view toggle / respawn)
Found from the user's live 2-node demo: a Crimson MadCat went GREY in its own view after a V (inside/outside) toggle, with NO new [paint] or MakeMechRenderables line in the log -- so no rebuild, just a re-parse. ROOT CAUSE: the per-pilot colour/badge/patch is applied by rewriting MATERIAL NAMES while a BGF parses, and only while the substitution callback is installed (SetupMaterialSubstitutionList .. TearDown, bgfload.cpp:15-18). BTL4VideoRenderer::ApplyViewSkeleton re-parses every shown segment BGF on each view toggle AND on respawn (that is what its fresh graphic-state read is for), but did so OUTSIDE that bracket -> raw %color% placeholders -> unpainted materials -> grey. Nothing caches the geometry (d3d_OBJECT caches only textures), so every call re-parses. This is #38's mechanism: 'colors not preserved on respawn' was never a replication or teardown bug. FIX: re-install the substitution list around the reload, reusing the serial the mech was BUILT with -- SetupMaterialSubstitutionList advances the global %serno% per call, so a naive re-bracket would stamp a different serial and still resolve the wrong names. MechRenderTree gains paintSerno (captured before Setup at build); ApplyViewSkeleton installs/restores around the loop and logs it. Rig-verified: the crimson MadCat stays crimson (yellow patch intact) across repeated inside/outside toggles; each toggle now logs '[paint] color=red serno=0' + '(paint serno 0)'. ALSO -- corrections to yesterday's #44 name plate from the adversarial pass: - kPlateARGB 0xFF808080 -> 0x80FFFFFF. BMAP.BMF tag 0x0027 is dpfB_MATERIAL_OPACITY_TAG [T0 libDPL/dsys/PFBIZTAG.H], NOT a diffuse colour: the materials carry NO colour tag at all, so the plate is the unmodulated callsign raster at ~50% opacity, not a grey label. (Two independent workflows converged on this.) - the plate's K = 1/2.8 is NOT the hotbox's constant (2.8145) -- de-unified. - the Lock attribute is HUD attr id 10, an int/Logical*, not a Scalar*. - the PNAME pair split is the V half, not U (our per-player texture is one 128x32 cell, so the port quad samples v 0..1). - retracted bgf-format's 'tag 0x0027 likely SPECULAR [T4]'. KB: new reconstruction-gotchas section on the whole bug class (load-time-only state must be re-installed on every RE-load, incl. the serno trap). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
35c750dc7c |
Gitea #44: the reticle TARGET NAME PLATE (PNAME1-8) -- the target's callsign under the crosshair
Reconstructed the last deferred piece of the 1995 reticle, decoded from the Execute disassembly @004cdcf0 [T1]: - selection: target_mech+0x190 (owning BTPlayer) -> player+0x1e0 (playerBitmapIndex), 1-based into the mesh table at this+0x2e8 - placement: re-placed every frame the aim moves -- scale 0.12 uniform, translate (K*retPos.x, K*retPos.y - 0.08, -1.0), K = 1/2.8 (the x87 long double @0x4cee64) -- so the label TRACKS THE AIM POINT, not screen centre - visibility: the LOCK attribute (this+0x184, cached +0x188) AND an owning player; reticle-off / simple-X also hide it - art: the plate quad UV-addresses a shared 128x64 bitslice texmap whose texels the pod OVERWROTE each mission with the egg's callsign rasters (original source commented out at L4VIDEO.cpp:5682-5710) -- the baked 'PLAYER n' art in BMAP.BSL is only the shipped fallback. Material colour is a neutral (0.5,0.5,0.5): grey-white, not phosphor green. Port: same geometry in reticle units (0.224 below the aim point, 0.336x0.084) drawn as the target's egg callsign texture through the reticle's own MapX/MapY mapping, so it follows the world view in every layout and BT_SHOT captures it. New: dpl2d_DrawTexturedRect, BTReticleTargetPlate, BTGetPlayerNameTexture. Also: kRetCaret corrected 0.02f -> 0.025f (_DAT_004cd7f4 read from the binary; the old value was a T3 guess, 20% small). KB: gauges-hud + open-questions corrected (the chain was filed as deferred and mis-attributed to 'the 3D marker'); TWO PNAME consumers now distinguished (this plate, and CameraShipHUDRenderable's ranking window which shipped 2026-07-18); new bgf-format section on the plate atlas. Rig-verified: 2-node relay, BT_GOTO=enemy -> '[hud] name plate: bmp=2 tex=1' and 'Boreas' rendered under the crosshair on the locked target. Solo clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
b7107b1020 |
Gitea #43: the Comm pilot list now shows EVERY pilot -- three stacked fixes
1. Roster count used the binary's raw entry+0x29&0x40 read on our compiled objects (databinding trap) -> garbage latched pilotCount=1. Decoded: the flag is Player::NonScoringPlayerFlag (bit 14 = Entity::NextBit); both loops now use IsScoringPlayer(). Also closes the pilotIDs[1] overrun. 2. The build-once latch races async replicant arrival (rig-proven 1-then-2): rebuild on scoring-census change [T3 accommodation, demand-latch precedent]; also un-dangles departed peers. Forensic: '[score] pilot roster built: N'. 3. Name icons were a NULL stub + zeros blank authentically -> rows invisible. Wired BTPilotNameBitmap (playerBitmapIndex -> Mission small callsign raster, the radar-label chain); replaces the raw pilot+0x1e0 key. Rig-verified: Aeolus + Boreas rows with callsigns on both nodes incl. the staggered-start race; solo unregressed (1 scoring pilot). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
7494e2ec6c |
KB: pilotList MP claim corrected -- only the local row renders live (Gitea #43, rig-reproduced)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
ef5a15ae2c |
KB: late-join make-sync gap + revolving-door plan digest (multiplayer, open-questions)
The 1995 engine tolerates late HOST attach but never finished late ENTITY sync (NotifyOfReplacementEntityCreation = Fail stub, makes broadcast-once). Routes to docs/REVOLVING_DOOR_PLAN.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg |
||
|
|
da34e2d8ab | KB: remote control channel + the interest-teardown audio crash (multiplayer.md) | ||
|
|
a235e2464d | KB convention: bindings.txt grammar is a compatibility surface (player customizations must survive upgrades) | ||
|
|
85ac54fcee | KB gotcha: dropped mid-sequence call passes first-use tests (issue #42 lesson) | ||
|
|
72a8b124f9 | KB: direct-axis release-edge gotcha (issue #36) | ||
|
|
0d1507027c | KB: relaunch-storm + static-seat digest (multiplayer.md, issues #33/#34) | ||
|
|
c2ee7299b2 |
Generic joystick / HOTAS / pedals support: DirectInput 8 layer + capture wizard
Tester ask: configure non-Xbox controllers. New L4JOY layer (DI8): enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling with range normalization, reacquire-on-loss, 3s hot-plug re-enum. XInput devices are excluded via the documented RawInput IG_ VID/PID check (live-verified skipping a real XBOX360 pad -- no double-feed). bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/ deadzone; hats as 4-direction buttons with diagonal chords). PadRIO runs them through the existing channel/event machinery -- per-binding edge arrays release automatically on detach (the #24 rule), and a direct throttle axis maps full lever travel onto the 0..1 channel while the gait detent still snaps a lever parked in the walk/run dead band. BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move each control when prompted (stick/twist/throttle/fire), invert derived from the move direction, hat look cluster auto-added, output written between markers in bindings.txt with the rest of the file preserved. Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched. Verified: grammar accept/reject per line, XInput-exclusion live, 30s in-mission no-device polling clean. Awaiting a tester with real stick hardware for axis verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
86e387b6c7 |
Valve forensics + KB: the boost mechanic is real; the field cook-off was a detent question
Measured A/B/C (Blackhawk, sustained autofire, 150s soaks): SRM6 on
Condenser2 plateaus ~531 and COOLS at share 0.91 (boost 2), climbs to
the jam band at balanced (0.167), and runs at the 2000 failure line
starved (0.018). MoveValve reruns the redistribute every press;
veteran passes the novice guard. The field 'boosted loop 2 still
cooked' therefore means the valve was NOT at the 50 detent during the
fight -- the 1->5->50->0 cycle turns the loop OFF one press past max.
MoveValve presses now log always-on ('[valve] <name> -> detent N' +
a CLOSED warning) so the next field session records the detents.
Also: solo mission clock verified live (60s egg self-ends,
'[mission] solo game clock expired' -> RunMissions returns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9b0f571d16 |
KB: state-7 (heat-failed) weapons go dark with no lamp -- authentic (#30 remainder closed)
Field capture 2026-07-23: right SRM6 tripped FailureHeat and went dark with no indication -- reported as a suspected bug. Decomp @004c9a38: the gauge cluster's only weapon-state read is ==5 (jam lamp); state 7 has no presentation anywhere. EJECT tap revives it while rounds remain. (Same session: first live verification of the #31 eject wiring -- jammed left SRM6 cleared by a tap.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2edd1b5363 |
Issue #31: wire the EJECT button (EjectAmmo msg 0xb @004bb9b8) -- the in-mission unjam
Disasm-faithful transcription of the export-gap handler onto ProjectileWeapon (MESSAGE_ENTRY id 0xb, the #19 pattern). PRESS arms the ~3s UpdateEject countdown (bin alarm -> Ejecting(3); gate 2 parks the weapon offline mid-eject -- authentic). HOLD to completion = DumpAmmo jettisons the bay -> NoAmmo. TAP (release early) cycles ONE round out via FeedAmmo and returns the weapon to Loading -- clears a jam at the cost of a round (and recovers a FailureHeat 7 while rounds remain). Binary structure kept exactly incl. the press-no-bin fall-through into the release block. AmmoBin: Ejecting(3)/Ejected(4) alarm levels decoded + named SetAmmoState accessor (databinding rule). Always-on forensics: '[weap] EJECT tap' + '[ammo] bay DUMPED overboard (N rounds)'. Rig-verified live (BT_EJECTTEST=1 / =hold on LRM15): taps eject one round each and return the weapon to service (15->8 across cycles); hold dumps all 16 -> NoAmmo. Awaiting a human press of the actual ENG-page button on a jammed weapon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
225fe43f0c |
CORRECTION: EJECT tap IS the in-mission unjam (EjectAmmo @004bb9b8 decoded)
Old-timer testimony (tap = eject one round, hold = eject the bay, eject clears a jam) checked against the binary: raw disasm of the export-gap handler @004bb9b8 confirms it byte-for-byte. PRESS arms the ~3s UpdateEject countdown (hold-to-completion = DumpAmmo whole bay -> NoAmmo); RELEASE before completion (TAP) ejects ONE round (@004bd4f4) and, with ammo remaining, sets weaponAlarm 3 + restarts the reload -- the weapon returns to service. A tap even recovers a FailureHeat level-7 launcher while rounds remain (empty-bin gate 2 re-pins 7 only when the bay is dry). The earlier 'jams are mission-permanent / no unjam exists' claim read only the HOLD path -- corrected + swept: projweap.cpp case-5/jam-log/ CheckForJam comments, players/README.txt, decomp-reference.md (message-tables entry), open-questions.md (state-7 recovery note). Gitea #31 updated; wiring the handler now restores the pod's actual jam-recovery mechanic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a999e5c49f |
Audio-dropout fix: OpenAL source-pool lifecycle (uninit aliasing, atomic-delete leak, stranded partials)
Field report: audio cutting in and out toward the end of the match. Three structural defects in the port's source-pool lifecycle: 1. SourceSet.sources[] was uninitialized heap garbage until first acquisition -- AL names are small ints, so a recycled-heap slot could alias a LIVE source owned by another sound; alIsSource() skipped generation, two sounds shared one source, and whichever released first deleted the other's mid-play. SourceSet ctor now zero-inits. 2. ReleaseSourceSet's bulk alDeleteSources(count, sources) is ATOMIC on invalid names (AL spec) -- one empty/-1 slot and NOTHING deleted; after the first pool exhaustion every partial release leaked and the pool never recovered. Now per-slot guarded delete, slots parked at 0. 3. Failed acquisitions stranded partial sets forever (dropped transients are never released by anything). ReleaseChannels() handback at both failure sites (StartRequest + dormant-resume) + a dtor backstop. Verified: 2-node MP smoke (mission + lobby-loop relaunch clean, census live/acquireFails 0/0) + 100s solo combat smoke (52 audio triggers, zero AL errors). Awaiting live playtest confirmation at scale. KB: context/wintesla-port.md audio-dropout section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
12a1fea823 |
Score replication: peers' scores now reach every pod (rank fix)
Field report (playtest night, voice channel): everyone's HUD showed them in 1st place. Root cause: the engine's score-replication machinery was complete -- Player::WriteUpdateRecord ships currentScore, the replicant ReadUpdateRecord applies it, CalcRanking sums every scoring player -- but nothing marked the player's update record dirty on a score change (the engine only ForceUpdates at mission end for the fade sync). Every peer's replicant player therefore stayed at score 0 and each machine ranked its own (only-nonzero) player first. Fix: ForceUpdate() after every score application in the BTPlayer handlers (main, inflicted, and the severed-vehicle branch) -- the existing update-record path does the rest. Verified 2-node (force-damage kill scoring): the victim's machine ranks the killer's REPLICANT at its replicated score (24) above its own master (0); standings consistent on both nodes. BT_RANK_LOG=1 kept as the 1 Hz per-player ranking dump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c8cba8c764 |
Connection audit: every connect/disconnect scenario handled + verified
Systematic actor-death x lifecycle-phase sweep (operator request after the WaitingForEgg zombie). New handling, all live-verified: 1. PRE-EGG console-pad loss (the real zombie phase): before the egg the pod's only link is the console pad -- the game socket doesn't exist, so the earlier RelayGameDown hook could never fire (the verification run itself caught this). Detection now lives in HostDisconnectedMessageHandler ConsoleHostType (relayMode + pre-scene -> BTRelayRejoinNow); mesh keeps 1995 behavior. 2. MID-MISSION relay loss: the STOP is relay-sent (1995: the console owned the clock), so a pod losing its relay mid-match played a peer-less FOREVER-mission. RelayGameDown (scene presented) now posts StopMissionMessage at +15s -> normal end -> lobby relaunch. 3. POST-RELEASE pod death stalled the round (survivors wait forever on the dead peer's connection gate): relay _abort_round -> route -11 REJOIN to survivors -> everyone re-seats in seconds (reset-first ordering makes the drop cascade re-trigger-proof). 4. BTRelayRejoinNow carries BT_SEAT_CLAIM (mirrored tag): without it the rejoiner's own reclaim-hold blocked assignment -> ROSTER FULL loop for the 90s window (found by verification round 2). 5. Beacon NAT keepalive (SIO_KEEPALIVE_VALS 60s/10s) so long between-rounds waits can't be silently unseated. 6. BT_LOG_APPEND=1 preserves the log across lobby-loop generations (truncation erased round-1 verification evidence). Verified non-issues: relay pods never bind the -net listener (no double-launch collision on the internet path); mesh bind-fail exits cleanly. Full matrix + evidence: context/multiplayer.md; scratchpad/audit_verify.sh is the repeatable rig. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
56ca2f5c13 |
Mission loads 72s -> 4s: the wait screen was GPU-syncing every idle frame
The operator challenged the "inevitable ~minute" load (Task Manager showed idle CPU) and was right. Measured chain: - A/B same box/egg: dev 7s, +glass 30s, +BT_DEV_GAUGES 72s+. - Drain census: identical ~560-event make streams; 190 events/s in dev vs a metronomic 7/s in the console shape -- the loop, not the events. - Stack sampling: 4/6 samples inside ExecuteIdle's wait-screen paint -- GDI GetDC/FillRect on the D3DPOOL_DEFAULT offscreen surface forces a full GPU pipeline sync (~140ms/frame at glass window size). The 2026-07-22 flicker fix traded a cosmetic bug for a 25x load throttle, worst on the operator box (bigger window + gauge-window GPU work), with idle CPU because a GPU-sync stall isn't compute. Fix (ExecuteIdle): the staging surface is D3DPOOL_SYSTEMMEM (GetDC is pure CPU; survives Reset for free) uploaded via UpdateSurface, and a change gate skips the whole idle frame unless the spinner phase (12.5Hz) or state text changed -- between paints the loop runs free. Result: console-shape load 72s+ -> 4.1s (BEGIN -> READY), drain at 345 events/s; every pod benefits (same code), and the all-ready launch gate now waits on a seconds-scale slowest loader. Permanent pre-run diagnostics kept: the drain census + >50ms slow-event lines (EVENT.cpp, stand down at scene-live; slow-event identity captured BEFORE Process -- Receiver::Receive(Event*) deletes the event). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
02a41f165f |
Issue #27: projectiles carry the weapon's real damage type (ballistic)
Playtest matchlog forensics: across 2,591 applied-damage events in two rounds, Ballistic (type 1) damage NEVER appeared -- Collision/Explosive/ Laser/Energy only -- despite 136 projectile impacts and autocannon/Gauss mechs. The weapon parses its authentic damageData.damageType correctly (mechweap.cpp:316 maps "BallisticDamage" -> 1), but BTPushProjectile never carried the type and every projectile-impact site in mech4.cpp hardcoded ExplosiveDamageType. So autocannon/Gauss rounds were scaled against armor by damageScale[Explosive] instead of damageScale[Ballistic] (the model indexes a distinct scale cell per damage type) -- every ballistic weapon did the wrong damage all night. Lasers/PPCs were unaffected (their SendDamageMessage path carries the weapon's own damageData); missiles happen to BE Explosive so only autocannon & Gauss were mis-scaled. BTProjectile gains a damageType field, threaded from the firing weapon (damageData.damageType) at launch through to the impact dispatch; missiles resolve Explosive from their own streamed type, autocannon/ Gauss now resolve Ballistic. The missile-splash template stays Explosive. Enum is anonymous so the field casts to Enumeration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
44f7c39ee1 |
Issue #24: release held pad buttons on controller disconnect (look latch)
XInput button emission is edge-based against previousPadButtons; the
disconnect path zeroed that memory WITHOUT emitting release edges, so
any button held when a controller (wrapper) died stayed pressed
game-side forever -- and the zeroing also erased what a reconnect would
have released. Field signature (playtest night): a PS3-pad-in-xbox-
mode wrapper died mid-hat-hold -> the look-view machine (hold-based,
mppr) latched a side view with the authored look pitch ("stuck side
view + pitch at ground, no recovery"); the keyboard-only run was clean.
On the connected->disconnected transition PadRIO now emits
EmitButton(address, 0) for every binding whose mask was held, then
forgets. Not reproducible headless (needs a real hardware unplug
mid-hold) -- awaiting the reporter's controller for live verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5f115eca19 |
Issue #22: respawn resets ammo + repairs weapons (the +0x28 mislabel)
ROOT CAUSE [T1]: Mech::Reset's subsystem sweep (@0049fb74 loop-2) calls
vtable slot +0x28, which the DATA-section vtables prove is
ResetToInitialState (0050f9d8/00511d2c/00512078/0051242c all carry the
class RTIS at +0x28) -- our reconstruction mislabeled it DeathReset,
the EMPTY engine base virtual, so the whole per-subsystem reset was a
silent no-op ("ammo and weapons do not reset on respawn", playtest
night; matchlogs: 3 of 7 players fired zero projectiles after their
first death).
SECOND LAYER: the binary never refilled ammo at all -- its respawn
severed the vehicle and DropZoneReply built a NEW mech (fresh ctor =
full bins, pristine subsystems: the pod behavior testers remember).
Our port deliberately reuses the entity (the new-mech path was the
two-mech/camera-glitch family), so Reset now EMULATES ctor-freshness:
- the sweep starts at i=2 (binary-faithful: skips mapper + voltage
bus) and dispatches the engine DeathReset virtual as the vehicle;
per-class overrides chain each class's own authentic slot-10 RTIS
body (the recon RTIS namesakes are statically bound -- no common
virtual): MechWeapon @004b96ec, Emitter @004ba4d0 (charge cycle
restarts from Loading), ProjectileWeapon @004bbaf8 (unjams).
- MechSubsystem::RespawnRepair (PORT): heals the subsystem's PRIVATE
crit damage zone + status alarm -- the mech-zone heal never touched
those, which kept destroyed weapons dead across respawns.
- AmmoBin::DeathReset (PORT): refills to initialAmmoCount (@0x220, the
ctor value) + Loaded alarm + idle feed + cook-off disarmed;
ResetToInitialState stays authentically EMPTY per AMMOBIN.TCP.
Verified (2-node force-damage kill/respawn rig, BT_DEATH_LOG): every
respawn logs [respawn] ammo bin refills for both bins to streamed
capacity across 4 death cycles; mission un-regressed. KB corrected +
swept (multiplayer.md task #52 notes). Awaiting live tester
verification of depleted-bin refill + revived weapons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
8368163b04 |
Lobby loop: auto-rejoin between rounds + seat reclaim + live mission edits
The between-rounds arcade flow (first-playtest-night field request): at
mission end every pod exited, seats dropped, and every round required
everyone to re-run join.bat with no way to adjust the mission.
1. AUTO-REJOIN: StopMission (operator END / mission clock -- NOT a
window close) sets gBTMissionStoppedByConsole; after the matchlog
upload btl4main relaunches the pod into the join wait with the same
cmdline (BT_CALLSIGN/BT_MECH ride the inherited environment).
2. SEAT RECLAIM: the assigned tag is mirrored file-scope
(BTRelaySelfTag) and re-presented as the 3rd SEAT_REQUEST payload
field (BT_SEAT_CLAIM); the relay holds a dropped seat for its tag
for 90s and a returning player gets their EXACT seat back -- static
ordering across rounds, the real-pod model. Non-claim joiners skip
reclaim-held seats.
3. LIVE MISSION EDITS: the console gains "Apply mission settings"
(enabled while a session runs) writing arena/time/weather/length
into the session egg; the relay re-reads the file at round reset and
egg release (roster shape locked mid-session).
Verified 2-pod 3-round: pods self-relaunched after each clock end; the
SECOND-to-rejoin still reclaimed its original seat (ordering held); the
between-rounds edit landed ("mission length now 75s"). Known edge
noted in the KB: a stale ready flag from a not-yet-exited old conn can
satisfy the launch gate mid-rejoin -- operators launch on green dots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7b3e76db38 |
Load-time attribution: the minute is the renderers' entity-make streams
Measured (loadphase markers + queue blocker dump): renderer LoadMission 31ms, model loads 0.2s -- the teal->green minute is the LoadingMission ready-gate (IsPriorityEmpty counts timed events too) waiting out the THREE renderers' entity-make streams: every entity/subsystem creation posts message 3 to each Renderer receiver (classIDs 8/30/36 = video/audio/gauge), hundreds deep per queue, draining across the whole window (the audio renderer's share is why sound starts mid-load). Instrumentation kept: [loadphase] markers (renderer span + busy-pass counter) always on; GeneralEventQueue::DumpBlockers + the class-named DumpData fallback behind BT_LOAD_DIAG=1 (hundreds of lines per load). Optimizing the drain (batching/budget) is an open perf item -- deliberately untouched before the 8-player night. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9bec4b2050 |
READY light: pods report load-complete to the operator roster
The roster showed "registered" for a still-loading pod and a ready one alike -- with multi-minute contended loads the operator couldn't tell who they'd be waiting on. When the app ladder reaches WaitingForLaunch the pod sends one empty route -10 frame on its live relay game socket (BTRelayNotifyReady; socket mirrored at HELLO); the relay announces SEAT n READY (k/m loaded) and the console GUI lights the seat bright-green with a "N pod(s) LOADED+READY" banner. Verified LIVE by the operator: blue (seated) -> teal (registered) -> green (ready) in order, twice. Also resolved the load-speed mystery: loads are ~30s uncontended; the 1-2+ minute cases were assistant background test runs competing with the user's interactive sessions on the same machine (memory note added -- no game-spawning tests while the user is testing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
53c86fa1dd |
Launch pend counter: no timeout, no queue entry (mid-load launch crash 2)
The take-1 deferral (re-post at +1s, capped at 120) turned a SLOW load into a crash: loads on a busy operator box measure 1-2+ minutes, the cap expired, and the original "Not ready to run" Fail/abort fired (field report: sound after ~1 min, crash ~2 min later; the log shows exactly 120 deferrals all in LoadingMission). A mid-load RunMission now just bumps gBTRunMissionPendCount -- nothing enters the event queue and there is NO timeout to outlive -- and CheckLoadMessageHandler drains the counter immediately after advancing LoadingMission -> WaitingForLaunch, re-posting that many stack RunMissionMessages (the engine's own no-console launch pattern at the same site, which also demonstrates Post spools stack messages safely). Verified (two-round rig, round 1): both relay launches pended through the load, fired at load completion, the requested mech spawned and the scene went live. Round reset + round-2 rejoin remain verified from the prior run; why loads take 1-2+ min on a busy box is left as an open perf question. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3a9b35fc06 |
Round reset + device-creation retry + load-time pump (rejoin trilogy)
1. ROUND RESET (the "picked Hellbringer, got a Blackhawk" field bug): trim/prefs finalize the session egg for ONE round, and that was permanent for the relay's lifetime -- a rejoin after a mission or crash got the stale finalized egg and its new callsign/mech request was recorded but never applied. _maybe_reset_round() (both drop paths) restores the original egg/roster/launch state once every pod is gone; live beacons re-key by tag position (conn.seat_tag). Verified two-round e2e: blkhawk mission -> exit -> RESET -> lok1 request -> egg rewritten -> the Hellbringer spawned. 2. DEVICE-CREATION RETRY: CreateDevice(HARDWARE_VERTEXPROCESSING) fails transiently when a just-exited instance still holds the adapter (caught live; also the parked "exit 139" join crash). The old double-failure path called PostQuitMessage(1) -- which does NOT stop execution -- and the NULL mDevice deref right after was the real crash. Now: HW->SW retry with 500ms backoff and a 20s deadline, then a clean error box + ExitProcess. 3. LOAD-TIME PUMP: mission load is thousands of back-to-back synchronous model loads; the starved message pump ghosted the window "Not Responding" mid-load. BTLoadPump() (hooked from d3d_OBJECT::LoadObject) pumps + repaints the wait screen at most every 150ms pre-run; no-op once the scene is live. Also: round-reset originals captured after the roster parse (the first cut crashed the relay constructor -- caught by the two-round test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
638cfca18d |
Defer RunMission until the mission load is ready (silent launch crash)
The relay fires the RunMission pair a fixed 20s after the egg ACK -- but the ACK happens at egg RECEIPT, and a long mission load (glass + dev gauges on a busy box measured ~50s) is still in LoadingMission when the launch lands. L4Application::RunMissionMessageHandler's default case Fail()ed on that state -- an abort() the 1995 code could afford because the real console launched operator-paced -- which presented as a SILENT crash at mission start (c0000409, no WER record; cdb stack: ucrtbase!abort <- RunMissionMessageHandler <- RoutePacket; 3/3 reproducible; matches the field report's 464 crash that died mid-LOD-load). The default case now DEFERS: re-post the message to ourselves at +1s (the VehicleDead re-post pattern) until the ladder reaches WaitingForLaunch, bounded at 120 deferrals so a truly wedged load still surfaces the original Fail. Verified: the crashing console-shaped run deferred 104x through its ~50s load, then launched, rendered (scene LIVE) and ran clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c421ec4af8 |
Wait screen: paint into the backbuffer, not after Present (load flicker)
The pre-mission overlay painted the window DC AFTER ExecuteIdle's Present -- every cycle flashed the black D3D frame before the GDI redraw, visible as flicker while the game loads (user report). The overlay now lands IN the presented frame: the backbuffer refuses GetDC on this driver (D3DERR_INVALIDCALL without LOCKABLE_BACKBUFFER, not worth forcing game-wide), so the text+spinner GDI-paint goes into a cached offscreen plain surface (GetDC always legal there) and StretchRects onto the backbuffer before Present. D3DPOOL_DEFAULT cache released at every device-Reset site; window-DC path remains for the pre-device seat wait and as the fallback; one-shot [waitscreen] boot log says which path is live. Verified: 14 rapid PrintWindow captures of the waiting state all carry the text (pre-fix runs alternated black frames). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
10eee05b20 |
Launch with whoever connects + live roster display in the console
The operator no longer has to match the roster row count to the exact player count (or watch the "empty seats will STALL" warning). Two mechanisms: 1. PRESENCE BEACONS: the pod keeps its seat-request TCP connection open for the process lifetime; the relay reads a live beacon as "seated" and a pre-claim FIN frees the seat (no ghost seats -- a ghost stalls every pod at the connection gate). Reservation expiry spares beaconed seats. 2. TRIM-ON-LAUNCH (manual/GUI mode): operator LAUNCH before the roster fills shrinks the session to the seats present -- egg [pilots]/pages trimmed, seat ids REMAPPED by position (pods re-derive their host id from their tag's roster position, so walk-up prefs and beacons re-key consistently), roster/expected_ids shrink, eggs release, the launch pair fires as ACKs land. GUI: roster rows now update LIVE from the walk-up requests (SEAT n PRESENT/FREED lines -> callsign/mech cells + a blue "seated" status light), and LAUNCH MISSION enables from the first seated player with a "starts with whoever is here" banner. Verified 2-node: 2-of-4 trim ran the mission with the requested mechs; and the mid-roster GAP case -- three joined, the middle player quit pre-launch (beacon FIN freed the seat), trim remapped seat 4->3 with the survivor's callsign/mech following, both survivors registered under the remapped ids and ran the mission. Also: stdin reader guarded against sys.stdin=None spawn shapes (the GUI QProcess pipe is the supported operator channel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fcedc046cf |
Walk-up callsign/mech request: join menu -> relay -> the session egg
The 1995 front-desk conversation, over the internet. join.bat/
join_lan.bat now open a JOIN-GAME menu (the FE in BT_FE_JOIN trim:
CALLSIGN edit + the 18-mech list + JOIN); the choice relaunches into the
normal join with BT_CALLSIGN/BT_MECH env, rides the relay SEAT_REQUEST
as {callsign NUL mech NUL} (empty payload = roster defaults, wire-
compatible), and the relay validates the mech tag, HOLDS egg delivery
until every pod's console pad is connected, rewrites the egg via
eggmodel (vehicle= + rasterized callsign bitmaps, graceful no-PySide6
fallback) and streams it to all pads -- so every player's egg copy
carries every player's callsign.
The hold is gated on CONSOLE-PAD count, not game-side registration: a
pod HELLOs only after parsing the egg's roster, so a registration gate
deadlocks (hit live in the first run; pods animate in WAITING FOR
MISSION ASSIGNMENT during the hold).
Verified 2-node e2e (env-injected requests): thor/vulture requested over
roster defaults bhk1/ava1 -> relay held 1/2, released at 2/2, rewrote
both seats (relay log), the delivered egg carries vehicle=thor/vulture +
name=VIPER/MONGOOSE, both pods launched and built [cyl] tables for both
requested mechs. Join-menu layout screenshot-verified; the menu's
click-through relaunch awaits live human verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
eb618fe9e6 |
BT_EXPIRE: tester builds stop working 14 days after build (compile gate)
Distributed test zips go stale and stray. BT_EXPIRE (CMake option,
DEFAULT ON so a zip can never accidentally ship without it) makes the
exe refuse to start BT_EXPIRE_DAYS (default 14) after its build day:
boot logs the remaining window, an expired build logs + shows a
"test build expired -- ask the operator for the current build" box and
exits. btversion.h now stamps BT_BUILD_UNIX (UTC-midnight-rounded so
the only-on-change header churns once per day, not every build) +
BT_BUILD_DATE. Dev is unaffected: every build re-stamps the day.
-DBT_EXPIRE=OFF builds a non-expiring exe; mkdist reads the cache and
marks such a zip "-noexpire" + prints a warning, and the tester README
expiry note is {EXPIRE}-marker-gated like the steam lines. Second
compile-gate exception after BT_STEAM (convention updated in
glass-cockpit.md). Quality gate, not DRM -- a clock rollback defeats it.
Verified live: default build logs "test build window: 14 day(s) left";
a -DBT_EXPIRE_DAYS=0 build logs TEST BUILD EXPIRED and parks on the
explanation box (window title checked) without starting the game;
restored to 14 and rebuilt clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
753540de96 |
MP match forensics: per-peer matchlog + relay auto-upload + matchcheck
For the 8-player playtest. Damage authority is VICTIM-side, so no single peer sees the whole match: every -net instance now writes a compact machine-parseable matchlog_<date>_<time>_<pid>.txt (auto-armed by -net; BT_MATCHLOG=1/0 overrides; solo silent), and at mission end a relay-mode pod dials the relay game port (the seat-request throwaway-dial pattern, envelope route -9) and streams the file back -- the relay saves every peer's copy under matchlogs/ on the operator's machine, so testers send nothing by hand. tools/matchcheck.py <dir> reconciles all of them: damage claimed (FIRE/PROJ/SPLASH/RAM, shooter-side) vs applied (DMG, victim-side authoritative), kill/death replication across observers, PLAYER_DEAD counts, scores, version skew, replicant-application and unattributed-damage anomalies, mid-mission disconnects. Hooks at the authoritative sites: Mech::TakeDamageMessageHandler (DMG, post-base, zone + post-application damageLevel), MechWeapon:: SendDamageMessage (FIRE), projectile impact/splash/collision dispatch (PROJ/SPLASH/RAM), wreck entry (DEATH -- a ONE-SHOT latch at MovementMode 9, NOT the transition: a replicant arrives at 9 straight from the type-6 record header and never runs the transition branch), BTPlayer vehicle/death/score handlers (VEHICLE/PLAYER_DEAD/SCORE), zone crit cascade (CRIT), APP.cpp RunningMission (MISSION), L4NET host handlers (PEER_UP/PEER_DOWN). Every line t=/w=/st=-stamped + fflushed. Verified 2-node: mesh run pairs A's 22 FIRE 1:1 with B's 22 DMG (same amounts, cylinder-resolved zones, ~200ms latency); force-damage kill run logs 103 DMG + 3 DEATH inst=M + 3 PLAYER_DEAD across three respawn cycles victim-side and the kill SCOREs shooter-side; relay-mode run auto-uploaded BOTH peers' files at the mission-clock stop and matchcheck produced the full report with exactly one (correct) anomaly -- the force hook's claim-less damage, the unattributed-damage detector working. Relay gained a route-specific 8MB cap for the upload frame (game frames stay capped at 1600). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3097d269b4 |
KB: menu-gate bullet updated for the glass desktop default
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e85f6dc56 |
Desktop default platform = GLASS (kills the recurring "old keymap" trap)
Root cause of the keymap whack-a-mole: three input layers exist (PadRIO/ bindings.txt under glass; btinput/CONTROLS.MAP under DEV; the bring-up drive bridge also under DEV, which clobbers e.g. Alt-reverse every frame), and DEV was the DEFAULT -- so every launcher that forgot BT_PLATFORM=glass booted the old stack: the play bats, then play_steam, then the operator console's local instances, each fixed with the same one line. Omission can't resurrect it now: with no BT_PLATFORM, desktop boots GLASS (and the menu on a zero-arg launch -- bare double-click of btl4.exe opens the mission menu). BT_PLATFORM=dev explicitly opts into the legacy DEV profile for debugging. BT_PLATFORM=pod is the cabinet profile; the real pod's SETENV.BAT must set it (contract recorded in glass-cockpit.md -- the old "bare boot == cabinet shape" menu-guard assumption is retired). Verified: no-env mission launch boots GLASS; bare no-env launch opens the MENU; BT_PLATFORM=dev boots DEV. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fe1cc0d79c |
docs/CREDITS.md: name the original 1995-96 VWE developers
Source archaeology (the per-file modification tables in the surviving headers) surfaced six programmer initials: JM, ECH, CPB, GDU, JMA, GAH. Read to VWE veterans on the project Discord (2026-07-21), VGL Lynx -- who watched the first multiplayer Thor walk terrain on a Red Planet map -- identified them: Jim Albertson, Eric Nichols (uncertain), Chris Brewer, Gabe Underwood, Joanna Mason (mapping inferred), Garth Hermanson; hardware initials MC = Marek Ciolek (AC unresolved), via Cyd. Team size per Lynx: dev ~8, art ~6, MUNGA by a smaller parallel internal team with crossovers, no outsourcing. Identifications marked T3 (oral history, as-typed spellings) against the T0 initials; a corrections invitation is included. Linked from context/project-overview.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
21b2bfc1c9 |
Issue #21: overcharge-bricked weapons -- latent ARCADE bug, deliberate divergence
The tester's ER small laser died permanently (recharge arc + ready dot dark, no fire) after idle seek-gear clicking. Root cause, byte-verified + headless- reproduced -- a latent bug in the SHIPPED ARCADE BINARY: - The charge integrates toward the GENERATOR voltage (@004ba838), not the gear target; "full" is only detected inside the +-1% snap window around seekVoltage[idx] (@004ba738), and overcharge reads ZERO (the byte-verified _DAT_004ba830 = 0.0 clamp). - Toggle seek while a charge is in flight and the level can land ABOVE the new gear's window: rechargeLevel pins to 0, the ==1.0 Loaded test can never fire, and charging continues to the generator ceiling where EVERY gear reads overcharged. Permanent brick; generator reselect cannot help. - The arcade dodged it by circumstance (locked 60 fps, rare seek use); the port's clickable seek button hits it in seconds of casual clicking. Fix (marked divergence in the Loading tick): overcharge counts as fully charged -> Loaded. The arcade's own discharge algebra says full == the gear's seek voltage, so an overcharged weapon IS full. Verified: the same 2.5-min BT_SEEKTEST abuse that bricked the laser pre-fix now fires 38 overcharge rescues and ends Loaded/pct=1. [seek] log now also prints the per-gear voltage table + gen voltages (diagnosis instrumentation). Ruled out on the way (documented in #21): overheat fuse (no firing occurred; a 4-min autofire+coolant-abuse run wouldn't fuse), generator detach (gen cycling + healthy siblings), seek-table corruption (table {6000,7000,8000, 9900} healthy). KB: decomp-reference.md records the arcade-latent-bug analysis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e9db161404 |
Issue #20: wire Mech BalanceCoolant (id 0x16) -- the auto-cooling-balance button
Third instance of the silent-swallow pattern (unregistered handler id ->
Receiver NullHandler). The Mech handler table @0x50BDF8 binds
{0x16, "BalanceCoolant" -> @0049f728}; the reconstruction never registered it.
Disasm-verified body (@0049f728): press-only guard (msg+0xc > 0; the binary
has NO novice lockout on this one), set EVERY condenser's valveState@0x1D0
to 1 (equal weights), then the shared redistribute @0049f788 -- which the
port ALREADY had faithfully as BTRecomputeCondenserValves (flow =
valve/total + condenserAlarm pulse; MoveValve calls it). So the fix is:
- mech.hpp/mech.cpp: BalanceCoolantMessageID=0x16 + handler + MESSAGE_ENTRY
- heatfamily_reslice.cpp: BTBalanceCondenserValves bridge (Condenser is a
complete type there; sets valves to 1 + redistributes)
- mech4.cpp: BT_BALTEST scripted harness (MoveValve press alternating with
a Balance press)
VERIFIED headless (BT_BALTEST=1 BT_VALVE_LOG=1):
boot -> all valves 1, flows 0.1667 each
MoveValve-> condenser#1 valve 5, flows 0.5 / 0.1x5 (the priority boost)
BALANCE -> all valves 1, flows 0.1667 each (equalized)
Awaiting the tester's live ADV-mode confirmation.
KB: decomp-reference.md Mech-table row marked wired.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|