14ff3519ac80e5571cc1a0ce19784fa024b54259
354
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
14ff3519ac |
#83: the COLLISION DIVERT -- type-0 damage prices as internal rattle, never armor
The crash self-damage reconstruction (
|
||
|
|
9c83a468db |
#82 ROOT FIX: reconstruct the crash SELF-DAMAGE -- wall-grinding now hurts,
the knockdown storm self-limits, peers keep the limp
The 'peers see a limping mech skating' report decomposed into a wall-grind
KNOCKDOWN STORM: a gimped mech held against a blocking solid re-crashes each
time its gg cycle rebuilds speed (the gg ceiling is authentically the clip's
natural speed -- bhk1 ~14.9 u/s, ceiling+divisor @0x544/0x548, loader formula
byte-matched -- so iv2 ~222 >> the 40.0 threshold @0x4ab184), and every
type-5 KNOCKDOWN broadcast floors all peers' replicants into knockdown/trn
churn: gliding + leg-lifts = skating. Turning and crushable cars were
A/B-exonerated (grass circling: 198 replicant gimp entries, 17 crunches,
zero broadcasts).
What the binary has that the port lacked -- recovered from the EXPORT GAP by
raw disasm (scratchpad/night6/gap_4a9770.txt, @4aa89f-4aaab4) -- is the crash
branch's tail: fallDirection = worldToLocal(damageForce) (FUN_0040879c, M^T v,
un-normalized), fallScalar = -(fallDirection . localVelocity.linearMotion),
and a self-dispatched TakeDamageMessage{0x64, zone=-1, the engine-computed
collision Damage verbatim}. A wall crash COSTS ARMOR, so the grind degrades
the mech instead of looping forever. That dispatch was the long-standing
'DEFERRED' note in the response policy; now reconstructed.
Falsified en route (comments + KB corrected, do not revive): the stagger's
FUN_004a4c54(1)/(0x20) 'action-request flags' feed NO drive suppressor --
image-wide sweep + full gap disasm show word[this+0x18] is read only by the
net record emitter; the bmp clip applies NO root motion (adv=0 measured); the
'gimp cap 5.8' figure was the BACK-cycle stride printed under a misleading
label (now ggCapL/R; @0x52c has no binary consumer).
Verified (mp_gimpAB rigs): arena1 wall-grind now 4 bounded strikes, limper
dies of its crashes and respawns cleanly twice (START->RESET, no strand);
grass circling unchanged-clean (198 replicant gimp states, 0 knocks).
Rig: pid capture race fixed (a missed winpid left a stale node holding the
-net port -> null runs), per-config sweep added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4e89baeff3 |
bench parity: local nodes now launch exactly like a player's shortcut
Local benches had drifted from the shipped launchers and every divergence
cost a debugging session:
* no BT_START_INSIDE -> benches opened in the EXTERNAL CHASE camera while
every player starts in the COCKPIT. The first first-person look at a
bench window was misread as a broken HUD.
* novice bench eggs -> every SHIPPED egg is expert; novice gates off the
entire heat model, crits and jams, so bench combat was not field combat.
(Verified: the aligned node now boots experience=3 simLive=1
heatModelOn=1, where it used to boot 0/0/0.)
* 1-LP affinity pins -> starved the gauge executive and produced a false
"the comms panel never counts deaths" reading. Two LPs per node keeps
the documented single-box jitter fix without the starvation.
scratchpad/night6/bench_common.sh now owns the contract (bt_player_env,
bt_launch, bt_expert_egg, bt_novice_egg) copied verbatim from play_solo.bat,
with bt_assert_player_env warning if the shipped bat ever drifts. All three
4-node benches source it. The limp bench keeps a novice egg -- expert crits
the aimed leg and the mech turns-but-never-moves -- and now says so loudly.
Also: BT_SHOT_EVERY=<n> headless backbuffer capture (btl4vid.cpp), the tool
this was diagnosed with. It must sit above the warp phase-0 early-out since
that fn is the per-frame alpha-pass hook. OS screen-capture is off-limits:
a foreground-lock failure photographed the user's browser instead of the game.
KB: build-and-run.md gains the bench-parity rule + the capture diag;
cockpit-view.md gains the measured aspect/FOV finding (the surround's 2.05:1
view collapses vertical FOV to 31.5 deg vs the pod's 46.8, so fixed canopy
geometry covers 68.6% of the lower half -- NOT a regression: the shipped 643
binary reports identical geometry).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
82f1ea1183 |
bench: symbolic leg-zone targeting, gimp-follow/goto-release, legal egg colors
- BT_SELF_DAMAGE_ZONE=leg resolves the mech's actual leg zone via the streamed LegDamageZone flags (new Mech__DamageZone::IsLegZone) -- numeric zone ids are per-mech-type and a wrong one aims at a vital (killed the limp bench's mech instead of gimping it: 2 ticks, death cycle, respawn healed, harness spent -- 3 minutes of healthy circling). - BT_GOTO=gimp: beeline mode that prefers the nearest LIMPING peer (gimp half-states 3/4 ride the one-cell in every update record); stands and waits while nobody limps. BT_GOTO_RELEASE=1 hands the mech back to the keyboard once the beeline arrives. - MP.EGG/MP4.EGG/MP4L.EGG: color=Red and color=Blue are PATCH values, not colors (RES vehicletable colors: Black/Brown/Crimson/Green/Grey/Tan/ White) -- the two pilots carrying them rendered as untextured pastel mechs. Boreas Red->Crimson, Caicias Blue->Grey. MP4L.EGG = the 4-pilot bench egg on the open grass map. 4-node limp verification (user-eyeballed, all instances): the gimped mech step-drags with the authentic limp on every observer -- no skating (#82 holds in the 4-node overlap case). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
bfa1b04990 |
#81 follow-ups: peer warp must not stomp the POV vortex; panel draw probe; mech3 ghost symbols
Two field observations from watching the 4-node stress, both run down: 1. "A respawn happened without the blue vortex" -- REAL. The translocation effect is ONE global slot shared by the POV lifecycle and the world-anchored peer sphere (a port extension). A peer's un-wreck arriving while the local pilot's own collapse/wait/expand was in flight overwrote gWarpPhase/gWarpPOV and killed the POV vortex. Invisible before the respawn fix only because overlapping respawns barely existed; now they are routine. Fix: the POV lifecycle owns the slot -- BTStartWarpEffect self-skips while it is active ([tloc] peer warp SKIPPED). 2-node bench: 20/20 POV collapse+expand pairs, 9 peer spheres played, 11 correctly skipped; solo: 8/8 pairs unchanged. 2. "Comms panel counted no deaths" -- panel machinery CORRECT; the 4-node zeros were the bench's own CPU crush (4 core-pinned instances starved the PilotList to <0.6 Hz, so rows redrew minutes-stale). Added the arbiter: [score] panel DRAW slot/pilot/kills/deaths edge log (BT_SCORE_LOG) -- the 2-node rerun drew 0->9 / 0->11 live on both nodes, local AND replicated (SBMIRROR rows confirm owner->replicant tally flow on all 4 stress nodes). Also: the mech3 offline-authoring stubs declared every <Subsystem>::DefaultData as Entity__SharedData while the real statics are Simulation__SharedData (this engine derives Entity FROM Simulation) -- ~20 ghost symbols /FORCE silently resolved to garbage. DefaultData half fixed (SubsystemDefaultData now returns the true common base Simulation::SharedData); the CreateStreamedSubsystem stub signatures remain wrong (nested SubsystemResource* + ResourceFile*), are cold (no callers), and are tracked in open-questions + gotchas §6 stub-typedef corollary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a357dc4265 |
#81 GHOST MECH FIXED: release the death latch + stop the duplicate VehicleDead
Two defects that were masking each other, both now fixed and benched.
1) THE LATCH NEVER RELEASED ON FAILURE. The binary's FUN_004c012c tail is
Post(...) ; *(this+0x290)=0 ; *(this+0x258)=0 (part_013.c:10519-10523).
We had the Post and the suppressConsole and were missing the middle
instruction, so deathPending cleared only on SUCCESS paths. One failed
respawn latched the pilot for the whole mission: every later death hit the
dedup and was SWALLOWED, so the cycle could never restart -- a transient
hiccup became a PERMANENT ghost (dead, un-Reset, still driveable, a burning
wreck on every peer that sinks after ~18s and can never be drawn again).
Binary evidence: +0x290 is written in exactly THREE places in all of
BTL4OPT.EXE (0x0b75fb, 0x0bffe3, 0x0c0a05) and all three store a ZEROED
register; there is no write of 1 -- or any non-zero, in any instruction form
-- anywhere. The dedup gate itself IS authentic (@004c05c4 does
mov edx,[ebx+0x290]; test edx,edx; jne ret), so it is kept.
2) VehicleDeadMessage WAS DISPATCHED TWICE PER DEATH. BTPostKillScore
(btplayer.cpp:2263) sent a second one "to credit a death", but that message
is the RESPAWN-CYCLE TRIGGER, not a scoreboard increment, and the tally is
already credited by the handler's ++deathTally (:538). Both fire inside the
same death transition (BTPostKillScore at mech4.cpp:2006, the hardened
authentic notify at :2110), so they were always paired. Removed; the kill
credit above it is untouched (it correctly uses a ScoreMessage).
THEY HID EACH OTHER: the duplicate made the latch look necessary, and the latch
made the duplicate invisible. Every "death ... SWALLOWED" warning in the field
logs was just the latch deduping our own duplicate -- 8 of 8 deaths, a 100% base
rate, which is exactly why it correlated with nothing when tested. Fixing
either alone makes things visibly worse (the first bench of fix 1 alone produced
a DOUBLE cycle: deathCount double-incremented, cycle 1's re-post gone stale and
tripping the drop-zone MISMATCH). That is why earlier passes at #57/#55 kept
adding clear-sites instead of finding the root; the binary broke the tie.
VERIFIED
solo: 17 consecutive death/respawn cycles, every one START->RESET,
0 swallowed / 0 mismatch / 0 crash. Pre-fix this strands permanently
after cycle 1.
MP : two nodes over a real network path with cross-machine drop-zone replies
(each node's request is answered by the OTHER machine) -- A 11 cycles,
B 12, 0 swallowed / 0 mismatch / 0 discarded / 0 crash.
harness: BT_SELF_DAMAGE_REPEAT=1 re-arms the self-damage bench after respawn
so multiple cycles can be driven (a one-death harness can never
exercise this fix). scratchpad/night6/mp_ghost.sh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
0dbdad17f3 |
#81 ghost: light the SILENT DISCARD -- the only path a respawn can vanish on
Walked the respawn handshake end to end and measured the assumptions. Two
eliminations and one find:
ELIMINATED: the engine hunt re-posts to itself every 2s forever while the mech
is dead (PLAYER.cpp:325-327; its only state-based exit never fires, see below),
and the DropZone re-grants the SAME slot to a repeat request from the same
requester+deathCount even while busy (DROPZONE.cpp:182-194). So neither a
transient "no slot" nor a single lost reply can strand a respawn -- the request
keeps being re-sent and re-granted. A permanent strand needs a permanent cause.
THE FIND: BTPlayer::DropZoneReplyMessageHandler ends
if (!playerVehicle) ... else if (deathCount == message->deathCount) ... else { return; }
and that last branch was a BARE RETURN WITH NO LOG. The drop zone grants a
spot, replies, the numbers disagree, the reply is dropped, deathPending stays
latched, the mech is never Reset -> permanent ghost, zero evidence. That is
why 6 of 8 field cycles stranded silently. Now always-on, printing the
DIRECTION of the mismatch: msgDeath < ours = genuinely stale (dropping is
right); msgDeath > ours = our counter is behind and we just threw away a LIVE
respawn -> points straight at #45 (the death tally does not replicate).
Deliberately NOT auto-recovered: guessing would be a stand-in, and the wrong
guess resets a mech that is still alive.
LANDMINE DOCUMENTED IN CODE: Set_Alarm_Level is an empty stub (btstubs.cpp:87),
so the death path's Set_Alarm_Level(this+0x2c,1) and the reply's (+0x2c,2) are
no-ops. Their values decode against Player's enum as DropZoneAcquiredState(1)
and VehicleTranslocatedState(2), which makes "these should obviously be
SetSimulationState() calls" both attractive and CATASTROPHIC: the engine hunt is
gated on GetSimulationState() != DropZoneAcquiredState, so setting 1 on death
would stop AssignDropZone ever being dispatched and ghost EVERY pilot. Measured
our simulationState at 0x24 (not 0x2c) and the write leaves it 0; in the binary
+0x2c is the Simulation-base alarm (the field the mech side calls graphicAlarm,
@0x4ac126 "owner alarm+0x2C -> level 9"), which our layout models only on Mech.
Verified solo: a healthy death+respawn logs the grant and the RESET and emits
ZERO discard/gate-off lines (no false positives).
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>
|
||
|
|
98907f45af |
hotfix: null-damageZone guard on the crit path (field crash, build 641, Conn Man's Owens)
First field session with real crits (#80) found a weapon subsystem with a NULL damageZone: Mech__DamageZone::CriticalHit -> ApplyDamageAndMeasure -> MechWeapon::TakeDamage +0xf (the engine base derefs the zone unguarded -- every 1995 subsystem shipped with one). Stack symbolized from the field log; the path was unreachable before tonight because ApplyDamageAndMeasure was a stub until #80. Guards at both choke points; a one-shot [crit] log NAMES the zoneless subsystem when hit so the root cause (build that subsystem's zone) can be fixed from the next field log. Owens crit bench: no crash, guard inert on zoned subsystems. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cfa52a28d8 | Merge remote-tracking branch 'origin/glass-cockpit-refit' | ||
|
|
5b19cd6c3f |
demo/bench harness polish: BT_SELF_DAMAGE_DELAY + two harness bug fixes + the OBS limp demo bat
- BT_SELF_DAMAGE_DELAY=<s>: hold the harness off for n seconds (capture demos: healthy walking first). Fixed its underflow bug (the countdown crossed below zero, which is also the "read the env" sentinel -- the delay re-armed forever). - The one-death latch was fed by EVERY mech's update: a mission that generated any destroyed mech entity tripped it at frame one and the harness silently never fired (random per mission roll). Now viewpoint-mech only. - run\limpdemo.cmd: the OBS capture demo -- 30 s healthy auto-walk, then the leg crosses half on the final damage tick: klaxon-klaxon-"reverse disabled" + the limp, then unlimited limp footage (waypoint 8000,8000). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
95cf49d1b6 |
gimped turn-in-place (#78 field find): arm the trn step in the gimp leg driver
Field test: a gimped mech pivoted as a rotating statue. The binary's turn dispatcher (FUN_004a9b5c, master perf) runs OUTSIDE the driver selection so it arms trn for gimped mechs too -- 71f4's case 4 exists to advance it. The port relocated that dispatcher into the NORMAL leg driver's Standing case, which stops running when the gimp driver takes over. Mirror the arming (same gates + lockstep body arm) into AdvanceLegAnimationGimp's Standing case. No gimp-turn clip exists in the RES -- a limping mech step-turns with the normal trn clip, authentic. Field-verified: stepping pivots both directions while gimped, heading sweeping, clean re-entry into the wgr/ggl limp on throttle-up. [T2] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
36f68718c2 |
the visible limp (#78): there was never a jump-jet clip set -- the 'Airborne' drivers ARE the gimp gait machines
The port had FUN_004a5bf8/71f4 fully reconstructed as AdvanceBody/Leg-
AnimationAirborne, gated on (MovementMode()==3||4) && jumpCapable@0x580 and
believed dead ("the test mech never jumps"). The binary says otherwise:
mech+0x40 in that gate is the graphicAlarm LEVEL (3=left-leg gimp, 4=right)
and +0x580 is hasGimpClips, set by the conditional loader block that probes
'wgl' and fills clip slots 22-27 (wgl/wgr/ggr/ggl/gsl/gsr) plus the four
measurements at 0x53c-0x548 (wg entry strides = speed caps, gg cycle strides).
Renamed the five jump* members + both drivers accordingly.
New: GimpBodyClipFinished @004a6344 / GimpLegClipFinished @004a7970 -- the
gimp transition machines, branched from the normal finished-callbacks. Phase-
correct limp entry (left-gimp enters 0x16/wgl only from a RIGHT step, right-
gimp 0x17/wgr from a LEFT step), gg cycles at gimp cadence, gs exits, and the
demand clamp to the gimped side's speed cap (leg cb writes it back into the
mapper -- the binary's authentic slowdown; the T3 x0.5 stand-in in mechmppr is
retired, BT_GIMP_SPEED now defaults 1.0). The binary's gimp machines have no
reverse entry -- the "reverse disabled" behavior is now binary-proven.
Reviving the dead drivers replayed two port-glue bugs (gotcha #24): the raw
*(controlSource) mapper read (null -> crash at first engagement) and the
missing alarm->member state re-sync (machine pinned in one run state). Both
fixed; bench harness gained BT_SELF_DAMAGE_TICKS=<n> to hold a zone past
LegHalfStructure without destroying it.
Bench-verified (madcat, novice, zone 16): crossing -> alarm 4 -> wgr entry
from a left step -> 8k+ frames stable in the ggl limp cycle at cadence 14.77
(vs 18.5 walk / 22+ run) with raw demand still 50. [T2]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
520f6eecd3 |
myomer damage reaches the wheels, legs gimp at half structure, reverse refuses -- and an ODR trap unmasked (#75/#78)
The speed-demand site (MechControlsMapper::InterpretControls) now applies the
drive scale the mover's feed roster applied in 1995: speedDemand multiplies by
the myomers' live speedEffect (gear ratio, thermal curve, 1 - zone damage, via
the BTMyomersDriveOf bridge) and, while GIMPED, by 0.5 [T3: VGL Lynx's
"roughly 50%", BT_GIMP_SPEED overrides]. The gimp states were already being
raised -- mechdmg sets graphicAlarm 3 (left) / 4 (right) when a LegDamageZone-
flagged zone crosses half structure -- but nothing downstream ever saw them.
While gimped, reverse input is refused ("reverse disabled"), matching the
old-timers' account; the pod's audio cue rides the alarm's watchers.
Bench, one trajectory, arithmetically exact: a deterministic right-leg ramp
(the new BT_SELF_DAMAGE_ZONE harness) crosses 0.5 and the demand goes
44.837 -> 6.726 = 44.837 x 0.5 (gimp) x 0.3 (a crit-chewed myomers from the
same ramp -- the #80 crits composing with #75's scale, unprompted).
The reason "nothing downstream ever saw them" is the real find of the night,
now gotcha #23: AlarmIndicator is typedef'd to DIFFERENT TYPES per header
family -- mech.hpp says ReconAlarm (4 bytes), heat.hpp says GaugeAlarm (0x54)
-- so Mech::graphicAlarm and EVERY member after it sit at different offsets
depending on a TU's include order. mechdmg wrote level 4 and read it back;
mechmppr read 0 from the same object, same expression. No compiler error can
catch it: each TU only ever sees one definition. Until the split is audited,
cross-TU reads of the gimp level go through BTMechGimpLevel (compiled in
mechdmg's TU) -- and the same split-brain explains why the port carries the
binary's ONE movementMode cell as two live members (engine simulationState vs
graphicAlarm level) that never meet.
Also landed en route: the Myomers un-powered self-repair observed healing in
the field logs at exactly 0.011 x the authored Explosive scale per tick --
the 2026-07-29 reversal confirmed live; and Mech message 0x15 "RealMaxSpeed"
raw-decoded (@0x49f604: sets mech+0x7a0 from the message unless the +0x7a4
latch holds -- a console-tunable top speed).
Open on #78, documented in locomotion.md: the Gimp animation clips (authored
keys in the binary's model-record parser; mech2's state enum vs mech3's
reverse-fix disagree about slots 0x12-0x17 -- reconcile before wiring) and
the audio-cue binding.
Diags: BT_SELF_DAMAGE_ZONE, BT_DRIVE_LOG, BT_GIMP_SPEED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8a99972f22 |
aimed fire strikes the part under the crosshair: the per-segment pick (#73)
The port's target pick was a whole-mech bounding-box slab test, and every hit -- aimed or not -- dispatched zone -1 into the victim's cylinder lottery. The recovered 1995 model (the division-card scene intersection) struck a SEGMENT and credited that segment's own damage zone. This is the port's equivalent. At render-tree build, each segment's draw object and its PrimaryDamageZone -- authored per segment in the skeleton stream, read by JMOVER.cpp:290 -- are recorded in MechRenderTree::segPick. The pick (BTL4VideoRenderer:: MechSegmentPick) ray-tests the per-segment bounding spheres on the live posed skeleton, using the draw-cached mLocalToWorld (at most one frame stale, fine for aiming). Selection is SPECIFICITY-FIRST: among the spheres the ray pierces, the smallest radius wins, normalized-distance tie-break. Both obvious rules were measured failing the same way before this one: the torso mesh's sphere (r~4.1 on the MadCat, vs shoulders at r~1.0) envelops nearly the whole mech, so its front face is nearest for any aim AND any near-body ray normalizes to ~0 against it. Limb spheres nest inside the envelope; smallest-pierced picks the most specific part on the aim line, and the torso wins only when no limb is threaded -- the per-part semantic the pod's mesh test produced. mech4.cpp tries the segment pick per candidate; the box PickRayHit survives only as the fallback (no tree yet, wrecked, spectator), still carrying zone -1 into the lottery, and a structure occlusion clears the zone. The winner's zone rides MECH_TARGET_SUBIDX + targetReticle.targetDamageZone into SendDamageMessage, so aimed hits now dispatch a real zone; the victim's handler applies it directly (bursts 2+ still re-lottery, authentic per the recovered @0x4a0230 loop). Bench, the same L/C/R sweep that exposed the bug: aiming left now lands 36/41 hits on zone 2 = jointlshoulder -- the left arm -- with a 0.30 thread score, where the same aim was a 6-way lottery spray before. The MadCat's authored segment->zone map is rich (shoulders 2/9, guns 6/17, hip 1, six leg zones, torso 0). Known approximations, flagged for field verification: sphere bounds rather than triangles, and a torso-envelope graze credits the torso where the pod's exact mesh test would have missed into air. The field protocol is the one the testers already ran on night 6: stationary mechs, short range, fire only at one arm -- the paper doll should now damage THAT arm. Diag: BT_PICK_LOG ([segpick] map at build, [pickwin] per pick). 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>
|
||
|
|
a5fb96ae96 |
the crit system, complete: two gap functions recovered, the dead sink revived, and a wrong verdict reversed (#80)
The whole critical-hit pipeline was dark, three layers deep, and one of those
layers had fooled us into a false conclusion about the 1995 binary itself.
LAYER 1 -- the trigger, recovered from the un-exported gap. The Mech MESSAGE
TABLE at 0x50bdf8 ({id, name, handler} rows) names the real
Mech::TakeDamageMessageHandler at 0x4a0230 -- message 0x12 "TakeDamage" --
plus seven sibling handlers (PlayerLink, RealMaxSpeed, BalanceCoolant,
Set/ClearBurningState, EjectPilot, DuckRequest). Inside it, the crit chance
at 0x4a0164: p = clamp(0.7 * damageLevel^2 + 0.01, 0..1), gated on the
player's simLive flag (+0x25c -- novice never crits), rolled PER BURST on the
current zone, skipping a zone already burning. Chance is ~1% on fresh armour,
~18% at half-stripped, ~58% at 90% -- crits arrive exactly as armour fails.
The handler's application loop replaces the engine base's single call, which
ignored burstCount entirely (multi-burst damage under-applied (burst-1)x).
Faithful shape: per burst, crit-roll -> CriticalHit @0049ccc4 (which routes
half the amount through the armour internally and picks ONE critical
subsystem by criticalWeight) else zone->TakeDamage -- then RE-RUN the
cylinder lottery from the impact point for the next burst, stopping early
once the mech is disabled. Multi-burst damage sprays across zones by design.
LAYER 2 -- the sink. MechSubsystem::TakeDamage was an empty btstubs stand-in;
the real body is at 0x4ac0bc (CLASSMAP had that address mislabeled
"HandleMessage"): zone damage, then on level >= 1.0 the Destroyed alarm, the
PrintState gate, the 1.0 pin, and -- for a vital subsystem -- the owner
mech's graphicAlarm to level 9, the same fall/death level the leg path
raises. That is the #28 vital-subsystem kill machinery, now real.
LAYER 3 -- the one that rewrites yesterday. The subsystem ctor DID copy
armour points + per-type scales into the private zone -- through the
ReconDamageZone PROXY, whose fields sit at struct offsets +4/+8, not the
binary's +0x140/+0x144. The floats landed on the engine object's header and
the real damageScale[] stayed zero. The 2026-07-28 experiment that "proved"
subsystem zones cannot be damaged -- and that the Myomers un-powered
self-repair was dead code in the original -- was measuring exactly this port
bug. Both verdicts reversed: the binary ctor (0x4ac7bb) initializes the zone
from the resource keys WeaponDamagePoints (required) + CriticalHitScoreBonus
(required) + Collision/Ballistic/Explosive/Laser/EnergyDamagePoints, none of
which the CSS parsed. Now parsed (with the binary's own error strings), and
the ctor writes the engine's NAMED members -- layout-parity holds, so they
land on +0x140/+0x144 faithfully. The Myomers repair branch is LIVE, in 1995
and here. KB corrected and swept (combat-damage, subsystems WAVE 6,
myomers.cpp, CLASSMAP).
Live-verified twice: [subarmor] prints real parsed scales for every subsystem
at spawn (HeatSink pts=10 scale=0.1x5, Condensers pts=5 scale=0.2x5, ...);
[critroll] landed full-chain crits in both runs (zone -> weighted pick ->
subsystem's own zone driven to 1.0 -> Destroyed); mech death/respawn and the
ammo gates un-regressed; zero crashes/asserts. Honest gaps: burst>1 spraying
is transcribed but not yet exercised live (self-damage fires burst=1), the
damageType==0 COLLISION divert (@0x49ffcc) is documented-not-reconstructed,
and the id-0x16 damage/kill report messages to the players (the authentic
stats plumbing, decoded to field level in the KB) are deferred to the #45
work.
Diags: BT_CRIT_LOG ([subarmor] + [critroll]), the existing BT_DMG_LOG.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4c7f6fd9b1 |
environ.ini: document BT_GLASS_LAYOUT in the shipped default
The default environ.ini template (BTWriteDefaultEnvironIni) now carries a commented BT_GLASS_LAYOUT block in the cockpit section, next to BT_GLASS_PANELS: off/save/load explained in player-facing terms, noting glass_layout.cfg lives beside the file and delete-to-reset. Ships commented out like every other option, so a fresh install still applies nothing. Verified: Release builds clean; the new text is embedded in btl4.exe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5fc969ae95 |
the log day now starts at 6am, because the playtesters are night owls
A midnight boundary splits one evening across two files at exactly the moment the session is most worth reading whole: the 01:30 crash lands in a different file from the 23:00 run that set it up. And a 02:00 session is "last night" to everyone who was in it, so filing it under the new calendar date reads wrong even when nothing goes bad. So shift the clock back 6h before taking the date for the FILENAME. The log day runs 06:00 -> 06:00 and one night stays in one file. Only the filename moves. The session header and the lastrun breadcrumb each call GetLocalTime separately (three distinct SYSTEMTIMEs in this function), so every timestamp a human reads is still true local time -- checked the scoping rather than assuming it, since sharing one variable would have silently backdated the header by six hours. FileTimeToSystemTime carries the month and year boundaries, verified against a scratch harness rather than reasoned about: 2026-07-28 20:00 -> solo_20260728.log one playtest night, 2026-07-28 23:59 -> solo_20260728.log start to finish, 2026-07-29 00:01 -> solo_20260728.log in a single file 2026-07-29 05:59 -> solo_20260728.log 2026-07-29 06:00 -> solo_20260729.log the boundary 2026-08-01 01:00 -> solo_20260731.log month 2027-01-01 03:00 -> solo_20261231.log year 2026-03-01 02:00 -> solo_20260228.log non-leap February Nothing in the tooling parses these filenames -- the operator app's own log is operator_relay.log and _collect_egg is mission settings, not logs -- so the console auto-transfer is unaffected. The player-facing text did need fixing though: the bats and README told players to send the log "dated today", which is now wrong for anyone who plays past midnight. They now say to take the NEWEST one and ignore the date, with a note on why. That was already the safer instruction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
819772f974 |
the rest of the Myomers wrapper: two live outputs recovered, and one branch that never fires
Follow-on to |
||
|
|
b70654dd11 |
torso twist died after a respawn: Myomers ran the inner integrator, never the wrapper
Gitea #70. Torso twist intermittently stops working after a death/respawn and stays stopped, with the generator at a full 10000V and the voltage link resolving fine. A stale state, not a live one. The Torso is a PowerWatcher that mirrors the electrical level of the subsystem it watches -- for the MadCat that is roster slot 15, Myomers -- and torso.cpp:570 holds effectiveTwistRate at 0 whenever that mirrored level is not Ready. So the question was never about the torso. It was: why does Myomers sit at NoVoltage forever? Because it never ran the state machine that would walk it back. The Myomers ctor @004b8fec stores [0x511620] into activePerformance, and that pointer resolves to 0x4b8b9c -- whose first instruction is `call 0x4b0bd0`, which is PoweredSubsystem::PoweredSubsystemSimulation. @004b8d18, the function this port had registered, is the INNER drive-heat integrator that wrapper goes on to run. We registered the inside of the onion. PoweredSubsystemSimulation is the only code that both enters NoVoltage (source == 0) and leaves it (NoVoltage -> Starting -> Ready), so a Myomers that lost power during the death/reset window had no way back out. Order is load-bearing: the base call precedes the heat-model gate in the binary. Gating first -- as this did -- also denied the electrical machine to every non-expert pilot, since OwnerAdvancedDamage() reads the +0x260 heat-model flag and that is off below veteran. Measured with BT_SELF_DAMAGE + BT_TORSO_LOG, before and after: pristine, before 48/48 Ready after 38/38 Ready post-respawn 95/97 Ready 89/89 Ready Two dead windows per respawn, now none, and the clean case stays clean. The wrapper has a tail this does not yet reconstruct: @004b8bb9-0x4b8c1e runs an effect when electricalStateAlarm == 1 and the myomer's damage zone is below 1.0, building a small vector (const 0xbc343958, about -0.011f) against the owner's localOrigin@0x100. Almost certainly the un-powered movement penalty. Flagged in the KB as T4 rather than guessed at. Worth generalizing, and noted as such in context/subsystems.md: when a subsystem's Performance address in a PTR_LAB_* slot does not match the function we reconstructed, suspect a wrapper that chains the base sim. The whole family does it -- Generator and PoweredSubsystem both lead with HeatSink::HeatSinkSimulation, and the Torso's own perf @004b5cf0 leads with UpdateWatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c5537a1247 |
BT_SELF_DAMAGE: kill your own pilot in solo, so the RESPAWN family is bench-testable at last
Nothing could kill the LOCAL pilot on a bench. BT_MP_FORCE_DMG only targets REPLICANTS (mech4.cpp:4884 skips anything else), a solo dummy never shoots back, and every other damage path needs a second live pod. So the entire respawn family -- #70 torso twist after respawn, #22 ammo/weapons not resetting, #55 coolant/heat/generator restore, #57 the respawn latch -- could only ever be tested by asking a playtester to die. That is why they are all still open or awaiting-verification. BT_SELF_DAMAGE=<amount/sec> dispatches an unaimed TakeDamage at our OWN mech through the same virtual Entity::Dispatch a real beam hit uses, so the cylinder hit-location lookup, the zone cascade, the vital-subsystem kill and the whole authentic death -> 5s -> DropZone -> Mech::Reset cycle all run for real. No state is poked. =60 kills a fresh MadCat in about a minute. It LATCHES OFF at the first death. That matters more than it sounds: live damage knocks the power bus down, which is exactly the signal a respawn test wants to read. The first run of this harness produced a torso electrical state flapping 4 <-> 1 and I nearly filed it as #70 -- it was the harness still shooting me. One death, then silence, then measure. FIRST RESULT -- #70 does NOT reproduce in solo, but something near it does. After the respawn the MadCat's torso recovers fully: rate back to 0.872665 (the authored 50 deg/s), elec=4 Ready. Twist is not broken by a respawn. However the post-respawn mission shows INTERMITTENT power dropouts that a pristine mission never shows: pristine control (no damage, no death): 48/48 samples elec=4, ZERO dips post-respawn (harness silent): 95/97 elec=4, 2 dips to elec=1 A dip zeroes effectiveTwistRate (torso.cpp:570), so a pilot mid-turn feels the twist cut out -- plausibly what "torso twist stops working after a respawn" actually is: not a permanent stop but a stutter. Both dips are transient and the tail of the run is steady, so it is not a stuck state. Cause not yet found; the watched subsystem's own electrical level (wElec) dips with it, while the generator holds a full 10000V against a 5000V brownout threshold -- so it is NOT the brownout path in PowerWatcher::UpdateWatch. Filed here as the next thread to pull, with the harness that makes it reproducible in one solo run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
034e55c7f5 |
prove generator FAILOVER, not just re-attach: BT_POWER_DETACH_TEST takes a subsystem name
The #62 verification so far only showed a subsystem detaching and re-attaching to the SAME generator it started on. That exercises the roster walk but never the case players actually hit: your generator dies and Auto has to find a DIFFERENT one. (Raised by the operator, who also pointed out solo has no way to damage a generator -- but it does not need one: a generator has its own on/off button.) BT_POWER_DETACH_TEST now accepts a NAME (=PPC_1, =ERSLaser_1, ...) instead of firing on whichever powered subsystem happens to tick first, so the scenario can be aimed at a real weapon. "1" keeps the old first-one behaviour. Verified end-to-end, everything through real paths -- the detach via DetachFromVoltageSource @004b0e30, the generator kill via its actual RIO button 0x1A through the click seam (EmitButton -> RIO queue -> manager drain -> Generator::ToggleGeneratorOnOff @004b1ed0), no state pokes: BT_POWER_DETACH_TEST=PPC_1 BT_BTNTEST=0x1a,300,320 BT_POWER_LOG=1 [power] TEST: detaching PPC_1 (forcing Auto) [power] AutoConnect RE-ATTACHED PPC_1 -> generator GeneratorA [btntest] PRESS 0x1a at poll 300 <- GeneratorA switched OFF [power] AutoConnect RE-ATTACHED PPC_1 -> generator GeneratorB So the hunt skips the dead generator (HasVoltage requires GeneratorStateOf()==2 plus real measured voltage, powersub.cpp:860-876) and finds a live one. Same result first observed on Avionics; the named form proves it on a WEAPON, which is the player-visible case. Second proof in the same output: EXACTLY ONE re-attach line, then silence. The hunt re-runs every frame while `mode==Auto && !HasVoltage()`, so an attached-but-dark weapon (the #21 symptom) would spam that line forever. One line then quiet means GeneratorB is really supplying it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
901cf1b459 |
four ENG-page lamps were invisible, not missing: the ctors dropped their colour parameters
Playtest report: "generators don't seem to have an Auto setting" / "no auto mode is ever displayed", plus the operator's own memory that the BUS MODE button once stepped through three states. The state machine was NEVER wrong: @004b0abc has exactly two branches (<2 -> Auto(2); ==2 -> detach + Off(0)), Manual(1) is set only by the four SelectGenerator buttons, and the auto-hunt gates on ==2 -- all byte-verified, and the #62 re-attach fires live. Manual is a one-way door out of BUS MODE by design: a single cycle button cannot know WHICH generator manual should mean. Verified on-screen by the operator this session: A-D returns to manual, BUS MODE toggles auto/off thereafter. What was actually broken: the connect-mode lamp -- and three siblings -- never drew a pixel. OneOfSeveralStates (@004c5470) and OneOfSeveralInt (@004c5148) forward caller-supplied background/foreground colours in the binary; the recon dropped both parameters and hardcoded 0,0 into the base. A BitMap-strip lamp draws SetColor(bg) + DrawBitMapOpaque(fg,...), so 0/0 painted colour-0 on colour-0: invisible. Affected: btemode (connect mode, 1x3 -- THE report), btecmode (coolant on/off, 1x2), and both bteseek gear-step lamps (1x4). The cluster call sites pass 0xff/0, byte-verified (@004c866c disasm; part_014.c:1479-1481, :2146-2147). Same dropped-element family as issue #42's MoveToAbsolute. THE TRAP THAT WAS NOT LANDED, recorded so it stays unlanded: with the lamp first made visible, the frames appeared inverted against the levels (art reads AUTO/MANUAL/OFF top-down; levels run OFF/MANUAL/AUTO), and a row=(rows-1)-selected "fix" was proposed. An adversarial workflow proved it wrong: the gauge blit addresses SOURCE rows BOTTOM-UP (Video16BitBuffered::DrawBitMapOpaque, L4VB16.cpp:3846-3850 -- sTop = map_max_y - sTop, rows walked upward), the 1995 blit @0046bdfc performs the identical flip, and the vertical strips are AUTHORED bottom-up to match. Identity level->row therefore draws the pod-correct display; the inversion would have created the very bug it claimed to cure. Two of five investigators (and the first human pass) assumed top-down; the engine source overruled all three. Convention + warning now recorded in context/gauges-hud.md. btecmode doubles as the standing tripwire: a second vertical strip that must show ON when coolant is available -- if it ever reads inverted, the bottom-up verdict is falsified. Also corrected: @004c552c is OneOfSeveralStates' EXECUTE override (clamp >= 0, chain the base draw) -- the port had the body on BecameActive under a wrong label; vtable-diffed against OneOfSeveral (0x518b24 vs 0x518bf0). BecameActive is inherited. Behaviorally inert today (the state source never goes negative), byte-faithful now. README: the "KNOWN ISSUE -- automatic re-attach is not working yet" text is replaced with how power routing actually works (A-D = manual, BUS MODE = auto then off, two presses off / one back to auto). That text shipped in 600, which already contained the #62 fix -- players were being TOLD auto was broken while it worked, which is half of how a painted-out lamp became "no auto mode". Diagnostic kept: BT_GENSEL_TEST=<id> now drives any of the five power-routing message ids (4..8, default 7) and pulses four times, so the whole mode cycle is observable headlessly under BT_FIRE_LOG ([gensel] lines). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c32d02b3cc |
the heat schematic was lit at every spawn: a dropped x87 tail, and the last #47 derivation
Playtest night 5: "heat damage @ launch is lit up like a Christmas tree",
confirmed by two players at ANY spawn -- so not the respawn-reset bug it was
first filed as. Two stacked causes, both fixed here.
1. THE RATIO WAS NEVER RECONSTRUCTED. HeatConnection::Transfer @004c3720 sets
the colour index a ColorMapper pushes into the palette slot. The port wrote
*currentColorIndex = HeatRound(heat->currentTemperature);
straight into an index whose range is 0..99. A stone-cold mech sits at ~77
and the generators idle near 260, so every one of the 24 cmHeat mappers in
GAUGE/L4GAUGE.CFG (GeneratorA-D, Condenser1-6, HUD, Avionics, Gyroscope,
Torso, GAUSS, the lasers, SRM6, Myomers) saturated at the hot end from the
first frame and stayed there.
The real computation was INVISIBLE in the decomp. Ghidra renders the tail as
`uVar1 = FUN_004dcd94();` -- an arg-less __ftol, exactly the carve artifact
reconstruction-gotchas §19 documents: the x87 expression that left the value
in ST0 is dropped from the export. The previous author reconstructed the
only thing visible and flagged the scaling as unreconciled in a comment.
Raw disasm @0x4c379a-0x4c37c1 recovers it:
fld [num] ; fdiv [den] ; ratio
fcomp 1.0f ; jbe -> ratio=1 ; clamp
fmul 99.0f ; call __ftol ; -> 0..99
i.e. "how close to failure am I", not a raw temperature. At spawn that is
77/2000 -> 4, and the schematic reads cold.
The two operands come from one of two branches, chosen by a class flag at
+0x14 that the port did not model at all (ctor disasm @0x4c3682-0x4c36c2):
HeatableSubsystem (0x50e3ec) -> own temp@0x114 / own failureTemperature@0x11C
HeatWatcher (0x50e604) -> WATCHED subsystem's temp@0x114
/ the WATCHER's failureTemperature@0x124
neither -> source NULLed, Transfer writes 100
Note the watcher asymmetry: temperature from the watched subsystem, reference
from the watcher itself. Bridged as BTHeatWatcherSample so the gauge TU need
not include the heat family's headers.
2. THE WATCHER BRANCH COULD NEVER BE SELECTED. With (1) fixed, HUD, Gyroscope
and Torso still pinned at 99 reading temp=1.6369e-35 failAt=0 -- uninitialised
bytes. HeatWatcher's C++ base had been re-based to MechSubsystem, but its
DERIVATION chain still said HeatableSubsystem, so every watcher answered "yes,
I am heat-bearing", took branch one, and read its own watchedLink@0x114 as a
temperature. This is the last surviving instance of the #47 bug -- in the
file that fix's own comment cites as already correct. The two families are
disjoint in the binary and the branch test depends on it.
VERIFIED LIVE (solo, F6 to the Heat schematic, BT_HEATGAUGE_LOG=1): mode mask
reached 0x510421 (ModeSecondaryHeat) and the gauge tracks per subsystem rather
than saturating -- GeneratorA 259.5->13, Condenser3 202.0->10, SRM6 182.8->9,
lasers 90-97->4-5, and AmmoBinSRM6_1 (a watcher) resolves its link and reads
182.7->9 through the branch that previously read 1.6369e-35. Nothing pinned at
99. Operator confirms the panel is blue, not red.
BT_HEATGAUGE_LOG kept as a permanent env-gated diagnostic: bind-time
classification plus per-sample temp/failAt/ratio/index. A fresh spawn reading
99 is this regression returning.
Not covered: ColorMapperCritical (cmCrit / ModeSecondaryCritical) already
computes a proper damageLevel*100 ratio and was read, not measured -- if the
Critical page specifically still misbehaves that is a separate lead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1777d5a62e |
one log per day, and every log says who it sent it -- rotation was eating the crash stacks
Conn Man crashed twice in an Owens on 2026-07-27, kept playing, and by the time
we asked for his logs they no longer existed. Not a mystery: every launcher did
if exist content\X.old.log del content\X.old.log
if exist content\X.log ren content\X.log X.old.log
which keeps TWO generations. He relaunched more than twice, so his own bat
deleted both crash sessions. The player who crashes is exactly the player who
relaunches immediately, so the retention window was aimed at the wrong case.
THE LOG. BT_LOG set still means "use this path verbatim" -- that contract is
load-bearing (mp_a.log/mp_b.log for 2-node runs from one cwd, btoperator's
operator_N.log, every scratchpad/mp_*.sh that greps the file it named) and is
untouched. Only when BT_LOG is UNSET does the exe now name the file itself:
content\<stem>_YYYYMMDD.log, appended, one per day, no rotation window to fall
out of. The stem (solo/join/steam/joyconfig) comes from the gates the bat
already sets, so solo still cannot overwrite the MULTIPLAYER log.
Self-named files append UNCONDITIONALLY. The default open mode is ios::out --
TRUNCATE -- and the operator GUI's exported bats never set BT_LOG_APPEND, so
they were truncating already; leaving append implicit would have let the second
launch of the day erase the morning.
IDENTITY, because these arrive from many players at once and Discord renames
half of them to message.txt (most of the night-5 evidence had to be
re-identified by hand):
===== BT411 SESSION build=4.11.603 (d64d75f+) machine=... user=...
callsign=Conn Man mode=solo pid=13192 local=... log=... =====
Also the session separator inside a per-day file, and it puts build+hash ABOVE
any [crash] block so btl4+0xNNNN offsets stay matchable to a PDB across builds.
SIZE. Never delete, but stay sendable: past 8 MB the next launch rolls to
<stem>_YYYYMMDD.1.log. Checked only at open, so a live session is never split.
CONSOLIDATION. launch_report.txt is gone, folded into lastrun_<stem>.txt (one
launch record: the exe appends its block at first breath, the bat appends the
exit line). Per-stem because one shared lastrun.txt let a second launcher's
`del` destroy the first launch's block. Its ABSENCE after a run is now the
#41 "never reached WinMain" probe -- the old "if not exist X.log" test cannot
work against a per-day file, which survives earlier launches, and would have
reported every healthy run as blocked by antivirus. marshal.log folded into
the day log too; it keeps its own handle and gained a CRITICAL_SECTION because
it runs on a worker thread while the engine log stream is single-threaded.
play_steam.bat gained a launch bracket it never had -- which is why a Steam
player killed before WinMain previously left no evidence at all.
_putenv_s, not SetEnvironmentVariableA, to pin the resolved name: MSVC's CRT
keeps its own environment copy, so getenv() in-process does NOT see a
SetEnvironmentVariableA write (measured). btl4console/btl4lobby resolve the
day log via getenv, so with the Win32-only call they silently fell back to
marshal.log and the fold never happened.
logfile.open() is now checked -- a failed open used to rebind cout to a dead
buffer, silently dropping the header, [boot] and any crash stack while
lastrun still claimed log=<name>.
Verified: append across sessions with distinct pids, crash block landing under
its own header, BT_LOG verbatim unmangled, an inherited BT_LOG cleared by the
bats, roll-over at 8 MB, and both forensic verdicts (exe ran / exe blocked).
Console auto-retrieval is unaffected -- it uploads the matchlog, a different
file, and a crashed round never reaches the upload call anyway.
Known gaps, deliberately not in this commit: the setlocal hoist for gate
leakage when two bats share one console (dev-only), and btoperator's exported
bats still lack the lastrun bracket -- do not press Export on a shipped zip.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e26f4e6285 |
Gitea #62 FIXED: AutoConnect can re-attach again -- slot 16 is HasVoltage(source), not GetStatusFlags()
A weapon detached by two eng-page BUS MODE presses stayed dark for the REST OF
THE MISSION: no voltage, blank recharge arc, dead ready dot, will not fire.
The arcade recovers by pressing bus-mode back to Auto and letting the per-frame
auto-hunt re-tap a generator. In the port that hunt's body was UNREACHABLE.
THE DEFECT, visible in three lines of powersub.cpp:
if (modeAlarm == AutoConnect && GetStatusFlags() == 0) // outer: NOT damaged
for (...)
if (... && GetStatusFlags() != 0 && Attach(sub)) // inner: DAMAGED?!
Both call sites modelled the no-argument GetStatusFlags(), so the outer demanded
"no status flags" and the inner "some status flag", with nothing in between able
to change the value -- mutually exclusive, body never entered. The binary's
@004b0bd0 calls vtable slot +0x40 (slot 16) with TWO SHAPES: slot16(this, 0) == 0
("am I unpowered?") and slot16(this, candidate) != 0 ("would THIS generator
supply me?").
WHAT WAS ACTUALLY WRONG was narrower than the issue assumed -- the BODY was
already a faithful transcription of @004b0b5c (complete-type accessors, no raw
offsets). Only its NAME and its WIRING were wrong:
* it was called `IsSourceShorted`, asserting the INVERSE of what it computes:
state 2 is the generator's ON-LINE state (Generator::GeneratorReady, what
@004b215c's stateAlarm->2 sets) and the fabsf test requires meaningfully
non-zero output voltage. True means "live and supplying".
* it was declared NON-VIRTUAL, so it could not be the slot-16 body, and a weak
stand-in (`electricalStateAlarm == Ready`, no source arg, no voltage test)
occupied `HasVoltage()` instead.
FIX: rename to `virtual Logical HasVoltage(Subsystem *source = 0)`, delete the
stand-in, restore the two AutoConnect call shapes, and rename Myomers' slot-16
override (`HasAdequateVoltage` -> `HasVoltage`) so it actually overrides -- it
tightens the test to "at least the SELECTED SEEK voltage", which is why it
exists.
BLAST RADIUS (the issue's warning): GetStatusFlags ORs the BadPower bit 0x40 on
!HasVoltage(), and that bit feeds #47's annunciator -- so power, firing and
annunciation semantics moved together. Checked: a healthy solo mission shows
zero spurious BadPower, subsystems simulate normally, no faults.
VERIFIED with a new diagnostic hook in the codebase's existing family
(BT_POWER_DETACH_TEST=1, off by default): it reproduces the player's exact state
-- drop the voltage link and force Auto, i.e. what the second BUS MODE press
does -- so the fix can be tested without the eng-page UI. Live result:
[power] TEST: detaching Avionics (forcing Auto) -- the auto-hunt must recover it
[power] AutoConnect RE-ATTACHED Avionics -> generator GeneratorA
Before the fix that second line was impossible. BT_POWER_LOG=1 prints every
re-attach; both hooks stay for regression use.
(The file-static one-shot is deliberate: PoweredSubsystem's layout is byte-locked
by PoweredSubsystemLayoutCheck, so a test flag as an instance member would break
sizeof and every offset assert downstream.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
26e678e570 |
#67 part 2, ROOT-CAUSED AND FIXED: 1995 latent uninitialized fields, exposed by the port's allocator
THE HUNT (the deterministic rig repro made it a two-hour arc):
1. cdb write-watch armed from the Torso ctor (bp plants `ba w4 this+0x21c`
per torso -- ASLR-proof). The poison reproduced (atUpd=-1250 this run;
-3750 and int-15-as-float before)... and the watch stayed SILENT. Nobody
writes the garbage. The field is never INITIALIZED.
2. Confirmed in our ctor reconstruction: it inits every neighbour but skips
targetTwist @0x218 and twistAtUpdate @0x21C (the function that zeroes
them is a death-reset handler, not the ctor).
3. Confirmed in the BINARY: the real ctor @004b6b0c contains no store to
either offset -- a genuine 1995 latent bug (uninitialized read on the
copy path).
4. Why the pod never showed it: MemoryBlock arenas carve fresh OS-zeroed
pages, one pool per type, low churn -- first allocations read as zero.
The engine's own DEBUG_NEW_ON NaN-fill proves the developers knew the
hazard class. Why WE show it: mechrecon.hpp's Memory::Allocate shim
("a plain heap allocation is behaviour-equivalent" -- false) recycles
dirty heap. Replicant torsos spawned with garbage twist targets; the
limit clamp turned any garbage into FULL TWIST -- rendered "twisted full
right while not using TT" to every peer (playtest night 4, 3 reporters).
THE FIX -- environmental, class-wide, byte-faithful to the binary's code:
Memory::Allocate / AllocateArray / Alloc now ZERO-FILL, reproducing the pod's
EFFECTIVE allocation semantics for every never-stored field in every
reconstructed factory at once (Torso, Reservoir, all of them). No ctor gains
stores the binary lacks.
PROVEN on the rig: the replicant thor now spawns cur=0 target=0 atUpd=0
(was cur=-3.31613 = hard against the limit). Teardown clean.
Ships with part 1 (the dead-reckoning clock fix) in the next zip. Remaining
on #67 for the next games night: live confirmation that twist TRACKING looks
right in play, and whether the fire-from-centre facet (very plausibly the same
never-stored-field class, now zeroed) is cured with it.
Tools kept: scratchpad/torso_watch.cdb + rig_watch.ps1 (the ctor-armed
write-watch pattern -- reusable for any "who wrote this field" hunt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a3329a64da |
#67 torso replication, part 1: the dead-reckoning clock fix + the bug REPRODUCED on the rig
THE CLOCK FIX (landed, verified live). The replicant torso's extrapolation
window was hardwired to ZERO: both shim accessors returned the same field
(torso.hpp, the "dormant in single-player" note). The engine maintains both
clocks exactly as the binary expects -- WriteUpdateRecord stamps
lastUpdate=lastPerformance on the master, ReadUpdateRecord stamps
lastUpdate=Now() on the replicant (SIMULATE.cpp:276/296, the 1995 "HACK"
comment intact) -- so the fix is one mapping: GetCreationTime (misnamed; now
GetLastUpdateTime) reads lastUpdate. ComputeTargetTwist's
`current - lastUpdate` window is real again. Why legs always replicated
while torsos did not: Mover is ENGINE code reading these fields natively; the
Torso is our reconstruction with the collapsed shim. Rig-verified: the
copy-side log now shows distinct lastUpd/now values.
THE FIELD BUG REPRODUCED (2-pod rig, thor vs thor -- thor because the first
attempt used FOGDAY's Black Hawk, whose torso is authentically FIXED like the
Owens: hEn=0, limits +/-0.01 deg):
[torso-copy] cur=-3.31613 target=-3.31613 atUpd=-3750
The replicant renders at EXACTLY its twist limit (thor: +/-3.31613 rad)
because twistAtUpdate holds garbage (-3750) and the limit clamp slams it to
the stop -- "twisted full right but was not using TT", on demand. An earlier
run poisoned it with 2.1e-44 (= int 15 as float, suspiciously the torso's
subsystem index).
NARROWED: the tx/rx probes added here (BT_TORSO_LOG prints every torso record
both directions, header + raw payload dwords) logged ZERO records in the
poisoned runs -- Torso::Read/WriteUpdateRecord never executed. The garbage
therefore arrives OUTSIDE the torso record path: a raw state write during
mech spawn/consolidation spraying the torso's fields (stream-walker framing
or a consolidation blit). That writer is part 2's hunt; the probes and the
deterministic repro make it a short one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
184121c597 |
kill-storm repro rig for #35 (owens laser crash): parameterized damage hook + honest NEGATIVE
BT_MP_FORCE_DMG=<n> now sets the per-tick probe damage (plain =1 keeps the original amount) -- kill-storm rigs need lethal ticks. rig_killstorm.ps1: offset-port relay + owens shooter under BT_AUTOFIRE vs a respawning victim. Result: NEGATIVE, twice. The respawn cycle (death anim + warp + handshake) caps the harvest at 1-2 kill-teardown windows per 4-minute round, and none crashed. With July's 254-volley negative the conclusion firms up: the window needs the reporter's slow-machine timing, not more attempts here. The field net (crash self-report + join.old.log rotation + the sweep guards) means the next real occurrence names its own site. Refined theory recorded in the ledger: six-beam volley vs a target dying mid-volley, 515-class teardown race. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ecb9b338bd |
the X button means QUIT: a user-closed window exits for real instead of relaunching
Operator report (00:30): "my local instance auto-relaunches whenever I stop and
start a session, even if I close my window." Root cause is the pod's AUTHENTIC
immortality: an arcade cabinet's join loop never exits, so every RunMissions
return relaunches into the join wait -- and a human closing the window was
indistinguishable from a round ending. On a desktop that made the client
unkillable outside Task Manager, and explains every orphaned-plasma zombie
hunted this week (my own test teardowns included), plus tonight's "eventually
all I could find was my plasma display".
FIX, one flag + one gate:
* btl4main WndProc WM_CLOSE stamps gBTUserRequestedExit before DestroyWindow
(covers X, Alt-F4, taskbar close, Task Manager's polite phase).
* BTFE_RelaunchSelfAndExit -- the single choke point every relaunch path
funnels through (round end, console-pad lost, round abort, FE loop) --
exits for real when the flag is set, logging 'window closed by the user
-- exiting for real (no relaunch)' to the marshal log.
Round-end auto-rejoin is UNTOUCHED: the flag only sets on WM_CLOSE.
VERIFIED both directions on the built exe (4.11.591):
* WM_CLOSE posted to a live solo instance -> the whole process tree is GONE
8s later: no relaunched generation, no plasma orphan. [was: immortal]
* Offset-port rig, full launch -> StopMission -> both pods AUTO-REJOINED and
re-ACKed for the next round (2/2 ready, WAITING FOR OPERATOR); zero
exiting-for-real lines fired (nobody closed a window).
Requires the next zip for players; the operator's running instance keeps the
old behavior until restarted onto this build.
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>
|
||
|
|
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> |
||
|
|
62e89018f5 |
Merge glass-cockpit-refit: cockpit scaling + one button geometry, the keyboard button board, RGB keylight, crouch, the cwd guard
Cyd's 9-commit branch, reviewed before merge (clean merge-tree, zero overlap with the console/relay work that landed after his fork point; his L4VB16 refit preserves the #48 plane-audit tripwires, and his lamp-decode fix corrects the #47 flash rendering). Four review findings are fixed in the follow-up commit: the stale README controls table, the unguarded backtick view-toggle, the environ.ini one-shot loophole, and volume-key documentation (the -/= -> PgUp/ PgDn move itself landed pre-merge so this branch's Comm-bank -/= bindings are collision-free). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9384c38c08 |
volume keys: -/= -> PgUp/PgDn, ending the zoom double-fire (prep for the cockpit-refit merge)
The issue #26 volume poll watched VK_OEM_MINUS/VK_OEM_PLUS -- the same physical keys the authentic 1995 typed-character target-zoom hotkeys ('+'/'-') live on, so a single press zoomed AND stepped the volume (operator-reported annoyance). The incoming glass-cockpit-refit branch also binds -/= as Comm-bank buttons, which would have made it a TRIPLE dispatch on fresh installs. PgUp/PgDn produce no WM_CHAR at all, so they cannot collide with any typed hotkey in any install; they are unbound in every bindings layout including the refit's 74-key board; and they exist on tenkeyless keyboards. Numpad +/- were considered and rejected: their typed characters feed the same zoom channel that made -/= wrong. README/CONTROLS documentation follows in the merge-fixes commit (the whole controls table is being redone there for the new default board). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8408165d92 |
Find content\ ourselves: a bare exe launch no longer scatters config and dies
Field report: `.\btl4.exe -fit` from build\Release opens the mission console,
then the mission crashes on Launch.
ROOT CAUSE (pre-existing landmine, newly reachable). The engine resolves
BTL4.RES, VIDEO\, BTDPL.INI, the eggs AND both player config files
(bindings.txt, environ.ini) RELATIVE TO CWD. Every launcher .bat does
`cd content` first, so this never showed. A bare launch of the exe therefore
found no resources ("Resource file btl4.res v1.0.0.0 is obsolete!"), wrote a
stray bindings.txt / environ.ini / btl4.log NEXT TO THE EXE -- so the player's
real ones in content\ looked like they had never been created -- and killed the
mission generation the menu launched (the child inherits the parent's cwd).
Recent work made that path inviting rather than obscure: glass is the desktop
default, a zero-arg launch opens the menu, and -fit is documented as something
you pass on the command line.
FIX: BTEnsureContentDirectory() runs before anything resolves a relative path
(the log file included, so a bare launch logs where the launchers log). If cwd
has no BTL4.RES, probe from the exe's own directory -- ..\..\content for the
shipped layout, plus the flattened and in-content cases -- and
SetCurrentDirectory there. Says so in the boot line when it had to go looking.
Belt and braces: the environ.ini writer refuses to write unless BTL4.RES is
beside it, so a failed probe cannot scatter a settings file either.
ALSO: -fit was silently dropped when the menu relaunched into a mission. The
front end rebuilds the child's command line from scratch, and the flag was
parsed at window creation -- code the menu process exits long before reaching.
Parsed early into gBTFitDisplay now, and appended to the relaunch.
Verified: bare `btl4.exe -fit` from build\Release leaves ZERO files next to the
exe, writes environ.ini + btl4.log into content\, and the mission child's exact
command line (-net 1501 -platform glass) from that same wrong cwd now logs
"cwd: found the content directory from the exe path", loads resources and
enters RunMissions instead of dying.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1debbeb240 |
Mech::DuckRequest -- the CROUCH button, and a dead-button census that lied
The 2026-07-20 audit listed 8 panel buttons dispatching a streamed message with
no reconstructed handler. Only ONE was still missing: generator on/off,
ToggleSeekVoltage, EjectAmmo, ToggleCooling and BalanceCoolant had all landed
between 07-20 and 07-25 while pod-hardware.md and open-questions.md still called
them dead. docs/INPUT_PATH_AUDIT.md had already flagged the census as "stale in
both directions" and was right -- swept both files, and recorded the rule: check
the code, not the census.
DuckRequest @0049fa00 (id 0x1a, RIO 0x13 -- the manual gives crouch a whole
section). The binary's entire body:
if (0 < *(int *)(param_2 + 0xc)) { *(undefined4 *)(param_1 + 0x398) = 1; }
Press-only; sets duckState (mech+0x398, attribute 0x37). A one-shot REQUEST
flag, not a posture toggle -- the handler never clears it and the only other
writer in the whole binary is the mech reset (part_012.c:9439, the same reset
that zeroes incomingLock, which is how that region was already mapped).
WHY THE FLAG HAS NO READER, AND WHY THAT IS CORRECT. duckState has ZERO readers
anywhere in the decomp, because it is published as an ATTRIBUTE and consumed
through DATABINDING: content/GAUGE/L4GAUGE.CFG runs a 3-frame bduck.pcc
oneOfSeveralPixInt bound to DuckState -- "crouch mode: button 4" on the map's
legend column. Verified live by capturing the Secondary/Radar window before and
after a 0x13 press: the crouch icon goes grey -> orange. So the handler is
COMPLETE. A crouch pose invented here would be a stand-in for data we have not
found (no SQUAT clip name survives in the decomp or in content/, only
DuckServo01.wav in AUDIO1.RES).
Bonus: those gauge widgets sit at offsets 537/430/322/215/108 -- a 107 pitch,
independently corroborating the map legend grid measured from pixels.
Still open from that census: MechRIOMapper's own Keypress @004d2514 (id 0x19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
61563c9efe |
Glass cockpit refit: one mode resolver, one button geometry, and a cockpit that scales
Replicates the RP412 cockpit line into BT411's own architecture -- porting the geometry and keeping our renderers, so BT_SHOT single-frame verification stays intact. MODE RESOLVER. Where the secondary displays go was decided in TWO places with duplicated precedence, and the boot banner read NEITHER -- it announced "per-display cockpit windows" for every glass boot, surround included. The split had also broken BT_COCKPIT=0: documented as the dock-bottom opt-out, the profile block converted it to BT_GLASS_PANELS=1, so the docked strip was UNREACHABLE under the glass profile. One resolver now, consumed by the banner, the pad-panel decision and the window sizing; BT_GLASS_PANELS is explicit-only; dock/window modes auto-raise BT_PAD_PANEL so the button field always has a home. L4RIOBANK -- one button geometry. Both renderers carried their own copy and had drifted: an MFD button was 156x138 reaching under the glass in the exploded window and a 76x24 sliver entirely OUTSIDE the glass in the surround. One module owns it now, both are consumers, placement stays per-renderer. The pod's under-glass rule (RP412 L4MFDVIEW): reach half the glass in behind the display, leave a lamp strip clearing the edge, paint buttons first and imagery over -- so the lamp reads as a bar and practically the whole display is the press target. The strip scales off the display's SHORT axis (the map is portrait) with a readable floor. Retired L4GLASSWIN's three local placers and seven layout constants. LAMP FLASH DECODE was wrong [T1]. BTLampBrightnessOf returned max(state1, state2) and blanked on the alternate phase; RIO::LampState (L4RIO.h [T0]) says solid shows state 1 and flashing ALTERNATES the two. Agrees only when one state is Off -- true for the Panic lamp, which is why it survived -- but L4LAMP.cpp:252 commands flashFast + state1Dim + state2Bright, a dim->bright pulse that rendered as a hard bright->off blink. Three copies existed (l4vb16.h, L4GLASSWIN, L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline. THE COCKPIT SCALES. The fixed canvas was stretched into whatever the client area was, so a window dragged to a different shape squashed the instruments (the projection was aspect-corrected in task #20; the panels never were). Now one uniform scale, centred, leftover black. D3D9 applies it as a Present destination rect, which DISCARD forbids -- so the WINDOWED swap effect becomes COPY when the surround is up and multisampling is off. The click mapping had to follow (mapping against the full client drifts the hit test off every button by the bar width), as did the world aspect (under a uniform scale it is the view rect's own). -fit / -windowed-fullscreen: borderless over the monitor. - ordering trap: the first WM_SIZE beats the device, so a -fit boot logged aspect=3.14 and applied it on frame 1. The letterbox INTENT is decided in btl4main; L4VIDEO only confirms or withdraws it. PLAYER-TUNABLE DISPLAYS. BT_MFD_SCALE (+ _UL/_UC/_UR/_LL/_LR), BT_RADAR_SCALE, BT_RADAR_POS (CENTER/LEFT/RIGHT/MIDLEFT/MIDRIGHT). The surround BANDS derive from the resolved sizes -- that is why the sizes could not stay constants: the band a display hangs in has to grow with it or the canvas clips it. 100% reproduces the historical L276 R276 T223 B336 exactly. A corner map goes flush to the CANVAS edge and the lower MFD slides beside it (measuring off the view edge overlapped them by 232px). MAP LEGEND GRID -- measured, not inherited. scratchpad/measurelegend.py over a native capture: top 3, cell 102, pitch 107 of 640. RP412's map is 13 + 6x102 @ 105 -- same cell height, different top and pitch, so its numbers do NOT transfer. Our old even division had the pitch right by luck and sat 3px high of the labels. environ.ini. It was read ~300 lines into WinMain, AFTER the platform-profile block had run its getenv()s -- so every setting the profile reads was silently ignored FROM THE FILE and only worked as a real env var. It also putenv()'d comments verbatim. Now loaded immediately after the first-breath line, comments skipped, the real environment WINS over the file, and a fully documented default is written on first run (the bindings.txt convention: untracked, so extract-over-top never clobbers a player's settings). VERIFICATION HARNESS (new, reusable): BT_RIOBANK_LOG=1 dumps every bank; checkbank.py proves no address is SHADOWED (an address whose rect is covered by earlier buttons is dead however big it looks -- the overlapping under-glass banks make that a live hazard); clickbank.py posts a real click at every button centre. Verified: 72/72 placed, 0 shadowed, 72/72 dispatched in BOTH modes, after a resize, at 150%/135%, and at 75%+BOTTOMRIGHT; wide/tall drags and -fit undistorted on a 3440x1440; exploded/dock/pod/dev un-regressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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
|
||
|
|
4fa7eee54f |
K/D scoreboard ROOT-CAUSED for real (#45): the kill credit was rerouted, not lost
Kills/deaths only ever appeared on ONE machine. The cause is not a missing
tally -- it is Entity::Dispatch:
if (GetInstance() == ReplicantInstance) // ENTITY.cpp:244-251
application->SendMessage(ownerID, EntityManagerClientID, message);
BTPostKillScore runs on the VICTIM's node (the only node whose mech carries a
populated lastInflictingID), resolves the killer's Player -- a REPLICANT there --
and Dispatch()es to it. The engine reroutes that to the owning host, so
++killCount lands on the killer's own PC and nowhere else, and nothing carried it
back out: Player__UpdateRecord is currentScore + dropZoneLocation only. Every
other pod's copy therefore read 0 all mission. playerLink was never NULL for
these kills -- the dispatch proves it resolved.
Field proof over the whole corpus (255 node-logs): 125 of 125 SCORE type=2 rows
credit the LOGGING node's own player, not one credits a remote pilot; and 0 of
8800 DMG rows target a replicant, so no other node can even know the killer.
This means the owner's counter is already the single authoritative copy,
incremented exactly once per kill. So the "design decision" the plan was blocked
on collapses: there is no second writer to reconcile, only a one-way
owner->replicant mirror to add.
* BTPlayer__UpdateRecord = Player::UpdateRecord + killTally + deathTally, with
Read/WriteUpdateRecord overrides. No new data member, no new virtual ->
sizeof(BTPlayer)==0x28c and every existing offset lock still holds.
* Both counter writes already ForceUpdate() (:508 death, :829/:840 score), so
no dirty-bit edit was needed (plan risk 6 avoided).
* recordLength guard in ReadUpdateRecord: a pod on a build without the
extension degrades to "remote counters don't move" instead of reading a
neighbouring record as a kill count.
* Size locks added per plan risk 2 (no false offsetof assert).
Rig-verified, 2-node loopback (scratchpad/rig45_up.ps1, BT_MP_FORCE_DMG):
owner 3:1 finished kills 3/deaths 2 and the peer read kills=3 deaths=2; owner 2:1
finished kills 2/deaths 3 and the peer read kills=2 deaths=3. Exact convergence
where a remote column previously never left 0. 3 respawns per side with intact
death sequences (deathCount is only overwritten on replicant copies; the
handshake runs on masters), no crash, no GLITCH rows.
Kept byte-faithful: the kill handler's partner increment (the binary's
inc [ebx+0x27c] / inc [edx+0x27c] wrong-column slip) is still reproduced. It
always lands on a replicant copy, so the owner's record now overwrites it -- the
rig caught the correction repeatedly (wasKills=3 -> kills=2). The phantom kill
stops being visible without silently deciding the fidelity question.
Still open and deliberately untouched: which field the LOCAL pod's DEATHS column
should read (+0x280 vs deathCount), last-hitter-takes-all/ram kills, and the
slip's fidelity. Awaiting live multi-pod verification by a human.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
373889760c |
#66: joyconfig.bat no longer hangs the game -- clear BT_JOYCONFIG once the wizard is done
Found while sweeping the shipped launchers for the PATH-shadowing bug class.
Previously recorded ONLY as row 10 of docs/INPUT_PATH_AUDIT.md and never
filed, so it was invisible on the tracker.
joyconfig.bat sets BOTH `BT_JOYCONFIG=1` and `BT_FE_SOLO=1`, so after the
wizard writes bindings.txt the boot continues into the FRONT END, which hands
off by CreateProcessW()ing the next generation with the CURRENT environment.
That clear-list (btl4console.cpp) covers only BT_FE_EGG/PODS/SECS/LOOP --
BT_JOYCONFIG is never cleared -- so the handed-off generation re-entered
BTJoyConfigWizard() and blocked on _getch() BEHIND the 3D window: an
unrecoverable hang, hitting exactly the players most likely to run that bat
(flight stick / HOTAS / pedals).
Fix: SetEnvironmentVariableA("BT_JOYCONFIG", NULL) immediately after the
wizard returns. Cleared in the WIN32 environment (what CreateProcess
inherits, matching the console's own pattern) rather than the CRT copy, so
exactly one generation ever runs it. Chosen over extending the console's
clear-list because it also covers any future relaunch path.
NOT YET REBUILT: the exe was locked by a live play session when this landed.
Must be rebuilt + verified (run joyconfig.bat, finish the wizard, confirm no
second "=== BattleTech joystick setup ===" banner and that the game proceeds
into the menu) before the next zip is cut.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
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>
|