832bec0966fe163c89999a7eb717bc1cb14815fc
208
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7b003243ae |
#146 respawn: release the DESKTOP throttle -- and close #137, which was never a bug
#137 ("respawn came back with MYOMERS heat MAXED", Oracle; "overheating generator D", Sauron) sent us through a full two-sided audit of Mech::Reset and the whole RTIS chain. Both sides were correct. The answer was in the field log all along, one line after the reset: [respawn] Mech::Reset 3:30 healed+moved to (...) alive=1 [techstat] ... every live condition CLEARED [techstat] Myomers condition 3 SET <- Overheating, immediately [mppr] in thr=1 -> ... [gaitSM] cycleSpeed=14.6 state=12 <- already RUNNING [techstat] Condenser5 condition 3 SET <- "dumping into coolant loop 5" [techstat] GeneratorD condition 3 SET <- Sauron's generator D The mech respawns STILL UNDER POWER and earns the heat honestly. Two facts close it: 1. condition 3 is an OPERATING flag, not an alarm. Census over one match: LLaser_2 33 SET / 33 CLEARED, LLaser_1 31/31, SRM4 26/26, PPC_2 18/18 -- every volley trips it and clears it. EVERY subsystem is balanced (GeneratorD 5/4, Myomers 5/4, Condenser5 1/1; the extra SET is only the log ending mid-heat). cond 6 BadPower behaves the same (Myomers 8/8). Nothing latches. A post-respawn SET is not evidence of anything. 2. Mech::Reset's subsystem loop starts at index 2 and the ControlsMapper is index 0, so the throttle is never reset -- and the BINARY does the same. That is right for a pod: the throttle is a PHYSICAL lever still under the pilot's hand. Respawning under power is authentic and stays. Oracle's read that the myomer heat rate "felt right" was correct. WHAT IS a real defect (#146), desktop only: the glass bridge merely EMULATES that lever, with the static ramp accumulator sLever (mech4.cpp:3250) zeroed ONLY by the X all-stop and a direction-crossing snap. A pad/keyboard pilot is physically holding nothing and cannot see the lever, so they respawned at speed for no reason they could perceive -- and ate the heat load above. The Thrustmaster/RIO path was never affected: InterpretControls (@004d2150) rebuilds throttlePosition every frame from the databound throttleForward. Fix: queue the existing all-stop at Mech::Reset, reusing the proven path (it already clears the zero-crossing detent too). LOCAL VIEWPOINT MECH ONLY -- gBTDrive is the local bridge's state and Reset also runs for replicants, so an ungated write would all-stop the player whenever a REMOTE mech respawned. Pod-safe besides: with a RIO present the key bridge is off and gBTDrive.throttle is never read. BT_NO_RESPAWN_THROTTLE_RELEASE=1 reverts. Benched 2-node (scratchpad/night13/throttlerespawn.sh): the release fires 1:1 with local respawns on both nodes independently (A 2/2, B 1/1) and never spuriously. HONEST LIMIT: the viewpoint gate was NOT stressed -- B ran Mech::Reset 0 times for A's mech, so the remote-respawn path never fired. The gate is correct by construction (the isPlayerMech idiom), not proven. BT_AUTODRIVE cannot test the lever itself (forced mode reads forcedThrottle, never sLever), and the zeroing path is the X button, proven in the field. Also keeps BTReportHeatAtReset (heat.cpp, BT_HEAT_LOG): the [heat-t] census runs on a 5s timer, far too coarse to sample AT the reset. It is what proved every roster subsystem including all six Condensers sits at T=77 start=77, and it corrected an earlier false negative from filtering on IsDerivedFrom(HeatSink). KB: context/decomp-reference.md gains the routine/self-clearing condition semantics + this post-mortem, so it is not re-chased; cross-ref in context/gauges-hud.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC |
||
|
|
1f0923747b |
Mech::Reset: restore the POSTURE clears the port had dropped (#142)
Oracle: "crouch wasn't resetting on respawn ... mechs always spawn standing".
Correct -- Mech::Reset (@0049fb74) stands the mech up and the port cleared
none of it:
*(this+0x398) = 0 duckState
Set_Alarm_Level(this+0x39c,0) legStateAlarm -> standing
Set_Alarm_Level(this+0x714,0) bodyStateAlarm -> standing
*(this+0x650/0x654/0x658) = 0 death + leg/body reset latches
*(this+0x5ac) = 1.0f idleStrideScale
A pilot who died CROUCHED came back crouched -- leg parked in 'sqd' -- and now
that the cockpit strip works, showing the up-arrow "press to rise" frame on a
standing mech.
Benched (crouchrespawn.sh): A squats, dies while down, respawns -> legLvl 0
(standing) after Mech::Reset. Weak but the failure mode (stuck legLvl=1) is
absent. NB the first attempt was void: force-damage kept A dying before it
could crouch (legLvl 22/24 = death clips), so the run tested a STANDING death.
Switched to self-damage so the mech is stopped long enough to crouch.
Also carries the #142 gauge work: the crouch strip is a BUTTON-STATE indicator
(grey unavailable / orange down-arrow ready / orange up-arrow crouched),
decoded by rendering BDUCK.PCC rather than inferring it; and the gauge
factory's missing-image path no longer uses the no-op DebugStream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
03c4d55672 |
#142 crouch: duckState is a THREE-state posture -- the strip is an animation
Operator confirmed on screen: bduck.pcc is a real duck ANIMATION, and stepping
duckState 0->1->2 plays it. So the attribute is not a flag:
0 = standing 1 = moving between 2 = crouched
Everything else was already right -- asset, element (OneOfSeveralPixInt
@004c5204), factory registration, L4GAUGE.CFG:5001, and the attribute binding
(new [gauge] receipt confirms 'bduck.pcc' frames=3x1 attr=BOUND). We were
writing a two-value flag into a three-frame strip, so frame 2 was unreachable
and the cockpit saw a snap: "it lights up and sticks, no animation".
The handler is back to the binary's exact write (duckState = 1, @0049fa00).
That value now MEANS the middle frame, so the press gives immediate visual
feedback and the earlier toggle divergence is retired.
Needed a separate duckRequest cell, which I tried twice to avoid:
* duckState cannot be both the request and the display. Settling it to the
real posture destroys the request, so on the frame the squat clip parked
the consumer read "crouched + pending" and issued the opposite direction --
69 transitions from 2 presses, benched, twice.
* reading the CACHED legAnimationState instead of the alarm made it worse:
the cache refreshes only at the top of AdvanceLegAnimation, so right after
SetLegAnimation it still reads the old state. Read the alarm.
duckRequest is port-only, appended, never read by offset.
Also fixes a silent failure in the gauge factory: the missing-image path used
DebugStream -- the no-op ReconStream (project gotcha) -- so a strip that failed
to load reported NOTHING. Now DEBUG_STREAM, plus an ungated one-line receipt
per element naming the image, frame grid, port and whether the attribute BOUND
or came back NULL. That receipt is what proved the element was healthy and
sent me looking at the value instead of the plumbing.
Benched (crouch142.sh, madcat): 2 presses -> exactly 2 transitions,
SQUAT -> parked (settles to 2) then RISE (settles to 0). Refusal while moving
still holds (posture=0, authentic per Lynx).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
fcd1a0ca8d |
#142 crouch: refuse-and-snap, not queue -- and honour the must-be-stopped rule
Fixes a regression I introduced in
|
||
|
|
59f53da07b |
#142 crouch: duckState is the POSTURE the cockpit animation reads
The crouch symbol animation is fully present and we were starving it.
content/GAUGE/BDUCK.PCC the 3-frame strip
OneOfSeveralPixInt @004c5204/@004c52d8 the element, reconstructed
btl4grnd.cpp:144 registered in the factory
L4GAUGE.CFG:5001 oneOfSeveralPixInt(
E,ModeAlwaysActive,
bduck.pcc,3,1,DuckState)
ATTRIBUTE_ENTRY(Mech, DuckState, duckState) attribute 0x37
A 3-frame mech symbol beside the CROUCH button, indexed by duckState -- the
standing<->crouching animation Lynx and Draco describe. It never played
because the consumer zeroed duckState the frame after the press, in BOTH
directions, so the strip sat on frame 0 with a one-frame blip to frame 1.
That is the field report verbatim: "button flickers sometimes on press ...
state does not change. Remains in stand mode."
THE ZEROING WAS OURS. Every writer of +0x398 in the export is the
DuckRequest handler (=1) and Mech::Reset (=0). FUN_004a9b5c -- the master
perf, which contains the address the old comment cited as "the DuckRequest
consumer (@0x4aa011)" -- does not reference 0x398 at all. mech.hpp's own note
already said "duckState has NO code reader anywhere in the decomp ... whatever
consumes it consumes it through DATABINDING". The databinding consumer is
this gauge strip, and we were clearing it behind the gauge's back.
Restructure: drive on DESIRED vs ACTUAL. duckState is the desired posture;
the parked leg alarm is the actual. Act only on a mismatch -- no re-fire, and
nothing clears the attribute. A frame where mapPosture is not ready now
RETRIES (throttled [duck] WAITING) instead of silently dropping the request,
which retires the old "request consumed, posture=N" miss as well.
ONE DOCUMENTED DIVERGENCE: the handler now TOGGLES. The binary writes a bare
1 and clears the cell only in Mech::Reset, with no per-frame reader, so a
second press could never rise -- and a pod pilot's second press must un-crouch
(Lynx: "Mech is immobilized until crouch is pushed again, and mech rises").
One cell, same meaning, noted at the site.
Benched (crouch142.sh, madcat):
duckState -> 1 (crouch) -> SQUAT -> [holds 1 while crouched] ->
duckState -> 0 (rise) -> RISE
Value now persists across the crouched period instead of blipping, so frames
0/1 of the strip are reachable and stable. Also removed the interim REQUEST
DROPPED receipt: after the restructure nothing is dropped, and a receipt that
says otherwise is a trap for the next session.
STILL OPEN on #142: no immobilization while crouched (Lynx) -- nothing gates
movement on duckState or the parked leg alarm. A driven mech with a parked
leg channel is the [skate] signature (#52), so it may not be cosmetic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
0530366687 |
#142 crouch: the mech is fine -- it is a missing PANEL ANIMATION
Benched solo AND in MP on the same chassis: both presses reach
DuckRequestMessageHandler, zero drops, SQUAT -> squat clip parked -> RISE.
Locomotion is not the bug, and MP is not refusing it.
Added an ungated [duck] REQUEST DROPPED receipt at the consumer's silent miss.
The squatCapable==0 path skips the consumer entirely AND leaves duckState
latched at 1 with NO log today; the posture-gate miss logged only under
BT_DUCK_LOG, which no player sets. Neither fired on madcat.
What the pilot sees, traced with BT_LAMP_LOG: the button lamp is momentary
press feedback, not state --
PRESS -> [lamp] 0x13 <- 0x3c
SQUAT -> mech crouches, clip parked
RELEASE -> [lamp] 0x13 <- 0x14 <-- while still CROUCHED
so crouched and standing look identical.
Era testimony corrects the scope: the button should ANIMATE A MECH SYMBOL
beside it, standing <-> crouching (operator). Lynx: 'When a mech stops,
crouch button lowers its stance and plays crouch animation. Mech is
immobilized until crouch is pushed again, and mech rises.' Draco concurs.
Two real gaps, neither fixed here:
1. no immobilization while crouched -- nothing gates movement on duckState
or the parked leg alarm. NB a driven mech with a parked leg channel is
the [skate] signature (#52), so this may not be cosmetic.
2. no stance symbol -- no gauge element draws one, and the decomp carries no
crouch/squat/stance/duck graphic string, so it is an authored IMAGE on the
secondary MFD; find it in that gauge's element list, not by string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
61f21107b4 |
#108 THE EJECT GHOST: the death-edge latch tested the wrong field
One substitution, three field symptoms. Mech::TakeDamageMessageHandler arms
the whole death tail -- kill report, VehicleDead, death blast -- from a
was-alive-at-entry latch:
const int deathBlastArmed = !IsMechDestroyed(); // graphicAlarm >= 9
The binary tests movementMode 9|10 there (@0x4a0303). The port swapped in the
graphic alarm and justified it: "the death transition sets mode 9 synchronously
with the structural flag on every path through here, so the edges coincide".
True of every DAMAGE path. False of the one that matters:
Mech::EjectPilotMessageHandler raises graphicAlarm to 10 (the EJECT state)
BEFORE dispatching its self-damage, while movementMode is still 1. So on an
eject the handler entered already reading "destroyed", the latch never armed,
and the death tail was skipped entirely -- including VehicleDead, which IS the
respawn trigger.
Everything the field reported on night 13 follows from that:
* "they all self destructed with panic button and didn't respawn properly"
-- no VehicleDead, so no drop-zone hunt, so no respawn;
* the EJECT GHOST -- the peer wrecks the mech and never un-wrecks it, because
the un-wreck rides the master's respawn. Normal deaths replicated fine all
along (9 deaths -> 8 un-wrecks, benched), which is why only ejects ghosted;
* the manual chart's "-1000 ejecting" never materialised -- the negated kill
award and the death cost both live in the tail that never ran.
Fix: use the binary's own predicate. MovementMode is untouched by the eject's
alarm write, so the latch arms on an eject exactly as on a combat death.
WHY SEVEN RIGS MISSED IT: the punch-out was being REFUSED, not undelivered.
EvaluateEjectPermission (@0049fa1c) grants only on
liveWeapons < ejectMinWeapons || liveGenerators == 0 || coolantFrac < 0.05
|| (leg-gimped && !simLive)
-- armour damage satisfies none of them, and every bench ejected a healthy
mech. An [ejecttest] receipt (2 lines) proved the dispatch fired every time
and the handler declined; the "[eject] REFUSED (mech not crippled enough)" line
was sitting in the very first bench log, ungrepped. BT_KILL_SUBSYS's
comma-list form ("GeneratorA,GeneratorB,...") was already built for this bench.
Verified 2-node (scratchpad/night13/ejectreal.sh), before -> after:
PUNCH-OUT landed 0 (785 refusals) -> 1, charge=500
peer wreck-enters 1 -> 1
peer UN-WRECKS 0 (the ghost) -> 1
eject score (type=2) absent -> award=-1000.00, score 1000 -> 0
death cost never ran -> APPLYING, penalty=500
That -1000 is the manual chart's eject row to the digit: killBonus 500 plus the
500 self-damage tally, negated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
|
||
|
|
f44be87ab2 |
#60 PART 2: the RE-EXPORT -- dark code 90KB -> 41KB, coverage 87.3% -> 93.5%
Installed JDK 21 + Ghidra 12.1.2 (no admin, %LOCALAPPDATA%\bt411-tools beside DXSDK/cmake; runner uses 8.3 SHORT paths because Ghidra's .bat expands %JAVA_HOME% unquoted and the profile has a space). New tooling: reference/ghidra_scripts/ExportGaps.java -- ExportAll's exact output contract PLUS a gap-fill pass (force disassembly + createFunction at E8 call targets outside functions, data->code pointers at a plausible prologue, and the census's discovered starts; iterated to a fixpoint, logged to gapfill_report.tsv). tools/ghidra_reexport.sh (headless runner, 'reprocess' mode) and tools/gapdiff.py (score two censused exports); gapcensus.py now censuses any export dir. Results: 6267 -> 6472 functions (+205 created in 2 rounds: 195 census starts, 6 call targets, 4 data pointers; 56.1KB newly covered), ZERO decompile failures. Dark real code 90.4 -> 40.8 KB (54.8% recovered); game-side dark 53.1 -> 21.1 KB; regions 428 -> 321. EVERY historically dark function now has pseudocode -- including @0x4c05c4 VehicleDead, the absence that opened this issue. VALIDATION: the new pseudocode confirms this week's hand reconstruction of the crouch field-for-field (mapPosture/duckState/squatCapable/myomerEff/ novice gate/SetLegAnimation/ForceUpdate/stability alarm) -- and exposed one branch the raw pass missed: AIRBORNE AUTO-RISE (mode 3|4 && legState 1 -> forced squ), now implemented in mech4.cpp and re-benched un-regressed. PROMOTION: the re-export is canonical reference/decomp/; the previous export is preserved at reference/decomp/archive_2025export/ so old `part_0NN.c:LINE` citations still resolve (addresses are stable across both; line/shard membership is NOT -- cite @ADDR). New lead recorded: @0x4c0904 is the MASTER BTPlayer Performance (team resolution, EndMission console post, score heartbeat) -- our @0x4c083c PlayerSimulation attribution needs a re-check. KB: source-completeness, gotcha #20 (the rule is cheap now -- look it up), CLAUDE.md router/layout. Log: phases/phase-04-gap-census.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e1c3f2db6a |
Myomer factor: correct the false 'feeder unreconstructed' claim + wire the
crouch gate to the LIVE drive value The myomer system was ALREADY COMPLETE (2026-07-31 seek audit): Performance wrapper @004b8b9c, AvailableOutput @004b8ac0 (gear clamp x quadratic heat degrade x (1 - zone damage)), and the master-perf chain walk + speedDemand scale + turn freeze in mechmppr.cpp:990 -- the same @0x4a9cf2-0x4a9da4 bytes the crouch dig re-decoded. The 2026-08-05 banners calling the feeder dark were an export-gap-blind grep (named members, not offsets). Fixes: mechmppr publishes the chain MAX into mech->myomerEffectiveness (the binary's +0x79C home) so the crouch posture gate reads the live factor (dead/overheated myomers now genuinely refuse squat/rise -- previously the gate read a neutral 1.0 and never fired); the duplicate speedDemand multiply in the posture block is removed (mechmppr's is the one application); banners and locomotion.md corrected. Squat re-benched green on the live wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
591d205b19 |
CROUCH complete: full cycle + MP replication verified
The WIP's 'pose does not hold' was a chain of bench-instrument errors, not a code bug: every capture ran in COCKPIT view (the pilot cannot see their own legs; the eye-height residual masked as reversion). Joint probes prove the park holds indefinitely (knee 1.138, root -2.219 steady); the 2-node bench shows the observer's replicant fully crouched and held (duckmpA_031 -- the type-3 state record carries it with zero new replication code); the second scripted press (new BT_BTNTEST2 env) verifies RISE -> standing zeros. MP button delivery confirmed mode-mask-clean (the one miss was round-start jitter). Diags added, all BT_DUCK_LOG-gated: SetLegAnimation re-arm tracer, 1 Hz joint probe, RIO press mode-mask, BT_TREE_LOG topology dump, and the squat-park log. RESIDUAL filed: pilot's own cockpit eye does not ride the root drop (DPLEyeRenderable chain composition; cosmetic). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dd70061e0e |
CROUCH reconstruction (WIP): the master posture/duck machine, decoded + wired
Raw-disasm of the dark master-perf region (@0x4a9cf0-0x4aa0af): the myomer effectiveness factor (+0x79c, MAX over heatables' +0x31c, scales speedDemand -- feeder @004b8be3 unreconstructed, neutral 1.0 [T3]), the posture selector (+0x3f8: mode/novice/leg-state/myomer gates -- novices cannot crouch), and the DuckRequest consumer (standing -> SetLegAnimation(2) 'sqd', ducked -> SetLegAnimation(3) 'squ', ForceUpdate 8+1 ships the type-3 state record, stability alarm flips, request consumed). +0x1DC = mountSegment... er, the searchlight learned that one; here: mapPosture @0x3f8 + myomerEffectiveness @0x79c members land; value-space note (port normal mode == 1, binary 0). VERIFIED: request->consumer chain fires ([duck] SQUAT), the sqd clip plays (22kf/7joint, ends root -2.22 crouched -- clip data parsed from BTL4.RES, squ is its exact mirror; loader slot map re-verified byte-exact). OPEN: the parked crouch pose does not HOLD on screen (reverts ~1 frame after clip end with NO SetLegAnimation re-arm logged) -- the hold's render path is the remaining dig; [duck] re-arm tracer left in SetLegAnimation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
91bd28669e |
#45/#134 authentic score/death report tail -- replaces the scoring stand-ins
Reconstructs the dark-gap tail of Mech::TakeDamageMessageHandler
(@0x4a02f4-0x4a0890, raw disasm): the three id-0x16 score reports (kill to
the shooter's player / type-0 wire-fidelity / received to the victim's
player) and the BT 0x38-byte VehicleDeadMessage extension {killed-by player,
kill zone} dispatched from the death tail. Retires BTPostDamageScore /
BTPostKillScore and the per-hit inflicted credit (never existed in 1995:
@0x4c0200 is bound in no handler-table entry -- byte-scan receipt in
decomp-reference). Suicides now dispatch and the handler negates the award
(the #134 panic penalty). Collision divert falls through to the death tail
per @0x4a0375 (wall deaths respawn + blast; no score). ScoreMessage fields
renamed to decoded truth (vitalHit/zoneIndex/subsysID) + wire asserts;
console VTVDamaged points_transfered corrected (Round(award), not Now()).
Benches: scorekill.sh cross-node kill (kills=1 award=4.88, killedBy=2:1
zone=3, single death cycle), scoreself.sh suicide (type=2 award=-39.00
kills=0), deathblast2.sh re-verified (72 bursts at ~9u).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
297127d0d7 |
#89 DEATH BLAST reconstructed: a dying mech splashes its neighborhood
The missing half of Advanced Damage, found by call-scanning Explosion::SplashDamage @0042fad0: TWO callers, not one -- Missile::Perform (the known #62 path) and 0x4a0bda, the UN-EXPORTED tail of Mech::TakeDamageMessageHandler itself. Raw disasm @0x4a07b8-0x4a0bda: when the victim ENTERS dead(9)/eject(10) during the applications, the binary sets the wreck burning (id 0x17, deferred -- handler not yet reconstructed), spawns the death Explosion (model 0x31 -- our death-list visuals stand in), and SPLASHES: gates : owning player's advancedDamageOn (+0x264) AND NOT suppressConsole (+0x258 -- eject sets it: PUNCH-OUTS NEVER BLAST, the authentic anti-suicide-bomb rule) damage: type 2 Explosive, amount = deathSplashDamage (mech+0x520), bursts = round(0.001 * moverMass * 15.0) -- scales with tonnage radius: deathSplashRadius (mech+0x524); per-victim falloff bursts/dist^1.25 in the shared core Draco's collision-divert suspicion is settled: the blast is TYPE 2, the divert never touched it -- the tail was simply never reconstructed. Port: deathSplashDamage/Radius PROMOTED from the Wword scratch bank to named Mech members (the bank is one GLOBAL array -- authored per-chassis values were clobbered to the last-loaded mech); BTSplashCore split out of the #62 weapon splash and shared; BTApplyDeathSplash + the death-edge arm in the handler tail; BTPlayerConsoleSuppressed bridge (friend). Bench (2-node, B parked 8.9u from a self-destructing A): blast fired with authored madcat data (radius=50, amount=5, mass=75000 -> 1125 base bursts), B took 73 bursts (falloff exact: 1125/8.91^1.25), cross-pod delivery + cylinder spray verified on B's own log ([dmghit] type=2 burst=73 across zones). ~365 damage at 9u -- Draco's 'double kills on drops' economy restored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
546aabd5ba |
#131 false lock FIXED: miss-means-miss -- the pick answers only for drawn geometry
Night-12 field report (Ronin/Conn Man/Oracle, blackhawk-correlated): lock ring lit with the reticle visibly off the mech + no-reg complaints. Root cause: TWO port stand-ins answered where the 1995 card (which cast against the DRAWN geometry) would miss -- the pick's any-object sphere fallback and the caller's whole-mech AABB fallback. The regime that exposes them: a LEVEL boresight over a SHORT mech -- the blackhawk's mesh tops out below eye-ray height, so the ray clears every triangle but pierces the fat cull spheres; the ring lights with the reticle above the mech's head (the operator watched exactly this on the sweep bench). Careful aimed-down fire rides triangles, which is why Oracle's per-panel audit passed on the same build. Fix: MechSegmentPick returns 1=drawn-geometry hit / 0=TRUE MISS / -1=no render tree; the sphere may answer ONLY for a mesh the reader cannot parse (pm==0 -- currently none exist: counters objs/invFail/noTri all clean); the AABB survives ONLY as the pre-tree replicant grace. A readable mesh the ray misses is a MISS -- no lock. Verified (2-node vs bhk1 at 100u, all runs on force-relinked string- verified exes after today's stale-link flake): - LEVEL lock-sweep: 0 locks all run (pre-fix: lock band from 168 sphere answers; picksrc tri=0 sphereFB=168). - DOWN-PITCHED sweep: locks return 100%% tri-sourced (tri=158 sphereFB=0), landing on real parts (rarm/ldleg/rdleg) with honest gaps. - Full zone-walk matrix: tri=18874 sphereFB=0 box=0; victim took 156 hits across 16 zones incl. both side torsos -- combat un-regressed. New instruments (all env-gated): BT_LOCK_SWEEP=<axis> torso pan (the operator-visible lock-envelope bench), [locksweep] transition log, BT_LOCK_ENVELOPE synthetic unit-sweep probe, [picksrc]/[pickbox] source telemetry with objs/invFail/noTri localization counters. Bench: scratchpad/night12/zonewalk_bhk.sh. NOTE for the field: locking is now strictly TIGHTER (ring = reticle truly on the machine). If era testers feel the pods were more forgiving, Draco's "slight lock linger" memory becomes a deliberate investigation (sourced hysteresis), not an accidental sphere halo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
26ab2fee3a |
Respawn-reset audit: valve restore + peer smoke cleanup (both binary-grounded)
Operator reports audited vs the binary (full matrix in RESPAWN_REARM_PLAN addendum): - VALVES (real gap): Condenser reset @004ae534 was missing from the decomp export -- raw disasm shows it chains HEATSINK (coolant refill runs; the old body chained HeatableSubsystem per the stale TCP shard) then, respawn- side, resets valveState to detent 1 and restores massScale from refrigerationFactor. Mech::Reset now also runs the binary's tail call (@0049f788 BTRecomputeCondenserValves) so flow fractions rebuild from the reset detents. Bench: detent 5 -> death -> "[respawn] Condenser1 valve detent 5 -> 1". - #129 SMOKE (real gap, peers-only): the replicant un-wreck edge rebuilt the model without the @004d0c14 per-entity effect cleanup, so the observer's last 10s wreck-plume window rode the teleport onto the fresh mech. BTStopEntityPfx now runs on the edge; bench shows no plume line after any un-wreck until the next death. - AUTHENTIC (no fix): weapon->generator taps persist (@004b0e6c only resolves the link) and MFD display/control modes persist (mapper vtables 0050f45c/0051e440 slots 8-11 = plain root bodies, read from the exe). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1f995ee35a |
#124 twist-sign VERIFIED correct (live missile bench) + the rig to do it
The frame-adapter's inferred twist-sign flip (SelectSlice theta -= twist, vs the binary's += pre-reflection) was the last unverified half of #124 -- every earlier probe ran at twist 0. Bench: stationary madcat target with the torso PINNED at 0 / +140 / -140 deg, LRM salvos from a fixed shooter (missiles = the authentic cylinder path; the binary DROPPED zone -1 beam damage). Result: slice picks track the physically-facing flank in BOTH directions (twist-left -> right-family zones for left-flank impacts, twist-right -> left-family), deterministic, wrong-sign outcome (slice 7 vs observed slice 1) clearly excluded. No game-code change needed. Instrumentation added (all env-gated): - torso.cpp BT_FORCE_TWIST=<-1..1>: HOLD the sim's analogTwistAxis (the input-level pin was dead -- live input rides the CONTROLS.MAP device push, and Basic mode auto-centers; sim-level is plumbing-independent). - dmgtable.cpp [slice] line: rot flag, live twist, thetaIn/thetaAdj, chosen slice -- the weighted leaf roll made zone-only logs ambiguous. - [dmgresolve] now names the zone (BTMechZoneSegAndName). - mech4.cpp BT_FORCE_TWIST input pin + one-shot mode cycle (kept as doc of the dead path), mechmppr [mppr] mode probe. - scratchpad/night11/twistsign.sh: the 3-config bench. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
42b0691592 |
#119 leak-voice stutter SETTLED: authentic -- the wooHoo latch is authored OFF
Deterministic leak bench (BT_KILL_SUBSYS partial form, "Condenser4=0.5":
zone level without the crit -- a clean leak source) + the aud-tail receipts
decode the voice stutter end to end: the warning is a phrase-sequenced
voice patch (Warnings01 zones as notes), and every techstat leak-bit edge
restarts/stops the sequence mid-phrase (authored zero-release = hard cut).
At a drained tank the draw HUNTS the authored 0.0025/0.003 band -> edge
streams -> progressive clipping as more systems hunt.
The anti-spam wooHoo latch would bound exactly this -- but no armer exists
anywhere in the flat export, and the authored tuning is minDur=0 range=0
chance=0 (dumped live): DORMANT BY AUTHORING. The clipping is the 1995
experience; the port's one real bug here was the 2x hunt cadence, already
fixed by the 28 Hz filter (#119,
|
||
|
|
0089b3284f |
Eject firsthand prep: G = coolant Flush binding + BT_FLUSH_HOLD scalpel
The Flush ACTION existed in the input engine but no key was bound in the shipped CONTROLS.MAP -- desktop players could never hold the flush. G (free) now maps to it. BT_FLUSH_HOLD=<frames> extends the BT_FLUSH_TEST scalpel past its 60-frame edge test for tank-dry benches. Dry-run finding (fl.log): a full flush drains the tank to 0/14 in ~30 s, but does NOT arm eject by itself -- InjectCoolant moves tank coolant INTO the loops (the bank RISES), and idle heat draw is too slow to burn the loops down to the 5% arm threshold in minutes. Firsthand arming needs sustained fire after tank-dry (or coolant-loop leak damage / generator loss). Matches the binary's intent: eject arms when LEAKED nearly dry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8165cee9da |
#118: the eject option comes alive -- per-frame arm chain closed (FUN_004a9b5c+0x10)
The evaluator's "unexported caller" found by E8-scan: the Mech MASTER PERFORMANCE (FUN_004a9b5c, in the 004a977x..004ab188 export hole) calls EvaluateEjectPermission as its FIRST act every frame. Without that call the port's ejectPermitted stayed at ctor-0 forever -- the panic-armed mode never raised, so the PANIC lamp never lit and testers "never saw the option for eject come alive". Restored at the top of Mech::PerformAndWatch (masters only, like the binary). Mislabel swept: mech+0x414 is ejectPermitted, NOT "missionReviewMode" -- the mapper's @004d196c edge-watch arms PANIC mode 0x200000 from it (the real review mode is the GLOBAL DAT_004fd550; btl4pb now reads that global, the GetMissionReviewMode stub is retired, mapper member renamed previousEjectPermitted). Verified live: kill 4 generators -> "[eject] panic-arm mode ON" -> the miniconsole Panic button lights dark->bright (pixel captures; the pad panel shades it from the same PadRIO lamp state the pod's physical button uses) -> BT_EJECT_AT press -> PUNCH-OUT -> respawn heals -> mode disarms -> healthy presses REFUSED. BT_EJECT_LOG=1 logs the arm edges. Note: no shipped gauge rides mode 0x200000 -- the 1995 eject indication is the button lamp; the weapon-eng MFD "UNJAM/EJECT" is the ammo-jam indicator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
440cee1e4f |
#87 dig closure: host response is linear (raw decomp re-read); the pixel curve was div-card firmware
Re-read the material damage watcher RAW (@004573e4 ctor / @00457784 change-push): 16 floats snapshotted, damaged = pristine x 0.1 (literal at the MakeMechRenderables call site, watching zone+0x158), linear lerp, 13 floats written back, then FUN_0048d4d4 -- which is "flush_material": a dpl command-stream marshal (opcode 100) to the i860 division card. The material->ramp->texel response curve was CARD firmware and is not in BTL4OPT.EXE; logged as an open question (ask: firmware/DIV docs/period screenshots). Field reconciliation, measured live: one 25-pt hit (ERLG class) snaps a 77-pt arm 0 -> 0.3247 in a frame -- "a single missile leaves visible armor damage" holds under the linear curve for heavy hits; 2-pt ERS creep (the leg-audit weapon) is the invisible case. Bench additions: BT_WALK_ZONES=<name-list> walker filter (concentrated per-region audits, scratchpad/night10/legwalk.sh) + BindArmourDamage ORPHAN inventory under BT_ARMOR_LOG (proved every drawn op is zone-claimed on the full tree). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
00dfbf8f9f |
#124 RESOLVED against the authored skeleton: the pick's zone is the
struck drawn segment's dzone, ALWAYS -- the cylinder embellishment removed
The open question ('what zone did 1995 designate for hull hits') was
already answered in the KB's #73 dig [T0+T1]: no software pick writer
exists -- the pod's pick was a dpl scene intersection, struck triangle
-> DCS segment -> its SKL-authored dzone; the cylinder lottery was only
ever the UNAIMED path (missiles/splash/rams). The live segpick dump
completes it with the authored data: the madcat's pickable skeleton is
18 segments -- hip, shoulders, guns, thighs, knees, ankles+toes, and
ONE hull piece (jointshakey -> dtorso). No ltorso/rtorso/utorso/rear/
door/searchlight segments exist. Aimed fire at any torso panel
authentically designates CENTER TORSO; the side/rear/upper panels strip
via the unaimed path.
Changes:
* the shared-carrier -> cylinder override (yesterday's embellishment)
is REMOVED: triangle hit -> that segment's dzone, period;
* SegAimPoint: the walker's hull anchor moves from the joint ORIGIN
(crotch gap -- rays sailed between the legs to the terrain sentinel)
to the carrier segment's cull center (the chest);
* [picktri]/[pickwin] diagnostics grew tri/seg/zone fields -- three
consecutive 'mystery' regressions in this stretch were the STALE-EXE
trap (CWD-relative cmake --build from content/ resolved to a
nonexistent dir and the old binary kept running) -- the harness doc's
warning, hit again.
Final zone-walk matrix (spinning target, real MP, full cycles):
limbs 55/70 in-zone (legs/feet 6/6; arm strays = hull/occlusion);
hull-family aims -> dtorso 56/69; victim consumption matches. The
night-10 field table now reads as this model's fingerprint: 'only LCT
gets hits' from aimed energy IS the pod -- one hull piece, dtorso.
What was actually broken and is now fixed: the sphere mis-picks
(rgun-from-a-dtorso-aim class), the unaimed path's 180-degree frame,
and the vertical scatter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1a3c268278 |
#124: the aimed pick now intersects the DRAWN GEOMETRY -- sphere
approximation retired; shared-hull hits route through the cylinder The zone-walk matrix caught the pick red-handed: with the servo verifiably holding the reticle on the dtorso segment, the per-segment BOUNDING-SPHERE pick returned rgun/ruleg -- the gun pods and legs thread the ray before the torso from most angles (its own comments admitted a foot could be unhittable behind its own knee). The 1995 pick was a dpl scene intersection against the drawn geometry. Restored that semantic: * BTGetPickMesh caches each segment d3d_OBJECT's triangles CPU-side once (its own BGF VB/IB, managed-pool locks); MechSegmentPick now runs sphere PRE-FILTER -> Moller-Trumbore nearest-hit across the threaded segments' posed meshes; the old smallest-sphere selection survives only as the no-triangle fallback. * Segments claimed as CARRIER by 2+ zones (the shared hull: madcat seg 4 carries dtorso+ltorso+rtorso+utorso+rears) cannot resolve one zone from geometry -- those hits return zone -1 WITH the accurate triangle point, and the victim's bit-verified (frame-fixed) cylinder assigns the panel by band/wedge. Unique carriers (legs, feet, arms, gun pods) keep the direct zone. * ZoneAimPoint: the walker aims at a zone's VISUAL center (largest pick object's cull-center) instead of the segment origin -- joint origins made feet/lower legs strike the part above. * Walker upgrades from live operation: 3-column truth (aim/pick/land), engage gate, damped servo with polarity watchdog, approach port. Zone-walk verdict (full cycles, spinning target, real MP): limbs 5-6/6 direct in-zone; every hull panel routes CYL; the victim's landed zones now include ltorso/rtorso/reardtorso/rearutorso/rearrtorso -- the panels night-10 reported unhittable. Residuals tracked on #124: rtorso/utorso aim-anchor placement, doors/searchlight small-zone sample, twisted-torso twist-sign verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
16e75d6e1a |
#124 bench: the ZONE-WALK MATRIX -- systematic per-panel targeting, operator-watchable
The precision rig the operator specified: two nodes, target visible, every zone aimed at deliberately through the REAL reticle/pick path. * BT_SPIN_SELF=<deg/s> (target node): rotates the node's own viewpoint mech in place (the BT_SPAWN_AT write pattern per frame) so every aspect passes the shooter's boresight; the operator watches this node's paper doll take the hits. * BT_ZONE_WALK=<secs/zone> (shooter node): walks the target's damage zones in order, resolves each zone's carrier segment's live world position (BTResolveSegmentWorld + the new BTMechZoneSegAndName bridge), SERVOS the torso twist + aim elevation until the centered reticle ray (BTGetAimRay) points at the segment, fires 3 laser pulses, advances. [walk] ZONE/FIRE/HOLD on A pairs with [dmghit] zone/level lines on B. * scratchpad/night10/zonewalk.sh: launch both + relay, map sed'd to grass/day, NO kill timer -- the session stays up for observation. Hard-won servo constraints (documented in test-harness.md so they are never relearned): the ENGAGE GATE (ray live + range<150 + bearing<1.1; outside it RELAX the twist -- servoing at the twist limit while the goto marches is a limit-clamp fight that visibly shakes the mech, and a 0.55 gate deadlocks against the goto's ~0.55 resting bearing); YAW POLARITY -1 (the twist cell's angular sense is opposite atan2(x,-z) world yaw -- operator-observed live, the SECOND witness for the #124 SelectSlice twist-sign flip) with a divergence watchdog that self-flips; damped correction (gain .40, cap .025/frame -- the aim ray lags the twist write a frame); and a one-shot approach PORT to 100u off the target instead of a cross-map march. First live run: walker cycles zones, settles, fires; the target's luleg climbed 0.458->0.523 under its own aimed pulses -- aimed shot, correct panel, damage consumed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b0b7adc4ea |
#124 CORRECTION: the frame adapter is a Z-REFLECTION, not a pi rotation --
proven by the mech's own pods
The first fix (
|
||
|
|
e996be249d |
EJECT/PANIC wired: Mech::EjectPilot (id 0x19 @0049f854) + the crippled-mech gate
The button died twice before reaching game logic: no handler (0x19 was
unregistered) and no sender (the pod's panic was a control bit, not a
mappable). Both halves reconstructed from raw disasm -- the handler AND
its permission evaluator sat in export gaps.
@0049f854 EjectPilot: press-only; gated on ejectPermitted (@0x414) and
!IsDisabled; console eject notice (relay wire = tracked tail; the
suppressConsole@0x258 latch that prevents the death double-notify IS
wired); graphicAlarm -> 10, which kills via the >=9 predicate; then a
self TakeDamage {inflicting=SELF, zone -1, Explosive, amount =
ScenarioRole::killBonus}. Role layout byte-settled via the role reader
@00429bec dest offsets + the ctor record copy: +0x1c IS killBonus, +0x20
is the 4.10-only SpecialCaseDeathPenalty, +0x28 returnFromDeath (all
prior citations reconciled). KillBonus authors NOWHERE in shipped
content -> the charge is 0 and the ALARM does the killing: our bench
outcome is the pod outcome.
@0049fa1c EvaluateEjectPermission: eject only from a CRIPPLED mech --
bank coolant fraction < 0.05 | zero live generators | live weapons
below mech+0x448 (no exported writer: zero, clause inert) | leg-gimped
novice. A healthy mech REFUSES the button; no free resets.
Input: binding-engine "Eject" action -- Backspace / pad LeftThumb
(default profile + shipped CONTROLS.MAP). Bridges per the databinding
rule: weapon/generator/bank/player reads land in their complete TUs;
MechSubsystem gains the both-cells destroyed accessors (gotcha #22).
Bench scalpels: BT_EJECT_AT=<frame> (path-identical synthetic press),
BT_KILL_SUBSYS now takes a comma list.
Verified single-node: healthy press REFUSED (x2), four generators
killed, next press PUNCH-OUT -> death, wreck smoke, respawn; the
respawned mech refuses again (permission re-evaluates after Reset).
Death rides the normal damage/death chain, so MP replication is the
proven path. Tails tracked: console relay notice, RIO 0x38 panic
control, alarm-10 eject audio/canopy, SpecialCaseDeathPenalty consumer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
152249cb76 |
#95: the SCOREBOARD banked one missile per salvo -- credit the DELIVERED amount
Two consumers of one impact, only one was ever verified: the victim gets
TakeDamage{amount=per-missile, burstCount=cluster roll} and applies it
burstCount times (armor was always right); the score post on the next line
sent the bare per-missile amount. An LRM10 salvo dealing 7-35 armor banked
3.5 points -- Rajel's "~3 points to score", to the digit.
Ground truth: the binary's score is the victim handler's tally (amount once
per applied burst @0x4a04da + crit bonuses) reported to the INFLICTING
player (the id-0x16 tail, deferred #45). The shooter-side stand-in now
posts amount x burstCount -- the identical figure handed to the victim, at
the identical one-post-per-TakeDamage granularity. Same pass:
* splash never credited score at all -- the binary tallies every
TakeDamage; now posted per splash victim (amount x falloff bursts);
* the bridge credited the LOCAL player for ANY registered hit -- AI-master
fire on the player, a dying mech's death-blast splash; now refused
unless the shooter IS the local vehicle (MP unaffected: only local fire
carries live damage on a node);
* direct-fire unchanged -- beams author burstCount=1 (emitter.cpp:355).
Verified per the harness doctrine: single-node field composition (madcat,
real fire, real enemy) 22/22 impact credits paired at damage x burst, zero
bare 3.33 posts; two-node replicant-victim run BOTH directions 31/31
paired across three missile authorings (3.33/2.0/5.0 per-missile) + 25-pt
ballistics, leftovers all burst-1 beam amounts. test-harness.md gains the
map=grass note (MP.EGG authors cavern/night; GOTO mechs shoot rock).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9b1353bfbf |
ONE Explosion per salvo -- the port detonated every visual round, amplifying explosion sound + smoke + audio demand by the missile count
The user challenged the 7% clock story ("i think its not just a 7% discrepancy.
did you base your analysis in the decomp") -- correctly. The stop-path trace
it forced found the real amplifier, and it is byte-grounded:
THE ARCADE (byte-verified in the #95 work): FireWeapon @004bcc60 spawns ONE
Missile entity per trigger -- no loop, missileCount never read at fire. One
Missile -> one detonation (@004be078) -> ONE Explosion entity: one 4-voice
sound + one authored smoke package per salvo. The cluster's N-fold nature is
delivered as DAMAGE (burstCount), not as N explosions.
THE PORT: the N-round visual ripple (a deliberate [T3] port visual; its rounds
are damage-0 tracers) called BTSpawnRoundDetonation at EVERY round's contact --
N full Explosion entities per salvo, N x BigExpInt x 4 voices, N x the
explosion PFX, N=2..20. Measured in a 100s solo missile bench: 559 explosion
sound setups; BigExpInt 208. On observer nodes the stale-point mirror bug
(fixed in
|
||
|
|
6dbeb3d266 |
wreck smoke dies WITH the wreck; the audio/effect clock is calibrated to the byte-proven 28
USER REPORT: "when you destroy a mech, it continues to smoke for a very long
time after the mech disappears... up to 30secs to a minute."
MEASURED (new [pfx] timed census with per-def particle attribution): the
death plume (psfx 1 DDTHSMK) was re-armed on a 10s cadence as a WORLD effect
(ownerTag=0) -- so neither the burial transition nor the respawn cleanup
(StopAllEntityEffects) could touch it. The last-armed window kept emitting
over the empty spot for up to 10s after the hulk vanished, plus 6+-2s authored
particle lives, and back-to-back windows stretched the visible tail to the
reported range. The re-arm itself is a marked PORT ADDITION [T3]; the binary
fired DDTHSMK once -- its 10s window + particle lives ended right around the
~17s hulk sink. The authored design: the smoke dies WITH the burial.
FIX: the plume is spawned ATTACHED (BTStartPfxAttached, tagged to the dying
entity -- it also rides the sinking hulk now), and the burial transition calls
BTStopEntityPfx alongside the respawn cleanup that already did. Verified over
a 175s run containing FOUR wreck events (the enemy kill + three player bay-
fire deaths): every plume's emission ends at its wreck's burial or respawn,
particles fade within ~8-11s, no orphan emitters remain.
THE COUPLING (user relay of Oracle: "prolonged smoke and explosion INCLUDING
SOUND... same lever?") -- CONFIRMED as a family: the binary's audio layer
times in FRAME COUNTS converted by the engine frame-rate global:
AudioTime::Seconds_To_Frames == fmul [0x52140c]=28.0; fadd 0.5; round
(@0x42c611, @0x42dd86 -- the 0x42xxxx cluster of the ~90 global consumers)
Our engine's DefaultRendererRate said 30: every audio duration, sequence
delay and compression window ran ~7% fast. Calibrated to 28 [T1]. NOT yet
explained by this: a truly SUSTAINED/looping explosion sound (7% is not
"prolonged") -- the effect stop-path trace stays open on #114/#51.
Tooling: BTPfxParticle carries defIndex; [pfx] census (BT_PFX_LOG, 2s) prints
emitters + particles PER EFFECT SLOT with a timestamp; wrecksmoke.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
da70bd58b2 |
#84: the replicant salvo now HOMES AT THE LIVE TARGET -- the stale-point double detonation is gone
Oracle (674): "missile appear to register hit explosions twice, once where
target was and again where the target is". Rajel (693): "once where the mech
was when I fired and on the expected impact".
MECHANISM, measured on two nodes (BT_PROJ_LOG "DET at" now logs EVERY contact
detonation, including damage-0 rounds, which were invisible before): the
replicant salvo mirror pushed rounds with NO entity handle at the FROZEN
fire-time aim point, and the pool's contact test is proximity to p.targetPos --
so mirror rounds "contacted" the empty air where the target USED to be and
detonated there, on every peer, every salvo (45 salvos -> 234 frozen-point
air bursts in a 200s bench). The true impact appears at the live position
(the shooter's own homing rounds on his node; the victim's damage reaction on
peers) -- two sites per salvo, exactly as reported.
THE BINARY'S MODEL (CLASSMAP, Missile flight-entity cluster): missiles are
ENTITIES with authoritative/GHOST Performance variants (Projectile
PTR_LAB_005129e8 / 005129f4) and their own WriteUpdateRecord (@4bef4c, slot 7)
-- peers ran ghost missiles updated from the wire and saw the TRUE trajectory.
One explosion, correct place, every node. The frozen-point mirror is the port
infidelity (the entity Missile itself stays blocked by the documented 2007
Entity-base mismatch; mislanch.cpp:301).
FIX: the MissileLauncher update record now carries the locked target's EntityID
(salvoTargetID; EntityID::Null = point fire). The mirror resolves it on the
receiving node via HostManager::GetEntityPointer (engine T0, an index-socket
Find -- NULL-safe on any ID), refuses non-mechs via BTIsRegisteredMech, and
pushes the mirror rounds WITH the handle -- they re-lead on the live local
replicant exactly as the master's own rounds do, and detonate at the true
position. Guards: record-length gate (a short record from an older build has
no ID field -- its tail would be garbage and must not resolve) + Null check +
registered-mech check.
VERIFIED (two nodes, 200s, missile autofire both ways):
* every record arrives len=52/52 with sane aims ([mlrec] diag)
* 22/46 salvos resolve (tgtID=3:22 -> the victim's local master) and their
mirror rounds home: the frozen-air DET population fell 234 -> 102, the
live-homing population rose to 155/207 per node
* the unresolved remainder is BENIGN and correct: EntityID::Null point fire
plus hostID=-1 LOCAL entities -- i.e. the victim's WRECK, which autofire
keeps shooting after the kill; a wreck does not move, so the frozen aim IS
its true position
Downstream expectations: Ronin's smoke-screen (#114) should drop by ~the mirror
half, and the audio census (#32) loses the duplicate Static3DPatchSource
explosion sounds on peers -- both to be read from the next field logs.
Also in this commit: [projectile] DET diagnostic (every contact detonation with
position/damage/handle/aim), [mlrec] record-arrival diagnostic (len/recID/
subsys/counter/aim, capped 200), scratchpad/night9/mp_double.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3b7c19c232 |
#95: REVERT the salvo-damage hack; deliver the cluster the way the binary does
Supersedes the mislanch change in |
||
|
|
efc3e9ff1a |
#95/#84: missile salvos deliver their FULL authored damage, and stop double-exploding
THE SALVO DAMAGE (#95). Players measured an LRM 15 landing "3ish points". The logs agreed: [projectile] IMPACT damage=3.33333 (Oracle, LRM15) and 3.5 (Rajel, LRM10). Those are right PER MISSILE -- 50/15 and 35/10 -- and the bench shows why the salvo still under-delivers: each launcher pushes N rounds of which exactly ONE carries damage. Two individually-correct changes composed into an N-fold shortfall: * The ctor does what the binary's MissileLauncher ctor does (@0x3ac/@0x3d4): damageData.burstCount = missileCount; damageData.damageAmount /= missileCount; The record holds the PER-MISSILE amount plus the count; the arcade reconstitutes amount x burstCount when it applies the hit. * Task #62 then correctly stopped the port applying the hit once per visual round (that was ~missileCount-x too lethal) by damaging only the lead round -- but handed it the already-divided amount. Our DamageZone::TakeDamage is `damageLevel += amount * scale` and drops burstCount, so the salvo delivered amount/missileCount. Since the port collapses the cluster to one damaging round, multiply the count back in there. Bench: an SRM6 salvo now lands amt=35 (the authored total) taking a zone 0 -> 0.556, where it previously landed 5.83. THE DOUBLE EXPLOSION (#84). Oracle: "missile appear to register hit explosions twice, once where target was and again where the target is." There are two spawn sites: BTSpawnRoundDetonation at the round's own impact point, and the message manager's bundled explosion at the CONSOLIDATED point a frame later. The duplicate was known and thought harmless -- "among a rippled volley it is invisible" -- which held only while a salvo landed N detonations. A projectile now marks its weapon (MarkRoundDetonated) and the consolidation skips queueing a second blast for it; direct-fire weapons never mark, so lasers/AC keep the bundled explosion they rely on. Bench: 4 missile impacts -> 4 SKIPPED, while 11 direct-fire hits still queue normally. SWEPT CONTACT (#84 tail). Contact was a 10-unit sphere sampled only at the END of each step. With the authored thruster live (#84) field rounds arrive at v=955 -- a ~16 unit step at 60fps, larger than the radius -- so samples can straddle the target. (Pre-#84 rounds flew ~100-300 = a 1.7-5 unit step and could never skip it: the velocity fix exposed this, it did not cause it.) Now tests the whole segment travelled and bursts at the point of NEAREST APPROACH, which also stops the detonation being flung past the target at speed. RETRACTION: I posted tunnelling as the leading explanation for the lost salvo. The bench disproves it -- zero fizzles, and the "missing" rounds are the dmg=0 visual rounds of the cluster, which never registered damage by design. The sweep is kept as speed-independent robustness, not as the #95 fix. Bench: scratchpad/night8/salvo.sh (BT_PROJ_LOG + BT_FIRE_LOG). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
137c151951 |
#87: mech armour PANELS now darken with damage -- the .DZM material system was loaded and unused
Players: "the actual enemy mech in external view is not showing darkened armor panels". We swapped destroyed LIMB meshes but never darkened a panel. The 1995 game darkens armour through the MATERIALS. MakeMechRenderables (FUN_004cef28) built, per (damage zone, material), a watcher FUN_004573e4(material, &zone->damageLevel, 0.1f) that snapshotted the materials |
||
|
|
5410371b0c |
#86: destroyed weapons can no longer fire -- the fire gates read a
never-written cell (the split-cell gotcha) Both weapon fire gates (ProjectileWeapon gate 1 @4bbd36, Emitter hard-failure @4baab9) test the binary's subsystem+0x40 for Destroyed(1). In the 1995 layout that offset sits INSIDE the embedded status alarm (statusAlarm@0x2C + level@+0x14 = 0x40) -- ONE cell, written by ForceCriticalFailure when a zone's crit cascade kills the subsystem. The port models the same address as TWO members: the AlarmIndicator AND a plain int simulationState@0x40. Every destruction path writes the ALARM (so the MFD draws its X correctly) while the gates read the plain int, which nothing ever writes -- so a weapon on a blown-off arm showed destroyed on every panel and kept firing and scoring, locally and on peers (night-7: all three testers, screenshots of a missile leaving a destroyed pod). The tell had been sitting in our own logs for weeks: [ammo] SRM6_1 -> NoAmmo (gate1): destroyed=0 ... on a mech whose launcher was X'd out. Fix: both gates now read statusAlarm.GetLevel()==1 as well as the int. Also added: the crit-cascade log names the destroyed subsystem, a BT_SELF_DAMAGE_ZONE=dz_* named-zone bench mode, and BT_KILL_SUBSYS=<name> (force ForceCriticalFailure on one named subsystem -- the exact call the zone cascade makes, so a bench can ask 'the panel says dead, does it still shoot?' without hunting for the zone that carries a given weapon). Verified A/B in ONE run: before the kill both SRM6 launchers fired 2 salvos each; after, the destroyed launcher fired ZERO (gate log destroyed=1) while its twin kept firing normally. KB: new gotcha #22 (the SPLIT CELL -- one binary offset, two port members, only one written; sibling of #1) + combat-damage entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
4704ab3d41 |
#84: missiles ACCELERATE like the binary -- the pool flew at constant
rack-eject speed; the authored MissileThruster was never applied The binary Missile hosts a MissileThruster subsystem: authored acceleration for BurnTime seconds after the MuzzleVelocity eject. Authored values decoded from BTL4.RES (type-15 missile model records, reached from the AmmoBin's ammoModelFile via the type-1 MODELLIST indirection): SRM 2.5s @ 600 u/s^2, LRM 10s @ 300 (climb 50), Streak 3s @ 300 (turn 360deg/s), NARC 10s @ 300; splash 30 all (port constant was already right); authored drag ~0.001 = negligible. The port pool flew every round at CONSTANT |MuzzleVelocity| -- SRMs 100 u/s, LRMs 30 u/s (10x slower than authored) -- the entirety of the night-7 "missiles are very slow" report, and the driver of the "explosion before the missiles arrive" perception (the salvo-lead detonates while the slow spread rounds straggle in). Fix: BTMissileThrustOf (mech4.cpp) lazily parses + caches the RES thruster table (ModelList ids aliased to their type-15 member's burn/accel); both launch paths (master FireWeapon + the replicant salvo mirror -- peers see the same speed) resolve through the launcher's bin and pass burn/accel into the pool; the advance integrates speed += accel*dt while burn remains, heading preserved. Ballistic rounds (autocannon/gauss) pass 0/0 -- unchanged. Impact log now prints v=/burnLeft= evidence. Verified live (solo, BT_SPAWN_ENEMY + BT_AF_MISSILE): thrust table 8 entries cached; Black Hawk Streak resolve burn=3 accel=300; impacts at v=277 u/s (was a constant 100). Fun authenticity note: the Black Hawk's "SRM6" fires strk (STREAK) ammo per its authored bin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
ac7c46b87a |
#93: fix the end-of-round PerformAndWatch crash -- the STATIC projectile
pool held dangling entity pointers across round teardown Root cause (disasm-pinned on the shipped 4.11.659 exe): RajelAran's crash (call to 0x65676769 = ASCII 'igge', EBP-walk return btl4+0x2b91a) is the tgt->Dispatch vtable call in BTUpdateProjectiles' NON-MECH impact branch (mech4.cpp:1598; the site matches the disasm literally -- the 0x12/0x64 TakeDamageMessage ctor, the EntityID::Null ternary, the getenv gate after). The trap: the mech branch is guarded by BTIsRegisteredMech(tgt) -- but at round teardown a destroyed mech is DEREGISTERED, so a round still in flight (the pool is a static array that outlives the round; missiles are slow, #84) holding it as p.target now FAILS the mech check and falls into the !BTIsRegisteredMech branch, which treats the freed mech as a cultural icon and dispatches into freed memory. The liveness check itself routed the dangling pointer into the unguarded branch. Freed heap reused by a string -> vtable slot +0x10 read 'igge' -> call 0x65676769. (The EBP walker explains the stack shape: the faulting call pushed its return address on ESP, but the walk reads [EBP+4] = PerformAndWatch's frame.) Fix -- scrub at the source of truth: - BTProjectilesDropEntity(e): every pool entry drops a dying entity from p.target/p.shooter; called from ~Mech and ~CulturalIcon (the only free paths for targetable entities). Flight + impact code is already null-guarded on both fields. - BTProjectilesClearAll(): kills all rounds at RunMissions exit (cross- mission hygiene for the static pool). Verified: 6 short-mission cycles (45s missions with a spawned dummy + autofire, mission expiring mid-combat) -- 0 exceptions, 6/6 clean 'RunMissions returned'. The original crash was a heap-reuse race with no deterministic repro; the scrub eliminates the dangling-pointer class by construction. (Bench note: blind autofire never launches missiles -- the launcher needs a target lock -- so the exact in-flight race was not re-created; the non-regression + the pinned mechanism carry the verdict.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J |
||
|
|
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
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |