17 Commits
Author SHA1 Message Date
arcattackandClaude Opus 4.8 145a69f865 Combat: FIX missile splash over-application -- once per salvo, not per round (task #62)
Live regression: a clustered bystander died in ~2 missile salvos ("suddenly
lethal"). Root cause, not authentic: the arcade fires ONE cluster Missile per
trigger (burstCount=missileCount) doing ONE SplashDamage event with baseBurst =
missileCount, floored at 1 ONCE. The port re-expresses that cluster as N flying
BTProjectile rounds, and task #62 fired splash PER ROUND -- each baseBurst=1,
each floored at 1 -- so the distance floor was applied N times = ~missileCount x
too much splash.

Fix: BTProjectile.splashBurst tags ONLY the salvo-lead round. MissileLauncher::
FireWeapon passes nmiss (the cluster count) on i==0 and 0 on every other round;
the contact + world-impact splash hooks fire only when splashBurst>0, using it as
baseBurst. One splash event per salvo with baseBurst=missileCount -- matches the
single arcade cluster missile. Replicant mirror rounds carry 0 (damage 0 too) so
the master's cross-pod splash isn't doubled. AC unaffected (not a MissileLauncher;
splash_burst defaults 0).

Verified headless (BT_SPAWN_ENEMY=2 clustered rig, 45s): bystander now takes 6
splash events (1/salvo, baseBurst=6) and SURVIVES (was ~2 shots); primary dies to
direct hits in ~3 trigger pulls (36 missiles == single-enemy TTK); AC does not
splash; no crash. Also: BT_SPAWN_ENEMY read as a COUNT (=2 clusters a bystander
within SplashRadius) for eyeballing splash; entity-id logging on [enemy]/[splash].
KB: combat-damage.md documents the N-round cluster trap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:43:50 -05:00
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00
arcattackandClaude Fable 5 bb795e2805 MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified:

COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity
per contact and ProcessCollisionList walks EVERY touched solid per frame; with
2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a
walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy).
Fix: frameEntryWorldVelocity restore per contact (damage always priced at the
real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes
the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry.
Gotcha #16 (engine-facility drift class).

MISSILES: peer-visible salvos (the launcher record extension carries a salvo
counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone
never serialized), the authentic arc (authored MuzzleVelocity vector + the
Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78),
world-impact bursts (rounds detonate on cave geometry instead of phasing
through), contact-only damage (flight-cap expiry = fizzle, no more teleport
damage), live re-lead, and ballistic (unguided) shells for autocannons.
projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6
corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded.

RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244
is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation
(never the translation); worldToView now Invert(view) built rotation-first.
CulturalIcons sorted out of the moving grid (the phantom red pips were map
props), visible-radius culls on all three draw passes, live pip verified at
|delta| x ppm px.  Gotcha #17 (verify the FUN_ body, not its call shape).

WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means
FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X
lamps, the panel look, the children enable and the ready-lamp gate (which had
NEVER executed).  Polarity chain corrected end-to-end (failedState, fed by real
damage saturation).  Root cause of the freezes: MFD page-mode gating -- the dev
composite shows ALL pages at once, so off-page dials legitimately stopped; under
BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio
untouched).  The SEH gauge guard now names its kills; repaint-heal resets the
incremental arc after panel repaints; [panel]/[arc] probes added.

COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole
Player__StatusMessage queue; wired the binary's one producer -- the kill branch,
victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS
counted via the observed-death tally (each node scores every pilot from locally
observed events, the same model as the KILLS credit) and the -2/-1 engine seed
clamped for display.

DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480,
true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering,
BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar
(first second-chassis live outing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:24:15 -05:00
arcattackandClaude Fable 5 dd27238ceb Gauges: the CLOSEOUT wave -- radar pips, Myomers dedup, attribute parity, ledger sweep (tasks #14-#17)
TASK #17 -- AUTHENTIC RADAR SYMBOLOGY (the cross-blip stand-in retired).
The recon proved the 'missing pip raster set' never existed as BT game code:
pips are the ENGINE's L4GaugeImage vector-stroke system (T0 source in tree,
L4GAUIMA.cpp == FUN_0046f0c0 line-for-line), the 'pip table' is L4Warehouse::
gaugeImageBin keyed by Entity::resourceID, and BTL4.RES ships 110 type-0x12
shapes (every mech/vehicle/building/tree).  The six btl4rdr stubs are wired
to the real facilities: contacts draw their authentic model silhouettes with
LOD selection, the target gets the binary's 4px-inflated box highlight, and
player-name labels resolve via Mission::GetSmallNameBitmap (the prebuilt
64x16 egg rasters keyed by the player's bitmapindex).  DECODED: the invented
'VideoObject' was Entity::owningPlayer all along (+0x190; nameID =
playerBitmapIndex@0x1E0, target = BTPlayer::objectiveMech@0x284 -- new
bridge BTPlayerObjectiveMechOf); the 'LabelledEntity' class is Landmark
(cultural.h; label path dormant -- no landmark content ships, no runtime
landmarkID writer).  The L4GREND BT_DEV_GAUGES warehouse guard is removed
(its AV had a different culprit, below); resource type 18 corrected to
GaugeImageStream in decomp-reference (was mislabeled 'ModelList').

TASK #14 -- the Myomers ODR duplicate ELIMINATED: the powersub.cpp/hpp
'Myomers' (classID 0xBC3 -- actually Sensor) is retired whole; it duplicated
?DefaultData@Myomers@@ against the real class (dumpbin-verified) and /FORCE
picked the winner by link order.  The real Myomers (0xBC6) now chains
PoweredSubsystem's handler set (ids 4-8) and publishes its SEVEN binary
attributes (@00511588: SpeedEffect/Current/Recommended/Min/MaxSeekVoltage-
Index/SeekVoltage/OutputVoltage) -- the old empty unchained index starved
the Myomer engineering panel of every resolve.

TASK #16 -- attribute parity: MechWeapon publishes the FULL binary table
@0x511890 (11 entries; ids renumbered to binary truth -- the port aliases
had squatted the binary's DistanceToTarget/TargetWithinRange ids; the
streamed TriggerState 0x13 binding unchanged).  Binary names resolved two
TODO members: pipState -> estimatedReadyTime (attr 0x1A), and the EXT-model
flag is the binary's RearFiring (0x1B).  ThermalSight LightState published.
HUD (offset conflicts) + missile-side tables (id encoding suspect)
documented for a re-dump instead of publishing blind.
  THE CRASH THIS EXPOSED [T2, cdb-verified]: gotcha #11's dense-table gap
is a LATENT AV, not a guaranteed one -- the old table's 0x0D..0x12 gap
(task #5) survived on heap luck; the renumber reshuffled allocations and
Find() AV'd on a garbage entryName in WeaponCluster's PercentDone resolve.
Fixed with five named PAD entries (the mech.cpp attrPad idiom) + a
static_assert locking the pad base to PoweredSubsystem::NextAttributeID.
Gotcha #11 amended with the proof.

TASK #15 -- stale-ledger sweep: GAUGE_COMPOSITE ('composite not yet built',
Reservoir shadow, PlayerStatus/vehicleSubSystems 'remaining', valve-dormant-
until-0xBD3, sensor guard, the superseded 'Heat MFD near-static' reframe --
all banner-corrected), gauges-hud frontmatter, L4VB16 + powersub comments.

Verified live: 50/50 config attribute bindings resolve, 0 NULLs, 0 parse
skips, mech spawns and simulates 31/31 subsystems, no cross-blip fallbacks,
the pip cache fills through entity registration without the old guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:57:45 -05:00
arcattackandClaude Fable 5 d7b900d108 Cockpit: GENERATOR POWER ROUTING -- assign weapons to generators (task #12)
The pilot's second heat-management tool: the PoweredSubsystem message table
@0x50F4EC (ids 4-8) registered and implemented -- SelectGeneratorA-D
(@004b099c..@004b0a74: FindGeneratorByNumber roster walk on generatorNumber
@0x1E0 -> AttachToVoltageSource re-tap with tap accounting -> modeAlarm
Connected) and ToggleGeneratorMode (@004b0abc: Manual -> Auto -> detach+Manual
cycle).  Weapons inherit via the MechWeapon handler chain.  Desktop: F5-F8
assign the selected weapon (BT_CONFIG_SLOT) to Generator A-D, F9 toggles
reconnect mode; BT_GENSEL_TEST scripts a headless verify.

Verified live end-to-end: dispatch -> handler -> re-tap ("PPC_1 ->
GeneratorD (tapped)") -> the charging I^2R physically moved (GenD cold ~90K
baseline -> ~1570K carrying the PPC; GenA relieved), stable over a sustained
autofire soak with thermal-breaker trips.

FOUR defects found and fixed on the way [all T1/T2]:
- THE e17 HEAT EXPLOSION: Generator::SourceLevel misread *(this[0x38]+0x158)
  as linkedSinks->heatEnergy; it is the engine-base DamageZone @0xE0 ->
  damageLevel [0..1] (the same named-member pattern as the bank radiator's
  zone read).  A breaker-restarting generator emitted (1 - 4e8) x 10000
  volts; squared through the customers' I^2R feed, one restart blew the
  whole thermal network to e17.  Authentic: a damaged generator yields
  proportionally less voltage.
- FUN_004ac9c8 is NOT "IsDamaged": raw body = owner -> mech+0x190 player ->
  roleClassIndex(+0x274) == 0 -- the ROOKIE-role lockout for advanced
  cockpit systems.  New bridge BTPlayerRoleLocksAdvanced (NULL player =
  unlocked [T3]; bring-up role 2 = unlocked).  The old stand-in gated the
  handlers off permanently (healthy subsystems have simulationState==1).
- MESSAGE_ENTRY tables must be FUNCTION-LOCAL statics inside the accessor:
  as namespace-scope arrays they are read by other TUs' static-init chains
  before their own initializers run -- Build copies zeros and every id in
  the table is silently dropped (ids 9/10 only worked by TU-order luck;
  both tables relocated; gotcha recorded, reconstruction-gotchas #9).
- The AutoConnect hunt scanned GetSegment() -- the raw @004b0bd0 walks the
  subsystem ROSTER (+0x124/+0x128); EntitySegments were being cast to
  Subsystems (the classic +0x128 gotcha).

Also: the dense handler table's GAP slots (skipped ids) are uninitialized
heap -- the name-based Find strcmp-walks them and AVs; diagnostic probes use
the id-based Find only (the 1995 binary's own tables carry the same holes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:16:48 -05:00
arcattackandClaude Opus 4.8 a35f321ba1 Warp: exact POVTranslocateRenderable replica (collapse/throb/reveal + world mask)
Stop approximating -- replicate POVTranslocateRenderable::Execute() behaviorally,
byte-for-byte on the constants and transitions (engine T0, L4VIDRND.cpp:1763-2076),
per the user's "replicate the effect exactly".  The five prior piecemeal attempts
each fixed one facet and broke another because they left out the WHOLE mechanism.

Full state machine (btl4vid.cpp), driven by two decoupled events (NOT the
SimulationState trigger -- that dial also drives camera/POV+targeting, the f053535
regression; SetIsDead is a separate pure-render flag, verified safe):
  - LOCAL DEATH -> InitialCollapse: scale (pct_left*100)+1 over 1.3s (world visible),
    then SetIsDead(true) + WaitForReincarnate.  (btplayer VehicleDeadMessageHandler,
    deathCount==-1, local-guarded.)
  - WaitForReincarnate: world BLACK (mask), scale 1, Lissajous throb
    (cos(t*3.33), sin(t*2.5)) x2.0 -- the "dances around playfully".
  - LOCAL RESPAWN -> ExpandReveal: SetIsDead(false) + scale (pct_used*150)+1 over
    1.0s -- blasts open, world revealed ("shoots off into the distance").  (btplayer
    DropZoneReplyMessageHandler, local-guarded.)

Draw EXACTLY as the engine (isDeathDraw -> drawAsSky): PASS_SKY, OPAQUE,
CULLMODE=CW, z-test ON (L4VIDEO.cpp:7526/7568-7570/7693).  This kills the "glitchy
funnel" -- opaque = one write/pixel (no translucent double-blend), CW = one winding
of the double-sided mesh (no coincident z-fight), z-on = the expand shell is
occluded/revealed by the returning world.  Placement = Scale * inverse(view) with
NO recenter (the mesh is authored off-origin so the eye sits inside, authentic);
dropped the wrong mCullCenter recenter and the Z-off overlay hack.  Colour is the
"sky" ramp (per prior commit); no geometry spin (myRotateY is dead) -- swirl is the
texture scroll.  SetIsDead reached via a BTSetWorldDead bridge (L4VIDRND.cpp).

STUCK-BLACK SAFETY (the one new risk of decoupling mask-on from mask-off): a 12s
WaitForReincarnate render-loop timeout un-masks if no respawn arrives, plus
BTWarpForceUnmask on the mission-ending + no-DropZones abort paths.  Smoke-verified
2-node: collapse->wait(masked=1)->expand(masked=0), ends masked=0, no crash.

The peer-observer warp stays a world-anchored translucent alpha draw (port
extension; the authentic effect is POV-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:43:54 -05:00
arcattackandClaude Opus 4.8 a9d1534292 Warp: faithful POVTranslocateRenderable -- eye-centered, ramp color, no spin
Deep-dived the authentic engine effect (L4VIDRND.cpp POVTranslocateRenderable, T0)
and reworked our one-shot to match it, fixing the user's "blob from my own POV /
not aligned to my orientation" + "gray, not blue" reports:

1. PLACEMENT was the misalignment. The engine parents the sphere to the VTV/eye
   ("rotated and scaled around the VTV", L4VIDRND.cpp:1812) -- it is centered on and
   oriented to the viewpoint. We world-fixed it at the mech's feet, so from your own
   camera it sat off-axis and, once expanded, engulfed the eye as a shapeless blob.
   Now: the own-respawn warp (BTStartWarpEffectPOV, from btplayer) uses
   world = Scale(s) * inverse(view) -> centered on + carried by your eye (the tunnel
   wraps you). Observing a PEER (BTStartWarpEffect from mechdmg) stays world-anchored
   at the peer's point.

2. GEOMETRY SPIN was inauthentic. myRotateY/myRotateYSpeed are set in the engine
   ctor but NEVER applied -- the swirl's rotation is entirely the material's texture
   SCROLL (bintA, SCROLL 0.0 0.0 0.1 0.5). Dropped our world-Y geometry spin.

3. COLOR (gray) fixed at the source. The blue/white is the "sky" RAMP remapping the
   grayscale bintA cloud per-texel (argb=lerp(0,0,0.6 -> .99,.99,.99, luminance),
   L4D3D.cpp:480). The port bakes exactly this but GATED it off for normal-bearing
   meshes; tsphere has normals so it arrived raw gray, and a MODULATE tint can't add
   the blue floor (it only multiplies) -> the wash-out. Un-gate the ramp for
   tsphere_mtl (bgfload.cpp) so the bound texture is already the blue-white swirl;
   draw it MODULATE by white (no tint), alpha from TFACTOR.

4. SCALE to authentic 100 (collapse) / 150 (expand end) -- correct now that it's
   eye-centered (you're meant to be inside it as it collapses through you and blasts
   back open, the "blast off into the distance").

Still deferred (higher risk / SimulationState-coupling trap): the WaitForReincarnate
Lissajous wobble + the SetIsDead world-mask (black-out during the dead hold). Noted
for follow-up; the placement/color/motion are the parts the user flagged.

Smoke-verified 2-node: own respawn logs POV(eye), observer logs world-anchored, no
crash. BT_WARP_ADDITIVE / BT_WARP_COLOR / BT_WARP_SCALE remain as live tuning knobs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:42:02 -05:00
arcattackandClaude Opus 4.8 247e51e1e1 Reset-based respawn: reuse+heal the mech, not create-a-new-one (task #52)
The respawn glitches (2 mechs, on-fire respawn, camera-inside, can't-control,
wreck-never-disappears) all traced to one architectural divergence: our respawn
SEVERED playerVehicle on death and CREATED a new mech, leaving the old as a
permanent wreck and building a duplicate viewpoint whose old render tree was
never torn down.

The authentic engine (FUN_0049fb74 + RPPlayer) REUSES the same mech entity: on
respawn Mech::Reset heals it and moves it in place. Implemented faithfully,
adapted to our layout (the 1995 raw offsets map to different 2007 engine fields,
so reset the equivalent named members, not the offsets):
- Mech::Reset (real, was a reposition-only stub): reposition + kill dead-reckon
  (projectedOrigin/projectedVelocity/updateVelocity + our relocated gait
  accumulators) so the replicant stops lerping to the death spot; clear the
  death latch (movementMode=1, graphicAlarm=0); Heal every damage zone
  (new Mech__DamageZone::Heal: full structure, intact skin); DeathReset
  (vtable+0x28) every subsystem; ForceUpdate to broadcast.
- btplayer.cpp: VehicleDead no longer severs playerVehicle; the respawn re-post
  gates on the mech still being dead; DropZoneReply resets the EXISTING mech in
  place (heal+move) instead of creating a new one, then fires the warp. Warp
  moved to the shared placement (initial drop-in + respawn).

Verified 2-node: mech entity ID stays 3:22 across 3 deaths (reused, not a new
3:32); each Reset logs alive=1, 20 zones healed, 33 subsystems reset; A sees ONE
mech (no wreck+new pair). Warp fires each respawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:13:44 -05:00
arcattackandClaude Opus 4.8 f0535356bc Warp: decouple from player SimulationState (fix camera/targeting regressions)
Pulsing the player's SimulationState (DropZoneAcquired->Translocated) to trigger
the translocation sphere regressed everything else that dial drives in our
reconstruction -- the camera flipped to inside-view, targeting/firing gated off,
shadow pass glitched. That dial is load-bearing; co-opting it was wrong.

Replace with a self-contained render one-shot: btplayer.cpp respawn calls
BTStartWarpEffect(dropZoneOrigin); the effect plays its own collapse->expand and
touches nothing but the render. No SetSimulationState, no DropZoneLocation write,
no per-entity renderable walk (the tree's BTTranslocationRenderable objects are
now inert). Scale capped 30/40 (authentic 100/150) so the sphere -- centred on
your own reinsertion -- doesn't envelop the camera; tunable via BT_WARP_SCALE.

Smoke-verified 2-node: warp fires on each respawn (collapse 31->5, expand ->35),
repeats, respawn cycle intact, no crash. Camera/targeting restored (state pulse
gone).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:58:38 -05:00
arcattackandClaude Opus 4.8 63c1c5a460 Implement the translocation sphere -- the "blue warp" respawn effect (task #52)
BTTranslocationRenderable was a no-op stub; now reconstructed over the engine's
POVTranslocateRenderable: loads tsphere.bgf and runs a collapse-on-arrival /
expand-on-reveal sphere (scale 100->1 over 1.3s, then 1->150 over 1.0s, rotating)
keyed on the player's SimulationState dial. Drawn direct from the render loop by
BTDrawTranslocationSpheres (beside BTDrawBeams, PASS_ALPHABLEND) -- the same
accommodation the beams/reticle use.

The asset loads by FILENAME (tsphere.bgf), not through the RES table -- which is
why every resource-name search missed the effect for three rounds.

Trigger wiring: btl4vid.cpp MakeEntityRenderables builds the sphere for the LOCAL
player (the authentic wiring builds it only for replicants + a POV fade for self,
but peer player-attribute replication isn't wired -- on a replicant SimulationState/
DropZoneLocation read uninitialised). btplayer.cpp pulses SimulationState
DropZoneAcquired->VehicleTranslocated at respawn (a 1.4s flip timer stands in for
the engine's +1s drop-zone re-post) and writes the respawn origin into the
DropZoneLocation attribute.

Verified 2-node (BT_TLOC_LOG): on respawn the sphere collapses (~100->5) then
expands (->150) at the valid drop-zone origin, deduped to <=2 active, respawn
cycle un-regressed, no render errors. Visual appearance (colour/size) still needs
a live look; the authentic see-others'-spheres path needs player replication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:41:57 -05:00
arcattackandClaude Opus 4.8 83b3f31957 Whirlwind respawn: authentic death->drop-zone->recreate cycle (task #52)
Death transition (mech4) now dispatches VehicleDead(-1) to the owning
player; BTPlayer::VehicleDeadMessageHandler restructured to the authentic
@004c05c4 three-way branch: -1 = death bookkeeping + sever playerVehicle
(wreck stays) + 5s re-post; >=0 = engine drop-zone hunt -> DropZoneReply
-> CreatePlayerVehicle (new mech); -2 = the acquire probe. Guarded on a
DropZones group so a zone-less mission stays dead instead of aborting.

Three latent bugs the respawn path exposed, all fixed:
- IsMechDestroyed latched on graphicAlarm>=9 alone; a later leg hit on the
  wreck rewrites the alarm to 4/3, un-latching -> the death transition
  re-ran (double kill/score, abort). Now latches on movementMode 2||9.
- Score handlers dereferenced the severed playerVehicle during the dead
  window; guarded.
- The console score flush routed a NetworkClient::Message through the
  player's Entity::Dispatch, which stamps entityID past the smaller struct
  -> /RTC1 stack overflow. Sent via application->SendMessage instead.
- Renderer LoadMission re-entry (per viewpoint-make) re-read the env INI;
  its light block Fail'd on stale sceneLightCount. Reset it in
  DPLReadEnvironment so the respawn's second read is clean.

Verified 2-node self-drive: B killed repeatedly by A respawns each time
(wreck stays), ticks + reloads + takes fresh damage on the new mech, no
abort/hang across multiple death->respawn cycles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:26:55 -05:00
arcattackandClaude Fable 5 a9c3e96f20 MP combat step 1 (target any peer) DONE; step 2 (cross-pod damage) localised (task #46)
STEP 1 -- target any peer mech: DONE + verified. A live-mech registry
(BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects
every mech -- player, dummy, and peer replicants. The boresight
world-pick walks BTGetTargetCandidates (all mechs != shooter, closest
ray hit) and the whole fire/damage block retargets from the solo
gEnemyMech to the picked hotTarget; missile/projectile validation
generalised via BTIsRegisteredMech. Verified one-box: instance A
enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3).
Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED).

STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and
invoked with a valid owner, but the dispatched message doesn't reach
the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's
msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A
dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6
damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute
FIRES; the break is downstream in the transport/delivery of a
dispatched entity-message (update records flow fine -- net-rx works).
Next MP task = that wire delivery. Diagnostics BT_MP_LOG /
BT_MP_FORCE_DMG retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 10:13:39 -05:00
arcattackandClaude Fable 5 a834866fcd Enemy spawn faces the player (was copying the player's pose -> back turned)
The dummy spawned 120u along the player's forward with the SAME orientation,
so both mechs faced the same way and the player stared at its back (also why
first shots read as rear hits).  Flip the enemy's yaw 180 deg about Y (engine
convention: Z basis = (sin y, 0, cos y) -> yaw = atan2(z.x, z.z) + pi) so the
two spawn face to face.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:39:15 -05:00
arcattackandClaude Opus 4.8 7fc4acb89f gauge-complete P4g: MessageBoard reconstructed + registered -> LAST parse-skip cleared (0 unregistered gauges)
The "messageBoard" cockpit primitive (the secondary-MFD comm/status message ticker,
L4GAUGE.CFG:4913) was PROSE-ONLY (no bodies/methodDescription) -> parse-skipped. This
was the last unregistered gauge primitive.

Reconstructed byte-verified (Make @4cb678, ctor @4cb704, dtor @4cb788, BecameActive
@4cb7fc, Execute @4cb82c; vtable 0051bddc; sizeof 0xA4). THREE header mislabels fixed
(decode + adversarial verify): int enabled -> Entity* trackedMech (a pointer deref'd at
+0x190, set by SetSource not SetEnable); previousMessageId/previousNameId were SWAPPED
(BecameActive writes 0x9c=-1/0xa0=-2; Execute compares messageId vs 0xa0, name vs 0x9c).
Execute blits the strip cell (id -> (id&3)<<7,(id>>2)<<5) + sender name; methodDescription
= 4 params (rate, mode, btsmsgs.pcx, color). Registered in BTL4MethodDescription[].

DEFERRED / EMPTY by design (authentic for bring-up): the source is never bound (SetSource
has no recovered caller) AND the per-player status queue (StatusMessagePool, btstubs.cpp:62)
is a NULL stub -> no status messages exist -> Execute early-returns on the NULL source (safe
no-op == empty board). Data read via a BTResolveMessageBoard bridge in btplayer.cpp (a real
/FORCE-safe stub returning False; the raw mech+0x190/+0x1dc reads stay in the complete-BTPlayer
TU, dodging the databinding trap). The name-cell path is a marked structural simplification to
restore when the feed lands.

VERIFIED: the gauge parse-skip list is now EMPTY ([gskip]=0 -- every config gauge primitive
registered + built); BTResolveMessageBoard resolves (no /FORCE __ImageBase AV); combat TARGET
DESTROYED, no crash; gauge composite renders identically (300/77, clusters, radar un-regressed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:39:47 -05:00
arcattackandClaude Opus 4.8 a9210e038a gauge wave P3: fix the DEATHS runaway + RANK=-1 (solo scoreboard correct)
RANK=-1: Player::DefaultFlags creates every player NonScoring (CalcRanking only
ranks IsScoringPlayer()==true, else -1), and the bring-up never cleared it.  Call
SetScoringPlayerFlag() in DropZoneReplyMessageHandler's mech branch (a piloted mech
IS an active scoring combatant; the camera-ship branch stays non-scoring).  -> RANK=0.

DEATHS runaway (climbed ~1/s, not the "1" first seen): @004c012c -- which our
MESSAGE_ENTRY binds as the VehicleDead handler -- is actually the drop-zone
RESPAWN-RETRY helper.  The engine's RequestDropZone (PLAYER.cpp:400) posts a
2s-delayed VehicleDead(deathCount=-2) as a "did the drop zone reply?" timer; our
handler ++deathCount + RE-POSTED it UNCONDITIONALLY -> an infinite loop, because the
drop-zone handshake never "completes" in the bring-up (no console/drop-zone system).
Traced it to a pure boot artifact (13 fires with zero combat; the only VehicleDead
producers are the re-post + my kill-producer, and the kill fires AFTER the loop
starts).  FIX (correct respawn-retry semantics): if the player already has a vehicle
the drop zone WAS acquired (the DropZoneReply created it), so the retry is moot --
return without counting a death or re-posting.  -> DEATHS=0 in solo, loop gone.

Verified DBASE+dev gauges: the Comm roster shows KILLS=1 / DEATHS=0; the KILL log
reads killCount=1 deaths=0 score=164 rank=0; 0 crashes.  (MP real-death DEATHS -- a
destroyed vehicle via the separate @004c05c4 path -- remains deferred per the plan;
solo never dies so the retry gate is correct there.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:19:00 -05:00
arcattackandClaude Opus 4.8 34aaa7dda4 gauge wave P3: wire the combat scoring feed (KILLS tracks combat)
The Comm roster + score gauges read 0 for 4 independent reasons (none was handler
logic -- the BTPlayer ScoreMessage/VehicleDead/ScoreInflicted handlers were already
reconstructed).  Mapped by the scoring-feed-decode workflow; fixed all 4:

- NO PRODUCER: combat only dispatched Entity::TakeDamageMessage, never a scoring
  message.  Added producers (btplayer.cpp bridges, called from mech4.cpp): per-hit
  ScoreInflicted at the beam (:2171) + projectile (:831) damage dispatch -> SCORE;
  KillScore at the TARGET-DESTROYED edge (:2198) -> KILLS.  senderMechID = the
  VICTIM (so the local player is credited via the !=our-mech branch, not the
  suicide-negate branch); dispatched to application->GetMissionPlayer().
- CROSS-FAMILY MECH OFFSETS: MECH_OWNING_PLAYER/TONNAGE/DAMAGE_BIAS read raw binary
  offsets (garbage in our 0x638 Mech).  MECH_OWNING_PLAYER -> Entity::GetPlayerLink()
  (NULL for the ownerless dummy); tonnage/bias stubbed 1.0/0.0 (bring-up).
- NULL scenarioRole: the BTMission role registry has no WinTesla analog, so every
  award path NULL-deref'd.  CalcInflictedScore returns the neutral (damage+bias)
  when scenarioRole==0; DamageReceived guarded.
- NULL-owner dummy: the KillScore sender-owner increment + StatusMessage now guard
  GetPlayerLink() (the dummy has none) so a solo kill credits only the local player.
- DATABINDING: the PilotList read KILLS/DEATHS at raw offsets (pilot+0x27c/+0x200)
  that don't match our compiled layout -> silent 0.  Repointed to the compiled
  members via bridges (BTPilotKills/BTPilotDeaths -> BTPlayer::GetKillCount/GetDeaths).
  ⚠ ROOT-CAUSE of the empty scoreboard: the PilotList SELECT-TARGET highlight did a
  raw-offset deref (*(local+0x284) objectiveMech, then *(tgt+0x190)) that AV'd on our
  layout -- caught silently by the SEH GuardedExecute, aborting Execute BEFORE the
  KILLS/DEATHS draw.  Replaced with BTPilotIsSelected (accessors + GetPlayerLink).
- SCORE persistence: PlayerSimulation's 10s flush zeroed currentScore (the binary's
  console delta); now only flushes/zeros when a console host exists (solo keeps the
  running score for the gauge + CalcRanking).

Verified DBASE+dev gauges (BT_SCORE_LOG): SCORE climbs +5.83/hit -> 164; on the
kill KILLS 0->1, and the Comm roster RENDERS KILLS=1 (screenshot).  No crash.
Follow-ups (noted): DEATHS=1 is a pre-existing spawn/respawn-handshake artifact
(not from the producers -- the dummy's GetPlayerLink is NULL so no VehicleDead is
posted; confirmed killCount=1 not 2); RANK=-1 is CalcRanking's non-scoring mark
(Plasma serial surface, invisible on dev); MP VehicleDead/tonnage/role registry
remain per the plan.  Diagnostics kept: BT_SCORE_LOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:03:38 -05:00
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00