Commit Graph
34 Commits
Author SHA1 Message Date
arcattackandClaude Fable 5 8057c37e5b Cockpit: punch patches ARE the windows -- all 8 mechs authentic (task #55)
Resolved the non-Madcat blocked views. Rendering every *_COP shell offline
from its mech's AUTHORED eye (SKL [jointeye] translation -- incl. the Thor's
+1.13 offset cockpit) with faces coloured by punch flag showed the answer
plainly: the PUNCH-tagged patches sit exactly over the window apertures in
all 8 mechs. On these texture-less black-diffuse shells dpl_Punchize renders
the tagged geogroup as holes -- the punch patches are the transparent
WINDSHIELDS, and the visible frame is only the non-punch geometry.

The loader now drops the punch batches of _cop meshes (bgfload finish();
BT_COP_KEEPPUNCH=1 keeps them for diagnosis). In-game sweep on pure defaults:
- madcat: the dark dome frame (matches gameplay footage)
- thor / owens / sunder: authentic brace/dash/pillar remnants
- bhk1 / loki / vulture / avatar: authentically FRAMELESS (all-punch
  canopies -- the pod showed no frame for those mechs)

(An early-session test that "disproved" drop-punch ran with the broken
pre-inverse eye; its conclusion was wrong and is corrected in the KB.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 07:49:33 -05:00
arcattackandClaude Fable 5 abaa145b6f Cockpit: authentic eyepoint + canopy lattice render (task #55)
The first-person cockpit now shows by default and matches gameplay footage
(verified on the Madcat). Two decomp-verified reconstructions:

EYEPOINT (FUN_004579a8 + caller part_014.c:5525, FUN_004c22c4):
- eye offset = siteeyepoint GetBaseOffset (not GetSegmentToEntity, not the
  upright hack), parented on the PARENT segment DCS (btl4vid.cpp)
- live view = affine INVERSE of the eye world matrix (L4VIDRND Execute) --
  replaces the hand-rolled LookAtRH whose +Z-forward/+Y-up row guess aimed
  some mechs into the canopy; combine order fixed to baseOffset * R
- chase camera basis + aim boresight re-expressed for the inverse convention
  (row2 = back); boresight now derives from the view matrix (the stale rows
  aimed the pick ray at the sky = no target, no discharge)

CANOPY (*_COP.BGF, dpl_Punchize geogroups):
- every shell is an open strut lattice (38-59% boundary edges, all 12 mechs);
  rendered single-sided with per-face INWARD winding (l/r torso patches are
  mirrored, so no global winding works) the struts are the dark frame and the
  openings show the world
- frame colour = the 'softer' ramp near its dark end (texture-less unlit ramp;
  BLXSKIN.BMF has no texture -- the old "punch texels" theory was wrong)
- scoped by filename (meshIsCop); wrong drop-punch + view-cut paths removed
- BT_FORCE_MODEL=<name> forces the player mech for per-mech bring-up

KB: new context/cockpit-view.md topic; punch split corrected in bgf-format;
gotchas 14 (LookAt axis guess) + 15 (per-patch edge namespaces); env gates +
eye/punch addresses in decomp-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 06:46:54 -05:00
arcattackandClaude Opus 4.8 7b0b4f64ab Diag: add BT_SHOT backbuffer dump + correct/park the -net render glitch (task #53)
Scouted the "-net node render glitch". Two prior framings were wrong and are
corrected in the KB: it is NOT device contention (retracted earlier), and it is
NOT a from-load glitch -- the trigger is a RESPAWN (user correction). It's a
post-respawn render-rebuild fault on the MP path.

Could not reproduce in 9 automated 2-node loads + a forced death/respawn (all
clean). Debug uninit is a deterministic 0xCDCDCDCD, so the intermittency implies
a race -- and the task #52 respawn fixes (LoadMission re-entry, viewpoint relink,
death-latch) plausibly already closed it. Parked as WATCH; re-open if it recurs.

Tooling (general, off by default): BT_SHOT=<path> dumps a per-instance backbuffer
PNG from the world-render path (L4VIDEO.cpp) every 90 frames -- lets us capture a
specific node's frame without foregrounding the window. Used with
scratchpad/hunt53.sh (N-run divergence hunter) to verify a fix if the glitch
returns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:48:54 -05:00
arcattackandClaude Opus 4.8 0bfb3d4ab3 Warp: restore the translocation-vortex look + fix texture-scroll precision collapse (task #52)
The death/respawn "blue whirlwind" (tsphere) now matches the original cabinet
photo (capture.png): a smooth spinning lavender vortex with a bright core.

Root cause of the long-standing "radial spokes" artifact was NOT the warp code
but a general engine bug: L4D3D::SetTextureScrolling computed its texture-matrix
offset as scrollDelta * absolute_time, which grows unbounded and collapses UV
float precision -> a smooth scrolled cloud shatters into grainy radial steps.
Wrapped with fmodf(..., 1.0f) (identical under REPEAT tiling, full precision).
This also cleans the scrolling bexp beam grit and any other SCROLL material.

Visual reconstruction (verified against the real 45-vtx TSPHERE.BGF bicone,
offline-rasterized then ported):
  - view ON-AXIS (eye centred on the throat) + spin in place -> concentric rings
    (decomp FUN_00453dc4 does spin-about-local-Z + submit; the port had stubbed it)
  - bintA cloud through a WIDE lavender ramp at full contrast (drawn as SKY);
    no geometry "bands", no log-polar twist, no off-axis tornado (all discarded)
  - tessellate the 12-facet bicone smooth; isotropic + trilinear; ramp baked into
    the texture and drawn SELECTARG1(TEXTURE) to avoid double-tinting

Env knobs (BT_WARP_*) default to the verified values; BT_WARP_SELFTEST/SELFSHOT
are an off-by-default visual-verification harness (backbuffer frame dump).

Docs: new context/translocation-warp.md (geometry/material/visual/lifecycle/env);
reconstruction-gotchas.md gains the accumulated-time precision-collapse bug class;
rendering.md / multiplayer.md / decomp-reference.md / CLAUDE.md cross-linked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 14:53:50 -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 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 47aede3094 Shadow: tag tshd proxies in the LOADER (V toggle broke the pipeline)
User report: the shadow broke MID-SESSION -- suddenly solid opaque black and
buried on any slope.  The session log pinned the trigger: pressing V.  The
view toggle (SetViewInside) RELOADS every segment mesh through the object
loader and swaps them into the render tree -- including blh_tshd.bgf -- and
the reload path never called SetIsShadow(1) (only the original tree-build
call site tagged it).  Untagged, the entire shadow pipeline disengages: no
translucent TFACTOR state block (solid black), no depth bias (buried), wrong
pass.  The damage-swap reloads had the same hole.

Fix: tag *tshd* filenames in LoadObjectBGF itself, next to the existing
all-shadow-material detector (which misses the mech proxy -- plain black
material, not shadow_mtl).  Every load path -- build, damage swap, view
toggle -- now yields a tagged object; the drawOps alphaTest routing reads
the flag downstream.  User-verified live: the shadow survives repeated V
toggles, translucent and slope-correct.

This also likely explains the RECURRING "shadow vanishes on inclines"
reports: any V toggle silently broke it; every relaunch "fixed" it.  KB
(locomotion.md): loader-level tag recorded as co-dependent part 5 of the
shadow arrangement; the vanish-diagnostic note now lists all three causes
(reload untag / gait desync / genuine burial) with check-first guidance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 17:10:33 -05:00
arcattackandClaude Fable 5 b6fe686b22 Shadow: authentic terrain tilt + decal draw order (feet layer correctly)
User report: the ground shadow painted OVER the mech's feet.  Root cause: the
flat quad + big depth-bias (-0.004 ~= 2-4 world units) workaround for slope
burial -- a bias big enough to beat the TERRAIN also beat the FEET in the
depth test, and the shadow drew in the late alpha-blend pass (after the mech),
so z-arbitration was the only layering control.

Fix, four co-dependent parts (user-verified live: feet on top, opaque,
survives inclines/declines):

- TILT (mech.cpp UpdateShadowJoint): apply the SKL's own contract for
  jointshadow ("apply terrain angle to pitch and roll") -- quad up aligned to
  the surface normal via an orthonormal basis fed to the ENGINE's own
  LinearMatrix->EulerAngles conversion.  Hand-derived Euler signs are exactly
  how the prior tilt attempts "dug into the hillside"; the engine conversion
  is convention-proof.  ~35 deg cliff-guard cap.
- SAMPLER (mech4.cpp): surface gradient from the collision probes PLUS
  BTVisualGroundLift per probe -- the quad hugs the VISIBLE terrain, whose
  lift varies across a slope; world->local rotation by the TRUE yaw from
  localToWorld, not the drift-prone gDriveHeading mirror (the task-#48 bug
  class).  BT_SHADOW_LOG traces the normals.
- DRAW ORDER (new PASS_SHADOW, l4d3d.h/L4VIDRND/L4VIDEO): shadows draw
  between the STATIC terrain and the DYNAMIC opaque bodies -- the classic
  decal order.  The mech, drawn after, z-passes over the shadow: the bias
  can never paint the shadow over the feet, structurally.
- BIAS pairing (L4D3D.cpp): tilt on (default) -> decal epsilon -0.0008;
  BT_SHADOW_TILT=0 flat fallback -> the old -0.004.  BT_SHADOW_BIAS overrides.

KB (locomotion.md): the four-part arrangement recorded as co-dependent, with
the gait-desync-symptom diagnostic hint retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:59:48 -05:00
arcattackandClaude Fable 5 0dfbb97dcc Fix torso-pitch aim ray: gyro-stabilized boresight from the mech torso
The aim/pick ray was the active RENDER EYE's view direction, which made a
mech unable to aim at a distant enemy:
  (a) it inherited the mech BODY pitch -- on a slope the body tilts ~8 deg to
      conform to terrain, so the boresight pointed that far DOWN and the pick
      ray hit the ground ~50-100m short of the target (groundPicks, never
      mechPicks); and
  (b) in CHASE view (the default) it was the elevated behind-the-shoulder
      camera, so the ray started over the mech's shoulder and flew high.

Fix (L4VIDRND DPLEyeRenderable::Execute, the BTSetAimCamera publish):
- LEVEL the boresight: drop the view-direction pitch and rebuild an upright
  basis (world +Y up) -- the guns fire along the mech's gyro-stabilized
  HORIZONTAL heading, not the terrain-pitched body.  Reticle X still carries
  the torso twist (BTTwistToReticleX); reticle Y any elevation.
- ANCHOR the ray origin at the mech's TORSO (myEntity->localToWorld
  translation + ~5) instead of the render eye, so it is correct in BOTH the
  chase (default) and cockpit views.

Verified end-to-end: solo, enemy 120m ahead, BOTH chase and cockpit view ->
[target] "MECH under boresight ... mechPicks=59 groundPicks=0" -> beam ->
*** DESTROYED.  Also proved PickRayHit resolves a REPLICANT when aimed at it
(hit=1 at 757m, 2-node run).

mech4: BT_GOTO="enemy" test aid now chases the nearest peer of ANY instance
(solo dummy OR replicant), not just replicants.

Still open (locomotion, not aim/MP): BT_GOTO/BT_AUTODRIVE stall short of the
enemy, so an AUTOMATED 2-node kill via the real beam isn't demonstrated yet
(the FORCE_DMG hook still is).  A human can now aim correctly in the default
view.  Solo un-regressed; build clean; KB validator clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:11:54 -05:00
arcattackandClaude Fable 5 04a2049ce2 MP task #47: DISPROVE the Entity-layout root cause; correct the record
The prior commit (c78662a) filed cross-pod damage failure under a
"cross-TU Entity/EntityID layout divergence" (and the P5 base-region
audit). A compile-time offsetof probe injected into BOTH translation
units disproves that: game/reconstructed AND engine/MUNGA compute an
IDENTICAL Entity layout --

  sizeof(Entity)=444  offsetof(entityID)=380 (0x17C)
  offsetof(ownerID)=388  offsetof(simulationFlags)=32
  offsetof(Mech,entityID)=380  (Entity subobject at 0)

So Entity::Dispatch reads this->entityID at the SAME offset mech4's
GetEntityID() does -- there is no per-TU read difference, and the
logged 3:22 (mech4 candidate) vs 3:19 (engine Dispatch) cannot be one
object read two ways. Those lines were different objects/messages,
mis-correlated. The distinct, REAL P5 "base-region layout divergence"
(HARD_PROBLEMS.md) is about raw-offset stomps at this+0x2d4..0x2f0 --
far above entityID -- and is unrelated to #47.

Changes:
- Revert the committed BT_MP_NET engine diagnostics (ENTITY/EVENT/
  NTTMGR/RECEIVER/L4NET) to the clean a9c3e96 baseline -- those are
  the very instruments that produced the mis-correlated data.
- mech4 BT_MP_FORCE_DMG hook: dispatch via the real m->Dispatch(&td)
  path (no false id stamping); comment records the offsetof finding.
- context/multiplayer.md: layout-divergence RULED OUT; 3:22/3:19 marked
  unconfirmed; leading hypotheses reframed as H2 (wire host-relative
  (de)serialization) vs H3 (replicant id != master's registered key),
  to be distinguished by a 2-node run with per-message correlation.

Solo un-regressed (mech walks + targets, 0 faults). Cross-pod delivery
remains open, but the investigation is redirected off the wrong (large,
structural) layout-audit path onto the EntityID wire/id-assignment path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 12:09:50 -05:00
arcattackandClaude Fable 5 c78662a0f1 MP cross-pod damage ROOT-CAUSED: EntityID localID corrupted by hostID on the wire (task #47)
Traced the dispatched-message delivery end to end with BT_MP_NET /
BT_MP_FORCE_DMG. Everything works except one wire step:
- A's Entity::Dispatch reroutes the replicant's TakeDamage
  (application->SendMessage(ownerID=3, EntityManager, msg)); POST-Dispatch
  the message carries entityID=3:22 (host:local) -- the replicant's own
  ID, matching B's master (GetEntityID()==3:22). VERIFIED sent.
- On B the message ARRIVES, GetEntityPointer finds an entity, Posts it,
  the event drains (ProcessEventTask = ProcessOneEvent(0)), Event::Process
  runs, Receive finds+calls a handler. VERIFIED the full deliver chain.
- BUG: B receives entityID=3:19, NOT 3:22 -- the localID dropped by
  exactly the hostID (3) between A's send and B's receive. So
  GetEntityPointer(3:19) returns the WRONG entity (classID 48, not the
  mech 0xBB9), whose base handler ignores the unaimed hit -> 0 damage.
  Auto-replicated UPDATE records (msgID 18) arrive with the correct 3:22
  and find the mech, so the corruption is specific to the dispatched-
  message wire path/direction.

Next: the host-relative EntityID (de)serialization on the dispatched-msg
path (RoutePacket / packet EntityID encoding) -- diff vs the update path
which translates correctly. Diagnostics retained (all BT_MP_NET-gated,
off in solo -- verified: solo 22 hits, 0 probe noise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:03:32 -05:00
arcattackandClaude Fable 5 fa88f74c68 HUD aspect correction: square reticle units at any window shape (task #44)
User: "it doesn't scale with our screen resolution?" -- correct. The
HUD draws into the fixed 800x600 backbuffer and the present stretches
it into the client area: on non-4:3 windows everything distorted
(circles -> ellipses, the bottom tape widened vs the ladder, positions
drifted outward).

- BTGetPresentAspect (L4VIDEO, from gWindowAspect; 4:3 fallback).
- dpl2d x-unit = (bbW/2)/presentAspect (== bbH/2 at 4:3 -- unchanged
  there), circles pre-squished in bb space so the stretch restores
  round.
- The reticle<->NDC conversions (BTGetAimRay / BTProjectToReticle /
  BTProjectHotBox / BTTwistToReticleX) use the PRESENT aspect; the
  mouse map (BTClientToReticle) is now pure client-relative.
- Placement itself is authentic and stays put: the binary clusters the
  instruments around the boresight (ladder x=0.35 half-heights, spans
  +-0.25) -- NOT at the display edges.
- Crash en route: the BTGetPresentAspect extern declared INSIDE dpl2d's
  anonymous namespace = a different, unresolved symbol -> /FORCE
  garbage call (landed in MissileLauncher::DefaultData). Moved to
  global scope; gotcha noted in the comment.

Verified 4:3 regression: aimed lock + zone hits + no crash; hud-geom
MapX/MapY unchanged at 4:3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:14:36 -05:00
arcattackandClaude Fable 5 a8a56c57c9 Targeting correction: reticle = TORSO BORESIGHT, not a free mouse cursor (task #39)
Colleague flagged that targeting is torso-locked with no fine cursor.
Correct: the pod stick's yaw drove the TORSO TWIST
(MechControlsMapper -> HUD::SetFreeAimSlew(stick_x) @cockpit+0x28C,
gated on torso-horizontal-enabled; hud.hpp:167) and reticlePosition
@HUD+0x1FC is COMPUTED by HudSimulation from the mech pose quaternion +
target geometry, zeroed to centre with no target (part_013.c:5680) --
never a free-floating cursor. The engine Reticle struct is general
(shared with Red Planet); BT drives it from the torso boresight.

Fix:
- Removed the mouse-cursor slew (a mis-sourced stand-in from the
  RP-shared struct). The crosshair is now the torso boresight:
  BTTwistToReticleX(torsoTwist) = tan(twist) projected through the live
  per-axis projection. Dead-centre on the fixed-torso BLH
  (TorsoHorizontalEnabled=0); you aim by steering the whole mech.
- BT_AIM="x y" retained as the headless test harness.

Verified: face-to-face spawn -> HOT lock + aimed zone hits (36);
BT_FORCE_TURN circling -> 31 no-target vs 1 HOT (steering off-target
drops the lock, as it must). KB swept (combat-damage / gauges-hud /
open-questions); checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:29:42 -05:00
arcattackandClaude Fable 5 18d49491b7 Reticle Execute @004cdcf0 RECOVERED: every HUD instrument now live (task #37)
The one un-exported gap in the reticle chain, read via capstone
(tools/disas2.py; the annotated disasm preserved at
reference/decomp/reticle_execute_004cdcf0.disasm.txt). Draw() is now a
transcription of the real per-frame logic, and the HUD attr-table names
(hud.hpp ids 4/5/6/8/A/B/C/D) are CONFIRMED by their Execute usage:

- Range ladder: BAR from ladder-top to the caret + caret translate.
- The bottom 21-tick tape is the TORSO-TWIST indicator (NOT heading):
  deflection = -/+(span/2) x (RotationOfTorsoHorizontal / twist limit),
  attrs 4/5/6. Fixed-torso BLH reads centred -- authentic static.
- The circle-with-stem is the COMPASS (attr 0xD, rad->deg rotation) at
  (botX, botY - 3*tickMajor - 0.03), with the THREAT trail (attr 0xC)
  in its rotated frame: 0.05-unit attack-direction marks, fresh < 2s
  red, expiring at 6s. Port feed: player TakeDamage pushes the impact
  direction (dormant vs the passive test dummy).
- Pips (composed into subB6): hidden when destroyed (attr 1 == 1), LIT
  when the fire cycle is LOADED (attr 0x1c == 2; port source
  rechargeLevel >= 1) else the dark charging ring; filtered by the
  weapon-GROUP bits (weaponMode & elementMask&0xF). Range plays NO part
  (the stored TargetWithinRange slots are never read by Execute).
- Lock ring: subB9 at frame centre SPINNING 4 deg/frame while locked
  ([0x9d] is the spin matrix, not a heading list).
- Target HOTBOX: a rectangle hugging the projected extents (x+-4 around
  the top-centre hotbox point, +1/-11.5 vertical; baked K=2.8145 -- the
  port projects through the live per-axis projection), switching to the
  edge arrows past +-1.6 or behind (BTProjectHotBox, L4VIDEO).
- Reticle state Off/On + PrimaryHudOn full-HUD/simple-X switch, aim
  translate on slew move, 3D marker + PNAME player-name mesh identified
  (chain still deferred).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:38:50 -05:00
arcattackandClaude Fable 5 cf9f56044e Aim coordinates: route all conversions through the true NDC (fixes the
sideways lock offset)

The windowed present stretches the fixed 800x600 backbuffer into the
client area while the projection's aspect follows the CLIENT (resize
rebuild) -- so the dpl2d/reticle frame, the client frame and NDC only
coincide for a backbuffer-shaped window. The mouse mapping and the
square-frame ray shortcut (cx = rx*tanHalfFov) were exact at screen
centre and drifted outward with a side-flipped sign: aiming left of the
enemy locked as if right of it (user report).

- BTSetAimProjection now publishes BOTH proj scales (P11 carries the
  aspect) + the backbuffer size (the dpl2d frame). NOTE: GetViewport at
  the loop top holds the PREVIOUS frame's last pass viewport (gauge
  passes shrink it) -> use the renderer's own GetWidth/GetHeight.
- BTGetAimRay / BTProjectToReticle convert reticle <-> NDC per axis
  (ndc_x = rx * vh/vw; camera x = ndc_x/P11) -- exact for any window
  shape and FOV.
- New BTClientToReticle undoes the present stretch for the mouse
  (client px -> viewport px -> reticle coords); mech4 uses it instead
  of the raw client-height mapping.
- 1Hz aim telemetry in the [target] log (hits/noRay/noPick): verified
  100% pick hit rate once the projection is live (the pre-visibility
  mission-load seconds report noRay -- that was the "flicker").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:13:45 -05:00
arcattackandClaude Fable 5 d78bde066d Authentic target acquisition LIVE: reticle slew + pick-ray lock + aimed zone damage (task #36)
The engine Reticle model (MUNGA/RETICLE.h [T0]) reconstructed end to end:
- Mech::targetReticle is a real Reticle member bound to the TargetReticle
  attribute (0x1d), per the RP VTV analog (VTV.h targetReticle).
- Crosshair slew: mouse -> client rect -> reticle coords (the pod stick
  free-aim channel's dev-box stand-in); BT_AIM="x y" pins it headless.
  LMB fires lasers / RMB missiles (alongside SPACE/CTRL).
- Pick ray: the ACTIVE eye publishes pos + LookAtRH basis (BTSetAimCamera,
  L4VIDRND view-write site) + the render loop publishes proj._22;
  BTGetAimRay builds the world ray, Mech::PickRayHit slab-tests it against
  the collision template's ExtentBox via the engine's BoundingBox::HitBy
  (local frame; clips the Line at entry) -> world hull point.
- Designation: the mech under the crosshair designates (sticky; re-hover
  refreshes; cleared when the target leaves the roster at burial); the
  entity target slots 0x37c/0x388/0x38c feed the whole weapon path.
- Aimed fire: while HOT the impact point is the PICKED hull point -> the
  STEP-6 cylinder lookup resolves the zone under the crosshair (verified:
  center-aim -> head-band zone 13 dominant). Off-crosshair the sticky
  designation converges on center mass.
- HUD: the aim group draws at the slewed position ([0x9a] translate,
  contained by push/pop); the designator ring tracks the target's
  projected point (subB9 hot / subB8 designated, BTProjectToReticle);
  edge arrows when off-screen/behind.
- AUTHENTIC gating: no fire arc exists in the binary (FireWeapon fires
  whenever HasActiveTarget, part_013.c:7758) -> BT_FIRE_ARC is now an
  explicit OPT-IN presentation clamp; the hardwired gEnemyMech lock and
  the projectile path's gEnemyMech fallback are removed.
- Fixed en route: every renderable rebuild stomped mCamera back to the
  chase eye (start-inside silently lost the cockpit camera; the aim feed
  exposed it). BTL4VideoRenderer::mViewInside persists the chosen view.

Verified headless: BT_AIM="0 0" -> HOT lock, pick hits the hull face at
exact range, aimed zones resolve; BT_AIM="0.8 0.3" -> no lock, zero
damage, zero missile launches; kill chain completes to wreck + smoke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:57:11 -05:00
arcattackandClaude Fable 5 48b17750e5 HUD reticle + weapon pips LIVE: dpl2d 2D display-list port (task #35)
- dpl2d API fully recovered from the binary recorders (@487f34-488630):
  opcode model (points/lines/polyline/circle/color/width/matrix/push-pop),
  CallList = INLINE include (state persists to caller), centered coordinate
  frame (unit = half viewport height). game/reconstructed/dpl2d.cpp rework.
- BTReticleRenderable ctor @004cc40c transcribed with the authentic
  calibration (originX .35, originY .25, scaleY .5, 0..1200m right range
  ladder, bottom heading tape, FUN_004cd938 tick ladders, lock rings,
  turn arrows); range caret slides from the live target range fed by the
  mech4 targeting step (BTSetHudTargetRange).
- Weapon pips: the binary gate is IsDerivedFrom(0x511830 =
  MechWeapon::ClassDerivations) [T1: part_014.c:5386 hard-aborts on missing
  weapon attrs; part_012 counts + roster ORs capabilityFlags@+0x334] so ALL
  7 BLH weapons register (3 lasers + 2 PPCs + 2 MissileLaunchers). Pip A
  (lit, authored PipColor) on TargetWithinRange, else dark ring B.
- AddWeapon @004cdac0 store map corrected to the verified order
  (part_014.c:4827-4837); both state attrs are literally named
  "SimulationState" (strings @51d526/51d577) -> weapon simulationState.
- Mech roster this[0x1ef] renamed poweredSubsystems -> weaponRoster
  (0x511830 is MechWeapon, not PoweredSubsystem=0x50f4bc); derivation-tag
  table added to context/decomp-reference.md.
- Draw hook BTDrawReticle after the 3D scene, cockpit view only. Binary
  Execute @004cdcf0 is an un-exported gap -> Draw dynamics [T3], tracked
  in context/open-questions.md with the blx_cop canopy + PNAME pip meshes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:05:44 -05:00
arcattackandClaude Fable 5 56f02b2176 Cockpit view live: V toggle, camera gate, real EyepointRotation, inside skeleton
- V toggles the authentic cockpit eyepoint <-> the chase camera.  Both eyes
  coexist; DPLEyeRenderable::Execute now writes the VIEW only when it IS
  mCamera (unconditional writes let the last-executed eye stomp the toggle).
- EyepointRotation is a real zeroed EulerAngles member on Mech (was bound to
  the shared junk attrPad -- the eye composes this attribute into the view
  EVERY frame, so the cockpit camera was rotated by garbage: the canted
  horizon, then the black screen).  The eye-slew systems write it later.
- The cockpit eye mounts at the eyepoint's REST position with a clean upright
  forward basis on the tree root (the live joint chain fed it the site tilt +
  torso pose; authentic pitch/yaw is the deferred gyro eye chain).
- The inside view swaps the player to the INSIDE skeleton mesh set
  (SkeletonType_A): 19 body segments hide, exactly one mesh remains --
  blx_cop.bgf, the authentic cockpit canopy shell around the eyepoint.  It
  currently renders as a black enclosure (the black-screen report), so it is
  HIDDEN pending its interior/punch-material rendering (BT_INSIDE_COCKPIT=1
  shows it for that work).  Damage gstates respected in both directions;
  RemakeEntity keys off the DISPLAYED skeleton.
- Dev: BT_START_INSIDE=1 starts in the cockpit view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 19:23:07 -05:00
arcattackandClaude Fable 5 31626a3b97 Beams at the models' NATURAL width, per-weapon tube models (de-cartooned)
The "cartoonish" beams: the port drew the ermlaser tube INFLATED -- a 3.0x
glow + 0.9x core two-layer hack, 13x the authored radius.  Parsed the beam
models: ERMLASER/MLASER 0.22u radius, SLASER 0.11, LLASER 0.32, PPC 0.62 --
thin pencil beams with an authored per-class size progression; the PPC bolt
is genuinely ~3x fatter with its own 62-vert shape.

Now: ONE draw per beam at natural model scale (width multiplier 1.0), using
the weapon's OWN tube model (ermlaser for lasers, ppc.bgf for PPCs; the
s/m/llaser tubes load for other mechs' loadouts).  BTPushBeamKind carries the
model selector; BTPushBeam keeps its signature (kind 0).  Tint = 40+215x the
authored PipColor; the thin natural tubes stay under additive saturation so
the scrolling grit reads without a hand-dimmed core layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 18:33:20 -05:00
arcattackandClaude Fable 5 9b5004c39d Missiles: authentic dsrm smoke trail + torso aim (placeholder tracer removed)
The "white lines toward the feet" report: the LAUNCHES are authentic (the
mech's real MissileLauncher/ProjectileWeapon roster subsystems firing on
their own recharge cadence -- that's the every-Nth-shot rhythm), but the
visual was the bring-up 3-segment beam tracer, and the aim point was the
victim's ORIGIN (ground level between the feet).

- Trail: per-frame puffs of the AUTHENTIC dsrm effect (psfx 0, "the lrm
  smoke trail") along the flight path, frame-oriented so the smoke streams
  behind the round (its .PFX velocities are +Z = backward); the round itself
  is a short hot streak.  New BTPfxTrailPuff spawns slot particles directly
  at a moving point (no emitter instance).
- Aim: the resolved target point lifts to torso height (the beams already
  converged there) -- impacts now cyl-resolve to body zones instead of
  diving at the feet/legs.

Follow-up noted: the round MESHES (BULLET/LRMS.BGF ship) for a true
model-per-projectile look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 16:15:03 -05:00
arcattackandClaude Fable 5 5236d4263a Wreck sink (quadratic burial) + ldbr debris field -- the 1996 script completed
Answers "does the wreck fade away?": YES, by sinking.  FUN_00456410 (the 1996
sink renderable) computes offsetY = rate * t^2; the hulk's authored rate is
-0.025 -> the ~7-unit hulk is fully underground ~17s after the kill.  The
script also pairs the standing hulk with the LDBR strewn-debris field (12x13u
flat scatter), parented together and sinking together.

Also verified from the mesh data: BLHDBR (1537 verts -- more than the intact
torso) IS the authored Blackhawk wreck: the classic standing-leg-in-rubble
sculpt.  The "just a leg standing there" report is the authentic art.
(THRDBR -- the mesh the 1996 script hardcoded -- parses to ZERO vertices;
more evidence the hardcode was an unfinished dev shortcut.)

Implementation: SwapToWreck adds the ldbr piece; TickWreck applies the
quadratic sink per frame (driven from the dead mech's UpdateDeathState),
hides both pieces at burial and reports it so the wreck-smoke re-arm stops
with the wreck.  DPLStaticChildRenderable::SetOffsetTranslation added
(Execute re-reads OrientationMatrix per frame -- same in-place idiom as
SetDrawObj).

Lifecycle verified live: kill -> 'blhdbr.bgf' + ldbr debris -> smoke re-arm
@10s -> wreck buried @~17s -> smoke stops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:55:01 -05:00
arcattackandClaude Fable 5 c0fa6bf21a Death = the burning WRECK-HULK swap (effect 104) -- the authentic chain, reconstructed
The user was right: a dead mech turns into a pile of wreckage.  The authentic
1995 chain, recovered end-to-end:

  kill -> the victim's per-mech DEATH ModelList ('blhdead'/'lokdead'/'owndead'/
  'thrdead', .RES ids 22-25) -> its authored effects: 104 (the WRECK script) +
  1007 (dnboom big explosion) + 1001 (ddthsmk rubble smoke plume) + a damage-
  smoke burst (3/4/5/15).

Effect 104 = ExplosionScripts case 4 (part_008.c:2663, LIVE in the 1996 binary;
the "disabled" warning is case 6): loads the destroyed hulk + flamesml/flamebig
flame meshes with sweep flicker.  Every mech ships its hulk (BLHDBR/MADDBR/
LOKDBR/... + GENDBR generic); the 1996 script hardcoded thrdbr.bgf (dev
shortcut) -- we use the victim's own.

Reconstructed as BTL4VideoRenderer::SwapToWreck: hide every segment mesh, hang
"<prefix>dbr.bgf" on the tree root (pending-swap if death precedes tree build);
routed from the engine's ExplosionClassID dispatch (effect 104 ->
BTSwapMechToWreck(explosion->GetEntityHit())).  The kill now fires the
authentic 'blhdead' resource (manual 7+1 pfx calls removed -- the list carries
1007/1001 itself).  Verified live:
  [death] firing authentic death list 'blhdead' id=22
     ** effect_number = 104
  [BTrender] wreck swap: victim -> 'blhdbr.bgf'

Follow-ups noted: mesh flames + hulk settle (cosmetic), DeathSplash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:43:08 -05:00
arcattackandClaude Fable 5 b44c0f98f6 PFX continuous-rate emission (data-verified) + persistent wreck smoke
The one-burst emitter batching was wrong: in EVERY shipped .PFX,
maxIssue/rate == release_period (DAFC 25/100~=0.2, DNBOOM 35/150~=0.2, DDAM2
35/16~=2, DDTHSMK 30/3=10) -- release_period is the EMISSION WINDOW and rate
is particles/second emitted continuously across it.  The death smoke plume is
authored as a 3/sec trickle for 10 seconds (+6s particle life), not one
same-frame puff.  Emitters now integrate rate*dt until maximum_issue.

Wreck smoke (port addition, [T3]): a destroyed mech re-arms the death/rubble
smoke plume (psfx 1, DDTHSMK) every 10s -- its own authored window -- so the
dead hulk visibly keeps smoking instead of standing there looking alive (the
freeze-death is invisible on an already-stationary target).  New member
Mech::wreckSmokeTimer; UpdateDeathState takes dt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:13:10 -05:00
arcattackandClaude Fable 5 a3d67cc639 Combat visible + killable: Wword root-cause fix, .PFX effect layer, RemakeEntity swap
The 'can't kill the enemy / no visible damage' cluster, root-caused and fixed
faithfully:

- STEP-6 unaimed path was INERT: the cylinder table was 'cached' at Wword(0x111)
  -- the recon ABSORBER bank (stores nothing, reads 0) -- so every unaimed hit
  silently no-op'd.  Promoted to the named member Mech::damageLookupTable
  (binary this[0x111], was mislabeled ammoExpended).  New gotcha class recorded
  (reconstruction-gotchas §2) + sweep; 2 dead multiplayer branches logged.

- Fire path migrated off the stale vital-zone aim onto the completed STEP-6
  unaimed dispatch (zone=-1 + beam entry point -> cylinder resolves the
  exterior zone).  No more invisible 1-shot kills; death via the authentic
  cascade (~14 center-mass hits).  Wreck stays TARGETED on kill (beams stop on
  it); scoring latches off.

- SendSubsystemDamage AV fixed: unbound critical-subsystem plug guard (43
  unbound plugs/mech logged as an open question -- the binding itself is a gap).

- RemakeEntity (render damage swap): the 1996 render state machine's missing
  Remake state, reconstructed as an in-place SetDrawObj mesh swap keyed by each
  segment's damage-zone graphic state (tree dtor doesn't cascade -> never
  rebuild).  Destroyed arms/guns visibly wreck (the only variants the RES
  registers).

- BT .PFX particle layer (L4VIDEO.cpp): the 1995 explosion/damage effect layer,
  unported since 2007 (DPLIndependantEffect/ReadPSFX/ExplosionScripts all
  stubs).  Parses the authentic VIDEO/*.PFX definitions via the [pfx_day]
  psfxN mapping; premultiplied blending renders BOTH families from the same
  data (additive-style fire + occluding smoke -- DDAM2 is 30% grey, DDTHSMK
  ramps negative: impossible additively); depth-sorted billboards with a
  radial-masked grit sprite; impact-frame orientation (.PFX offsets are
  authored mech-local, -Z = out of the struck armor toward the shooter) for
  weapon hits AND damage bands (via lastInflictingID, now maintained -- was
  declared but never written).  Both effect-number encodings route (raw dpl
  <100 + WinTesla 1000+slot carried by the band resources).  Death fires the
  authentic dnboom (7) + ddthsmk smoke plume (1).

- Effects anchor at the impact point / damaged zone's segment, not the mech
  origin (no more fire at the feet).

- Dev force-input gates BT_AUTOFIRE / BT_AUTODRIVE for headless fire-chain
  verification; BT_PFX_ADD=1 flips the particle blend for A/B.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 14:43:32 -05:00
arcattackandClaude Opus 4.8 4a4ec6855c gauge-complete P4e: SectorDisplay reconstructed + registered -> radar SECTOR X/Z read-out LIVE
The "sectorDisplay" cockpit primitive (Secondary overlay, the radar SECTOR X/Z
coordinate read-out) was PROSE-ONLY in btl4gau3 (placeholder Make, no ctor/
methodDescription/registration) -> the config line was parse-SKIPPED and never built.

Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c,
methodDescription PE-parse): SectorDisplay : GraphicGauge, sizeof 0xC4. Its Execute
reads the linked mech's world position and shows two 100-unit sector numerics:
  numericA = Round(-localOrigin.z * 0.01) + 500
  numericB = Round( localOrigin.x * 0.01) + 500
(rounding = round-to-nearest == FUN_004dcd94, corrected from the reviewer's wrong
"truncate" claim; 0.01 const PE-verified; -Z/+X axis + fchs confirmed from asm).
Overridden slots: LinkToEntity(9) caches the subject, BecameActive(3, non-inactivating),
Execute(16) -> satisfies the container-Execute rule. Layout overflow-locked
(static_assert sizeof<=0xC4). Make/ctor/dtor mirror the registered PilotList sibling;
the config image name is copied (nameCopy) since Execute reads it per-frame.
Registered in BTL4MethodDescription[].

VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 pos=(125,579) image=helv15.pcc gridCached=1;
Execute -Z=960.4 X=361.6 -> sectorA=510 sectorB=504 (Round(9.6)+500=510, Round(3.6)+500=504
-- authentic 100-unit sectors from live mech position). Gauge composite renders full,
no crash. The skip list is now exactly the two remaining widgets (prepEngr x12, messageBoard).

Also: a permanent BT_GAUGE_SKIP_LOG diagnostic (GAUGREND.cpp, gated) that logs each
unregistered gauge primitive the dev-parse skips -- the tool that pinned this down
(earlier "not built" runs were killed before the lazy gauge-renderer init).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:25:48 -05:00
arcattackandClaude Opus 4.8 070af409f7 gauge wave P1a: publish heat/power/weapon attributes + radar zoom
The gauge-databinding-map workflow found most cockpit gauges resolve NULL
because the reconstructed subsystems publish only a fraction of the attributes
the config binds.  First publishing batch (attribute tables are read-only static
data; ids kept a dense prefix from each parent's NextAttributeID):

- HeatSink table dense-append: DegradationTemperature/FailureTemperature (the
  condenser temp-bar warn/max endpoints -- were NULL, so the two-part bars could
  not scale), NormalizedPressure/DegradationPressure/CoolantMassLeakRate, and the
  HeatSink link.  Condenser/Reservoir inherit this -> all 6 condenser temp bars
  now resolve current/warn/max (verified: BT_GAUGE_ATTR_LOG all OK).
- PoweredSubsystem::GetAttributeIndex() (new) publishes InputVoltage->voltageSource
  -- the cluster power-branch gate (the power-lamp/generator-voltage/state-lamp
  sub-branch is skipped when it resolves NULL).  Flows to Sensor/Myomers/weapons.
- MechWeapon::GetAttributeIndex() (new) publishes OutputVoltage/PercentDone->
  rechargeLevel; Emitter/PPC/ProjectileWeapon/MissileLauncher/GaussRifle DefaultData
  re-pointed at it (they carried an EMPTY default-constructed index -> resolved
  NOTHING).  Verified: the ER MED LASER / PPC / STREAK weapon clusters now render
  live recharge dials (were blank TEMP/STATUS).
- Mech::SetTargetRange un-stubbed (radarRange = range) -> the radar map scale +
  overlay range readout track the mapper's zoom (was frozen at 1000).
- GAUGREND ParseAttribute: env-gated per-binding resolution trace (BT_GAUGE_ATTR_LOG)
  -- durable diagnostic infra for the wave.

Verified DBASE+dev gauges: no startup/gauge-construction crash (dense chain intact),
combat un-regressed (TARGET DESTROYED), clusters build with InputVoltage resolving.
Remaining config-binding NULLs: HeatSink/AmbientTemperature (aggregate bank, P3) +
Searchlight/LightOn (P1b).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:22:45 -05:00
arcattackandClaude Opus 4.8 0bceb9a638 gauges/map: populate the radar entity grid + draw mech contact blips
Root-caused why the radar showed 0 contacts: the gauge renderer's moving/static
entity grids (queried by GetMoving/StaticEntitiesWithinBounds) were NEVER
populated -- the 1995 game fed them from ExecuteImplementation's InterestingEntity
iterator, which the WinTesla port dropped.

Fix (engine): add GaugeRenderer::RebuildEntityGrid() -- Clear + repopulate
movingEntities from the world's DynamicMaster + DynamicReplicant entities (the
vehicles/mechs; AllEntity would flood the grid with ~300 props/effects/terrain).
Called from the map's Execute phase 0 (under the gauge's guarded Execute, so a
fault disables only the map).  Verified: contacts are now found, combat un-
regressed (TARGET DESTROYED), heap-clean under BT_HEAPCHECK.

Fix (map): DrawMoving now draws a cross blip for mech-class contacts (0xBB9) when
the authentic pip raster set is absent (the BT pip table is stubbed in this engine
build).  KEY PROJECTION FIX: worldToView is a camera (view->world) matrix and
Point3D::Multiply applies only rotation+scale (NOT translation), so projecting the
absolute entity position left the viewpoint un-subtracted and the blip landed
off-screen (verified: blip @(580,-1154)).  Project the RELATIVE position (delta =
entity - viewpoint, already computed for the range check) instead -> the blip
projects viewpoint-relative + heading-rotated correctly (@(164,221)).

radarRange default 500->1000 (1km zoom) so contacts within ~500m show on the
radar (SetTargetRange, the real player zoom, is still stubbed).  BT_MAP_LOG traces
the blip coords.

STATE: the grid population + viewpoint-relative blip projection are correct and
combat-safe.  A reliable on-screen contact demo still needs the display-scale vs
spawn-distance reconciliation (the BT_SPAWN_ENEMY dummy sits at a varying, large
distance -- 377-828m -- beyond the display radius across runs) + the authentic pip
symbol/name/video-object infrastructure (still stubbed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:07:19 -05:00
arcattackandClaude Opus 4.8 fb8c1d6ace gauges: colorMapperMultiArmor (worst-of-8-zones) + fix the interpreterTable overflow [widget recon 4]
Reconstructs colorMapperMultiArmor -- tints one schematic colour (a whole torso
section) by the WORST of up to 8 damage zones:
- MultiArmorConnection (@004c346c/@004c34f4): scans 8 zones via
  entity->damageZones[idx], keeps the max damageLevel, count==0 ? 100 :
  Round(worst*100).
- ColorMapperMultiArmor::Make/ctor (@004c3c48/@004c3d60): resolves 8 named
  zones to indices via Entity::GetDamageZoneIndex (13-param methodDescription).

ALSO fixes a latent HEAP CORRUPTION that registering this widget exposed (and
that would have blocked every further widget): GaugeInterpreter's bytecode
buffer is a fixed interpreterTableSize=46864 (GAUGREND.h) tuned for RP's config,
and its Insert() bounds guard is a Verify() that compiles out at DEBUG_LEVEL 0.
As each BT gauge widget is registered, more of BT's larger L4GAUGE.CFG resolves
into bytecode (vs being parse-skipped); colorMapperMultiArmor's 13 params x
hundreds of calls pushed the total past 46864 -> silent overflow past the char[]
-> heap smash detected later in mission bitmap loading (not the gauge code).
Fix: interpreterTableSize -> 262144 (sized for BT's full config).

Verified: parses, builds, all zones resolve, no /FORCE unresolved, combat
un-regressed (TARGET DESTROYED, 0 crashes), stable. Details: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:56:13 -05:00
arcattackandClaude Opus 4.8 b526e92cad gauges: all 6 cockpit MFD surfaces in a SEPARATE dev window (Milestone C)
Generalizes the single-'sec'-inset (Milestone B) into a full 6-surface
compositor presented in its own top-level window -- the default under
BT_DEV_GAUGES (BT_DEV_GAUGES_DOCK=1 docks it into the main window instead).
A 960x384 window tiles all six pod instrument screens: Heat (COOLANT/BALANCE/
RES + condenser gauges), Comm (KILLS/DEATHS/SELECT TARGET), Mfd1/2/3 (DISPLAY/
PROGRAM/NEAREST frames), and the color radar (SCALE grid + dials + ARMOR
DAMAGE). Main 800x600 3D view is un-occluded; default DEV un-regressed
(TARGET DESTROYED after 8 hits, 0 crashes).

Design (mapped by the mfd-multisurface-map workflow):
- All 6 surfaces are bit-plane MASKS over the ONE shared SVGA16/pixelBuffer
  (sec=palette low byte; Heat=0x4000 UL, Mfd2=0x0400 UC, Comm=0x8000 UR,
  Mfd1=0x0100 LL, Mfd3=0x1000 LR), so the compositor reaches the SVGA16 once
  and extracts each by mask. No port-name reconcile needed on the dev path --
  fetch the BT names directly; the RP aux* names only matter to the pod's own
  SVGA16::Update demux (a deferred pod-only fallback).
- SVGA16::DrawDevSurface: two kernels -- palette-LUT (sec) + mono bit-plane->
  tint ((word&mask)?tint:0). BTDrawGaugeSurfaces iterates a 6-entry table.
- Separate window = one CreateAdditionalSwapChain on the existing device (no
  2nd D3D device). BTGaugeWindowRenderAndPresent (after the main EndScene):
  SetRenderTarget -> SetDepthStencilSurface(NULL) -> Clear -> BeginScene ->
  6 tiles -> EndScene -> restore -> swap->Present.
- KEY BUG fixed: the main 800x600 depth surface stays bound when rendering to
  the 960x384 gauge backbuffer; a bound depth smaller than the RT is invalid
  -> all draws silently fail (clear color, no geometry). Unbind depth (Z is
  off), restore after.

Pod SVGA16::Update/BuildWindows path byte-unchanged; all gated BT_DEV_GAUGES.
Content is still the authored cockpit frames + base-table gauges -- the live
MFD widgets need the BTL4MethodDescription reconstruction (this window is now
the live viewer for that work). Details: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:43:31 -05:00
arcattackandClaude Opus 4.8 a256813395 gauges: secondary/radar MFD composites live on a dev box (Milestone B)
The pod's secondary/radar cockpit surface now renders as an inset in the
800x600 dev window under BT_DEV_GAUGES: radar/tactical grid, SPEED/HEADING/
MAGNETIC/PROP dials, and the color-coded ARMOR DAMAGE mech schematic -- real
gauge content (nzSec=27247), composited from the shared CPU pixelBuffer.

Composite pass (engine): SVGA16::DrawDevInset palette-expands the secondary
plane into a MANAGED texture on the MAIN device and draws an XYZRHW inset quad;
BTDrawGaugeInset() reaches the gauge renderer's 'sec' port and is called from
DPLRenderer::ExecuteImplementation as the last draw before EndScene.

Three reconstruction bugs fixed to get real content:
1. BTL4Application::MakeGaugeRenderer signature mismatch (REAL fix): the
   reconstructed no-arg override only HID the 2007-engine's widened 3-arg
   virtual MakeGaugeRenderer(int*,int*,int*), so the engine built a base
   L4GaugeRenderer whose ctor never parsed gauge/l4gauge.cfg -> empty symbol
   table -> "undefined label 'bhk1Init'" -> no ports/gauges. Matched the 3-arg
   signature so it truly overrides (args ignored; BT is fullscreen). Same bug
   class as the BTL4GaugeRenderer(false,NULL,NULL,NULL) ctor fix.
2. Parse hung on undefined primitives (gated dev accommodation): the BT gauge
   primitive table (BTL4MethodDescription) is still a stub, so an unknown
   primitive -> ReportParsingError -> Fail() -> a MODAL dialog freezing the
   parse. Under BT_DEV_GAUGES, skip the unknown primitive's params so labels
   register and the base "configure" primitive builds the ports.
3. Gauge widgets AV on NULL data bindings (gated): NumericDisplayScalar's NULL
   value_pointer -> GaugeConnectionDirectOf ctor deref (bind NULL->static zero);
   RankAndScore::Execute derefs unreconstructed game state (Gauge::GuardedExecute
   SEH wrapper -> Disable(True) on first fault).

All guards gated on BT_DEV_GAUGES: default DEV un-regressed (TARGET DESTROYED,
0 crashes) and the pod path is byte-unchanged (strict Fail, no guards).

Remaining (docs/GAUGE_COMPOSITE.md): the real gauge WIDGET reconstruction
(BTL4MethodDescription method table + gauge->game-state data bindings), then
Step 2 (RP<->BT MFD port-name reconcile) + Step 3 (2-window layout).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 10:59:45 -05:00
arcattackandClaude Opus 4.8 c13e72d0ba platform profile + gauge dev-composite (Milestone A: gauge renderer woken)
PLATFORM PROFILE (-platform pod|dev / BT_PLATFORM / run.cmd pod; default dev):
an env-preset selector, NOT a code fork -- it picks which environment the
existing video/gauge/input code reads.  dev = single 800x600 window + keyboard
(the working build); pod = RIO input + (on real hardware) the multi-surface
gauges/MFDs, mirroring content/SETENV.BAT (the pod path -- FindBestAdapterIndices
/ SVGA16 -- is left untouched).  The multi-surface needs the pod's 2 video cards,
so -platform pod does NOT auto-enable L4GAUGE on a dev box (stays bootable,
single-window).  Also fixes a real engine bug: SVGA16::BuildWindows
PostQuitMessage'd on a CreateDevice failure but fell through to a null-device
deref (segfault) -- now breaks (inert on the pod).

GAUGE DEV-COMPOSITE, Milestone A (option B; opt-in BT_DEV_GAUGES, default OFF):
wake the (dormant) gauge renderer so its CPU-rastered pixelBuffer can later be
composited into the dev window, WITHOUT touching the pod SVGA16 output path.
SVGA16 does no per-surface D3D in this mode (a DevGaugeComposite() gate forces
the no-surface path + short-circuits Update/Refresh).  Waking the never-exercised
gauge subsystem exposed 4 latent reconstruction bugs, each guarded:
SVGA16::Update / Refresh (empty mSurfaces[]), LBE4ControlsManager::MakeLinkedLamp
(NULL lamp manager), L4GaugeRenderer::NotifyOfNewInterestingEntity (garbage
warehouse chain).  It now boots STABLY; the gauge reconstruction is incomplete
(shadowed lamp-manager + warehouse members) -- Milestone B (the composite pass)
will reveal whether the content is real.  See docs/GAUGE_COMPOSITE.md.

Verified: default DEV un-regressed (combat DESTROYED, 0 crashes); pod path
untouched; BT_DEV_GAUGES boots stable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 10:18:40 -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