Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee84e14f25 | ||
|
|
74a0db4edd | ||
|
|
1164098d61 |
@@ -637,6 +637,16 @@ Benches: `scratchpad/night12/scorekill.sh` (cross-node kill: killer `kills=1 awa
|
||||
victim respawns, death #1 single-cycle) + `scoreself.sh` (#134 negation). Collision-death tail
|
||||
fallthrough is inspection-tier [T3] — shares the benched tail code; field wall-deaths exercise it.
|
||||
|
||||
**The handler's OTHER damageType branch — `damageType==4` (Energy) = the PPC cockpit-sync glitch
|
||||
[T1, 2026-08-06].** Between the collision divert and the burst loop sits a second type test
|
||||
@`0x4a03f3`: `cmp [esi+0x2c],4 / jne 0x4a0423`. On a match it calls the gauge renderer's vtable
|
||||
slot 19 with `((float)damageType × 0.2, 0)` = `(0.8f, 0)`, which detunes the **VGA CRTC Horizontal
|
||||
Total by −9** for 0.8 s — every secondary cockpit display loses horizontal sync, the main VPX view
|
||||
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`. **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`
|
||||
(half the damage to armour, half to ONE critical subsystem chosen by `criticalWeight`, capped by
|
||||
|
||||
@@ -13,6 +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` 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
|
||||
@@ -755,6 +756,89 @@ pooling fix) — an alarm that cannot acquire a source is silent.
|
||||
⚠ The bench cannot confirm audibility: it runs with no audio device (`live=0
|
||||
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]
|
||||
|
||||
**✅ 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,
|
||||
every one PPC or ERPPC**; everything else is Ballistic (16), Explosive (30),
|
||||
Laser (78). So a branch keyed on type 4 is structurally PPC-exclusive.
|
||||
|
||||
The chain, on the **VICTIM's** machine (all `@` from `BTL4OPT.EXE`,
|
||||
md5 `a97075bcb5634d13263e9ad5a2b96fd0`):
|
||||
|
||||
1. `Mech::TakeDamageMessageHandler` @`0x4a0230`, branch @**`0x4a03f3`** — sits
|
||||
between the collision divert and the burst loop, so it runs **once per
|
||||
damage message**, not per burst:
|
||||
```
|
||||
004a03f3 mov ecx,[esi+0x2c] ; damage.damageType
|
||||
004a03f6 cmp ecx,4 ; EnergyDamageType
|
||||
004a03f9 jne 0x4a0423 ; everything else -> burst loop
|
||||
004a03fb mov eax,[0x4efc94] ; the global `application`
|
||||
004a0400 mov eax,[eax+0x4c] ; -> gauge renderer
|
||||
004a0405 je 0x4a0423 ; null-guarded
|
||||
004a0407 fild dword [esi+0x2c] ; (float)damageType == 4.0
|
||||
004a040a fld xword [0x4a0c08] ; long double 0.2
|
||||
004a0410 fmulp st(1) ; => 0.8
|
||||
004a041d call dword [edx+0x4c] ; vtable slot 19, args (0.8f, 0)
|
||||
```
|
||||
⚠ The duration is **derived, not constant**: `(float)damageType × 0.2`.
|
||||
2. Gauge-renderer vtable @`0x51cebc`, slot 19 (`+0x4c`) = @**`0x46ffcc`**
|
||||
(an `L4GaugeRenderer` method — the 0x46xxxx MUNGA_L4 range, so the
|
||||
capability is shared-engine; BT is what wires it to Energy damage).
|
||||
Second arg must be 0 (`sub eax,1; jae ret`). Body: `svga16 = this[+0x1c52c]`
|
||||
(null-guarded — same member RP fetches for its `FlashPalette`), then
|
||||
`this[+0x1c534] = 1` (active) and `this[+0x1c538] = now + 0.8 s` in ticks.
|
||||
3. @`0x46d840` — thin wrapper, drops `this`, forwards the flag.
|
||||
4. @**`0x47d76d`** — the payload, straight VGA CRTC I/O:
|
||||
```
|
||||
out(0x3D4,0x11); v=in(0x3D5); out(0x3D5, v & 0x7F) ; unlock CRTC regs 0-7
|
||||
out(0x3D4,0x00) ; CRTC 0 = HORIZONTAL TOTAL
|
||||
if (arg==0) { out(0x3D5, saved); modified=0; } ; restore
|
||||
else if (!modified) { modified=1; saved=in(0x3D5);
|
||||
out(0x3D5, saved-9); } ; <<< shorten the scanline
|
||||
out(0x3D4,0x11); out(0x3D5, v) ; restore write-protect
|
||||
```
|
||||
Globals: `modified` @`0x4fe0fe`, `saved` @`0x4fe0ff`. The `modified` latch
|
||||
makes it **idempotent** — overlapping PPC hits do NOT stack, and a second
|
||||
hit does not re-save an already-detuned value.
|
||||
5. @**`0x47003c`** (per frame): `if (active && now >= expiry) { active = 0;
|
||||
SVGADistortSync(svga16, 0); }` — restores the saved Horizontal Total.
|
||||
|
||||
**Why it reads as a degauss, and why only the secondaries.** CRTC register 0 is
|
||||
the character-clock count per scanline — it *is* the horizontal scan frequency.
|
||||
−9 drives every attached monitor's horizontal oscillator off frequency: the
|
||||
image shears/rolls/wobbles until it re-locks, then snaps back 0.8 s later. All
|
||||
six secondary displays are derived by the VDB from that one VGA's timing, so
|
||||
they glitch **together**; the main view comes off the Division VPX card on an
|
||||
independent timing chain and is untouched. No relay, no VDB register, no
|
||||
palette work — the VDB just propagates a deliberately corrupted sync. (The
|
||||
`LampTesla1/2/3` "solid-state relays" in `L4CTRL.HPP` are NOT involved and are
|
||||
driven by nothing in the surviving tree.)
|
||||
|
||||
⚠ **Do not confuse this with the `SVGA16::FlashPalette` pixel-mask cycler**
|
||||
(`flashRate`/`mask[4]`, ports `0x302/0x30A/0x312`). That machinery is linked and
|
||||
its per-frame cycler runs in BT, but `FlashPalette` @`0x46d5f4` has **zero call
|
||||
sites and zero address-of references** in `BTL4OPT.EXE` — BT never arms it.
|
||||
**RP does**: `RPL4OPT.EXE` @`0x4addce` calls `FlashPalette(palette 1 =
|
||||
SecondaryPalette, rate 2.0, masks {FF,BF,7F,3F})` from its gauge-renderer ctor —
|
||||
hardware-assisted alarm blinking by masking off the top two pixel bits. Same
|
||||
pods, so it is an easy source of cross-game misattribution.
|
||||
|
||||
Scope note: callers of vtable slot 19 were not exhaustively enumerated (virtual
|
||||
dispatch); the PPC site was found via the three `application+0x4c` uses
|
||||
(`0x4a03fb` here, `0x4cc3be` / `0x4d1559` unrelated). The low-level path IS
|
||||
exhaustive — @`0x47d76d` has exactly one caller, and @`0x46d840` exactly two
|
||||
(set @`0x47002b`, restore @`0x470076`).
|
||||
|
||||
## Key Relationships
|
||||
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
|
||||
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
|
||||
|
||||
@@ -812,13 +812,18 @@ 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;
|
||||
|
||||
Check(graphicsDisplay);
|
||||
((SVGA16*) graphicsDisplay)->FunkyVideo(True);
|
||||
((SVGA16*) graphicsDisplay)->FunkyVideo(True, duration); // duration -> the recovery envelope
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
+234
-12
@@ -615,17 +615,41 @@ 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): map the SOURCE read through the recovery
|
||||
// envelope (collapse toward a line -> broaden -> lock). Identity when not
|
||||
// armed (ScrambleParams returns False). Output columns outside the collapsed
|
||||
// band read index 0 (black), so the picture squeezes to a line and grows back.
|
||||
double scl, shr, roff; int cx, shake;
|
||||
Logical scr = ScrambleParams(w, &scl, &shr, &roff, &cx, &shake);
|
||||
double invS = scr ? (1.0 / scl) : 1.0;
|
||||
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++)
|
||||
{
|
||||
int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1;
|
||||
Word *srcRow = source + sry * w; // vertical shake bounces the row
|
||||
double rowScroll = roff + shr * (double)y; // scroll + diagonal, per row
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
PaletteTriplet *pe = &(pal->paletteData.Color[*source & mask]);
|
||||
Word px;
|
||||
if (scr)
|
||||
{
|
||||
double srcBase = ((double)x - cx) * invS + cx; // collapse band
|
||||
if (srcBase < 0.0 || srcBase >= w)
|
||||
px = 0; // black outside the line
|
||||
else
|
||||
{
|
||||
long sx = (long)(srcBase + rowScroll + 0.5);
|
||||
sx %= w; if (sx < 0) sx += w; // roll SCROLLS within source
|
||||
px = srcRow[sx];
|
||||
}
|
||||
}
|
||||
else px = srcRow[x];
|
||||
PaletteTriplet *pe = &(pal->paletteData.Color[px & mask]);
|
||||
*dest = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3);
|
||||
dest++; source++;
|
||||
dest++;
|
||||
}
|
||||
dest += postRowIncrement;
|
||||
}
|
||||
@@ -637,10 +661,27 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
|
||||
Word tint = (Word) monoTint;
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1;
|
||||
Word *srcRow = source + sry * w; // vertical shake bounces the row
|
||||
double rowScroll = roff + shr * (double)y;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
*dest = (*source & mask) ? tint : 0;
|
||||
dest++; source++;
|
||||
Word px;
|
||||
if (scr)
|
||||
{
|
||||
double srcBase = ((double)x - cx) * invS + cx;
|
||||
if (srcBase < 0.0 || srcBase >= w)
|
||||
px = 0;
|
||||
else
|
||||
{
|
||||
long sx = (long)(srcBase + rowScroll + 0.5);
|
||||
sx %= w; if (sx < 0) sx += w;
|
||||
px = srcRow[sx];
|
||||
}
|
||||
}
|
||||
else px = srcRow[x];
|
||||
*dest = (px & mask) ? tint : 0;
|
||||
dest++;
|
||||
}
|
||||
dest += postRowIncrement;
|
||||
}
|
||||
@@ -712,16 +753,38 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
|
||||
Word *base = pixelBuffer.Data.MapPointer;
|
||||
SVGA16Palette *pal = &palette[paletteID];
|
||||
|
||||
// PPC sync-scramble (phase-14) -- the recovery envelope (collapse -> broaden ->
|
||||
// lock), mapped in SOURCE space so it survives the rotation. Identity when not
|
||||
// armed (ScrambleParams False). Reads outside the collapsed band -> index 0 (black).
|
||||
double scl, shr, roff; int cx, shake;
|
||||
Logical scr = ScrambleParams(w, &scl, &shr, &roff, &cx, &shake);
|
||||
double invS = scr ? (1.0 / scl) : 1.0;
|
||||
|
||||
if (rotateQuadrant == 0)
|
||||
{
|
||||
// Native orientation, top-down straight copy.
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
Word *src = base + y * w;
|
||||
int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1;
|
||||
Word *src = base + sry * w; // vertical shake bounces the row
|
||||
unsigned long *d = dst + y * w;
|
||||
double rowScroll = roff + shr * (double)y;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
Word s = src[x];
|
||||
Word s;
|
||||
if (scr)
|
||||
{
|
||||
double srcBase = ((double)x - cx) * invS + cx; // collapse band
|
||||
if (srcBase < 0.0 || srcBase >= w)
|
||||
s = 0; // black outside the line
|
||||
else
|
||||
{
|
||||
long sx = (long)(srcBase + rowScroll + 0.5);
|
||||
sx %= w; if (sx < 0) sx += w; // roll SCROLLS within source
|
||||
s = src[sx];
|
||||
}
|
||||
}
|
||||
else s = src[x];
|
||||
if (monoTint < 0)
|
||||
{
|
||||
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
|
||||
@@ -741,6 +804,8 @@ 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).
|
||||
// The scramble maps the SOURCE column (sx keyed on source row sy) through the
|
||||
// same envelope, so the radar recovers the same way the landscape MFDs do.
|
||||
int ow = h, oh = w;
|
||||
for (int oy = 0; oy < oh; oy++)
|
||||
{
|
||||
@@ -758,7 +823,21 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
|
||||
sx = w - 1 - oy;
|
||||
sy = ox;
|
||||
}
|
||||
Word s = base[sy * w + sx];
|
||||
int syS = sy + shake; if (syS < 0) syS = 0; else if (syS >= h) syS = h - 1; // shake
|
||||
Word s;
|
||||
if (scr)
|
||||
{
|
||||
double srcBase = ((double)sx - cx) * invS + cx; // collapse band (source col)
|
||||
if (srcBase < 0.0 || srcBase >= w)
|
||||
s = 0;
|
||||
else
|
||||
{
|
||||
long msx = (long)(srcBase + roff + shr * (double)sy + 0.5);
|
||||
msx %= w; if (msx < 0) msx += w; // roll SCROLLS within source
|
||||
s = base[syS * w + msx];
|
||||
}
|
||||
}
|
||||
else s = base[syS * w + sx];
|
||||
if (monoTint < 0)
|
||||
{
|
||||
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
|
||||
@@ -5395,6 +5474,9 @@ 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
|
||||
scrambleStartMs = 0;
|
||||
scrambleDurationS = 0.0;
|
||||
//STUBBED: VIDEO RB 1/15/07
|
||||
# if defined(DEBUG)
|
||||
Tell("SVGA16::SVGA16()\n");
|
||||
@@ -6354,13 +6436,153 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
// PPC sync-scramble (phase-14), animated as a RECOVERY. The original
|
||||
// (`//STUBBED: VIDEO RB 1/15/07`) detuned the VGA CRTC Horizontal Total so every
|
||||
// secondary monitor lost horizontal lock for ~0.8 s. There is no CRTC here, so
|
||||
// FunkyVideo records the start + duration and the per-surface expansions
|
||||
// (DrawDevSurface / ExpandPlaneToBGRA) map the pixelBuffer read through the
|
||||
// ScrambleParams envelope: at the hit the image COLLAPSES horizontally toward a
|
||||
// line, then BROADENS back out as the tear + roll DECAY, and LOCKS to the full
|
||||
// clean display at the end. The 0.8 s window + non-stacking latch stay in
|
||||
// L4GaugeRenderer; the shape lives here.
|
||||
//
|
||||
void
|
||||
SVGA16::FunkyVideo(Logical on_off)
|
||||
SVGA16::FunkyVideo(Logical on_off, Scalar duration)
|
||||
{
|
||||
//STUBBED: VIDEO RB 1/15/07
|
||||
//Check(this);
|
||||
//SVGAFunkyVideo(on_off);
|
||||
//Check_Fpu();
|
||||
if (on_off)
|
||||
{
|
||||
scrambleActive = True;
|
||||
scrambleStartMs = GetTickCount();
|
||||
scrambleDurationS = (duration > 0.0f) ? (double)duration : 0.8; // the HOLD
|
||||
}
|
||||
// FunkyVideo(False) is a NO-OP: the card restoring the sync (at HOLD end) is
|
||||
// where the RECOVERY begins, so ScrambleParams keeps running past the flag,
|
||||
// self-terminating at hold+recovery. scrambleActive stays armed; the clock gates.
|
||||
}
|
||||
|
||||
//
|
||||
// Two-phase envelope (per the "hold the bad sync, THEN recover" direction):
|
||||
// HOLD [0, hold] -- the card holds the detuned CRTC. Deep collapse (a line),
|
||||
// and a WILDLY fast horizontal roll that decelerates over
|
||||
// the hold (content scrolls, wrapping, so it can't be read).
|
||||
// RECOVERY [hold, +] -- the sync is restored and the monitor re-locks: the line
|
||||
// BROADENS back to full while the residual scroll + tear
|
||||
// settle to zero, then LOCKS (returns False = clean).
|
||||
// Fills the read-loop parameters: scale (collapse band width fraction), rollOff
|
||||
// (SCROLL offset in px, wrapped within the source), shear (px/row diagonal),
|
||||
// centerX. Tunables (read once): BT_SCRAMBLE_COLLAPSE (min band, deeper=smaller),
|
||||
// BT_SCRAMBLE_ROLL (initial scroll px/sec -- "wildly high"), BT_SCRAMBLE_SHEAR
|
||||
// (px/row), BT_SCRAMBLE_RECOVER (recovery seconds), BT_SCRAMBLE_DUR (hold seconds
|
||||
// override). BT_SCRAMBLE_TEST loops it; BT_SCRAMBLE_CYCLE loops stepping the roll.
|
||||
//
|
||||
Logical
|
||||
SVGA16::ScrambleParams(int width, double *scale, double *shear,
|
||||
double *rollOff, int *centerX, int *shakeRow) const
|
||||
{
|
||||
*shakeRow = 0;
|
||||
if (width <= 0)
|
||||
return False;
|
||||
|
||||
static int sInit = 0, sTest = 0, sCycle = 0;
|
||||
static double shearMax = 3.0, rollMax = 9000.0, collapse = 0.03,
|
||||
recover = 0.5, holdEnv = 0.0, shakeAmp = 10.0;
|
||||
if (!sInit)
|
||||
{
|
||||
sInit = 1;
|
||||
sTest = (getenv("BT_SCRAMBLE_TEST") != NULL) ? 1 : 0;
|
||||
sCycle = (getenv("BT_SCRAMBLE_CYCLE") != NULL) ? 1 : 0;
|
||||
const char *e;
|
||||
if ((e = getenv("BT_SCRAMBLE_SHEAR")) != NULL) shearMax = atof(e);
|
||||
if ((e = getenv("BT_SCRAMBLE_ROLL")) != NULL) rollMax = atof(e);
|
||||
if ((e = getenv("BT_SCRAMBLE_COLLAPSE")) != NULL) collapse = atof(e);
|
||||
if ((e = getenv("BT_SCRAMBLE_RECOVER")) != NULL) recover = atof(e);
|
||||
if ((e = getenv("BT_SCRAMBLE_DUR")) != NULL) holdEnv = atof(e);
|
||||
if ((e = getenv("BT_SCRAMBLE_SHAKE")) != NULL) shakeAmp = atof(e);
|
||||
if (collapse < 0.004) collapse = 0.004;
|
||||
if (collapse > 1.0) collapse = 1.0;
|
||||
if (recover < 0.05) recover = 0.05;
|
||||
}
|
||||
|
||||
double hold = (holdEnv > 0.0) ? holdEnv
|
||||
: (scrambleDurationS > 0.0 ? scrambleDurationS : 0.8);
|
||||
double total = hold + recover;
|
||||
double rlMax = rollMax;
|
||||
double elapsed;
|
||||
|
||||
if (sTest || sCycle)
|
||||
{
|
||||
// Tuning: loop the whole effect (hold+recovery) + a short locked pause.
|
||||
// CYCLE steps the initial roll speed each loop.
|
||||
double loop = total + 0.6;
|
||||
double t = (double)GetTickCount() * 0.001;
|
||||
long n = (long)(t / loop);
|
||||
double ph = t - (double)n * loop;
|
||||
if (ph >= total)
|
||||
return False; // locked pause between loops
|
||||
elapsed = ph;
|
||||
if (sCycle)
|
||||
{
|
||||
static const double kR[] = { 3000.0, 6000.0, 9000.0, 14000.0, 20000.0 };
|
||||
int nS = (int)(sizeof(kR) / sizeof(kR[0]));
|
||||
rlMax = kR[(int)(n % nS)];
|
||||
static long lastN = -1;
|
||||
if (n != lastN) { lastN = n;
|
||||
DEBUG_STREAM << "[scramble-cycle] loop " << n << " rollMax=" << rlMax
|
||||
<< "px/s (hold " << hold << "s + recover " << recover << "s)\n"
|
||||
<< std::flush; }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!scrambleActive)
|
||||
return False;
|
||||
elapsed = ((double)(GetTickCount() - scrambleStartMs)) * 0.001;
|
||||
if (elapsed >= total)
|
||||
return False; // LOCKED -- clean display
|
||||
}
|
||||
if (elapsed < 0.0) elapsed = 0.0;
|
||||
|
||||
double sScale, sScroll, sShear;
|
||||
if (elapsed < hold)
|
||||
{
|
||||
// HOLD: bad sync held. Deep collapse; roll starts wild and decelerates to a
|
||||
// stop by the hold end. scroll = integral of v(t)=rlMax*(1-t/hold): the
|
||||
// content scrolls fast then coasts to rest (rlMax*hold/2 accumulated).
|
||||
sScale = collapse;
|
||||
sScroll = rlMax * elapsed * (1.0 - elapsed / (2.0 * hold));
|
||||
sShear = shearMax;
|
||||
}
|
||||
else
|
||||
{
|
||||
// RECOVERY: sync restored, monitor re-locks. Broaden line->full and settle
|
||||
// the residual scroll + tear to zero (smoothstep), then it hits total = lock.
|
||||
double tr = (elapsed - hold) / recover;
|
||||
double b = tr * tr * (3.0 - 2.0 * tr);
|
||||
sScale = collapse + (1.0 - collapse) * b;
|
||||
sScroll = (rlMax * hold * 0.5) * (1.0 - b);
|
||||
sShear = shearMax * (1.0 - b);
|
||||
}
|
||||
|
||||
// SHAKE: a per-FRAME jitter (LCG, so all surfaces shake together within a frame
|
||||
// but re-roll each frame), violent at the hit and fading through the recovery.
|
||||
// Horizontal folds into the scroll; vertical bounces the source row (shakeRow).
|
||||
double shakeEnv = (elapsed < hold)
|
||||
? (1.0 - 0.5 * (elapsed / hold)) // 1.0 at impact -> 0.5 at hold end
|
||||
: (0.5 * (1.0 - (elapsed - hold) / recover)); // 0.5 -> 0 through recovery
|
||||
if (shakeEnv < 0.0) shakeEnv = 0.0;
|
||||
unsigned int fr = (unsigned int)(GetTickCount() / 16); // ~per-frame index
|
||||
unsigned int r = fr * 1103515245u + 12345u;
|
||||
double jX = ((double)((r >> 16) & 0x7FFF) / 16384.0) - 1.0; // [-1,1)
|
||||
r = r * 1103515245u + 12345u;
|
||||
double jY = ((double)((r >> 16) & 0x7FFF) / 16384.0) - 1.0;
|
||||
|
||||
*scale = sScale;
|
||||
*shear = sShear;
|
||||
*rollOff = sScroll + shakeAmp * shakeEnv * jX; // horizontal shake -> scroll
|
||||
*centerX = width / 2;
|
||||
*shakeRow = (int)(shakeAmp * shakeEnv * jY + (jY >= 0.0 ? 0.5 : -0.5)); // vertical bounce
|
||||
return True;
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
|
||||
@@ -332,7 +332,27 @@ public:
|
||||
UnflashPalette(int palette_number);
|
||||
|
||||
void
|
||||
FunkyVideo(Logical on_off);
|
||||
FunkyVideo(Logical on_off, Scalar duration = 0.0f);
|
||||
|
||||
// PPC sync-scramble (phase-14): the modern stand-in for the VGA CRTC
|
||||
// Horizontal-Total detune, animated as a RECOVERY over the effect window -- at
|
||||
// the hit the image collapses horizontally toward a line, then broadens back
|
||||
// out as the tear + roll decay, and LOCKS to the full clean display at the end.
|
||||
// FunkyVideo(on, dur) records the start + duration; ScrambleParams derives the
|
||||
// per-frame envelope (returns True while active + fills scale/shear/rollOff/
|
||||
// centerX); DrawDevSurface (surround) + ExpandPlaneToBGRA (glass) map the
|
||||
// pixelBuffer read through it, so all secondary surfaces recover together and
|
||||
// the main 3D view is untouched. Tunable by eye: BT_SCRAMBLE_SHEAR (max
|
||||
// px/line), BT_SCRAMBLE_ROLL (max px/sec), BT_SCRAMBLE_COLLAPSE (min width
|
||||
// fraction 0..1), BT_SCRAMBLE_DUR (anim seconds override). BT_SCRAMBLE_TEST
|
||||
// loops the transition for tuning; BT_SCRAMBLE_CYCLE loops it stepping the
|
||||
// max shear. (Exact look depended on each pod monitor's PLL -- not recoverable.)
|
||||
Logical
|
||||
ScrambleParams(int width, double *scale, double *shear,
|
||||
double *rollOff, int *centerX, int *shakeRow) const;
|
||||
Logical scrambleActive;
|
||||
unsigned long scrambleStartMs;
|
||||
double scrambleDurationS;
|
||||
|
||||
protected:
|
||||
|
||||
|
||||
@@ -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 <GAUGREND.hpp> // 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
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
# Phase 14 — PPC hit = `scrambleVideo`, the cockpit-CRT sync detune
|
||||
|
||||
**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:** ✅ 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 (animated recovery):** `SVGA16::FunkyVideo(on, dur)` (was the 2007
|
||||
stub) records the start + hold; `SVGA16::ScrambleParams` (new) is a two-phase
|
||||
envelope read by BOTH `DrawDevSurface` (surround/dock) and `ExpandPlaneToBGRA`
|
||||
(glass windows, native + rotated radar), so all secondary surfaces recover
|
||||
together and the main 3D view (separate timing chain) is untouched. The sequence
|
||||
per hit: **collapse** to a thin centred line → **HOLD** for the ~0.8 s the card
|
||||
held bad sync (content scrolls wildly fast + decelerating, plus a violent
|
||||
**shake**) → **RECOVERY** (~0.5 s: the line broadens back to full while
|
||||
scroll/tear/shake settle) → **LOCK** (clean). The read loops map the source
|
||||
through the envelope: horizontal `scale` sets a black-bordered collapse band,
|
||||
`rollOff` scrolls (wraps) within it, `shear` is a per-row diagonal, and a
|
||||
per-frame LCG `shake` bounces the row + jitters the scroll. Tunables (by eye —
|
||||
the pod-monitor PLL look is not recoverable): `BT_SCRAMBLE_COLLAPSE` (min band
|
||||
fraction, 0.03), `BT_SCRAMBLE_ROLL` (initial scroll px/s, 9000),
|
||||
`BT_SCRAMBLE_SHAKE` (px, 10), `BT_SCRAMBLE_SHEAR` (px/row, 3), `BT_SCRAMBLE_DUR`
|
||||
(hold s), `BT_SCRAMBLE_RECOVER` (recovery s, 0.5). **`BT_SCRAMBLE_TEST=1` loops
|
||||
the whole transition** for tuning without a hit; `BT_SCRAMBLE_CYCLE=1` loops it
|
||||
stepping the roll speed.
|
||||
- **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. NB the SVGA16 animation runs
|
||||
the full hold+recovery on its own clock (`FunkyVideo(False)` is a no-op — the
|
||||
card restoring sync is where the recovery *begins*), so the recovery isn't cut
|
||||
off when the 0.8 s latch clears.
|
||||
- Verified [T2]: Release links clean; surround boots + runs with the effect looped
|
||||
(`BT_SCRAMBLE_TEST`) at both slowed and true full speed — every secondary MFD +
|
||||
the radar collapse/roll/shake/recover together while the out-the-window view
|
||||
stays clean, no crash (screenshots). **SHIPPING with the current defaults for
|
||||
playtester feedback.** Open: live PPC-fire confirmation (one arm per hit,
|
||||
non-stacking across two hits) + by-eye tuning of the envelope constants — none
|
||||
recoverable from the binary, so the testers who filed the report are the ground
|
||||
truth.
|
||||
|
||||
**Good news up front:** the engine half already exists in our tree under the
|
||||
**original VWE names** (`SpecialEffect` / `scrambleVideo` / `FunkyVideo`). Only
|
||||
two things are missing: the **trigger** (never ported) and the **visual**
|
||||
(stubbed out in 2007). This is a small, well-bounded job.
|
||||
|
||||
---
|
||||
|
||||
## 1. What the original did [T1 — disasm-verified]
|
||||
|
||||
On the **victim's** machine, `Mech::TakeDamageMessageHandler` @`0x4a0230` tests
|
||||
the damage type between the collision divert and the burst loop:
|
||||
|
||||
```
|
||||
004a03f3 mov ecx,[esi+0x2c] ; damage.damageType
|
||||
004a03f6 cmp ecx,4 ; EnergyDamageType
|
||||
004a03f9 jne 0x4a0423 ; everything else -> burst loop
|
||||
004a03fb mov eax,[0x4efc94] ; global `application`
|
||||
004a0400 mov eax,[eax+0x4c] ; -> gauge renderer
|
||||
004a0405 je 0x4a0423 ; null-guarded
|
||||
004a0407 fild dword [esi+0x2c] ; (float)damageType == 4.0
|
||||
004a040a fld xword [0x4a0c08] ; long double 0.2
|
||||
004a0410 fmulp st(1) ; => 0.8
|
||||
004a041d call dword [edx+0x4c] ; vtable slot 19 == SpecialEffect(0, 0.8f)
|
||||
```
|
||||
|
||||
→ `L4GaugeRenderer::SpecialEffect(scrambleVideo, 0.8f)` @`0x46ffcc`
|
||||
→ `SVGA16::FunkyVideo(True)` @`0x47d76d`, which reprograms the **VGA CRT
|
||||
controller**:
|
||||
|
||||
```
|
||||
out(0x3D4,0x11); v=in(0x3D5); out(0x3D5, v & 0x7F) ; unlock CRTC regs 0-7
|
||||
out(0x3D4,0x00) ; CRTC 0 = HORIZONTAL TOTAL
|
||||
saved = in(0x3D5); out(0x3D5, saved - 9) ; shorten every scanline
|
||||
out(0x3D4,0x11); out(0x3D5, v) ; restore write-protect
|
||||
```
|
||||
|
||||
A per-frame timer @`0x47003c` writes `saved` back 0.8 s later.
|
||||
|
||||
**Why only the PPC:** a `BTL4.RES` census gives `damageType` 4 (`Energy`) =
|
||||
**14 subsystem records, every one PPC or ERPPC**. Everything else is Ballistic
|
||||
(16), Explosive (30), Laser (78). The branch is structurally PPC-exclusive —
|
||||
no extra gating needed.
|
||||
|
||||
**Why only the secondaries:** all six secondary displays are derived by the VDB
|
||||
from that one VGA's timing, so they break together. The main out-the-window
|
||||
view comes off the Division VPX card on an independent timing chain and is
|
||||
unaffected. Playtesters confirm both halves.
|
||||
|
||||
---
|
||||
|
||||
## 2. What our tree already has
|
||||
|
||||
| Piece | Where | State |
|
||||
|---|---|---|
|
||||
| `enum VideoEffectType { scrambleVideo }` (value **0**) | `engine/MUNGA/GAUGREND.h:482` | ✅ present |
|
||||
| `virtual void GaugeRenderer::SpecialEffect(VideoEffectType, Scalar) {}` | `engine/MUNGA/GAUGREND.h:488` | ✅ base no-op |
|
||||
| `L4GaugeRenderer::SpecialEffect` — sets `scrambleVideoFlag`, `scrambleVideoTimeout = Now()+duration`, calls `FunkyVideo(True)` | `engine/MUNGA_L4/L4GREND.cpp:806` | ✅ implemented |
|
||||
| `L4GaugeRenderer::ProcessVideoEffects()` — on timeout, `FunkyVideo(False)` | `engine/MUNGA_L4/L4GREND.cpp:832` | ✅ implemented |
|
||||
| …called every frame from `ExecuteForeground` | `engine/MUNGA_L4/L4GREND.cpp:370` | ✅ live |
|
||||
| `SVGA16::FunkyVideo(Logical)` | `engine/MUNGA_L4/L4VB16.cpp:6358` | ❌ **STUBBED** (`//STUBBED: VIDEO RB 1/15/07`, body commented out) |
|
||||
| Any caller of `SpecialEffect` | — | ❌ **NONE** |
|
||||
|
||||
The base-class declaration carries its original comment:
|
||||
|
||||
> `// Quick and dirty hack to allow calling L4GaugeRenderer::SpecialEffect`
|
||||
> `// from non L4 level. GDU 2/28/96`
|
||||
|
||||
That hack exists **because the damage handler (non-L4 level) had to call it** —
|
||||
independent corroboration that the trigger belonged in `mech.cpp`, and the
|
||||
reason you can call it through the base pointer without dragging L4 headers
|
||||
into a game-layer TU.
|
||||
|
||||
---
|
||||
|
||||
## 3. Work item A — the trigger (`game/reconstructed/mech.cpp`)
|
||||
|
||||
In `Mech::TakeDamageMessageHandler`, **after** the `damageType==0` collision
|
||||
divert and **before** the burst loop, add the type-4 branch.
|
||||
|
||||
```cpp
|
||||
// @0x4a03f3 [T1] -- PPC/ERPPC only: EnergyDamageType is authored on exactly
|
||||
// the 14 PPC/ERPPC subsystem records and nothing else. Duration is DERIVED
|
||||
// from the type ordinal, not a constant: (float)damageType * 0.2 == 0.8f.
|
||||
if (damage.damageType == Damage::EnergyDamageType) // == 4
|
||||
{
|
||||
GaugeRenderer *gauges = (application != 0)
|
||||
? application->GetGaugeRenderer() : 0; // APP.h:355
|
||||
if (gauges != 0) // binary null-guards too
|
||||
{
|
||||
gauges->SpecialEffect(
|
||||
GaugeRenderer::scrambleVideo,
|
||||
(Scalar)damage.damageType * 0.2f); // long double 0.2 @0x4a0c08
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Placement matters.** The binary's branch is *outside* the burst loop, so it
|
||||
fires **once per damage message**, not once per burst. Putting it inside the
|
||||
loop would re-arm it `burstCount` times.
|
||||
|
||||
Use the named accessor — do not raw-read `application+0x4c` (databinding rule,
|
||||
`context/reconstruction-gotchas.md`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Work item B — the visual (`SVGA16::FunkyVideo`)
|
||||
|
||||
There is no CRTC to detune, so reproduce the **look**, applied to the gauge
|
||||
composite only.
|
||||
|
||||
What the original did physically: Horizontal Total sets character clocks per
|
||||
scanline. Subtracting 9 shortens every line by roughly **9%**, far outside any
|
||||
monitor's sync lock range, so the picture breaks into a rolling diagonal tear
|
||||
until the value is restored. VWE's own name for it — `scrambleVideo` — is the
|
||||
best description of the intended result.
|
||||
|
||||
Suggested model (per-scanline horizontal displacement of the gauge buffer):
|
||||
|
||||
```
|
||||
shift(y, t) = ( y * k + roll(t) ) mod width
|
||||
```
|
||||
|
||||
- `k` — per-line shear, the fraction of a line lost. ~9% of width is the
|
||||
physically-derived starting point; **tune by eye** against the playtester
|
||||
description rather than treating it as exact, because the on-screen result
|
||||
depended on how each pod monitor's H-sync PLL misbehaved — that is not
|
||||
recoverable from the binary.
|
||||
- `roll(t)` — a time-varying offset so the tear drifts rather than sitting
|
||||
static. The original rolled because the monitor never re-locked.
|
||||
|
||||
Constraints:
|
||||
|
||||
- **Gauge composite only.** Apply to the `SVGA16` `pixelBuffer`
|
||||
(`engine/MUNGA_L4/l4vb16.h:243`) or at the point the strip/surfaces are
|
||||
presented — *never* the main 3D view. The main view being clean is a
|
||||
confirmed observation, not an assumption.
|
||||
- **All secondary surfaces together.** They are bit-planes of one shared
|
||||
buffer, so a single buffer-level effect is authentic by construction; do not
|
||||
implement it per-MFD.
|
||||
- **Keep the existing timing path.** `SpecialEffect` / `ProcessVideoEffects`
|
||||
already own the flag and the 0.8 s timeout and are already called per frame.
|
||||
`FunkyVideo` should only set/clear state — no timing logic of its own.
|
||||
|
||||
---
|
||||
|
||||
## 5. Fidelity constraints (do not "improve" these)
|
||||
|
||||
1. Duration is `damageType * 0.2f`, **not** a literal `0.8f`.
|
||||
2. Fires on `EnergyDamageType` — never add an explicit PPC class check; the
|
||||
data authorship *is* the gate.
|
||||
3. **Idempotent, non-stacking.** The binary latches on `modified` @`0x4fe0fe`:
|
||||
a second PPC hit while the effect is live does **not** re-save the (already
|
||||
detuned) value and does **not** extend or double the effect. Our
|
||||
`SpecialEffect` currently *does* overwrite `scrambleVideoTimeout`, which
|
||||
extends the effect on a second hit — **that is a divergence.** Match the
|
||||
binary: ignore re-arm while `scrambleVideoFlag` is set. (The original's
|
||||
latch was in `FunkyVideo`; ours must go in `SpecialEffect` or `FunkyVideo`,
|
||||
but it must exist.)
|
||||
4. Victim-side only. The shooter sees nothing; this runs in the victim's
|
||||
damage handler.
|
||||
5. Null-guard the gauge renderer — the binary does, and headless/bench runs
|
||||
have none.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- **Headless:** add a one-line log in the new branch; fire a PPC at a dummy
|
||||
with `BT_DMG_LOG`. Expect exactly one arm per PPC message, zero for laser /
|
||||
autocannon / missile / Gauss.
|
||||
- **Non-stacking:** two PPC hits ~0.2 s apart must produce one 0.8 s effect
|
||||
measured from the *first* hit, not 1.0 s or two effects.
|
||||
- **Live:** confirm every secondary MFD scrambles together and the main view
|
||||
stays clean. Have the playtesters who filed the report compare — they are
|
||||
the only ground truth for `k` and `roll`.
|
||||
- **Against the original (optional, decisive):** in the DOSBox-X fork, log
|
||||
writes to CRTC index 0 via `0x3D4`/`0x3D5` during a real BT mission.
|
||||
Prediction: exactly one write of `saved-9` per PPC strike, one restore 0.8 s
|
||||
later, zero for every other weapon.
|
||||
|
||||
---
|
||||
|
||||
## 7. Gotchas
|
||||
|
||||
- Do **not** confuse this with `SVGA16::FlashPalette` (the pixel-mask cycler on
|
||||
ports `0x302/0x30A/0x312`). That machinery is linked and its per-frame cycler
|
||||
runs, but `FlashPalette` @`0x46d5f4` has **zero call sites and zero
|
||||
address-of references** in `BTL4OPT.EXE` — BT never arms it. **RP does**
|
||||
(`RPL4OPT.EXE` @`0x4addce`, palette 1, rate 2.0, masks `{FF,BF,7F,3F}`) for
|
||||
alarm blinking. Wrong mechanism, wrong game.
|
||||
- The `LampTesla1/2/3` "solid-state relays" in `L4CTRL.HPP` are **not**
|
||||
involved and are driven by nothing in the surviving tree.
|
||||
- Scope note: callers of gauge-renderer vtable slot 19 were not exhaustively
|
||||
enumerated (virtual dispatch), so another arming site may exist. The
|
||||
low-level path *is* exhaustive — @`0x47d76d` has one caller, @`0x46d840` two
|
||||
(set @`0x47002b`, restore @`0x470076`).
|
||||
|
||||
## References
|
||||
- `context/gauges-hud.md` §"PPC HIT = a deliberate CRTC horizontal-sync DETUNE"
|
||||
- `context/combat-damage.md` §`Mech::TakeDamageMessageHandler` (Energy branch)
|
||||
- `HISTORY.md` (TeslaRel410) §"Anatomy of a surviving weapon — the PPC"
|
||||
Reference in New Issue
Block a user