25-hit MP autofire trace: kick -> damped oscillation -> settle, peaks well
inside clamps, no NaN/drift; measured Y:X frequency ratio 7.75x matches
sqrt(springK.y/springK.x) exactly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Community reports said the camera looked too far back and the cockpit frames
were never this revealed; the user's hypothesis (our projection widens the
view) was exactly right. The original Tesla projection call, preserved in a
comment at DPLRenderer::SetView, is
dpl_SetViewProjection(view, -1, -aspect, +1, +aspect, 1/tan(viewangle/2))
i.e. BTDPL.INI's viewangle=60.0 is the HORIZONTAL field of view with the
vertical derived by aspect: the pod's frustum was 60 x 46.8 deg at 4:3. The
port fed the 60 to D3DXMatrixPerspectiveFovRH as the VERTICAL fov -> 75 x 60
at 4:3 (worse on widescreen): the world rendered ~25% farther away and far
more canopy was visible than the pod ever showed.
Fix: BTFovYFromHorizontal(viewangle, aspect) at all five projection sites
(main, per-frame resize, sky pass, WM_SIZE rebuild, fallback) -- the
horizontal field stays the authentic 60 deg at any window shape. The aim
boresight reads the live projection (gBTAimP22) and adapts; combat
re-verified (kill chain, no NaN). Cockpits now show the frames at the
pod's framing: viewports dominant, frames at the periphery.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The user caught the inversion (blocked viewports, see-through frames); the
answer came from the rasterizer board itself: an i860 disassembly of the
VREND.MNG firmware decoded the damageize handler (@0xf040f6f8) -- cmd 0x20
(vr_damage_action) writes the dpl_Punchize token triple onto the punch
geogroup's first three geometry chunks IN FILE ORDER:
chunk1 = the detailed window-aperture shapes -> an invisible per-pixel MASK
chunk2 = the coarse hull -> the ONLY colour-drawn geometry (where not masked)
chunk3 = byte-identical hull twin -> the damage-reset record, never visible
Visible canopy = hull-minus-apertures. Both earlier readings were wrong ways
around the same three chunks: dropping all punch left floating fragments (no
hull); hiding the duplicate pair hid the HULL and drew the MASK -- the exact
inversion observed.
Port: bgfload tags per punch patch {non-paired pmesh = mask (role 1),
first twin = hull (role 2), second twin = skip}; L4D3D DrawMesh runs
mask->stencil-1 (no colour/z-write, z-tested), hull->stencil-NOTEQUAL,
mask->stencil-0; device D24X8 -> D24S8 + stencil clear. The cut never
touches z, so later-drawn entities show through the apertures.
BT_COP_PLATES=1 disables (diag).
In-game, all 8 mechs on pure defaults: connected dark frames with clear
viewports -- thor (the footage mech) shows the central viewport + surrounding
frame exactly as filmed. Combat regression clean (kill + no NaN).
Arena textured punch uses the same firmware kit; the black-texel alpha path
there is a working approximation, flagged in the KB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
or a RANDOM horizontal fallback when ~zero (per-component sign roll then
value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
-> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
.{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
vibrationDirection@0x390) and verticalImpulse(yaw, dir)
Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.
Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor
DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.
Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.
Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):
- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
everything after was mis-offset, and clamps/accumulators were 0xCD fill --
every field now initialised per the binary; 33 offsets static_assert-locked,
sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
(GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
0xCDCDCDCD gated the writers off silently
Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).
Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
My earlier 'not a shipped asset' verdict was wrong. The effect is POVTranslocateRenderable
(engine) / BTTranslocationRenderable (our port), which loads tsphere.bgf BY FILENAME (not via
the RES table, so every resource-name search missed it) and runs a collapse-on-death /
expand-on-respawn sphere keyed on the SimulationState dial. Wiring already exists in
btl4vid.cpp; the renderable is a no-op stub in btstubs.cpp. That's why it never shows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Searched the full pod install (nick-games/tesla4pod_extracted REL410/BT;
BTL4.RES byte-identical to ours) and the whole nick-games archive: no
whirl/warp/tunnel/vortex asset by filename or RES resource name, no such
psfx in BTDPL.INI. The decomp drop path (DropZoneReply FUN_004bffd0) fires
no effect — only Mech::Reset + the level-2 "translocated" cockpit alarm.
BTPLAYER source is absent from all archives (binary-only). RP411 analog
uses InitialStasisFlag (sim gate, no visual). Verdict: the pod build's
respawn is mech-redrop + translocated alarm; the "blue whirlwind" is not
a shipped REL410 asset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
User report from 2-window play: after dying, the pilot could still steer the
wreck around invisibly. Zero throttle/turn/fire and the forced harnesses at
the gBTDrive source when IsMechDestroyed() -- deliberately INPUT-only, so the
gait/animation machinery keeps running and the death collapse plays out.
Also recorded in the KB: the killer's screen shows the wreck SWAP but not the
death SINK (needs the Mech-level death/knockdown update records, disasm
preserved from task #51), and the respawn-cycle scouting (type-12 intro
scripts are binary ref-records; adrop; DropZone + Mech::Reset machinery) for
the recovery-whirlwind reconstruction (task #52).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: lasers only visible on the window firing them. The peer's
replicant emitters never learned the master fired.
THE AUTHENTIC PIPE (decomp-verified):
- FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call)
does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c)
on the app+0x34 manager -- our per-weapon render walk already plays that
role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's
updateModel / ForceUpdate().
- Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message:
the roster walk already hands the entity's stream to every subsystem's
PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested
updateModel bit; Entity::UpdateMessageHandler routes received records by
subsystemID to the subsystem's ReadUpdateRecord. All engine machinery --
the missing pieces were the Emitter's serialize/apply pair + the triggers.
CORRECTIONS to the dormant task-33-era transcriptions (never exercised --
nothing ever set updateModel -- so the latent misreads never surfaced):
- The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord,
7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry +
body semantics; @004ba568 resolves an EntityID at rec+0x30 through the
entity index -- record semantics, not Damage). Renamed across MechWeapon /
Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is
undecoded (inherits MechWeapon for now).
- Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not
recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour --
the old `CopyColor(targetEntity+0x184)` was also a databinding trap.
- OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing
UpdateRecord typedef as the param type, silently NOT overriding the engine
virtual (the base ran instead; nothing would ever have serialized).
Base-typed params (Simulation__UpdateRecord*), casts inside.
- Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve
(drop unknown non-null targets), MechWeapon alarm apply chain, beam fields.
- ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one
final record at beam end (turns the peer's beam off).
- Mech::DrawWeaponBeams extracted from the player-only drive block so the walk
runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics
would have served the player's segment pointers as the replicant's muzzles).
VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all
225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun
ports). Solo un-regressed (150 beams, kill chain, no crash).
Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery
(reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record
types decoded (pose/alarm/leg-state+heat with the body-channel write-through
re-sync, knockdown, death, impact, movementMode) -- transcription deferred;
it replicates remote knockdown/death/heat and was not needed for beams.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report from the first live 2-window session: throttle worked, A/D turn
did nothing. [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).
Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe). Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.
ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it. In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0. MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode. This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).
Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits). Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).
VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A peer's replicant mech slid around as a frozen statue: DeadReckon moved
localOrigin but nothing animated the legs. Reconstruct the replicated-motion
display loop: after DeadReckon, derive a SIGNED speed demand from the
replicated velocity (the master publishes worldLinearVelocity in every update
record, Mover::WriteUpdateRecord MOVER.cpp:759; the replicant stores it in
updateVelocity.linearMotion :712; sign = forward dot against the -Z facing),
write it to controlsMapper->speedDemand (the LEG channel's live demand
source), and advance the LEG state machine for its JOINT writes only --
travel stays with DeadReckon, so position always follows the master and the
clip cadence matches the replicated speed.
Two replicant-only initialization holes found live (the master's equivalents
are primed per-frame in the PLAYER-only drive block):
- globalTimeScale/idleStrideScale read 0 -> clip advance time dt*0, the leg
machine engaged (state 11) but the clip froze (legCycle stuck 0);
- reverseSpeedMax2 (the run-cycle rise-clamp @0x7a0; LoadLocomotionClips does
NOT set it) read debug-heap garbage -> legCycleSpeed clamped to -4.3e8 on
entering run state 13. Primed both; legCycleSpeed sane-banded once.
VERIFIED 2-node (BT_GOTO driver + BT_REPL_LOG observer): the replicant runs
the full gait lifecycle -- stand -> walk(5,7) -> run(10,12), legCycle 7.5->34
tracking the master's speed, wind-down(8), Standing(0) when the master stops.
Solo un-regressed (goto->aim->kill chain intact, no crash).
projectedVelocity source-swap (the binary publishes the BODY channel's
smoothed projection instead of the actual travel velocity) evaluated and
DEFERRED: it risks a verified working replication feed for a smoothing
nuance; recorded in multiplayer.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
The shadow report was a desync symptom, not a bias/tilt bug: the ~2-4 unit
depth-bias margin is tight enough that v4's out-of-phase leg pose showing
through could push the flat quad under a slope. No shadow code changed;
both symptoms cleared together. Noted as a diagnostic hint (check
[sync]/BT_SYNC_LOG before touching bias/tilt for this report class).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User-reported recurrence of "legs stutter and lose sync". Live [sync] showed
advSum/legSum drifting 5->92 during walk-up churn, then locking. Reproduced
headlessly (BT_FORCE_OSC throttle feathering + BT_FORCE_TURN + the new
per-frame BT_SYNC_LOG probe) and root-caused in two parts:
- SEED: the bring-up turn-in-place trigger arms only the LEG channel, so the
walk re-entry lands one frame apart -> a persistent clip phase offset.
- AMPLIFIER: with offset clips the two end-of-clip callbacks fire on different
frames; a demand change landing between them picks DIFFERENT next states
(one channel winds down, the other keeps walking) -> opposite-phase churn
(observed bs=6 ls=7 -- body on walk-R, leg on walk-L).
Under v4 (display+travel = BODY) the out-of-phase LEG pose showed through on
every frame the body didn't write joints (Standing/wind-down) = the visible
stutter/pop.
DISASM GROUND TRUTH (neither Advance fn has a static decomp caller; found by
byte-scanning the CODE section for e8 calls):
- master perf (0x4a9b5c gap) -> AdvanceLegAnimation @0x4aa399 (air @0x4aa388)
-> -dist/dt into localVelocity (+0x1cc): the LEG drives LOCAL travel and
(writing last) the displayed pose;
- IntegrateMotion (0x4ab1c8, body advance @0x4ab312, caller FUN_004ab430 =
the projected-origin updater) -> -dist/dt into projectedVelocity (+0x2a0):
the BODY is the dead-reckoning PROJECTOR -- locally invisible.
v4's reading of FUN_004ab430 as the travel source was wrong.
FIX: advance body FIRST (projection; its writes get overwritten), leg LAST
(displayed pose); travel = legAdv under the two-channel split. Display ==
travel through the LEG by construction -- body-channel drift can no longer
become visible, structurally (not by input-symmetry luck).
Verified: trn+feathering repro clean; straight drive locked (adv==proj);
solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill
intact (241 hits, DESTROYED); no crashes. KB corrected (locomotion.md roles
+ P3_LOCOMOTION.md v5 entry). New gated instruments: BT_SYNC_LOG,
BT_FORCE_OSC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BT_GOTO beeline never actually closed on a target -- three separate faults:
1. It was gated behind `if(gBTDrive.forced)`, and `forced` was set ONLY by
BT_AUTODRIVE -- a chicken-and-egg where the block that would drive was gated
on the drive already being on. So BT_GOTO alone just turned in place (the
"stall"). Fix: BT_GOTO enables forced mode itself AND supplies its own
forward throttle (a beeline must drive, not only steer), cutting the throttle
inside a stop radius (weapon range for "enemy", so it holds + shoots).
2. Steering used the gDriveHeading SCALAR MIRROR (seeded to 0), but MP pilots
spawn facing a non-zero heading -> it steered the WRONG way. Fix: heading
error from the mech's ACTUAL forward (localToWorld's -Z axis), signed angle
to the target.
3. The goto turn was routed through controlsMapper->turnDemand, which zeroes out
in -net mode (the mapper key-bridge only shapes the local viewpoint mech
there) -- the heading froze and A drove straight past B. Fix: apply the goto
turn to the orientation integration DIRECTLY (if gBTGotoActive, turn=
gBTGotoTurn), after the mapper read.
Verified 2-node one-box: A with BT_GOTO="enemy" ALONE (no autodrive) drives
dist 1673->231m, holds at weapon range, mechPicks=59, autofire -> B hdlr=192,
DESTROYED -- a fully automated human-style cross-pod KILL via the real beam
path (not the FORCE_DMG hook). Solo un-regressed (plain autodrive still drives;
goto="enemy" resolves the enemy, mechPicks=59, no crash). BT_GOTO_LOG traces
the beeline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Re-measured interactive -net combat on a 2-node run. The KB's "aim ray dead /
drive dead in -net" was STALE: A's aim ray is live (noRay=0, the active eye
publishes the aim camera) and A drives (BT_AUTODRIVE/BT_GOTO move it). The real
blocker to a human cross-pod kill via the beam path is NOT the netcode:
- Boresight inherits the mech BODY pitch. The aim ray = the active eye's view
dir; on sloped terrain the body pitches to conform and the boresight points
~8 deg down ([EYE] up=(-.03,.99,-.14) == [pick] ldir.y=-0.145 every frame),
so at range the ray hits terrain ~50-100m short of the enemy (mechPicks=0).
- Drive/goto stalls ~700m short before closing to pick/weapon range.
- PickRayHit vs a replicant is thus unconfirmed, but the replicant localToWorld
IS correct (lstart magnitude == true A->B distance; the DeadReckon +
localToWorld=localOrigin block already tracks the replicated position).
Change: add a gated BT_GOTO="enemy" test mode (beeline toward the nearest live
replicant) -- MP spawn coords vary per run so a fixed "x z" can't reliably face
the peer. KB (multiplayer.md) updated with the corrected findings + the real
remaining work (torso-pitch/level-boresight aim ray + drive-to-range).
Solo un-regressed; btl4.exe builds; KB validator clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full cross-pod KILL now works: instance A (host 2) shoots B's mech (host 3)
dead over the network; B runs the death transition on its own screen. Verified
on a 2-node one-box run (BT_MP_FORCE_DMG on A): B's mech takes ~36 remote hits
of 12 and is DESTROYED, with wreck swap + explosion.
ROOT CAUSE (one bug): B's own MASTER mech was INVALID. Entity::Receive(Event*)
does `if(!IsValid()) event->Defer()` -- so a cross-pod-delivered TakeDamage that
arrived, resolved to B's real mech, and Posted, was DEFERRED forever; the handler
never ran. The reconstructed Mech::Make force-set ValidFlag only for replicants
(the P6 bring-up hack for the partial MakeReady handshake); a locally-created
master never got validated.
FIX (one line): Mech::Make now sets ValidFlag for the master too -- the
reconstructed ctor builds the mech synchronously, so it's safe. Nothing else
changed: targeting, the virtual victim->Dispatch, the replicant reroute, the
wire, receive, and id resolution were ALL already correct.
Two prior root causes were BOTH mis-diagnoses from the WRONG messageID:
Entity::TakeDamageMessageID is 18, not 21. Every earlier probe filtered
msgID==21, never matched a real TakeDamage, and mis-correlated the stray 21
hits' 3:19 / classID-48 against the real 3:22 hit -- inventing (a) a "cross-TU
Entity layout divergence" (disproven: offsetof identical in both TUs, sizeof
444, entityID@0x17C) and (b) a "localID corrupted on the wire". Both FALSE; the
id is correct end-to-end (3:22 = B's master).
- game/reconstructed/mech.cpp: validate master + replicant at Make.
- game/reconstructed/mech4.cpp: BT_MP_FORCE_DMG cleaned to a concise gated
2-node cross-pod reproducer (real virtual Dispatch path).
- context/multiplayer.md, reconstruction-gotchas.md: corrected root cause +
the validity-defers-cross-pod gotcha. Engine diagnostics reverted to clean.
Solo un-regressed (the solo enemy was already force-valid; the change only ADDS
ValidFlag). btl4.exe builds; full mission loop clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
One-box smoke test re-run on the current build: console egg -> mesh ->
RunningMission both instances, 174+ x144-byte update records each side,
2 mech trees per instance, 0 crashes (~6 min). The movement-replication
milestone is intact after all the targeting/weapon/death changes.
Gap map to a first playable LAN fight recorded in
context/multiplayer.md (wiring order): (1) generalize the world-pick
from gEnemyMech to all peer mechs; (2) exercise the Dispatch reroute
for cross-pod TakeDamage; (3) victim state visuals on the shooter's
screen (zone replication or a death event); (4) cross-pod beam visuals
via the AUTHENTIC beam-keepalive messages (FUN_0041c350, templates
@0x511e6c/78 -- already stubbed in emitter.cpp); (5) 2-window driving +
DEATHS scoring; respawn deferred to the P5 teardown debt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A period pod screenshot from players who know the game flagged the
overlay. Stroke-by-stroke re-read of the ctor (@4546-4587) against it:
- The range caret triangle is GREEN width 1 (ctor @4550-4551 sets
SetLineWidth(1)+SetColor(0,0.75,0) AFTER the yellow bar's CallList) --
my transcription left it yellow/2. Same for the bottom bowtie carets
(@4569-4570). Both fixed; the caret polylines also get the binary's
explicit 4th closing point (@4558).
- The screenshot otherwise CONFIRMS our layout (yellow range bar, green
caret, colored pip dots, tick tape + green bowtie, rotating compass,
mid-ladder range with no lock = the world-pick terrain range).
- Its crosshair is ~2.5x taller with arrowhead arms -- NOT our binary's
+-0.04..0.16 program: a different pod revision. Ours stays [T1] to
BTL4OPT.EXE 4.10.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Interim pod-like grouping ahead of the authentic ConfigureMappables/
ChooseButton mapper channels (decomp-anchored in open-questions):
- Three fire channels like three pod buttons: gBTWeaponTrigger (lasers,
key 1 or SPACE), gBTPPCTrigger (PPCs, key 2), gBTMissileTrigger
(missiles, key 3 or CTRL). Keyboard only per user (mouse buttons
removed).
- EmitterSimulation picks its channel by classID (PPC 3028 vs Emitter
3016); BT_AUTOFIRE holds all three.
- The BT_BEAM_LOG sampler modulus 30 aliased to ONE weapon under the
new synchronized 5-beam volleys (5 | 30) -> 31 (coprime).
Verified: all 5 energy weapons fire (31 samples each), aimed hits land,
kill completes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answering "do lasers and PPCs fire together?": in the pod, grouping was
a PILOT CHOICE. MechWeapon message handlers id 9 'ConfigureMappables'
(FUN_004b9550) and id 10 'ChooseButton' (FUN_004b95b8), handler table
@0x511860 (names read from the exe): ConfigureMappables binds the
weapon's fireImpulse@0x31C to controls-mapper fire-channel #N (roster
slot 0; vtable +0x38 register / +0x3c unregister / +0x44 choose),
ChooseButton = the in-cockpit re-binding. Weapons sharing a button fire
together. The port's SPACE=energy / CTRL=missiles split is a stand-in
default; the authentic per-button channels are a tracked follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[hud] pip diagnostic logs each registered pip's classID/PipPosition/
WeaponRange/PipExtendedRange/PipColor. The live dump corrects the
"everything is 500m" assumption: BLH lasers red @500 (x3, ext=1),
missiles amber @800 (x2), PPCs blue @900 (x2) -- the pips sit at their
weapons' max-range marks on the range ladder (caret below a pip = that
weapon reaches), so 7 weapons read as 3 weapon-system groups. KB +
comments corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User report: enemy takes hits, starts smoking, then beams pass through
-- no more damage, no death. Root cause: the death-effects dispatch
(blhdead explosion chain + wreck swap + KILL score) lived in the
SHOOTER's laser fire block kill check. A missile killing blow lands
frames later in BTUpdateProjectiles -- outside that block -- and after
task #41 the boresight pick skips a dead mech, so the fire block never
ran against it again: the death chain was silently skipped, leaving an
internally-dead, smoking, standing, invulnerable mech.
Fix: the dispatch moved to the VICTIM's own once-per-death transition
(UpdateDeathState's movementMode->9 moment) -- fires exactly once for
ANY kill source (laser, missile, collision). Killer id for the
Explosion message = lastInflictingID (the task-#31 bookkeeping). The
BT_ENABLE_TEARDOWN cdb harness was removed with the old block (findings
preserved in docs/HARD_PROBLEMS.md).
Verified headless (75s soak, missile-heavy kill): destroyed ->
'blhdead' id=22 -> wreck swap 'blhdbr.bgf' -> smoke re-arm -> sink ->
buried INERT; post-death picks fall through to terrain; 0 crashes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User video evidence (lasers firing at nothing) + the colleague's
torso-locked report + the binary all reconcile into one model:
The boresight pick hits the WORLD, and the target slot mech+0x388 holds
whatever entity is downrange:
- enemy mech under the boresight -> aimed target (hull point -> STEP-6
zone under the boresight, hotbox + lock ring, damage);
- else the TERRAIN downrange (BTGroundRayHit to 1200) -> the beam and
missiles fire at the scenery ("firing at nothing", as in the pod
videos), range caret reads the ground distance (authentic :5639), no
damage;
- else (sky) -> no target, and the weapon's own double 0x388 gate
refuses the discharge.
Binary evidence for non-mech targets: HudSimulation :5620 explicitly
handles a target WITHOUT damage zones (target->0x120 == 0) -- dead code
if only mechs were ever targeted. The pick is automatic every frame
(0x388: 11 reads / 0 direct stores in CODE).
Implementation: gBTTerrainEntity captured in MakeEntityRenderables
(Terrain::GetClassDerivations); mech4 pick = enemy PickRayHit else
ground ray -> terrain entity + ground point; damage/hotbox/lock only
for the mech pick; the task-40 enemy auto-converge REMOVED (facing away
now fires at the scenery, not magically at the enemy -- the user's
complaint). Verified all three states headless (BT_AIM 0,0 / .6,.5 /
0,-.8): aimed zone hits / terrain beams + 0 damage / no discharge.
Fourth (and evidence-complete) targeting model iteration; KB updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colleague: you fire without a lock, at "nothing." Double-checked the
binary:
- The weapon fire path is DOUBLY gated on mech+0x388 != 0:
EmitterSimulation (FUN_004baa88:7689) only calls FireWeapon with a
target, and FireWeapon (FUN_004bace8:7727) wraps its whole body --
including beamFlag(+0x46c)=1 -- in the same check. So no target => no
beam, literally (cannot fire into truly empty space). [T1]
- BUT a capstone scan of the entire CODE section finds 11 READS of
+0x388 and ZERO direct stores: the target is written INDIRECTLY (a
message/selector), i.e. AUTO-acquired -- there is no manual lock to
fire. The spinning-ring LOCK (HudSimulation 5619-5634) is a separate,
stricter state.
So the colleague is right that no lock is needed; my port was wrong to
gate firing on a pinpoint boresight-on-hull pick. Fix: mech+0x388 = the
enemy whenever it is ALIVE (auto-target); firing needs only that. A
pinpoint hull hit upgrades the shot to aimed-zone damage + the lock
ring; off-hull with the enemy present, the beam converges on centre
mass (body hits). Third correction from over-reading the RP-shared
Reticle struct (after sticky-lock and mouse-cursor).
Verified: BT_AIM="0.5 0.2" (off-hull) -> fires + lands damage (was 0
before); BT_AIM="0 0" (centred) -> HOT-aimed zone hits. KB swept;
checkctx CLEAN.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- LOCK rules transcribed from the exported HudSimulation
(part_013.c:5619-5634): lock requires a target AND your own HUD host
zone damage < 0.75 (_DAT_004b7ec4 -- a shot-up targeting computer
drops lock) AND the targeted zone damage < 1.0 (_DAT_004b7ec8;
whole-mech target checks zone 0 -- a wreck's dead zone can't
re-lock). Box and ring are now separate signals like the binary:
gBTHudLockState 1 = target held (hotbox draws), 2 = LOCKED (+ the
spinning ring).
- SIMPLE-X mode: the ctor's [0x99] minimal-reticle list transcribed
(@4689-4705: green +-0.02..0.08 cross on the aim translate); Draw
switches master <-> simple X on the PrimaryHudOn element bit (0x20),
completing the recovered Execute's state-list logic.
- Canopy diagnosis sharpened (open-questions): blx_cop is the torso
segment's inside-skeleton mesh enclosing the eye; the black box =
the skeleton render branch missing its texture + PUNCH cutout (the
canopy windows are punch texels). A rendering-branch task; stays
hidden by default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The sticky lock made aim irrelevant after the first acquisition (the
crosshair starts on the face-to-face enemy, so it locked at spawn and
never let go -- played as auto-targeting). The binary treats no-target
as a frequent live state (fire path re-checks 0x388!=0 at every step,
part_013.c:7689/7727; HUD range feed has a permanent no-target default,
:5636), so the target slots now mirror the reticle pick each frame:
locked while the crosshair is ON the mech, no lock otherwise. Keeping
the crosshair on the enemy is the gunnery.
Verified headless: BT_AIM="0 0" -> HOT + aimed zone hits; BT_AIM="0.6
0.4" -> no lock, zero damage.
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>
The engine Reticle struct (the mech's "TargetReticle" attribute) is the
acquisition chain: slewable screen-space crosshair -> pick-ray -> the
entity + damage zone under it become mech+0x388/0x38c, the intersection
point mech+0x37c. Convergence-on-lock is authentic (FireWeapon has no
aim test); targetWithinRange = dist < (1-damage) x weaponRange
(FUN_004b9bdc). Port acquisition is a bring-up stand-in (hardwired lock
+ BT_FIRE_ARC cone) -> tracked as the next combat-fidelity step.
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>