From 74a0db4eddfdec2385ba7e7129537610fffd5784 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 6 Aug 2026 16:18:26 -0500 Subject: [PATCH] PPC hit scrambles the secondary displays (phase-14, all three work items) Restores the PPC's authentic secondary-display effect: an EnergyDamageType hit scrambles every secondary cockpit display for 0.8s while the main out-the-window view stays clean. Was fully specced (phase-14) with the engine half already present under the original VWE names; only the trigger and the visual were missing (the visual STUBBED since 2007). A -- TRIGGER (game/reconstructed/mech.cpp): in TakeDamageMessageHandler, at the binary's @0x4a03f3 position (after the cylinder resolve, before the burst loop, so ONCE per damage message) fire GetGaugeRenderer()->SpecialEffect(scrambleVideo, damageType*0.2f) on EnergyDamageType (==4). That type is authored on exactly the 14 PPC/ERPPC records, so the branch is structurally PPC-exclusive -- no weapon-class check. Duration DERIVED from the ordinal (4*0.2==0.8s), not a literal. BT_DMG_LOG prints [ppc-scramble]. B -- VISUAL (SVGA16::FunkyVideo, was the 2007 stub): FunkyVideo now arms scrambleActive; new SVGA16::ScrambleRowShift is a per-source-row horizontal shear+roll, read by BOTH DrawDevSurface (surround/dock) and ExpandPlaneToBGRA (glass windows, native + rotated radar), so all secondary surfaces shear together in source space and the main 3D view (separate timing chain) is untouched -- the modern stand-in for the VGA CRTC Horizontal-Total detune. Tunable: BT_SCRAMBLE_SHEAR (px/line, def 4), BT_SCRAMBLE_ROLL (px/sec, def 220); BT_SCRAMBLE_TEST=1 forces it on for tuning by eye. C -- NON-STACKING LATCH (L4GaugeRenderer::SpecialEffect): ignore the re-arm while scrambleVideoFlag is set (matches the binary's `modified` latch) -- a second PPC during the window no longer extends it. Was a divergence. Verified: Release links clean; surround boots + runs with the effect forced on, every secondary MFD + the radar shear while the out-the-window view stays clean, no crash (screenshot). Open (for the playtesters who filed the report): live PPC-fire confirmation + by-eye shear tuning -- k/roll are not recoverable from the binary. Co-Authored-By: Claude Opus 4.8 --- context/combat-damage.md | 2 +- context/gauges-hud.md | 15 +++-- engine/MUNGA_L4/L4GREND.cpp | 7 ++- engine/MUNGA_L4/L4VB16.cpp | 80 +++++++++++++++++++++++--- engine/MUNGA_L4/L4VB16.h | 14 +++++ game/reconstructed/mech.cpp | 26 +++++++++ phases/phase-14-ppc-sync-distortion.md | 33 +++++++++-- 7 files changed, 155 insertions(+), 22 deletions(-) diff --git a/context/combat-damage.md b/context/combat-damage.md index d3002f6..ce60376 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -645,7 +645,7 @@ Total by −9** for 0.8 s — every secondary cockpit display loses horizontal s is untouched. `EnergyDamageType` is authored on **exactly the 14 PPC/ERPPC records and nothing else**, so this is structurally PPC-exclusive. It fires **once per damage message** (outside the burst loop). Full chain + addresses: [[gauges-hud]] §"PPC HIT = a deliberate CRTC horizontal-sync -DETUNE"; port spec: `phases/phase-14-ppc-sync-distortion.md`. **Not yet implemented.** +DETUNE"; port spec: `phases/phase-14-ppc-sync-distortion.md`. **Implemented 2026-08-06** (branch `ppc-sync-distortion`). ## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1] The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4` diff --git a/context/gauges-hud.md b/context/gauges-hud.md index 423afc0..70539b2 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -13,7 +13,7 @@ open_questions: - "Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired" - "Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90; wired 2026-07-25, powersub.cpp); plus 0x13 = Mech DuckRequest (CROUCH -- COMPLETE 2026-08-06, [[locomotion]]), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles (searchlight visuals done 2026-08-05) -- see pod-hardware.md + docs/GLASS_COCKPIT.md; statuses re-swept 2026-08-06" - "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)" - - "PPC `scrambleVideo` NOT IMPLEMENTED (found 2026-08-06 [T1]): a PPC hit scrambles every secondary display for 0.8 s via a CRTC Horizontal-Total detune. Engine half already present under the original names (SpecialEffect/scrambleVideo, L4GREND.cpp:806+832, called per frame at :370); missing = the trigger in Mech::TakeDamageMessageHandler (damageType==4) and the visual (SVGA16::FunkyVideo is STUBBED, L4VB16.cpp:6358). Also note our SpecialEffect re-arms the timeout on a second hit — the binary latches and does NOT stack. Spec: phases/phase-14-ppc-sync-distortion.md" + - "PPC `scrambleVideo` IMPLEMENTED 2026-08-06 (branch ppc-sync-distortion): a PPC hit scrambles every secondary display for 0.8 s (modern per-scanline shear stand-in for the CRTC Horizontal-Total detune). Trigger in Mech::TakeDamageMessageHandler (damageType==4), visual in SVGA16::FunkyVideo/ScrambleRowShift (DrawDevSurface + ExpandPlaneToBGRA), non-stacking latch in L4GaugeRenderer::SpecialEffect. Screenshot-verified; live PPC-fire confirmation + by-eye shear tuning (BT_SCRAMBLE_SHEAR/ROLL) left to playtesters. Spec: phases/phase-14-ppc-sync-distortion.md" --- # Cockpit Gauges / MFD HUD @@ -758,11 +758,14 @@ pooled=0`), so the control chain is verified but final playback is not. ## PPC HIT = a deliberate CRTC horizontal-sync DETUNE on every secondary display (2026-08-06) [T1 disasm-verified] -**NOT YET IMPLEMENTED IN THE PORT.** Hand-off spec: -`phases/phase-14-ppc-sync-distortion.md`. Reported by playtesters as "being hit -by a PPC makes it look like all of the secondary CRTs were being degaussed" — -main (VPX) view unaffected, PPC strikes only. Both observations are exactly -what the binary does. +**✅ IMPLEMENTED 2026-08-06** (branch `ppc-sync-distortion`; screenshot-verified — +every secondary MFD + the radar shear together, the out-the-window view stays +clean). Trigger + visual + non-stacking latch — see +`phases/phase-14-ppc-sync-distortion.md` for the port details and the +`BT_SCRAMBLE_*` tuning envs. Reported by playtesters as "being hit by a PPC makes +it look like all of the secondary CRTs were being degaussed" — main (VPX) view +unaffected, PPC strikes only. Both observations are exactly what the binary does. +The disasm chain below is the ground truth the port was built from. **The gate is the damage TYPE, and only the PPC has it.** A `BTL4.RES` subsystem census gives `damageType` 4 (`EnergyDamageType`) = **14 records, diff --git a/engine/MUNGA_L4/L4GREND.cpp b/engine/MUNGA_L4/L4GREND.cpp index 63965e6..2342934 100644 --- a/engine/MUNGA_L4/L4GREND.cpp +++ b/engine/MUNGA_L4/L4GREND.cpp @@ -812,7 +812,12 @@ void switch (type) { case scrambleVideo: - if (graphicsDisplay != NULL) + // NON-STACKING LATCH (phase-14 fidelity): the binary latches on `modified` + // @0x4fe0fe -- a second PPC hit while the scramble is ALREADY live does NOT + // re-save the (already detuned) value and does NOT extend the window. This + // original unconditionally overwrote scrambleVideoTimeout, so rapid PPC fire + // STACKED the effect -- a divergence from the binary. Ignore re-arm while live. + if (graphicsDisplay != NULL && !scrambleVideoFlag) { scrambleVideoFlag = True; scrambleVideoTimeout = ((Scalar)Now()) + duration; diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index 4da69a6..904d40c 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -615,17 +615,24 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa Word *source = pixelBuffer.Data.MapPointer; Word *dest = (Word*)rect.pBits; int postRowIncrement = (rect.Pitch / 2) - w; + // PPC sync-scramble (phase-14): read each SOURCE row with a horizontal wrap + // (ScrambleRowShift == 0 unless FunkyVideo is armed), so the surface tears + // like a monitor that has lost horizontal lock. Indexed read (srcRow[sx]) + // instead of the linear source++. if (monoTint < 0) { // PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0). SVGA16Palette *pal = &palette[paletteID]; for (int y = 0; y < h; y++) { + Word *srcRow = source + y * w; + int sh = ScrambleRowShift(y, w); for (int x = 0; x < w; x++) { - PaletteTriplet *pe = &(pal->paletteData.Color[*source & mask]); + int sx = x + sh; if (sx >= w) sx -= w; + PaletteTriplet *pe = &(pal->paletteData.Color[srcRow[sx] & mask]); *dest = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3); - dest++; source++; + dest++; } dest += postRowIncrement; } @@ -637,10 +644,13 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa Word tint = (Word) monoTint; for (int y = 0; y < h; y++) { + Word *srcRow = source + y * w; + int sh = ScrambleRowShift(y, w); for (int x = 0; x < w; x++) { - *dest = (*source & mask) ? tint : 0; - dest++; source++; + int sx = x + sh; if (sx >= w) sx -= w; + *dest = (srcRow[sx] & mask) ? tint : 0; + dest++; } dest += postRowIncrement; } @@ -719,9 +729,11 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate { Word *src = base + y * w; unsigned long *d = dst + y * w; + int sh = ScrambleRowShift(y, w); // PPC scramble (0 unless armed) for (int x = 0; x < w; x++) { - Word s = src[x]; + int sx = x + sh; if (sx >= w) sx -= w; + Word s = src[sx]; if (monoTint < 0) { PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]); @@ -742,6 +754,13 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate // 90-degree rotation: output is transposed (ow = h, oh = w). rotate 3 = CW, // rotate 1 = CCW (the DrawDevSurface convention; BT_GAUGE_SEC_ROT picks it). int ow = h, oh = w; + // PPC scramble: precompute the per-SOURCE-ROW shift once. sy is used per output + // pixel and ScrambleRowShift reads the clock, so it must not be called per pixel. + // The shear stays in SOURCE space (keyed on sy), so the rotation displays an + // authentically-sheared raster. + int rowSh[512]; + int shRows = (scrambleActive && h <= 512) ? h : 0; + for (int i = 0; i < shRows; i++) rowSh[i] = ScrambleRowShift(i, w); for (int oy = 0; oy < oh; oy++) { unsigned long *d = dst + oy * ow; @@ -758,6 +777,7 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate sx = w - 1 - oy; sy = ox; } + if (shRows) { sx += rowSh[sy]; if (sx >= w) sx -= w; } Word s = base[sy * w + sx]; if (monoTint < 0) { @@ -5395,6 +5415,7 @@ SVGA16::SVGA16( BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index); for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw mDevSurfaceTex[_i] = NULL; + scrambleActive = False; // PPC sync-scramble (phase-14) -- armed by FunkyVideo //STUBBED: VIDEO RB 1/15/07 # if defined(DEBUG) Tell("SVGA16::SVGA16()\n"); @@ -6354,13 +6375,54 @@ void Check_Fpu(); } +// +// PPC sync-scramble (phase-14). The original (`//STUBBED: VIDEO RB 1/15/07`) +// detuned the VGA CRTC Horizontal Total by -9 so every secondary monitor lost +// horizontal lock for 0.8 s. There is no CRTC here, so FunkyVideo just arms / +// disarms a flag; the per-surface expansions (DrawDevSurface for the surround, +// ExpandPlaneToBGRA for the glass windows) shear the pixelBuffer read while it is +// set. The 0.8 s window + non-stacking latch stay in L4GaugeRenderer. +// void SVGA16::FunkyVideo(Logical on_off) { - //STUBBED: VIDEO RB 1/15/07 - //Check(this); - //SVGAFunkyVideo(on_off); - //Check_Fpu(); + scrambleActive = on_off; +} + +// +// Per-SOURCE-ROW horizontal shift for the scramble. Model (phase-14 §4): +// shift(y,t) = ( y*shear + roll*t ) mod width +// a constant per-line shear (the CRTC detune offset every scanline by a fixed +// amount -> a diagonal tear) plus a time roll (the monitor never re-locked, so +// the tear drifts). The physically-derived shear is ~9% of a line, but the +// on-screen result depended on each pod monitor's H-sync PLL -- not recoverable, +// so the default is a legible moderate tear; tune by eye against the playtesters +// who filed the report (BT_SCRAMBLE_SHEAR px/line, BT_SCRAMBLE_ROLL px/sec). +// +int + SVGA16::ScrambleRowShift(int srcRow, int width) const +{ + // BT_SCRAMBLE_TEST forces the effect always-on so the shear can be TUNED by eye + // (adjust BT_SCRAMBLE_SHEAR / BT_SCRAMBLE_ROLL and relaunch) without waiting to + // be hit by a PPC. Off by default. + static int sTest = -1; + if (sTest < 0) sTest = (getenv("BT_SCRAMBLE_TEST") != NULL) ? 1 : 0; + if ((!scrambleActive && !sTest) || width <= 0) + return 0; + + static double shearPx = -1.0, rollPx = -1.0; + if (shearPx < 0.0) + { + const char *e = getenv("BT_SCRAMBLE_SHEAR"); + const char *r = getenv("BT_SCRAMBLE_ROLL"); + shearPx = (e != NULL) ? atof(e) : 4.0; // px/line (aggressive ~9%: width*0.09) + rollPx = (r != NULL) ? atof(r) : 220.0; // px/sec drift + } + double roll = rollPx * ((double)GetTickCount() * 0.001); + long s = (long)((double)srcRow * shearPx + roll); + s %= width; + if (s < 0) s += width; + return (int)s; } //######################################################################## diff --git a/engine/MUNGA_L4/L4VB16.h b/engine/MUNGA_L4/L4VB16.h index f6dc0a1..f9dda0e 100644 --- a/engine/MUNGA_L4/L4VB16.h +++ b/engine/MUNGA_L4/L4VB16.h @@ -334,6 +334,20 @@ public: void FunkyVideo(Logical on_off); + // PPC sync-scramble (phase-14): the modern stand-in for the VGA CRTC + // Horizontal-Total detune. FunkyVideo(on) sets scrambleActive; every + // gauge-surface expansion (DrawDevSurface for the surround, ExpandPlaneToBGRA + // for the glass windows) then reads the pixelBuffer with a per-SOURCE-ROW + // horizontal shift so all secondary displays shear together -- authentic by + // construction (one VGA's detuned sync drives them all). Returns the source-x + // offset for a source row, 0 when not scrambling. Tunable by eye + // (BT_SCRAMBLE_SHEAR px/line, BT_SCRAMBLE_ROLL px/sec) -- the exact look + // depended on each pod monitor's H-sync PLL and is not recoverable. + int + ScrambleRowShift(int srcRow, int width) const; + Logical + scrambleActive; + protected: void BuildWindows(unsigned int width, unsigned int height, bool windowed, int *secondaryIndex, int *aux1Gauge, int *aux2Gauge); diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 69c8ce5..c5b6da8 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -82,6 +82,7 @@ // The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp) // for the same reason. #include "dmgtable.hpp" +#include // GaugeRenderer::SpecialEffect (virtual) -- PPC scramble trigger extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there) // heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete // there) -- mech.cpp cannot include the subsystem headers (local-stub collision) @@ -1122,6 +1123,31 @@ void } } reportZone = message->damageZone; // post-resolve (@0x4a0396 write) + + // + // @0x4a03f3 [T1] -- PPC/ERPPC SECONDARY-DISPLAY SCRAMBLE. EnergyDamageType + // (==4) is authored on exactly the 14 PPC/ERPPC subsystem records and nothing + // else, so this branch is structurally PPC-exclusive -- NO explicit weapon + // class check (the data authorship IS the gate). It sits OUTSIDE the burst + // loop below, so it fires ONCE per damage message, not per burst. The + // duration is DERIVED from the type ordinal, not a literal: (Scalar)4 * 0.2 == + // 0.8s (binary loads long double 0.2 @0x4a0c08, fmulp). SpecialEffect -> + // L4GaugeRenderer scrambles every SECONDARY cockpit display (SVGA16 sync + // detune, FunkyVideo) for that window; the main 3D view is on an independent + // timing chain and is left clean. Spec: phases/phase-14-ppc-sync-distortion.md. + // + if (message->damageData.damageType == Damage::EnergyDamageType) + { + GaugeRenderer *gauges = + (application != 0) ? application->GetGaugeRenderer() : 0; // APP.h:355 -- named, not application+0x4c + if (gauges != 0) // the binary null-guards the renderer too (@0x4a0405) + gauges->SpecialEffect(GaugeRenderer::scrambleVideo, + (Scalar)message->damageData.damageType * 0.2f); + if (BTEnvOn("BT_DMG_LOG", 0)) + DEBUG_STREAM << "[ppc-scramble] EnergyDamageType hit -> scrambleVideo for " + << ((Scalar)message->damageData.damageType * 0.2f) << "s\n" << std::flush; + } + // // #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which // REPLACES the engine-base single application. Three things the base diff --git a/phases/phase-14-ppc-sync-distortion.md b/phases/phase-14-ppc-sync-distortion.md index acf5a1a..e77f193 100644 --- a/phases/phase-14-ppc-sync-distortion.md +++ b/phases/phase-14-ppc-sync-distortion.md @@ -3,11 +3,34 @@ **Goal:** restore the PPC's authentic secondary effect — a PPC strike scrambles **every secondary cockpit display for 0.8 s**, leaving the main view untouched. -**Status:** NOT STARTED. Spec only. Discovered 2026-08-06 by disassembly of the -shipped `BTL4OPT.EXE` (md5 `a97075bcb5634d13263e9ad5a2b96fd0`) after playtesters -reported *"being hit by a PPC makes it look like all of the secondary CRTs were -being degaussed."* Full findings: `context/gauges-hud.md` §"PPC HIT = a -deliberate CRTC horizontal-sync DETUNE"; cross-ref in `context/combat-damage.md`. +**Status:** ✅ IMPLEMENTED 2026-08-06 (branch `ppc-sync-distortion`) [T2 — +visual screenshot-verified, trigger by construction]. Discovered 2026-08-06 by +disassembly of the shipped `BTL4OPT.EXE` (md5 `a97075bcb5634d13263e9ad5a2b96fd0`) +after playtesters reported *"being hit by a PPC makes it look like all of the +secondary CRTs were being degaussed."* Full findings: `context/gauges-hud.md` +§"PPC HIT = a deliberate CRTC horizontal-sync DETUNE"; cross-ref in +`context/combat-damage.md`. + +**What shipped (all three work items):** +- **A — trigger:** `game/reconstructed/mech.cpp`, `Mech::TakeDamageMessageHandler` + @0x4a03f3 position (after the cylinder resolve, before the burst loop) — fires + `GetGaugeRenderer()->SpecialEffect(scrambleVideo, damageType*0.2f)` once per + EnergyDamageType message. `BT_DMG_LOG` prints `[ppc-scramble]`. +- **B — visual:** `SVGA16::FunkyVideo` (was the 2007 stub) now arms `scrambleActive`; + `SVGA16::ScrambleRowShift` (new) is the per-source-row horizontal shear, read by + BOTH `DrawDevSurface` (surround/dock) and `ExpandPlaneToBGRA` (glass windows), so + all secondary surfaces + the rotated radar shear together and the main 3D view + (a separate timing chain) is untouched. Tunable: `BT_SCRAMBLE_SHEAR` (px/line, + default 4), `BT_SCRAMBLE_ROLL` (px/sec, default 220). **`BT_SCRAMBLE_TEST=1` + forces it always-on for tuning by eye without waiting for a PPC hit.** +- **C — non-stacking latch:** `L4GaugeRenderer::SpecialEffect` now ignores the + re-arm while `scrambleVideoFlag` is set (matches the binary's `modified` latch); + a second PPC during the window no longer extends it. +- Verified: Release links clean; surround boots + runs with the effect forced on, + every secondary MFD + the radar shear while the out-the-window view stays clean, + no crash. **Open:** live PPC-fire confirmation (one arm per hit, non-stacking + across two hits) + the by-eye shear tuning belong to the playtesters who filed + the report — `k`/`roll` are not recoverable from the binary. **Good news up front:** the engine half already exists in our tree under the **original VWE names** (`SpecialEffect` / `scrambleVideo` / `FunkyVideo`). Only