c18929b23ae8d45a2e912b39a1441c52550a811f
667
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c18929b23a |
KB: manual cross-check -- myomer seek 'Super Charge' top speed is 4.0-manual behavior DROPPED by the 4.10 binary (stat sheets: Normal 143 / Super Charged 182 / Gimped 40 kph; printed ratio 1.27 vs 4.10's vestigial gear ratio 1.43)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
58f49ed942 |
KB: the seek dial's real stake is TORSO TWIST (PowerWatcher zeroes twist while myomers not Ready); generator has no load model -- gears 1-3 identical on a healthy mech
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
cf8618bcb9 |
myomer system COMPLETE -- the 'WAVE 6 mover cutover' was a phantom built on
a misattribution; the authentic couplings are all live
THE CORRECTION [T1]: @004b9550/@004b95b8 ('ConnectToMover'/'Disconnect
FromMover') are MechWeapon::ConfigureMappables/ChooseButton -- their +0x31C
is the weapon TriggerState and **(mech+0x128) is the CONTROLS MAPPER's
button roster (the MECHWEAP.CPP assert string + the trigger-edge detector
@004b9608 sit adjacent in the image). The binary has NO dynamic myomer->
speed feed: AvailableOutput has exactly TWO callers in the whole image --
RegisterMaxOutput (assembly) and the SeekVoltageGraph sampler -- and
speedEffect@0x31C is a published GAUGE attribute.
The authentic myomer system (now fully live):
1. drive heat (#85, this morning);
2. VITAL death -- myomers are the only authored vital=1 subsystem
(BTL4.RES res+0x48 scan): destroying them kills the mech via the #80
vital-crit path (this is also the ram-death mechanism -- myomers are a
0.35-weight collision-rattle target);
3. the assembly-time gait cap: RegisterMaxOutput @004b8ef0 raises
mech+0x7A0 to AvailableOutput(top gear) = base x ~1.4284 -- now called
(idempotent max, per tick; the 0x358 layout lock leaves no latch room)
with real OwnerBaseSpeed/OwnerMaxSpeed bridges; the old per-frame
reverseSpeedMax2 heal that would have clobbered it is garbage-guarded;
4. the ENG-page power curve at correct absolute scale (real base speed);
5. electrical sourcing (low gears run on a browned-out generator).
REMOVED (inventions): the mechmppr `speedDemand *= speedEffect` demand
multiplier + BTMyomersDriveOf + the Myomers ConnectToMover/MoverAttach
family. Damaged-but-alive myomers heat faster and drop the power curve
but do NOT slow the mech (gitea #75's premise is falsified by the binary;
the real damage slowdown is the LEG-damage gimp gait). Seek gears change
heat/sourcing/graph -- never speed.
Verified live: a mech at myomer dmg=0.234 (speedEffect attr 0.766)
beelines at full commanded speed with dmgGain=1.234 heat; 0 faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
|
||
|
|
31552c0444 |
#88 part 2: the central coolant pull-through -- HeatSink::DrawCoolant was a
return-0 stub; the Reservoir BAR now drops when a damaged mech leaks The user watched the coolant panel's level bar during the leak bench and it never moved -- correctly: a leaking subsystem drained only its own loop, because the slot-14 top-up (DrawCoolant) was stubbed. Disassembled the real base @0x4add00: the draw RECURSES UP the sink linkage, scaling by coolantFlowScale@0x15C per hop, terminating at the Reservoir's supply (@0x4af3b0 -- clamp to [0, coolantLevel], drain the tank, return granted -- already faithfully ported as Reservoir::DrawCoolant). The binary's terminal hop is the bank's slot-14 override @0x4ae8b0 resolving its reservoir connection @0x1D8 (the port's 'helper' member -- not vestigial after all); the port's existing Reservoir-ctor Attach + Reservoir override terminate the same chain equivalently (the extra bank hop scales by ctor-default 1.0). Implemented the base recursion (null-guarded -- the binary calls through the resolved link unguarded; authored topology always links) + a BT_COOL_LOG [resdraw] probe at the supply. Verified live: a destroyed myomer's damage leak pulled the central tank 6.0 -> 5.58 over ~30s ([resdraw] granted lines) -- Reservoir/CoolantMass, the cockpit coolant bar, now visibly drops as a damaged mech bleeds coolant, and DeathReset refills it on respawn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
a8a0042f28 |
#88: coolant leaks LIVE -- the #64 damageZone shadow was the single break
in the leak chain HeatSink::UpdateCoolant prices the damage-driven leak from the QUALIFIED engine member (Subsystem::damageZone @0xE0): coolantDraw = zoneDamage * heatLoad, with the coolantActive hysteresis (= the ReportLeak attribute the 19 authored leak watchers ride). But the MechSubsystem ctor only ever filled its re-declared SHADOW member (gitea #64, gotcha #1), so the engine member stayed NULL forever, zoneDamage pinned 0, and a coolant leak was STRUCTURALLY IMPOSSIBLE no matter how much damage landed -- Oracle's night-7 report exactly. The #80 crit fix and the #83 collision rattle write real subsystem-zone damage, but into an object the leak reader could never see. Fix: alias the ENGINE base member to the same zone in both MechSubsystem ctors. Every shadow reader keeps working (same object); the engine base Subsystem::TakeDamage latent null-AV (#64 consequence 2) is disarmed; the full #64 de-shadow sweep remains the long-term cleanup. Verified live (solo, BT_COOL_LOG/BT_CRIT_LOG): collision rattle drove a damaged Myomers to [cool] draw = dmg*heatLoad with the level draining; sustained weapon fire rolled real crits ([critroll]) and produced 588 leak-pricing lines. Authored routing extracted from BTL4.RES: collision rattle targets HeatSinkBank 0.3 / Gyro 0.35 / Torso 0.25 / Myomers 0.35 (Condenser+Reservoir authored 0 for collisions); weapon crits select via the zone's crit-entry list. Known-remaining fidelity item (documented): HeatSink::DrawCoolant (slot 14, central top-up) is still a return-0 TODO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
1570983fde |
#85: myomer drive heat LIVE -- the five Mech motion operands were return-0
shims, so the reconstructed integrator @004b8d18 accumulated zero forever
The integrator, its gates and the heat plumbing were all reconstructed and
running -- but OwnerVelocityMag/OwnerDriveMag/OwnerVelocityY/OwnerMotionGain/
OwnerMass were cross-family compile shims returning 0.0f, so the myomers
priced ratio^2 * damageGain * 0 every tick: ice cold at any seek (Oracle's
night-7 panel-confirmed report).
Operands re-grounded against the raw disasm + decomp and mapped to NAMED
members via a complete-Mech-TU bridge (BTMechMyomerMotionSample, mech4.cpp),
per the databinding rule:
+0x1C4 vec -> localVelocity.linearMotion
+0x82C vec -> localAcceleration.linearMotion (the authentic 15-ring
smoothed AccelerationLastFrame the port already maintains)
+0x20C -> moverMass (the collision divert's cell; the old "motion
gain" label was a misread)
*(+0x250) -> environment gravity (FUN_00421e2c: vy -= **(+0x250)/tick),
via GetEnvironment()->gravityConstant
So the heat physics reads: 0.005 * 1/2 m v^2 (kinetic work, per-tick/no-dt
in the binary -- kept verbatim, frame-rate note documented) + 0.2 * m g
|vy| dt (climb power) + 0.2 * m |v||a| dt (acceleration power), all scaled
by ratio^2 and (1 + zone damage). Restored the binary's fabs(v.y) (the
draft had signed vy -- descending would have COOLED).
Authored tuning extracted from BTL4.RES (18 mech records, one shared
tuning): VelocityEfficiency=0.995, AccelerationEfficiency=0.8, gears
0.3/0.5/0.7/0.9999 rec idx 2; myomer thermal profile 77/1000/2000,
conductance 1.9e5, thermalMass 2.5e5.
Live-verified (solo arena, BT_MYO_LOG + BT_HEAT_LOG): standstill generates
~0; hard circling drove Myomers T 77 -> 1225 (past the 1000 degradation
line) and the coolant loop conducted it back to ~520 equilibrium on
slowing; damageGain live at 1.048 from grind rattle.
Known-inert remainder (filed in open-questions): the climb term reads g=0
because Mover::localEnvironment is never populated in the port
(EnvironmentZone res type 23 unwired); the mover feed (speedEffect ->
locomotion) stays WAVE 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
|
||
|
|
37dd7f9663 |
collision-pricing audit: restore the binary's crash economy -- two fixes
A. KNOCKDOWN PACING: the 0.4s gBlockCooldown contact-hysteresis is gone. It suppressed the crash knockdown for as long as contact was held, letting the kinematic drive re-slam the obstacle at full commanded speed EVERY FRAME -- continuous full-price crash self-damage, a healthy 65-tonner dead in ~2-3s of grinding (the night-7 SAURON ram death, 110 priced frames at |v|~34.7). The binary needs no guard (crash threshold @0x4ab178 read from the exe = 0.0, dispatch per blocked frame is authentic): the bmp clip zeroes the drive, and a pressed mech re-triggers the knockdown at v>6.3 long BEFORE its grind can price anything -- the type-0 divert's 0.5-pt free floor needs v>~18.5 at 65t. Wall grinding is free taps + paced staggers; only genuine fast arrivals pay. B. RAW RAM DISPATCH: the *0.001 "ram economy normalization" (2026-07-12) predated the #83 type-0 divert and became a double-normalization -- the victim's divert scale (~4e-5) compounded with it, so NO physically possible ram could clear the 0.5-pt free floor: rams were a no-op against the victim while the rammer's own un-scaled crash path rattled for real. The binary dispatches raw [T1 @part_012:15324]; the divert IS the normalizer. Also un-starves cultural-icon crunches (trucks now crush on contact per their authored armor, not after ~19 bumps). Bench (scratchpad/night7/mp_rampricing.sh): - wall leg: 150s full-throttle wall push = 125 paced knockdown binds (~1.2s cadence, iv2 40-43), 4767 crash frames ALL under the free floor, 0 rattle, 0 deaths (pre-fix: dead in seconds). - ram leg: BT_GOTO=enemy + BT_GOTO_STOP=2 rams the peer; raw amount arrives byte-identical on the victim (66239.1 tx == rx), victim prices 2.55 pts internal rattle (first ram damage to a victim in the port's history), pressed follow-ups free, 0 deaths both nodes, 108 knockdowns -> exactly 108 type-5 records on the observer (no skating regression). KB swept: combat-damage.md (the 1.3e6 moverMass mis-attribution corrected to the measured 60-90k tonnage scale), locomotion.md, rendering.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
054c3f2eee |
#94: replicant un-wreck now rides the DEATH-STATE EXIT edge -- the zone
falling-edge latch was blind to zone-less (collision) deaths The peer wreck lifecycle listened to two different signals: wreck ON came from the replicated death explosion (unconditional), wreck OFF from a damage-zone falling edge (wasWrecked latch, armed only when a zone crossed >= 1.0 on the observer). The #83 collision damage prices as internal rattle and never moves a zone, so a fresh mech killed by ramming could NEVER un-wreck on peers -- the live respawned mech drove around wearing the hulk (night-7 field report: SAURON's Loki, screenshotted by both observers at the exact minute the log analysis places the ram death). MechDeathHandler::Tick now tracks prevMode and fires the rebuild + warp when the replicated MovementMode leaves the death modes {2,9} -- only Mech::Reset ever does that, and the state rides every record header, so the trigger is cause-agnostic. The zone falling edge just refreshes the cache. Side effect fixed for free: the peer respawn warp vortex now plays for zone-less deaths too (same gate). Bench (scratchpad/night7/mp_zoneless.sh + the new BT_SELF_DAMAGE_TYPE=collision harness option, which dispatches type-0 through the real TakeDamage divert): 14/14 zone-less deaths un-wrecked (mode 9->1) with zero zone rises on the observer; explosive regression leg 12/12 with zone destruction active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
e91d447405 |
#82 FINAL LAYER: the peer body-channel trn-lock -- a circling limper skated because the peer's turn-arm had no walk-demand yield and the trn speed exit read a dead replicant mapper cell
The master limped clean (218x gimp-cycle records, zero trn) -- a _ReturnAddress trap on SetAnimationState(4) proved ALL peer trn arming came from the port's own mech4 turn-step block, not the type-3 reader. Two dead ends, one root: - mech4 peer arm: gated on !wantsWalk (standSpeed < bodyTargetSpeed), exits trn when walking demand appears -- the leg twin's authentic precedence (part_012.c:12013, the Standing speed test outranks the turn test). - mech2 body case 4: bspd reads bodyTargetSpeed on ReplicantInstance; the local mapper's speedDemand is a dead cell on a peer (reads 0 forever -> no exit). Verified 2-node timeline: arena circle replicant 218x state-24 / 0x state-4 (was 116x trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths. Diagnostics kept: BT_TRNTRAP ra-trap, BT_ANIMIND_CAP, [gimpfeed], [replgimp] extension. KB: locomotion.md trn-lock entry + the dead-mapper-cell lesson. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRjrTQpJd6u9XyfnUbTB3v |
||
|
|
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>
|
||
|
|
19fe1e5fa2 |
KB: the wall-grind knockdown storm IS the surviving #82 skating -- A/B-isolated
Four-config matrix (mp_gimpAB.sh, [animind] replicant-state verdict): grass straight/circle = clean replicant limp (198 gimp entries; 17 car crunches during it -- crushable path broadcasts nothing, cars exonerated); arena1 straight = one wall strike, replicant recovers into the limp; arena1 circle = wall GRIND -- the bmp stagger clip's ~6.5 u/s root motion re-strikes the wall (6.5^2 > iv2 threshold 40), each rebind re-broadcasts the type-5 knockdown (26x in the user's run), and every peer's replicant lives in knockdown/recovery churn: gliding + trn leg-lifts = "skating". Turning while gimped is innocent; single strikes are benign. Authentic fix filed in open-questions: reconstruct the consumer of the stagger's action-request flags (FUN_004a4c54 bits 1/0x20 -- the drive suppressor while the bmp clip plays); the rebind guard at mech4.cpp:6861 is already marked bring-up-pending exactly that. 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> |
||
|
|
830a976192 |
#81: 4-node stress PASS -- 56 overlapping death cycles, zero strands; DEATHS tally exact
Four simultaneous networked nodes, all dying every ~35s on staggered timers so respawn handshakes overlap (the field condition that stranded 6 of 8 cycles): 56 cycles, every completed one START->RESET, 0 swallowed / 0 mismatch / 0 discarded / 0 ghost / 0 crash. Scoreboard: every node's PLAYER_DEAD rows show deaths=N tally=N in lockstep (13/13/15/15) -- exactly one increment per death, so removing the duplicate VehicleDead dispatch did not drop the DEATHS column and the latch release did not double it. The kill-credit path evaluated all 56 deaths and correctly declined each self-kill (NOCREDIT self=1); KILLS uses the separate ScoreMessage path the fix never touched. content/MP4.EGG: the 4-pilot bench egg (ports 1502/1602/1702/1802, novice). 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>
|
||
|
|
d4ba91bd39 |
#81 THE ANSWER: deathPending is OUR invention -- the binary has no death latch
Read BT's own respawn code and checked it against BTL4OPT.EXE itself. FAITHFUL: BT's Player::VehicleDeadMessageHandler (FUN_0042db80, part_003.c:12029) gates on message->deathCount == player->deathCount AND player+0x40 != 1 (simulationState != DropZoneAcquiredState) -- exactly WinTesla's two gates -- then runs the same closest-DropZone search skipping "win*" zones, dispatches AssignDropZone and re-posts to itself on a timer. So the retry loop and the cross-machine hunt are authentic 1995 behaviour, not a WinTesla artifact. NOT FAITHFUL: BT's BTPlayer::VehicleDeadMessageHandler (FUN_004c012c, part_013.c:10504) ENDS with *(param_1 + 0x290) = 0 -- it CLEARS the field our reconstruction calls deathPending, at the end of every death. Verified in the raw binary: +0x290 is written in exactly three places in the whole executable (0x0b75fb, 0x0bffe3, 0x0c0a05) and ALL THREE store a zeroed register (xor ecx,ecx / xor eax,eax / xor edx,edx immediately before). There is NO write of 1 or of any non-zero value to +0x290 anywhere in BTL4OPT.EXE. Two are ctor/reset sweeps; the middle one is the death handler, sitting right after the call to Post and add esp,0x14, matching the decompiled tail exactly. So 1995 has NO death-pending gate. We invented it (btplayer.cpp:505) and then needed six clear sites to patch the strandings it caused (:382 :469 :1431 :1442 :1461 :1532). #57 and #55 are artifacts of that invention. It is also what makes a ghost PERMANENT: in BT a failed respawn is harmless (the 2s re-post keeps hunting, the next death starts a clean cycle); in ours the first failure latches the pilot and every later death is SWALLOWED forever -- exactly the field signature of 8 cycles, 6 stranded, none recovering. Fix proposed in the doc (match the binary: clear instead of latch, drop the dedup gate) but NOT applied -- six sites depend on the latch and the dedup is load-bearing, so it wants a deliberate two-node bench, not a 1am edit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bf0555ccf1 |
#81: every MP respawn is a cross-machine round trip -- one degraded peer ghosts everyone
Two-node bench with the fixed host:local labels shows each player's drop-zone
request is answered by the OTHER machine's DropZone: A asks, B grants; B asks, A
grants. FindGroup("DropZones") iterates replicants of remotely-mastered zones
too and takes the geometrically closest, so a respawn is
my request -> an arbitrary peer's DropZone -> that peer's reply -> back to me.
With 5 players that is 5 round trips through arbitrary peers, and ONE degraded
peer can strand everybody else's respawn. That finally explains the otherwise
unexplained field datum that one machine stopped processing the death-transition
stream 57% into a match and never recovered (0 explosions/wreck swaps/burials
while four other machines logged 4/4/4) -- a node in that state cannot answer
anyone's respawn. It also explains why solo is 100% reliable (in-process) and
why a healthy 2-node bench passes.
Also fixes the instrumentation before it costs a night: every [dz] line printed
"entity 1" because a player's LOCAL entity id is 1 on every machine -- with five
players the log would have said a respawn stalled but not WHOSE. All [dz] lines
now print host:local (including the usedBy= owner of each busy slot).
Doc: two candidate fixes recorded (prefer a locally-mastered DropZone / make the
reply path tolerant of a deathCount that is ahead of ours), neither to be guessed
at -- the [ghost] DISCARDED line's mismatch direction decides it in one line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
372cbfa344 |
docs: GHOST_MECH_ANALYSIS -- the field analysis, durable across sessions
The ghost and the "wreckage moving and shooting" are the SAME failure at two ages: a stranded respawn (drop-zone reply never arrives) leaves the mech never Reset and never repainted, and the render-side wreck swap is ONE-WAY -- so the pilot drives a burning hulk that sinks (pure ~18s timer, btl4vid.cpp:1277) and then cannot be drawn at all. Records: the 8-cycle ledger (only 2 of 8 reached RESET; stranded correlates with ghosting 8/8 and 10/10); the host did not ghost because he never died; the SWALLOWED warning is BENIGN (100% base rate) and two earlier readings of mine that were wrong (spurious resets = aggregation artifact, per-match player IDs); the WinTesla-vs-1995 provenance caveat on DROPZONE.cpp; what the new instrumentation already killed (slots=8, so dropzone=one is not the bottleneck); and the four-separate-bugs breakdown of the "desync cluster" including the machine that stopped processing the death-transition stream mid-match. Raw per-machine logs + the verbatim agent findings stay in scratchpad/night6/ (uncommitted -- machine names + Steam identities). 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> |
||
|
|
1ac425860e |
particles.png recovered (#79, found by cyd) -- particle texture ships at last
content/VIDEO/particles.png (128x128 RGBA): the texture L4PARTICLES has tried to load since the engine was written. Boot-verified: [particles] device objects created (max=8192, texture=loaded). Every particle effect now draws textured -- the authored look; the arcade pods displayed untextured quads because the file never shipped. Comments updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cfa52a28d8 | Merge remote-tracking branch 'origin/glass-cockpit-refit' | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |