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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>