5b19cd6c3f3fe96441ada60507c855cf342f09d5
638
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
18766a167b |
locomotion.md: correct the gimp-audio verdict -- the reverse-disabled voice is real and wired
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> |
||
|
|
7838df2924 |
locomotion.md: the gimp audio verdict (servo loop, no voice line) + rebind watch item
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ed844456af |
audio: gated [statefire] diagnostic on AudioStateTrigger (BT_AUDIO_SPATIAL)
Logs animation-range state-trigger fires (trigState>=5, capped 60). Used to prove the #78 limp audio end-to-end: entering the gimp states fires the authored strained-servo component (statefire trigState=23 old=7 new=23, ctl Start) -- the 1995 wounded-leg sound, reachable for the first time now that the limp gait lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0fd33531a4 |
locomotion.md: record the gimped turn-step relocation find
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> |
||
|
|
3db1062eb6 |
limp demo bat + the glass controls doc fix (SHIFT is throttle, not W)
run\limp.cmd: double-click #78 demo -- novice copy of DEV.EGG (expert crits can kill the myomers and fake a no-drive bug), 2x60 into a bhk1 leg zone via BT_SELF_DAMAGE_TICKS, BT_KEY_NOFOCUS for the glass plasma-window focus trap, and the REAL controls in the header. build-and-run.md: the "WASD drive" line predated the glass merge -- the default profile drives with the 1995 throttle lever (SHIFT/CTRL slew + stick, ALT reverse). 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> |
||
|
|
e110b10ac8 |
the #73 pick dig, part 2: there was never a software pick -- the division card cast the ray
The hunt for the un-decompiled pick writer ends with the reason no scan could
find it: it does not exist. In 1995 the pick was a DPL SCENE INTERSECTION run
by the video board. The evidence converges from five directions:
- The WinTesla renderer still carries the result slots: dplHitInstance /
dplHitDCS / dplHitGeoGroup / dplHitGeometry + vehicleReticle, NULL-inited
in the ctor and never fed by the port.
- The stubbed 1995-era renderable constructors each took dpl_isect_mode_obj
("type of intersections to do on this object") plus an intersection MASK
-- every scene object was configured for ray queries.
- Auric, quoted in the KB long before this dig: "the pod's division card
cast from the view."
- VGL Lynx's night-6 LOD warning ("the hit test may run against a different
LOD") reads as firsthand knowledge: the ray tested the DRAWN geometry.
- Exhaustive byte- and pseudocode-level scans: nothing in the binary writes
rayIntersection/targetEntity/targetDamageZone. Game code only reads them,
constructs them (the Mech ctor @0x4a1674 -- identified this dig, along
with vtable +0x18/+0x1c = Mech::Read/WriteUpdateRecord, nine replication
groups, reticle not among them), gates them (FUN_004afd10, the look-state
machine: per-view crosshair positions, the pi rear case, the per-weapon
rear-fire mask walk), and ships them to the board for drawing
(FUN_00460a7c packages rayIntersection + elementMask into dpl).
What this means for #73: aimed fire in 1995 had PER-PART precision -- scene
ray, struck triangle on the active LOD, the DCS is the segment, the segment's
dzone is the credited zone. The cylinder lottery was only ever the UNAIMED
path. The port's whole-mech box pick funnels aimed fire through the unaimed
lottery, which is exactly what three testers documented on night 6: aim at
the arm, get the spray.
Fix design recorded in the KB: a per-SEGMENT ray test on the shooter side
(inverse(segmentWorld) * ray vs each segment BGF's local extent box, nearest
wins, its dzone dispatched as the aimed zone), falling back to box+lottery
when no segment resolves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bcbc7cff12 |
the #73 pick dig, part 1: mech+0x36c is an embedded engine Reticle -- the writer is still at large
Chasing the un-decompiled targeting pick (the residual of #73). The hunt for "who writes mech+0x37c/0x388/0x38c" kept coming back empty for a structural reason now understood: those three fields are the tail of an embedded ENGINE Reticle struct at mech+0x36c (RETICLE.h lays out position/state/pickPointingOn/ rayIntersection/targetEntity/targetDamageZone/elementMask, landing exactly on 0x37c/0x388/0x38c). Writers carry &mech->reticle and use small reticle-relative offsets, invisible to any mech-relative displacement scan. T0 corroboration, ENTITY3.h:131: "For BattleTech, damage zones are only valid via reticle based weapons." Ruled out as the writer: the engine Reticle itself (passive container -- ctor and resource parse only); HudSimulation @0x4b7830 (holds &owner->reticle in esi for its whole body but only READS the pick -- range caret, designator transform -- and slews reticlePosition via the @0x4b7ed4 ease); the 0x482xxx sites (mission-table target bookkeeping, address-of computations); the gyro coefficient reads at 0x4b2c24/5c/6e (that class's own +0x370..0x38c table). Still dark, with the candidates mapped: the un-exported 0x4a1674-0x4a2d48 stretch (reticle-touching at 0x4a16a5 in the Make-time init, 0x4a1f93, and a state+pickPointing+base trio at 0x4a294a-61 near entry 0x4a2971), the two unidentified Mech vtable overrides +0x18/@0x4a122c and +0x1c/@0x4a0c2c (both big switch functions), and a few lone sites. One of these computes the authentic pick; recovering it answers whether the pod tested the aim ray against the mech's cylinder (as the damage table's geometry suggests) or a box like the port's stand-in. Recorded in combat-damage.md \xc2\xa7Targeting so the next session starts from the map instead of the empty scans. Co-Authored-By: Claude Fable 5 <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>
|
||
|
|
f7cf9850b1 |
crit panel: the roll has no caller and the sink is a stub -- filed as #80
Night-6 observation (Conn Man, with screenshot): armor panel showing damage,
Critical damage display showing no crits. Investigated; it is a real port gap
in three layers, stacked on one authentic fact.
The authentic fact: the paper doll shows per-ZONE armor tint and the Critical
view is a per-SUBSYSTEM list. Different data by design -- the panel staying
dark while armor accumulates is correct right up until a subsystem takes
critical damage.
The gap: subsystem critical damage essentially cannot happen.
1. Mech__DamageZone::CriticalHit @0049ccc4 -- the authored roll, half the
hit to armour, half to one critical subsystem by criticalWeight -- has
ZERO callers in the port. A raw byte-scan of the binary finds exactly
one call site, @0x4a0461, and it sits in the un-exported decomp gap
(nothing covers 0x4a03xx-0x4a05xx, the same dark region as the
targeting-pick writer). The trigger conditions are unknown.
2. MechSubsystem::TakeDamage is an empty bring-up stub (btstubs.cpp:179),
so even a wired caller would measure a delta of zero through
ApplyDamageAndMeasure.
3. The only live crit sources are zone destruction (SendSubsystemDamage)
and ammo cook-off (DistributeCriticalHit), which pin subsystem damage
directly -- so the panel can light after a zone is destroyed outright,
never from accumulating fire.
#28 (the vital-subsystem-crit death path) very likely shares this root and is
cross-linked. Recovery plan on #80: raw-disasm the 0x4a04xx container for the
trigger, dump MechSubsystem vtable 0050e210 slot +0x24 for the real TakeDamage
body, wire both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a5dd0eabc5 |
issue #73 investigated: zone selection is a weighted lottery -- per-limb aiming never existed
Night-6 report: three players, controlled conditions, "fired only at the left arm, damage credited all over the paper doll, no crits". Chased it to the bottom and the bottom is the authored 1995 model, working as designed. The zone a hit credits is chosen by dice, in binary-faithful code reading binary-shipped tables (dmgtable.cpp, stream format byte-verified): impact point -> height layer (floor(layerCount*y/heightRef)) -> pie slice (atan2(z,x) around the vertical axis, optionally rotating with live torso twist) -> DamageZonePercentTable::SelectZone() = RandomUnit() rolled against cumulative percent thresholds (@0x49de14). Every slice carries an authored DISTRIBUTION of zones. Aiming at a limb at best biases which slice you strike; the zone inside it is a weighted roll. Pixel-precise limb damage does not exist in Tesla 4.10. Measured live to be sure the code read was real: solo dummy, walked to 8u, aim pinned left/center/right across the silhouette, trigger held. 24-46 hits per aim point, each spread across 6+ zones (the percent tables), with the distribution shifting by aim point (the slice selection responding). Both halves of the model visibly working. What remains genuinely open, and is now the whole of #73: the shooter's pick point comes from Mech::PickRayHit, a whole-mech AABB slab test -- a port stand-in, since the binary's 0x37c/0x388/0x38c writer sits in an un-exported gap nobody has decompiled. Theta computed from a flat box FACE clusters toward the slices facing the shooter, so flank slices (presumably arm-heavy) may be under-reachable from frontal shots compared with the pod, which plausibly intersected the mech's cylinder -- the damage table is literally cylindrical. Deciding that needs a per-hit theta probe and a one-shot wheel dump; if confirmed, the fix is a ray-vs-cylinder pick replacing the box slab. Filed on #73 with the plan. KB: combat-damage.md gets the lottery model as a load-bearing triage fact -- "damage landed somewhere I didn't aim" now has a documented base rate, and crit expectations from aimed fire are probabilistic by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dca2586aa8 |
the Owens crash: a device reset that never waited for the device (#35)
Eight byte-identical field stacks from night 6, all one player, all in an
Owens: ParticleEngine::Destroy +0x11, access=0 target=0x0, from the plain
per-frame render path. Nothing in the stack touches weapons or the Owens.
Conn Man's Surface Pro 9 (Iris Xe, 128 MB shared) is simply the only GPU in
the fleet that ever actually LOSES the D3D9 device -- his two-trigger
missile+laser bursts are what provoke the timeout, not what crashes.
What crashed is our device-loss handling, which was wrong three ways at once,
in two inline copies (the scene Present and the wait-screen Present):
1. On D3DERR_DEVICELOST it called Reset() IMMEDIATELY. Reset on a
still-lost device ALWAYS fails, and V() only logs. There was no
TestCooperativeLevel gate at all.
2. It then ran ParticleEngine::Initialize against the lost device. The
creates fail there and NULL their out-params -- proven, not assumed:
the bench repro faults at target=0x0, not at a dangling address.
3. The next lost frame called ParticleEngine::Destroy again, which
Release()d those NULLs blind. Read of vtable at 0x0. Dead.
So: lost frame 1 tears down and leaves NULLs, lost frame 2 crashes. Two
frames, every time, deterministic -- which is exactly why all 8 field stacks
are byte-identical.
Reproduced before fixing. BT_DEVICELOST_TEST=<frame>,crashrepro runs the
field sequence on the bench; on the unfixed build it died at Destroy +0x11,
access=0 target=0x0, and symbolized to the same four frames as the field
logs. Same shape, same offsets-modulo-hook. That run also proved the
out-param-nulling assumption the whole diagnosis rested on.
The fix -- one shared DPLRenderer::BTResetLostDevice() replacing both inline
copies:
- Destroy() is idempotent and null-safe, and nulls after release.
- Reset() is gated on TestCooperativeLevel() != D3DERR_DEVICELOST; while
the driver still says lost, skip the frame and retry.
- The Reset HRESULT is checked; on failure, log and retry next frame
instead of driving on.
- On success, re-create via the new CreateDeviceObjects(), NOT
Initialize(): Initialize memsets the installed-effects table, so every
reset that DID succeed silently killed all particle effects for the rest
of the mission. The quieter sibling bug, fixed by the same split.
- Initialize checks its HRESULTs and defends MAXPARTICLES<=0; the draw
paths guard the NULL buffer, and ExecuteParticles keeps draining
particles while the engine is dormant so they cannot pile up.
Verified: the crashrepro shape now logs SURVIVED and play continues; three
forced full loss/reset cycles each log "[render] device reset OK"; a plain
run is assert-free.
Found while verifying, worth its own line: VIDEO\particles.png has NEVER
existed -- not in the tree, not in BTL4.RES, not anywhere in git history.
The texture load has failed on every machine since the engine was written,
and every billboard particle ever rendered was untextured quads via
SetTexture(0, NULL). RenderParticles deliberately does NOT gate on the
texture -- that would disable all particles everywhere; untextured IS the
shipped look. Filed separately; a real particle sheet is a content task.
The field verification that counts is Conn Man flying his exact crash
loadout on this build: instead of a dead process he should see at worst a
brief hitch and "[render] device reset OK" in his log. #35 stays open until
that happens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
29c502df83 |
Glass windows remember where you drag them (BT_GLASS_LAYOUT)
The BT_GLASS_PANELS windows self-place in a pod-faithful ring and re-snap
once the main window is up. They carry WS_CAPTION so a dev can drag them,
but the drag never survived the menu->mission->menu relaunch loop.
BT_GLASS_LAYOUT (L4GLASSWIN.cpp) adds opt-in persistence to a cwd-relative
glass_layout.cfg beside bindings.txt (gitignored):
off/0/unset computed ring only, no file I/O (default, pod-faithful)
load restore saved positions on startup (per-window fallback
to computed); never writes
save/adjust restore first, then rewrite on each finished drag
(WM_EXITSIZEMOVE) and on teardown -- the round trip
One "<title>=x,y,w,h" line per window; position restored, size ignored
(frame size is deterministic from content, so an old w,h can't distort a
later geometry change). A restored window is flagged so ComputeLayout's
post-main-window re-snap leaves hand-placed windows alone. Native
analogue of TeslaRel410 pod-launch's per-rig --bridge-pos/--layout args.
Verified [T2 runtime round-trip]: load restored 7 from a seeded cfg;
save wrote all 7 with a window moved to 777,333 (GetWindowRect read the
live window); relaunch in load put that window physically at 777,333
after the re-snap fired. Release links clean (only the 40 tolerated
/FORCE externals).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
8284b8d9a7 |
Glass panels: MFD phosphor green (21FF42) + respect BT_COCKPIT_TINT
The BT_GLASS_PANELS=1 per-display windows tinted the mono MFD surfaces pure WHITE (MfdMonoTint 0x00FFFFFF) -- the cockpit surround already used phosphor green via BT_COCKPIT_TINT (L4VB16 CkTint). New GlassMfdTint(): default the standard green rgb(33,255,66)=0x0021FF42, overridable by the SAME BT_COCKPIT_TINT=RRGGBB env the surround honours (used verbatim as the 0x00RRGGBB ExpandPlaneToBGRA tint -- no R5G6B5 packing, which is the surround's D3D path). Pixel-verified live (BT_GLASS_PANELS=1): all lit interior MFD pixels R33 G255 B66 = 21FF42 by default (was white); BT_COCKPIT_TINT=FF8000 -> R255 G128 B0. Radar keeps its amber palette (monoTint -1, unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2cbf9cd88f |
mkdist: archive the .map beside the .pdb, and stop shipping d3dx9_43.dll twice
Symbols only matter if they survive the build that produced them, so archive BOTH per version, not just the PDB. The .map is the one that gets used in practice: tools/symcrash.py resolves btl4+0xNNNN from plain text with no debugger installed, which is the normal state of an operator box -- this machine has no cdb either. Warns loudly if either file is missing, since that silently means a crash in that build can never be read. Also: d3dx9_43.dll was written into the zip twice. It exists both next to the exe and in redist/, and both paths land at build/Release/<name> in the archive, so the entry was duplicated -- about 2 MB wasted and a duplicate-name warning from Python's zipfile that some extractors also complain about. Redist entries now skip anything already taken from beside the exe. Neither the pdb nor the map can reach a zip: .gitignore covers *.pdb and dist/, and the archive only ever picks up .dll from those directories. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
68ffe556ce |
make a field crash resolvable: no Release PDB existed, and /O2 had eaten the frame pointers
BTCrashFilter writes `[crash] ... addr=0x... (btl4+0xNNNN)` plus an EBP-chain
walk into the tester's day log, and its own comment claims "we hold the PDB".
We did not. Two independent gaps, both fatal to the point of the thing:
1. No Release PDB was produced AT ALL -- only build/Debug/btl4.pdb, and the
shipped exe embedded no PDB path. btl4+0xNNNN from a tester could not be
turned into a function name by anyone.
2. Release flags were /O2 /Ob2 /DNDEBUG with no /Oy- anywhere, and /O2 implies
/Oy on x86. The walker follows EBP, so the stack it printed was unreliable
even when the faulting address was not.
Net effect: when the Owens crash finally lands we would have received an address
nobody could resolve, and a call chain we could not trust. Cheaper to fix before
tonight's session than to wait for the crash to happen twice.
/Zi emit debug info -> a PDB. Does not change codegen.
/Oy- keep EBP as a frame pointer so the walk is trustworthy.
/DEBUG + /OPT:REF + /OPT:ICF -- /DEBUG turns the last two OFF by default,
which would have quietly bloated the shipped exe with unreferenced
code. The exe grew 512 bytes, the debug directory entry, and nothing else.
Also emitting /MAP, which turned out to matter: this machine has no cdb, and a
tester's operator may not have one either. The .map is plain text, so an offset
can be resolved with a text editor. tools/symcrash.py does it properly --
nearest-preceding-symbol against the archived map, module-external addresses
(ntdll, kernel32) passed through untouched since they are not ours.
Verified end to end rather than assumed, using the BT_CRASHTEST=1 hook that
exists for exactly this:
[crash] addr=0xb8260 (btl4+0x8260) access=1 target=0x0
[crash] stack: btl4+0x8260 btl4+0xfe7ed 0x763ffcc9 ...
btl4+0x8260 -> _WinMain@16 +0x7e0
btl4+0xfe7ed -> __scrt_common_main_seh +0xf8
which is exactly right: BT_CRASHTEST does *(volatile int *)0 = 0 inside WinMain,
called from the CRT entry. Correct stack AND correct names.
The PDB and map are archived per build and never shipped: .gitignore already
covers *.pdb and dist/, and mkdist only picks up .dll next to the exe, so
neither can reach a zip by accident. The session header stamps
build=4.11.<n> (<hash>), so an archived pair matches a tester's log exactly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bc27313f20 |
sweep the knowledge base up to this session: log day, crash symbolization, a misnamed accessor
Audit of what actually landed in context/ versus what only ever got said in
chat. Five gaps and one stale claim repeated in four files.
New env gates were the biggest hole -- BT_TORSO_LOG, BT_MYOMERS_LOG,
BT_MYOMERS_REPAIR_TEST, BT_SELF_DAMAGE and BT_POWER_DETACH_TEST were all in the
code and none of them in decomp-reference §6, which is supposed to be the hub.
Added with the reasoning that makes them usable: why the myomers probe samples
every frame while unpowered (the NoVoltage window is about a second and a 1 Hz
probe steps straight over it), why the repair test also forces Manual (or
AutoConnect restores power a frame later), why SELF_DAMAGE latches off at the
first death, and why DETACH_TEST taking a NAME is what proves failover rather
than a same-generator re-attach.
The 6am log day was in the code and the player bats but not the KB. Now in
build-and-run with the stem table, the unconditional append, the 8 MB part
roll-over and the BT_LOG-truncates trap, plus the operator-facing note in
operator-console and OPERATOR_GUIDE that you ask for the NEWEST log, never
"today's".
Recorded the crash-symbolization gap, which is the one with teeth. BTCrashFilter
writes a stack into the day log and its own comment claims "we hold the PDB". We
do not: no Release PDB is produced at all, and /O2 implies /Oy so the EBP walk is
unreliable anyway. When the Owens crash finally lands we get an address we cannot
resolve. Written down with the fix (/Oy-, /Zi + /DEBUG, archive the PDB per
build) rather than left as something I mentioned once.
New gotcha 22: HeatModelOff() reads simulationState == 1, i.e. "am I destroyed",
and has nothing to do with the heat model. Behaviour at the call sites is right,
the name is not, and while chasing #70 it reads as "novice pilots cannot twist"
and sends you hunting an experience-level bug that does not exist. Same misnomer
in torso/gyro/sensor headers.
Also documented the dist flavor trap, having nearly shipped it myself: mkdist
reads build/CMakeCache.txt, so a tree configured BT_STEAM=OFF silently produces
a -nosteam zip with no play_steam.bat and no steam_api.dll. OFF is the CMake
default; ON is the documented dev state.
Swept the stale `btl4.log` filename out of reconstruction-gotchas,
reconstruction-method, build-and-run and CLAUDE.md itself -- the log has been
<stem>_YYYYMMDD.log since
|
||
|
|
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> |
||
|
|
ca6718a876 |
turrets: correct the record -- the props are scenery, but PGN is a real turret that was cut
Amends what
|
||
|
|
0534e93eec |
the level turrets are scenery: TurretClassID is a red herring, 0xBDE is ThermalSight
Playtest question: "many levels have turrets/cannons that don't fire, aren't those supposed to shoot at players?" Chased it properly because the enum really does look like a smoking gun. engine/MUNGA/VDATA.h:209 declares TurretClassID as the LAST entry of the BT block -- right after MechTechClassID, immediately before the ND section -- which is exactly where a real BattleTech class would live. And the mech factory has a live `case 0xbde:` for it (part_012.c:10186). Both of those point the wrong way. That enum slot computes to 3038 = 0xBDE, and 0xBDE in the shipped runtime is ThermalSight: ctor @4b8718, already reconstructed and done in thermalsight.cpp. Same enum-vs-runtime label drift CLASSMAP already records for HUD and MechTech at 0xBD6/0xBDC, which is why the rule is to resolve the ctor address and never trust the factory case label. The case is also in the mech SUBSYSTEM factory (roster param_1[0x4a]), not an entity factory, so it could not spawn a world object even if the label were right. Everything else agrees. There is no `class Turret` anywhere in the engine or the port. BTL4OPT.EXE contains zero occurrences of turret, sentry, emplac, brain, patrol, aggro, hostile or npc (the apparent "ai"/"bot" hits are substrings of failureheat and verticallimitbottom). jointturret, which looked promising, is a skeleton joint on OWN/PGN/STI with own_tur.bgf -- a mech turret-torso, not a world gun. And the destructible world classes in CULTURAL.h -- Landmark, CulturalIcon, UnscalableTerrain -- have no weapon, fire or target members at all; they take damage and break, and that is the whole of it. So the turret and cannon models in the maps (TT1/TT2/TWR/TK1/APC, each with a D damaged variant) are scenery. They did not fire in 1995. Making them fire would be inventing a feature rather than reconstructing one, and it would need an entity class, a targeting model and a threat model that the binary has no trace of -- which is consistent with the standing T1 finding that BT shipped no AI at all and is PvP-only by design. Written up in combat-damage.md next to the No-AI section, since the enum is convincing enough that someone will find it again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1af2dfc769 |
back the audio cap off to opt-in: 7-23 already fixed the complaint, and the cap is a governor
Correcting my own framing in |
||
|
|
7e57816d39 |
audio cut out in firefights because nobody ever asked OpenAL for more than 256 voices
Field report from 2026-07-23 was "audio cutting in and out toward the end of the
match". The night-5 logs say it precisely: 2017 ACQUIRE FAILED lines in a single
match, and every last one of them at live=256.
It is not a leak, which is where I started and where the code comments still
pointed. The source census sits at a healthy 45-60 live, spikes during
firefights, and drains right back down afterwards -- 226 back to 42 in one
window -- so the engine's priority steal loop is doing exactly what it should:
AudioSourceStop/SuspendMaintenance -> ReleaseChannels -> ReleaseSourceSet ->
alDeleteSources, with the live counter following it down. The 2026-07-23 fix
(
|
||
|
|
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>
|
||
|
|
d64d75fcf7 |
handoff: fold in the orphan investigation, the launcher fix, and what it means for the release
The tracker split moves to 26 open (adds #71/#72 filed today). Flags the one decision left for the road: the :btwait fix is at HEAD but NOT in the 600 zip, so it ships only if the zip is re-cut -- and names the single manual check that would close out the fix's verification. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7afbda900e |
the terminal that never closes: every launcher waited for ANY btl4.exe on the machine
Operator report: "X-closing the game leaves the terminal open and leaves orphaned processes." Investigated on the rig against 4.11.600. The terminal half is real and is THIS: :btwait polled `tasklist /FI "IMAGENAME eq btl4.exe"`, which is machine-wide, so a bat that launched nothing at all keeps spinning while an unrelated instance lives -- proved with btwait_probe.ps1. A second client, the operator's own pod, or an orphan from a crash therefore hangs every join window, which reads as "the game never exited" and invites people to start killing processes. All four launchers carried the identical block. Fix: snapshot the btl4 PIDs alive BEFORE the launch; wait only on PIDs absent from that snapshot. The `if /I "%%P"=="btl4.exe"` guard is deliberately kept -- tokens=2 alone parses tasklist's "INFO: No tasks are running" line as a PID and spins forever with nothing running, which would be worse than the bug. Verified with the text lifted verbatim from the shipped play_solo.bat: nothing running -> signs off (the regression guard); someone else's instance -> signs off; our own generation -> keeps waiting; decoy gone -> signs off. The patched bat still launches (pid + launch_report.txt). NOT verified: the full handoff E2E, because the bat blocks on the FE menu waiting for a human. The orphan half did NOT reproduce on 600: closing the MAIN window exits cleanly in ~1s during solo model-load, in the relay join wait, and after a real console launch, with the relay logging the seat freed. The orphans the playtesters saw match 584 and earlier, where every close relaunched. Full write-up, including the aux windows that hide instead of closing and the WM_QUIT that BTLoadPump swallows, in phases/phase-12-orphan-processes.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
afbbb7c59c |
road handoff: the state of every live investigation, checked against the machine and the tracker
Written for picking the work back up on a laptop. Facts verified rather than recalled: the parked relay is NOT running (no listener on 1500/1501/1507), RDP is up, Tailscale is not installed, and the tracker splits 24 genuinely-open / 26 awaiting-verification. Also commits the tracker snapshot script so the issue split can be re-derived from the road instead of trusting the numbers frozen in the doc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
39cb8647c7 |
launcher logs: rotate, never delete -- and solo/joyconfig must stop destroying the MULTIPLAYER log
Prompted by "does the steam path still generate our logs?". Answer: yes -- play_steam.bat sets BT_LOG=steam.log with append, and matchlogs arm because both Steam launch paths pass -net (the default trigger). But the audit found two evidence-destroying bugs on the way: 1. play_steam.bat DELETED the previous steam.log at every launch -- the same pattern fixed in join.bat/join_lan.bat last night. A Steam player who crashes and relaunches loses the stack. Now rotates to steam.old.log. 2. WORSE: play_solo.bat and joyconfig.bat deleted content\join.log -- the MULTIPLAYER log, not theirs. A player who crashed in MP and then ran solo to investigate (or ran the joystick wizard) DESTROYED THEIR OWN CRASH EVIDENCE. That is a very plausible part of how Conn Man's owens-laser stack vanished (#35) -- he had every reason to poke around after crashing. Each bat now writes its OWN log (solo.log / joyconfig.log), rotates it, and never touches join.log. Their launch-forensics + "send the operator" lines are repointed to match. VERIFIED in the zip layout (the bats need build\ + content\ beside them, so a repo-relative test is meaningless -- my first two attempts hit the badpath guard and proved nothing): solo.log = [boot] btl4 4.11.599 ... (the NEW run) solo.old.log = OLD-SOLO-CONTENT (rotated, not deleted) join.log = SEEDED-MP-EVIDENCE (untouched by the solo run) 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>
|
||
|
|
9bb95d8580 |
steam-networking KB: the first real multi-player field verification (night 4)
Three back-to-back Steam lobbies, 3+ players, host-and-join, no operator console: launch worked, DAMAGE APPLIED (previously suspected dead over Steam), 2 kills, clean round end, next mission launched. Records what worked, the one genuinely Steam-side defect found (#68 silent exit on a failed join), and the triage lesson -- a bug found in a Steam lobby is only a Steam bug if it lives in the lobby/token/transport layer; the three replication symptoms that night produced (#67/#70/#55) are above the seam and would reproduce on the relay. 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> |
||
|
|
35bdb505df |
join bats keep the previous session's log as join.old.log
Conn Man's owens-laser crash stack (#35) was in his join.log -- and re-running join.bat deleted it before it could be sent. The bat now rotates instead: join.log -> join.old.log at launch. One generation of history, the exact insurance that would have closed #35 tonight. 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>
|
||
|
|
33487ab846 |
night ledger: post-night fix session status (guards verified, dup-pilot repro NEGATIVE, five fixes landed)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef0ec48f9a |
map dropdown = the AUTHENTIC console catalog; the relay names a phantom-map stall instead of holding silently
THE 40-MINUTE OUTAGE'S REAL FIX. The GUI's map dropdown offered every RES
name passing the type-14+26 existence check -- which includes INTERNAL
FRAGMENTS: artrucks is an include-node of arena1 (PROGRESS_LOG map anatomy:
arena1 -> {arenall -> cavern, artrucks}), not a mission. Picking it stalled
every pod's mission load forever with no error anywhere, and End/Re-arm kept
restoring the poisoned egg -- 22:02..22:49, three "different" failures, one
cause.
* eggmodel.CONSOLE_MAPS: the Mac 4.10 operator console's own adventure-tree
catalog (Console.ini; the same 8 the solo menu ships in btl4fe.cpp kMaps):
cavern grass rav polar3 polar4 arena1 arena2 dbase. ValueSets.maps now
offers exactly that, catalog order, filtered to what the RES carries
(permissive fallback only if the intersection is empty -- a foreign RES
must not present zero maps). artrucks is the one fragment the old filter
let through; now hidden.
* The relay WARNS at egg load/reload when the mission names a non-catalog
map (the camo-color-warning precedent: hand-edited eggs still work, the
operator just cannot miss it).
* The launch hold gains the PHANTOM-MISSION SIGNATURE diagnostic: 0/N ready
for 60s means every pod is stalled in shared mission content -- one line
naming the egg's map and the recovery (End Mission -> fix map -> Re-arm),
instead of the silent 10s HELD drumbeat the operator stared at for 40
minutes. Hint re-arms per launch.
VERIFIED: new suite scratchpad/test_map_guard.py (5 checks: catalog exact +
ordered, artrucks hidden, warning fires on a doctored egg, silent on a real
one, the 60s hint names the map); all five console suites pass. Python-only.
(While placing the hint, a blind text-insert broke the launch-received block's
indentation -- caught by py_compile before anything ran, repaired, and the
whole area re-verified by the rearm suite.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
becaddb662 |
console logs ROTATE instead of truncate -- tonight's evidence loss cannot repeat
Two destroyers, both in btoperator.py:
* Start Session zeroed operator_relay.log -- took the whole 23:08 session
(the reservoir-crash round's relay trace) minutes after it happened;
* Launch local instances os.remove()'d operator_N.log -- took the pod's
[crash] self-report block before it could be copied.
Both now rotate the previous file to <name>.1 (btrelay_park's pattern: drop
the old .1, os.replace, never block on a locked file). One generation of
history is exactly what post-mortems needed twice tonight. All four console
suites pass; picks up on the console's next restart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|