The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
16 KiB
Markdown
202 lines
16 KiB
Markdown
---
|
||
id: cockpit-view
|
||
title: "First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint"
|
||
status: living
|
||
source_sections: "task #55 (2026-07-10); reference/decomp part_007.c/part_013.c/part_014.c/part_011.c; engine/MUNGA_L4/bgfload.cpp, L4VIDRND.cpp; game/reconstructed/btl4vid.cpp"
|
||
related_topics: [rendering, bgf-format, gauges-hud, subsystems, decomp-reference]
|
||
key_terms: [material-ramp, siteeyepoint]
|
||
open_questions: ["gyro tail: gait jolt/rumble [T3 gates], sway model, torso-pitch eye writer, damageForce fill", "exact authored frame colour: the unlit constant (0.13,0.12,0.15) is T3 tuned-to-footage (BT_COP_FRAME overrides; the ramp-index approach + BT_COP_RAMP_L are RETIRED)"]
|
||
---
|
||
# First-Person Cockpit: the *_cop Canopy + the Authentic Eyepoint
|
||
|
||
The pod's only view was the cockpit eyepoint looking out through a per-mech canopy shell.
|
||
Task #55 reconstructed both halves — the CANOPY rendering and the EYE camera — from the binary.
|
||
Verified in-game on all 8 mechs (Thor vs gameplay footage). [T2]
|
||
|
||
## The canopy shell (*_COP.BGF)
|
||
|
||
- One per mech (12: AVX BLX FIX JAX LOX MAX OWX RAX SNX STX THX VUX; model→skeleton table in
|
||
BTL4.RES @~3232850: `madcat=mad`, `blkhawk=blh`, … — the INSIDE skeleton is the X-variant, so
|
||
Madcat = `MAX_COP`). It is the torso segment's SkeletonType_A mesh; the inside view loads
|
||
EXACTLY ONE segment mesh (the `_cop`) — 25 others hidden. [T2]
|
||
- Material: `<pfx>skin:blakskn_dz_{u,r,l}torso_mtl` — DIFFUSE/AMBIENT (0,0,0) + RAMP_REF
|
||
`softer`, **NO texture** (BLXSKIN.BMF is 238 bytes total; byte-verified). The 56 nonzero-UV
|
||
verts in BLX_COP sample nothing (planar-projection authoring leftover). [T1]
|
||
- Geogroups carry the `SV_SPECIAL " dz_*torso PUNCH "` token → `dpl_Punchize` (see
|
||
[[bgf-format]] PUNCH). The board opcode is a constant — the frame/window discriminator is the
|
||
GEOMETRY, not a key. [T1]
|
||
- **Every `_cop` is an OPEN STRUT LATTICE** (boundary-edge ratios, per-patch edge namespacing:
|
||
38-59% across all 12). The struts ARE the frame; the openings ARE the windows. Beware the
|
||
edge-counting trap: counting edges across patches (patch-local indices collide) undercounts
|
||
boundaries catastrophically (BLX "7%" wrong → 59% right). [T1]
|
||
- **Rendering (FINAL treatment 2026-07-11; bgfload.cpp keyed by filename `meshIsCop`):**
|
||
1. **The PUNCH STENCIL-CUT kit** (see §FINAL below + [[bgf-format]]): the TAG_PATCH scan
|
||
hash-pairs the punch geogroup's chunks into mask / hull / twin roles; L4D3D executes the
|
||
3-step stencil cut (D24S8). `BT_COP_PLATES=1` disables (diag).
|
||
2. **Double-sided** like all no-normal geometry — the earlier single-siding + per-face inward
|
||
winding existed solely to fight the solid-box look, which the un-cut PLATES caused, not the
|
||
winding; both are REMOVED (bgfload.cpp:533 comment). `BT_COP_SINGLE=1` restores
|
||
single-siding, `BT_COP_FLIP=1` flips its winding (diag only).
|
||
3. **UNLIT constant frame colour** (the map-lighting bug fix, 2026-07-11): the frame batch is
|
||
routed through the renderer's pure-emissive branch (black diffuse + emissive
|
||
`(0.13,0.12,0.15)` [T3 tuned-to-footage]; `BT_COP_FRAME="r g b"` overrides). The previous
|
||
lit path multiplied the colour by the MAP light — cavern-night `ambient=0.8` washed the
|
||
frame gray (user-reported). `BT_COP_RAMP_L` + the flat-ramp-index approach are RETIRED.
|
||
- The shell SHOWS by default in the inside view (btl4vid.cpp ApplyViewSkeleton);
|
||
`BT_HIDE_COCKPIT=1` hides it (diag).
|
||
- **Wrong turns, recorded so they stay dead:** (a) "windows are punch texels" — no texel exists;
|
||
(b) "the punch patch IS the visible frame / drop-punch removes the cockpit" — an artifact of
|
||
testing with the broken pre-inverse eye; (c) a view-direction face-cut heuristic — not
|
||
data-derivable; (d) "drop the whole punch geogroup = the windows" and (e) "duplicate pairs =
|
||
plates, hide both" — both superseded by the firmware-decoded STENCIL-CUT kit (§FINAL below),
|
||
which is the authentic mechanism.
|
||
|
||
## The authentic eyepoint (decomp-verified [T1])
|
||
|
||
Pipeline: `eyeWorld = parentSegmentWorld · baseOffset · R(EyepointRotation)`; `VIEW =
|
||
inverse(eyeWorld)`. No LookAt anywhere.
|
||
|
||
- **Eye ctor = FUN_004579a8** (part_007.c:9274, = DPLEyeRenderable). Caller (part_014.c:5525-66,
|
||
BTL4VID.CPP) gates on segment name `"siteeyepoint"`, passes:
|
||
- offset matrix = the segment's **GetBaseOffset()** (segment+0x74, the LOCAL rest transform) —
|
||
NOT GetSegmentToEntity, NOT an upright basis;
|
||
- parent = the eyepoint segment's **parent draw component** (`dcs_array[GetParentIndex()]`) —
|
||
liveness reaches the eye through this parent chain in the PORT TOO (task #58 FINAL): the
|
||
twist path is `Torso::PushTwist → Joint::SetRotation → hinge rotationAmount →
|
||
HingeRenderable::Execute` (multiplies the live hinge into the matrix stack), and the eye
|
||
executes inside that scope (`jointtorso → jointeye → siteeyepoint`; VERIFIED: with the real
|
||
jointtorso swept, the active cockpit `[eyefwd]` swings ~60° and the torso hinge renderable
|
||
sits in the eye's parent chain). The CAMERA yaws with the twist, the canopy rides the same
|
||
subtree, the crosshair stays screen-centered — see [[combat-damage]] Targeting.
|
||
⚠ TWO instrumentation traps produced false "eye is frozen" readings first: (1) the headless
|
||
default camera is the CHASE eye (root-parented, same DPLEyeRenderable class — gate probes on
|
||
the COCKPIT eye being live); (2) `BT_FORCE_TORSO` used to override the joint names with the
|
||
BLH demo pair (`jointshakey2`/`jointtshadow`), silently redirecting a MadCat sweep into the
|
||
SHADOW hinge — the hook now only fills joints the resource left unresolved. An explicit
|
||
eye-side twist compose added on those false readings DOUBLE-ROTATED the view (2× vs the 1×
|
||
canopy) and is retired;
|
||
- the `EyepointRotation` entity attribute (EulerAngles; Mech member, init 0).
|
||
- **Combine order:** `FUN_0040b104(dst,A,B)` computes `dst = B × A` (translation from B) →
|
||
the eye local = `baseOffset × R`, R applied FIRST in eye-local space. Recomputed only when the
|
||
euler CHANGES (FUN_00457b48 @part_007.c:9360, epsilon gate).
|
||
- **Per-frame view = FUN_004c22c4** (part_013.c:11742): rebuilds eyeWorld from the view-DCS's
|
||
live accumulated transform, then `FUN_0040b244` = TRUE AFFINE INVERSE → the view matrix, then a
|
||
UNIFORM zoom scale. The forward/up axes fall out of the eye's own basis — the renderer never
|
||
picks an axis. (Our old hand-rolled `D3DXMatrixLookAtRH` with forward=+Z/up=+Y row extraction
|
||
was exactly why some mechs aimed into the canopy — see [[reconstruction-gotchas]].)
|
||
- Port implementation: btl4vid.cpp (eye build: offset_matrix + parent_DCS), L4VIDRND.cpp
|
||
DPLEyeRenderable::Execute (order `Orient × R`, `D3DXMatrixInverse` on the live path — note the
|
||
ctor also holds a copy of this code but its view write is DEAD; patch the Execute).
|
||
|
||
## Status per mech + the gyro (task #56 — gyro now LIVE)
|
||
|
||
- **Madcat (MAX_COP): verified good on pure defaults** — dark domed frame (top arch +
|
||
cross-braces + A-pillars + dash), world through the openings. Un-regressed with the gyro live.
|
||
- **The gyro is RE-ENABLED and clean** (task #56): ctor field-map + integrators + writers
|
||
reconstructed byte-exact from @004b3778/@004b2ec0/@004b30ec/@004b34ec, layout
|
||
static_assert-locked (sizeof==0x3D0), joint-write dispatch moved to the MECH performance tail
|
||
(`GyroFrameJointWrite`, matching the binary calls @0x4aaf74/83), gyro↔torso pitch link wired
|
||
(`gyro+0x258 = &torso currentTwist`). Runtime: both joints resolve type 5, Performance
|
||
installed (flags 0x100), `WriteMechJoint eyePos=(0,0,0) body=(0,0,0)` finite every frame — the
|
||
old NaN is gone. Mechanism: WriteMechJoint drives ONE node (`jointeye`, which parents
|
||
`siteeyepoint`): TRANSLATION = the eye position spring, ROTATION = the body tip spring.
|
||
WriteEyeJoint is a MULTIPLICATIVE sway attenuator on `jointlocal`'s animated rotation
|
||
(gated legAnimState!=0; must run after the animation pass). [T2 runtime-verified]
|
||
- **Steady-offset hypothesis DISPROVEN [T2]:** the .MDL spring targets are SYMMETRIC
|
||
(pos=(0.1,0.15,0.1), neg=(−0.1,−0.15,−0.1) — same for madcat AND bhk1) → the eye equilibrium
|
||
is (0,0,0) and the clamps limit gyro motion to ±0.1..0.15u. The gyro is a BOUNCE/sway
|
||
mechanism, NOT a large steady offset. The authentic resting eye = raw baseOffset for every mech.
|
||
- **FINAL (2026-07-11, i860-firmware-decoded): PUNCH is a three-chunk STENCIL-CUT KIT.** The
|
||
VREND.MNG board firmware's 'damageize' handler (@0xf040f6f8, disassembled with a custom i860
|
||
tool) writes the dpl_Punchize token triple {dmg_set, undmg2enbl|texture, dmg_clear} 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, drawn where NOT masked; chunk3 = a byte-identical hull twin = the
|
||
damage-reset record, never visible. The visible canopy = hull-minus-apertures. Port:
|
||
bgfload identifies the within-patch twin pair (hull + skip) and tags the non-paired pmeshes as
|
||
masks; L4D3D draws mask (stencil 1, no colour/z-write), hull (stencil-reject), mask (stencil 0)
|
||
on a D24S8 device; the cut never touches z, so later-drawn entities show through apertures.
|
||
BT_COP_PLATES=1 disables the kit (diag). In-game, all 8 mechs on pure defaults [T2]:
|
||
connected dark frames with clear viewports; thor (the footage mech) shows the big central
|
||
viewport + surrounding frame exactly as filmed. TWO earlier readings are superseded and were
|
||
WRONG: "punch patches = windows, drop them" (left floating frame fragments) and "duplicate
|
||
pairs = plates, hide both" (hid the HULL and drew the MASK = the inversion the user caught).
|
||
- **Hit-bounce: LIVE and verified [T2] (2026-07-11).** The damage→gyro fan-out **FUN_004b2980**
|
||
was re-disassembled byte-exact from the raw image (capstone; scratchpad/dis_4b2980.py) and
|
||
reconstructed as `Gyroscope::ApplyDamageResponse` (gyro.cpp): zero-damage + Collision(0) no-op;
|
||
hit direction = `Damage::damageForce` or a RANDOM horizontal fallback when ~zero (our senders
|
||
don't fill damageForce yet — the fallback is binary-legal; directional fidelity = fill it later);
|
||
rotated by the yaw-only torso-twist frame; per-type scaling `amount / damageMultiplier[type] *
|
||
damageResponse[type].{trans,pitchRoll,yaw,vibration}` (Explosive alone multiplies burstCount);
|
||
clamp 1.3; four kicks — impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
|
||
**vibrationDirection@0x390 = (0,1,0)** — the "animationOffset/Scale/Phase scalars" were this
|
||
unit vector all along), verticalImpulse(yaw, dir). Call sites wired: the take-damage hub
|
||
(mech.cpp TakeDamageMessageHandler, FIRST action, binary @0x4a0264), the crushable-icon crunch
|
||
(mech4.cpp, torque 0.4 along Δv + up-impulse 0.2, @4aa81e/@4aa86c), the firing recoil
|
||
(projweap.cpp FireWeapon: damage>3 → impulse (0,0.6,−1.5) × damage/16, @4bc136-4bc19c).
|
||
Verified live: `[gyro-dmg]` fires per hit with correct scaling, eyePosition shows the damped
|
||
oscillation (ramp to ~0.015u, Y oscillating, decay to 0), no NaN, kill chain un-regressed.
|
||
**Quantitatively verified (2026-07-11 MP trace):** `BT_GYRO_TRACE=1` logs the integrator state
|
||
every displaced frame (`[gtrace]`); a 25-hit autofire session (scratchpad/GYRO_TRACE.png)
|
||
shows per-hit kick → damped oscillation → settle-to-zero-and-stay, peaks ±0.028u X/Z ±0.005u Y
|
||
(well inside the ±0.1/0.15 clamps), body pitch/roll ±0.02 rad, zero NaN/drift — and the
|
||
measured frequency ratio matches the authored constants: Y period 9.5 frames vs X/Z ~75 ≈
|
||
√(springK.y/springK.x)=√60≈7.75× as dictated by springK=(−0.2,−12,−0.2). [T2]
|
||
- **Deferred gyro tail:** (a) the alternate-gait engage JOLT + 0.4s engaged-gait RUMBLE
|
||
(@4aa158-4aa365 — full byte recipe in the wf_6880e605 synthesis; **flagged [T3] on the gate
|
||
naming** and it mutates the gait state machine tasks #49/#50 stabilized — do NOT wire without
|
||
re-verifying the gate semantics); (b) the mech+0x3F0 overspeed sway model (swayBias fed 0);
|
||
(c) the per-frame `EyepointRotation.pitch = torso pitch` writer (FUN_004b66b4 @0x4aafab) + the
|
||
mapper glance-look states (FUN_004afd10); (d) senders filling `Damage::damageForce` (beam:
|
||
impact−muzzle; projectile: velocity) for directional bounce fidelity; (e) FireWeapon's heat add
|
||
(pre-existing separate gap). `gyroRumbleTimer@0x5c4` was re-typed from the mis-named
|
||
`int clipLoadGuard` (binary uses it as a float rumble countdown).
|
||
|
||
## Offset cockpits — the placement-model verification [T1/T2]
|
||
|
||
Some mechs have laterally OFFSET cockpits, and the data + our port agree exactly (2026-07-11):
|
||
`[jointeye]` rest translation per SKL vs the `_COP` canopy X-center — **thor: tranx=+1.13, canopy
|
||
center +1.13 (exact)**; loki −0.25/−0.15; owens +0.19/+0.16; the other five ≈0/≈0. Our in-game
|
||
measured eye (`[EYECHK]`) reproduces the Thor offset to the hundredth (lateral 1.13 from the mech
|
||
origin). The eye chain (baseOffset + parent-segment DCS composition) is therefore CORRECT per-mech;
|
||
the canopy-render question was subsequently resolved by the stencil-cut kit — all 8 render
|
||
authentically (see the FINAL paragraph above).
|
||
|
||
## The playable roster is 8, not 12 [T1]
|
||
|
||
`BTL4.RES` contains exactly 8 mech model resources (avatar, bhk1/blkhawk, loki, madcat, owens,
|
||
sunder, thor, vulture — each with `<name>`, variants `<pfx>1/2`, and a `<pfx>dead`). Strider /
|
||
Raptor / Firestarter / Blackjack have `_COP` meshes, SKLs and gauge configs but NO model resource
|
||
(cut/other-version leftovers); forcing them via `BT_FORCE_MODEL` feeds NULL to
|
||
`ResourceDescription::Lock` → AV in `CreatePlayerVehicle` (cdb-verified). Valid `BT_FORCE_MODEL`
|
||
names: `avatar bhk1 loki madcat owens sunder thor vulture` (+ variants ava1/lok1/lok2/mad1/mad2/
|
||
own1/snd1/thr1/vul1/blkhawk).
|
||
|
||
## The authentic view field (FOV) — viewangle is HORIZONTAL [T1]
|
||
|
||
`BTDPL.INI [dpl_defaults] viewangle=60.0` is the **horizontal** field of view: the original Tesla
|
||
call (preserved in the DPLRenderer::SetView comment, L4VIDEO.cpp) is
|
||
`dpl_SetViewProjection(view, -1, -aspect, +1, +aspect, 1/tan(viewangle/2))` — horizontal
|
||
half-extent 1.0, vertical scaled by aspect. Authentic frustum at 4:3 = **60° × 46.8°**. The
|
||
port had fed the 60 to D3DX as the VERTICAL fov (75° × 60° at 4:3, worse on widescreen) — the
|
||
world read ~25% farther away ("camera too far back" reports) and the canopy frames were
|
||
massively over-revealed. Fixed (`BTFovYFromHorizontal`, all 5 projection sites incl. sky +
|
||
resize): the horizontal field stays 60° at any window shape. The aim/pick feed reads the live
|
||
projection (`gBTAimP22`) so targeting adapts automatically (combat re-verified). Credit: a
|
||
user-community report ("frames were never this revealed") + the resolution/FOV hypothesis.
|
||
|
||
## Diagnostics / env gates
|
||
|
||
`BT_FORCE_MODEL=<name>` (force the player mech: madcat/owens/sunder/…; btl4mssn.cpp),
|
||
`BT_HIDE_COCKPIT`, `BT_COP_PLATES` (disable the stencil kit), `BT_COP_FRAME="r g b"` (frame
|
||
colour override), `BT_COP_SINGLE` / `BT_COP_FLIP` (single-siding diag), `BT_COP_DEBUG` (paint
|
||
the shell green), `BT_COP_DUMP` (per-batch punch/bbox), `BT_EYE_FWD=<f>` (eye position probe),
|
||
`BT_GYRO_LOG` / `BT_GYRO_TRACE` (gyro bring-up log / per-frame integrator trace, gyro.cpp),
|
||
`[EYECHK]` log (eye world pos + look dir + entity pos, first frames).
|
||
(RETIRED: `BT_COP_DOUBLE`, `BT_COP_RAMP_L` — removed with the stencil-cut/unlit-frame landing.)
|
||
|
||
## Key Relationships
|
||
- Uses: [[bgf-format]] (PUNCH, vertex tags) · [[rendering]] (ramp model, skeleton branch)
|
||
- Feeds: [[gauges-hud]] (the HUD overlays this view) · [[subsystems]] (gyro, task #56)
|
||
- Gotchas: [[reconstruction-gotchas]] §14 (LookAt axis guess), §15 (per-patch edge counting)
|