Operator called binary-faithful; the trace agrees. The hook (bfc072b/
fa3d634) fired ALONGSIDE the key's authored per-page function, so on a
weapon eng page an armed pilot pressing UNJAM/EJECT to clear a jam would
self-destruct -- a destructive hijack of a real control. Removed; the
authored press model stands alone: pilot KEYPAD bank (desktop numpad) or
the PANIC key while armed. The flashing engEject cell is the invite lamp.
Regressions: armed PANIC press -> PUNCH-OUT (1); armed soft-key 0x0B press
-> no eject (0); streamed ammo-eject route untouched (the hook was purely
additive).
Remaining marked deviations, both flagged in code and handoff: the coolant
arm hysteresis (0.05/0.06) and the Panic-button->keypad desktop wire (the
pod's physical matrix emulated).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The absolute-pointer scan missed the E8-relative call; the later byte-scan
found it (FUN_004a9b5c+0x10 -- the master performance evaluates eject
permission per frame in the shipped binary). Under deadline pressure the
disproven 'unfinished/dead code' claim leaked back into three comments and
the handoff; swept per the correction mandate.
Settled press model, now stated correctly everywhere: pilot eject = the
pilot KEYPAD bank while armed (+ the PANIC key reporting through that
matrix). The MFD soft keys NEVER pilot-eject in the binary -- every page
routes them to authored functions; the flashing engEject cell is the
INVITE LAMP. The port's page-gated eng-key eject is a MARKED CONVENIENCE
deviation (operator-requested), and the coolant hysteresis is a MARKED
smoothing deviation for a degenerate boundary oscillation -- not
completions of unfinished code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-respawn under sustained fire the bank fraction hovers at the 0.05 arm
threshold and the plain compare flipped ejectPermitted EVERY FRAME --
hundreds of mode ON/off pairs churning the mask, the lamp, and the arm cue.
The evaluator was dead code in shipped 4.10, so the boundary flap is a
first-ever-exercised condition; completed with the engine's own idiom
(HeatSink coolantActive dual thresholds): ARM below 0.05 (the binary
constant), release above 0.06, no new members. Stress bench: 1 arm
transition in 3 minutes (was: hundreds).
NOTE: the operator's ticking SOUND persists past this fix -- it is the
separate, pre-existing night-10 "audio stutter on coolant leak" family;
hunt continues at the audio-cue chokepoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The proper RE the corruption arc demanded: counting GRAPH2D.h's declared
virtual order, GraphicsView vtbl+0x18 = SetColor and +0x24 = MoveToAbsolute
land exactly -- and +0x58 is NOT DrawBitMap (+0x54) but the NEXT virtual,
DrawBitMapOpaque(background, rotation, bmp, sx1, sy1, sx2, sy2). The wipe
is a two-span OPAQUE draw with INVERTED fg/bg pairs -- the literal "inverse
wipe" -- and FUN_004c5fb8 transcribes byte-exactly with the real signature
(the ++level between spans is inclusive-bounds bookkeeping, no gap). The
base ctor fields were already right (colors +0x94/+0x98, bitmap Size.x/y at
+0x9C/+0xA0, decomp @004c5e84).
BallisticWeaponCluster::ejectWipe re-wired (bteejtm.pcc <- PercentOfEject).
Stress-verified: 4 gens dead + autofire jams + leak warning, weapon pages
pristine end to end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The operator's three captures decode as ONE mistake drawn two ways: the
cockpit compositor deliberately draws the side buttons as BIG rects tucked
UNDER the display surfaces (step 2), with only the protruding edge visible
-- the edge IS the port's lamp light (the pod's backlit keys sat beside the
CRT; there is no in-display face to light). My on-top "flash overlay"
(86a6bbb L4VB16, 5f79dce L4GLASSWIN) violated that design: drawn after
DrawDevSurface without resetting the bound texture state it rendered the
gauge atlas as green striped garbage at the button rects; with the state
reset it drew honest full faces -- solid flashing red covering that whole
portion of the MFD. Both overlays removed; the original step-2 rendering
(which already tick-animates lamp shades) is the whole story.
Pixel-verified through the full stress (4 gens dead, autofire jams, leak
warning): weapon pages pristine end to end, and the 0x27 edge lamp
square-waves 34.0 <-> 28.9 across consecutive captures -- the invite
flashes at its authentic scale.
(The eject-wipe gauge -- the OTHER corrupting draw, visible in the earliest
sighting -- stays re-stubbed pending a real RE of FUN_004c5fb8's draw op.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BitMapInverseWipeScalar Execute passed the colour constants as
DrawBitMap's first argument; the sibling wipe always passes 0 there (colour
selection is SetColor only). The nonzero arg smeared striped garbage across
the weapon MFD pages from the gauge's first draw (operator's capture.png).
Pixel-verified clean after the fix (weapon pages pristine, eject graphic
path unchanged).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first visibility fix painted L4GLASSWIN -- a window the operator's
single-window layout never shows. The main-window cockpit compositor is
BTDrawCockpitPanels (L4VB16): same under-the-surface masking, so the RIO
flash was again invisible. The 3b overlay now re-draws flashing lamps
(state & 3) above the surfaces there, tick-phased; [ckflash] (BT_LAMP_LOG)
logs each animated draw. Verified in-engine: addr 0x27 state 0x37
alternating shade 1 -> 3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The glass windows paint buttons UNDER the display surface (only a sliver of
edge protrudes), so the RIO flash commands were arriving ([lamp] 0xb <- 0x37
FLASHING in every trace) while the operator "never saw it flash" -- the
blink was a few masked pixels. Lamps carrying RIO flash bits (state & 3)
now re-draw their full face above the surface with the tick-phased shade:
the gotoEngineering / engEject invites read like the pod's backlit keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The page-gated eject query ran in the window-click thread (PadRIO
EmitButton) and dereferenced the TU-local application global -- NULL there
under the /FORCE duplicate-symbol layout -> c0000005 at mode-manager +0x50
the moment the operator pressed the eng eject key (field crash,
live.log [crash] record). Moved into LBE4ControlsManager::ProcessRIOEvent's
ButtonPressedEvent branch, where mode_mask is already resolved on the game
thread; PadRIO now only queues events. Same gate semantics (armed + that
bank's ENG-page mode bits); verified no-crash + no-eject on the quad page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decode that settles it: the eng-page side-key functions align key-for-
key with the engEject lamp map (selectGenA-D = 0xF-0xC, EJECT = 0x0B,
GEN-MODE = 0xA, COOLING = 0x9, BACK = 0x8; banks 2/3 at 0x27-/0x7-), and
the streamed weapon pages route the same key to EjectAmmo -- so 0x0B/0x23/
0x03 ARE the authored EJECT position. On generator pages the shipped
stream leaves the key with NO live route (msg 9, no handler anywhere):
eject was UNFINISHED in 4.10 (the evaluator was dead code -- the arcade
never armed panic at all). Intent-completion [T3, marked]: while PANIC is
armed AND that bank displays an ENGINEERING page (kBTEngModeMask bits --
the same gating the alarm lamps use), the flashing EJECT key fires the
pilot-keypad eject. Any other page: authored function untouched (the two
field regressions came from skipping the page gate).
Verified: armed + QUAD page + 0x0B press -> no eject (gate holds); panic /
numpad / Backspace unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last hop lands: a destroyed Generator now reaches its OUT display state
(stateAlarm 4 -- enum renamed from the placeholder "GeneratorRecovered";
[T3 inferred link, both endpoints byte-verified: the eject evaluator's
state==4 dead clause @0049fa1c and the authored alarm streams' conditions
4/5 -> gotoEngineering 0x80 + engEject 0x85]). Wired from the one crit
chokepoint (MechSubsystem::ForceCriticalFailure -> BTGeneratorMarkOut
bridge). Verified live: gen kill -> the quad-select columns FLASH
(RIO 0x37 fast-flash) -- the pod's guided "punch out now" trail.
Plus the authentic panel eject input: CONTROLS.MAP binds the desktop
NUMPAD to the pilot keypad (keypad pilot 0-9) -- while armed, any pilot-
keypad key fires the eject (the binary's only armed binding); un-armed
they feed program entry.
The full 1995 eject experience is now live: alarm -> flashing invite trail
-> lit Panic button -> press (Panic / numpad / Backspace) -> punch-out ->
death without honor -> respawn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The streamed .CTL dump (BT_CTRLMAP_LOG, 109 records on the madcat) settles
the eject-input question from CONTENT: no 0x200000 record exists -- the MFD
soft keys carry their authored per-page routes unconditionally (elem 0xB on
a weapon eng page = msg 0xB EjectAmmo, the round eject), so they must never
route to pilot-eject. The invite-cell routing (b9430cc) hijacked exactly
that key -- the operator's "bottom left button self destructed" -- and is
REMOVED.
The faithful model, now fully grounded: the flashing engEject soft-key LAMP
is the INVITE indicator; the eject PRESS is the guarded PANIC key (no
button-space consumer in the image -> it reported through the pilot keypad
matrix; the 0x3D->keypad wire stays) or any pilot-keypad key while armed.
Verified: armed + soft-key 0x0B press -> NO punch-out (authored function
preserved); panic path unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field-caught by the operator within minutes: the blanket MFD key-space
routing (0x00-0x2F) made the DISPLAY navigation key eject the mech
mid-page-switch while armed. Narrowed to exactly the guarded PANIC button
(0x3D) and the three eng-page EJECT INVITE cells (0x0B/0x23/0x03, the
addresses the engEject 0x85 alarm lamp flashes on). Navigation is safe
while armed; the flashing cell still punches out (re-verified through the
click seam: armed -> press 0x0B -> PUNCH-OUT).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The operator's pod flow ("the engineering panel's bottom-left button flashes
EJECT, you push it") is input #3 of 3. The eng-page eject cell resolves to
MFD side-column addresses 0x0B/0x23/0x03 (kBTEngBankTop - 4 per bank), and
the binary's ONLY armed eject binding is the KeyboardPilot group (0x200000
-> mech msg 0x19; exhaustive 0x200000 scan: no other consumer) -- so on the
pod those keys report through the pilot keypad matrix. Desktop mirror: MFD
key-space presses (0x00-0x2F) and Panic (0x3D) also land as pilot-keypad
keys. Outside panic mode the binding is mode-masked dead -- normal MFD
operation untouched; armed, the flashing cell (or any keypad key -- the pod
semantic) punches out.
Verified through the click seam: gens dead -> armed -> press 0x0B ->
PUNCH-OUT.
All three eject inputs now live: Panic button, Backspace/pad LeftThumb,
and the flashing MFD eject cell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BT_LAMP_LOG now dumps each gauge-alarm stream item as it reads
([galm-item] sub/cond/itemCond/lampCode). First dump against the shipped
content settles the operator's "shouldn't something flash?" question:
GeneratorA itemCond=4 -> lampCode 0x85 (engEject) + 0x80 (gotoEngineering)
GeneratorA itemCond=5 -> lampCode 0x85 + 0x80
The 1995 pod FLASHES "GOTO ENGINEERING" and the engineering page's EJECT
cell when a generator goes OUT (conditions 4/5) -- the authored eject
invite. The Panic button lamp itself is solid by authoring ([lamp] 0x3d <-
0x3c, no flash bits) -- both halves of the field observation are genuine.
Remaining: the BT_KILL_SUBSYS scalpel skips the authentic state-4
transition, so the flash did not fire in the bench; verify a genuinely
damaged/overheated generator reaches state 4/5 and lights the invite
(the #118 "Generator Out vs !=4 clause" tail).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The operator's field report ("eject slot flashing but no graphic in it") was
the tracked-NULL bring-up stub in BallisticWeaponCluster. Reconstructed the
missing gauge class from the binary:
- BitMapInverseWipeScalar @004c61c8 (vtable 0x518a14; base @004c5e84,
Execute @004c5fb8, BecameActive @004c5fa4): a bitmap COLUMN SWEEP whose
level tracks a live Scalar -- two spans with INVERTED colour pairs,
[0..level] fg colorA / bg colorB, remainder the inverse.
- Wired per the binary call site (part_014.c:2312): bteejtm.pcc at (0xF,0)
on the weapon's eng port, colours 0/0xFF, watching the weapon's
PercentOfEject (@0x3F8, the round-eject 0..1 progress) via a new
complete-type bridge BTWeaponPercentOfEjectPtr (databinding rule).
At idle (level 0) the EJECT graphic now renders in the slot; during a
round-eject cycle it sweeps with the countdown. BTEEJTM.PCC ships in
content/GAUGE (the loose gauge bitmap set). Smoke-tested: constructs clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two finds close the press path the operator field-tested:
1. The pod's guarded PANIC key reports through the PILOT KEYPAD bank -- the
binary's ONLY panic press binding is keyboardGroup[KeyboardPilot].Add(
mode 0x200000, mech, 0x19) (FUN_004d266c); there is NO buttonGroup[0x3d]
consumer (0x3d is the lamp address). The desktop Panic click now ALSO
lands as a pilot-keypad key (PadRIO::EmitButton -> EmitKeypad), so the
armed-mode binding fires; a healthy press stays a mode-masked no-op,
exactly the pod behaviour.
2. KeypressMessageID resolved to 0x17 via enum-chain arithmetic; the binary
passes LITERAL 0x19 (two mapper ids are unreconstructed). The armed
keypad send arrived at the Mech as msg 0x17 -- no handler, silently
swallowed. Pinned to 0x19 (Mech::EjectPilot's id on the OWNER receiver --
the collision is the design).
Verified headless through the REAL click seam (BT_BTNTEST=61): generators
killed -> panic-arm ON -> screen-click Panic -> [lbe4key] unit 0 mode
0x650421 -> PUNCH-OUT. Also: IsDisabled eject refusals now log; [padkey]/
[lbe4key] seam tracing under BT_PAD_LOG.
Still open (cosmetic): the flashing eject slot on the engineering MFD shows
no graphic -- the streamed page element's pixmap is unreconstructed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
The binary's effect watcher (FUN_0042aa2c) raises
ForceUpdate(DamageZoneUpdateModelFlag) when a zone's damageLevel CROSSES a
band-descriptor threshold (FUN_0042a5f4, master-gated on entity+0x28 & 0xc)
-- that send is how every other pod's replicant learns zone levels
mid-fight. The port's band hub kept only the graphic-state branch, so an
observer's copy sat at 0.0 until destruction: no enemy hull darkening, no
doll movement, ever (the night-10 "no armour discoloration" report).
Restored the level branch with the already-reconstructed DescriptorCrossed
(@0042a5f4) + the master gate on both branches (mesh swap still runs on
every instance). 2-node verified: A-side armour watcher pushed 114 level
changes; every replicant peak matched the master's finals to 4 decimals
(dtorso 0.9321 == 0.9321).
Render path cleared separately (gotcha #23 discharged): BT_ARMOR_FORCE 0/1
A/B captures prove the tint renders (hull -> charcoal at 1.0). Solo
perception is the authentic economy: 2-25 pt lasers vs 68-185 pt pools.
Bench: zonewalk.sh node A now carries BT_ARMOR_LOG + BT_SHOT_EVERY (the
standing zone-replication receipt) + digest_walk.py; armorpx.sh is the
force-pair pixel rig.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
proven by the mech's own pods
The first fix (c5dfb00) negated x AND z. The operator challenged it:
'they couldn't hit rear panels AT ALL -- a 180 rotation couldn't explain
that' -- and a rotation would also have relocated rear damage, not
erased it, while the bench premise (the dummy faces its attacker) was
assumed, never measured.
The zero-premise probe (BT_ASPECT_TEST): four self-impacts at known
WORLD cardinals + one at each weapon's PHYSICAL MUZZLE position -- the
mech's own asymmetric geometry as the left/right anchor. Measured:
* WorldToLocal is a clean R(yaw), no hidden terms (yaws -30/150/40 deg
all consistent);
* our frame: RIGHT = +X -- SAME as the 1995 ring (W0/W7 = Right*) --
and FORWARD = -Z, flipped vs the ring's Front*-in-+Z-arc;
* under the pi rotation the pods CROSSED (left muzzle -> rtorso);
under z-negation every anchor lands its own side.
So: negate local z only, and -- since a reflection reverses angular
direction -- the SelectSlice torso-twist term flips sign with it
(twist was 0 in all probes; twisted-torso verify tracked on #124).
Anchor acceptance (fresh exe, third yaw): LRM15_1 left pod -> ltorso,
LRM15_2 -> right wedges, left/right arm muzzles -> larm/rarm, nose ->
front cells, tail -> rear cells, flank cardinals -> rgun / left wedges.
The BT_ASPECT_TEST probe + BTWeaponMuzzleWorld bridge stay as bench
scalpels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Night-10's systematic targeting audit (rear panels unhittable from
direct fire, Owens left torso hitting the RIGHT weapon pod and vice
versa, missiles favoring the far side) measured down to one geometric
fact: the authored wedge ring puts the Front* cells in the +Z arc
(slice-name audit, bit-verified tables), i.e. the 1995 resolver frame
has mech-forward = +Z -- while OUR engine's entity frame carries
forward along -Z. Bench proof: a victim facing its attacker took
frontal beams at local z = -4..-10, theta ~270 deg, resolving the
REAR-named cells (zones 12/14/15) every time.
The two frames differ by a pi ROTATION about Y -- rotation, not
reflection, because the field flank observations CROSS (left->right)
rather than persist. ResolveHit now rotates the local impact into the
cylinder's frame (negate x and z; y untouched; handedness preserved so
the SelectSlice torso-twist add keeps its sign).
A/B (172 resolves, live missiles+beams on a facing victim): before
theta~4.7 -> rear zones only; after theta~1.42-1.59 -> utorso/dtorso/
ltorso/rtorso/arm -- the front-authored cells, with the per-burst
re-roll scatter intact.
NOT this bug: the "one band LOW" Y-axis half of the night-10 table --
that is #16 (pick-ray boresight parallax, code-confirmed, still open).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The clipping coolant-leak voice ("warning... war... warning coo...")
measured to a ReportLeak relaxation oscillation at a drained tank:
draw = zoneDamage x heatLoad hunts across the authored 0.0025/0.003
hysteresis band (constants byte-verified) as the dry sink's conduction
collapses and the tank trickle refills it. State machine, constants,
refill dynamics and the sequencer's chase-and-cut stop (T0 AUDSEQ.cpp)
are all the binary's own -- the ONE divergence was the 15-sample
heatLoad filter: per-Perform in the binary = a 0.536 s window at the
pod's 28 Hz, but only 0.25 s at the port's ~59 fps -- heatLoad twice as
twitchy, the flap at double the pod cadence (the myomer-kinetic dt-less
class, third instance).
UpdateHeatLoad now accrues real time and samples at 28 Hz on any
machine (catch-up capped at the 15-sample window). Per-instance clock
in a static map -- the heat-family layouts are factory-size-locked
(sizeof(Myomers) == 0x358 exact), no new members. Ctor/reset prime
calls sample unconditionally.
A/B (260 s leak-to-empty bench): trigger fires 12 -> 8, transitions
11 -> 7. The residual slow restart at a bone-dry tank is authentic 1995
behavior (same engine sequencer chase-cut); a minimum-retrigger
interval would be an opt-in deviation for the operator to decide.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* SpecialCaseDeathPenalty restored to the engine ScenarioRole (SCNROLE.h/
.cpp) -- the 4.10-only role field the 2007 WinTesla source dropped
(record slot [1] after KillBonus, fail-hard read; role+0x20; the
DefaultRendererRate pattern). Its consumer decoded from the @004c05c4
export gap (@004c07cd, missing from the #52 reconstruction): every
death with advancedDamageOn hands the ENGINE base ScoreMessageHandler
(@0042da20 == PLAYER.cpp:138, a DIRECT call bypassing the BT type
Verifies) a type-1 award of -penalty. Wired verbatim. Shipped content
authors the key nowhere -> cost 0 in the field. NUANCE [T4]: the 1995
engine adds at Player+0x1c8, which no BT scoreboard reads (+0x278) --
the pod's cost may never have displayed; our single-cell port shows it.
* The eject console notice: FUN_004c198c IS the
ConsolePlayerMechDeathWithoutHonorMessage ctor (T0 BTCNSL.CPP survives
and is compiled) -- "death without honor" is the punch-out record.
BTPlayerEjectBookkeeping now sends it to the console host through the
same ConsoleClientID path as the VTVDamaged notify, then latches
suppressConsole: the eject notice REPLACES the death notify. Verified
two-node: "notice -> console host 1" fires on the ejecting node; the
relay-side delivery of client-5 traffic is a pre-existing question
shared with the never-observed VTVDamaged sibling (operator-console
workstream).
* Eject alarm-10 audio: resolved NEGATIVE -- the five Eject*.wav ship in
AUDIO/ but BTL4.RES references them nowhere (orphaned like the SQUAT
clips); wiring them would be a stand-in. Operator-optional deviation.
* RIO 0x38 panic control: deferred with the plumbing documented (the
glass Flight-Controls window already renders the 0x38 bank; consumer =
read buttonGroup[ButtonPanic] alongside the binding-engine edge) --
pod-hardware-only, untestable on this bench.
Two-node verify (fresh-exe rerun after a stale-exe trap): REFUSED
healthy, PUNCH-OUT after generator kill, death + wreck swap + smoke +
un-wreck warp all replicated on the observer, respawned mech refuses
again, console notice fired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The open question under every heat bracket was what the 1995 sim does with
time. Answered at the byte level:
* FUN_0041c018 IS the arcade Simulation::PerformAndWatch: slice =
(till.ticks - last.ticks) / DAT_0052140c, stored, Perform(slice) --
VARIABLE-STEP in real SECONDS, line-for-line the WinTesla SIMULATE.cpp
body. Every dt-carrying reconstruction is unit-faithful; no tick/second
gap exists anywhere.
* The myomer heat term's ONE caller is the unexported MyomersSimulation
body @0x4b8b9a (E8-scan + raw disasm; export gap): once per Perform, raw
slice, gated measuredVoltage>0 only. The dt-less kinetic pulse therefore
fires at the FRAME cadence -- 28 Hz nominal, sagging under load
(emergent hardware, not a code constant).
* Mover velocity is u/s (dead-reckoner @0x421f7c: pos += vel x ticksD/28),
so the v operand is unit-faithful too.
The 0.5 "loaded-rate" witness bracket is RETIRED -- it modeled the sag as
a constant, wrong in exactly the reported scenario (open-field cruising is
a light scene; the pod held ~28 there). kSeekHeatCalib = 1.0.
Measured at the faithful rate (madcat cruise benches): recommended gear
equilibrates ~1450 -- inside the authored degradation band as a SOFT 20%
output governor (heatFactor = 1 - d^2/range^2), weapons untouched at
~110-150, ZERO jams, zero bay fires from movement alone; supercharge
~1770 (deep governor -- the manual's own expert-mode warning). That is
the authored system: degradeT=1000 is governor onset, failT=2000 the
cliff, and the coolant-loop controls are the player's lever.
New leads filed in open-questions: the climb term's gravity operand
samples 0 (bridge points at a dead cell); the engine Mover's own gravity
subtraction is per-frame dt-less (port impact unread); gait-noise phantom
acceleration feeds the accel term.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Byte-level audit of the entire damage chain vs the binary, all links exact:
zone stream reads (@0041df5c == engine DAMAGE.cpp), ctor normalization
(@0049ce50; constants 1.0/1e-4/0.5; shipped cells uniform 1/armorPts),
TakeDamage @0041e4e0, the BT override @0049c690 (vital/leg 1.0/0.5/0.0),
CriticalHit @0049ccc4 (0.5 fraction, 1.0 cap, authored-percentage cap),
cook-off chain (@004bc3fc per-salvo stamp / @004bd300 rounds x amount /
@004ac274 divide across the bin's plugged zones).
Measured kill route: 6 of 7 bench deaths were bay-fire detonations --
LRM15 bin 10 rounds x 50 = 500 into its ONE authored carrier zone
(content: madcat bins single-zone; 15 of 22 zones vital) -> one
application 0->1.0 -> instant death, armor bright everywhere else.
Field logs confirm dominance (Rajel 12 bay-fire events, ConnMan 10).
The mech that died conventionally ended 0.97/0.93/0.92/0.85 charred --
darkening accumulates fine when mechs live long enough.
Levers: the #96 heat economy (arms the bay fires) and purge training
(EJECT hold extinguishes -- wired, logged, manual-trained in the pod).
Also corrected a wrong KB address: FUN_0041e4e0 was labeled '.DZM res
compiler' in decomp-reference.md/rendering.md -- it is DamageZone::
TakeDamage (byte-matches DAMAGE.cpp:379); swept both files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
"do we need to audit every possible weapon independently?" -- one dump answers
all of them. [wspec]/[wspec-msl] (BT_SPEC_LOG) print every weapon's AUTHORED
damage/recycle/range/heat + missile count and post-divide per-missile at build.
THE KEY DISCOVERY: the catalog's FULL-NAME loadouts (madcat, thor, avatar,
sunder, vulture, loki, blkhawk, owens) author DIFFERENT weapons than the
4-char variants (mad1, thr1, ...) -- and the named ones are what players fly.
Every night-9 field number reconciles exactly:
madcat/thor LRM15 = 50 total -> 3.33/missile (field: 3.33, bursts 3..15)
avatar/sunder LRM10 = 35 -> 3.50 (field: 3.50, bursts 2..10)
vulture LRM20 = 65 -> 3.25 (field: 3.25, bursts 5..20)
loki SRM4 = 35 -> 8.75 (field: 8.75, bursts 1..4)
Missile delivery is FAITHFUL across the real fleet: per-missile = authored /
count (divide confirmed live post-ctor), delivered total = per-missile x the
binary's Random(n)+n/4 roll = 25..100% of listed, zone-scattered.
The "80+ point hits" recollection fits the data as an ALPHA: madcat full alpha
= 50 + 25 (AFC100) + 6 + 2 = 83. A single 4.10 salvo cannot exceed 65
(vulture LRM20). The "3-5 points" reports read our [projectile] IMPACT
per-missile diagnostic as a total.
Also: bac1 is not a valid vehicle (unhandled AV at btl4+0xac4cd on spawn --
noted for a guard someday); wspec-msl now prints AFTER the ctor divide.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audit of Cyd's hit-model artifact against THE BINARY (user mandate: not vs the
port). res29_scan.py parses the type-29 streams straight from BTL4.RES bytes:
18 streams / 8 distinct confirmed; the 7-wedge cells (BLH band 6, VUL band 4)
and the all-fixed BLH/OWN twist patterns are authored fact; every artifact
spot-check cell matched the raw bytes verbatim. Chassis->table resolved by
live-dump matching (MadCat and Avatar ride DIFFERENT same-zone-set tables).
Authored slice names fix the wedge orientation: FRONT = the +Z arc, W1|W2 seam
= dead ahead -- corrects the #92 comment's W6-frontal claim. Collision type-0
divert re-grounded at 0x4a0361/0x4a036d: rams never touch the cylinder.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
For verifying Cyd's hit-location artifact (the platform serves it to this
session as an unreadable public artifact, so the diff base is published here
instead). scratchpad/night9/hitmodel_groundtruth.txt: every chassis's 7
layers x per-layer slices with zone NAMES and percentages, from the live
BT_DMGTABLE_LOG dump joined against [zone-armor] names.
Structure fingerprints (rot = rotateWithTorso per layer 0..6):
ava/lok/mad/snd/thr/vul rot=0001111 (legs hull-fixed, torso bands twist)
bhk/own rot=0000000 (chicken-walkers: NOTHING twists)
slices: 8 everywhere EXCEPT bhk layer 6 = 7 and vul layer 4 = 7
Variants share tables: lok2==lok1, mad2==mad1 -- '8 distinct chassis' is exact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 da70bd5) DOUBLED the train again.
One infidelity, three field symptoms:
* Oracle: "prolonged smoke and explosion including sound" -- a staggered
train of 6-20 overlapping BigExpInt instances per salvo reads as one long
explosion+roar (Rajel correctly tied it to #84)
* Ronin: missile-impact smoke "so much it functioned as a smoke screen"
* #32: the saturating audio class was 1005 Static3DPatchSource requested=4
-- each surplus detonation reserved 4 more voices
FIX: BTProjectile carries salvoLead; both detonation sites (target contact +
world/terrain burst) gate on it. Master salvo: lead = round 0 (the damage
carrier). Mirror salvo: round 0, so observers see exactly one explosion per
salvo at the live target. AC/laser single rounds default lead=1 (one round =
one explosion, authentic). Non-lead rounds keep their small tracer end-puffs.
MEASURED A/B (same bench): 80 of 96 contacting rounds no longer spawn the
package; 16 leads ~= the salvo count. BigExpInt setups 208 -> 72 (~18
packages for ~16 salvos + deaths = one per salvo). LaserExplosion unchanged
in kind. Splash was already lead-only (splashBurst) -- damage totals move
NOWHERE in this change.
Also corrected the rack-tube comment that claimed "the binary's Missile
entities each launch from their own rack tube... their 12 detonations" -- there
are no 12 binary missiles; the ripple is ours.
The 28Hz audio-clock calibration from the previous commit stands, but is
demoted to what it is: a small systemic correction, not the explanation for
the field reports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The user's question broke my first story: "did lynx host tho?" He did (Oracle
joined LYNX's lobby that night) -- but more importantly the affected players'
logs read mode=steam, which gets the FULL menu, so the FeJoinOnly fix in
3109cfc patched a real gap (mode=join) that is NOT the path the affected trio
ran. Oracle COULD see and cycle the selector; it still didn't matter. Traced:
* The steam lobby serialized exactly three member fields: nm / vh / cl.
Experience never travelled.
* The steam HOST rebuilds mission.pilots[] from the lobby roster
(name/vehicle/color only), so every non-self page has EMPTY experience --
and BTFeMission_WriteEgg stamps the "veteran" fallback on empty.
* The steam JOINER writes NO egg at all (spec->eggPath[0]=0); his mission
arrives from the HOST's console feed -- i.e. the host's egg, where HIS
page says veteran no matter what his own console shows.
* The host's own page keeps his menu choice only as slot-0 residue of the
pre-roster self fill -- why BOTH hosts (Sauron AND Lynx) were clean while
all three joiners heated in "standard".
FIX: experience is now published as lobby member data ("xp") beside nm/vh/cl,
unpacked into BTLobbyMember, and stamped onto each member's egg page in the
host's roster loop. Both exported lobby entry points + PublishSelf gain the
parameter; the fe passes the local menu selection. An older client in the
lobby simply has no "xp" -- the veteran fallback applies to THAT member only,
exactly the pre-fix behavior, so mixed-build lobbies do not break.
Per-player mixing (the original design, per the user: the sysop set each
user's tier; mixed matches were legal) now works over steam end to end: each
member's OWN choice -> lobby -> the host-authored egg page -> the console feed
-> that node's master player.
VERIFICATION BOUNDARY: builds both configs; the chain is code-traced; the
lobby legs need a real Steam session -- field re-test is a JOINER selecting
Standard and logging [exp] experience=1 heatModelOn=0 (plus the [fe] line on
both sides). The 3109cfc join-trim selector stays: correct for mode=join.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Oracle isolated it: "standard mode still having heat and leaks only occurred in
steam not in solo. Sauron and Lynx did not have the issue but Conn Man, Rajel
and myself did." The night-9 logs close the case:
affected (Oracle/ConnMan/Rajel): [exp] experience=2 heatModelOn=1
clean (Sauron's std sessions): [exp] experience=1 heatModelOn=0
CAUSE: the glass front end's JOIN layout trimmed the menu to "the mech list +
the JOIN button; everything else is the operator's call" -- and the experience
selector's hidden default is 2 (veteran). So HOSTS got whatever they picked
(Sauron hosted the lobbies all night -> his standard landed), while every
JOINER launched as veteran regardless of anything they did -- which is why
Oracle's cycling-the-settings experiment changed nothing, and why solo (full
menu) never showed the problem. heat >= veteran, hence "standard but heating".
DESIGN CONFIRMED BY THE USER before fixing: experience is PER-PLAYER by the
original design -- the sysop set each user's tier and mixed-experience matches
were legal. Architecture already supports it end to end: each node's master
player reads its OWN egg's experience (btMission+0xE4 -> BTPlayer @004c0bc8),
so per-node choice IS per-player choice.
FIX: the join layout gains the GroupExperience selector (the selection->egg
write path is the pre-existing host path, field-proven). Also: the front end
now prints "[fe] pilot experience=<x> (join|host/solo)" at launch, so every
future field log answers this class of report without asking anyone.
VERIFICATION BOUNDARY, stated plainly: builds both configs; the write path is
shared with the host flow which the field already exercises; the visible
selector + the [fe] line need one join-mode launch to eyeball, and the field
re-test is Oracle running a standard steam match as a JOINER and seeing no
heat.
Tooling note recorded in test-harness.md: bash-heredoc python collapses one
backslash level even single-quoted -- a "\n" arrives as a real newline and
replaces silently no-op. Build backslashes from bytes([92]); verify replaces
by length delta, not by the script saying "fixed". (This burned four edit
rounds tonight and several earlier C2001 hunts.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Frame-rate testimony from the morning follow-up, now on record beside the bytes:
* Oracle, asked if the pod frame rate was rock solid: "I think I could do as
well with a flipbook"; heavy fights "would turn to a slide show".
* Lynx (original era): "RP had a target frame rate of 30FPS. It wasn't hard
coded or locked. BT never had a locked frame rate in 1st release
testing... coming from 3.0, where 20FPS was a miracle."
* The engine pacer's own design agrees: fixed budget + background fill + NO
overrun catch-up (APPMGR.cpp, T0) = "target 28 but allowed to slip", with
the 18.2065 BIOS fallback anticipating degraded timing.
Also settles epilectrik's open confirmation ("frame rate is advancing at least
some of the subsystems -- i have to fully confirm that"): ALL of them --
UpdateManager::Execute runs every interesting master every frame,
Entity::PerformAndWatch runs every executable subsystem per entity tick, no
throttle at any layer. The crisp rule: CADENCE is frame-linked for
everything; only terms missing a dt multiply are rate-SENSITIVE.
Corollary worth keeping: a choking 486 generated LESS heat/s exactly during
the biggest fights -- the slideshow was mercy. Veteran memory of seek-4
endurance encodes the LOADED rate, which is why bracketing by their consensus
is the correct instrument rather than a concession.
Follow-up filed in the KB: sweep the reconstruction for OTHER dt-less per-tick
accumulators (known: particle trail density, rendering.md; the 15-sample
heatFilter window is rate-dependent smoothing, benign). Each found one should
normalize against the same 28-nominal/bracketed-effective pair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"no guesses. we want to decompile and find the right answer" -- done. Every
function in the heat-removal chain read from the decomp and diffed against our
reconstruction:
HeatSinkSimulation @004ad924 E += pending; T = E/mass; alarms EXACT
UpdateHeatLoad @004ad7f0 radiated = T x coolant; clamp(0.002x) EXACT
ConductHeat @004ad8ac exchange into linked + rebalance EXACT
ComputeHeatFlow @004ad9ec (T1*mScale - (E1+E2+pend2)/den) * tau
* (1 - e^(-dt*k*(cool/cap)*flow/den)) EXACT
BalanceCoolant @004ada94 radiated-equalising move, 3 clamps EXACT
Condenser sim @004ae4d8 mScale = clamp>=1((1-zoneDmg)*refrig) EXACT
Reservoir sim @004aef78 inject-only; thermally passive EXACT
Every term is properly dt-scaled -- the dissipation side is rate-INDEPENDENT.
The only rate-dependent energy term in the whole thermal system remains the
myomer kinetic generation, i.e. the calibration knob is pointed at the right
factor and nothing on the cooling side can absorb the blame.
LIVE chain verification (new [heatflow] census, incl. a RADIATOR tag on the
bank's own Performance, which the base-sim census could not see):
Myomers 490 -> Condenser5 420 -> bank 337 -> ambient (relaxing onto the
300K setpoint). The exit exists, runs, and holds the bank just above 300.
AND ONE REAL FINDING -- my night-8 "no per-chassis cooling variation" claim was
WRONG, and Oracle's stated mechanism is vindicated to the digit:
bank k = 0.1 x HeatSinkCount x 231000: Owens count=5 -> k=115,500
Thor count=13 -> k=300,300
The Thor sheds 2.6x the Owens' heat through the mech's ONLY ambient exit --
"a thor could run seek 4 for extended periods because of the number of
heatsinks" is exactly the authored model. My hsparm chassis comparison had
printed RESOURCE constants BEFORE the ctor's count scaling; the dump now
carries a warning to stop the next person misreading it the same way.
(The KB was never wrong -- open-questions.md:593 documented the radiator
scaling when task #9 landed it. The wrong claim lived in my #96 comment.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
epilectrik: "the issue seems to be related to a tick rate that advances the
heat accumulation... i need to trace out what the clock origin was." Traced.
THE CLOCK ORIGIN [T1]: the DOS binary keeps its engine frame rate in the
global DAT_0052140c, set once at startup:
0x401ace: mov [0x52140c], 0x41E00000 = 28.0f (nominal)
0x401ada: mov [0x52140c], 0x4191A6E0 = 18.2065f (BIOS-tick fallback)
The DOS main pushes it straight into the ApplicationManager ctor (0x401189:
push [0x52140c]), and ~90 sites across the image fmul/fdiv by it -- it is THE
per-frame<->per-second conversion scalar of the whole 1995 engine. The engine
executes every interesting master every frame and every executable subsystem
every entity tick (UPDATE.cpp / ENTITY.cpp, T0 -- no throttle at any layer),
so subsystem Performance cadence == this rate: the pod's myomer tick was
NOMINALLY 28 Hz. My previous 30 Hz reference (the i860 BOARD frame) was
wrong by 7%, not the 2x the veterans hear.
THE RESIDUAL IS EFFECTIVE RATE, NOT NOMINAL: Oracle, on the halved build,
still asks "If you could please halve the myomer heat rate again. We will
bracket to something reasonable." A 486 host missing beats halves the real
Performance cadence without changing any constant -- and the 18.2 fallback
existing at all says slow paths were expected. Statics cannot settle the
in-pod effective rate, so his bracket is the right instrument:
effective default = 28 [T1 nominal] x 0.5 [T3 testimony calib] = 14 Hz
BT_MYO_HZ=<hz> still overrides absolutely for bracketing
the resolved rate now logs under BT_MYO_LOG, tier tags inline
Measured (Owens, seek 4, flat out): peak T 1007 -> ~640, degradation never
reached -- the halving Oracle requested, delivered as one auditable knob
instead of a silent constant edit.
KB: decomp-reference.md gains the DAT_0052140c section (~90 consumer sites --
whenever the decomp shows an unexplained mul/div by _DAT_0052140c it is
per-second<->per-frame at 28); btl4main.cpp's "30 = pod authentic" pacer note
corrected (30 kept deliberately for display smoothness, the divergence now
documented). Candidate follow-up recorded: the frame-rate-dependent particle
trail density (rendering.md) should normalize against the same 28, which may
bear on Ronin's smoke-density report (#114).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
User mandate (2026-08-02): "do tests like this from now on" -- after the #110
grind bench, where the field composition (peer fire destroying an arm over the
wire) replaced the constructed proxy that had let #86 be called fixed while
players kept hitting it.
The topic carries two halves on purpose:
DOCTRINE -- what counts as VERIFIED:
1. reproduce the REPORTED scenario, not a convenient adjacent one
2. scalpel hooks (BT_SELF_DAMAGE_ZONE / BT_KILL_SUBSYS / BT_FORCE_*) locate
defects; they support a "fixed" claim only with proven path-identity to
the field composition -- and the field composition still gets run
3. MP symptom -> two-node proof (master-side correctness says nothing about
what a peer sees)
4. visual symptom -> pixel proof (gotcha 23)
5. coverage claims need the axis enumerated and measured (all gates, all
chassis), because per-chassis behaviour lives in authored data
6. an unexplained extra effect in a passing run means the run has not passed
MACHINERY -- the bench_common.sh contract (summarized, file = source of truth),
single-node and two-node skeletons (relay, ports, affinity, fire cadence,
GOTO_STOP standoff), process hygiene (stale-node taskkill first, never
double-background, teardown kill order, stale-exe tells), and log-reading rules
(capped diagnostics are not evidence of absence; alarm lines are not trends;
name the actor at every refusal; field logs have no gates set -- spawn-time
summaries ungated, per-frame traces gated).
Routed: Quick Lookup row, CLAUDE.md reasoning step 4, build-and-run parity
section, reconstruction-method Key Relationships. checkctx CLEAN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two two-node benches close the last stated gap ("no two-node run has watched a
peer's view of an arm death"), the second at the user's insistence on the REAL
field composition after the first used the self-damage scalpel:
1. mp_armchain.sh (scalpel): B self-destroys dz_rarm mid-fight. A's screen
receives the whole cascade -- [zone-repl] dz_rarm lvl 0->1 AND the cascaded
dz_rgun lvl 0->1 -- plus the respawn reset coming back clean (1->0). B's
post-cascade AFC pushes turned out to be the RESPAWN RE-ARM, correct
behavior, not a gate failure. (Also confirmed: the self-damage harness
dispatches a real Entity::TakeDamageMessage -- the identical path a peer's
round takes, so the scalpel and the field share every line from the handler
down.)
2. mp_armgrind.sh (the field composition): stand-off at 180, NO self-damage
anywhere, A grinding B with real cross-pod fire for six minutes. On B:
* 25 incremental arm-zone hits from A's weapons via the cylinder tables
(dz_rgun reached 1.0 from pure accumulation -- 15 cascades on rehits)
* dz_rarm finished by B's OWN AFC bay cooking off (AmmoBinAFC100 BAY FIRE
350, half into dz_rarm) -- itself lit by A's fire crits: peer-originated
end to end
* [cascade] zone 9 -> AFC100 + AmmoBinAFC100 + Condenser6 force-failed,
fire gate refusals logged
On A: [zone-repl] dz_rarm 0->1 and dz_rgun 0->1 -- the shooter SEES the arm
and pod die. B symmetrically received A's torso deaths.
Also: DamageZone::ReadUpdateRecord gets a change-only [zone-repl] diagnostic
(BT_MP_LOG) -- the probe that made the peer side measurable; mp_armgrind.sh
opens with the stale-node taskkill the other benches already had (a leftover
node from an aborted launch joined a later lobby as a third instance).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answering "are you 100% positive this works for every weapon type" -- it was
not a yes when asked, and the gaps were closed with measurement rather than
assurance:
1. AUTHORED DATA, all eight chassis (zonesweep.sh; the [zone-armor] dump now
carries the zone NAME): every arm zone AND every missile-rack zone on every
chassis authors descend=1 (ava/bhk/lok/mad/own/snd/thr/vul); every gun/rack
zone lists 1-4 critical subsystems; doors are inert (0/0/0); bhk uniquely
adds destroySibs=1 on its arm chain. No chassis is missing the cascade
authoring.
2. WEAPON-CLASS GATES, end to end through a ZONE death (not direct weapon
destruction, which was #86's verification gap):
ballistic AFC100 (projweap gate1) -- dz_rarm, ava1 [prior commit]
energy PPC (emitter gate) -- dz_larm, ava1 [prior commit]
missile NRK5_2 (projweap gate1) -- dz_rmissle, own1 [this pass]
[cascade] zone 12 -> NRK5_2 + AmmoBinNRK5_2 force-failed
[ammo] NRK5_2 -> NoAmmo (gate1): destroyed=1
All three fire-gate code paths are now exercised by the cascade.
3. One anomaly chased to ground rather than waved off: the Owens rack cascade
also killed Avionics/HUD/Searchlight/ThermalSight -- zone 15 dz_searchlight,
an authored CHILD SEGMENT of the missile rack (the sensor mast sits on the
shoulder pod). Authored skeleton + authored descend flag, walked faithfully.
Remaining surface NOT bench-verified, stated plainly: cross-pod -- the cascade
is master-authoritative (crit plugs bind on masters only) and the resulting
alarm states ride the existing weapon update records, but no two-node run has
watched a peer's view of an arm death. The field re-test (Lynx/Conn Man's own
audit, in MP) covers exactly that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lynx: "You can destroy an upper arm, and the gun pod is intact and can still
fire." Conn Man's audit: three chassis, both arms, reproducible. And the user
is right that this family was claimed fixed before: #86 gated fire on the
WEAPON's own destruction and was verified by destroying the weapon directly
(BT_KILL_SUBSYS) -- the field scenario was the ARM ZONE dying with the weapon
subsystem healthy, which that bench never reproduced. Oracle's 693-night "I can
still fire destroyed energy weapons" was almost certainly this mechanism.
ROOT CAUSE -- a silent stub. Mech__DamageZone::RecurseSegmentTable @0049cad4
was transcribed faithfully... against three local shim types whose iterators
unconditionally return NULL:
struct SegmentIterator { DZRef *Next() { return 0; } };
struct SegTableX { SegmentRecord *operator[](int) { return 0; } };
The cascade fired on zone death (bench: 72 [cascade] lines), "descended", and
touched nothing. Every line read correct; none of it did anything.
THE REAL WALK (decomp re-read, FUN_0049cad4): iterate the mech's segment table
(mech+0x300, vtbl+0x34 GetNth) to this zone's segment, then
destroySiblingsOnDestruction (Wword 0x68): recurse every other zone on the
SAME segment (seg+0xD0), setting graphic state 2 (Gone)
descendOnDestruction (Wword 0x67): recurse every zone on every CHILD
segment (seg+0xE8 -> child+0xD0) -- the arm takes the gun
The engine ALREADY HAS both structures: EntitySegment::damageZoneTable @0xD0 /
childIndexTable @0xE8 (TableOf is 0x18 -- byte-identical to the decomp offsets),
populated at stream time by JMOVER.cpp:373 [T0]. The stub was never necessary.
Implemented with the engine iterators via two new read accessors on
EntitySegment (SEGMENT.h); the binary's asymmetry is reproduced as-is (the
sibling loop checks graphic state != 1, the child loop does not; re-hits on a
1.0 zone re-run the cascade -- idempotent in effect).
VERIFIED (ava1, self-damage to one zone, autofire everything):
dz_rarm -> [cascade] zone 9 -> zone 17; AFC100 + AmmoBinAFC100 + Condenser6
ForceCriticalFailure'd; "[ammo] AFC100 -> NoAmmo (gate1): destroyed=1";
torso LRM5/SRM2 keep firing (correct)
dz_larm -> [cascade] zone 2 -> zone 6; PPC + Condenser4 force-failed;
"[emitter] 'PPC' fire REFUSED (destroyed=1)" x4998 -- the emitter gate now
logs refusals BY NAME (the FIRED line never named its weapon, which is how
#86's verification gap survived)
regression smoke: frame time 7.13/7.20ms, 0 asserts, 0 cascades in ordinary
combat (they fire only on genuine zone deaths)
Authored data (now dumped under BT_DMG_LOG): descend=1 on exactly the four arm
zones (ava1: 2/6/9/17), destroySibs=0 everywhere -- so legs/torso behavior is
untouched by this change.
KB: combat-damage.md cascade section; reconstruction-gotchas #26 (the
silent-stub trap: a shim that returns EMPTY converts a reconstruction into
fiction that reads correct -- shims must Fail() loudly or log their emptiness).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>