Death camera: forensics, capture, and probes (fix not yet applied)
Pilot sees the escape-pod/wreck interior at death instead of the portal ride. Findings (DEATH-SEQUENCE-NOTES.md): two death variants exist -- respawn deaths transit the chain camera to the portal at the origin (fp_cam's <100m vehicle-distance guard rejects exactly that ride = the fix target); mission-end deaths (tonight's capture) have no transit on the wire at all, just the fog fade at the wreck. At death the 0x1f articulation batches switch to driving origin-anchored nodes = the portal diorama animating; type-3 view flushes decode as projection+fog only (hither/yon at floats 12/13). Reference capture captures/netdeath-20260708.fifodump (full networked mission incl. death) + replay probes in probes/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# Death-sequence camera — findings (2026-07-09)
|
||||
|
||||
Pilot report (networked mission one, arena, Mad Cat): at death the bridge
|
||||
render shows **the inside of the escape pod** instead of the authentic
|
||||
sequence (trainer video: the dying pilot never sees the pod interior — the
|
||||
view rides to the blue-swirly translocation portal, the new mech steps out
|
||||
of the portal onto the field; the pod eject is a third-party visual only).
|
||||
|
||||
Reference capture: `captures/netdeath-20260708.fifodump` (4.5 MB, complete
|
||||
networked mission incl. the death and mission-end fade; taken 2026-07-08
|
||||
UTC-night). Probes: `probes/deathcam_probe.py`, `probes/viewflush_probe.py`
|
||||
(run with `py -3.13`; they replay the capture into a headless VirtualBoard).
|
||||
|
||||
## What the wire actually says
|
||||
|
||||
- **The camera DCS chain never leaves the wreck.** Chain eye stays ~7.8 m
|
||||
from the vehicle root through the whole death tail (never >100 m from it
|
||||
anywhere in 18,477 frames). So the `fp_cam` <100 m chain-sanity guard in
|
||||
live_bridge.py is NOT the culprit, and the transit is NOT a camera-chain
|
||||
move.
|
||||
- **Type-3 (view) flushes carry projection + fog only**, no camera pose:
|
||||
floats [0..8] frustum edges, [9] ≈1.73 FOV scale, [10..11] = 832×512,
|
||||
**[12]/[13] = hither/yon** (0.25/1300 in gameplay), [18]/[19] = fog
|
||||
near/far, [20..22] = fog RGB. Only 65 flushes in the whole mission — they
|
||||
fire on change, not per frame.
|
||||
- **The mission-end fade is on the wire and already renders**: ~22 flushes
|
||||
sweeping fog near/far from (193, 1205) down to (0.01, 0.05) with RGB
|
||||
fading to black — the reverse of the mission-start black hold.
|
||||
- **At death the 0x1f articulation batches switch to driving nodes anchored
|
||||
at the world origin (0, 1, 0)** — the portal diorama animating — while
|
||||
the mech's own nodes stop updating. This is the clearest wire signature
|
||||
that the death sequence is active.
|
||||
|
||||
## Hypothesis (matches the BT411 decomp)
|
||||
|
||||
`BTPOVStartEndRenderable` (game source) is constructed with BOTH
|
||||
`dplMainZone` and `dplDeathZone`. The authentic transit is a **view/zone
|
||||
re-parent**: the game moves the pilot's dpl_VIEW into the death zone, whose
|
||||
own camera root does the ride (death site → portal at origin → dropzone).
|
||||
Our bridge only follows the main-zone camera chain, so it stays at the
|
||||
wreck — with the escape-pod geometry spawned around it.
|
||||
|
||||
## Reconciliation with the EARLIER respawn-death capture
|
||||
|
||||
The 2026-07-09 analysis of `netdeath_snap.fifodump` (~9MB, the earlier
|
||||
networked death WITH respawn; see weapon-visuals memory + death_strip.py)
|
||||
found the chain camera DOES transit: death site → (0, 10, 0) at the portal
|
||||
during the black hold → dropzone for the fade-in. Two death variants:
|
||||
|
||||
- **Respawn death** (lives remain): chain camera rides to the portal. The
|
||||
fp_cam `<100 m from vehicle` sanity guard REJECTS exactly this ride →
|
||||
falls back to the wreck → pilot sees pod/shroud geometry. **For this
|
||||
variant the guard IS the bug**: relax it once a mission is established
|
||||
(e.g. keep the guard only until the first accepted lock, or accept large
|
||||
excursions when the chain eye moves CONTINUOUSLY frame-to-frame).
|
||||
- **Mission-end death** (tonight's capture): no transit at all — fog fade
|
||||
at the wreck, mission over. The wreck-side view may be authentic here;
|
||||
compare period footage of an end-of-mission death before changing it.
|
||||
|
||||
## Next steps
|
||||
|
||||
1. Find the death-zone view root on the wire: at the death frame, diff the
|
||||
node topology (nest/link records) for a second camera-like chain, and
|
||||
check which display list the portal diorama nodes hang from.
|
||||
2. Bridge fix: detect death-sequence-active (origin-anchored 0x1f
|
||||
activity + fog fade + the known w3/w4 flips) → release `fp_cam` and
|
||||
follow the death-zone chain (or, worst case, synthesize the ride:
|
||||
ease the eye from the wreck to the portal diorama at origin).
|
||||
3. Note: this capture's death was at mission end (no respawn) — capture a
|
||||
MID-mission death (lives remaining) to see the full portal ride +
|
||||
new-mech reveal before calling the fix authentic. Compare against the
|
||||
pilot's VHS stills (7-frame sequence, matched 2026-07-08).
|
||||
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Death-camera forensics on the saved networked death:
|
||||
per draw frame, where is the CHAIN camera eye vs the player vehicle root?
|
||||
Does the chain legitimately transit >100m away (portal ride) -- i.e. is the
|
||||
fp_cam <100m sanity guard what forces the escape-pod-interior view?
|
||||
Also log view-flush params (fog near/far/rgb + full float head) around the
|
||||
transit to find a robust 'death sequence active' gate."""
|
||||
import struct, sys, os
|
||||
sys.path.insert(0, r'c:\VWE\TeslaRel410\dpl3-revive\patha')
|
||||
os.environ.setdefault('SDL_VIDEODRIVER', 'dummy')
|
||||
import numpy as np
|
||||
from vrboard import VirtualBoard, Msg
|
||||
from vrview import Renderer
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), '..', 'captures', 'netdeath-20260708.fifodump')
|
||||
data = open(path, 'rb').read()
|
||||
board = VirtualBoard()
|
||||
r = Renderer(w=64, h=40)
|
||||
|
||||
frames = 0
|
||||
created = {}
|
||||
fogs = [] # (frame, floats[:24])
|
||||
track = [] # (frame, chain_eye, veh_t, dist)
|
||||
o = 0
|
||||
while o + 8 <= len(data):
|
||||
if data[o:o+4] != b'VPXM':
|
||||
o += 1; continue
|
||||
ln = struct.unpack_from('<I', data, o+4)[0]
|
||||
if o + 8 + ln > len(data): break
|
||||
body = data[o+8:o+8+ln]; o += 8 + ln
|
||||
if len(body) < 4: continue
|
||||
a = struct.unpack_from('<I', body, 0)[0]
|
||||
if a >= 0x100: continue
|
||||
p = body[4:]
|
||||
if a == 1 and len(p) >= 8:
|
||||
created[struct.unpack_from('<I', p, 4)[0]] = struct.unpack_from('<I', p, 0)[0]
|
||||
elif a == 3 and len(p) >= 96:
|
||||
h = struct.unpack_from('<I', p, 0)[0]
|
||||
if created.get(h) == 3:
|
||||
fogs.append((frames, struct.unpack_from('<24f', p, 4)))
|
||||
try: board.handle(Msg(False, 0xff, a, p))
|
||||
except Exception: pass
|
||||
if a == 9:
|
||||
frames += 1
|
||||
try:
|
||||
r.cache.maybe_rebuild(board)
|
||||
Mc = np.asarray(r.cam_matrix(board), float)
|
||||
ec = Mc[3, :3]
|
||||
except Exception:
|
||||
ec = None
|
||||
anim = board.anim_abs
|
||||
t = None
|
||||
chain = getattr(r.cache, 'cam_chain', None)
|
||||
h = None
|
||||
if chain and chain[-1] in anim:
|
||||
h = chain[-1]
|
||||
elif anim:
|
||||
h = max(anim, key=lambda k: float(np.abs(np.array(anim[k][9:12])).sum()))
|
||||
if h is not None:
|
||||
t = np.array(anim[h][9:12])
|
||||
if ec is not None:
|
||||
d = float(np.linalg.norm(ec - t)) if t is not None else -1.0
|
||||
track.append((frames, ec.copy(), None if t is None else t.copy(), d))
|
||||
|
||||
print(f"total draw frames: {frames}, tracked: {len(track)}")
|
||||
|
||||
# find where the chain eye leaves the vehicle by >100m (the guard threshold)
|
||||
first_far = next((i for i, (f, ec, t, d) in enumerate(track) if d > 100.0), None)
|
||||
print(f"first frame with chain-vehicle distance >100m: "
|
||||
+ (f"f{track[first_far][0]}" if first_far is not None else "NONE"))
|
||||
|
||||
# print the trajectory every ~30 frames over the last 1200 frames
|
||||
print("\n-- chain eye vs vehicle root (tail):")
|
||||
tail = track[-1200:]
|
||||
for i in range(0, len(tail), 30):
|
||||
f, ec, t, d = tail[i]
|
||||
ts = "None" if t is None else f"({t[0]:8.1f},{t[1]:7.1f},{t[2]:8.1f})"
|
||||
print(f" f{f}: eye=({ec[0]:8.1f},{ec[1]:7.1f},{ec[2]:8.1f}) veh={ts} dist={d:8.1f}")
|
||||
|
||||
# fog/view flushes in the tail window, with the full float head for gate hunting
|
||||
if track:
|
||||
cut = track[-1200][0] if len(track) >= 1200 else track[0][0]
|
||||
print(f"\n-- view flushes after f{cut} (floats 14..23):")
|
||||
last = None
|
||||
for f, fl in fogs:
|
||||
if f < cut: continue
|
||||
k = tuple(round(x, 2) for x in fl[14:24])
|
||||
if k != last:
|
||||
print(f" f{f}: " + " ".join(f"{x:.2f}" for x in k))
|
||||
last = k
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Type-3 (view) flush anatomy: are floats 0..13 a live camera pose?
|
||||
Sample across the whole mission + densely in the death tail; compare the
|
||||
candidate position triplet(s) against the vehicle root per frame."""
|
||||
import struct, os, sys
|
||||
sys.path.insert(0, r'c:\VWE\TeslaRel410\dpl3-revive\patha')
|
||||
import numpy as np
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), '..', 'captures', 'netdeath-20260708.fifodump')
|
||||
data = open(path, 'rb').read()
|
||||
|
||||
frames = 0
|
||||
created = {}
|
||||
veh = {} # latest max-translation 0x1f pose per frame
|
||||
flushes = [] # (frame, view_handle, floats24, veh_t)
|
||||
last_t = None
|
||||
o = 0
|
||||
while o + 8 <= len(data):
|
||||
if data[o:o+4] != b'VPXM':
|
||||
o += 1; continue
|
||||
ln = struct.unpack_from('<I', data, o+4)[0]
|
||||
if o + 8 + ln > len(data): break
|
||||
body = data[o+8:o+8+ln]; o += 8 + ln
|
||||
if len(body) < 4: continue
|
||||
a = struct.unpack_from('<I', body, 0)[0]
|
||||
p = body[4:]
|
||||
if a == 9:
|
||||
frames += 1
|
||||
elif a == 1 and len(p) >= 8:
|
||||
created[struct.unpack_from('<I', p, 4)[0]] = struct.unpack_from('<I', p, 0)[0]
|
||||
elif a == 0x1f and len(p) >= 8:
|
||||
# crude: largest |t| pose in the batch = player vehicle root
|
||||
n = struct.unpack_from('<I', p, 0)[0]
|
||||
off = 4
|
||||
best = None
|
||||
for _ in range(min(n, 64)):
|
||||
if off + 4 + 48 > len(p): break
|
||||
h = struct.unpack_from('<I', p, off)[0]
|
||||
f12 = struct.unpack_from('<12f', p, off + 4)
|
||||
t = f12[9:12]
|
||||
m = abs(t[0]) + abs(t[1]) + abs(t[2])
|
||||
if best is None or m > best[0]:
|
||||
best = (m, t)
|
||||
off += 4 + 48
|
||||
if best:
|
||||
last_t = best[1]
|
||||
elif a == 3 and len(p) >= 100:
|
||||
h = struct.unpack_from('<I', p, 0)[0]
|
||||
if created.get(h) == 3:
|
||||
flushes.append((frames, h, struct.unpack_from('<24f', p, 4), last_t))
|
||||
|
||||
print(f"draw frames={frames}, view flushes={len(flushes)}")
|
||||
handles = {}
|
||||
for f, h, fl, t in flushes:
|
||||
handles[h] = handles.get(h, 0) + 1
|
||||
print("view handles:", {hex(k): v for k, v in handles.items()})
|
||||
|
||||
def show(fr, h, fl, t):
|
||||
ts = "?" if t is None else f"({t[0]:7.1f},{t[1]:6.1f},{t[2]:7.1f})"
|
||||
print(f" f{fr} h={h:#x} veh={ts}")
|
||||
print(f" [0-8] " + " ".join(f"{x:7.3f}" for x in fl[0:9]))
|
||||
print(f" [9-13]" + " ".join(f"{x:9.2f}" for x in fl[9:14])
|
||||
+ f" [14-17] " + " ".join(f"{x:.2f}" for x in fl[14:18]))
|
||||
|
||||
# sparse samples through the mission
|
||||
print("\n-- sparse samples:")
|
||||
step = max(1, len(flushes) // 12)
|
||||
for i in range(0, len(flushes), step):
|
||||
show(*flushes[i])
|
||||
|
||||
# dense tail: last 30 flushes
|
||||
print("\n-- death tail (last 30):")
|
||||
for rec in flushes[-30:]:
|
||||
show(*rec)
|
||||
Reference in New Issue
Block a user