Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e29a4d97 |
-10
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = (1−hostZoneDamage)×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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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; N−1
|
||||
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(range−200, 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 0–1200 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 0–100 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.)
|
||||
@@ -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.
|
||||
=============================================================================
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user