Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc83f6ca24 | ||
|
|
42c789b993 | ||
|
|
2133abd31d |
@@ -717,6 +717,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
|
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.
|
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]
|
## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1]
|
||||||
The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4`
|
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
|
(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"
|
- "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"
|
- "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)"
|
- "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
|
# 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
|
⚠ 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.
|
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
|
## Key Relationships
|
||||||
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
|
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
|
||||||
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
|
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
|
||||||
|
|||||||
@@ -115,28 +115,6 @@ each, so they cover the foot band too, and the hit test takes the first match
|
|||||||
what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests
|
what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests
|
||||||
first-hit reachability, not mere presence.
|
first-hit reachability, not mere presence.
|
||||||
|
|
||||||
**A FLASHING radar button goes RED — `BT_RADAR_FLASH_RED` (2026-08-14, player request)
|
|
||||||
[T2 pixel-measured]. GLASS-ONLY DEVIATION (#154-class).** The radar's buttons are the yellow
|
|
||||||
class, so an alerting lamp pulsed *yellow among yellows* — a brightness change only, and the
|
|
||||||
night-16 miss that also produced the #172 ring and the protrusion bonus. The three desktop
|
|
||||||
renderers now swap the **bright** colour of a **flashing** radar button to the house red
|
|
||||||
(230,70,70 — the MFD family), so an alert reads as a *colour* change. Only the bright phase
|
|
||||||
moves; dim/off keep the yellow family, so a non-alerting radar button is unchanged. One
|
|
||||||
decode, `BTRadarLampBright(lampState)` in `L4VB16.h` beside `BTLampBrightnessOf`, consumed by
|
|
||||||
all three renderers (`CkLampColors` in the surround, `PaintGlass` in the exploded windows,
|
|
||||||
`L4PADPANEL`) so they cannot drift — the lesson of the L4RIOBANK geometry split. `=0` restores
|
|
||||||
the yellow bright. The pod's own buttons are single-colour backlit hardware and cannot do this;
|
|
||||||
the serial RIO lamp path is untouched, so this is desktop-only by construction.
|
|
||||||
|
|
||||||
**Bench hook `BT_LAMPTEST=<addr>[,<state>]` (2026-08-14).** Forces ONE lamp's state at
|
|
||||||
`PadRIO::GetLampState` — the single read point all three desktop renderers share — so lamp
|
|
||||||
rendering can be checked without inducing the fault that would raise it (a generator trip, an
|
|
||||||
ammo alarm). Default state `0x33` = the Panic lamp's alert shape (flashFast + state1Off +
|
|
||||||
state2Bright, a hard dark/bright pulse); addresses and states take `0x` hex. This is how the
|
|
||||||
red-flash change above was verified: `BT_LAMPTEST=0x1b` (GeneratorB's radar lamp, the night-16
|
|
||||||
one) then a pixel tally of the radar window — **620 house-red px with the feature on, 0 with
|
|
||||||
`BT_RADAR_FLASH_RED=0`**, and the yellow count falling by exactly that many (2820 → 2196).
|
|
||||||
|
|
||||||
## Sticky window placement — BT_GLASS_LAYOUT (2026-07-28) [T2 round-trip-verified]
|
## Sticky window placement — BT_GLASS_LAYOUT (2026-07-28) [T2 round-trip-verified]
|
||||||
|
|
||||||
The `BT_GLASS_PANELS` windows self-place in a pod-faithful ring around the main window
|
The `BT_GLASS_PANELS` windows self-place in a pod-faithful ring around the main window
|
||||||
|
|||||||
@@ -72,16 +72,13 @@ A. FIXED THIS BUILD -- PLEASE VERIFY (headliners first)
|
|||||||
The Nova spawned showing its REAR laser's range pips in the forward
|
The Nova spawned showing its REAR laser's range pips in the forward
|
||||||
view. The HUD now commits the forward view at spawn. (Only the pips
|
view. The HUD now commits the forward view at spawn. (Only the pips
|
||||||
were wrong -- rear weapons never actually fired forward.)
|
were wrong -- rear weapons never actually fired forward.)
|
||||||
#151 (Oracle) Panic eject scored -499 -- CORRECTION: -499 IS the machine
|
#151 (Oracle) Panic eject scored -499
|
||||||
Our line here was wrong (we mis-read our own ticket history; the
|
Solved, and the machine agrees with YOUR math: eject = your mech
|
||||||
correct decode was already on the ticket). Bench-verified: eject
|
self-destructs (scored as self-damage, ~-500) + the death cost
|
||||||
is scored as SUICIDE (about -999, the kill value of your own
|
(-500) = about -1000, i.e. 1000 points -> roughly ZERO. The extra
|
||||||
mech) PLUS the death cost (-500). 1000 - 999 - 500 = -499
|
-500 you saw was the #162 double-death bug, fixed on testing night.
|
||||||
exactly -- the original's own arithmetic, reproduced three times
|
TEST: repeat the maneuver -- you should land within a few points
|
||||||
on the bench, never doubled. If you eject tonight and land at
|
of zero, not hundreds below.
|
||||||
-499 again, the machine is RIGHT, not broken.
|
|
||||||
DESIGN CALL (restored): keep the authentic ~-1500 swing, or
|
|
||||||
house-rule a flat eject cost? Opinions to the thread.
|
|
||||||
FEEL Three more fixes on the original's clock (same bug class as #173):
|
FEEL Three more fixes on the original's clock (same bug class as #173):
|
||||||
the cockpit HIT-SHAKE (was bouncing ~2x too fast -- hits now rock
|
the cockpit HIT-SHAKE (was bouncing ~2x too fast -- hits now rock
|
||||||
you at the pod's pace with less rebound), the FOOTSTEP loudness
|
you at the pod's pace with less rebound), the FOOTSTEP loudness
|
||||||
@@ -203,7 +200,7 @@ G. INTERNAL ENGINEERING BACKLOG (nothing to test)
|
|||||||
|
|
||||||
=============================================================================
|
=============================================================================
|
||||||
End of list. 57 open issues total:
|
End of list. 57 open issues total:
|
||||||
16 verify (3 headliners), 12 reproduce, 6 era questions, 3 design calls,
|
17 verify (3 headliners), 12 reproduce, 6 era questions, 2 design calls,
|
||||||
10 known gaps, 11 internal -- plus 4 "not bugs" that are the real machine.
|
10 known gaps, 11 internal -- plus 4 "not bugs" that are the real machine.
|
||||||
Since Wednesday: 15 fixes fielded, 4 tickets closed by evidence alone, and
|
Since Wednesday: 15 fixes fielded, 4 tickets closed by evidence alone, and
|
||||||
Oracle's questionnaire + soundboard find settled five more. Your reports
|
Oracle's questionnaire + soundboard find settled five more. Your reports
|
||||||
|
|||||||
@@ -1472,21 +1472,14 @@ static void
|
|||||||
const GButton &b = w->buttons[i];
|
const GButton &b = w->buttons[i];
|
||||||
const RECT &r = b.rect;
|
const RECT &r = b.rect;
|
||||||
int held = (b.address == pressedAddress) || latched[b.address & 0x7F];
|
int held = (b.address == pressedAddress) || latched[b.address & 0x7F];
|
||||||
int lampState = PadRIO::GetLampState(b.address);
|
int shade = LampBrightnessOf(PadRIO::GetLampState(b.address), tick);
|
||||||
int shade = LampBrightnessOf(lampState, tick);
|
|
||||||
if (held)
|
if (held)
|
||||||
shade = 3;
|
shade = 3;
|
||||||
|
|
||||||
// A FLASHING radar button goes RED at its bright phase (L4VB16.h).
|
|
||||||
unsigned long radarBright = BTRadarLampBright(lampState);
|
|
||||||
COLORREF radarBrightRGB = RGB((radarBright >> 16) & 0xFF,
|
|
||||||
(radarBright >> 8) & 0xFF,
|
|
||||||
radarBright & 0xFF);
|
|
||||||
|
|
||||||
COLORREF fill, text_color;
|
COLORREF fill, text_color;
|
||||||
if (b.color == ClrYellow)
|
if (b.color == ClrYellow)
|
||||||
{
|
{
|
||||||
fill = (shade == 3) ? radarBrightRGB
|
fill = (shade == 3) ? RGB(245, 210, 60)
|
||||||
: (shade != 0) ? RGB(140, 118, 38)
|
: (shade != 0) ? RGB(140, 118, 38)
|
||||||
: RGB(70, 60, 24);
|
: RGB(70, 60, 24);
|
||||||
text_color = RGB(0, 0, 0);
|
text_color = RGB(0, 0, 0);
|
||||||
@@ -1508,7 +1501,7 @@ static void
|
|||||||
// Solid lamp fill + a bright border. Under the imagery, only the protruding
|
// Solid lamp fill + a bright border. Under the imagery, only the protruding
|
||||||
// edge shows once the surface is blitted on top.
|
// edge shows once the surface is blitted on top.
|
||||||
{
|
{
|
||||||
COLORREF bright = (b.color == ClrYellow) ? radarBrightRGB
|
COLORREF bright = (b.color == ClrYellow) ? RGB(245, 210, 60)
|
||||||
: (b.color == ClrBlue) ? RGB(205, 228, 255)
|
: (b.color == ClrBlue) ? RGB(205, 228, 255)
|
||||||
: RGB(230, 70, 70);
|
: RGB(230, 70, 70);
|
||||||
HBRUSH face = CreateSolidBrush(fill);
|
HBRUSH face = CreateSolidBrush(fill);
|
||||||
|
|||||||
@@ -829,13 +829,18 @@ void
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case scrambleVideo:
|
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;
|
scrambleVideoFlag = True;
|
||||||
scrambleVideoTimeout = ((Scalar)Now()) + duration;
|
scrambleVideoTimeout = ((Scalar)Now()) + duration;
|
||||||
|
|
||||||
Check(graphicsDisplay);
|
Check(graphicsDisplay);
|
||||||
((SVGA16*) graphicsDisplay)->FunkyVideo(True);
|
((SVGA16*) graphicsDisplay)->FunkyVideo(True, duration); // duration -> the recovery envelope
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -290,21 +290,14 @@ static void
|
|||||||
// commanded lamp state (with flash phase) decides.
|
// commanded lamp state (with flash phase) decides.
|
||||||
//
|
//
|
||||||
int shade = 0;
|
int shade = 0;
|
||||||
int lampState = 0;
|
|
||||||
if (lamp_capable)
|
if (lamp_capable)
|
||||||
{
|
{
|
||||||
lampState = PadRIO::GetLampState(cell.address);
|
shade = LampBrightnessOf(PadRIO::GetLampState(cell.address), tick);
|
||||||
shade = LampBrightnessOf(lampState, tick);
|
|
||||||
}
|
}
|
||||||
if (held)
|
if (held)
|
||||||
{
|
{
|
||||||
shade = 3;
|
shade = 3;
|
||||||
}
|
}
|
||||||
// A FLASHING radar/Sec button goes RED at its bright phase (L4VB16.h).
|
|
||||||
unsigned long radarBright = BTRadarLampBright(lampState);
|
|
||||||
COLORREF radarBrightRGB = RGB((radarBright >> 16) & 0xFF,
|
|
||||||
(radarBright >> 8) & 0xFF,
|
|
||||||
radarBright & 0xFF);
|
|
||||||
|
|
||||||
COLORREF fill;
|
COLORREF fill;
|
||||||
COLORREF text_color;
|
COLORREF text_color;
|
||||||
@@ -315,7 +308,7 @@ static void
|
|||||||
}
|
}
|
||||||
else if (yellow)
|
else if (yellow)
|
||||||
{
|
{
|
||||||
fill = (shade == 3) ? radarBrightRGB
|
fill = (shade == 3) ? RGB(245, 210, 60)
|
||||||
: (shade != 0) ? RGB(140, 118, 38)
|
: (shade != 0) ? RGB(140, 118, 38)
|
||||||
: RGB(70, 60, 24);
|
: RGB(70, 60, 24);
|
||||||
text_color = RGB(0, 0, 0);
|
text_color = RGB(0, 0, 0);
|
||||||
|
|||||||
@@ -1282,30 +1282,6 @@ int BTPadRIOActive(void)
|
|||||||
int
|
int
|
||||||
PadRIO::GetLampState(int unit)
|
PadRIO::GetLampState(int unit)
|
||||||
{
|
{
|
||||||
//
|
|
||||||
// BENCH HOOK (2026-08-14): BT_LAMPTEST=<addr>[,<state>] forces ONE lamp's
|
|
||||||
// state, so the desktop lamp rendering can be checked without inducing the
|
|
||||||
// fault that would raise it (a generator trip, an ammo alarm, ...). The
|
|
||||||
// default state is the alert shape the Panic lamp uses -- flashFast +
|
|
||||||
// state1Off + state2Bright (0x33), a hard dark/bright pulse. Addresses and
|
|
||||||
// states take 0x hex. Read once; every desktop renderer reads its lamps
|
|
||||||
// through here, so one hook covers the surround, the exploded windows and
|
|
||||||
// the pad panel alike.
|
|
||||||
//
|
|
||||||
static int s_testAddr = -2;
|
|
||||||
static int s_testState = 0x33;
|
|
||||||
if (s_testAddr == -2)
|
|
||||||
{
|
|
||||||
s_testAddr = -1;
|
|
||||||
const char *e = getenv("BT_LAMPTEST");
|
|
||||||
if (e != NULL)
|
|
||||||
sscanf(e, "%i,%i", &s_testAddr, &s_testState);
|
|
||||||
}
|
|
||||||
if (s_testAddr >= 0 && unit == s_testAddr)
|
|
||||||
{
|
|
||||||
return s_testState;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeInstance == NULL || unit < 0 || unit >= LampCount)
|
if (activeInstance == NULL || unit < 0 || unit >= LampCount)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+240
-23
@@ -615,17 +615,41 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
|
|||||||
Word *source = pixelBuffer.Data.MapPointer;
|
Word *source = pixelBuffer.Data.MapPointer;
|
||||||
Word *dest = (Word*)rect.pBits;
|
Word *dest = (Word*)rect.pBits;
|
||||||
int postRowIncrement = (rect.Pitch / 2) - w;
|
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)
|
if (monoTint < 0)
|
||||||
{
|
{
|
||||||
// PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0).
|
// PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0).
|
||||||
SVGA16Palette *pal = &palette[paletteID];
|
SVGA16Palette *pal = &palette[paletteID];
|
||||||
for (int y = 0; y < h; y++)
|
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++)
|
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 = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3);
|
||||||
dest++; source++;
|
dest++;
|
||||||
}
|
}
|
||||||
dest += postRowIncrement;
|
dest += postRowIncrement;
|
||||||
}
|
}
|
||||||
@@ -637,10 +661,27 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
|
|||||||
Word tint = (Word) monoTint;
|
Word tint = (Word) monoTint;
|
||||||
for (int y = 0; y < h; y++)
|
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++)
|
for (int x = 0; x < w; x++)
|
||||||
{
|
{
|
||||||
*dest = (*source & mask) ? tint : 0;
|
Word px;
|
||||||
dest++; source++;
|
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;
|
dest += postRowIncrement;
|
||||||
}
|
}
|
||||||
@@ -734,16 +775,38 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
|
|||||||
Word *base = pixelBuffer.Data.MapPointer;
|
Word *base = pixelBuffer.Data.MapPointer;
|
||||||
SVGA16Palette *pal = &palette[paletteID];
|
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)
|
if (rotateQuadrant == 0)
|
||||||
{
|
{
|
||||||
// Native orientation, top-down straight copy.
|
// Native orientation, top-down straight copy.
|
||||||
for (int y = 0; y < h; y++)
|
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;
|
unsigned long *d = dst + y * w;
|
||||||
|
double rowScroll = roff + shr * (double)y;
|
||||||
for (int x = 0; x < w; x++)
|
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)
|
if (monoTint < 0)
|
||||||
{
|
{
|
||||||
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
|
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
|
||||||
@@ -791,6 +854,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,
|
// 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).
|
// 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;
|
int ow = h, oh = w;
|
||||||
for (int oy = 0; oy < oh; oy++)
|
for (int oy = 0; oy < oh; oy++)
|
||||||
{
|
{
|
||||||
@@ -808,7 +873,21 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
|
|||||||
sx = w - 1 - oy;
|
sx = w - 1 - oy;
|
||||||
sy = ox;
|
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)
|
if (monoTint < 0)
|
||||||
{
|
{
|
||||||
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
|
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
|
||||||
@@ -1028,17 +1107,12 @@ static void CkFill(LPDIRECT3DDEVICE9 dev, int x, int y, int w, int h, D3DCOLOR c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lamp fill + border for a button, from the glass PaintGlass color ramps.
|
// Lamp fill + border for a button, from the glass PaintGlass color ramps.
|
||||||
static void CkLampColors(int colorClass, int shade, int inert, D3DCOLOR *fill, D3DCOLOR *border,
|
static void CkLampColors(int colorClass, int shade, int inert, D3DCOLOR *fill, D3DCOLOR *border)
|
||||||
int lampState = 0)
|
|
||||||
{
|
{
|
||||||
if (colorClass == 1) // yellow (radar) -- RED while flashing (see L4VB16.h)
|
if (colorClass == 1) // yellow (radar)
|
||||||
{
|
{
|
||||||
unsigned long bright = BTRadarLampBright(lampState);
|
*border = D3DCOLOR_XRGB(245, 210, 60);
|
||||||
D3DCOLOR brightC = D3DCOLOR_XRGB((int)((bright >> 16) & 0xFF),
|
*fill = (shade == 3) ? D3DCOLOR_XRGB(245, 210, 60)
|
||||||
(int)((bright >> 8) & 0xFF),
|
|
||||||
(int)( bright & 0xFF));
|
|
||||||
*border = brightC;
|
|
||||||
*fill = (shade == 3) ? brightC
|
|
||||||
: (shade != 0) ? D3DCOLOR_XRGB(140, 118, 38)
|
: (shade != 0) ? D3DCOLOR_XRGB(140, 118, 38)
|
||||||
: D3DCOLOR_XRGB(70, 60, 24);
|
: D3DCOLOR_XRGB(70, 60, 24);
|
||||||
}
|
}
|
||||||
@@ -1223,7 +1297,7 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
|
|||||||
int shade = BTLampBrightnessOf(state, tick);
|
int shade = BTLampBrightnessOf(state, tick);
|
||||||
if (shade < 2) continue; // dark half-cycle: no ring
|
if (shade < 2) continue; // dark half-cycle: no ring
|
||||||
D3DCOLOR fill, border;
|
D3DCOLOR fill, border;
|
||||||
CkLampColors(b.colorClass, 3 /*bright*/, b.inert, &fill, &border, state);
|
CkLampColors(b.colorClass, 3 /*bright*/, b.inert, &fill, &border);
|
||||||
int rx = b.x - kRingGap - kRingWidth;
|
int rx = b.x - kRingGap - kRingWidth;
|
||||||
int ry = b.y - kRingGap - kRingWidth;
|
int ry = b.y - kRingGap - kRingWidth;
|
||||||
int rw = b.w + 2 * (kRingGap + kRingWidth);
|
int rw = b.w + 2 * (kRingGap + kRingWidth);
|
||||||
@@ -1244,7 +1318,7 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
|
|||||||
int shade = BTLampBrightnessOf(state, tick);
|
int shade = BTLampBrightnessOf(state, tick);
|
||||||
if (gCkPressed == b.address || gCkLatched[b.address & 0x7F]) shade = 3;
|
if (gCkPressed == b.address || gCkLatched[b.address & 0x7F]) shade = 3;
|
||||||
D3DCOLOR fill, border;
|
D3DCOLOR fill, border;
|
||||||
CkLampColors(b.colorClass, shade, b.inert, &fill, &border, state);
|
CkLampColors(b.colorClass, shade, b.inert, &fill, &border);
|
||||||
CkFill(device, b.x, b.y, b.w, b.h, border);
|
CkFill(device, b.x, b.y, b.w, b.h, border);
|
||||||
CkFill(device, b.x + 1, b.y + 1, b.w - 2, b.h - 2, fill);
|
CkFill(device, b.x + 1, b.y + 1, b.w - 2, b.h - 2, fill);
|
||||||
|
|
||||||
@@ -5549,6 +5623,9 @@ SVGA16::SVGA16(
|
|||||||
for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw
|
for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw
|
||||||
mDevSurfaceTex[_i] = NULL;
|
mDevSurfaceTex[_i] = NULL;
|
||||||
paletteGeneration = 0; // GLASS dirty-skip palette tracking
|
paletteGeneration = 0; // GLASS dirty-skip palette tracking
|
||||||
|
scrambleActive = False; // PPC sync-scramble (phase-14) -- armed by FunkyVideo
|
||||||
|
scrambleStartMs = 0;
|
||||||
|
scrambleDurationS = 0.0;
|
||||||
//STUBBED: VIDEO RB 1/15/07
|
//STUBBED: VIDEO RB 1/15/07
|
||||||
# if defined(DEBUG)
|
# if defined(DEBUG)
|
||||||
Tell("SVGA16::SVGA16()\n");
|
Tell("SVGA16::SVGA16()\n");
|
||||||
@@ -6508,13 +6585,153 @@ void
|
|||||||
Check_Fpu();
|
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
|
void
|
||||||
SVGA16::FunkyVideo(Logical on_off)
|
SVGA16::FunkyVideo(Logical on_off, Scalar duration)
|
||||||
{
|
{
|
||||||
//STUBBED: VIDEO RB 1/15/07
|
if (on_off)
|
||||||
//Check(this);
|
{
|
||||||
//SVGAFunkyVideo(on_off);
|
scrambleActive = True;
|
||||||
//Check_Fpu();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################################
|
//########################################################################
|
||||||
|
|||||||
+21
-41
@@ -110,46 +110,6 @@ inline int BTLampBrightnessOf(int state, unsigned long tick)
|
|||||||
return ((tick / half) & 1) ? level2 : level1;
|
return ((tick / half) & 1) ? level2 : level1;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Is this lamp FLASHING rather than solid? Bits 0-1 of the wire word are the
|
|
||||||
// flash mode (0 = solid, 1/2/3 = slow/med/fast), so any non-zero value means
|
|
||||||
// the lamp is pulsing -- which is the pod's own way of saying ALERT.
|
|
||||||
//
|
|
||||||
// RADAR FLASH = RED (2026-08-14, player request; GLASS-ONLY DEVIATION). The
|
|
||||||
// radar's buttons are the yellow class, and a yellow lamp pulsing among other
|
|
||||||
// yellow lamps is easy to miss -- the night-16 miss that also produced the
|
|
||||||
// #172 ring and the protrusion bonus. The desktop renderers therefore swap
|
|
||||||
// the BRIGHT colour of a FLASHING radar button to the red family, so an alert
|
|
||||||
// reads as a colour change and not just a brightness change. The pod's own
|
|
||||||
// buttons are physical bicolour-less lamps and cannot do this, so it is a
|
|
||||||
// desktop affordance only -- and it is confined to the three desktop
|
|
||||||
// renderers (surround, exploded windows, pad panel); the serial RIO lamp
|
|
||||||
// path is untouched. BT_RADAR_FLASH_RED=0 restores the yellow bright.
|
|
||||||
//
|
|
||||||
inline int BTLampIsFlashing(int state)
|
|
||||||
{
|
|
||||||
return (state & 0x3) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// The bright colour a radar (yellow-class) button should use right now: the
|
|
||||||
// red family while the lamp is flashing, its own yellow otherwise. ONE
|
|
||||||
// definition, consumed by all three desktop renderers so they cannot drift
|
|
||||||
// (the lesson of the L4RIOBANK geometry split). Returns 0x00RRGGBB.
|
|
||||||
//
|
|
||||||
inline unsigned long BTRadarLampBright(int lampState)
|
|
||||||
{
|
|
||||||
static int enabled = -1;
|
|
||||||
if (enabled < 0)
|
|
||||||
{
|
|
||||||
const char *e = getenv("BT_RADAR_FLASH_RED");
|
|
||||||
enabled = (e != 0 && e[0] == '0') ? 0 : 1; // default ON
|
|
||||||
}
|
|
||||||
return (enabled && BTLampIsFlashing(lampState))
|
|
||||||
? 0x00E64646ul // 230,70,70 -- the house red (MFD family)
|
|
||||||
: 0x00F5D23Cul; // 245,210,60 -- the radar's own yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
//########################################################################
|
//########################################################################
|
||||||
//######################### Video16BitBuffered ###########################
|
//######################### Video16BitBuffered ###########################
|
||||||
//########################################################################
|
//########################################################################
|
||||||
@@ -372,7 +332,27 @@ public:
|
|||||||
UnflashPalette(int palette_number);
|
UnflashPalette(int palette_number);
|
||||||
|
|
||||||
void
|
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:
|
protected:
|
||||||
|
|
||||||
|
|||||||
@@ -394,14 +394,6 @@ static const char *kEnvironIniDefault =
|
|||||||
"# Phosphor colour of the mono MFDs, RRGGBB. Default is a green tube.\n"
|
"# Phosphor colour of the mono MFDs, RRGGBB. Default is a green tube.\n"
|
||||||
"#BT_COCKPIT_TINT=21FF42\n"
|
"#BT_COCKPIT_TINT=21FF42\n"
|
||||||
"\n"
|
"\n"
|
||||||
"# A FLASHING button on the radar turns RED instead of yellow. The radar's\n"
|
|
||||||
"# buttons are all yellow, so a lamp calling for attention used to change only\n"
|
|
||||||
"# in BRIGHTNESS -- easy to miss with eleven other yellow buttons around it.\n"
|
|
||||||
"# Now the bright half of its flash is red, so an alert changes COLOUR. Only\n"
|
|
||||||
"# a flashing button is affected: a radar button doing nothing looks exactly as\n"
|
|
||||||
"# it always did. 1 = on (the default), 0 = keep the yellow.\n"
|
|
||||||
"#BT_RADAR_FLASH_RED=0\n"
|
|
||||||
"\n"
|
|
||||||
"# ---- Display ----------------------------------------------------------------\n"
|
"# ---- Display ----------------------------------------------------------------\n"
|
||||||
"\n"
|
"\n"
|
||||||
"# The cockpit is fitted into the window at ONE uniform scale, centred, with\n"
|
"# The cockpit is fitted into the window at ONE uniform scale, centred, with\n"
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
|
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
|
||||||
// for the same reason.
|
// for the same reason.
|
||||||
#include "dmgtable.hpp"
|
#include "dmgtable.hpp"
|
||||||
|
#include <GAUGREND.hpp> // GaugeRenderer::SpecialEffect (virtual) -- PPC scramble trigger
|
||||||
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
|
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
|
||||||
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
|
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
|
||||||
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
|
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
|
||||||
@@ -1198,6 +1199,31 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
reportZone = message->damageZone; // post-resolve (@0x4a0396 write)
|
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
|
// #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which
|
||||||
// REPLACES the engine-base single application. Three things the base
|
// 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