Author SHA1 Message Date
CydandClaude Opus 4.8 d0e29a4d97 radar: composite the OVERLAY plane -- SECTOR/SCALE were blank on every desktop path
Playtester DOS-binary comparison: our radar showed blank SECTOR:/SCALE: values.
Root cause [T1]: the secondary CRT's low byte is authored as TWO ports
(L4GAUGE.CFG:4395-4396) -- sec (0x3F, btspal) + overlay (0xC0, btopal,
TransparentZero) carrying the btsec1ov graticule, the SCALE numeral
(RadarRange) and the SECTOR numerals (sectorDisplay).  Both numeral writers
are reconstructed and RUN -- their pixels were in the shared buffer's 0xC0
bits all along -- but every desktop expand indexed the LUT with pixel & 0x3F,
stripping them.  The arcade DAC indexed the FULL low byte.

Key mechanism: clut0 ALREADY holds the DAC's combined 256-entry table --
BuildSecondaryPalette (AllChannels, sec) fills all 256 indices and the
overlay's TransparentZero pass overwrites groups 0x40/0x80/0xC0 -- so the
composite is exactly a mask change, no blending code:

  - BTSecCompositeMask(): sec->GetBitMask() | overlay's (NULL-guarded, same
    display), used by BTDrawGaugeSurfaces + BTDrawCockpitPanels (surround),
    SVGA16::Update case 0 (pod parity), and the glass BlitSurface palette
    expand.
  - GlassWindowToken: the palette-expanded radar window's dirty token now
    includes the overlay port -- the SECTOR numerals tick on that plane, and
    a pixel-only 0x3F token would never repaint them.

The overlay's ColorMapper re-stomp over combined indexes is authentic (the
shipped machine ran the identical palette code) and is left alone.

Verified live (glass panels): SECTOR: 497.503 ticking with movement,
SCALE: 1000 at spawn (radarRange init 250*2^2 -- the DOS reference's 4000 is
max zoom, not a delta), and the btsec1ov graticule rules now draw.  KB:
GAUGE_COMPOSITE row 28 + polish list updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 07:18:43 -05:00
19 changed files with 141 additions and 2369 deletions
-10
View File
@@ -61,13 +61,3 @@ content/*.log
content/matchlog_*.txt
content/btl4.exe
content/*.EGG
# scratchpad: session working files -- NEVER bulk-add (the 2026-08-13 6a744ef
# incident: a stray `git add scratchpad` pushed a session handoff carrying the
# gitea credential). Bench .sh scripts are added INDIVIDUALLY by full path.
scratchpad/**/*.out
scratchpad/**/*.exe
scratchpad/**/*.dll
scratchpad/**/SESSION_HANDOFF*.md
scratchpad/**/AGENT_FINDINGS*.md
scratchpad/**/message*.txt
+8 -17
View File
@@ -199,23 +199,14 @@ Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0` — no jointtorso in the
boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
dist < (1 hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap.
~~`weaponRange` is fire-control data only~~ **RETRACTED 2026-08-13 (the #171 hunt): that
was MISSILE-only truth mis-generalized to ballistics.** The AC's own FireWeapon @004bc104
(recovered by the 2026-08-06 re-export; #168 worked from a stale "not recovered" banner and
generalized from the missile launcher) is TRIGGER-TIME HITSCAN: batched rounds, dist vs LIVE
effectiveRange at fire time (the same gate energy uses), ONE single-panel damage message to
the locked target, nothing spawned (the shell class 0xBD1 is dead code; the visible round is
the 0xBCD renderer tracer). So: an AFC50 landing hits beyond its authored range is a PORT
DEVIATION, not arcade behavior; "a locked AC could not miss a mover" IS the arcade behavior
(#168's "stale-point auto-hit" kill removed an authentic design while fixing its stale-point
detail). Missiles: genuinely flying + seeker-led (the machine leads for the player), 4.0u
proximity fuze, swept contact, salvo randomization — the #168 flight direction stands THERE.
Gauss CORRECTED (2026-08-13 audit): the FIELDED Gauss record is CLASS 0xBCD (ProjectileWeapon)
— full AC hitscan WITH damage; port agrees. GaussRifle 0xBCE = dead code both sides (zero
shipped records). The earlier no-damage claim was class-level truth mis-applied to the field.
#112 (AFC multi-zone shotgun) is the port's per-round scatter vs the binary's one batched
message. Full chain + evidence: [[decomp-reference]] §5 (corrected); port fix pending the
#171 group decision (Oracle's HOLD).
`weaponRange` is **fire-control data only** (#168 audit [T1]): projectile FLIGHT is never
range-capped — a plain shell (AFC) lives exactly 5.0 s (@4bddec) and a Missile lives
BurnTime+10 s with a y<1 kill-plane (@4bef78); FireWeapon @4bcc60 has no range gate, so an
AFC50 (authored range 750) landing hits at 2000+ u is AUTHENTIC arcade behavior. Constants +
the whole flight/expiry chain: [[decomp-reference]] §5 "Projectile/Missile flight & expiry".
The same audit killed the pool's stale-point auto-hit (unguided contact was tested against
the fire-time pick, so a locked AC shell could not miss a moving target — fixed 2026-08-11:
the contact test now tracks the target's live position on every round kind).
**`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT
heatLoad** — the port originally computed `effectiveRange = (1 heatLoad) × weaponRange`
(mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in
+9 -50
View File
@@ -253,56 +253,20 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
@004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30
(generator client capacity @0x1e4/count @0x1e8).
### Projectile/Missile flight & expiry (#168 audit 2026-08-11; ⚠ CORRECTED by the #171 hunt 2026-08-13) [T1]
- **#168's "no range cap / WeaponRange is fire-control only" was MISSILE-ONLY truth,
mis-generalized.** The AC's own FireWeapon @004bc104 (recovered by the 2026-08-06 re-export;
the old projweap.cpp "body NOT recovered" banner was stale) IS range-gated AND hitscan:
batch counter +0x408, every TracerInterval-th (+0x438) round → dist = |aimPoint(+0x37c)
muzzle| vs LIVE effectiveRange +0x328 (dynamic: WeaponRange × (K owningZone structure),
recomputed @004b9bdc; K @_DAT_004b9c98 presumed 1.0 [T4-unread]) → ONE single-panel Damage
(batchedCount × DamageAmount) to the LOCKED target *(mech+0x388) via @004b9728 AT FIRE TIME.
Out of range = batch silently discarded (tracer still renders). The visible AC round is the
0xBCD renderer TRACER (tracerModel+0x41C, TOF @004bc06c) — cosmetic. MissileLauncher
@004bcc60 (no fire-time damage, no range read) is the genuinely-flying family.
**GAUSS CORRECTED (weapons audit 2026-08-13): the FIELDED Gauss is a 0xBCD ProjectileWeapon
record** (BTL4.RES @0xf16e6, bytes cd0b verified raw — 20 pts Ballistic, 900 range, 8s cycle,
16 rounds, yellow pip) — it runs the FULL AC hitscan chain WITH damage; port and binary agree.
The GaussRifle class 0xBCE (@004bdca4 charge-dump-only FireWeapon) + shell 0xBD1 are real code
with ZERO shipped content records — dead on both sides. (The earlier "port Gauss damage is an
invention" claim was wrong at the fielded level and is retracted.)
**Every FireWeapon (all classes) requires a LOCKED target**: both sims gate the trigger on
*(mech+0x388) != 0 — no lock, no fire [T1].
- **Plain Projectile (shell) class 0xBD1 is DEAD CODE in the shipped binary**: 0xbd1 appears
exactly once in the whole export (the DefaultData lookup @004c1590); Make @004be384 has zero
code callers; the fully-recovered AC fire path spawns nothing. Its flight body @4bddec
(below) is a complete never-instantiated Model-B implementation — its 5.0s clock is real
code that nothing shipped ever runs. [T1; 6.5% export-gap caveat]
- **Plain Projectile (dead class) integrator @4bddec** (master); expiry: age since
### Projectile/Missile flight & expiry (#168 audit, 2026-08-11) [T1]
- **No range cap exists anywhere in the flight chain.** WeaponRange feeds fire-control only
(effectiveRange @0x328 = (1hostZoneDamage)×WeaponRange — targeting/reticle "in range" call).
FireWeapon @4bcc60 reads no +0x328: the trigger fires at any target distance.
- **Plain Projectile (AFC shell) live integrator @4bddec** (master); expiry: age since
spawn(+0x19c) > `_DAT_004be054` = **5.0 s** → FUN_0042061c kill. Dead-reckon divergence²
vs predicted pos(+0x260, predictor FUN_00422060 via +0x254) > `_DAT_004be050` = 0.1 →
updateModel(+0x18)|=1 (ForceUpdate, replication-dirty — NOT a kill). Contact: swept test
@0042291c per tick; on hit → explosion msg 0x5C at live pos + direct TakeDamage (zone 1)
to the struck entity via @004be078; at EXPIRY silent removal, no damage.
updateModel(+0x18)|=1 (ForceUpdate, replication-dirty — NOT a kill).
- **Missile live integrator @4bef78** (master); expiry: age > burnTime(+0x340, model rec
+0x44) + `_DAT_004bf5ac` = **10.0 s** coast margin, OR pos.y < `_DAT_004bf5b0` = **1.0**
kill-plane → FUN_0042061c. Keepalive: no update for `_DAT_004bf594`=2.0 s → dirty bit.
**Proximity fuse**: seeker rangeToTarget(+0x10C) < `_DAT_004bf5a4` = **4.0** → detonate on
targetEntity without a geometry hit. Seeker drops a destroyed target (movementMode 2|9,
FUN_0049fb54).
- **Missile FLIGHT MODEL (velocity/dt audit 2026-08-13) [T1]:** per frame:
`MissileThrusterSimulation` **@004be474** (the standalone Performance body — recovered;
misthrst.cpp's "folded" note was stale) slerps the missile toward the seeker aim
≤ MaxThrusterRotationRate·dt and, while burning, adds `(0,0,ThrusterAccel)` to the
frame-zeroed accumulator; @4bef78 adds quadratic drag `COD·ρ·sign(v)·v²` per body axis
(burn AND coast; the dead plain-Projectile @4bddec uses power **1.0** via
ApplyAirResistanceAndGravity(1.0)), zeroes LATERAL local vel/accel while burning (velocity
slaved to the nose; slaving STOPS at burnout), applies gravity (env, default **6.5**) ONLY
when coasting, then integrates ×dt (@00421bac). Net: `dv/dt = T COD·v²`. **Authored
CODs = 0.001 ALL axes, all four models** (BTL4.RES type-15 rid 315 srm / 319 lrm /
323 strk / 327 nrk, raw floats); ctor @4bf5b4 (part_013.c:18416) precomputes **terminal
speed `sqrt(T/negCOD.z)` → missile+0x348** (the seeker lead-time base): LRM/Streak/NRK
**547.7 u/s**, SRM **774.6**. Port pool carries the same ODE since the 2026-08-13 drag
fix (mech4.cpp; drag was previously omitted as "negligible" — it is the speed governor).
- **Performance split** (Projectile ctor @4be1bc / Missile ctor @4bf5b4): instance
(flags&0xC)==ReplicantInstance(4) → FUN_004221c0 dead-reckoning smoother (no physics);
master → 4bddec/4bef78. (CLASSMAP's old authoritative/ghost labels were swapped —
@@ -617,14 +581,9 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
(`FUN_0041bd98`) — **VARIABLE-STEP, slice in real SECONDS**, line-for-line the
WinTesla SIMULATE.cpp body. Corollaries: every Performance's `time_slice` is
seconds in BOTH binary and port (no hidden tick/second unit gap); a dt-LESS
per-frame term into PERSISTENT state (myomer kinetic @4b8d18's middle term)
fires at the FRAME cadence = the tick rate, 28 Hz nominal, sagging under load.
⚠ The Mover gravity `-= **(mover+0x250)` @0x421e77 is NOT in that class
(corrected 2026-08-13, velocity/dt audit): `Mover::PerformAndWatch` =
**`FUN_00422360`** zeroes `localAcceleration` every frame (== T0
MOVER.cpp:672), so gravity/drag/thrust adds define a fresh per-frame
ACCELERATION integrated ×dt by `ApplyWorldAccelerations` @00421bac
frame-rate-independent, no 28 Hz rescale. Mover velocity is u/s (the replicant
per-frame term (myomer kinetic @4b8d18's middle term; the Mover gravity
`-= **(mover+0x250)` @0x421e77) fires at the FRAME cadence = the tick rate,
28 Hz nominal, sagging under load. Mover velocity is u/s (the replicant
dead-reckoner @0x421f7c does `pos += vel × ticksΔ/DAT_0052140c`). The myomer
heat term's ONE caller is the **unexported MyomersSimulation body
@0x4b8b9a-0x4b8d0d** (E8-scan + raw disasm; the export gap hid it): calls
+7 -15
View File
@@ -194,8 +194,7 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
- **Environment gravity unwired — the myomer CLIMB-heat term is inert (found 2026-07-31, #85).**
`Mover::localEnvironment` (MOVER.h:271) is declared and never populated anywhere in the port;
`GetEnvironment()->gravityConstant` is the named analog of the original Mover's gravity POINTER at
`+0x250` = `localEnvironment` (MOVER.h; `FUN_00421e2c` = ApplyAirResistanceAndGravity does
`worldAccel.y -= **(+0x250)` each frame onto the frame-zeroed accumulator), which the myomer drive-heat
`+0x250` (`FUN_00421e2c` does `vy -= **(+0x250)` per tick), which the myomer drive-heat
integrator uses for its `m·g·|vy|·dt` climb-work term. The bridge (`BTMechMyomerMotionSample`)
null-guards it, so climbing currently generates no myomer heat (flat maps: moot; cavern slopes:
a missing cost). EnvironmentZone resources (RES type 23) exist in BTL4.RES — wiring them (or at
@@ -716,19 +715,12 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
the Mover holds to the live gravity cell). Harmless on flat ground (|vy|≈0 anyway) but the
climb-work heat never accrues on slopes. Find where our Environment gravity actually lives
and re-point the bridge. [T2 measured]
- **Engine Mover gravity needs NO 28Hz rescale — CLOSED 2026-08-13 (velocity/dt audit) [T1/T0].**
The worry was that the per-frame `worldAccel.y -= **(+0x250)` (@0x421e77, no dt) would scale
with our frame rate. It cannot: **`Mover::PerformAndWatch` = `FUN_00422360` ZEROES
`localAcceleration` at the top of every frame** (`FUN_0040a7f4(+0x1dc, &DAT_004e0fd4)` ==
T0 MOVER.cpp:672 `localAcceleration = Motion::Identity`), so gravity/drag/thrust adds are
per-frame *acceleration definitions* on a fresh accumulator — a constant `a` integrated ×dt
by `ApplyWorldAccelerations` @00421bac (`v += a·dt`), correct at ANY frame rate. The port's
linked MOVER.cpp has the identical structure. (Distinct from the myomer per-tick accumulates,
which land in PERSISTENT state and did need the ×(dt·28) fix.) The separate gap — the port's
`GetEnvironment()->gravityConstant` sampling 0 (unwired Environment, entry above) — is still
open; note the target constant is **6.5** (LATTICE.cpp:413 default), not 9.8 (9.8 appears only
in the AC tracer fire-control math @0x4bc67c). Full audit:
`docs/WEAPONS_DRIFT_AUDIT.md` §VELOCITY/DT INTEGRATION.
- **Engine Mover gravity is dt-LESS per-frame in the binary** (`@0x421e77: vy -= **(+0x250)`,
raw, once per Perform — same class as the myomer kinetic term). If our linked WinTesla
MOVER.cpp kept that form, gravity acceleration in the port scales with OUR ~59Hz frame rate
(≈2.1× the pod's 28) — jump arcs, falls, missile droop all stiffer than the pod. CHECK the
WinTesla source's gravity line; if per-frame, it needs the same 28Hz reference-rate
normalization the myomer kinetic term got. [T1 binary side / T4 port impact until read]
- **Gait-noise phantom acceleration feeds the myomer accel term** (`[myoheat] a=4..33` at
steady cruise — our per-frame velocity re-derivation jitters; the binary read the same
`AccelerationLastFrame` cell off ITS OWN gait, noise level unknown). Bounded contributor
-8
View File
@@ -635,14 +635,6 @@ CONCLUSION: the coolant-loop reconstruction is faithful; every difference is 4.0
(loadout rework on Loki, small-laser redistribution elsewhere), NOT a bug. `BT_SPEC_LOG` (mech4)
re-dumps on demand.
### Weapon-range cross-check RESULT (2026-08-13) [T1] — manual prints only 4 range figures
The manual has NO weapons table and prints NO per-weapon damage number anywhere; the per-mech
stat sheets carry no range/damage column. The only printed ranges are MFD screenshot mockups:
PPC 900M (p20) and AFC100 400M (p21) MATCH the shipped 4.10 content; SRM4/SRM6 450M (p22/p23)
vs the shipped 800 is the same 4.0→4.10 drift class as the Loki rework (and missile WeaponRange
is display-only — no physics change either way). Full table + the rangefinder-scale verdict:
`docs/WEAPONS_DRIFT_AUDIT.md` §RANGE AUTHENTICITY.
## Key Relationships
- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view).
- Input: [[locomotion]] (the mapper).
+1 -4
View File
@@ -122,10 +122,7 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
(mech4.cpp): `+0x1C4` vec = `localVelocity.linearMotion`; `+0x82C` vec =
`localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame);
`+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong);
`*(+0x250)` = the environment **gravity pointer** (= `Mover::localEnvironment`, MOVER.h;
`FUN_00421e2c` = ApplyAirResistanceAndGravity does `worldAccel.y -= **(+0x250)` each frame —
onto an accumulator `Mover::PerformAndWatch` @00422360 zeroes every frame, so it is a constant
acceleration integrated ×dt, NOT an accumulating per-tick term; velocity/dt audit 2026-08-13).
`*(+0x250)` = the environment **gravity pointer** (`FUN_00421e2c` does `vy -= **(+0x250)`/tick).
Physics: `heat += ratio²·(1+dmg)·[0.005·½mv² + 0.2·m·g·|vy|·dt + 0.2·m|v||a|·dt]` — kinetic work
+ climb power + acceleration power. The binary `fabs`es v.y (the port's old draft didn't).
**Authored tuning extracted from BTL4.RES** (all 18 mech variants share one record):
-26
View File
@@ -234,29 +234,3 @@ and asserts [crit] DESTROYED + [techstat] X + zero named FIRED after + REFUSED>0
fires; P2 partial-crits to 0.5 and asserts the weapon KEEPS firing (freeze regression, the #164
upstream writer). LESSON (gotcha SS30): BT_KILL_SUBSYS writes the cells directly and can only
validate READERS -- state benches must drive the real writer path.
## weapons_sweep (night16) -- the all-families weapons bench (#171/#174/lane-12, PASS 2026-08-13)
`scratchpad/night16/weapons_sweep.sh` + `weapons_check.py`: ONE 2-node bench proving every
weapons family on one tree. Rig: **A=mad2 "Zanin Neko"** (AFC50 + ERPPC + LRM15 + SRM6 x2 +
ERMLaser x2 -- all three families on one chassis, the AC carrier per L4GAUGE.CFG) vs **B=madcat**
(AFC100 + LRM15 x2 + ER lasers), grass/day, `BT_GOTO=enemy` + throttled BT_AUTOFIRE +
BT_AF_MISSILE (periods 4/7), 300s, env `BT_DMG_LOG BT_PROJ_LOG BT_DEATH_LOG BT_AMMO_LOG`.
Asserts per family: energy named [emitter] FIRED + type-3/4 dmghits; AC type=1 dmghits ALL
burst=1 + ballistic (guided=0) PUSHes ALL dmg=0 (#171 hitscan; damage DETs reconciled to guided
pushes per log); missiles [ammo] FIRED + IMPACT dmg>0 with burst>1 + type=2 dmghits (contact
through the 4.0u fuze at drag-governed speeds); #174 cascade-per-life<=1; bystander receipts
individually classified; hard rig floors. `--selftest` feeds the detector pre-#171/pre-#174
shapes + bystander false-positive shapes and requires each to FAIL (run first, gated).
Rig lessons [T2]:
- **avatar-vs-avatar forms NO locks** (three prior runs, 0 dmghits even from lasers) while the
madcat family fights fine -- dodge with madcat-family chassis; `mad2` is the AC carrier.
- Node->pilot-page mapping: `-net 1501` claims pilot `127.0.0.1:1502`, `-net 1601` claims
`:1602` (net port + 1) -- sed each pilot page's vehicle= line EXACTLY for mixed pairings.
- **BYSTANDER receipts legitimately fire in a 2-mech fight**: the production aim ray designates
SCENERY constantly (the ac_bench BT_FIRE_AT_ICON lesson generalizes to the real pick), and a
mech crossing a scenery-locked round's path detonates it -- the binary world-sweep shape
(FUN_0042291c via @004bef78 [T1]). All 11 receipts of the PASS run were true positives
(struck = the other live mech, aim 146-1016u away, one paired IMPACT each;
`scratchpad/night16/bys_forensics.py`). A zero-receipt assertion is miscalibrated; assert
the false-positive conditions instead (self-strike / phantom id / det-at-lock-aim / missing
paired delivery / print-cap 24 reached).
+2 -2
View File
@@ -217,7 +217,7 @@ yet reconstructed (parse-skipped). The separate window is now the **live viewer*
- **Pod MFD port-name reconcile (pod-only):** a ~5-line positional dual-name fallback in `SVGA16::Update`
(`UL = GetGraphicsPort("auxUL2"); if(!UL) UL = GetGraphicsPort("Heat"); …`) so BT MFDs reach the pod's real
monitors. NOT a CFG rename (the CFG uses BT names pervasively + `MUNGA_L4` is shared with RP's aux names).
- **Polish:** overlay-plane compositing over `sec`; a pod-accurate RGB-packing toggle; texture atlas
- **Polish:** ~~overlay-plane compositing over `sec`~~ (DONE 2026-08-13, row 28); a pod-accurate RGB-packing toggle; texture atlas
(one lock/upload); device-reset recreate hardening for the additional swap chain.
## Widget reconstruction — the real fix (in progress; mapped by `bt-gauge-widget-recon-map`)
@@ -691,7 +691,7 @@ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr b
| 25 | sec: headingPointer needle + numeric | YawPitchRoll decomposition (engine-convention port of euler[0]) | LIVE: 089→120 while turning under BT_GOTO | T2 | CORRECT |
| 26 | sec: numericSpeed | LinearSpeed (abs(adv)/dt) | LIVE: 0 parked → 176-182 walking | T2 | CORRECT (units question stays open) |
| 27 | sec: digitalClock MISSION TIME | engine mission clock (format enum arg) | LIVE: 09:19→06:04 counting | T0/T2 | CORRECT |
| 28 | sec: sectorDisplay (overlay) | localOrigin → Round(Z·0.01)+500 / Round(X·0.01)+500 | commit 4a4ec68 [BT_SECTOR_LOG live]; NOTE: overlay plane not composited into the dev sec cell (polish item) — data verified by log | T2 | CORRECT |
| 28 | sec: sectorDisplay (overlay) | localOrigin → Round(Z·0.01)+500 / Round(X·0.01)+500 | commit 4a4ec68 [BT_SECTOR_LOG live]; **overlay plane COMPOSITED 2026-08-13** (branch sec-overlay-composite): every sec expand (BTDrawGaugeSurfaces, BTDrawCockpitPanels, SVGA16::Update case 0, glass BlitSurface) now uses sec\|overlay = 0xFF — clut0 already held the arcade DAC's combined table (BuildSecondaryPalette spreads each port over the other's bit combos), so the fix is mask-only; the glass dirty token gained the overlay port so the numerals repaint. Verified live: SECTOR ticks + SCALE:1000 at spawn (1000 = radarRange init; the DOS shot's 4000 is max zoom) + the btsec1ov graticule now draws | T2 | CORRECT |
| 29 | sec: schematic ARMOR view (cmArmor/multiArmor, dama.pcc) | zone damageLevel ×100 → adpal ramp | incr.3/4; all-green pristine LIVE | T2 | CORRECT (AUTH-STATIC all-green solo) |
| 30 | sec: schematic CRITICAL view (cmCrit) | subsystem simulationState/damage | LIVE this audit: N-cycle shows the full subsystem list (GEN A-D, LOOP 1-6, HUD, SENSORS, GYRO, TORSO, weapons) | T2 | CORRECT |
| 31 | sec: schematic HEAT view (cmHeat) | subsystem currentTemperature tint | #6 pixel-verified; re-cycled this audit (mask 0x450421→0x490421→0x510421) | T2 | CORRECT |
-588
View File
@@ -1,588 +0,0 @@
# Weapons Drift Audit — binary vs port, per weapon lane (2026-08-13)
**What this is:** the AI-facing ledger of the per-weapon-class physics audit
(the #171 batch + this session's raw-byte verifications). The tester-facing
twin is `docs/WEAPONS_FIELD_GUIDE.txt` — keep the two in sync; the field
guide translates every address below into behavior and carries the era
questions to the group.
**Evidence tiers:** T1 = decomp/raw-byte verified · T2 = runtime receipts ·
T3 = port approximation · T4 = guess. Addresses are `@ADDR` in the rebuilt
2026-08-06 export (`reference/decomp/all/part_*.c`).
**HOLD notice:** corrections in the "Corrections owed" section are HELD for
the #171 group decision per Oracle's HOLD — do not sweep them into the KB or
the queued ticket comment until the group signs off.
---
## Verdict summary
| # | Lane | Verdict |
|---|------|---------|
| 1 | Lasers (Emitter 0xBC8, 6 marks) | FAITHFUL |
| 2 | PPC/ERPPC fire chain (0xBD4) | FAITHFUL |
| 3 | PPC rider (a) victim display scramble | KNOWN-DIVERGENCE [T3] (unimplemented; visual OPEN) |
| 4 | PPC rider (b) victim generator short | FAITHFUL (tag-label scope OPEN) |
| 5 | Seek-voltage gears + #21 overcharge brick | KNOWN-DIVERGENCE (deliberate rescue) |
| 6 | Autocannons AFC25/50/100 (0xBCD) | FIXED-THIS-BUILD (#171; T2 field receipts pending) |
| 7 | AC cosmetic tracer visual | KNOWN-DIVERGENCE [T3] (cosmetic; constants recovered — close) |
| 8 | GAUSS (fielded, 0xBCD record) | FAITHFUL (correction sweep owed, HELD) |
| 9 | GaussRifle class 0xBCE (+ shell 0xBD1) | FAITHFUL (dead code both sides) |
| 10 | MissileLauncher core (0xBD0, all racks) | FAITHFUL |
| 11 | Missile presentation (salvo visuals/gains) | KNOWN-DIVERGENCE [T3] (presentation-only) |
| 12 | Missile bystander sweep | KNOWN-DIVERGENCE [T3] (largest remaining gap) |
| 13 | Missile splash (@0042fad0) | FAITHFUL under stock content (gate wiring [T3] on custom eggs) |
| 14 | LRM flight + loft | FAITHFUL |
| 15 | Streak racks (strk model) | FAITHFUL |
| 16 | NRK5 / nrk-fed racks | FAITHFUL (KB model-table wording sweep owed) |
| 17 | Shared reload/ammo/eject chain | FAITHFUL |
| 18 | Jam roll | KNOWN-DIVERGENCE [T3] (extra gate — remove) |
| 19 | Incoming-missile warning | OPEN (consumer unlocated; era testimony wanted) |
---
## Per-lane entries
### 1. Lasers — Emitter 0xBC8 (SLaser/ERSLaser/MLaser/ERMLaser/LLaser/ERLLaser)
One C++ class, data-differentiated; **no pulse variants exist** in shipped
content.
- **Binary [T1]:** trigger-time hitscan beam @004bace8: charge-gear state
machine (4 states, no jam/ammo), lock required (`*(mech+0x388)!=0`),
viewFireEnable look-direction arming, `dist<=effectiveRange` gate AT FIRE.
K byte-confirmed **1.0f @004b9c98** → effRange = (1.0 zoneDamage) ×
WeaponRange. Damage = authored × (charge/seekV)². ONE aimed single-panel
message. Out-of-range: beam draws, damage silently discarded. 0.2s
target-tracking afterglow. Hard heat-FAILURE stop (no gradual
degradation). Heat/gear-toggle vet-expert only.
- **Port [T2]:** `emitter.cpp` live-verified same machine end-to-end; 60Hz
sub-stepped Loading tick (±1% snap-window robustness, port mechanism);
beam from real `ermlaser.bgf` tube via `BTPushBeamKind` ([T3] dpl layer
unported, #164 stuck-beam guards).
- **Verdict:** FAITHFUL.
### 2. PPC / ERPPC — 0xBD4 fire chain
- **Binary [T1]:** one-line tail-call into `Emitter::FireWeapon`
(@004bb878); vtable/ctor/CSS otherwise identical — everything distinctive
is authored data (EnergyDamage(4), 12/16 pts, 900/950 range, 5.0s
recharge, blue pip).
- **Port [T2]:** same tail-call structure; full charge = authored 12/16
verified live; blue beam under its own renderer case.
- **Verdict:** FAITHFUL.
### 3. PPC EnergyDamage rider (a): victim display scramble
- **Binary [T1 call, T4 visual]:** @004a0230 (~0x4a03f3): damageType==4 →
no-arg virtual on app-global +0x4c — identified via the port's own app
decode as THE GAUGE RENDERER (`DAT_004efc94+0x4c` = GetGaugeRenderer,
`btl4mppr.cpp:249`, cf. `btl4galm.cpp:404`) — a cockpit gauge/MFD scramble
per Energy hit. Exact visual/duration unread (which vtable slot +0x4c does
visually: OPEN).
- **Port:** NOT implemented — grep of the port `TakeDamageMessageHandler`
body finds no damageType==4 gauge call. Candidate for the #156/
weapon-state polish list.
- **Verdict:** KNOWN-DIVERGENCE [T3] (unimplemented rider; visual detail
OPEN — era testimony wanted BEFORE building; question shipped in the
field guide).
### 4. PPC EnergyDamage rider (b): victim generator short
- **Binary [T1]:** @0049c690: damageType==4 + critical subsystems in struck
zone → one random critical rolls `ForceShortRecovery` @004b11bc
(brownout, charge stall, cockpit electronics dip). Tag 0x50f4bc
Generator-vs-PoweredSubsystem label conflict still open.
- **Port [T2]:** implemented — `mechdmg.cpp:471-477` (damageType==4 &&
criticalSubsystemCount>0 → ForceShortRecovery).
- **Verdict:** FAITHFUL (tag-label scope OPEN). NOTE: the two riders'
port status differs — never lump them in vet-facing docs.
### 5. Seek-voltage gears + #21 overcharge brick (all energy weapons)
- **Binary [T1]:** `ToggleSeekVoltage` @004ba478 (vet/expert only): wrap
from gear 4→1 skips `ResetFiringState` → charge above snap window →
`ComputeOutputVoltage` overcharge reads ZERO → weapon PERMANENTLY
BRICKED. The arcade shipped this latent brick.
- **Port [T3 deliberate, documented]:** overcharge counts as Loaded (issue
#21, 38 benched rescues) — the port's ONE deliberate energy-lane
divergence; lives in the deviations appendix.
- **Verdict:** KNOWN-DIVERGENCE (deliberate rescue of a shipped latent
brick).
### 6. Autocannons — AFC25/50/100 (ProjectileWeapon 0xBCD)
- **Binary [T1]:** trigger-time hitscan @004bc104: lock required, batched
per TracerInterval (all authored 1), fire-time `dist<=live
effectiveRange` gate, ONE single-panel Ballistic message to the locked
target, out-of-range batch silently discarded (round consumed, tracer
flies), gyro recoil kick dmg/16 dir (0, 0.6, 1.5) (constants
3.0f/0.0625 byte-confirmed), jam p=0.41·T/Tfail clamped [0.05,1.0]
vet/expert-only, EJECT tap=one round / hold 3.0s=dump bay, shell 0xBD1
dead code.
- **Port [T2 bench]:** `projweap.cpp` at bbb3b4e implements the hitscan
line-by-line (13-round batch + DET dmg=0 receipts); closes #112
single-panel and the #168 mis-fix. Field verify armed for Friday (local
rig can't form avatar locks).
- **Verdict:** FIXED-THIS-BUILD (#171 batch; T2 field receipts pending
Friday). The field guide carries the #27 "rounds can genuinely miss"
RETRACTION.
### 7. AC cosmetic tracer visual
- **Binary [T1 weapon-side, T4 renderer-side]:** `launchVelocity.z =
effRange/sqrt(2.0·(1/9.8)·muzzleHeight)` — both constants BYTE-CONFIRMED
this audit: @0x4bc678 = 2.0f (float32); @0x4bc67c = 1/9.8 stored as an
**80-bit x87 EXTENDED** (bytes `8a 6c 37 99 43 c6 fa d0 fb 3f` — does NOT
decode as float32, likely why it stayed "unresolved") → ~1s to cross full
range for a ~5u muzzle. Drawn by the unported 'Tracer' dpl renderable;
renderer feed unrecovered.
- **Port [T3]:** `projweap.cpp` still ships 1.0f stand-ins for both
constants (marked 'unresolved' — now false) and drops the muzzleHeight
term: tracer speed = effRange/1.0. Numerically close for ~5u muzzles by
accident.
- **Verdict:** KNOWN-DIVERGENCE [T3] (cosmetic-only; constants recovered —
CLOSE IT next build; stale 'unresolved' comments must go in the same
change).
### 8. GAUSS — the fielded weapon (lok2 'GAUSS' record)
- **Binary [T1 raw bytes]:** BTL4.RES @0xf16e6: classID **0xBCD
ProjectileWeapon** (bytes `cd 0b` / `d0 01` size 0x1D0, verified this
session) — the pod's Gauss ran the FULL AC hitscan chain WITH damage:
20 pts Ballistic single-panel out to 900, 8s cycle, 16 rounds
(AmmoBinGAUSS 0xBCB), near-cold (1e6 heat), yellow pip, 1.25 gyro kick.
Class histogram across all 18 streams: 78×0xBC8 / 16×0xBCD / 30×0xBD0 /
14×0xBD4, **ZERO 0xBCE**.
- **Port [T1]:** factory dispatches 0xBCD →
`CreateProjectileWeaponSubsystem` (`mech.cpp:1939-1942`) — the port's
fielded GAUSS ALSO deals AC-hitscan damage. The "port damage is an
invention" framing (commit 80037a7, queued #171 comment, projweap
banner) is WRONG at the fielded level — port and binary agree.
- **Verdict:** FAITHFUL (mechanics match). Correction sweep owed — HELD
(see below). Era question reframed and shipped in the field guide
(headline). Chassis attribution (lok2 vs the weapsub scan's nearest-name
'blh') rides the flagged heuristic — the field guide asks the vets which
mech carried it rather than asserting.
### 9. GaussRifle class 0xBCE (+ shell 0xBD1)
- **Binary [T1]:** real code, dead content: `FireWeapon` @004bdca4 = one
store (charge=0), no damage/beam/heat; zero shipped records instantiate
it. Shell 0xBD1 `Make` @004be384 has zero callers; flight body
unreachable.
- **Port [T1]:** same classes exist (GAUSS.CPP shipped source), same
factory case, never constructed by shipped content; equally dead.
- **Verdict:** FAITHFUL (dead code both sides). The CLASS-level "@004bdca4
is a no-op" claim stands even after the fielded-Gauss correction.
### 10. Missile family core — MissileLauncher 0xBD0 (SRM/LRM/Streak/NRK)
One class, data-differentiated.
- **Binary [T1]:** genuinely flying, the ONE ungated family: lock-to-fire
via the shared Loaded case (no dumb-fire trigger path, no Streak-special
code), one entity per salvo, per-missile dmg = authored/count, seeker
homes on the fire-time picked panel point with constant-bearing lead,
4.0u fuze + swept contact (bystanders hittable), lock-drop on target
death (@0049fb54 movementMode 2||9), WeaponRange is fire-control display
only — reach is physics (burn + 10.0s coast, y<1.0 kill plane, expiry
fizzles), impact cluster roll `min(Random(n)+n/4, n)` then zone 1
lottery with per-burst re-roll on the victim.
- **Port [T2]:** full chain live-verified (382-impact night-9 corpus
matches the roll exactly; Streak 277 u/s; fuze/lock-drop benched);
direct Dispatch instead of the SubsystemMessageManager (deliberate,
preserves burstCount, #95).
- **Verdict:** FAITHFUL.
### 11. Missile presentation (salvo visuals + steering gains)
- **Binary [T1]:** ONE Missile entity per salvo (single spawn call
@004bcc60 — the pod drew one flying round + one explosion); body-turn
slerp at authored MaxThrusterRotationRate; velocity slaved to nose while
burning.
- **Port [T3]:** N-round visual ripple (lead round carries damage; N1
damage-0 tracers), ±2.5° deterministic tube spread, steering gains
4.0/8.0 port-tuned (the old 'MissileTurnGain=4.0' attribution was the
proximity fuze), muzzle safety clamp.
- **Verdict:** KNOWN-DIVERGENCE [T3] (presentation-only; damage economy
identical).
### 12. Missile bystander sweep
- **Binary [T1]:** FUN_0042291c swept real world geometry including movers
each tick — a third mech in the flight path could be struck by someone
else's missile.
- **Port [T3]:** pool rounds contact only the locked target + terrain; a
mech between shooter and target is flown through (splash alone can touch
bystanders).
- **Verdict:** KNOWN-DIVERGENCE [T3] — largest remaining missile-physics
gap. Field-guide watch item for Friday ("hit by missiles aimed at
someone else" beyond splash range).
### 13. Missile splash — Explosion::SplashDamage @0042fad0
- **Binary [T1]:** gated on the FIRING player's advancedDamage (+0x264;
every stock egg stamps 1 → splash WAS live on the pods), radius 30
authored on all missiles, baseBurst = FULL rack count (post-roll restore
@part_013.c:18283-18284, read + confirmed this session), falloff bursts
= round(base/dist^1.25) floored 1 (1.25 = shipped double; divide
grounded in T0 EXPLODE.cpp), excludes the explosion + direct victim;
shooter self-exclusion **unprovable from the export**.
- **Port [T2 mechanics / T3 gate]:** same math, full-count baseBurst (now
[T1]-pinned correct), excludes shooter + direct victim; enable =
SplashRadius>0 instead of the advancedDamage gate — coincides under all
shipped eggs, diverges only on custom eggs.
- **Verdict:** FAITHFUL under stock content (gate wiring KNOWN-DIVERGENCE
[T3] on custom eggs — site option; shooter self-splash OPEN — era
question shipped).
### 14. LRM family flight (lrm model) + loft
- **Binary [T1]:** lrm rid=319: turn 60°/s, burn 10s @ 300 u/s², climb 50,
splash 30 — climb>threshold triggers the seeker loft (aim.y +=
0.1×min(range200, 300), up to +30u): the signature climb-then-plunge
arc; soft 30 u/s eject, some mounts +5 up-tilted.
- **Port [T2]:** #84 fixed the thrust resolution (pre-fix 10× slow WAS the
'missiles are slow' field report); loft constants exact
(@004bec18/24/28).
- **Verdict:** FAITHFUL.
### 15. Streak racks (strk model — 'SRM6' bins on at least one chassis)
- **Binary [T1]:** strk rid=323: turn 360°/s (3× SRM — 6× the lrm), burn
3s @ 300, climb 0 — the round that out-turns evasion. NO Streak class or
special lock code exists; the ammo model alone is the identity.
- **Port [T2]:** same records resolved; 277 u/s impact speed verified
live.
- **Verdict:** FAITHFUL.
### 16. NRK5 'Narc' + every nrk-fed rack
Several 'LRM5/10/15' bins author nrk ammo.
- **Binary [T1]:** no beacon mechanism exists anywhere in the binary
(single launcher class, no beacon input on the seeker). nrk rid=327
byte-dumped this session: turn 360°/s, burn 10s @ 300, **CLIMB 50**,
splash 30 — the shipped NRK5 is a light LRM-profile rack WITH
Streak-grade tracking AND the loft (missileB's 'no loft' corrected;
dataA right). Field-visible agility difference between visually
identical 'LRM' racks is authentic.
- **Port [T2]:** same data-driven resolution; nothing Narc-special to
port.
- **Verdict:** FAITHFUL (KB model-table wording needs the nrk climb/turn
correction — sweep owed).
### 17. Shared reload/ammo/eject chain (0xBCD + 0xBD0)
- **Binary [T1]:** recoil countdown only while generator route powered,
heat-scaled (hot generator reloads slower), MinVolt% 0.3 freeze; EJECT
(@004bb9b8, read this session): TAP = one round out + weaponAlarm→3
Loading UNCONDITIONALLY if rounds remain — the in-mission UNJAM (clears
Jammed(5)); HOLD 3.0s = DumpAmmo whole bay → NoAmmo(7) roach motel (only
re-arm/respawn Reset clears); novice locked out of eject.
- **Port [T2]:** same machine (`projweap.cpp:751-968`); tap-unjam
semantics match (the #166 advice rides on it).
- **Verdict:** FAITHFUL.
### 18. Jam roll (ballistic + missile racks)
- **Binary [T1]:** @004bbfcc: rolled after each shot whenever simLive AND
the weapon's own heatAlarm != 0; p = 0.41·T/Tfail (0.41 byte-confirmed)
clamped [0.05, 1.0]. Practically vet/expert-only (std/novice heat model
off → never heat-alarms).
- **Port [T3 marked bring-up gate]:** rolls only at heatAlarm >=
DegradationHeat (`projweap.cpp:692`) — added when temps were static;
now that the heat economy is live it masks authentic warm-weapon jams.
Binary rolls at ANY nonzero heat alarm.
- **Verdict:** KNOWN-DIVERGENCE [T3] (minor; REMOVE the extra gate next
build — announced in the field guide).
### 19. Incoming-missile warning
- **Binary [T1 registration, T4 consumer]:** Missile ctor registers with a
MECH target via a virtual on target+0x418; the consumer (threat lamp?
beeper tempo?) is unlocated in the decomp.
- **Port [T3]:** structurally dead: the authored beeper/tempo attributes
latch (`mech4.cpp:8536`) but the only producer lives in the dormant
Missile TU — the pool never reports; the beeper never fires (#83).
- **Verdict:** OPEN (needs the consumer decomp + era testimony — question
shipped in the field guide).
---
## Contradictions resolved this audit (evidence log)
1. **GAUSS (the headline):** BTL4.RES @0xf16e6 GAUSS record = classID
0xBCD (bytes `cd 0b`, size 0x1D0); class histogram 78×0xBC8 / 16×0xBCD
/ 30×0xBD0 / 14×0xBD4, zero 0xBCE; port factory 0xBCD →
CreateProjectileWeaponSubsystem (`mech.cpp:1939-1942`). The fielded
Gauss is FAITHFUL on both sides; the earlier "port damage is an
invention" audit examined the content-dead 0xBCE class. CLASS-level
claim (@004bdca4 no-op) stands.
2. **K constant:** `_DAT_004b9c98` dumped: CODE file+0xb9a98 = `0000803f`
= 1.0f. [T4] closed → [T1]; undamaged effRange = exactly authored
WeaponRange; port's 1.0 (`mechweap.cpp:558`) correct.
3. **AC launch-speed constants:** @0x4bc678 = 2.0f (float32); @0x4bc67c =
0.10204081 = 1/9.8 as a 10-byte x87 EXTENDED (`8a6c379943c6fad0fb3f`) —
not float32-decodable, hence the stale "unresolved". projweap.cpp 1.0f
stand-ins + dropped muzzleHeight term now closable.
4. **Jam recovery:** @004bb9b8 (part_013.c:15709-15723): eject RELEASE
path calls FeedAmmo and unconditionally SetLevel(weaponAlarm, 3
Loading) when rounds remain — a tap clears Jammed(5). missileB's "jam
permanent until re-arm" refuted; in-sortie unjam = YES (novice locked
out).
5. **nrk flight model:** BTL4.RES type-15 rid=327: turn 360°/s, burn 10s,
accel 300, climb 50, splash 30. dataA right; missileB's "no loft"
corrected. KB sweep owed (combat-damage.md "LRM … the only climber").
6. **SRM6 count-4 variant:** the record authors MissileCount=4, Damage=20
→ 5.0/missile (dataA's 3.33 assumed count 6). ALL per-chassis
attributions ride the nearest-name heuristic — re-verify against the
egg/VehicleTable before any chassis-specific claim ships to the vets
(the field guide deliberately names no chassis in Appendix A).
7. **Splash baseBurst:** part_013.c:18271-18290: cluster roll stored to
+0x338 for direct dispatch, then IF splash enabled the ORIGINAL full
count is RESTORED (+0x338 = uVar3 at :18284) before SplashDamage — the
port's full-missileCount baseBurst exactly faithful.
8. **PPC display-scramble callee:** DAT_004efc94+0x4c = the GAUGE RENDERER
(`btl4mppr.cpp:249`, `btl4galm.cpp:404`) — the Energy-hit rider is a
gauge/MFD-side scramble virtual. Still open: what slot +0x4c does
visually. Port does not implement the call; the generator-short rider
IS ported — statuses differ.
9. **SRM2 flight profile — UNRESOLVED:** dataA says the ava1 SRM2 bin
feeds 'strk'; missileB groups SRM2 under 'srm'. The weapsub scan
printed weapons only (no bin ammoModelFile column). Needs the bins
pass before any SRM2 homing claim ships (field guide explicitly
declines to state it).
10. **@004bb9b8 relabel:** it is the EJECT handler, not fire-consume;
FeedAmmo @004bd4f4 is the consume. Carry into any KB address table
that copied the old tasking label.
---
## Corrections owed — HELD for the #171 group decision (Oracle HOLD)
- Commit 80037a7's "Gauss deals NO damage in the shipped binary / port
damage is an invention" framing.
- `scratchpad/night16/queued_171_comment.md` — Gauss section + the queued
era question (the reframed question now lives in the field guide). The
#112/#168 sections are correct as written.
- `context/decomp-reference.md` §SS5 and `context/combat-damage.md` Gauss
claims.
- Any `projweap.cpp` / gauge banner repeating the no-damage framing.
## Stale banners confirmed present (sweep with the next projweap change)
- `projweap.cpp:990` still reads "the #168 flight-contact model" above the
implemented hitscan body — same failure genus as the stale banner that
caused #168 (gotcha: verify banners when the body changes).
- The "unresolved" comments on `_DAT_004bc678`/`67c` are now false
(constants byte-confirmed, see entry 7).
## Actionable closures queued (next build)
1. AC tracer launch speed: land 2.0f + 1/9.8 + the muzzleHeight term in
`projweap.cpp`; delete the stale comments and the :990 banner. Cosmetic;
era question 14 (round pace) tunes the final feel.
2. Jam roll: remove the `heatAlarm >= DegradationHeat` gate
(`projweap.cpp:692`) — binary rolls at any nonzero heat alarm.
Announced to the vets in the field guide §9.
3. PPC gauge-scramble rider: BLOCKED on era testimony (field guide §3) —
build the visual only after the group answers.
4. Missile bystander sweep: design exists; field guide §6 arms the Friday
watch item.
## Era questions shipped to the vets
The 14 questions live in `docs/WEAPONS_FIELD_GUIDE.txt`, distributed into
the per-section "WE NEED YOUR MEMORY" blocks: Gauss killed + carrier
chassis (headline, §5); PPC hit visuals + generator short (§3); splash
proximity + shooter self-splash (§7); ballistic recoil, AFC25 eaten
trigger, out-of-range round fate, round pace (§4); vet-only jams +
tap-eject unjam, NoAmmo latch with ammo remaining (§9); gear-wrap brick
(§2); incoming-missile warning (§10); generator switching mid-charge,
out-of-range beam (§1); rack-name quirks, homing agility + Narc-did-
nothing (§8).
---
## RANGE AUTHENTICITY — printed manual cross-check + the rangefinder scale (2026-08-13)
**Why:** playtesters report ranges "feel wrong" and hits landing "beyond
the maximum range on the range scale." Two legs: (1) diff every weapon
range/damage number the original player manual prints against the
authored content values (Appendix A of the field guide — this audit's
numbers table); (2) pin down how the HUD rangefinder's displayed maximum
relates to authored WeaponRange.
### Leg 1 — the manual cross-check
Source: `reference/manual/Tesla40_BT_manual.pdf` (34 pp). Finding about
the source itself [T1 — full-text sweep + page renders, this audit]: the
manual prints **no weapons table**. The per-mech stat sheets (pp. 25-29)
carry loadouts, coolant loops, tonnage/armor/reservoir/speeds — no
per-weapon range or damage column. The ONLY printed range figures are
MFD screenshot mockups (eng-data panels), four values total; **no
per-weapon damage number appears anywhere in the manual** (the p. 4
scoring page says damage is delivered "the same way their BattleTech
boardgame counterparts do" — a fidelity claim with no numbers; the
authored 4.10 values do NOT numerically track boardgame values, e.g.
PPC 12 vs the boardgame's 10 — treat the sentence as blurb, not spec).
| Weapon | MANUAL prints | CONTENT authored | Verdict |
|---|---|---|---|
| PPC | RANGE 900M (p. 20, PPC eng-data screen) | 900 | **MATCH** |
| AFC100 | RANGE 400M (p. 21, AFC eng-data screen) | 400 | **MATCH** |
| SRM4 | RANGE 450M (p. 22, Engineering MFD mockup) | 800 (display-only) | **MISMATCH** |
| SRM6 | RANGE 450M (p. 22 AND p. 23, two independent screens) | 800 (display-only) | **MISMATCH** |
| SLaser | — | 1.5 dmg / 150 | manual-silent |
| ERSLaser | — | 2 / 225 | manual-silent |
| MLaser | — | 2.5 / 350 | manual-silent |
| ERMLaser | — | 3.5 / 500 | manual-silent |
| LLaser | — | 5 / 600 | manual-silent |
| ERLLaser | — | 6 / 750 | manual-silent |
| ERPPC | — | 16 / 950 | manual-silent |
| AFC25 | — | 7 / 900 | manual-silent |
| AFC50 | — | 13 / 750 | manual-silent |
| GAUSS | — | 20 / 900 | manual-silent (pip-color note below) |
| LRM5/10/15/20, NRK5 | — | 6000 (display-only) | manual-silent |
| ALL weapons, damage | — (zero numbers printed) | Appendix A | manual-silent |
All four manual figures verified by page RENDER, not OCR alone (the
450M reads twice on p. 22 and once on p. 23) [T1].
**The SRM mismatch read:** 450 (manual) vs 800 (shipped 4.10 content,
BLH live pip dump [T2] + field-guide Appendix A). Two mitigations:
(a) the manual's panels are mockups from the earlier software revision —
the same 4.0→4.10 drift already proven on the Loki loadout rework,
the small-laser loop redistribution, and supercharge
(`context/pod-hardware.md` §Manual); (b) for MISSILE racks,
WeaponRange is fire-control display ONLY (lane 10 [T1]) — real reach is
flight physics on both values, so the 450→800 change altered the pip
position and panel text, never a trajectory. Verdict: authoring drift
between revisions, NOT a port defect; the port displays the shipped
4.10 content values.
**Pip-color side note:** manual p. 10 codes the readiness pips PPC=Blue,
Laser=Red, LRM=Green, SRM=Brown, AFC=Orange, **Gauss=Grey** — blue/red
match our dumps exactly, SRM "brown" is the authored amber (0.6,0.4,0),
but the shipped GAUSS record authors a YELLOW pip (lane 8 [T1]). Same
drift class as the SRM range; cosmetic.
### Leg 2 — the rangefinder scale (what the ladder max actually is)
The tester model behind the complaint — "the scale maxes at the selected
weapon's range" — is FALSE in both the binary and the port. What the
HUD right ladder actually is (full detail `context/gauges-hud.md`
§Cockpit HUD reticle):
- **The ladder is a FIXED 01200 m scale** — the ctor's hardcoded
calibration (@004cc40c, ctor param 11 = 0x44960000) [T1]; port
identical (`btl4vid.cpp:2382` kRetMaxRange = 1200.0f). It is never
derived from any weapon's WeaponRange or live effectiveRange.
- **Each weapon marks its own reach as a PIP** at its authored
WeaponRange, CLAMPED into [0..1200] (`AddWeapon` @004cdac0; port
transcription `btl4vid.cpp:3139-3147`) [T1]. Caret below a pip = that
weapon reaches the target. An LRM/NRK rack authored 6000 therefore
pins its pip AT THE LADDER TOP — authentic on both sides.
- **The caret is the displayed target range**, sliding at 500 m/s toward
the true pick range (HudSimulation part_013.c:5652 [T1]; port
mech4.cpp targeting step [T2]); pegs at 1200 with no target. During
depth-discontinuity walks the displayed value legitimately lags true
range by hundreds of meters (the Gitea #4 measurement).
- **The ONE displayed-range divergence:** the binary subtracts
`_DAT_004b7ecc` = 100.0f from RangeToTarget (@0x1EC) every frame the
timed flag @0x22C is set (timer @0x21C to limit @0x1D8) — the port
does NOT implement this state (`hud.cpp:79-82` tracks it;
`context/open-questions.md` §HUD range-bias) [T1 read, unimplemented].
Direction: the shipped machine UNDER-reads range by up to 100 m in
that state; the port never does. So when the state is active a
true-850 m missile hit against an 800 pip DISPLAYED as in-range on
the pod but displays as beyond-the-pip in the port — a bounded
(≤100 m) port-side contributor to "hit beyond the scale" reports.
Whether the state ever triggers in the field is unknown (what sets
@0x22C is the open question) [T4 for field prevalence].
**Rangefinder verdict:** the displayed scale is AUTHENTIC — no port
miscalibration. "Hits beyond the maximum range on the range scale" is
expected, authentic behavior with three drivers, largest first:
(1) missile reach is flight physics, not WeaponRange — SRMs genuinely
hit past their 800 pip and LRMs past the 1200 ladder top [T1];
(2) energy/AC pips mark AUTHORED range while the live fire gate is
effectiveRange = (1 zoneDamage) × authored — a damaged weapon's real
reach sits BELOW its pip, never above, so those lanes cannot produce
beyond-pip hits; (3) the 500 m/s caret slide lags true range during
fast picks. The single true divergence is the unimplemented 100 m
bias state (above) — worth reconstructing if Friday's reports cluster
in the 0100 m band past a pip; anything farther past the pip is
missiles being missiles.
**Guide action:** MISMATCHES exist (SRM display ranges + the Gauss pip
color) → per the audit rule the field guide was NOT touched; no
reassurance paragraph shipped.
---
## VELOCITY/DT INTEGRATION — term-by-term audit (2026-08-13)
**Scope:** the projectile velocity integration, binary vs port pool
(`mech4.cpp BTUpdateProjectiles` / `BTPushProjectile`), plus the AC tracer
launch-speed constants. Binary side read end-to-end this audit:
Missile::MoveAndCollide @004bef78, Seeker::FindTarget @004be9a0 /
LeadTarget @004beae4, **MissileThrusterSimulation @004be474 — the
standalone Performance body, RECOVERED** (misthrst.cpp's "folded into
MoveAndCollide, no distinct @ADDR survives" note predated the 2026-08-06
re-export and is corrected), Missile ctor @004bf5b4, launch composer
@004bcc60, and the engine Mover lane, decomp↔T0-source matched
line-for-line: **FUN_00422360 = Mover::PerformAndWatch**,
FUN_00421bac = ApplyWorldAccelerations, FUN_00421e2c =
ApplyAirResistanceAndGravity(power), FUN_00421ca8 = CalculateDrag,
FUN_00421b2c/FUN_00421b6c = UpdateWorldMotion/UpdateLocalMotion;
mover+0x250 = localEnvironment, +0x254 = deadReckoner (MOVER.h/.cpp).
**The dt model [T1/T0]:** every Performance slice is variable-step REAL
SECONDS (the #96 sim-time result), and `Mover::PerformAndWatch`
**ZEROES `localAcceleration` at the top of every frame**
(@00422360: `FUN_0040a7f4(+0x1dc, &DAT_004e0fd4)` == T0 MOVER.cpp:672
`localAcceleration = Motion::Identity`). Thrust, drag and gravity are
re-added FRESH each frame and then integrated ×dt (@00421bac:
`p += v·dt + ½a·dt²; v += a·dt`). Net missile physics is the clean ODE
`dv/dt = ThrusterAccel COD·ρ·v²` — **there is NO myomer-style 28 Hz
per-tick accumulate anywhere in this lane**, so the port's
`speed += accel·dt` needs no ×(dt·28) rescale. (The myomer idiom applies
to PERSISTENT accumulators; the Mover acceleration is not one.)
**The headline: drag was wrongly waved off.** All four missile models
author linear drag CODs **0.001 on every axis** (BTL4.RES type-15 raw
floats read this audit: srm rid=315 / lrm 319 / strk 323 / nrk 327;
airDensity = 1.0, engine default). 0.001 multiplies **v²**: it is the
flight model's GOVERNOR, and the binary says so itself — the Missile
ctor @004bf5b4 (part_013.c:18416) **precomputes terminal speed
`sqrt(ThrusterAccel / negCOD.z)` into missile+0x348** and the seeker
uses it as the lead time base. Terminal speeds: LRM/Streak/NRK
**547.7 u/s**, SRM **774.6 u/s**. The port pool (per #84's "drag
~0.001 = negligible") flew undamped: an LRM reached **3030 u/s** by
burnout — 5.5× the binary cap. Simulated head-to-head (28 Hz binary
loop vs the port step): 800u arrival LRM 2.54 s @ 492 u/s (binary) vs
2.22 s @ 695 (undamped port); at t=5 s the undamped LRM had covered
3912u @ 1530 u/s vs the binary's 2108u @ 544. **Fixed this build**:
the pool integrates `speed += (accel_burn 0.001·speed²)·dt` for
thruster rounds, burn AND coast (a burned-out round decelerates,
`v(t) = v0/(1+0.001·v0·t)` — Streak at t=5 s: 256 u/s, was 1005
frozen). AC tracers (p.accel==0) untouched — the binary's visible AC
round is a renderer cosmetic with no Mover physics.
| Term | Binary [T1] | Port (pre-audit) | Verdict | Player-visible consequence |
|---|---|---|---|---|
| dt model | variable slice, SECONDS; accel accumulator zeroed/frame (@00422360); integrate ×dt (@00421bac) | `speed += accel·dt`, `pos += vel·dt` | **FAITHFUL** (no per-tick accumulate here) | none |
| Launch velocity | authored MuzzleVelocity (z negated) + shooter velocity (@004bcc60 / FUN_004b9cbc) | same (#67) | **FAITHFUL** | none |
| Launch acceleration | ZERO in the make message (@004bcc60 authors the DAT_004e0fd4 zero Motion) | thrust applies from frame 1 (binary thruster adds the same frame) | **FAITHFUL** | none |
| Thrust | @004be474: while burning, `burnLeft = dt`, `localAccel += (0,0,ThrusterAccel)` onto the fresh accumulator ⇒ constant accel | `speed += accel·burn_dt` | **FAITHFUL** | none |
| **Drag** | `COD·ρ·sign(v)·v²` per body axis, EVERY frame, burn + coast (@004bef78, power-2 inline; plain-Projectile dead class uses power 1.0 @004bddec); CODs 0.001 [T1 bytes]; terminal `sqrt(T/c)` precomputed @004bf5b4→+0x348 | **OMITTED** ("negligible") | **WAS DIVERGENT → FIXED-THIS-BUILD** | was: LRM 3030 u/s (5.5× cap), 13% time-to-800u, +40% impact speeds, map-crossing runaways past 2.5 s; now governed at 547.7/774.6 |
| Speed clamp | none — drag IS the clamp | none — drag now is | **FAITHFUL** (post-fix) | — |
| Velocity↔seeker slaving | burning: lateral local vel/accel zeroed each frame ⇒ velocity ≡ speed×nose; nose slerps ≤ MaxThrusterRotationRate·dt (@004be474); at burnout slaving STOPS (ballistic coast) | rotate-toward-aim at port gains 4/8·dt, speed-normalized; steers through coast | KNOWN-DIVERGENCE [T3] (lane 11) | turn dynamics shape; coast homing (real impacts are in-burn) |
| Gravity | none during burn; coast: `worldAccel.y = 6.5` fresh each frame (env default) + y<1 kill plane | absent (loft arc + ttl only) | DIVERGENT [T3 **minor**, unfixed] | post-burnout flights only (beyond ~1400u SRM / ~4800u LRM); missed rounds fly level instead of dropping |
| Position step | `p += v_old·dt + ½a·dt²` | `p += v_new·dt` (semi-implicit) | FAITHFUL-approx | ≤ ~12u lead over a full SRM burn @60fps; imperceptible |
| Expiry | burn+10 s, y<1 (@004bef78 tail) | same (#168) | **FAITHFUL** | none |
| Seeker lead | `aim += targetVel × range/terminal(+0x348)`; loft climb-gated (thruster+0xF0 > _DAT_004bec1c) | live-position re-lead; loft constants exact | KNOWN-DIVERGENCE [T3] (lane 11) | small lead-shape difference |
| AC tracer speed | `launchVelocity.z = effRange/sqrt(2.0·(1/9.8)·muzzleHeight)` @004bc3fc:16157-16164 (bytes @0x4bc678 = `00000040` 2.0f; @0x4bc67c = `8a6c379943c6fad0fb3f` 1/9.8 x87 EXTENDED) | 1.0f stand-ins, no muzzleHeight | **WAS DIVERGENT (cosmetic) → FIXED-THIS-BUILD** | tracer pace now the authentic fall-time scale (~1 s to effRange at a ~5u mount) |
**Port changes landed (this audit, build clean):**
1. `mech4.cpp BTUpdateProjectiles` — quadratic drag `0.001·v²` on thruster
rounds, burn and coast; stale "drag negligible / range-capped" comment
replaced with the byte-cited model.
2. `projweap.cpp``_DAT_004bc678 = 2.0f`, `_DAT_004bc67c = 1/9.8`
(bytes above), muzzleHeight term restored via
`ResolveLaunchVelocity()` (deferred to first use — the port ctor runs
mid-roster-build; z==0 sentinel; complete-Mech-TU bridge
`BTWeaponMountHeight` in mech4.cpp). Stale "unresolved" constant
comments, the :990 "#168 flight-contact model" banner, and the header
"FireWeapon BODY NOT recovered" line all swept.
3. `misthrst.cpp` — banner corrected: @004be474 IS the standalone
MissileThrusterSimulation (steer + burn + thrust-add), recovered.
**KB corollary (swept):** the "Mover gravity is dt-less per-frame → may
need 28 Hz rescale at our frame rate" open question is CLOSED-NO: the
per-frame gravity/drag adds land on an accumulator that PerformAndWatch
zeroes every frame — they are per-frame *acceleration definitions*, not
accumulating increments, and integrate ×dt correctly at ANY frame rate.
(The port's separate "environment gravity reads 0" gap stays open.)
-522
View File
@@ -1,522 +0,0 @@
=============================================================================
BT411 -- WEAPONS FIELD GUIDE: EVERY WEAPON, AS THE ORIGINAL BUILT IT
Updated 2026-08-13 -- reconstruction review edition, for the whole group.
=============================================================================
WHAT THIS IS: we have now read the original machine's firing code for
every weapon family and compared it, behavior by behavior, against what
our port does. This document is the result. For each family you get:
what the shipped machine really did, what our port does today, what
changes in the next build, and -- where the code alone cannot settle
it -- what we need from your memory.
HOW TO REVIEW: you flew the real pods; we only read their code. Read
each section and mark anything that CONTRADICTS your memory of the
machine -- and anything that confirms it. Silence is not confirmation:
if a section simply matches what you remember, a one-word "matches" is
genuinely useful data.
HOW TO REPLY: EDIT THIS FILE. Every section that needs your input
ends with a YOUR NOTES block -- write directly in it (callsign first,
then MATCHES / CONTRADICTS / CAN'T RECALL, then your detail) and post
the edited file back when you're done. Which floor, which revision,
which mech you flew -- machine-room details turn weak memories into
strong evidence.
ONE RETRACTION UP FRONT: the last issues handout (#27) told you
ballistic rounds could now "genuinely MISS a moving target". The
deeper dig behind this document proved that framing wrong: the
original decides a cannon hit AT THE TRIGGER, and the round you watch
fly is a tracer drawn for show. We were wrong; details in the
AUTOCANNONS section. Your review of that section is part of the
group's sign-off on the correction.
=============================================================================
-----------------------------------------------------------------------------
1. LASERS -- SMALL / MEDIUM / LARGE, STANDARD AND ER
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
- A charge-and-release energy weapon. No ammo, no jam. Six marks, one
mechanism -- only the numbers differ (Appendix A). No pulse lasers
exist anywhere in the shipped content.
- Firing needs three things at the trigger: a LOCKED target, your
view actually on the target (the weapon only arms while you are
looking where it points), and the target inside effective range.
- The beam is instant. The hit is decided the moment you fire -- no
travel time, no leading. What you see is the discharge drawing
itself, plus a 0.2-second afterglow that tracks the target.
- Damage scales with the SQUARE of the charge: a full-charge shot
does the listed number; a half-charge release does a quarter.
- Out of range: the trigger works and the beam draws, but the damage
is silently thrown away. No hit, no denied tone we can find.
- Effective range shrinks in proportion to damage on the body zone
carrying the weapon: a half-wrecked arm fires at half reach. An
undamaged weapon reaches exactly its listed range -- confirmed
down to the byte this week.
- Overheat is a hard stop: a heat-failed laser quits outright. There
is no gradual weakening on the way down.
- Each hit lands on ONE panel of the target.
OUR PORT TODAY
Same machine, verified live end to end. The beam is drawn from the
real laser-tube art. Charge timing is smoothed for modern frame
rates -- a robustness measure, not a behavior change. (The #164
stuck-beam guards are already in.)
CHANGING IN THE NEXT BUILD
Nothing mechanical.
WE NEED YOUR MEMORY
- Firing past range: do you remember shots that visibly fired at a
distant target and just did nothing? That is what the machine did.
- Generator switching mid-charge: did switching or re-routing
generators while a laser was charging COST you the in-flight
charge (with an electrical alarm)? A yes pins down a control path
we have not been able to identify from the code alone.
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
2. THE CHARGE GEAR (ALL ENERGY WEAPONS) -- AND THE BRICK THE ARCADE SHIPPED
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
Veteran/expert pilots could cycle an energy weapon's charge gear
(four settings). The machine shipped with a latent flaw: cycling
PAST the top gear wrapped to the bottom without resetting the firing
state. The stored charge now sat above the new gear's release
window, the output read zero, and the weapon went permanently dark
-- charged, lit, and unable to fire until re-arm. The arcade
shipped this.
OUR PORT TODAY
We deliberately rescue it: an overcharged weapon counts as loaded
and fires (#21; 38 rescues proven on the bench). This is the port's
ONE deliberate deviation in the energy lane, and it stays on the
deviations list so nobody mistakes it for the original.
CHANGING IN THE NEXT BUILD
Nothing -- unless the group votes for the authentic brick.
WE NEED YOUR MEMORY
- Did cycling a laser's gear past the top setting ever leave the
weapon DARK -- charged but refusing to fire until re-arm? If pods
genuinely ate weapons this way, some of you will remember it.
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
3. PPC AND ER PPC
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
- The firing machine is literally the laser's -- the same code, the
same rules (lock, look, range gate, square-law charge). Everything
distinctive is the numbers: hardest energy hit (12; ER 16),
longest energy reach (900 / 950), a slow 5-second recharge, and
the only BLUE pip.
- What makes a PPC special is what it does to the VICTIM. Energy
hits -- and only energy hits -- carry two riders:
(a) DISPLAY SCRAMBLE: every energy hit pokes the victim's cockpit
display system once -- some kind of gauge/MFD disturbance per
hit. What it looked like and how long it lasted is NOT
recoverable from the code.
(b) GENERATOR SHORT: if the struck zone holds critical equipment,
one random critical component takes a forced short --
generator brownout, charge stall, cockpit electronics dip.
OUR PORT TODAY
The firing machine and the generator short are in and verified. The
display scramble is NOT implemented -- right now your screens shrug
off a PPC hit, and that is a known gap on the polish list.
CHANGING IN THE NEXT BUILD
Not yet. We will not invent the visual: the scramble gets built
AFTER your testimony, not before.
WE NEED YOUR MEMORY (this one really matters)
- Taking a PPC hit, from inside the pod: what happened on your
screens? Static or interference? A flash? Gauges and MFDs going
briefly to garbage? How long did it last?
- Separately: after taking PPC hits, do you remember the generator
browning out or your weapons losing charge (the short rider)?
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
4. AUTOCANNONS -- AFC25 / AFC50 / AFC100
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
- The cannons are point-and-hit, not fly-and-hit. At the trigger,
with a lock, the machine checks the range; in range, the damage
lands THEN -- one solid ballistic hit on ONE panel of the locked
target. The shell you watch fly is a TRACER, drawn for show after
the decision is already made.
- No leading. The lock does the aiming, and a dodging target does
not shake a round already fired.
- Out of range: the round is consumed, the tracer flies anyway, and
the damage is silently discarded. No hit, no warning.
- Recoil: every shot with real punch (damage over 3) kicks your own
pod up and back through the gyro, in proportion to the shot --
the AFC100 kicks about 3.5 times as hard as the AFC25.
- Jams roll after each shot only while the weapon itself is running
a heat alarm -- in practice veteran/expert only, since the lower
sim levels never heat-alarm a weapon. Chance grows with weapon
temperature.
- One panel per pull. The old "shotgun across zones" spread in our
port (#112) was our bug, never the machine's.
OUR PORT TODAY
Until now our cannons flew a real shell that could miss --
reasonable, and wrong. The trigger-time machine is now implemented
against the original's code line by line and bench-verified: full
batches land as one hit, out-of-range rounds are consumed and
discarded. What it needs now is a real night of fire.
CHANGING IN THE NEXT BUILD
- The trigger-time cannon goes live in the build you test next.
This is the big #171 correction -- please hammer the cannons.
- The tracer's flight speed: the original derives it from the gun's
range and muzzle height (roughly one second to cross full range).
Our stand-in speed was close by accident; the recovered authentic
numbers go in.
WE NEED YOUR MEMORY
- When YOU fired cannons, did the pod's view visibly kick up and
back (distinct from being hit)? Was the AFC100's kick noticeably
harder than the AFC25's?
- The AFC25 specifically: did it occasionally eat a trigger pull
silently -- a click, no shot, on an apparently ready gun? (Its
ammo feed timer exactly equals its firing cycle -- the only gun
where the two can race.)
- Firing beyond range: did the visible round fly on forever, or
vanish? (The code consumes the round and discards the damage; the
tracer's visual fate is the one part we cannot recover.)
- Round pace: roughly how fast did cannon rounds visibly fly?
About a second to cross full range, or very different?
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
5. GAUSS ** THE HEADLINE QUESTION **
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR (as best we can prove it)
- The Gauss the pods actually fielded is built ON THE CANNON
MACHINE. The shipped content authors it as an autocannon-class
weapon: about 20 points of solid shot out to 900 (tied with the
AFC25 for longest ballistic reach), a slow 8-second cycle, only
16 rounds, runs nearly cold, and the only YELLOW ballistic pip.
It hit, and it hurt.
- The code ALSO contains a second, "true" Gauss-rifle mechanism
that charges up and does NOTHING when fired -- no damage, no
heat, nothing. No shipped content anywhere uses it. Dead code.
- Full honesty: for a while our audit believed the pods fielded the
do-nothing Gauss and that our port's Gauss damage was an
invention. Reading the raw content bytes settled it the other
way -- the fielded Gauss is the cannon-class one.
OUR PORT TODAY
Same content, same machine: our Gauss already deals cannon-class
damage. Port and original agree. Nothing changes.
WE NEED YOUR MEMORY (the headline)
- Confirm the Gauss KILLED: heavy single hits at extreme range, a
slow cycle, a small magazine, a gun that never warmed up, and a
yellow pip on the display. We expect YES.
- Which mech do you remember carrying it? (We believe the Loki;
your memory checks our content map.)
- If instead you remember a Gauss that charged up and did NOTHING
when fired -- say so loudly. That would be evidence that some
floor revision shipped content for the dead mechanism, content
we do not have.
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
6. MISSILES -- HOW EVERY RACK REALLY FIRES
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
- Missiles are the one family that genuinely FLIES. Everything else
in the game decides its hit at the trigger; a missile earns its
hit in the air.
- Lock required, same as every weapon. There is no dumb-fire path
anywhere in the machine -- the trigger will not take without a
lock. (No special Streak lock code exists either; all racks fire
through the same gate.)
- ONE flying round was drawn per salvo, plus one explosion -- the
pod drew a single missile even for a 15-tube volley. The damage
bookkeeping rides that one round.
- The seeker LEADS: it flies a constant-bearing intercept toward
the exact spot on the target picked at the moment of fire, and
detonates on a close-proximity fuze with a swept contact check.
- If the target dies mid-flight, the lock drops and the round goes
stupid.
- A rack's listed "range" is fire-control display only. Real reach
is physics: motor burn, then up to 10 seconds of coasting. Rounds
that hit the floor die; rounds that time out fizzle.
- On impact, the cluster roll: out of an n-missile salvo, between
roughly a quarter and all of them count as hits, scattered across
the victim's zones with a fresh scatter every burst.
OUR PORT TODAY
The whole chain is live-verified: a 382-impact night corpus matches
the original's cluster roll exactly, and the fuze and lock-drop are
benched. (Flight SPEED is the one thing that corpus measured against
the old dragless model -- see CHANGING below: speeds drop to the
original's governed values next build.)
Presentation deliberately differs: we draw the full ripple of N
rounds (the lead round carries the damage, the rest are tracers)
with a small tube spread. The damage economy is identical.
CHANGING IN THE NEXT BUILD -- AND WHAT TO WATCH
MISSILES SLOW DOWN, AND THIS IS THE ORIGINAL'S SPEED. The audit
found the original governs every missile with air drag that caps
its top speed -- an LRM tops out near 550, an SRM near 775. Our
rounds had no drag: an LRM was leaving its burn at over 3000,
five times the machine's speed, and flying nearly twice as far
downrange. Next build restores the governor. Missiles will feel
heavier, arcs will read slower, and long shots will take
noticeably longer to arrive -- that is the pod's real feel, not a
nerf. If missile flight FINALLY looks like you remember, say so;
that confirmation is worth as much as a bug report.
The bystander gap. The original's rounds swept everything in the
flight path every tick -- a third mech between shooter and target
could eat someone else's missile. Our rounds currently contact only
the locked target and the terrain: a mech in between gets flown
through, and only splash can touch it. This is the largest
remaining missile-physics gap; the fix is designed but not landed.
WATCH FRIDAY: if you take missile hits aimed at someone else --
beyond splash distance -- report it with the shooter's name.
-----------------------------------------------------------------------------
7. MISSILE SPLASH
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
Splash was LIVE on the pods -- every stock mission enables it. An
impact splashes a radius of about 30 around the hit point; strength
scales from the FULL rack size and falls off steeply with distance;
the mech that took the direct hit is excluded from its own splash.
One thing the code cannot prove either way: whether the SHOOTER was
protected from splashing themselves at point-blank.
OUR PORT TODAY
Same math, verified -- including the full-rack-size splash strength,
pinned to the original's code this week. We exclude both the direct
victim and the shooter. The on/off wiring differs slightly from the
original in a way that only matters for custom missions -- a site
option question for later, not a Friday one.
WE NEED YOUR MEMORY
- Do you remember splash hurting mechs standing NEAR an impact?
- Critically: could you hurt YOURSELF firing missiles point-blank
into a wall or an adjacent mech? Our port says no; the original's
code will not tell us. Your memory is the tiebreaker.
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
8. HOMING AGILITY, THE LRM ARC, AND THE NARC
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
- Three flight profiles exist in the shipped content:
LRM round: sluggish 60 deg/sec turn, 10-second burn, and the
LOFT -- at long range the seeker aims high and
the round flies the signature climb-then-plunge
arc.
Streak round: 360 deg/sec turn -- six times the LRM -- short
3-second burn, no loft. The round that out-turns
evasion.
Narc round: the surprise -- Streak-grade 360 deg/sec tracking
PLUS the loft PLUS the long 10-second burn. A
light rack that flies like a hawk.
- There is NO beacon mechanism anywhere in the machine. The Narc
never tagged anyone for anything; it is a light, hard-tracking
rack and nothing more.
- Several racks LABELED "LRM" actually feed Narc-profile rounds,
and at least one "SRM6" feeds Streak rounds. Two visually
identical racks homing very differently -- one sluggish, one
snapping onto target -- is authentic, not a bug.
- Rack names lie in other ways too, all authentic authored data:
an "SRM6" that fires 4 tubes, an "SRM4" that fires 2, LRM10s that
hit nearly twice as hard on one chassis as another.
OUR PORT TODAY
Same data-driven resolution -- the racks read the same content, so
all of the above carries over for free. One detail we are NOT
stating yet: which profile the 2-tube SRM racks fly -- our two
content reads disagree and the pass that settles it has not run.
CHANGING IN THE NEXT BUILD
Nothing mechanical.
WE NEED YOUR MEMORY
- Do you remember visually identical LRM racks homing very
differently on different mechs? And can you confirm the Narc did
nothing special -- no tag, no beacon, just a fast light rack?
- Do you remember SRM racks that fired FEWER missiles than their
name -- a 4-tube "SRM6", a 2-tube "SRM4"? Or LRM10s/LRM15s that
hit noticeably harder on one chassis than another?
- THE SRM RANGE PIP: the original PRINTED MANUAL shows the SRM
range mark at 450 in three separate display mockups; the game
content we have authors it at 800. That smells like the range
changed between software revisions. Which number do you remember
on the ladder when you selected an SRM rack -- 450 or 800?
(Range on a missile is display-only either way -- the rounds fly
on physics -- but your answer dates our content revision.)
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
9. AMMO, EJECT, AND JAMS (CANNONS AND RACKS)
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
- The reload countdown runs only while the weapon's generator route
is powered, and a HOT generator reloads slower. Below minimum
voltage the countdown freezes entirely.
- The EJECT control (novice pilots are locked out of it):
TAP = eject one round, and the weapon resets to loading if
rounds remain. This is the in-mission UNJAM.
HOLD (3 seconds) = dump the entire bay. The weapon is out of
ammo for good until re-arm or respawn.
- Jams roll after each shot while the weapon itself runs a heat
alarm; the chance grows with weapon temperature. Standard and
novice sims never heat-alarm a weapon -- so in practice only
veteran/expert pilots ever jammed.
OUR PORT TODAY
Same machine, tap-unjam included -- it is the mechanism behind
#166's "tap EJECT" advice. One divergence: our jam roll currently
waits for a higher weapon temperature than the original -- a
leftover safety from before the heat economy went live.
CHANGING IN THE NEXT BUILD
The leftover jam gate comes out. Warm cannons and racks on
veteran/expert may jam a touch more often. That is the authentic
rate.
WE NEED YOUR MEMORY
- Did weapons only ever jam for veteran/expert pilots? And could
you CLEAR a jam mid-sortie by tapping EJECT to cycle one round
out?
- Did a rack or cannon ever go permanently dark WITH ammo remaining
after sustained fire? At what firing intensity? (Field case from
our own nights: an SRM6 latched out with 19 rounds left. Whether
that could happen at authentic intensity is an open question.)
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
10. INCOMING-MISSILE WARNING
-----------------------------------------------------------------------------
THE ORIGINAL'S BEHAVIOR
Every missile launched at a mech announces itself to its target --
the machinery for a warning is there. What CONSUMED that
announcement (a warning tone? a beeper tempo change? a threat lamp?)
we have not located in the original's code.
OUR PORT TODAY
The beeper plumbing exists but nothing feeds it -- the warning is
structurally dead (#83 on the known-gaps list).
WE NEED YOUR MEMORY
- Did the pod audibly warn you of inbound missiles? A tone, a tempo
change, a lamp -- anything. If it did not, #83 closes for free.
If it did, describe it and we build exactly that.
YOUR NOTES (callsign first, one entry per line):
>
>
>
-----------------------------------------------------------------------------
APPENDIX A -- THE AUTHORED NUMBERS (straight from the shipped content)
-----------------------------------------------------------------------------
Range is in the same units your fire-control shows. Cycle in seconds.
ENERGY (damage = full-charge shot; scales with the square of charge)
WEAPON DMG RANGE CYCLE HEAT/SHOT
SLaser 1.5 150 1 0.75
ERSLaser 2 225 1 1.5
MLaser 2.5 350 2 2
ERMLaser 3.5 500 2 3.5
LLaser 5 600 4 6
ERLLaser 6 750 4 8.5
PPC 12 900 5 11
ERPPC 16 950 5 16
Reach shrinks with damage to the carrying zone: listed x (1 - zone
damage). An undamaged weapon reaches exactly the listed range.
BALLISTIC (one panel, decided at the trigger; recoil kick = dmg/16)
WEAPON DMG RANGE CYCLE NOTES
AFC25 7 900 2 feed timer = cycle (see sec. 4)
AFC50 13 750 4
AFC100 25 400 8 hardest hit, shortest reach
GAUSS 20 900 8 16 rounds, yellow pip, near cold
Cannons and racks run effectively cold next to the energy bank.
MISSILE RACKS (DMG = whole-salvo total; per-round in parentheses)
RACK TUBES DMG CYCLE
SRM2 2 10 (5.0) 2
SRM4 4 35 (8.75) 3
SRM4 4 20 (5.0) 3
SRM4 2 10 (5.0) 2
SRM6 6 50 (8.3) 5
SRM6 6 35 (5.8) 5
SRM6 4 20 (5.0) 3
LRM5 5 10 (2.0) 2.5
NRK5 5 10 (2.0) 2.5
LRM10 10 35 (3.5) 4
LRM10 10 20 (2.0) 4
LRM15 15 50 (3.3) 6
LRM15 15 35 (2.3) 6
LRM20 20 65 (3.25) 9
Same rack name, different numbers = different chassis fits, all
authentic. We are re-verifying the per-chassis map before naming
which mech carries which variant. SRM racks display range 800;
LRM/NRK racks display 6000 -- display only; real reach is flight
time. Cluster roll on impact: of n launched, between about n/4 and
n count as hits.
FLIGHT PROFILES (authored on the round, not the rack)
LRM round: turn 60 deg/s burn 10 s loft YES
Streak round: turn 360 deg/s burn 3 s loft no
Narc round: turn 360 deg/s burn 10 s loft YES
All rounds splash radius 30 on impact. After burnout a round coasts
up to 10 seconds before fizzling.
=============================================================================
WHEN YOU'RE DONE: post your edited copy of this file back to the
weapons thread. "MATCHES" alone in a notes block is real data. The
GAUSS and PPC-HIT questions are the two we cannot answer without you.
=============================================================================
+19
View File
@@ -1313,6 +1313,20 @@ static void
int mask = port->GetBitMask();
int palId = port->paletteID;
int tint = w->monoTint;
// OVERLAY COMPOSITE (2026-08-13): the radar's low byte is TWO ports -- sec
// (0x3F) + overlay (0xC0: the graticule art, the SCALE numeral and the
// SECTOR numerals). The arcade DAC indexed the full low byte and clut0
// already holds the combined 256-entry table (BuildSecondaryPalette spreads
// each port over the other's bit combinations), so the palette-expanded
// window composites both planes with just the OR'd mask -- previously the
// 0x3F mask left SECTOR:/SCALE: blank while their writers were live.
if (tint < 0)
{
L4GraphicsPort *ov =
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort("overlay"));
if (ov != NULL && ov->graphicsDisplay == port->graphicsDisplay)
mask |= ov->GetBitMask();
}
static int sMfdPal = -1;
if (sMfdPal < 0) sMfdPal = getenv("BT_GLASS_MFD_PAL") ? 1 : 0;
if (sMfdPal && tint >= 0)
@@ -2110,6 +2124,11 @@ static unsigned long
const char *ports[8];
int np = 0;
ports[np++] = w->portPrimary;
if (w->monoTint < 0)
ports[np++] = "overlay"; // composited into the palette-expanded
// radar (2026-08-13) -- the SECTOR/SCALE
// numerals tick on THIS plane, so their
// churn must mark the window dirty
if (w->portAlt != NULL) ports[np++] = w->portAlt;
for (int gi = 0; gi < w->groupCount && np < 7; ++gi)
{
+32 -3
View File
@@ -860,6 +860,31 @@ static const float kBTPanelH = 480.0f;
// channel-enabled and which is BlankColor; the draw loop below skips the
// blanked plane, so the dev cell shows exactly what the pod monitor shows.
// The Eng entries share their sibling's cell rect and FOLLOW it in the list.
//
// OVERLAY COMPOSITE (2026-08-13). The secondary CRT's low byte is authored as
// TWO ports -- sec (bits 0x3F, btspal) and overlay (bits 0xC0, btopal,
// TransparentZero) carrying the graticule art, the SCALE numeral (RadarRange)
// and the SECTOR numerals (sectorDisplay). The arcade DAC indexed the FULL low
// byte, blending both planes; every port expand used to mask to sec's 0x3F,
// which is why SECTOR:/SCALE: read blank on desktop while their reconstructed
// writers were live in the buffer the whole time. clut0 already holds the
// DAC's combined 256-entry table (BuildSecondaryPalette spreads each port's
// colors across the other's bit combinations), so the composite is exactly
// this mask -- no blending code.
//
static int BTSecCompositeMask(GaugeRenderer *gr, L4GraphicsPort *sec_port)
{
int mask = sec_port->GetBitMask();
if (gr != NULL)
{
L4GraphicsPort *ov =
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort("overlay"));
if (ov != NULL && ov->graphicsDisplay == sec_port->graphicsDisplay)
mask |= ov->GetBitMask();
}
return mask;
}
static const BTGaugeSurfaceDesc kBTGaugeSurfaces[9] =
{
{ "Heat", 0xFFFF, 0.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
@@ -946,7 +971,9 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
rot = s_secRot;
}
svga->DrawDevSurface(
device, i, port->GetBitMask(), port->paletteID, d.monoTint,
device, i,
d.monoTint < 0 ? BTSecCompositeMask(gr, port) : port->GetBitMask(),
port->paletteID, d.monoTint,
px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph, rot);
}
@@ -1322,7 +1349,9 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
}
int surf = ckSlotOf[i];
int useTint = (d.monoTint < 0) ? -1 : tint;
svga->DrawDevSurface(device, i, port->GetBitMask(), port->paletteID, useTint,
int useMask = (d.monoTint < 0) ? BTSecCompositeMask(gr, port)
: port->GetBitMask();
svga->DrawDevSurface(device, i, useMask, port->paletteID, useTint,
(float)L.surfX[surf], (float)L.surfY[surf], (float)L.surfW[surf], (float)L.surfH[surf], rot);
}
}
@@ -5833,7 +5862,7 @@ Logical SVGA16::Update(Logical forceAll)
//Drawing secondary, make sure we got secondary
if (secPort != NULL)
{
secMask = secPort->GetBitMask();
secMask = BTSecCompositeMask(renderer, secPort); // sec | overlay (DAC parity)
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
} else
{
+28 -259
View File
@@ -817,28 +817,6 @@ void
out = m->localOrigin.linearPosition; // safe non-garbage fallback (owner origin)
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BTWeaponMountHeight -- the @004bc3fc ctor's muzzle-height operand (velocity/
// dt integration audit 2026-08-13): the ProjectileWeapon ctor computes
// launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleTransform.y)
// (@004bc3fc part_013.c:16157-16164 -- FUN_004b9948 muzzle transform, then
// FUN_0040a968 extracts the translation and local_5c = its Y). The binary
// takes that Y at CTOR time = the bind-pose mount height; the port resolves
// lazily at first use (ctor runs mid-roster-build), so return the mount's
// height ABOVE THE MECH ORIGIN -- the same quantity for a standing mech,
// and terrain-independent. Complete-Mech-TU bridge per the databinding rule.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Scalar
BTWeaponMountHeight(void *ownerMech, int segIndex)
{
Mech *m = (Mech *)ownerMech;
if (m == 0)
return 0.0f;
Point3D p;
BTResolveWeaponMuzzle(ownerMech, segIndex, p);
return p.y - m->localOrigin.linearPosition.y;
}
// First vital damage-zone index (Mech__DamageZone::vitalDamageZone is protected; Mech has access).
int
Mech::FirstVitalZone() const
@@ -1768,29 +1746,11 @@ static void
// autocannon could not miss a moving target. Track the entity for the
// contact test; only GUIDED rounds also steer onto it.
extern int BTIsRegisteredMech(Entity *e);
if (p.target != 0 && BTIsRegisteredMech(p.target))
if (p.target != 0 && BTIsRegisteredMech(p.target)
&& !((Mech *)p.target)->IsMechDestroyed())
{
// #171 LOCK DROP predicate = @0049fb54 EXACTLY: movementMode 2||9,
// the latched death modes -- NOT IsMechDestroyed(), whose extra
// graphicAlarm>=9 term fires at the vital-kill TRIGGER, seconds
// before the collapse (modes 5-8) reaches 2/9. The binary seeker
// keeps TRACKING (and its 4.0u fuze stays live) all through the
// fall; it releases only once the wreck latches disabled
// (@004bef78: IsKindOf(Mech) && FUN_0049fb54 -> seeker+0xfc = 0).
const int mm = ((Mech *)p.target)->MovementMode();
if (!(mm == 2 || mm == 9))
{
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
p.targetPos.y += p.aimOffsetY;
}
else if (p.guided)
{
// Seeker released: the round flies on UNGUIDED; only geometry
// can detonate it now. (The old port kept steering to the
// corpse's last position and could still sphere-contact it.)
p.guided = 0;
p.target = 0; // fuze disarmed with the lock
}
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
p.targetPos.y += p.aimOffsetY;
}
if (p.guided)
{
@@ -1822,74 +1782,28 @@ static void
}
}
// #84 THRUSTER BURN + QUADRATIC DRAG (velocity/dt integration audit
// 2026-08-13). Binary model [T1]: Mover::PerformAndWatch @00422360
// ZEROES localAcceleration EVERY frame (`FUN_0040a7f4(+0x1dc,
// &DAT_004e0fd4)` = the T0 MOVER.cpp:672 `localAcceleration =
// Motion::Identity`), the MissileThruster's own Performance @004be474
// (the standalone MissileThrusterSimulation body -- recovered by the
// 2026-08-06 re-export) re-adds (0,0,-ThrusterAccel) while burning,
// and Missile::MoveAndCollide @4bef78 adds the Mover quadratic drag
// -COD*airDensity*sign(v)*v^2 per body axis, then integrates with dt
// in SECONDS (@00421bac ApplyWorldAccelerations: v += a*dt). Net:
// dv/dt = ThrusterAccel - COD*v^2 (burn; lateral vel zeroed)
// -- a clean variable-step ODE. There is NO myomer-style 28Hz
// per-tick accumulate in this lane: `speed += accel*dt` is the
// faithful form and needs no (dt*28) rescale.
// AUTHORED [T1, raw floats read from BTL4.RES type-15 records
// 2026-08-13]: linear CODs = 0.001 on ALL axes of ALL four models
// (srm rid=315, lrm 319, strk 323, nrk 327); airDensity = 1.0
// (engine default, LATTICE.cpp:413). Drag is NOT negligible -- it is
// the flight model's GOVERNOR: the Missile ctor @004bf5b4 precomputes
// terminal speed sqrt(ThrusterAccel/negCOD.z) into missile+0x348 for
// the seeker's lead time (part_013.c:18416). Terminal: LRM/Streak/
// NRK 547.7 u/s, SRM 774.6. The old "drag ~0.001 negligible" note
// (#84) let an LRM reach 3030 u/s by burnout -- 5.5x the binary cap
// (sim: binary 492 u/s at 800u vs undamped 695; port t+15% to 800u).
// Drag runs during burn AND coast (a burned-out round DECELERATES,
// v(t)=v0/(1+COD*v0*t)); thruster rounds only -- the AC round is the
// 0xBCD renderer tracer, a cosmetic with no Mover physics. Coast
// gravity ported below (audit close 2026-08-13). Still unported
// [T3 minor]: the binary's burnout end to velocity-slaving (lives
// beyond practical impact ranges -- see WEAPONS_DRIFT_AUDIT
// 'VELOCITY/DT INTEGRATION').
if (p.accel > 0.0f)
// #84 THRUSTER BURN: the binary Missile hosts a MissileThruster whose
// authored acceleration drives the round while BurnTime remains
// (MISTHRST: acceleration = (0,0,-thrusterAccel) in the missile frame,
// integrated by Missile::MoveAndCollide @4bef78). The pool flew at
// CONSTANT |MuzzleVelocity| -- the rack-eject speed (SRM 100, LRM 30
// u/s) -- which is why field missiles crawled (Oracle/Rajel, #84).
// Authored (BTL4.RES type-15 missile models, +0x44/+0x48): SRM
// burn 2.5s @ 600 u/s^2, LRM 10s @ 300, Streak 3s @ 300. Drag is
// authored ~0.001 (negligible at combat ranges) -- integrate accel
// along the current heading, un-damped, range-capped as before. [T1
// values / T2 integration]
if (p.burnLeft > 0.0f && p.accel > 0.0f)
{
Scalar burn_dt = (dt < p.burnLeft) ? dt : p.burnLeft;
if (burn_dt > 0.0f)
p.burnLeft -= burn_dt;
Scalar ns = p.speed + p.accel * burn_dt
- 0.001f * p.speed * p.speed * dt; // authored COD x airDensity 1.0 [T1]
if (ns < 1.0f) ns = 1.0f; // guard: drag alone never reverses flight
p.burnLeft -= burn_dt;
Scalar ns = p.speed + p.accel * burn_dt;
if (p.speed > 0.01f)
{
Scalar k = ns / p.speed;
p.vel.x *= k; p.vel.y *= k; p.vel.z *= k;
}
p.speed = ns;
// COAST GRAVITY (WEAPONS_DRIFT_AUDIT dt-table row closed
// 2026-08-13). The binary applies the environment gravity
// (default 6.5, FUN_00421e2c `vy -= g`) ONLY while COASTING --
// the @4bef78 else-branch; during burn the thruster re-adds
// thrust with NO gravity term onto the frame-zeroed accumulator
// [T1, decomp-reference flight-model bullet]. It is a fresh
// per-frame ACCELERATION (not an accumulate), so `vy -= 6.5*dt`
// is the faithful integration at any frame rate. coast_dt is
// this frame's non-burning remainder (exact at the burnout
// boundary). Applies to EVERY thruster round, lock or no lock
// -- it is Mover physics, not seeker logic (a lock-dropped
// round coasts ballistic and now DROPS instead of flying level).
Scalar coast_dt = dt - burn_dt;
if (p.burnLeft <= 0.0f && coast_dt > 0.0f)
{
p.vel.y -= 6.5f * coast_dt; // env gravityConstant default [T1]
Scalar sv = (Scalar)sqrtf((float)(p.vel.x*p.vel.x
+ p.vel.y*p.vel.y + p.vel.z*p.vel.z));
if (sv > 0.01f)
p.speed = sv; // keep speed == |vel| (the step
// length + normalizations below
// divide by p.speed)
}
}
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt;
p.age += dt;
@@ -1978,154 +1892,25 @@ static void
// detonation being flung past the target at high speed.
Point3D hitPos = p.pos;
Scalar contactD2;
Scalar tTgt = 0.0f; // target's nearest-approach segment param
// (the bystander sweep compares against it)
{
const Scalar sx = p.pos.x - prev.x, sy = p.pos.y - prev.y, sz = p.pos.z - prev.z;
const Scalar seg2 = sx*sx + sy*sy + sz*sz;
Scalar t = 0.0f;
if (seg2 > 1.0e-6f)
{
const Scalar wx = p.targetPos.x - prev.x,
wy = p.targetPos.y - prev.y,
wz = p.targetPos.z - prev.z;
tTgt = (wx*sx + wy*sy + wz*sz) / seg2;
if (tTgt < 0.0f) tTgt = 0.0f; else if (tTgt > 1.0f) tTgt = 1.0f;
t = (wx*sx + wy*sy + wz*sz) / seg2;
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
}
hitPos.x = prev.x + tTgt*sx; hitPos.y = prev.y + tTgt*sy; hitPos.z = prev.z + tTgt*sz;
hitPos.x = prev.x + t*sx; hitPos.y = prev.y + t*sy; hitPos.z = prev.z + t*sz;
const Scalar cx = p.targetPos.x - hitPos.x,
cy = p.targetPos.y - hitPos.y,
cz = p.targetPos.z - hitPos.z;
contactD2 = cx*cx + cy*cy + cz*cz;
}
// BYSTANDER SWEEP (WEAPONS_DRIFT_AUDIT lane 12, closed 2026-08-13).
// The binary's per-tick contact test is a WORLD sweep (FUN_0042291c,
// called from Missile::MoveAndCollide @004bef78): every solid in the
// flight path can detonate the round -- including a third mech between
// shooter and target [T1]. The port pool tested only the locked
// target sphere + the terrain ray, so a mech standing in someone
// else's crossfire was flown through (splash alone could touch it).
// Port shape [T3 -- radius approximation of the real solid sweep]:
// nearest-approach of this frame's flight segment vs a VERTICAL
// CAPSULE per other registered mech. The capsule is derived from the
// mech's OWN collision template (Mover::GetCollisionTemplate, the same
// BoxedSolid whose maxY is CylinderReferenceHeight): axis = mech
// origin up template minY..maxY, radius = the template's larger
// horizontal half-extent (the axis ignores the template's near-zero
// horizontal center offset, so no yaw rotation is needed). Fallback
// when the template is unresolved: 3.5u x 14u [T3 fixed constants,
// fielded-mech scale -- CylinderReferenceHeight ~14]. Wrecks stay in
// the sweep: the binary detonates on ANY solid, and the victim-side
// zone-state guard (#174) keeps a dead zone from cascading or scoring.
// Cheap by construction: damage-carrying GUIDED rounds only (= the one
// salvo lead; the N-1 visual tracers are damage-0), per-mech
// distance cull, shooter excluded by the registry helper, locked
// target excluded (its own 4.0u fuze above is the authentic test).
Entity *bysMech = 0;
Scalar bysT = 2.0f;
Point3D bysHit = p.pos;
if (p.guided && p.damage > 0.0f)
{
extern int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut);
Entity *cand[32];
const int nc = BTGetTargetCandidates(p.shooter, cand, 32); // excludes the shooter
const Scalar sx = p.pos.x - prev.x, sy = p.pos.y - prev.y,
sz = p.pos.z - prev.z;
const Scalar seg2 = sx*sx + sy*sy + sz*sz;
const Scalar step = p.speed * dt;
for (int ci = 0; ci < nc && seg2 > 1.0e-6f; ++ci)
{
Entity *e = cand[ci];
if (e == 0 || e == p.target || !BTIsRegisteredMech(e))
continue;
Mech *m = (Mech *)e;
Point3D mp = m->localOrigin.linearPosition;
// distance cull: farther than this frame's step + the largest
// plausible capsule reach -> untouchable this frame
const Scalar cdx = mp.x - prev.x, cdz = mp.z - prev.z;
const Scalar cull = step + 24.0f;
if (cdx*cdx + cdz*cdz > cull*cull)
continue;
Scalar rr, cy0, cy1;
BoxedSolid *tmpl = m->GetCollisionTemplate();
if (tmpl != 0 && tmpl->maxY > tmpl->minY)
{
const Scalar hx = 0.5f * (tmpl->maxX - tmpl->minX);
const Scalar hz = 0.5f * (tmpl->maxZ - tmpl->minZ);
rr = (hx > hz) ? hx : hz;
cy0 = mp.y + tmpl->minY;
cy1 = mp.y + tmpl->maxY;
}
else
{
rr = 3.5f; cy0 = mp.y; cy1 = mp.y + 14.0f; // [T3] fixed fallback
}
if (!(rr > 0.5f)) rr = 0.5f; // degenerate/garbage template
if (rr > 10.0f) rr = 10.0f; // guards (NaN falls to 0.5)
// closest points between the flight segment prev+t*(sx,sy,sz)
// and the vertical axis segment (mp.x, cy0..cy1, mp.z) --
// standard segment-segment closest point (RTCD 5.1.9 with
// d2 = (0,h,0))
const Scalar h = cy1 - cy0;
const Scalar rx = prev.x - mp.x, ry = prev.y - cy0,
rz = prev.z - mp.z;
const Scalar ee = h*h;
const Scalar f = h*ry;
const Scalar c = sx*rx + sy*ry + sz*rz;
const Scalar b = sy*h;
const Scalar den = seg2*ee - b*b;
Scalar t = (den > 1.0e-6f) ? (b*f - c*ee) / den : 0.0f;
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
Scalar s = (ee > 1.0e-6f) ? (b*t + f) / ee : 0.0f;
if (s < 0.0f) { s = 0.0f; t = -c / seg2; }
else if (s > 1.0f) { s = 1.0f; t = (b - c) / seg2; }
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
const Scalar qx = prev.x + t*sx - mp.x;
const Scalar qy = prev.y + t*sy - (cy0 + s*h);
const Scalar qz = prev.z + t*sz - mp.z;
if (qx*qx + qy*qy + qz*qz < rr*rr && t < bysT)
{
bysT = t;
bysMech = e;
bysHit.x = prev.x + t*sx;
bysHit.y = prev.y + t*sy;
bysHit.z = prev.z + t*sz;
}
}
}
// #171 FUZE RADIUS: a GUIDED damage round detonates at the binary's
// proximity fuze _DAT_004bf5a4 = 4.0 (seeker rangeToTarget +0x10C <
// 4.0, @004bef78 -- the SAME constant the old steering comment
// misattributed as "MissileTurnGain"; the 4.0/8.0 turn rates above are
// PORT TUNING [T3], not that constant). Cosmetic rounds (damage 0:
// AC tracers, cluster visuals) keep the wider 10u so their burst
// visuals still read at speed.
const Scalar fuzeR = (p.guided && p.damage > 0.0f) ? 4.0f : 10.0f;
int contact = (contactD2 < fuzeR * fuzeR);
Entity *victim = p.target;
if (bysMech != 0 && (!contact || bysT < tTgt))
{
// a third mech sits EARLIER on this frame's flight segment than
// the locked target's fuze point -- it takes the round (the
// binary's sweep detonates on the FIRST solid in the path). The
// struck mech becomes the DIRECT victim; the salvo cluster roll
// and the splash package below run unchanged against it.
contact = 1;
victim = bysMech;
hitPos = bysHit;
static int s_bysPrints = 0; // capped receipt (field forensics)
if (s_bysPrints < 24)
{
++s_bysPrints;
DEBUG_STREAM << "[projectile] BYSTANDER id="
<< (int)bysMech->GetEntityID()
<< " t=" << bysT
<< " at(" << bysHit.x << "," << bysHit.y << "," << bysHit.z
<< ") dmg=" << p.damage
<< " lockedTgt=" << (void *)p.target
<< ((s_bysPrints == 24) ? " (receipt cap reached)" : "")
<< "\n" << std::flush;
}
}
const int contact = (contactD2 < (10.0f*10.0f));
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
{
if (getenv("BT_PROJ_LOG"))
@@ -2150,13 +1935,10 @@ static void
<< "," << p.targetPos.z << ")" << std::endl;
if (p.salvoLead) // ONE Explosion per salvo (@004bcc60)
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hitPos);
Entity *tgt = victim;
// Deliver to the struck mech -- normally the launcher's locked
// target (the shooter's 0x388 slot; any peer mech in MP, task #46),
// or the BYSTANDER the sweep above found first on the flight
// segment (lane 12: it becomes the direct victim, exactly as if it
// had been the lock). A replicant victim reroutes cross-pod via
// Dispatch.
Entity *tgt = p.target;
// Deliver to the projectile's target mech -- the launcher set p.target
// from the shooter's 0x388 slot (the picked victim; any peer mech in
// MP, task #46). A replicant target reroutes cross-pod via Dispatch.
extern int BTIsRegisteredMech(Entity *e);
if (tgt != 0 && BTIsRegisteredMech(tgt) && p.damage > 0.0f)
{
@@ -2196,22 +1978,9 @@ static void
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
if (bursts > 1)
{
// #171: the binary roll (part_013.c:18272-18281) has NO
// floor-of-1 -- rolled = min(Random(n) + n/4, n), and
// Random(n) is 0..n-1, so n<4 can roll ZERO. The
// VICTIM side is what floors it: the binary burst loop
// is a DO-WHILE (part_012.c:14660-14685 -- local_2c =
// burstCount, apply, decrement, `if (local_2c < 1)
// break` AFTER the body), so a 0-burst message still
// applies ONE burst; mech.cpp:1229's `burstsLeft < 1
// -> 1` guard is the faithful mirror of that shape.
// The old sender-side floor made the end-to-end count
// identical, but shipped a message field the binary
// never sends (burstCount 1 where the binary says 0);
// roll it exactly as the binary does and let the
// receiver's do-while supply the floor.
int rolled = Random(bursts) + (bursts >> 2);
if (rolled > bursts) rolled = bursts;
if (rolled < 1) rolled = 1;
bursts = rolled;
}
dmg.burstCount = bursts;
+5 -15
View File
@@ -14,21 +14,11 @@
//
// Coverage:
// confident : ctor @004be7c4, dtor thunk @004be8bc, TestInstance @004be8e8,
// CreateStreamedSubsystem @004bf8ec,
// MissileThrusterSimulation @004be474 -- the STANDALONE
// Performance body, RECOVERED by the 2026-08-06 re-export
// (the old "folded into Missile::MoveAndCollide, no distinct
// @ADDR survives" note here was stale). @004be474 does BOTH
// jobs each frame: (1) slerp the owner Missile's orientation
// toward the Seeker aim point (+0xE4), rate-capped at
// maxThrusterRotationRate*dt; (2) while ThrusterBurning:
// burnTimeRemaining -= dt and owner->localAcceleration +=
// this->acceleration (0,0,-thrusterAccel) -- onto the
// accumulator Mover::PerformAndWatch @00422360 zeroes every
// frame, i.e. a constant acceleration, dt-correct. The
// simplified body below remains the dormant-TU stand-in;
// the LIVE port path is the mech4.cpp pool (see
// docs/WEAPONS_DRIFT_AUDIT.md 'VELOCITY/DT INTEGRATION').
// CreateStreamedSubsystem @004bf8ec
// best-effort: MissileThrusterSimulation -- the Performance method pointer is
// installed by the ctor (PTR_LAB_00512b20) but its standalone body
// is folded into the host Missile::MoveAndCollide @004bef78 (which
// samples the thruster acceleration buffer at missile+0x234/+0x250).
// excluded : the 0x41xxxx engine vtable slots (pure Subsystem base behaviour;
// vtable @00512bbc overrides only slot0 = destructor)
//
+30 -148
View File
@@ -28,14 +28,11 @@
// ProjectileWeaponSimulation @004bbd04 (Performance -- FULLY RECOVERED
// by capstone disasm, Gitea #12 2026-07-19; see context/decomp-reference.md
// s5 -- the old RivetGun-modeled body is retired).
// FireWeapon @004bc104 (slot 18) -- RECOVERED by the 2026-08-06
// re-export (part_013.c:16023-16125; trigger-time hitscan, implemented
// in the #171 batch. The old "BODY NOT recovered" line here was the
// stale banner behind the #168 mis-fix -- see the FireWeapon block).
// best-effort (vtable slot proven, function prologue present, body still
// unrecovered -- reconstructed from context and clearly marked):
// GetStatusFlags @004bbf88 (slot 12), UpdateWeaponState @004bbc20
// (slot 16).
// best-effort (vtable slot proven, function prologue present, BODY NOT
// recovered by the decompiler -- bodies below are reconstructed from context
// and clearly marked):
// FireWeapon @004bc104 (slot 18), GetStatusFlags @004bbf88 (slot 12),
// UpdateWeaponState @004bbc20 (slot 16).
// excluded (belong to sibling/derived classes, NOT ProjectileWeapon):
// Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
// MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
@@ -48,9 +45,6 @@
// _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling)
// _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel)
// _DAT_004bb3b4 = 0000003f = 0.5f
// _DAT_004bc678 = 00000040 = 2.0f (tracer fall-time factor; audit 2026-08-13)
// _DAT_004bc67c = 8a6c379943c6fad0fb3f = 1/9.8 as 80-bit x87 EXTENDED
// (tracer gravity term; audit 2026-08-13)
// DAT_004e0f74 = {0,0,0} (zero vector)
// DAT_004e0fd4 = {0,0,0} (zero point)
// 0x40400000 = 3.0f (TotalTimeToEject default)
@@ -151,18 +145,8 @@ namespace {
static const Scalar _DAT_004bc100 = 1.0f; // time-of-flight bias
static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient
static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling
// Tracer launch-speed constants -- BYTE-CONFIRMED by the 2026-08-13 weapons
// audit (WEAPONS_DRIFT_AUDIT.md entry 7 / evidence log 3); the old 1.0f
// "unresolved" stand-ins are RETIRED:
// @0x4bc678 = 40000000 = 2.0f (float32)
// @0x4bc67c = 8a6c379943c6fad0fb3f = 0.10204081... = 1/9.8, stored as an
// 80-bit x87 EXTENDED (not float32-decodable -- why it sat
// "unresolved"). Together with the ctor's muzzle-height term:
// launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleHeight)
// i.e. the tracer crosses effectiveRange in the time a shell would fall
// from muzzle height under g=9.8 (~1s for a ~5u mount).
static const Scalar _DAT_004bc678 = 2.0f; // fall-time factor (2h/g)
static const Scalar _DAT_004bc67c = 0.10204081f; // 1/9.8 (x87 extended in the image)
static const Scalar _DAT_004bc678 = 1.0f; // muzzle-speed term (unresolved)
static const Scalar _DAT_004bc67c = 1.0f; // muzzle-speed term (unresolved)
// FUN_00408050 -- uniform [0,1) random (jam roll). The old `return 0.0f` stub ALWAYS
// jammed (0 < any positive jam chance), so a projectile weapon could never fire. A real
@@ -327,16 +311,11 @@ ProjectileWeapon::ProjectileWeapon(
minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; // +0x1C8 -> 0x404
tracerCounter = 0; // 0x408
// launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed),
// muzzleSpeed = sqrt((1/9.8) * 2.0 * muzzleHeight) -- @004bc3fc:16157-16164
// resolves the muzzle transform (FUN_004b9948) INSIDE the ctor and uses its
// translation Y. The port DEFERS the z term to first use
// (ResolveLaunchVelocity below): our ctor runs mid-roster-build, where the
// segment resolve can fall back to the mech origin and would bake a garbage
// constant. z == 0 is the unresolved sentinel (the binary's z is always
// negative after its ctor).
// launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed)
launchVelocity = Vector3D(0,0,0); // FUN_00408440(this+0x410, DAT_004e0f74)
Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678); // FUN_004dd138
tracerOrigin = Vector3D(0,0,0); // FUN_00408440(this+0x42c, DAT_004e0f74)
launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed)
totalTimeToEject = 3.0f; // 0x3F0 (0x40400000)
timeToEject = totalTimeToEject; // 0x3F4
@@ -727,30 +706,6 @@ Logical
return (p > UniformRandom()) ? True : False; // FUN_00408050
}
//
// The @004bc3fc ctor launch-speed expression, deferred to first use (see the
// ctor note). Byte-grounded constants (audit 2026-08-13): @0x4bc678 = 2.0f,
// @0x4bc67c = 1/9.8 (80-bit x87 extended `8a6c379943c6fad0fb3f`):
// launchVelocity.z = -effectiveRange / sqrt((1/9.8) * 2.0 * muzzleHeight)
// The binary computes it once at ctor time (bind pose, effectiveRange still ==
// authored WeaponRange); the port computes it once at first use with the mount
// height above the mech origin -- the same quantity for a standing mech.
//
void
ProjectileWeapon::ResolveLaunchVelocity()
{
if (launchVelocity.z != 0.0f)
return; // already resolved
extern Scalar BTWeaponMountHeight(void *ownerMech, int segIndex);
Scalar muzzleHeight = BTWeaponMountHeight(owner, GetSegmentIndex());
if (muzzleHeight < 0.5f)
muzzleHeight = 0.5f; // port guard: unresolved segment (muzzle==origin
// fallback) -- the binary's bind-pose height is
// always positive; keeps sqrt/divide sane
Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678 * muzzleHeight); // FUN_004dd138
launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed)
}
//
// @004bc06c -- lead-solution time-of-flight. Sample the current target
// position, advance it by launchVelocity, and re-run targeting. With no target
@@ -760,7 +715,6 @@ void
Scalar
ProjectileWeapon::ComputeTimeOfFlight()
{
ResolveLaunchVelocity(); // deferred @004bc3fc z term
GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420)
leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410
leadPosition.y += launchVelocity.y;
@@ -1015,27 +969,10 @@ void
//
// @004bc104 -- slot 18 FireWeapon (overrides MechWeapon's pure-virtual trap
// @004ba45c).
//
// STALE-BANNER CORRECTION (2026-08-13, the #171 hunt): this comment said
// "Body NOT recovered by the decompiler" -- TRUE of the 2025 export, FALSE
// since the 2026-08-06 re-export (part_013.c:16023-16125), and the stale
// banner is the ROOT CAUSE of the #168 mis-fix: the ballistic model was
// generalized from the MISSILE launcher @004bcc60 instead of this body.
// The RECOVERED truth [T1]: the AC FireWeapon is TRIGGER-TIME HITSCAN --
// it spawns NOTHING (no shell entity; class 0xBD1 is dead code; the visible
// round is the 0xBCD renderer tracer). It batches the round counter
// (+0x408) and every TracerInterval-th round (+0x438) computes
// dist = |mech aimPoint(+0x37c) - muzzle| and, gated dist <= LIVE
// effectiveRange (+0x328, the same gate shape as the Emitter), sends ONE
// single-panel Damage (amount = batchedCount x DamageAmount) to the LOCKED
// target *(mech+0x388) via SendDamageMessage @004b9728 -- at fire time.
// A locked AC inside range cannot miss; out of range the batch silently
// discards (tracer still flies). The body below IMPLEMENTS that hitscan
// (landed in the #171 batch, bbb3b4e; the earlier "still carries the #168
// flight-contact model" sentence here outlived the change -- the exact
// stale-banner genus this block documents). MissileLauncher (@004bcc60)
// is the genuinely-flying family (Model B) and overrides this slot.
// @004ba45c). Body NOT recovered by the decompiler (prologue confirmed at
// 0x4bc104, immediately following the 1.0f literal @0x4bc100). Spawns the
// generic projectile/tracer. MissileLauncher overrides this again at @004bcc60
// to launch Missile entities.
//
// Gitea #12: the view/target gate, the ammo pull and the recoil set are
// STRIPPED from this body -- they belong to the CALLER (the recovered
@@ -1049,9 +986,10 @@ void
{
Check(this);
// (ComputeTimeOfFlight @004bc06c is called INSIDE the tracer branch below,
// exactly where @004bc104 calls it -- :16091, between the muzzle resolve and
// the range gate. The old top-of-body placement predated the re-export.)
// Refresh the lead solution (leadPosition / targeting) for the spawn below
// (@004bc06c; the recovered sim no longer calls it -- best-effort placement
// inside the unrecovered @4bc104 body, which is where the lead data is used).
ComputeTimeOfFlight(); // @004bc06c
// THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
@@ -1093,82 +1031,27 @@ void
}
}
//
// #171 (2026-08-13) -- THE AUTHENTIC AC: TRIGGER-TIME HITSCAN (@004bc104
// [T1], the body the stale banner hid; the prior flying-damage round was
// the #168 mis-generalization from the MISSILE launcher). Binary shape:
// increment the round counter (+0x408); every TracerInterval-th (+0x438)
// round, dist = |owner aimPoint(+0x37c) - muzzle| gated against the LIVE
// effectiveRange (+0x328, the same gate the Emitter uses) -> ONE
// single-panel Damage (amount = batchedCount x DamageAmount) to the
// LOCKED target *(mech+0x388) via SendDamageMessage @004b9728, AT FIRE
// TIME. Out of range: the batch silently discards. A locked AC inside
// range cannot miss; nothing about the hit rides the visible round.
// (This also restores the binary's ONE-message-per-batch = single-panel
// damage -- closing #112's multi-zone shotgun, which was the port's
// per-round zone re-roll scatter.)
//
// WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port
// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
// Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|.
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
// The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's
// targeting block writes; a consistent producer/consumer pair on our
// compiled object). Task #41: the slots hold the WORLD PICK -- the enemy
// mech under the boresight, or the terrain downrange (a missile fired at
// the scenery flies to the ground point and detonates without damage), or
// null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate
// wouldn't have fired either).
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
++tracerCounter; // @0x408 -- incremented BEFORE the test
// @4bc104:16079: bVar1 = (interval != 0) && (counter % interval == 0).
// interval==0 -> bVar1 never set: NO damage ever, counter never resets
// (and the short-circuit means no divide) -- the port matches exactly.
if (tracerInterval != 0 && (tracerCounter % tracerInterval) == 0)
{
// @4bc104:16088-16091: muzzle transform (@004b9948, resolved above as
// `muzzle`), then the lead/targeting refresh -- INSIDE the tracer
// branch, not per-round. (Also re-derives effectiveRange via
// UpdateTargeting, so the gate below reads the LIVE value.)
ComputeTimeOfFlight(); // @004bc06c
Vector3D toAim;
toAim.Subtract(targetPos, muzzle); // :16096 FUN_00408644(aimPoint(+0x37c), muzzle)
Scalar dist = (Scalar)sqrtf(toAim.x*toAim.x + toAim.y*toAim.y + toAim.z*toAim.z);
if (dist <= effectiveRange && target != 0) // :16098 gate (+0x328); the null test is the
{ // port's guard (binary trusts 0x388!=0 upstream)
// @4bc104 fills EXACTLY TWO fields of the batched record (:16100-16120):
// damageAmount = counter x authored amount (:16110)
// damageForce = rootSegRot . leadPosition(+0x420) x float(+0x41c)
// impactPoint is NOT set here -- SendDamageMessage @004b9728 sets it
// (FUN_00408440(dmg+0x20, mech+0x37c) = the aim point); the port's
// SendDamageMessage doesn't do that write, so it is FOLDED here as
// impactPoint = targetPos (net-identical). burstCount RIDES the
// authored record untouched (Damage ctor = 1 for an AC; only
// MissileLauncher re-authors it, and that class overrides FireWeapon).
// damageForce divergence [T3, deliberate]: the binary's value is the
// lead point rotated by the mech root-segment matrix scaled by the
// float at +0x41c (a dword lifted from the tracer-model record --
// direction-equivalent junk; gyro rattle consumes DIRECTION only,
// and the Emitter's own path authors force = target - muzzle
// (emitter.cpp:360 @0x3B0)), so the port uses the same toAim shape.
Scalar batched = (Scalar)tracerCounter * damageData.damageAmount; // :16110
Damage batchSave = damageData; // binary builds a LOCAL copy (:16100);
damageData.damageAmount = batched; // the port mutates+restores because our
damageData.impactPoint = targetPos; // SendDamageMessage reads the member
damageData.damageForce = toAim;
SendDamageMessage((Entity *)target); // @004b9728 -- at fire time
damageData = batchSave;
}
tracerCounter = 0; // :16123 -- resets on HIT and MISS alike,
} // but ONLY on tracer rounds
// The VISIBLE round: the binary's 0xBCD renderer tracer, cosmetic only
// (the shell entity 0xBD1 is dead code -- nothing shipped ever spawns a
// damage-carrying ballistic round). Fly the pool round as that visual:
// damage 0, straight at the pick, authentic muzzle speed
// (= effRange / sqrt(2*(1/9.8)*muzzleHeight), the recovered @004bc3fc
// constants -- see ResolveLaunchVelocity).
ResolveLaunchVelocity();
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
+ launchVelocity.y*launchVelocity.y
+ launchVelocity.z*launchVelocity.z);
BTPushProjectile(muzzle, o, target, targetPos, speed, 0.0f /*cosmetic tracer*/,
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/,
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
0 /*splash_burst*/, -1 /*muzzle_seg*/,
(int)damageData.damageType);
(int)damageData.damageType /*issue: autocannon = Ballistic, was Explosive*/);
// task #61: mark this shot for REPLICATION so the peer sees the enemy's
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
@@ -1228,7 +1111,6 @@ void
// plus the DAFC muzzle flash.
Point3D mz;
GetMuzzlePoint(mz); // @004b9948
ResolveLaunchVelocity(); // deferred @004bc3fc z term
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
+ launchVelocity.y*launchVelocity.y
+ launchVelocity.z*launchVelocity.z);
-8
View File
@@ -283,14 +283,6 @@ class NotationFile;
Scalar
ComputeTimeOfFlight();
// The @004bc3fc ctor launch-speed expression
// (launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleHeight);
// constants byte-confirmed @0x4bc678/@0x4bc67c, audit 2026-08-13),
// deferred to first use because the port ctor runs mid-roster-build.
// z == 0 is the unresolved sentinel; no data member added (layout locked).
void
ResolveLaunchVelocity();
// --- cross-family isolation helpers (owning Mech subsystem roster +
// cockpit controls live in the mech/controls families). Declared
// here so the recovered bodies compile; the mech family wires the
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env python
# Forensics for the 11 [projectile] BYSTANDER receipts of the weapons_sweep
# run: classify every DET lock pointer by aim-point motion (a mech lock TRACKS
# the walking enemy; a scenery/wreck lock is stationary), then test each
# receipt: struck id vs the shooter's own id, det-to-aim distance, and the
# paired IMPACT delivery.
import io
import math
import re
RX_DET = re.compile(r"\[projectile\] DET at\(([^)]*)\) dmg=([\d.eE+\-]+) "
r"lead=(\d+) tgt=(\S+) aim\(([^)]*)\)")
RX_BYS = re.compile(r"\[projectile\] BYSTANDER id=(\d+) t=(\S+) at\(([^)]*)\) "
r"dmg=([\d.eE+\-]+) lockedTgt=(\S+)")
RX_RST = re.compile(r"\[respawn\] Mech::Reset (\d+):(\d+) ")
def p3(s):
a = [float(x) for x in s.split(",")]
return a
def dist(a, b):
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
for fn in ("ws_a.log", "ws_b.log"):
lines = io.open("/c/git/bt411/content/" + fn.replace("/", ""),
encoding="latin-1", errors="replace").read().splitlines() \
if False else io.open("C:/git/bt411/content/" + fn,
encoding="latin-1", errors="replace").read().splitlines()
own = None
dets = {} # tgt ptr -> list of (lineno, aim)
bys = [] # (lineno, id, at, dmg, lockedTgt)
for i, l in enumerate(lines):
m = RX_DET.search(l)
if m and m.group(4) != "0x0" and float(m.group(2)) > 0.0:
dets.setdefault(m.group(4), []).append((i, p3(m.group(5))))
continue
m = RX_BYS.search(l)
if m:
bys.append((i, int(m.group(1)), p3(m.group(3)),
float(m.group(4)), m.group(5)))
continue
m = RX_RST.search(l)
if m and own is None:
own = int(m.group(2))
print("=== %s (own mech entity=%s) ===" % (fn, own))
# classify pointers: max pairwise aim distance within a 1500-line window
cls = {}
for ptr, lst in sorted(dets.items(), key=lambda kv: -len(kv[1])):
move = 0.0
for j in range(1, len(lst)):
if lst[j][0] - lst[j - 1][0] <= 1500:
d = dist(lst[j][1], lst[j - 1][1])
if d > move:
move = d
cls[ptr] = "MECH-TRACKING" if move > 60.0 else "STATIONARY"
print(" lock %s damage-DETs=%3d max local aim motion=%7.1fu -> %s"
% (ptr, len(lst), move, cls[ptr]))
ok = True
for (i, mid, at, dmg, ptr) in bys:
# find this receipt's own DET (next damage DET line)
aim = None
imp = False
for j in range(i, min(i + 4, len(lines))):
m = RX_DET.search(lines[j])
if m and aim is None:
aim = p3(m.group(5))
if "[projectile] IMPACT damage=" in lines[j]:
imp = True
d_aim = dist(at, aim) if aim else -1.0
verdict = []
if mid == own:
verdict.append("SELF-STRIKE (geometry bug)")
if cls.get(ptr) == "MECH-TRACKING":
verdict.append("LOCK WAS THE MECH (exclusion failed)")
if aim is not None and d_aim < 30.0:
verdict.append("det at the lock's own aim point")
if not imp:
verdict.append("no paired IMPACT delivery")
tag = "TRUE-POSITIVE" if not verdict else "FALSE-POSITIVE: " + "; ".join(verdict)
if verdict:
ok = False
print(" bys@%-6d struck=%d dmg=%g lock=%s(%s) det-to-aim=%.0fu imp=%d %s"
% (i + 1, mid, dmg, ptr, cls.get(ptr, "?"), d_aim, imp, tag))
print(" %s: %s" % (fn, "all receipts TRUE positives" if ok
else "FALSE POSITIVES PRESENT"))
-533
View File
@@ -1,533 +0,0 @@
#!/usr/bin/env python
# weapons_sweep checker -- ONE comprehensive per-family PASS/FAIL adjudicator
# for scratchpad/night16/weapons_sweep.sh (2-node mad2-vs-madcat fight).
#
# Families / assertions (env on both nodes: BT_DMG_LOG BT_PROJ_LOG BT_DEATH_LOG
# BT_AMMO_LOG):
# A1 ENERGY : named [emitter] FIRED lines present AND laser/PPC [dmghit]
# (type 3/4) land on the victim.
# A2 AC : type=1 [dmghit] present, ALL of them burst=1 (single panel,
# trigger-time hitscan); ZERO damage-carrying ballistic rounds in
# the pool ([projectile] PUSH guided=0 must all be dmg=0 -- the
# #171 restoration: the visible round is the cosmetic 0xBCD
# tracer); every damage-carrying DET is reconciled to a guided
# (missile) push.
# A3 MISSILE: [ammo] LRM/SRM FIRED receipts, [projectile] IMPACT dmg>0
# deliveries with burst>1 salvo bursts, type=2 [dmghit]s landing
# (burst>1 present) -- contact through the 4.0u fuze at
# drag-governed speeds; plus >=1 death or zone cascade (vitality).
# A4 NO-REGR: no zone cascades twice within one life per log (the #174
# guard); every [projectile] BYSTANDER receipt in the 2-mech
# fight is a TRUE positive of the lane-12 sweep.
# CALIBRATION NOTE (2026-08-13 run): the original "zero receipts
# in a 2-mech fight" operationalization assumed a lock is always
# the enemy mech. The run disproved the ASSUMPTION, not the
# code: the production aim ray designates SCENERY constantly
# (the ac_bench lesson), and rounds locked onto a structure DO
# fly past the other mech -- the binary's world sweep detonates
# on ANY solid in the path (FUN_0042291c via @004bef78 [T1]), so
# a receipt there is the sweep WORKING. Forensics on all 11
# receipts of the calibration run (bys_forensics.py): struck ==
# the other live mech every time, aim point 146-1016u away from
# the strike (the round was flying elsewhere), one paired IMPACT
# delivery each. The check now asserts the actual false-positive
# conditions per receipt:
# - struck id == the shooter's OWN mech -> self-strike bug
# - struck id not a known player mech -> phantom strike
# - no DET/IMPACT pairing -> lost/duplicated delivery
# - strike within 30u of the round's aim -> the sweep raced the
# authentic 4.0u fuze at the lock itself (exclusion suspect)
# and flags a hit receipt-print cap (24) as unmeasurable.
# A5 FLOORS : rig-sanity minimums so a quiet rig cannot PASS.
#
# --selftest: prove the detector CAN fail (the week's hard rule) by feeding it
# synthetic pre-fix log shapes (pre-#171 damage-carrying ballistic round,
# pre-#174 double cascade, a bystander receipt, and an empty/quiet rig) and
# requiring each to FAIL the matching assertion.
import io
import math
import re
import sys
RX_DMGHIT = re.compile(r"\[dmghit\] mech=(\S+) zone=(-?\d+) vital=(\d+) "
r"type=(\d+) amt=([\d.eE+\-]+) burst=(\d+)")
RX_PUSH = re.compile(r"\[projectile\] PUSH target=(\S+) len=(\S+) speed=(\S+) "
r"dmg=([\d.eE+\-]+) guided=(\d+)")
RX_DET = re.compile(r"\[projectile\] DET .*?dmg=([\d.eE+\-]+) lead=(\d+)")
RX_IMPACT = re.compile(r"\[projectile\] IMPACT damage=([\d.eE+\-]+) .*?burst=(\d+)")
RX_EMIT = re.compile(r"\[emitter\] FIRED '([^']+)'")
RX_AMMO = re.compile(r"\[ammo\] (\S+) FIRED, rounds left=")
RX_CASC = re.compile(r"\[cascade\] zone (\d+) DESTROYED")
RX_BYS = re.compile(r"\[projectile\] BYSTANDER id=(\d+) t=(\S+) at\(([^)]*)\) "
r"dmg=([\d.eE+\-]+) lockedTgt=(\S+)")
RX_DETAIM = re.compile(r"\[projectile\] DET at\(([^)]*)\).*?aim\(([^)]*)\)")
RX_RESET = re.compile(r"Mech::Reset (\d+):(\d+)")
RX_DEATH = re.compile(r"\[death\] VehicleDead")
# rig-sanity floors (A5) -- calibrated against the proven kd_bench/cascade_bench
# engagement levels (worker's 180s single-shooter evidence run: 200 dmghits).
FLOOR_DMGHITS_TOTAL = 40
FLOOR_ENERGY_HITS = 5 # type 3+4 dmghits
FLOOR_AC_HITS = 3 # type=1 dmghits
FLOOR_MISSILE_HITS = 3 # type=2 dmghits
FLOOR_MISSILE_BURSTS = 2 # IMPACT dmg>0 burst>1 deliveries
FLOOR_EMITTER_FIRED = 10
FLOOR_VITALITY = 1 # deaths + cascades
def _p3(s):
try:
v = [float(x) for x in s.split(",")]
return v if len(v) == 3 else None
except ValueError:
return None
def _dist(a, b):
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
def parse(lines):
lines = list(lines)
d = {
"dmghit": [], # (mech, zone, vital, type, amt, burst)
"push": [], # (dmg, guided)
"det": [], # (dmg, lead)
"impact": [], # (dmg, burst)
"emitter": {}, # name -> count
"ammo": {}, # name -> count
"bystander": 0,
"bys_recs": [], # (line, struckid, at, dmg, lockptr, aim|None, paired)
"bys_capped": 0, # the 24-receipt print cap was reached
"own": None, # this node's own mech entity id (first Reset h:ID)
"mechids": set(), # every player-mech entity id seen in Reset lines
"deaths": 0,
"resets": 0,
"casc_total": 0,
"casc_worst": {}, # zone -> worst per-life repeat count
}
life = {}
for i, l in enumerate(lines):
m = RX_DMGHIT.search(l)
if m:
d["dmghit"].append((m.group(1), int(m.group(2)), int(m.group(3)),
int(m.group(4)), float(m.group(5)), int(m.group(6))))
continue
m = RX_PUSH.search(l)
if m:
d["push"].append((float(m.group(4)), int(m.group(5))))
continue
m = RX_BYS.search(l)
if m:
struck = int(m.group(1))
at = _p3(m.group(3))
if "receipt cap reached" in l:
d["bys_capped"] = 1
# the receipt's own detonation follows within a few lines: DET
# (carries the round's aim) then the mech delivery IMPACT
aim = None
paired = False
for j in range(i + 1, min(i + 5, len(lines))):
dm = RX_DETAIM.search(lines[j])
if dm and aim is None:
aim = _p3(dm.group(2))
if "[projectile] IMPACT damage=" in lines[j]:
paired = True
break
d["bys_recs"].append((i + 1, struck, at, float(m.group(4)),
m.group(5), aim, paired))
d["bystander"] += 1
continue
m = RX_DET.search(l)
if m:
d["det"].append((float(m.group(1)), int(m.group(2))))
continue
m = RX_IMPACT.search(l)
if m:
d["impact"].append((float(m.group(1)), int(m.group(2))))
continue
m = RX_EMIT.search(l)
if m:
d["emitter"][m.group(1)] = d["emitter"].get(m.group(1), 0) + 1
continue
m = RX_AMMO.search(l)
if m:
d["ammo"][m.group(1)] = d["ammo"].get(m.group(1), 0) + 1
continue
m = RX_CASC.search(l)
if m:
z = int(m.group(1))
life[z] = life.get(z, 0) + 1
if life[z] > d["casc_worst"].get(z, 0):
d["casc_worst"][z] = life[z]
d["casc_total"] += 1
continue
if "Mech::Reset" in l:
life = {}
d["resets"] += 1
m = RX_RESET.search(l)
if m:
mid = int(m.group(2))
d["mechids"].add(mid)
if d["own"] is None:
d["own"] = mid
continue
if RX_DEATH.search(l):
d["deaths"] += 1
return d
def adjudicate(logs, verbose=True):
"""logs: {name: parsed-dict}. Returns (fails, table_rows)."""
fails = []
def agg(key):
out = []
for nm in logs:
out.extend(logs[nm][key])
return out
hits = agg("dmghit")
push = agg("push")
det = agg("det")
impact = agg("impact")
emitter = {}
ammo = {}
for nm in logs:
for k, v in logs[nm]["emitter"].items():
emitter[k] = emitter.get(k, 0) + v
for k, v in logs[nm]["ammo"].items():
ammo[k] = ammo.get(k, 0) + v
bystander = sum(logs[nm]["bystander"] for nm in logs)
deaths = sum(logs[nm]["deaths"] for nm in logs)
resets = sum(logs[nm]["resets"] for nm in logs)
cascades = sum(logs[nm]["casc_total"] for nm in logs)
h_energy = [h for h in hits if h[3] in (3, 4)]
h_laser = [h for h in hits if h[3] == 3]
h_ppc = [h for h in hits if h[3] == 4]
h_ball = [h for h in hits if h[3] == 1]
h_missile = [h for h in hits if h[3] == 2]
h_mis_multi = [h for h in h_missile if h[5] > 1]
ball_multi = [h for h in h_ball if h[5] != 1]
push_ballistic = [p for p in push if p[1] == 0]
push_ballistic_dmg = [p for p in push_ballistic if p[0] != 0.0]
push_guided_dmg = [p for p in push if p[1] == 1 and p[0] > 0.0]
det_dmg = [x for x in det if x[0] != 0.0]
imp_dmg = [x for x in impact if x[0] > 0.0]
imp_multi = [x for x in imp_dmg if x[1] > 1]
ammo_ac = {k: v for k, v in ammo.items() if k.upper().startswith("AFC")}
ammo_mis = {k: v for k, v in ammo.items()
if k.upper().startswith(("LRM", "SRM", "STRK", "STREAK", "NARC", "NRK"))}
# ---- A1 ENERGY ----
a1 = []
if not emitter:
a1.append("no named [emitter] FIRED lines")
if len(h_energy) < FLOOR_ENERGY_HITS:
a1.append("laser/PPC dmghits %d < floor %d" % (len(h_energy), FLOOR_ENERGY_HITS))
if not h_laser:
a1.append("no type=3 laser dmghit landed")
if sum(emitter.values()) < FLOOR_EMITTER_FIRED:
a1.append("emitter FIRED total %d < floor %d"
% (sum(emitter.values()), FLOOR_EMITTER_FIRED))
# ---- A2 AC HITSCAN (#171) ----
a2 = []
if len(h_ball) < FLOOR_AC_HITS:
a2.append("type=1 dmghits %d < floor %d" % (len(h_ball), FLOOR_AC_HITS))
if ball_multi:
a2.append("%d type=1 dmghit(s) with burst!=1 (multi-panel ballistic -- "
"not the single-panel hitscan)" % len(ball_multi))
if push_ballistic_dmg:
a2.append("%d ballistic PUSH(es) with dmg!=0 -- a damage-carrying "
"ballistic round entered the pool (pre-#171 shape)"
% len(push_ballistic_dmg))
if not push_ballistic:
a2.append("no ballistic (guided=0) PUSH at all -- AC never cycled")
if not ammo_ac:
a2.append("no [ammo] AFC* FIRED receipt -- no autocannon cycled a round")
# every damage-carrying DET must be attributable to a guided (missile) push,
# per log (pool is node-local)
for nm in logs:
nd = len([x for x in logs[nm]["det"] if x[0] != 0.0])
ng = len([p for p in logs[nm]["push"] if p[1] == 1 and p[0] > 0.0])
if nd > ng:
a2.append("%s: %d damage DETs > %d guided damage PUSHes -- an "
"unguided round detonated carrying damage" % (nm, nd, ng))
# ---- A3 MISSILES ----
a3 = []
if not ammo_mis:
a3.append("no [ammo] LRM/SRM FIRED receipt")
if len(imp_dmg) < FLOOR_MISSILE_HITS:
a3.append("missile IMPACT deliveries %d < floor %d"
% (len(imp_dmg), FLOOR_MISSILE_HITS))
if len(imp_multi) < FLOOR_MISSILE_BURSTS:
a3.append("salvo bursts (IMPACT dmg>0 burst>1) %d < floor %d"
% (len(imp_multi), FLOOR_MISSILE_BURSTS))
if len(h_missile) < FLOOR_MISSILE_HITS:
a3.append("type=2 dmghits %d < floor %d" % (len(h_missile), FLOOR_MISSILE_HITS))
if not h_mis_multi:
a3.append("no type=2 dmghit with burst>1 (no salvo cluster landed)")
if deaths + resets + cascades < FLOOR_VITALITY:
a3.append("rig vitality: deaths+resets+cascades = %d < %d"
% (deaths + resets + cascades, FLOOR_VITALITY))
# ---- A4 NO REGRESSION ----
a4 = []
for nm in logs:
dup = {z: c for z, c in logs[nm]["casc_worst"].items() if c > 1}
if dup:
a4.append("%s re-descended zones %s within one life (#174 ALIVE)"
% (nm, dup))
# BYSTANDER receipts: every receipt must be a TRUE positive (see the
# calibration note in the header -- rounds locked onto scenery legitimately
# detonate on the other mech crossing the flight path; the binary's world
# sweep fires on ANY solid [T1]). False-positive conditions per receipt:
all_mechids = set()
for nm in logs:
all_mechids |= logs[nm]["mechids"]
bys_true = 0
for nm in logs:
own = logs[nm]["own"]
if logs[nm]["bys_capped"]:
a4.append("%s: BYSTANDER print cap (24) reached -- receipt count "
"unmeasurable, rerun a shorter window" % nm)
for (ln, struck, at, bdmg, lockptr, aim, paired) in logs[nm]["bys_recs"]:
bad = []
if own is not None and struck == own:
bad.append("SELF-STRIKE (sweep hit the shooter's own mech)")
if all_mechids and struck not in all_mechids:
bad.append("phantom strike: id %d is no known player mech"
% struck)
if aim is None or not paired:
bad.append("no paired DET/IMPACT delivery (lost or duplicated "
"round)")
elif at is not None and _dist(at, aim) < 30.0:
bad.append("strike within %.0fu of the round's own aim -- the "
"sweep raced the 4.0u fuze at the lock itself "
"(target-exclusion suspect)" % _dist(at, aim))
if bad:
a4.append("%s:%d BYSTANDER FALSE POSITIVE: %s"
% (nm, ln, "; ".join(bad)))
else:
bys_true += 1
# ---- A5 FLOORS ----
a5 = []
if len(hits) < FLOOR_DMGHITS_TOTAL:
a5.append("total dmghits %d < floor %d (quiet rig proves nothing)"
% (len(hits), FLOOR_DMGHITS_TOTAL))
rows = [
("ENERGY", a1, "emitterFIRED=%d(names=%d) hits t3=%d t4=%d"
% (sum(emitter.values()), len(emitter),
len(h_laser), len(h_ppc))),
("AC", a2, "acFIRED=%d hits t1=%d (burst!=1: %d) ballPUSH=%d "
"(dmg!=0: %d)"
% (sum(ammo_ac.values()), len(h_ball), len(ball_multi),
len(push_ballistic), len(push_ballistic_dmg))),
("MISSILE", a3, "misFIRED=%d IMPACTdmg=%d(burst>1:%d) hits t2=%d"
"(burst>1:%d) FIZZ n/a"
% (sum(ammo_mis.values()), len(imp_dmg), len(imp_multi),
len(h_missile), len(h_mis_multi))),
("NO-REGR", a4, "cascades=%d worstRepeat=%d bystander=%d "
"(true-positive crossfire=%d, false=%d)"
% (cascades,
max([c for nm in logs
for c in logs[nm]["casc_worst"].values()] or [0]),
bystander, bys_true, bystander - bys_true)),
("RIG", a5, "dmghits=%d deaths=%d resets=%d DETdmg>0=%d "
"guidedDmgPUSH=%d"
% (len(hits), deaths, resets, len(det_dmg),
len(push_guided_dmg))),
]
for fam, fl, _ in rows:
fails.extend("%s: %s" % (fam, f) for f in fl)
if verbose:
print("=== WEAPONS SWEEP -- per-family verdict ===")
for fam, fl, info in rows:
print("%-8s %-4s %s" % (fam, "PASS" if not fl else "FAIL", info))
for f in fl:
print(" - %s" % f)
print("weapon receipts (named, both logs):")
for k in sorted(set(list(emitter) + list(ammo))):
n = emitter.get(k, 0) + ammo.get(k, 0)
src = "emitter" if k in emitter else "ammo"
print(" %-16s %4d (%s)" % (k, n, src))
for nm in logs:
print("%s: dmghits=%d pushes=%d dets=%d impacts=%d casc=%d "
"resets=%d deaths=%d bys=%d"
% (nm, len(logs[nm]["dmghit"]), len(logs[nm]["push"]),
len(logs[nm]["det"]), len(logs[nm]["impact"]),
logs[nm]["casc_total"], logs[nm]["resets"],
logs[nm]["deaths"], logs[nm]["bystander"]))
print("RESULT:", "PASS" if not fails else "FAIL")
if fails:
for f in fails:
print(" FAIL:", f)
return fails
def load(fn):
return parse(io.open(fn, encoding="latin-1", errors="replace").read().splitlines())
# ---------------------------------------------------------------------------
# --selftest: the detector must FAIL on pre-fix log shapes (negative controls)
# ---------------------------------------------------------------------------
HEALTHY = [
# energy: named emitters + laser/PPC hits
] + [
"[emitter] FIRED 'ERMLaser_1' damage=8 heat=2" for _ in range(6)
] + [
"[emitter] FIRED 'ERPPC' damage=15 heat=6" for _ in range(6)
] + [
"[dmghit] mech=42 zone=%d vital=0 type=3 amt=8 burst=1 lvl 0->0.1" % (i % 5)
for i in range(8)
] + [
"[dmghit] mech=42 zone=2 vital=0 type=4 amt=15 burst=1 lvl 0->0.2",
] + [
# AC: named ammo fire, cosmetic tracer pushes, hitscan type=1 burst=1 hits
"[ammo] AFC50 FIRED, rounds left=93" for _ in range(4)
] + [
"[projectile] PUSH target=0x0 len=400 speed=250 dmg=0 guided=0 ttl=5 "
"mz=(1,12,1) relY=12 lv=(fallback)" for _ in range(4)
] + [
"[projectile] DET at(9,2,3) dmg=0 lead=1 tgt=0x0 aim(9,2,3)" for _ in range(4)
] + [
"[dmghit] mech=42 zone=%d vital=0 type=1 amt=12.5 burst=1 lvl 0->0.1" % (i % 4)
for i in range(6)
] + [
# missiles: guided pushes (1 lead + visuals), IMPACT bursts, type=2 hits
"[ammo] LRM15 FIRED, rounds left=110" for _ in range(4)
] + [
"[projectile] PUSH target=0x5a len=300 speed=180 dmg=52.5 guided=1 ttl=13 "
"mz=(1,14,1) relY=14 lv=(auth)" for _ in range(6)
] + [
"[projectile] DET at(4,5,6) dmg=52.5 lead=1 tgt=0x5a aim(4,5,6)" for _ in range(6)
] + [
"[projectile] IMPACT damage=52.5 subsys=21 v=540 burnLeft=0 burst=9 "
"(direct dispatch) (zone cyl-resolved)" for _ in range(6)
] + [
"[dmghit] mech=42 zone=%d vital=0 type=2 amt=3.5 burst=%d lvl 0->0.3"
% (i % 6, 4 + (i % 8)) for i in range(24)
] + [
"[cascade] zone 17 DESTROYED -> descend=1 destroySibs=0 crits=3",
"[respawn] Mech::Reset 3:42 healed+moved to (0,0,0) alive=1 zones=22 subsys=34",
"[cascade] zone 17 DESTROYED -> descend=1 destroySibs=0 crits=3",
"[death] VehicleDead(-1) dispatched to the owning player",
# the peer's mech id (a second life witness so 43 is a known player mech)
"[respawn] Mech::Reset 2:43 healed+moved to (9,0,9) alive=1 zones=22 subsys=32",
]
# a TRUE-positive bystander receipt: struck the OTHER mech (43 != own 42),
# far from the round's own aim, one paired delivery -- must NOT fail A4.
BYS_TRUE = [
"[projectile] BYSTANDER id=43 t=0.7 at(100,10,100) dmg=5 lockedTgt=0BADF00D",
"[projectile] DET at(100,10,100) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
"(direct dispatch) (zone cyl-resolved)",
]
def selftest():
ok = True
def expect(name, lines_a, want_frag):
nonlocal ok
logs = {"fx_a.log": parse(lines_a), "fx_b.log": parse(HEALTHY)}
fails = adjudicate(logs, verbose=False)
hit = any(want_frag in f for f in fails)
print(" selftest %-28s %s" % (name, "DETECTED" if hit else "** MISSED **"))
if not hit:
ok = False
for f in fails:
print(" got:", f)
print("=== DETECTOR SELF-TEST (negative controls; each MUST fail) ===")
# 1. pre-#171: a damage-carrying ballistic round + multi-panel ballistic hit
fx = list(HEALTHY) + [
"[projectile] PUSH target=0x5a len=400 speed=250 dmg=12 guided=0 ttl=5 "
"mz=(1,12,1) relY=12 lv=(fallback)",
"[projectile] DET at(9,2,3) dmg=12 lead=1 tgt=0x5a aim(9,2,3)",
"[dmghit] mech=42 zone=3 vital=0 type=1 amt=12 burst=3 lvl 0->0.2",
]
expect("pre-#171 ballistic round", fx, "damage-carrying ballistic")
expect("pre-#171 multi-panel t1", fx, "burst!=1")
# 2. pre-#174: same zone cascades twice inside one life
fx = list(HEALTHY) + [
"[cascade] zone 18 DESTROYED -> descend=1 destroySibs=0 crits=4",
"[cascade] zone 18 DESTROYED -> descend=1 destroySibs=0 crits=4",
]
expect("pre-#174 double cascade", fx, "#174 ALIVE")
# 3. bystander FALSE positives (each must be flagged) ...
fx = list(HEALTHY) + [
"[projectile] BYSTANDER id=42 t=0.5 at(50,10,50) dmg=5 lockedTgt=0BADF00D",
"[projectile] DET at(50,10,50) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
"(direct dispatch) (zone cyl-resolved)",
]
expect("bystander SELF-strike", fx, "SELF-STRIKE")
fx = list(HEALTHY) + [
"[projectile] BYSTANDER id=7 t=0.31 at(4,5,6) dmg=8 lockedTgt=0x5a",
]
expect("bystander phantom strike", fx, "phantom strike")
fx = list(HEALTHY) + [
"[projectile] BYSTANDER id=43 t=1 at(499,7,899) dmg=5 lockedTgt=0BADF00D",
"[projectile] DET at(499,7,899) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
"(direct dispatch) (zone cyl-resolved)",
]
expect("bystander fuze-race at lock", fx, "raced the 4.0u fuze")
fx = list(HEALTHY) + BYS_TRUE + [
"[projectile] BYSTANDER id=43 t=0.9 at(200,10,200) dmg=5 "
"lockedTgt=0BADF00D (receipt cap reached)",
"[projectile] DET at(200,10,200) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
"(direct dispatch) (zone cyl-resolved)",
]
expect("bystander print-cap reached", fx, "cap (24) reached")
# ... and the TRUE positive must NOT be flagged (no false alarm)
logs = {"fx_a.log": parse(HEALTHY + BYS_TRUE), "fx_b.log": parse(HEALTHY)}
fails = adjudicate(logs, verbose=False)
bysf = [f for f in fails if "BYSTANDER" in f or "SELF" in f]
print(" selftest %-28s %s" % ("bystander TRUE pos accepted",
"CLEAN" if not bysf else "** FALSE ALARM **"))
if bysf:
ok = False
for f in bysf:
print(" got:", f)
# 4. quiet rig: both logs empty must FAIL floors
logs = {"fx_a.log": parse([]), "fx_b.log": parse([])}
fails = adjudicate(logs, verbose=False)
quiet = any("quiet rig" in f for f in fails) and any("floor" in f for f in fails)
print(" selftest %-28s %s" % ("quiet rig floors",
"DETECTED" if quiet else "** MISSED **"))
ok = ok and quiet
# 5. the healthy fixture itself must PASS (no false alarms in the detector)
logs = {"fx_a.log": parse(HEALTHY), "fx_b.log": parse(HEALTHY)}
fails = adjudicate(logs, verbose=False)
print(" selftest %-28s %s" % ("healthy fixture passes",
"CLEAN" if not fails else "** FALSE ALARM **"))
if fails:
ok = False
for f in fails:
print(" got:", f)
print("DETECTOR:", "OK -- all negative controls detected" if ok else "BROKEN")
return ok
if __name__ == "__main__":
if len(sys.argv) >= 2 and sys.argv[1] == "--selftest":
sys.exit(0 if selftest() else 1)
if len(sys.argv) < 3:
print("usage: weapons_check.py <log_a> <log_b> | --selftest")
sys.exit(2)
logs = {fn: load(fn) for fn in sys.argv[1:]}
fails = adjudicate(logs, verbose=True)
sys.exit(0 if not fails else 1)
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env bash
# weapons_sweep.sh -- ONE comprehensive 2-node bench proving every weapons fix
# on the final (2026-08-13) tree: #171 AC trigger-time hitscan, missile
# contact through the 4.0u fuze at drag-governed speeds (b94636b), energy
# emitters, the #174 cascade guard, and the lane-12 bystander sweep's
# no-false-positive property in a 2-mech fight.
#
# RIG (modeled on cascade_bench.sh / kd_bench.sh -- the rigs that demonstrably
# engage). KNOWN TRAP dodged: avatar-vs-avatar forms ZERO locks (three runs,
# 0 dmghits even from lasers -- see HITSCAN_STATE.md / ac_bench.sh) while the
# madcat family fights fine, so BOTH nodes fly madcat-family chassis:
# node A (-net 1501 -> pilot 127.0.0.1:1502) : mad2 "Zanin Neko" (Madcat V2)
# -- AFC50 autocannon + ERPPC + LRM15 + SRM6 x2 + ERMLaser x2:
# every family from one shooter (AC carrier per L4GAUGE.CFG :3008).
# node B (-net 1601 -> pilot 127.0.0.1:1602) : madcat
# -- AFC100 + LRM15 x2 + ERLLaser + ERSLaser x2 (kd_bench's proven
# engager), sparser trigger.
# Engagement = the kd_bench pattern: BT_GOTO=enemy drives them onto each other
# and the production aim ray designates the enemy (mech+0x388 lock). NOT
# BT_FIRE_AT_ICON (that designates buildings -- fine for the cascade rig's
# splash chaos, useless for typed mech-vs-mech receipts). AF periods 4/7:
# throttled (unthrottled autofire trips the FailureHeat brick) and staggered.
#
# Assertions + PASS table: scratchpad/night16/weapons_check.py. The checker's
# --selftest (run FIRST, gate on it) proves the detector flags the pre-#171
# and pre-#174 log shapes, bystander false positives (self-strike / phantom /
# fuze-race / print-cap), and a quiet rig -- the week's hard rule: prove the
# bench CAN detect failure before trusting PASS.
#
# RESULT 2026-08-13 (evidence: scratchpad/night16/ws_a.log ws_b.log): PASS.
# ENERGY 184 FIRED / 72 laser + 7 PPC hits; AC 42 FIRED / 17 type=1 hits all
# burst=1 / 84 ballistic pushes all dmg=0; MISSILE 151 FIRED / 57 damage
# IMPACTs (50 salvo bursts) / 593 type=2 hits; 11 deaths; cascades 8, none
# re-descended. CALIBRATION FINDING: BYSTANDER receipts DO fire in a 2-mech
# fight (11x) and all were TRUE positives -- the production aim ray locks
# scenery, and the other mech crossing the round's path detonates it (the
# binary world-sweep behavior). The checker asserts the real false-positive
# conditions instead of a zero count; see weapons_check.py header.
set -x
python /c/git/bt411/scratchpad/night16/weapons_check.py --selftest || {
echo "DETECTOR SELF-TEST FAILED -- fix the checker before benching"; exit 1; }
. /c/git/bt411/scratchpad/night6/bench_common.sh
bt_assert_player_env
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3
rm -f ws_a.log ws_b.log ws_r.log
bt_expert_egg MP.EGG WS.EGG
# map/time sed is MANDATORY (test-harness: MP.EGG authors cavern/night --
# un-sedded copies park the mechs against cavern rock). Vehicle seds are
# EXACT-LINE (the ^vehicle=.* form would blindly stamp both pilots the same).
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=bhk1$/vehicle=mad2/; s/^vehicle=ava1$/vehicle=madcat/" WS.EGG
grep -q "^vehicle=mad2$" WS.EGG && grep -q "^vehicle=madcat$" WS.EGG || {
echo "FAIL: WS.EGG vehicle sed did not land (check MP.EGG pilot pages)"; exit 1; }
grep -q "advancedDamage=1" WS.EGG || { echo "FAIL: egg lacks advancedDamage=1"; exit 1; }
# node B FIRST (back window): madcat, sparser trigger, still all families.
( export BT_GOTO=enemy BT_GOTO_STOP=80 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=7
export BT_DMG_LOG=1 BT_PROJ_LOG=1 BT_DEATH_LOG=1 BT_AMMO_LOG=1
bt_launch ws_b.log WS.EGG 0x0C -net 1601 )
sleep 2
# node A: mad2 (Zanin Neko), the dense shooter -- AC + missiles + energy.
( export BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4
export BT_DMG_LOG=1 BT_PROJ_LOG=1 BT_DEATH_LOG=1 BT_AMMO_LOG=1
bt_launch ws_a.log WS.EGG 0x03 -net 1501 )
sleep 5
python ../tools/btconsole.py WS.EGG 127.0.0.1:1501 127.0.0.1:1601 > ws_r.log 2>&1 &
R=$!
sleep 300
kill $R 2>/dev/null
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1
python /c/git/bt411/scratchpad/night16/weapons_check.py ws_a.log ws_b.log