Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e29a4d97 |
-10
@@ -61,13 +61,3 @@ content/*.log
|
|||||||
content/matchlog_*.txt
|
content/matchlog_*.txt
|
||||||
content/btl4.exe
|
content/btl4.exe
|
||||||
content/*.EGG
|
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
|
boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test
|
||||||
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
||||||
dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap.
|
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
|
⚠ `weaponRange` is **fire-control data only** (#168 audit [T1]): projectile FLIGHT is never
|
||||||
was MISSILE-only truth mis-generalized to ballistics.** The AC's own FireWeapon @004bc104
|
range-capped — a plain shell (AFC) lives exactly 5.0 s (@4bddec) and a Missile lives
|
||||||
(recovered by the 2026-08-06 re-export; #168 worked from a stale "not recovered" banner and
|
BurnTime+10 s with a y<−1 kill-plane (@4bef78); FireWeapon @4bcc60 has no range gate, so an
|
||||||
generalized from the missile launcher) is TRIGGER-TIME HITSCAN: batched rounds, dist vs LIVE
|
AFC50 (authored range 750) landing hits at 2000+ u is AUTHENTIC arcade behavior. Constants +
|
||||||
effectiveRange at fire time (the same gate energy uses), ONE single-panel damage message to
|
the whole flight/expiry chain: [[decomp-reference]] §5 "Projectile/Missile flight & expiry".
|
||||||
the locked target, nothing spawned (the shell class 0xBD1 is dead code; the visible round is
|
The same audit killed the pool's stale-point auto-hit (unguided contact was tested against
|
||||||
the 0xBCD renderer tracer). So: an AFC50 landing hits beyond its authored range is a PORT
|
the fire-time pick, so a locked AC shell could not miss a moving target — fixed 2026-08-11:
|
||||||
DEVIATION, not arcade behavior; "a locked AC could not miss a mover" IS the arcade behavior
|
the contact test now tracks the target's live position on every round kind).
|
||||||
(#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).
|
|
||||||
⚠ **`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT
|
⚠ **`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT
|
||||||
heatLoad** — the port originally computed `effectiveRange = (1 − heatLoad) × weaponRange`
|
heatLoad** — the port originally computed `effectiveRange = (1 − heatLoad) × weaponRange`
|
||||||
(mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in
|
(mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in
|
||||||
|
|||||||
+22
-69
@@ -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
|
@004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30
|
||||||
(generator client capacity @0x1e4/count @0x1e8).
|
(generator client capacity @0x1e4/count @0x1e8).
|
||||||
|
|
||||||
### Projectile/Missile flight & expiry (#168 audit 2026-08-11; ⚠ CORRECTED by the #171 hunt 2026-08-13) [T1]
|
### Projectile/Missile flight & expiry (#168 audit, 2026-08-11) [T1]
|
||||||
- ⚠ **#168's "no range cap / WeaponRange is fire-control only" was MISSILE-ONLY truth,
|
- **No range cap exists anywhere in the flight chain.** WeaponRange feeds fire-control only
|
||||||
mis-generalized.** The AC's own FireWeapon @004bc104 (recovered by the 2026-08-06 re-export;
|
(effectiveRange @0x328 = (1−hostZoneDamage)×WeaponRange — targeting/reticle "in range" call).
|
||||||
the old projweap.cpp "body NOT recovered" banner was stale) IS range-gated AND hitscan:
|
FireWeapon @4bcc60 reads no +0x328: the trigger fires at any target distance.
|
||||||
batch counter +0x408, every TracerInterval-th (+0x438) round → dist = |aimPoint(+0x37c) −
|
- **Plain Projectile (AFC shell) live integrator @4bddec** (master); expiry: age since
|
||||||
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
|
|
||||||
spawn(+0x19c) > `_DAT_004be054` = **5.0 s** → FUN_0042061c kill. Dead-reckon divergence²
|
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 →
|
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
|
updateModel(+0x18)|=1 (ForceUpdate, replication-dirty — NOT a kill).
|
||||||
@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.
|
|
||||||
- **Missile live integrator @4bef78** (master); expiry: age > burnTime(+0x340, model rec
|
- **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**
|
+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.
|
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
|
**Proximity fuse**: seeker rangeToTarget(+0x10C) < `_DAT_004bf5a4` = **4.0** → detonate on
|
||||||
targetEntity without a geometry hit. Seeker drops a destroyed target (movementMode 2|9,
|
targetEntity without a geometry hit. Seeker drops a destroyed target (movementMode 2|9,
|
||||||
FUN_0049fb54).
|
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
|
- **Performance split** (Projectile ctor @4be1bc / Missile ctor @4bf5b4): instance
|
||||||
(flags&0xC)==ReplicantInstance(4) → FUN_004221c0 dead-reckoning smoother (no physics);
|
(flags&0xC)==ReplicantInstance(4) → FUN_004221c0 dead-reckoning smoother (no physics);
|
||||||
master → 4bddec/4bef78. (CLASSMAP's old authoritative/ghost labels were swapped —
|
master → 4bddec/4bef78. (CLASSMAP's old authoritative/ghost labels were swapped —
|
||||||
@@ -436,19 +400,18 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
|||||||
the Execute machine, but this handler overwrites it). The earlier "jams are
|
the Execute machine, but this handler overwrites it). The earlier "jams are
|
||||||
mission-permanent / no unjam" claim was WRONG (read only the hold path) — corrected +
|
mission-permanent / no unjam" claim was WRONG (read only the hold path) — corrected +
|
||||||
swept 2026-07-23 (projweap.cpp case-5/jam-log comments, players/README.txt).
|
swept 2026-07-23 (projweap.cpp case-5/jam-log comments, players/README.txt).
|
||||||
**#21 RETRACTED 2026-08-13 (era testimony -> adversarial re-audit; witnesses 6-0):** the
|
**LATENT ARCADE BUG found + diverged (issue #21, 2026-07-21) [T1 byte-verified + T2 repro]:**
|
||||||
"LATENT ARCADE BUG / permanently bricked / the arcade shipped this" claim that stood here was
|
the Emitter charge integrates toward the GENERATOR voltage (@004ba838 `fld [src+0x1dc]`), full
|
||||||
a CONSTANT-WIDTH MISREAD. `_DAT_004ba830` is a **DOUBLE 1.0** (bytes 00..00 f0 3f @0x4ba830 --
|
is detected only inside the +-1% snap window around `seekVoltage[idx]` (ComputeOutputVoltage
|
||||||
the old "0.0 byte-verified" read the low DWORD of a QWORD: 1.0's mantissa zeros; Ghidra's own
|
@004ba738: `|rl-1|<=0.01 -> 1.0`), and OVERCHARGE reads ZERO (`rl>1.01 -> 0`,
|
||||||
"globals overlap smaller symbols" warning applies). ComputeOutputVoltage @004ba738 truth [T1
|
`_DAT_004ba830 = 0.0` byte-verified). Toggle the seek gear while a charge is in flight and
|
||||||
raw disasm]: ratio = charge/seekV[idx]; snap |rl-1|<=0.01 -> 1.0f; else CLAMP to [0,1] --
|
the level lands ABOVE the new gear's window -> rechargeLevel pinned 0, the `==1.0` Loaded test
|
||||||
overcharge stores EXACTLY 0x3f800000, the `==1.0` Loaded test passes next tick, the weapon
|
(`_DAT_004bac04`) never fires, charging continues to the generator ceiling where EVERY gear
|
||||||
FIRES the stored charge (damage divisor is the RECOMMENDED gear, so a wrap-fired charge deals
|
reads overcharged -> the weapon is PERMANENTLY bricked (arc dark, ready-dot dark, no fire; gen
|
||||||
normal top-gear damage). ToggleSeekVoltage @004ba478 genuinely skips ResetFiringState on wrap
|
re-select cannot help). The arcade shipped this (locked 60 fps + rare seek use hid it); the
|
||||||
[T1] -- but nothing downstream ever bricks. The machine never had a dark laser; the port's
|
port's clickable seek button triggers it in seconds. **Deliberate divergence** (emitter.cpp
|
||||||
"rescue" was a patch over OUR misread (removed with the clamp fix, same date). Constants
|
Loading tick): overcharge (`currentLevel > seekVoltage[idx]`) counts as fully charged ->
|
||||||
extracted: 0x4ba818=0.0f 0x4ba81c=1e-4f 0x4ba820=1.0f 0x4ba824=0.01f 0x4ba828=dbl 0.0
|
Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro +
|
||||||
0x4ba830=dbl 1.0 0x4bac04=1.0f. Gotcha family 30/31 (width/scope misreads). Repro +
|
|
||||||
verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues,
|
verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues,
|
||||||
ends Loaded/pct=1. `[seek]` log prints the per-gear table + rescue events.
|
ends Loaded/pct=1. `[seek]` log prints the per-gear table + rescue events.
|
||||||
- **EJECT/PANIC cluster DECODED + WIRED 2026-08-02 [T1 raw disasm]:** `@0049f854`
|
- **EJECT/PANIC cluster DECODED + WIRED 2026-08-02 [T1 raw disasm]:** `@0049f854`
|
||||||
@@ -618,14 +581,9 @@ extracted: 0x4ba818=0.0f 0x4ba81c=1e-4f 0x4ba820=1.0f 0x4ba824=0.01f 0x4ba828=db
|
|||||||
(`FUN_0041bd98`) — **VARIABLE-STEP, slice in real SECONDS**, line-for-line the
|
(`FUN_0041bd98`) — **VARIABLE-STEP, slice in real SECONDS**, line-for-line the
|
||||||
WinTesla SIMULATE.cpp body. Corollaries: every Performance's `time_slice` is
|
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
|
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)
|
per-frame term (myomer kinetic @4b8d18's middle term; the Mover gravity
|
||||||
fires at the FRAME cadence = the tick rate, 28 Hz nominal, sagging under load.
|
`-= **(mover+0x250)` @0x421e77) fires at the FRAME cadence = the tick rate,
|
||||||
⚠ The Mover gravity `-= **(mover+0x250)` @0x421e77 is NOT in that class
|
28 Hz nominal, sagging under load. Mover velocity is u/s (the replicant
|
||||||
(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
|
|
||||||
dead-reckoner @0x421f7c does `pos += vel × ticksΔ/DAT_0052140c`). The myomer
|
dead-reckoner @0x421f7c does `pos += vel × ticksΔ/DAT_0052140c`). The myomer
|
||||||
heat term's ONE caller is the **unexported MyomersSimulation body
|
heat term's ONE caller is the **unexported MyomersSimulation body
|
||||||
@0x4b8b9a-0x4b8d0d** (E8-scan + raw disasm; the export gap hid it): calls
|
@0x4b8b9a-0x4b8d0d** (E8-scan + raw disasm; the export gap hid it): calls
|
||||||
@@ -774,12 +732,7 @@ default-ON (`'0'` disables).
|
|||||||
| `BT_WARP_*` | translocation-warp visual tuning — all default to the verified values (see [[translocation-warp]]): `EYE_UP=8.25` (on-axis), `SPIN=4`, `TWIST=0`, `CONTRAST=1.0`, `LO_*/HI_*` (lavender ramp), `BLUR=0`, `TESS=3`, `MIP=1`, `CULL=cw`, `ANISO=0` |
|
| `BT_WARP_*` | translocation-warp visual tuning — all default to the verified values (see [[translocation-warp]]): `EYE_UP=8.25` (on-axis), `SPIN=4`, `TWIST=0`, `CONTRAST=1.0`, `LO_*/HI_*` (lavender ramp), `BLUR=0`, `TESS=3`, `MIP=1`, `CULL=cw`, `ANISO=0` |
|
||||||
| `BT_WARP_SELFTEST` / `BT_WARP_SELFSHOT=<prefix>` | DIAG (off by default): force a held warp in a solo game / dump backbuffer frames to disk (visual-verification harness) |
|
| `BT_WARP_SELFTEST` / `BT_WARP_SELFSHOT=<prefix>` | DIAG (off by default): force a held warp in a solo game / dump backbuffer frames to disk (visual-verification harness) |
|
||||||
| `BT_GYRO_LOG` | gyro bring-up log (gyro.cpp) |
|
| `BT_GYRO_LOG` | gyro bring-up log (gyro.cpp) |
|
||||||
| `BT_GYRO_TRACE` | per-frame hit-bounce integrator trace (`[gtrace]`, carries wall-ms `t=`) |
|
| `BT_GYRO_TRACE` | per-frame hit-bounce integrator trace |
|
||||||
| `BT_GYRO_SPRING_HZ=<hz>` | gyro spring-damper integrator clock override (default 28 = the pod tick, census item 2; `=0` restores render-cadence stepping) |
|
|
||||||
| `BT_GYRO_KICK=<frame>[,amt]` | one-shot deterministic gyro impulse via the authentic GyroApplyDamage fan-out (impulse-response bench; default frame 900, amt 25; `[gyro-kick]` receipt) |
|
|
||||||
| `BT_AUD_SMOOTH_HZ=<hz>` | footstep AudioControlSmoother intake clock override (default 28, census item 3; `=0` restores per-poll intake) |
|
|
||||||
| `BT_AUD_DOPPLER_HZ=<hz>` | doppler velocity-operand sample-and-hold clock override (default 28, census item 4; `=0` restores per-call) |
|
|
||||||
| `BT_AUD_DOPPLER_LOG` | `[doppler]` receipt (cents/v/d/h/hm2, capped 6000; h=1 fresh / 0 held / -1 legacy) |
|
|
||||||
| `BT_CRIT_PROBE=<zone>` | hammer one own-mech zone every 4s (crit-propagation diag, task #2; mech4.cpp) |
|
| `BT_CRIT_PROBE=<zone>` | hammer one own-mech zone every 4s (crit-propagation diag, task #2; mech4.cpp) |
|
||||||
| `BT_REPL_LOG` | replicant/MP replication log (mech4.cpp) |
|
| `BT_REPL_LOG` | replicant/MP replication log (mech4.cpp) |
|
||||||
| `BT_GOTO="enemy"\|"x z"` / `BT_GOTO_LOG` | self-driving beeline (to the enemy or a map coordinate) + its log |
|
| `BT_GOTO="enemy"\|"x z"` / `BT_GOTO_LOG` | self-driving beeline (to the enemy or a map coordinate) + its log |
|
||||||
|
|||||||
@@ -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).**
|
- **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;
|
`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
|
`GetEnvironment()->gravityConstant` is the named analog of the original Mover's gravity POINTER at
|
||||||
`+0x250` = `localEnvironment` (MOVER.h; `FUN_00421e2c` = ApplyAirResistanceAndGravity does
|
`+0x250` (`FUN_00421e2c` does `vy -= **(+0x250)` per tick), which the myomer drive-heat
|
||||||
`worldAccel.y -= **(+0x250)` each frame onto the frame-zeroed accumulator), which the myomer drive-heat
|
|
||||||
integrator uses for its `m·g·|vy|·dt` climb-work term. The bridge (`BTMechMyomerMotionSample`)
|
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:
|
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
|
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
|
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
|
climb-work heat never accrues on slopes. Find where our Environment gravity actually lives
|
||||||
and re-point the bridge. [T2 measured]
|
and re-point the bridge. [T2 measured]
|
||||||
- **✅ Engine Mover gravity needs NO 28Hz rescale — CLOSED 2026-08-13 (velocity/dt audit) [T1/T0].**
|
- **Engine Mover gravity is dt-LESS per-frame in the binary** (`@0x421e77: vy -= **(+0x250)`,
|
||||||
The worry was that the per-frame `worldAccel.y -= **(+0x250)` (@0x421e77, no dt) would scale
|
raw, once per Perform — same class as the myomer kinetic term). If our linked WinTesla
|
||||||
with our frame rate. It cannot: **`Mover::PerformAndWatch` = `FUN_00422360` ZEROES
|
MOVER.cpp kept that form, gravity acceleration in the port scales with OUR ~59Hz frame rate
|
||||||
`localAcceleration` at the top of every frame** (`FUN_0040a7f4(+0x1dc, &DAT_004e0fd4)` ==
|
(≈2.1× the pod's 28) — jump arcs, falls, missile droop all stiffer than the pod. CHECK the
|
||||||
T0 MOVER.cpp:672 `localAcceleration = Motion::Identity`), so gravity/drag/thrust adds are
|
WinTesla source's gravity line; if per-frame, it needs the same 28Hz reference-rate
|
||||||
per-frame *acceleration definitions* on a fresh accumulator — a constant `a` integrated ×dt
|
normalization the myomer kinetic term got. [T1 binary side / T4 port impact until read]
|
||||||
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.
|
|
||||||
- **Gait-noise phantom acceleration feeds the myomer accel term** (`[myoheat] a=4..33` at
|
- **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
|
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
|
`AccelerationLastFrame` cell off ITS OWN gait, noise level unknown). Bounded contributor
|
||||||
@@ -1233,18 +1225,7 @@ consistent with internals-only, see #103). Logs: `scratchpad/night16/` + Oracle
|
|||||||
(x2 spawns, AFC50/Zanin Neko); once ABSENT for an entire fatal leak (5-6 voice repetitions,
|
(x2 spawns, AFC50/Zanin Neko); once ABSENT for an entire fatal leak (5-6 voice repetitions,
|
||||||
no flash on any MFD, died ~9:38PM CT -- the match's only death, receipt findable in night16).
|
no flash on any MFD, died ~9:38PM CT -- the match's only death, receipt findable in night16).
|
||||||
Distinct from the closed #135: this is the SET-edge / first-flash arming path.
|
Distinct from the closed #135: this is the SET-edge / first-flash arming path.
|
||||||
- **#173 -- chicken-walker seek-4 myomer overheat: ⚑ SETTLED AUTHENTIC (2026-08-13, three
|
- **#173 -- chicken-walker seek-4 myomer overheat** (see [[subsystems]] night-16 note).
|
||||||
lanes, see [[subsystems]] #173 blocks).** Bench matrix reproduced Elengil exactly (vulture
|
|
||||||
seek-4: degT in 13.9 s, governor-pinned eq ~1801, loop-5 bar full-scale ~15-20 s, sheds
|
|
||||||
−10/s only at standstill); the equilibrium is a chassis-blind governor thermostat — the
|
|
||||||
port's avatar (same 143-kph speed class) cooks identically, and the manual prints the big
|
|
||||||
walkers even FASTER (MadCat 175/220) than the port runs them. Counter-play measured: one
|
|
||||||
loop-5 valve press sustains drive 0.68 vs 0.54. TWO residuals stay open: (1) [T3] the
|
|
||||||
accel-term |a| ring is frame-cadence-sensitive (|a|≈14-20 at 59 fps on a straight flat
|
|
||||||
cruise = gait surge; pod 28 Hz would read ~2-3x smaller → pod eq likely ~1200-1500, softer
|
|
||||||
governor — if calibration is wanted, fix the |a| feed cadence, never the byte-exact
|
|
||||||
constants); (2) locomotion-lane: port per-chassis top speeds undershoot the manual prints
|
|
||||||
(thor 87 vs 143 N print) — correcting them raises heat further.
|
|
||||||
- **#165 ROOT-CAUSED + FIXED + BENCHED (2026-08-13, the 4-agent hunt):** gotcha SS30 -- the
|
- **#165 ROOT-CAUSED + FIXED + BENCHED (2026-08-13, the 4-agent hunt):** gotcha SS30 -- the
|
||||||
binary's ONE destroyed cell was split into four port stores and the crit-lottery kill path
|
binary's ONE destroyed cell was split into four port stores and the crit-lottery kill path
|
||||||
(MechWeapon::TakeDamage) wrote none the gate reads. Fixed (chain to MechSubsystem::TakeDamage,
|
(MechWeapon::TakeDamage) wrote none the gate reads. Fixed (chain to MechSubsystem::TakeDamage,
|
||||||
@@ -1264,49 +1245,3 @@ consistent with internals-only, see #103). Logs: `scratchpad/night16/` + Oracle
|
|||||||
wanted for the pair-leak confirmation.
|
wanted for the pair-leak confirmation.
|
||||||
- Nanook lost the coolant MFD glass window, recovered only by config edit -> #76 comment
|
- Nanook lost the coolant MFD glass window, recovered only by config edit -> #76 comment
|
||||||
(in-game layout-reset recovery path).
|
(in-game layout-reset recovery path).
|
||||||
|
|
||||||
|
|
||||||
## Cadence census -- the shared 28Hz sim-clock migration checklist (2026-08-13) [T1/T2]
|
|
||||||
Census of sampled-history-at-render-cadence sites (gotcha 32; full table in the census
|
|
||||||
workflow journal wf_d875938f + classifier verdicts). Night-17 queue receipts:
|
|
||||||
`scratchpad/night17/CADENCE_STATE.md` (benches + curves for every row below); commits
|
|
||||||
f811c64/567ffac/b355844/f9546d2.
|
|
||||||
|
|
||||||
DONE (each a per-instance 28Hz accumulator/hold, heat.cpp #119 pattern, env-override to
|
|
||||||
bracket, receipts armed):
|
|
||||||
- velocity ring default-28Hz (mech4.cpp, HIGH, the #173 fix -- myomer heat inherits);
|
|
||||||
heatLoad already pod-clocked (#119, the reference pattern).
|
|
||||||
- instability/sway operand: VERIFIED inherited from the ring fix (no code change) -- same-
|
|
||||||
build A/B via BT_MYO_RING_HZ=0: cruise unstablePct median 0.490 -> 0.109 (4.5x), |a| p90
|
|
||||||
48 -> 14.5. [T2]
|
|
||||||
- gyro eye/body spring-damper integrators (gyro.cpp, was MED): stepped on a 28Hz
|
|
||||||
accumulator, integrator bodies byte-untouched; impulse bench (BT_GYRO_KICK one-shot)
|
|
||||||
BEFORE/AFTER: overshoot 23%->7%, period 1.43->2.4s, settle equal (per-SECOND damping is
|
|
||||||
cadence-invariant); shape verdict = timescale-only. BT_GYRO_SPRING_HZ. [T2]
|
|
||||||
- footstep AudioControlSmoother intake (AUDCMP.cpp, was LOW): only the Add clocked; the
|
|
||||||
poll path measured at ~141-147/s (worse than assumed -- the wall window was 0.2s, not
|
|
||||||
0.5s); ramp90 0.55->1.23s (the authored ~1.07s window restored); N untouched.
|
|
||||||
BT_AUD_SMOOTH_HZ. [T2]
|
|
||||||
- doppler operand (AUDLOC.cpp, was LOW): relative WORLD velocity sample-and-held at 28Hz,
|
|
||||||
consumer-side only, head-frame geometry live; operand total-variation -20%, stride std
|
|
||||||
preserved. BT_AUD_DOPPLER_HZ + [doppler] receipt (BT_AUD_DOPPLER_LOG). [T2 mechanism /
|
|
||||||
T3 PERCEPTUAL -- solo-rig cents jitter moved only -11% (near-field geometry dominates at
|
|
||||||
d~1); the nearby-walking-mech warble claim still needs an ear A/B or a 2-node rig.]
|
|
||||||
- ring dt-accumulator registry productionized: 16-slot static -> heat.cpp map pattern
|
|
||||||
(17th-mech silent revert now impossible); smoke identical within run noise. [T2]
|
|
||||||
|
|
||||||
REMAINING for the BTSimClock migration (one shared 28Hz tick source; tick-native sites
|
|
||||||
subscribe; dt-native physics -- Mover/missile ODE -- stays on real dt):
|
|
||||||
- LOW published localVelocity 0.25s exponential smoothing (mech4.cpp:5578-5613): re-base to
|
|
||||||
the binary's own 15-sample@28Hz filter once the shared clock exists.
|
|
||||||
- NEW (night-17, receipt-exposed, pre-existing, UNTOUCHED): doppler sources linked to CHILD
|
|
||||||
entities of a moving mech read relative v = full ground speed (child worldLinearVelocity
|
|
||||||
= 0) -> dopplerCents ~ -200 on self-sounds at d~1 (the [doppler] receipt shows it
|
|
||||||
directly). Not a cadence bug -- a producer-frame question; determine whether the
|
|
||||||
binary's located-audio path behaved the same (the pod cab may simply have masked it)
|
|
||||||
before touching anything. [T3]
|
|
||||||
- DEFERRED dead-reckon divergence (@_DAT_004be050) -- only matters if the Missile entity
|
|
||||||
revives. ANNOTATE-ONLY: Simulate telemetry FilteredScalars (dormant).
|
|
||||||
- Consolidation: five per-site clocks now duplicate the same accumulator idiom (heat, ring,
|
|
||||||
gyro, smoother intake, doppler hold) -- fold onto the one BTSimClock when it lands, one
|
|
||||||
site per commit, using the existing env-overrides as the bracketing levers.
|
|
||||||
|
|||||||
@@ -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)
|
(loadout rework on Loki, small-laser redistribution elsewhere), NOT a bug. `BT_SPEC_LOG` (mech4)
|
||||||
re-dumps on demand.
|
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
|
## Key Relationships
|
||||||
- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view).
|
- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view).
|
||||||
- Input: [[locomotion]] (the mapper).
|
- Input: [[locomotion]] (the mapper).
|
||||||
|
|||||||
@@ -1064,26 +1064,3 @@ passes every reader-side bench. **Bench rule (the #165 lesson, second applicatio
|
|||||||
week):** prove the bench detects the bug BEFORE trusting its pass -- the negative control
|
week):** prove the bench detects the bug BEFORE trusting its pass -- the negative control
|
||||||
here (fix stashed) scored 147 cascades where the fixed build scores 1.
|
here (fix stashed) scored 147 cascades where the fixed build scores 1.
|
||||||
Bench: BT_ZONE_HAMMER=<zone>[,<amt>] (mech4.cpp) + scratchpad/night16/cascade_bench.sh.
|
Bench: BT_ZONE_HAMMER=<zone>[,<amt>] (mech4.cpp) + scratchpad/night16/cascade_bench.sh.
|
||||||
|
|
||||||
|
|
||||||
## 32. SAMPLED-HISTORY OPERANDS AT RENDER CADENCE: rate-scaling amounts cannot un-noise a signal (#173 flip, 2026-08-13)
|
|
||||||
The port's #137 pattern (term x dt x 28) correctly rate-normalizes per-tick heat AMOUNTS -- but
|
|
||||||
OPERANDS COMPUTED FROM SAMPLED HISTORY (rings, last-frame deltas, moving averages, per-tick
|
|
||||||
integrators carrying state) still sampled at RENDER cadence where the pod's board sampled at
|
|
||||||
28 Hz. The killer case: the AccelerationLastFrame velocity ring pushed per render frame read
|
|
||||||
|a| 17-26 of pure gait-aliasing noise on a flat straight cruise -- 64-66% of myomer drive-heat
|
|
||||||
generation was PHANTOM; at 28 Hz sampling the noise drops to 24-32% and generation HALVES at
|
|
||||||
equal speed. Consequence: every fast-heavy chassis was over-punished ~2x; the authored loop-5
|
|
||||||
valve boost genuinely sustains FULL supercharge seek-4 at 28 Hz (bench m173v2 leg G: 54-57 u/s
|
|
||||||
held, Tm stable ~1500) -- the machine's lever worked and the manual's Super Charged print was
|
|
||||||
honest. Witnesses (Draco/Oracle/Elengil + the user's own eyes) beat the validated-solver
|
|
||||||
extrapolation AND the first bench: 7-0 this week.
|
|
||||||
**Rules:** (1) any binary value derived from per-tick sampled history must be sampled on the
|
|
||||||
BINARY'S clock -- accumulate render dt, take one sample per 1/28 s measuring over the full
|
|
||||||
elapsed window (the heat.cpp #119 heatLoad clock is the reference implementation; the ring
|
|
||||||
default-28Hz in mech4.cpp is the second). (2) Rate-scaling the consuming term is NOT
|
|
||||||
sufficient -- it scales amounts, not signal quality. (3) A validated model EXTRAPOLATES; only
|
|
||||||
the bench INTERPOLATES -- never print "sustains indefinitely" from a solver row the bench
|
|
||||||
never flew (the #173 verdict flipped twice on exactly this). Census of the whole class (11
|
|
||||||
rows, 2 HIGH fixed, migration checklist for the shared 28 Hz sim-clock): open-questions.md
|
|
||||||
SSCadence-census + the wf_d875938f journal.
|
|
||||||
|
|||||||
+8
-172
@@ -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 =
|
(mech4.cpp): `+0x1C4` vec = `localVelocity.linearMotion`; `+0x82C` vec =
|
||||||
`localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame);
|
`localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame);
|
||||||
`+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong);
|
`+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong);
|
||||||
`*(+0x250)` = the environment **gravity pointer** (= `Mover::localEnvironment`, MOVER.h;
|
`*(+0x250)` = the environment **gravity pointer** (`FUN_00421e2c` does `vy -= **(+0x250)`/tick).
|
||||||
`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).
|
|
||||||
Physics: `heat += ratio²·(1+dmg)·[0.005·½mv² + 0.2·m·g·|vy|·dt + 0.2·m|v||a|·dt]` — kinetic work
|
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).
|
+ 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):
|
**Authored tuning extracted from BTL4.RES** (all 18 mech variants share one record):
|
||||||
@@ -342,178 +339,17 @@ speed in the spike frames. The calibration facts above (constants byte-exact, d
|
|||||||
kinetic term) all STAND; the in-life governor (running hot at sustained top speed derates the
|
kinetic term) all STAND; the in-life governor (running hot at sustained top speed derates the
|
||||||
myomers) is authentic and remains.
|
myomers) is authentic and remains.
|
||||||
|
|
||||||
## Spawn-discharge loop heat + per-chassis generator bias — ✅ CONFIRMED (2026-08-13, #173 bench lane) [T2 live receipts + T1 authored data]
|
## Spawn-discharge loop heat + per-chassis generator bias (night-16 field, 2026-08-12) [T3 field-observed, mechanism plausible-authentic]
|
||||||
Oracle's explanation for the long-standing "missiles cook off on one side first" reports
|
Oracle's explanation for the long-standing "missiles cook off on one side first" reports
|
||||||
(Lynx): at spawn ALL weapons start discharged, and the initial CHARGE of the energy weapons
|
(Lynx): at spawn ALL weapons start discharged, and the initial CHARGE of the energy weapons
|
||||||
dumps heat into their loops; generator assignments are biased per chassis (Avatar: BOTH large
|
dumps heat into their loops; generator assignments are biased per chassis (Avatar: BOTH large
|
||||||
lasers on GeneratorA), so that side's loops start hot and a missile launcher sharing the side
|
lasers on GeneratorA), so that side's loops start hot and a missile launcher sharing the side
|
||||||
cooks off first even if the player fires only missiles. **All three links verified:**
|
cooks off first even if the player fires only missiles. Not yet verified against the authored
|
||||||
(1) *Weapons spawn discharged* — Emitter ctor @004bb120 AND ResetToInitialState @004ba4d0 both
|
data -- verify via BTL4.RES generator/loop assignments before treating as established. [T3]
|
||||||
seed `currentLevel=0` + weaponAlarm Loading [T1]; (2) *charge heat lands in the powering
|
|
||||||
generator* — `TrackSeekVoltage` @004ba838: `src->pendingHeat += seekRate² · dtScale · dt`,
|
|
||||||
and the generator conducts to its authored condenser (GenA→C1, GenB→C2, GenC→C3, GenD→C5,
|
|
||||||
uniform all 18 variants) [T1]; (3) *the Avatar bias is authored* — BTL4.RES avatar chain:
|
|
||||||
ERLLaser_1 AND ERLLaser_2 both `VoltageSource=GeneratorA` (plus ERMLaser_1) [T1].
|
|
||||||
**Live receipt** (solo avatar, BT_HEAT_LOG, ZERO shots fired, first 5 s census,
|
|
||||||
`m173_ava_s4.log`): GeneratorA T=559 load pegged 1.0 vs GeneratorC T=172 / GeneratorD T=79;
|
|
||||||
Condenser1 (GenA's loop) 390→459 while C4/C6 sit at 75; and **LRM10_1 (authored to loop 1)
|
|
||||||
reads 144 vs LRM10_2 (loop 3) at 94** — the asymmetric missile-side heating with no trigger
|
|
||||||
pulled. Loops also BACK-HEAT their weapons (ConductHeat is two-way): vulture's ERMLasers on
|
|
||||||
hot loop 2 climbed 76→182 in 10 s, zero firing (`m173_vul_s1.log`). [T2]
|
|
||||||
|
|
||||||
Related open defect/question: **#173** -- Vulture (and per Oracle ALL large chicken walkers)
|
Related open defect/question: **#173** -- Vulture (and per Oracle ALL large chicken walkers)
|
||||||
cannot run seek-4 myomers without immediate overheat, loop 5 pegged, heat only sheds near
|
cannot run seek-4 myomers without immediate overheat, loop 5 pegged, heat only sheds near
|
||||||
standstill (Elengil; Oracle repro). ⚑ SETTLED 2026-08-13 by the three lanes below (authored
|
standstill (Elengil; Oracle repro). Investigate myomer heat rate + loop-5 capacity vs authored
|
||||||
data + cooling-side audit + live bench matrix): AUTHENTIC authored balance, reproduced and
|
data for those chassis; authored-balance vs port-artifact undecided. The myomer drive-heat
|
||||||
quantified; one bounded [T3] severity residual (accel-term ring cadence) and the locomotion
|
FORMULA is verified faithful (#137, above) -- if the rate is right, the question moves to the
|
||||||
top-speed cross-check remain flagged. See the BENCH lane block for the numbers.
|
authored loop capacity / condenser assignments.
|
||||||
|
|
||||||
## #173 COOLING-SIDE AUDIT (2026-08-13) -- the binary heat chain leans AUTHENTIC [T1 data/formulas, T2 equilibrium]
|
|
||||||
Full authored heat-family dump (content/BTL4.RES type-17 walk, scratchpad heatscan3.py) + the
|
|
||||||
dissipation chain from the decomp. The chain: Myomers -> Condenser5 -> Bank -> ambient-300.
|
|
||||||
- **Cooling is authored CHASSIS-FLAT** [T1]: every one of the 18 variants streams IDENTICAL
|
|
||||||
condensers (k=315000, mass=420000, refrig=3, degT=failT=2000), myomers (k=190000, mass=250000,
|
|
||||||
velEff .995/accEff .8, gears 0.3/0.5/0.7/0.9999 rec 2 at res+0x198..0x1AC), generators (rated
|
|
||||||
10000, GeneratorD AND Myomers both sinkIdx=8 = Condenser5 on ALL chassis), reservoir, and bank
|
|
||||||
(k=231000, mass=1.39e6) -- the ONLY per-chassis field is bank HeatSinkCount (owens 5, blkhawk 6,
|
|
||||||
mad2 10, mad1/thr1/sunder 13, madcat 14, avatar/loki 15, vulture/vul1/lok1/lok2 18, thor 28,
|
|
||||||
snd1 30), and at balanced valves it moves max shed by <2% (bottleneck is condenser->bank).
|
|
||||||
moverMass (type-15 GameModel +0x0): owens 35k, blkhawk/VULTURE 60k, loki 65k, avatar/thor 70k,
|
|
||||||
madcat 75k, sunder 90k. **No per-chassis gear cap exists** (shared 4-gear table + sentinel).
|
|
||||||
- **NO speed-dependent cooling anywhere in the binary** [T1]: full operand walks of
|
|
||||||
ComputeHeatFlow @004ad9ec (dt, k, coolantLvl/cap, flowScale, masses only), the bank radiator
|
|
||||||
@004ae73c (adds zone damage + frozen ambient-300 target), UpdateCoolant @004adbf8 (damage-driven
|
|
||||||
leak). No airflow term. "Cools only near standstill" = generation (~0.07·m·v²·ratio²/s kinetic
|
|
||||||
+ 0.2·m|v||a| gait term) vs a FIXED-capacity chain. Authentic behavior, not a port break.
|
|
||||||
- **Seek-4 heat multiplier is 2.04x, NOT 16x** [T1]: ratio = gears[cur]/gears[rec] CLAMPED >= 1
|
|
||||||
(@004b8d18), so gears 1-3 are heat-identical and gear 4 = (0.9999/0.7)^2 = 2.04x -- times the
|
|
||||||
supercharge speed feedback (v x1.4284 -> v^2 x2.04) = ~4.2x total at unrestricted cruise.
|
|
||||||
- **Equilibrium math validated against live data** [T2]: linearized per-second stage rates
|
|
||||||
(massScale=3 refrigeration included): myo->cond 44.4k/K-of-dT, cond->bank 28.8k x valveShare x
|
|
||||||
(2.73·Tc - 0.91·Tb), bank->ambient 23.1k x hsCount x (Tb-300). Solver (scratchpad equilib.py)
|
|
||||||
reproduces the measured BLH gear-4 governed equilibrium (predicted 1783 @ v=44 vs measured
|
|
||||||
1718-1770, seek4.log) and the radiator shed line. Balanced-valve chain capacity ~= 1.9e7/s at
|
|
||||||
Tm=2000 on EVERY chassis; **loop-5 boost (detent 50) = 2.7x capacity (5.07e7/s)** -- the
|
|
||||||
authored lever that sustains unrestricted gear-4 on a vulture (0.67-0.83x of capacity).
|
|
||||||
- **Why the big chicken walkers cook**: gear-4 unrestricted generation / failure-line capacity =
|
|
||||||
1.77x for blkhawk (hovers at ~1770, keeps moving) vs 2.2-2.8x for madcat/thor/vulture-if-faster
|
|
||||||
(blows THROUGH failT 2000 -> myomers fail -> speed+turn freeze; ~6 s to degT, ~12 s to failT
|
|
||||||
from cold; Condenser5's MFD bar pegs in ~3 s since load full-scale ~= T 300). GeneratorD shares
|
|
||||||
loop 5 everywhere, so a pegged loop back-heats it toward its own trip -- the freeze amplifier.
|
|
||||||
- **Residual for the verdict** [T3]: vulture mass = blkhawk mass (60k), so the field split
|
|
||||||
vulture-vs-blkhawk requires the vulture's CLIP-DERIVED base speed (mech+0x34C, run-cycle avg
|
|
||||||
root speed @0x4a80d4) and/or gait |a| bob to exceed the BLH's measured 43.6 u/s -- at equal
|
|
||||||
speed the two compute identically. Type-15 gamedata splits 33-36/43-44 into exactly two motion
|
|
||||||
groups (avatar/madcat*3/sunder*2/thor*2/vulture*2 vs blkhawk*2/loki*3/owens*2) [T4 field
|
|
||||||
meaning]. Bench lane: measure vulture v/|a| at gear 3/4 vs BLH.
|
|
||||||
**#173 AUTHORED-DATA lane result (2026-08-13) [T1]: the heat/cooling plumbing is
|
|
||||||
BYTE-IDENTICAL across ALL 18 mech variants** (parsed from `content/BTL4.RES` type-17
|
|
||||||
SubsystemModelStream, parser `scratchpad/night17/myoaudit.py`; walk verified byte-exact --
|
|
||||||
every stream consumed to the byte via the self-describing `subsystemModelSize@+0x24`).
|
|
||||||
Every chassis gets the SAME: Myomers record (velEff 0.995 / accEff 0.8 / tmass 2.5e5 /
|
|
||||||
cond 1.9e5 / seekV 0.3/0.5/0.7/0.9999 rec 2, hsi=8→Condenser5, vsi=13→GeneratorD), six
|
|
||||||
identical Condensers (tmass 4.2e5, cond 3.15e5, refrig 3.0, degT=failT=2000), identical
|
|
||||||
Generators (rated 10000 V, tap 6, GenD always hsi=8→Condenser5), identical Reservoir
|
|
||||||
(cap 20, squirt 0.5). Loop 5 = GeneratorD + Myomers ONLY on madcat/vulture/vul1/thor/
|
|
||||||
thr1/loki/lok1/lok2 (the fielded chicken walkers carry the MINIMUM possible loop-5 load);
|
|
||||||
the variants with extra loop-5 weapons are mad1 (2 MLaser), mad2 (ERPPC), ava1 (2 ERMLaser),
|
|
||||||
own1 (AFC25), sunder (2 MLaser) -- no walker bias. Manual COOLANT LOOPS pages print the
|
|
||||||
same GenD+Myomers loop-5 topology for every mech [T1 primary]. The ONLY chassis-varying
|
|
||||||
heat inputs are moverMass (type-15 GameModel +0x00: owens 35t, vulture/blkhawk 60t, loki
|
|
||||||
65t, thor/avatar 70t, madcat 75t, sunder 90t) and top speed/accel (manual stat sheets:
|
|
||||||
Loki/Thor 143 N / 182 SC kph, Vulture ~175 N [OCR-fuzzy T3], Owens 154, Blackhawk ~190
|
|
||||||
[OCR-suspect]; maxAccel authored: madcat 20, loki 15, vulture/thor/avatar/sunder 10,
|
|
||||||
blkhawk 30, owens 50). m·v² through the verified formula puts the 60-75 t / 143-175 kph
|
|
||||||
walkers at ~1.6-2.2x the light-humanoid kinetic heat rate into the SAME fixed-size loop --
|
|
||||||
seek-4 zero-cooling degradation bound ~7-10 s (vulture worst) vs ~16 s (owens). Verdict
|
|
||||||
lean: AUTHENTIC authored balance (fast-heavy chassis cook by design); residual port-defect
|
|
||||||
risk sits ONLY in the runtime velocity magnitude (locomotion lane -- cross-check the port's
|
|
||||||
per-chassis top speed vs the manual print figures; bench lane #9).
|
|
||||||
|
|
||||||
**#173 BENCH lane result (2026-08-13) [T2 measured, live receipts]: Elengil's report
|
|
||||||
REPRODUCED and quantified; the equilibrium is a GOVERNOR THERMOSTAT, and it is chassis-blind
|
|
||||||
above a speed threshold.** Rig: solo grass/day expert, full forced throttle + constant 0.12
|
|
||||||
turn, BT_FORCE_SEEK, 150 s/run (`scratchpad/night16/myo173.sh` + `myo173b.sh`, digest
|
|
||||||
`digest173.py`, logs `content/m173_*.log`). Streaming receipts on every chassis matched the
|
|
||||||
authored data exactly ([heat-link] Myomers sinkIdx=8→Condenser5 k=1.9e5 m=2.5e5; [spec]
|
|
||||||
hsCount 18/14/28/13/15/90t...; C5 mScale=3 flowScale=0.1667 balanced). Matrix (peak v /
|
|
||||||
steady v u/s, time to degT 1000, equilibrium T, C5 end):
|
|
||||||
- vulture seek-1: 24.5/21.1, never, **eq 423**, C5 333 — gears 1-3 are harmless (ratio floors 1).
|
|
||||||
- vulture seek-4: 56.9/31.9, **13.9 s**, **eq 1801**, C5 1410 (loop-load bar full-scale ~15-20 s).
|
|
||||||
- madcat seek-4: 56.8/28.4, 12.1 s, eq 1820, C5 1420. avatar seek-4 (HUMANOID, same 143-kph
|
|
||||||
speed class in the port): 56.2/29.5, 14.3 s, eq 1819, C5 1424 — **identical cook**.
|
|
||||||
- thor seek-4: 34.6/34.3, 79.9 s, 1311 still climbing; sunder (90 t): 34.6/34.0, 49.4 s, 1592
|
|
||||||
climbing — the port's slow (87-kph-class) chassis merely take longer.
|
|
||||||
- STRAIGHT-line control (no turn): eq 1802, |a|≈20 — same as turning, so the accel term is
|
|
||||||
GAIT-SURGE noise in the velocity ring, not commanded motion (see residual below).
|
|
||||||
- **Valve boost, the authored counter-play** (one MoveValve press on Condenser5, detent 1→5 =
|
|
||||||
share 0.5): myomers still governor-pinned ~1715 but **sustained drive 0.68 vs 0.54 balanced
|
|
||||||
(v 39-41 vs ~32) and C5 968 vs 1410** — boost buys back speed, not myomer temp. The run's
|
|
||||||
accidental map-edge stop measured the standstill shed: 1723→586 in ~35 s (−10/s) — "heat
|
|
||||||
only sheds near standstill" is the fixed-capacity chain being honest (slope +0.02/s while
|
|
||||||
cruising).
|
|
||||||
**Mechanics of the thermostat**: T rises past degT → AvailableOutput derates → v falls →
|
|
||||||
m·v² generation falls → equilibrium lands ~1700-1820, just UNDER failT 2000, on EVERY chassis
|
|
||||||
fast enough to out-run the loop — better cooling raises the sustained SPEED at the same
|
|
||||||
pinned T. Steady seek-4 cruising never freezes (failT) solo; the field freezes need adders
|
|
||||||
(damage dmgGain ≤2x, combat weapon heat, respawn transients, a GenD trip on the shared loop).
|
|
||||||
**Manual cross-check settles the speed residual in favor of AUTHENTIC** [T1 print, re-read
|
|
||||||
via fitz p24/25]: MadCat prints **175 N / 220 SC**, Vulture **175 N**, Thor/Loki 143/182 —
|
|
||||||
the machine's big chicken walkers were FASTER than the port's measured 143 N / 205 SC (thor
|
|
||||||
port 87/124 is far under its print), so authored m·v² generation on the pod was ≥ what these
|
|
||||||
benches produced; the phenomenon (manual p21 warns of it explicitly) is authored. The port's
|
|
||||||
per-chassis top-speed shortfalls remain a LOCOMOTION-lane item but cannot rescue a "port too
|
|
||||||
hot" theory — correcting them makes it hotter.
|
|
||||||
**Verdict: AUTHENTIC mechanism + authored balance [T2]; ONE bounded port-side severity
|
|
||||||
residual [T3]** — the accel term's |a| operand (15-sample velocity-ring derivative) is
|
|
||||||
frame-cadence-sensitive: at ~59 fps a straight flat cruise reads |a|≈14-20 (thor/sunder's
|
|
||||||
smoother gait ≈4) and the accel term carries 50-65% of steady seek-4 generation; the same
|
|
||||||
gait bob sampled through the pod's 28 Hz ring would read ~2-3x smaller, so the pod's
|
|
||||||
equilibrium may have sat ~1200-1500 (shallow governor, near-full supercharge speed held)
|
|
||||||
instead of the port's ~1800 (deep governor, speed sags to ~0.54). Same phenomenon, softer
|
|
||||||
edge — this is the gap that would reconcile Oracle's "ran 182 kph sustained" pod memory.
|
|
||||||
If field calibration is ever wanted, the knob is the |a| feed's cadence (sample the ring at
|
|
||||||
28 Hz), NOT the formula constants (byte-exact, #137). Also note the CLIMB term is inert in
|
|
||||||
the port (g=0, EnvironmentZone unwired) — the pod added heat on slopes that we do not.
|
|
||||||
|
|
||||||
**#173 DECISIVE VALVE-BOOST + CADENCE EXPERIMENT (2026-08-13 night17;
|
|
||||||
`scratchpad/night17/m173v_bench.sh`/`m173v2_bench.sh`/`m173v_check.py`, logs
|
|
||||||
`content/m173_vul_[ABCEFG]*.log`, captures `content/vlv_*.png`) — the residual above is now
|
|
||||||
MEASURED and the verdict sharpens:**
|
|
||||||
- **Valve delivery PORT-FAITHFUL, receipt-PROVEN [T2 live / T1 operands]**: the `BT_VALVE5=N`
|
|
||||||
hook (mech4.cpp, env-gated) presses Condenser5's MoveValve N times; every landed detent is
|
|
||||||
receipted (`[valve] Condenser5 valve -> detent 5/50/0` + the zero-sum redistribute lines:
|
|
||||||
share 0.5 → 0.909091 → 0, others 0.1 → 0.0182 → 0.2, exactly @0049f788's flow=valve/total).
|
|
||||||
C5 responds as flow dictates (cruise Tc5: balanced 1396 → detent-50 712 → CLOSED 1902 pegged).
|
|
||||||
- **The bottleneck SHIFTS to the valve-independent stage [T1+T2]**: myomer→Condenser5
|
|
||||||
conduction measures 4.1–5.0e4 heat-units/K of dT in EVERY run (balanced/boosted/closed,
|
|
||||||
both cadences) — constant because @004ad9ec's exponent reads the CALLER's own
|
|
||||||
flowScale@+0x15C (the myomer's, pinned 1.0), never the condenser's valve share [T1 raw
|
|
||||||
pseudocode]. The solver's 2.7x boosted whole-chain throughput is CONFIRMED live (4.8e7/s
|
|
||||||
through the chain at 60fps detent 50) but its "sustains under degT 1000" corollary is
|
|
||||||
WRONG at any cadence: sub-1000 Tm caps stage-1 flux at ~3e7/s < supercharge generation, so
|
|
||||||
the governor always engages; the boost buys SPEED at a lower pinned Tm, never sub-1000 Tm.
|
|
||||||
- **The cadence residual is REAL and ~2x [T2 measured]** (BT_MYO_RING_HZ=28 knob landed in
|
|
||||||
mech4.cpp — cadence only, constants untouched): at ~59fps the |a| gait-surge noise carries
|
|
||||||
64–66% of steady seek-4 generation (|a|≈17–26 flat grass); at 28Hz it drops to 24–32%
|
|
||||||
(|a|≈6–7) and generation at equal speed HALVES. The six curves (solo vulture seek-4, t1000 /
|
|
||||||
equilibrium / held v): 60fps bal 12.3s/1800/28 · 60fps d50 12.3s/1675/44 · 60fps CLOSED
|
|
||||||
13.9s/NO-EQ-1936-still-climbing/11.7 (drive 0.21, C5 pegged 1902 — the wrap-trap signature)
|
|
||||||
· 28Hz bal 18.3s/~1710/39–41 · **28Hz d50 22.5s/~1500 STABLE/54–57 = FULL supercharge speed
|
|
||||||
sustained indefinitely** (equilibrium sits on the soft governor shoulder, zero speed cost;
|
|
||||||
500 under failT). **The machine's loop-5 boost genuinely sustained unrestricted seek-4 — the
|
|
||||||
manual's Super Charged print is vindicated at pod cadence; the port at ~59fps over-punishes
|
|
||||||
by ~2x. Severity verdict flips to PORT CADENCE DEFECT (fix = 28Hz ring sampling; default
|
|
||||||
promotion = owners' decision).** Balance verdict unchanged: balanced-valve seek-4 cooks at
|
|
||||||
BOTH cadences (authentic, manual-warned).
|
|
||||||
- **Field "3–4s to governor" ≠ spawn preheat [T2]**: field-shaped run (autofire at scenery
|
|
||||||
from frame 0, no pre-settle) still takes 15.9s to degT from cold; GenD/loop-5 preheat is
|
|
||||||
real but slow (GenD 666 by 29s). The 3–4s feel is a WARM-START baseline (T≥800 → <3s) plus
|
|
||||||
the deep 59fps governor making the derate obvious.
|
|
||||||
- **Heat-MFD valve slider IN SYNC [T2 pixel-verified]**: the vertNormalSlider renders
|
|
||||||
coolantFlowScale@0x15C — the SAME cell the flow model consumes (single-cell design, no
|
|
||||||
split possible); measured bar fractions 0.20/0.51/0.90/0.04 for receipt shares
|
|
||||||
0.167/0.5/0.909/0.0 (perfectly linear; `vlv_*.png`). Detent 50 (≈90% up) and detent 0
|
|
||||||
(bottom) are clearly distinguishable; the residual legibility trap is CLOSED-vs-STARVED
|
|
||||||
(share 0 vs 0.018–0.1 ≈ 1–2px apart) and the silent 4th-press wrap — "maxed and couldn't
|
|
||||||
shed" field reports match the run-C wrap signature, not a display desync.
|
|
||||||
|
|||||||
@@ -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
|
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
|
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.
|
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).
|
|
||||||
|
|||||||
@@ -428,14 +428,6 @@ in the mech reset (part_012.c:9446-9447) — a genuine regression, not dead cont
|
|||||||
inbound lock/launch on the player exists; DistanceToMissile = range to nearest inbound missile
|
inbound lock/launch on the player exists; DistanceToMissile = range to nearest inbound missile
|
||||||
(far default when none). The authored watchers then reproduce the accelerating alarm unchanged.
|
(far default when none). The authored watchers then reproduce the accelerating alarm unchanged.
|
||||||
|
|
||||||
**ERA-WITNESS CONFIRMED (2026-08-13, Oracle, via the distributable soundboard):** Oracle found
|
|
||||||
the sample himself on the board — `[1:74] IncomingAlarm01 n58` — and confirms it is the
|
|
||||||
incoming-missile alarm, adding "**it was sped up in the pods**": independent field confirmation
|
|
||||||
of both the sample identity AND the authored DistanceToMissile→tempo acceleration (the 100..800
|
|
||||||
→ tempo 600..10 mapping above). Gitea #83 (comments 1781/1783). The reconstruction target is now
|
|
||||||
fully specified except the binary's exact IncomingLock/DistanceToMissile update rule (what sets
|
|
||||||
+0x3FC/+0x400 — seeker-locked-only vs any-inbound, and nearest-missile selection).
|
|
||||||
|
|
||||||
<a name="f5"></a>
|
<a name="f5"></a>
|
||||||
#### F5. FootStep is a contact LEVEL, not a clip-transition pulse (High, CONFIRMED)
|
#### F5. FootStep is a contact LEVEL, not a clip-transition pulse (High, CONFIRMED)
|
||||||
|
|
||||||
@@ -625,60 +617,6 @@ reduce source-pool starvation.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<a name="seqcensus"></a>
|
|
||||||
### The complete authored trigger-note census (2026-08-13, [T1]) — and the game-rate soundboard
|
|
||||||
|
|
||||||
**The playback-rate model, end to end:** effective output rate = WAV declared rate x
|
|
||||||
2^((note-60)/12) (`L4AUDIO.cpp:21-24` BTNotePitchFactor -> `alSourcef(AL_PITCH)` at :971
|
|
||||||
per-SetupPatch; per-zone SoundFont tuning is baked into each extracted WAV's declared rate per
|
|
||||||
F2, so note 60 == authored pitch). Sources default to note 60 (`AUDSRC.cpp:18 DEFAULT_NOTE`);
|
|
||||||
the ONLY senders of NoteAudioControlID are AudioControlSequences (grep-verified engine-wide),
|
|
||||||
so **every off-file-rate playback in the game comes from a sequence note**. Dynamic pitch-cents
|
|
||||||
(doppler, pitch-scale watchers) modulate on top and are not part of the base rate.
|
|
||||||
|
|
||||||
**BTL4.RES AudioControlSequence record format** (matches the ctor read order,
|
|
||||||
`AUDSEQ.cpp:165-194`): `[classID=75 i4][objectID i4][dump i4][looped i4][targetObjID i4]
|
|
||||||
[divisionsPerBeat i4][tempo i4][eventCount i4][count x (tick i4, ctlID i4, value f4)]`;
|
|
||||||
ctl 8 = note, 1 = start, 2 = stop, 6 = attack-volume. Every object record in the audio
|
|
||||||
streams is `[classID i4][objectID i4][payload]` with ClassID = the `VDATA.h` enum
|
|
||||||
(AudioStateTrigger=28, sequence=75, splitter=76, DirectPatchSource=1001, PatchResource=1004,
|
|
||||||
PatchLevelOfDetail=1011 — LOD payload `[voiceCount i4][renderType i4][suspendSecs f4]
|
|
||||||
[bank u1][patch u1][maxFilter i4]`). Patch resources/LODs live in the shared
|
|
||||||
`StaticAudioStream` (id 0); vehicle `*int.scp` streams reference them cross-stream.
|
|
||||||
Sequence -> target -> source -> resource -> LOD resolution yields the patch for every one of
|
|
||||||
the **324 sequences (15 unique patterns)** in the RES; cross-validated against live SetupPatch
|
|
||||||
logs (mp4l/solo/genedge).
|
|
||||||
|
|
||||||
| notes (authored order) | looped | patch | trigger (watcher census) |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 16,19,23 | yes | 2:113 Warnings01 | coolant-leak voice loop (ReportLeak, 19 subsystems) |
|
|
||||||
| 16,33,36 | no | 2:113 | ammo cook-off countdown voice (FireCountdownStarted) |
|
|
||||||
| 29,16,26 | no | 2:113 | generator-out voice phrase (GeneratorState/GeneratorOn) |
|
|
||||||
| 29,16,40 | no | 2:113 | Entity.SimulationState warning voice phrase |
|
|
||||||
| 30,18 / 49,43 / 95 | no | 2:115 Death01 | death phrases — a **random 3-pick list** `[3][seqA][seqB][seqC]` at death |
|
|
||||||
| 36,84 | no | 1:36/40/41/56/70/71/75 | weapon loaded/jam/misfire clunk-then-blip (WeaponState) |
|
|
||||||
| 36,70 | no | 2:64 DestroyedInt06 | subsystem destroyed (SimulationState) |
|
|
||||||
| 58 x6 | yes | 1:74 IncomingAlarm01 | missile-lock beeper (IncomingLock; DistanceToMissile -> tempo) |
|
|
||||||
| 51,51 | yes | 1:83 ProgramButton01 | weapon-configure ticker (ConfigureActivePress) |
|
|
||||||
| 30,48,46,96,44,63,66 | no | 2:83 MechExplosion01 | brnext.scp |
|
|
||||||
| 45,38 | no | 2:97 MechFireLoop01 | brnext.scp |
|
|
||||||
| 57,69,46,24 / 69,51 | no | 2:119+120 AuxExplosion01(+LOD2) | bigexp.scp / medexp+mbnexp.scp |
|
|
||||||
|
|
||||||
Net: **92 (sample-zone, note) pairs play rate-shifted vs their WAV header** (worst: the
|
|
||||||
Warnings01 voice zones at x0.079-x0.315 — the raw files render Yip's voice 3.2x-12.7x too
|
|
||||||
fast, which is why the old soundboard's voice sounded wrong; weapon ready blips x4; death
|
|
||||||
phrase C zones x7.55). Bonus census finding: **Death01 z8-11, AuxExplosion01 z6-7 and
|
|
||||||
AuxExplosion01LOD2 z3 are reachable by NO authored note** — dead zones.
|
|
||||||
|
|
||||||
**Tools:** `tools/soundboard.py` now plays every sample at its game rate (header-rewritten
|
|
||||||
temp copies, per-note buttons, raw-rate toggle); `tools/mksoundboard.py` builds
|
|
||||||
`dist/SOUNDBOARD_BT411.zip` — a self-contained tester soundboard page (file://-safe `<audio>`
|
|
||||||
elements, `preservesPitch=false`, playbackRate = the game shift) whose COPY buttons emit the
|
|
||||||
canonical `[bank:patch] Name n##` reference string for bug reports. Both share the census
|
|
||||||
table (`GAME_TRIGGERS` in soundboard.py).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<a name="validations"></a>
|
<a name="validations"></a>
|
||||||
## 4. Validations and KB corrections (no fidelity gap)
|
## 4. Validations and KB corrections (no fidelity gap)
|
||||||
|
|
||||||
@@ -745,5 +683,3 @@ table (`GAME_TRIGGERS` in soundboard.py).
|
|||||||
**Regression harness:** re-run the audit capture after each block and assert with
|
**Regression harness:** re-run the audit capture after each block and assert with
|
||||||
`scratchpad/audio_coverage.py` that the dead-watcher count monotonically drops; A/B pitch/volume
|
`scratchpad/audio_coverage.py` that the dead-watcher count monotonically drops; A/B pitch/volume
|
||||||
spot checks against the recovered-rate table for the bank regeneration.
|
spot checks against the recovered-rate table for the bank regeneration.
|
||||||
|
|
||||||
**EAR-VERIFIED 2026-08-13 (epilectrik): Yip's Warnings01 voice zones at GAME rate (x0.079-x0.315) confirmed correct by ear via the distributable soundboard — the raw-file 3.2-12.7x overspeed is settled as a storage artifact, the note-transposed rate is the pod's voice. SOUNDBOARD_BT411.zip approved for tester distribution.**
|
|
||||||
|
|||||||
@@ -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`
|
- **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
|
(`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).
|
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.
|
(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`)
|
## 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 |
|
| 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) |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 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 |
|
| 31 | sec: schematic HEAT view (cmHeat) | subsystem currentTemperature tint | #6 pixel-verified; re-cycled this audit (mask 0x450421→0x490421→0x510421) | T2 | CORRECT |
|
||||||
|
|||||||
@@ -1,604 +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) | FAITHFUL (brick claim RETRACTED 2026-08-13 -- constant-width misread; binary clamps overcharge to 1.0 and fires; witnesses 6-0) |
|
|
||||||
| 6 | Autocannons AFC25/50/100 (0xBCD) | FIXED-THIS-BUILD (#171; T2 field receipts pending) |
|
|
||||||
| 7 | AC cosmetic tracer visual | FIXED-THIS-BUILD (recovered TOF constants 2.0f + 1/9.8 applied, b94636b) |
|
|
||||||
| 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 | FIXED-THIS-BUILD (capsule sweep [T3 radius]; T2 field receipts pending) |
|
|
||||||
| 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) -- brick claim RETRACTED 2026-08-13
|
|
||||||
- **Binary [T1]:** `ToggleSeekVoltage` @004ba478 (vet/expert only): wrap
|
|
||||||
from gear 4→1 skips `ResetFiringState` → charge above snap window →
|
|
||||||
~~overcharge reads ZERO → PERMANENTLY BRICKED~~ **RETRACTED: _DAT_004ba830
|
|
||||||
is a DOUBLE 1.0 (width misread); the binary CLAMPS overcharge to 1.0 and
|
|
||||||
the weapon fires -- the machine never bricked. Era witnesses (6-0) triggered
|
|
||||||
the re-audit; full detail in context/decomp-reference.md.**
|
|
||||||
- **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 radius / port-shape, landed 2026-08-13]:** `mech4.cpp
|
|
||||||
BTUpdateProjectiles` — per frame, damage-carrying GUIDED rounds (the one
|
|
||||||
salvo lead) run a nearest-approach test of the flight segment against a
|
|
||||||
VERTICAL CAPSULE per other registered mech (BTGetTargetCandidates walk;
|
|
||||||
shooter + locked target excluded — the target keeps its own 4.0u fuze).
|
|
||||||
Capsule derived from the victim's own collision template
|
|
||||||
(Mover::GetCollisionTemplate BoxedSolid: axis = origin up minY..maxY,
|
|
||||||
radius = larger horizontal half-extent; fallback 3.5u × 14u when the
|
|
||||||
template is unresolved [T3]). Earliest contact along the segment wins
|
|
||||||
(bystander vs target fuze compared by segment param); the struck mech
|
|
||||||
becomes the DIRECT victim through the unchanged dispatch path (cluster
|
|
||||||
roll + splash semantics identical, splash excludes the new direct
|
|
||||||
victim). Wrecks stay in the sweep — binary detonates on any solid; the
|
|
||||||
#174 victim-side guard keeps dead zones from cascading/scoring. Capped
|
|
||||||
`[projectile] BYSTANDER` receipt (24 prints) for field forensics.
|
|
||||||
- **Verdict:** FIXED-THIS-BUILD (radius is a [T3] capsule approximation of
|
|
||||||
the real solid sweep; T2 field receipts pending — Friday watch item
|
|
||||||
unchanged: "hit by missiles aimed at someone else" now expected INSIDE
|
|
||||||
splash range too).
|
|
||||||
|
|
||||||
### 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: LANDED 2026-08-13 (lane 12 entry) together
|
|
||||||
with coast gravity (dt-table row); field guide §6's Friday watch item
|
|
||||||
now doubles as the T2 verification channel.
|
|
||||||
|
|
||||||
## 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) | **WAS DIVERGENT → FIXED 2026-08-13** (`vel.y −= 6.5·coast_dt` on thruster rounds after burnout, exact at the burnout boundary; speed re-synced to |vel|) | post-burnout flights only; missed rounds now drop instead of flying level |
|
|
||||||
| 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,579 +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 stored charge. You cannot
|
|
||||||
fire early -- the trigger does nothing until the charge completes.
|
|
||||||
The only partial-power shot is a LOWER CHARGE GEAR (section 2,
|
|
||||||
veteran/expert only): a gear at half voltage is ready sooner but
|
|
||||||
hits for a quarter. Distance never matters: full damage anywhere
|
|
||||||
inside effective range, zero damage past it.
|
|
||||||
- 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) -- SETTLED BY YOUR MEMORY
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
THE ORIGINAL'S BEHAVIOR
|
|
||||||
Veteran/expert pilots could cycle an energy weapon's charge gear
|
|
||||||
(four settings). Cycling past the top wraps to the bottom and the
|
|
||||||
weapon keeps working: a stored charge above the new gear's level
|
|
||||||
simply counts as FULL and fires (a wrap-fired top-gear charge deals
|
|
||||||
its normal top-gear damage -- no secret super-shot, no dead weapon).
|
|
||||||
|
|
||||||
FOR THE RECORD: an earlier version of this document claimed the
|
|
||||||
machine shipped a flaw here -- that wrapping the gear left the
|
|
||||||
weapon permanently dark. Several of you said flatly that never
|
|
||||||
happened. You were right. The re-audit you triggered found our
|
|
||||||
reading of one number in the original's code was wrong by a single
|
|
||||||
byte-width, and the "brick" existed only in our port, never on the
|
|
||||||
machine. This section is your correction, adopted.
|
|
||||||
|
|
||||||
OUR PORT TODAY
|
|
||||||
Fixed to match: cycling behaves exactly as you remember. (Before
|
|
||||||
the fix our gauges could briefly read EMPTY on an over-charge where
|
|
||||||
the machine read FULL -- if you saw that, it dies in the next
|
|
||||||
build.)
|
|
||||||
|
|
||||||
CHANGING IN THE NEXT BUILD
|
|
||||||
The correction above ships. The old "deviation" entry is withdrawn
|
|
||||||
-- the energy lane now has ZERO deviations from the original.
|
|
||||||
|
|
||||||
YOUR NOTES (callsign first, one entry per line):
|
|
||||||
>
|
|
||||||
>
|
|
||||||
>
|
|
||||||
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
2B. SEEK-4 ON THE HEAVY WALKERS (VULTURE / MADCAT / THOR / LOKI)
|
|
||||||
-----------------------------------------------------------------------------
|
|
||||||
THE ORIGINAL'S BEHAVIOR (audited to the byte this week)
|
|
||||||
- Drive heat scales with weight times speed squared, and no amount
|
|
||||||
of running ever cools you -- there is no airflow cooling anywhere
|
|
||||||
in this machine. The fast-heavy Clan walkers pay the most.
|
|
||||||
- Gears 1 through 3 make IDENTICAL heat: seek-3 is free speed.
|
|
||||||
Seek-4 roughly doubles drive heat AND raises your top speed --
|
|
||||||
a sprint gear. Cruising it on a heavy walker without the boost
|
|
||||||
crosses myomer degradation in under twenty seconds and the
|
|
||||||
governor bites. A same-weight humanoid at the same speed cooks the same:
|
|
||||||
it is physics, not a chicken-walker curse.
|
|
||||||
- The machine gave you the tool: BOOST COOLANT LOOP 5 (Generator D /
|
|
||||||
Myomers). One press visibly buys your speed back; a full boost
|
|
||||||
sustains seek-4 on a Vulture indefinitely.
|
|
||||||
- THE TRAP: one press past maximum WRAPS THE VALVE CLOSED. If you
|
|
||||||
ever boosted a loop and it cooked anyway -- you probably hit the
|
|
||||||
wrap. Two presses, never three.
|
|
||||||
- Standing still sheds heat fast (~10 degrees a second).
|
|
||||||
|
|
||||||
OUR PORT TODAY -- AND A CONFESSION YOUR REPORTS FORCED
|
|
||||||
You were right that something was off. Our build measured your
|
|
||||||
mech's acceleration at modern frame rates, and at those rates the
|
|
||||||
measurement picks up walking-gait jitter as phantom acceleration --
|
|
||||||
nearly TWO-THIRDS of your drive heat on a flat cruise was noise
|
|
||||||
that the original's electronics (sampling at their own slower
|
|
||||||
clock) never saw. The port was punishing heavy walkers roughly
|
|
||||||
TWICE as hard as the machine did.
|
|
||||||
|
|
||||||
CHANGING IN THE NEXT BUILD (the fix, measured)
|
|
||||||
Heat sensing now samples on the original's clock. Result on the
|
|
||||||
bench: a Vulture at seek-4 WITH the loop-5 boost holds FULL
|
|
||||||
supercharge speed indefinitely, running hot but stable -- the
|
|
||||||
valve trick works exactly as the machine intended, and the
|
|
||||||
manual's printed Super Charged speed is real. WITHOUT the boost,
|
|
||||||
seek-4 still cooks you (that part is the machine's own balance,
|
|
||||||
and it stays). The wrap trap also stays: two presses, never three.
|
|
||||||
|
|
||||||
WE NEED YOUR MEMORY
|
|
||||||
- Next build: boost loop 5, hold seek-4 on a big walker. Does the
|
|
||||||
sustained supercharge match how the machine felt? Your yes
|
|
||||||
closes the oldest heat complaint on the books.
|
|
||||||
- Did the loop-5 valve boost trick live in era pilot lore, or are
|
|
||||||
we rediscovering 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.
|
|
||||||
=============================================================================
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <map> // (BT411) the 28Hz smoother-intake clock (cadence census item 3)
|
|
||||||
#include "munga.h"
|
#include "munga.h"
|
||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
@@ -958,55 +957,8 @@ void
|
|||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
if (control_ID == controlID)
|
if (control_ID == controlID)
|
||||||
{
|
|
||||||
// (BT411 cadence census item 3, gotcha 32) SAMPLE INTAKE AT 28 Hz:
|
|
||||||
// the authored olympic windows (N=30/15, fill 0) are SIZED for one
|
|
||||||
// sample per POD frame (28 Hz -- AUDWTHR.h:457); the port's watcher
|
|
||||||
// poll feeds one per RENDER frame (~59), which halves the smoothing
|
|
||||||
// window in wall time and feeds a noisier operand. Clock ONLY the
|
|
||||||
// Add (the smoother's own intake) on a per-instance 28 Hz wall
|
|
||||||
// clock; the forward below still runs EVERY poll with the held
|
|
||||||
// average, so downstream watcher semantics are untouched. During a
|
|
||||||
// hitch the pod would have taken ~28 samples/s of the stalled
|
|
||||||
// value -- catch-up repeats the current sample, capped at the
|
|
||||||
// largest authored window (30), heat.cpp #119 pattern.
|
|
||||||
// BT_AUD_SMOOTH_HZ=<hz> overrides (=0 restores per-poll intake).
|
|
||||||
{
|
|
||||||
static Scalar s_hz = -2.0f;
|
|
||||||
if (s_hz < -1.0f)
|
|
||||||
{
|
|
||||||
const char *hz = getenv("BT_AUD_SMOOTH_HZ");
|
|
||||||
s_hz = (hz != 0 && *hz != '\0') ? (Scalar)atof(hz) : 28.0f;
|
|
||||||
}
|
|
||||||
if (s_hz > 0.0f)
|
|
||||||
{
|
|
||||||
static std::map<const void *, DWORD> s_nextMs;
|
|
||||||
const DWORD now = GetTickCount();
|
|
||||||
const DWORD tick = (DWORD)(1000.0f / s_hz); // 35 ms at 28
|
|
||||||
DWORD &next = s_nextMs[this];
|
|
||||||
if (next == 0)
|
|
||||||
{
|
{
|
||||||
audioControlAverage.Add(control_value);
|
audioControlAverage.Add(control_value);
|
||||||
next = now + tick;
|
|
||||||
}
|
|
||||||
else if ((long)(now - next) >= 0)
|
|
||||||
{
|
|
||||||
int added = 0;
|
|
||||||
while ((long)(now - next) >= 0 && ++added <= 30)
|
|
||||||
{
|
|
||||||
audioControlAverage.Add(control_value);
|
|
||||||
next += tick;
|
|
||||||
}
|
|
||||||
if ((long)(now - next) >= 0)
|
|
||||||
next = now + tick; // hitch: resync
|
|
||||||
}
|
|
||||||
// else: off-tick poll -- hold; the average is forwarded below
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
audioControlAverage.Add(control_value); // legacy per-poll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getenv("BT_AUDIO_SPATIAL")) { static int s_sa=0;
|
if (getenv("BT_AUDIO_SPATIAL")) { static int s_sa=0;
|
||||||
if ((controlID == 100 || controlID == 101) && s_sa++ < 3000)
|
if ((controlID == 100 || controlID == 101) && s_sa++ < 3000)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#include <cstdlib> // (BT411) getenv -- the 28Hz doppler-operand clock (cadence census item 4)
|
|
||||||
#include <map> // (BT411) per-instance sample-and-hold registry
|
|
||||||
#include "munga.h"
|
#include "munga.h"
|
||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
@@ -444,45 +442,6 @@ void
|
|||||||
);
|
);
|
||||||
Check(&relative_velocity_temp);
|
Check(&relative_velocity_temp);
|
||||||
|
|
||||||
//
|
|
||||||
// (BT411 cadence census item 4, gotcha 32) 28 Hz SAMPLE-AND-HOLD on the
|
|
||||||
// doppler velocity operand: worldLinearVelocity is a raw per-render-frame
|
|
||||||
// position derivative (the mech4 producer feeds other consumers and is
|
|
||||||
// NOT touched); at ~59 fps its gait-aliasing noise reaches this divisor
|
|
||||||
// directly and warbles dopplerCents per stride. The pod computed this
|
|
||||||
// once per 28 Hz frame. Hold the RELATIVE WORLD velocity per instance
|
|
||||||
// on a 28 Hz wall clock; the head-frame transform below stays live per
|
|
||||||
// call (geometry is smooth -- only the noisy operand is clocked).
|
|
||||||
// BT_AUD_DOPPLER_HZ=<hz> overrides (=0 restores per-call sampling).
|
|
||||||
//
|
|
||||||
int dopplerFresh = -1; // receipt: -1 legacy / 1 sampled / 0 held
|
|
||||||
{
|
|
||||||
static Scalar s_dopHz = -2.0f;
|
|
||||||
if (s_dopHz < -1.0f)
|
|
||||||
{
|
|
||||||
const char *hz = getenv("BT_AUD_DOPPLER_HZ");
|
|
||||||
s_dopHz = (hz != 0 && *hz != '\0') ? (Scalar)atof(hz) : 28.0f;
|
|
||||||
}
|
|
||||||
if (s_dopHz > 0.0f)
|
|
||||||
{
|
|
||||||
struct DopplerHold { DWORD nextMs; Vector3D held; };
|
|
||||||
static std::map<const void *, DopplerHold> s_dopHold;
|
|
||||||
DopplerHold &h = s_dopHold[this];
|
|
||||||
const DWORD now = GetTickCount();
|
|
||||||
if (h.nextMs == 0 || (long)(now - h.nextMs) >= 0)
|
|
||||||
{
|
|
||||||
h.held = relative_velocity_temp;
|
|
||||||
h.nextMs = now + (DWORD)(1000.0f / s_dopHz);
|
|
||||||
dopplerFresh = 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
relative_velocity_temp = h.held;
|
|
||||||
dopplerFresh = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relative_velocity.MultiplyByInverse(
|
relative_velocity.MultiplyByInverse(
|
||||||
relative_velocity_temp,
|
relative_velocity_temp,
|
||||||
head_entity->localToWorld
|
head_entity->localToWorld
|
||||||
@@ -514,19 +473,6 @@ void
|
|||||||
{
|
{
|
||||||
dopplerCents = cents;
|
dopplerCents = cents;
|
||||||
}
|
}
|
||||||
|
|
||||||
// (BT411) doppler receipt -- jitter A/B for the 28Hz operand clock.
|
|
||||||
// hm2 = |held relative WORLD velocity|^2 (the clocked operand itself,
|
|
||||||
// geometry-free); h = 1 fresh sample / 0 held / -1 legacy per-call.
|
|
||||||
if (getenv("BT_AUD_DOPPLER_LOG")) { static int s_dl = 0; if (s_dl++ < 6000)
|
|
||||||
DEBUG_STREAM << "[doppler] t=" << (GetTickCount() % 1000000)
|
|
||||||
<< " this=" << (void *)this
|
|
||||||
<< " cents=" << dopplerCents
|
|
||||||
<< " v=" << speed_of_source
|
|
||||||
<< " d=" << distanceToSource
|
|
||||||
<< " h=" << dopplerFresh
|
|
||||||
<< " hm2=" << relative_velocity_temp.LengthSquared()
|
|
||||||
<< "\n" << std::flush; }
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1313,6 +1313,20 @@ static void
|
|||||||
int mask = port->GetBitMask();
|
int mask = port->GetBitMask();
|
||||||
int palId = port->paletteID;
|
int palId = port->paletteID;
|
||||||
int tint = w->monoTint;
|
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;
|
static int sMfdPal = -1;
|
||||||
if (sMfdPal < 0) sMfdPal = getenv("BT_GLASS_MFD_PAL") ? 1 : 0;
|
if (sMfdPal < 0) sMfdPal = getenv("BT_GLASS_MFD_PAL") ? 1 : 0;
|
||||||
if (sMfdPal && tint >= 0)
|
if (sMfdPal && tint >= 0)
|
||||||
@@ -2110,6 +2124,11 @@ static unsigned long
|
|||||||
const char *ports[8];
|
const char *ports[8];
|
||||||
int np = 0;
|
int np = 0;
|
||||||
ports[np++] = w->portPrimary;
|
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;
|
if (w->portAlt != NULL) ports[np++] = w->portAlt;
|
||||||
for (int gi = 0; gi < w->groupCount && np < 7; ++gi)
|
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
|
// 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.
|
// 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.
|
// 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] =
|
static const BTGaugeSurfaceDesc kBTGaugeSurfaces[9] =
|
||||||
{
|
{
|
||||||
{ "Heat", 0xFFFF, 0.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
|
{ "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;
|
rot = s_secRot;
|
||||||
}
|
}
|
||||||
svga->DrawDevSurface(
|
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);
|
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 surf = ckSlotOf[i];
|
||||||
int useTint = (d.monoTint < 0) ? -1 : tint;
|
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);
|
(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
|
//Drawing secondary, make sure we got secondary
|
||||||
if (secPort != NULL)
|
if (secPort != NULL)
|
||||||
{
|
{
|
||||||
secMask = secPort->GetBitMask();
|
secMask = BTSecCompositeMask(renderer, secPort); // sec | overlay (DAC parity)
|
||||||
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
|
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,10 +24,7 @@
|
|||||||
// _DAT_004ba820 = 00 00 80 3f = 1.0f (full-charge reference / upper clamp)
|
// _DAT_004ba820 = 00 00 80 3f = 1.0f (full-charge reference / upper clamp)
|
||||||
// _DAT_004ba824 = 0a d7 23 3c = 0.01f (snap-to-1.0 tolerance)
|
// _DAT_004ba824 = 0a d7 23 3c = 0.01f (snap-to-1.0 tolerance)
|
||||||
// _DAT_004ba828 = 00 00 00 00 = 0.0f (below-range clamp result)
|
// _DAT_004ba828 = 00 00 00 00 = 0.0f (below-range clamp result)
|
||||||
// _DAT_004ba830 = 00 00 00 00 00 00 f0 3f = DOUBLE 1.0 (above-range clamp result;
|
// _DAT_004ba830 = 00 00 00 00 = 0.0f (above-range clamp result)
|
||||||
// #21 CORRECTION 2026-08-13: this operand is a QWORD -- the old
|
|
||||||
// "byte-verified 0.0f" dword-read only the LOW 4 bytes (1.0's mantissa
|
|
||||||
// zeros) of the 8-byte constant. gotcha 30/31 literal-width family.)
|
|
||||||
// _DAT_004ba9a4 = 00 00 00 00 = 0.0f (dischargeTimer expiry threshold)
|
// _DAT_004ba9a4 = 00 00 00 00 = 0.0f (dischargeTimer expiry threshold)
|
||||||
// _DAT_004bac04 = 00 00 80 3f = 1.0f (outputVoltage "fully charged" test)
|
// _DAT_004bac04 = 00 00 80 3f = 1.0f (outputVoltage "fully charged" test)
|
||||||
// _DAT_004bac08 = 00 00 00 00 = 0.0f (firing dischargeTimer expiry threshold)
|
// _DAT_004bac08 = 00 00 00 00 = 0.0f (firing dischargeTimer expiry threshold)
|
||||||
@@ -501,16 +498,21 @@ void
|
|||||||
// generator + calibrated voltageScale/EC; TrackSeekVoltage integrates
|
// generator + calibrated voltageScale/EC; TrackSeekVoltage integrates
|
||||||
// the charge over the authored RechargeRate seconds).
|
// the charge over the authored RechargeRate seconds).
|
||||||
//
|
//
|
||||||
// POD-FRAME SUB-STEPPING (task #11): run the binary's own Loading
|
// POD-FRAME SUB-STEPPING (task #11, THE WEAPON-BRICK FIX): the binary's
|
||||||
|
// Loading tick assumes the pod's LOCKED 60 fps -- the charge integrates
|
||||||
|
// toward the generator's 10000V and the Loaded transition only fires
|
||||||
|
// while rechargeLevel crosses the +-0.01 snap window around seekV
|
||||||
|
// (level in [7920, 8080]; ~15 pod frames wide). Under the port's
|
||||||
|
// variable dt, one spike frame (dt >= ~0.25s) jumps the whole window:
|
||||||
|
// the level overshoots, ComputeOutputVoltage's byte-verified >1.0
|
||||||
|
// clamp (_DAT_004ba830 = 0.0) zeroes rechargeLevel, and the weapon is
|
||||||
|
// PERMANENTLY stuck Loading at level ~10000 (observed live:
|
||||||
|
// "level=9999.68 alarm=3 edge=1"). Fix: run the binary's own
|
||||||
// tick -- TrackSeekVoltage + ComputeOutputVoltage + the Loaded test --
|
// tick -- TrackSeekVoltage + ComputeOutputVoltage + the Loaded test --
|
||||||
// at the binary's locked 60 fps. Justification: the I^2R generator
|
// at the binary's own frame rate. This also keeps the I^2R generator
|
||||||
// feed integral -- TrackSeekVoltage's seekRate^2 heat evaluated over
|
// feed integral accurate (big steps evaluate seekRate at a stale level
|
||||||
// big variable-dt steps uses a stale level and OVER-heats the
|
// and OVER-heat the generators). Sub-step count is bounded; leftover
|
||||||
// generators. (The former "weapon-brick fix" rationale was FALSE --
|
// time under extreme throttling just resumes next frame.
|
||||||
// see the #21 correction in ComputeOutputVoltage: the >1.0 clamp
|
|
||||||
// stores 1.0f, so an overshot charge goes Loaded next tick at any dt.)
|
|
||||||
// Sub-step count is bounded; leftover time under extreme throttling
|
|
||||||
// just resumes next frame.
|
|
||||||
{
|
{
|
||||||
static const Scalar kPodFrame = 1.0f / 60.0f;
|
static const Scalar kPodFrame = 1.0f / 60.0f;
|
||||||
Scalar remaining = time_slice;
|
Scalar remaining = time_slice;
|
||||||
@@ -529,13 +531,32 @@ void
|
|||||||
weaponAlarm.SetLevel(2); // -> Loaded
|
weaponAlarm.SetLevel(2); // -> Loaded
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// The old OVERCHARGE RESCUE (issue #21, 2026-07-21) lived here.
|
// OVERCHARGE RESCUE (issue #21, 2026-07-21) -- a DELIBERATE
|
||||||
// REMOVED 2026-08-13: it patched an artifact of the
|
// divergence from the binary, which DEADLOCKS here: change the
|
||||||
// _DAT_004ba830 width misread (double 1.0 dword-read as float
|
// seek gear while a charge is in flight and the level can land
|
||||||
// 0.0). With the faithful clamp in ComputeOutputVoltage an
|
// ABOVE the new gear's snap window (level/seekV > 1.01) -- the
|
||||||
// overcharged weapon snaps rechargeLevel to exactly 1.0f, so
|
// byte-verified ComputeOutputVoltage clamp then zeroes
|
||||||
// the ==1.0f test above takes it to Loaded naturally -- the
|
// rechargeLevel (_DAT_004ba830 = 0.0) and the ==1.0 Loaded test
|
||||||
// binary never deadlocked here and neither do we.
|
// can never fire again while TrackSeekVoltage keeps charging
|
||||||
|
// toward the generator voltage: the weapon is PERMANENTLY
|
||||||
|
// bricked (arc dark, dot dark, never fires -- reproduced
|
||||||
|
// headless, and trivial to trigger from the clickable seek
|
||||||
|
// button; the pod's locked 60 fps + rare seek use hid it).
|
||||||
|
// The arcade's own math says "full == the gear's seek voltage"
|
||||||
|
// (discharge energy computes from seekV[rec]), so an
|
||||||
|
// overcharged weapon IS fully charged -- treat it as Loaded.
|
||||||
|
if (currentLevel > seekVoltage[seekVoltageIndex]
|
||||||
|
&& seekVoltage[seekVoltageIndex] > 0.0f)
|
||||||
|
{
|
||||||
|
rechargeLevel = 1.0f; // full (the display/damage ratio)
|
||||||
|
weaponAlarm.SetLevel(2); // -> Loaded
|
||||||
|
if (getenv("BT_SEEK_LOG"))
|
||||||
|
DEBUG_STREAM << "[seek] " << GetName()
|
||||||
|
<< " overcharge rescue: level=" << currentLevel
|
||||||
|
<< " > seekV=" << seekVoltage[seekVoltageIndex]
|
||||||
|
<< " -> Loaded" << std::endl;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -780,17 +801,7 @@ void
|
|||||||
}
|
}
|
||||||
else if (rechargeLevel > 1.0f) // _DAT_004ba820
|
else if (rechargeLevel > 1.0f) // _DAT_004ba820
|
||||||
{
|
{
|
||||||
// #21 CORRECTION (2026-08-13, adversarial re-audit, raw-byte disasm):
|
rechargeLevel = 0.0f; // _DAT_004ba830 (NB: also 0.0f)
|
||||||
// _DAT_004ba830 is a DOUBLE 1.0 (bytes 00 00 00 00 00 00 f0 3f), NOT
|
|
||||||
// float 0.0 -- the old "byte-verified 0.0f" dword-read the LOW HALF of
|
|
||||||
// the qword (1.0's mantissa zeros; gotcha 30/31 literal-width family).
|
|
||||||
// Overcharge clamps to EXACTLY 1.0f (0x3f800000 stored), so the ==1.0f
|
|
||||||
// Loaded test (_DAT_004bac04) passes next tick and the weapon fires.
|
|
||||||
// The machine NEVER bricked on overcharge; era testimony that no weapon
|
|
||||||
// ever went permanently dark triggered the re-audit. This faithful
|
|
||||||
// clamp supersedes the old "overcharge rescue" divergence (removed
|
|
||||||
// from EmitterSimulation's Loading case).
|
|
||||||
rechargeLevel = 1.0f; // _DAT_004ba830 (double 1.0, stored as float)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,6 @@
|
|||||||
#endif
|
#endif
|
||||||
#include <JOINT.hpp> // Joint, JointSubsystem (fwd shim)
|
#include <JOINT.hpp> // Joint, JointSubsystem (fwd shim)
|
||||||
#include <ROTATION.hpp> // EulerAngles, Radian (fwd shim)
|
#include <ROTATION.hpp> // EulerAngles, Radian (fwd shim)
|
||||||
#include <time.h> // clock() -- [gtrace] wall-ms stamps (cadence bench)
|
|
||||||
#include <map> // the 28Hz integrator clock (heat.cpp #119 pattern)
|
|
||||||
#if !defined(APP_HPP)
|
#if !defined(APP_HPP)
|
||||||
# include <app.hpp>
|
# include <app.hpp>
|
||||||
#endif
|
#endif
|
||||||
@@ -480,51 +478,8 @@ void
|
|||||||
if (swayAngle > maxAnimationNoise) swayAngle = maxAnimationNoise;
|
if (swayAngle > maxAnimationNoise) swayAngle = maxAnimationNoise;
|
||||||
if (swayAngle < minAnimationNoise) swayAngle = minAnimationNoise;
|
if (swayAngle < minAnimationNoise) swayAngle = minAnimationNoise;
|
||||||
|
|
||||||
// (cadence census item 2, gotcha 32) THE INTEGRATOR CLOCK: the binary
|
IntegrateEyeJoint(time_slice); // FUN_004b2ec0
|
||||||
// steps these two spring-damper integrators once per Perform = once per
|
|
||||||
// pod frame (28 Hz), and the math is PER-TICK (no-dt position step,
|
|
||||||
// damping overwrite carrying last-tick state) -- at the port's ~60 fps
|
|
||||||
// the springs stepped 2.14x as often and the cockpit bounce ran a
|
|
||||||
// foreign timescale. Step them on a 28 Hz accumulator (heat.cpp #119
|
|
||||||
// pattern: per-instance static map -- the factory size-locks the layout,
|
|
||||||
// no new members), passing the pod tick as the time_slice exactly as
|
|
||||||
// the machine's frame did; remainder carries. The integrator BODIES
|
|
||||||
// stay byte-exact (including the no-dt position step and the damping
|
|
||||||
// carry -- per-tick semantics, untouched). Impulses landing between
|
|
||||||
// ticks sit in the force accumulators until the next tick, same as a
|
|
||||||
// between-frame hit on the pod. Catch-up is capped at 28 ticks (1 s);
|
|
||||||
// a longer hitch drops the excess like the heat clock does.
|
|
||||||
// BT_GYRO_SPRING_HZ=<hz> overrides for bracketing (=0 or negative
|
|
||||||
// restores raw render-cadence stepping).
|
|
||||||
{
|
|
||||||
static Scalar s_springHz = -2.0f;
|
|
||||||
if (s_springHz < -1.0f)
|
|
||||||
{
|
|
||||||
const char *hz = getenv("BT_GYRO_SPRING_HZ");
|
|
||||||
s_springHz = (hz != 0 && *hz != '\0') ? (Scalar)atof(hz) : 28.0f;
|
|
||||||
}
|
|
||||||
if (s_springHz > 0.0f)
|
|
||||||
{
|
|
||||||
static std::map<const void *, Scalar> s_springClock;
|
|
||||||
Scalar &clock = s_springClock[this];
|
|
||||||
clock += time_slice;
|
|
||||||
const Scalar kTick = 1.0f / s_springHz;
|
|
||||||
int catchUp = 0;
|
|
||||||
while (clock >= kTick && ++catchUp <= 28)
|
|
||||||
{
|
|
||||||
clock -= kTick;
|
|
||||||
IntegrateEyeJoint(kTick); // FUN_004b2ec0, pod-tick slice
|
|
||||||
IntegrateBody(kTick); // FUN_004b30ec
|
|
||||||
}
|
|
||||||
if (catchUp > 28)
|
|
||||||
clock = 0.0f; // hitch: drop the excess
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IntegrateEyeJoint(time_slice); // FUN_004b2ec0 (render cadence)
|
|
||||||
IntegrateBody(time_slice); // FUN_004b30ec
|
IntegrateBody(time_slice); // FUN_004b30ec
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE (task #56, byte-verified): the binary Performance @004b275c ENDS here.
|
// NOTE (task #56, byte-verified): the binary Performance @004b275c ENDS here.
|
||||||
// WriteEyeJoint/WriteMechJoint are NOT called from the gyro -- they are called
|
// WriteEyeJoint/WriteMechJoint are NOT called from the gyro -- they are called
|
||||||
@@ -704,8 +659,6 @@ void
|
|||||||
+ bodyOrientation.z*bodyOrientation.z;
|
+ bodyOrientation.z*bodyOrientation.z;
|
||||||
if (m2 > 1e-9f)
|
if (m2 > 1e-9f)
|
||||||
DEBUG_STREAM << "[gtrace] g=" << (void *)this << " f=" << s_traceFrame
|
DEBUG_STREAM << "[gtrace] g=" << (void *)this << " f=" << s_traceFrame
|
||||||
<< " t=" << (long)clock() // wall ms (MSVC CLOCKS_PER_SEC=1000) -- the
|
|
||||||
// bounce-timescale bench needs real time, not frames
|
|
||||||
<< " eye=" << (float)eyePosition.x << " " << (float)eyePosition.y
|
<< " eye=" << (float)eyePosition.x << " " << (float)eyePosition.y
|
||||||
<< " " << (float)eyePosition.z
|
<< " " << (float)eyePosition.z
|
||||||
<< " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y
|
<< " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y
|
||||||
|
|||||||
+29
-399
@@ -159,7 +159,6 @@
|
|||||||
#include <matchlog.hpp> // MP match forensics (DEATH/PROJ/SPLASH/RAM event lines)
|
#include <matchlog.hpp> // MP match forensics (DEATH/PROJ/SPLASH/RAM event lines)
|
||||||
#include "btinput.hpp" // the CONTROLS.MAP + XInput binding engine
|
#include "btinput.hpp" // the CONTROLS.MAP + XInput binding engine
|
||||||
#include <string.h> // [aud-tail] strchr -- BT_FIRE_PULSE "on,off" parse (Gitea #5, instrumentation only)
|
#include <string.h> // [aud-tail] strchr -- BT_FIRE_PULSE "on,off" parse (Gitea #5, instrumentation only)
|
||||||
#include <map> // the ring 28Hz dt-accumulator registry (heat.cpp #119 pattern, census item 5)
|
|
||||||
#if !defined(PLAYER_HPP)
|
#if !defined(PLAYER_HPP)
|
||||||
# include <player.hpp> // Player::VehicleDeadMessage -- the death->respawn notification (task #52)
|
# include <player.hpp> // Player::VehicleDeadMessage -- the death->respawn notification (task #52)
|
||||||
#endif
|
#endif
|
||||||
@@ -818,28 +817,6 @@ void
|
|||||||
out = m->localOrigin.linearPosition; // safe non-garbage fallback (owner origin)
|
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).
|
// First vital damage-zone index (Mech__DamageZone::vitalDamageZone is protected; Mech has access).
|
||||||
int
|
int
|
||||||
Mech::FirstVitalZone() const
|
Mech::FirstVitalZone() const
|
||||||
@@ -1769,30 +1746,12 @@ static void
|
|||||||
// autocannon could not miss a moving target. Track the entity for the
|
// autocannon could not miss a moving target. Track the entity for the
|
||||||
// contact test; only GUIDED rounds also steer onto it.
|
// contact test; only GUIDED rounds also steer onto it.
|
||||||
extern int BTIsRegisteredMech(Entity *e);
|
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 = ((Mech *)p.target)->localOrigin.linearPosition;
|
||||||
p.targetPos.y += p.aimOffsetY;
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (p.guided)
|
if (p.guided)
|
||||||
{
|
{
|
||||||
Point3D aim = p.targetPos;
|
Point3D aim = p.targetPos;
|
||||||
@@ -1823,74 +1782,28 @@ static void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #84 THRUSTER BURN + QUADRATIC DRAG (velocity/dt integration audit
|
// #84 THRUSTER BURN: the binary Missile hosts a MissileThruster whose
|
||||||
// 2026-08-13). Binary model [T1]: Mover::PerformAndWatch @00422360
|
// authored acceleration drives the round while BurnTime remains
|
||||||
// ZEROES localAcceleration EVERY frame (`FUN_0040a7f4(+0x1dc,
|
// (MISTHRST: acceleration = (0,0,-thrusterAccel) in the missile frame,
|
||||||
// &DAT_004e0fd4)` = the T0 MOVER.cpp:672 `localAcceleration =
|
// integrated by Missile::MoveAndCollide @4bef78). The pool flew at
|
||||||
// Motion::Identity`), the MissileThruster's own Performance @004be474
|
// CONSTANT |MuzzleVelocity| -- the rack-eject speed (SRM 100, LRM 30
|
||||||
// (the standalone MissileThrusterSimulation body -- recovered by the
|
// u/s) -- which is why field missiles crawled (Oracle/Rajel, #84).
|
||||||
// 2026-08-06 re-export) re-adds (0,0,-ThrusterAccel) while burning,
|
// Authored (BTL4.RES type-15 missile models, +0x44/+0x48): SRM
|
||||||
// and Missile::MoveAndCollide @4bef78 adds the Mover quadratic drag
|
// burn 2.5s @ 600 u/s^2, LRM 10s @ 300, Streak 3s @ 300. Drag is
|
||||||
// -COD*airDensity*sign(v)*v^2 per body axis, then integrates with dt
|
// authored ~0.001 (negligible at combat ranges) -- integrate accel
|
||||||
// in SECONDS (@00421bac ApplyWorldAccelerations: v += a*dt). Net:
|
// along the current heading, un-damped, range-capped as before. [T1
|
||||||
// dv/dt = ThrusterAccel - COD*v^2 (burn; lateral vel zeroed)
|
// values / T2 integration]
|
||||||
// -- a clean variable-step ODE. There is NO myomer-style 28Hz
|
if (p.burnLeft > 0.0f && p.accel > 0.0f)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
Scalar burn_dt = (dt < p.burnLeft) ? dt : p.burnLeft;
|
Scalar burn_dt = (dt < p.burnLeft) ? dt : p.burnLeft;
|
||||||
if (burn_dt > 0.0f)
|
|
||||||
p.burnLeft -= burn_dt;
|
p.burnLeft -= burn_dt;
|
||||||
Scalar ns = p.speed + p.accel * 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
|
|
||||||
if (p.speed > 0.01f)
|
if (p.speed > 0.01f)
|
||||||
{
|
{
|
||||||
Scalar k = ns / p.speed;
|
Scalar k = ns / p.speed;
|
||||||
p.vel.x *= k; p.vel.y *= k; p.vel.z *= k;
|
p.vel.x *= k; p.vel.y *= k; p.vel.z *= k;
|
||||||
}
|
}
|
||||||
p.speed = ns;
|
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.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt;
|
||||||
p.age += dt;
|
p.age += dt;
|
||||||
@@ -1979,154 +1892,25 @@ static void
|
|||||||
// detonation being flung past the target at high speed.
|
// detonation being flung past the target at high speed.
|
||||||
Point3D hitPos = p.pos;
|
Point3D hitPos = p.pos;
|
||||||
Scalar contactD2;
|
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 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 seg2 = sx*sx + sy*sy + sz*sz;
|
||||||
|
Scalar t = 0.0f;
|
||||||
if (seg2 > 1.0e-6f)
|
if (seg2 > 1.0e-6f)
|
||||||
{
|
{
|
||||||
const Scalar wx = p.targetPos.x - prev.x,
|
const Scalar wx = p.targetPos.x - prev.x,
|
||||||
wy = p.targetPos.y - prev.y,
|
wy = p.targetPos.y - prev.y,
|
||||||
wz = p.targetPos.z - prev.z;
|
wz = p.targetPos.z - prev.z;
|
||||||
tTgt = (wx*sx + wy*sy + wz*sz) / seg2;
|
t = (wx*sx + wy*sy + wz*sz) / seg2;
|
||||||
if (tTgt < 0.0f) tTgt = 0.0f; else if (tTgt > 1.0f) tTgt = 1.0f;
|
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,
|
const Scalar cx = p.targetPos.x - hitPos.x,
|
||||||
cy = p.targetPos.y - hitPos.y,
|
cy = p.targetPos.y - hitPos.y,
|
||||||
cz = p.targetPos.z - hitPos.z;
|
cz = p.targetPos.z - hitPos.z;
|
||||||
contactD2 = cx*cx + cy*cy + cz*cz;
|
contactD2 = cx*cx + cy*cy + cz*cz;
|
||||||
}
|
}
|
||||||
// BYSTANDER SWEEP (WEAPONS_DRIFT_AUDIT lane 12, closed 2026-08-13).
|
const int contact = (contactD2 < (10.0f*10.0f));
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
|
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
|
||||||
{
|
{
|
||||||
if (getenv("BT_PROJ_LOG"))
|
if (getenv("BT_PROJ_LOG"))
|
||||||
@@ -2151,13 +1935,10 @@ static void
|
|||||||
<< "," << p.targetPos.z << ")" << std::endl;
|
<< "," << p.targetPos.z << ")" << std::endl;
|
||||||
if (p.salvoLead) // ONE Explosion per salvo (@004bcc60)
|
if (p.salvoLead) // ONE Explosion per salvo (@004bcc60)
|
||||||
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hitPos);
|
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hitPos);
|
||||||
Entity *tgt = victim;
|
Entity *tgt = p.target;
|
||||||
// Deliver to the struck mech -- normally the launcher's locked
|
// Deliver to the projectile's target mech -- the launcher set p.target
|
||||||
// target (the shooter's 0x388 slot; any peer mech in MP, task #46),
|
// from the shooter's 0x388 slot (the picked victim; any peer mech in
|
||||||
// or the BYSTANDER the sweep above found first on the flight
|
// MP, task #46). A replicant target reroutes cross-pod via Dispatch.
|
||||||
// segment (lane 12: it becomes the direct victim, exactly as if it
|
|
||||||
// had been the lock). A replicant victim reroutes cross-pod via
|
|
||||||
// Dispatch.
|
|
||||||
extern int BTIsRegisteredMech(Entity *e);
|
extern int BTIsRegisteredMech(Entity *e);
|
||||||
if (tgt != 0 && BTIsRegisteredMech(tgt) && p.damage > 0.0f)
|
if (tgt != 0 && BTIsRegisteredMech(tgt) && p.damage > 0.0f)
|
||||||
{
|
{
|
||||||
@@ -2197,22 +1978,9 @@ static void
|
|||||||
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
|
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
|
||||||
if (bursts > 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);
|
int rolled = Random(bursts) + (bursts >> 2);
|
||||||
if (rolled > bursts) rolled = bursts;
|
if (rolled > bursts) rolled = bursts;
|
||||||
|
if (rolled < 1) rolled = 1;
|
||||||
bursts = rolled;
|
bursts = rolled;
|
||||||
}
|
}
|
||||||
dmg.burstCount = bursts;
|
dmg.burstCount = bursts;
|
||||||
@@ -7836,98 +7604,6 @@ void
|
|||||||
gBTValveKey = (s_vtFrame >= 600 && s_vtFrame < 610) ? 1 : 0;
|
gBTValveKey = (s_vtFrame >= 600 && s_vtFrame < 610) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #173 decisive experiment (BT_VALVE5=N): press Condenser5's MoveValve
|
|
||||||
// exactly N times, spaced BT_VALVE5_SPACING frames (default 30) apart,
|
|
||||||
// starting at frame BT_VALVE5_AT (default 300) -- walks the detent
|
|
||||||
// cycle 1->5->50->0 (@4ae464) a CONTROLLED number of steps on the
|
|
||||||
// MYOMER loop. Dispatches the same ReceiverDataMessageOf<ControlsButton>
|
|
||||||
// press payload the pod's engineering-screen aux button delivers; the
|
|
||||||
// handler's unconditional [valve] receipt proves each landed detent.
|
|
||||||
// Self-contained (does NOT ride gBTValveKey, so it cannot collide with
|
|
||||||
// the BT_VALVE_TEST edge detector). Env-gated, off by default.
|
|
||||||
if ((Entity *)this == application->GetViewpointEntity()
|
|
||||||
&& getenv("BT_VALVE5"))
|
|
||||||
{
|
|
||||||
static int s_v5Frame = 0;
|
|
||||||
static int s_v5Sent = 0;
|
|
||||||
++s_v5Frame;
|
|
||||||
int want = atoi(getenv("BT_VALVE5"));
|
|
||||||
const char *atEnv = getenv("BT_VALVE5_AT");
|
|
||||||
const char *spEnv = getenv("BT_VALVE5_SPACING");
|
|
||||||
int startAt = (atEnv != 0) ? atoi(atEnv) : 300;
|
|
||||||
int spacing = (spEnv != 0) ? atoi(spEnv) : 30;
|
|
||||||
if (spacing < 2) spacing = 2; // edge needs a gap
|
|
||||||
if (s_v5Sent < want
|
|
||||||
&& s_v5Frame >= startAt + s_v5Sent * spacing)
|
|
||||||
{
|
|
||||||
Subsystem *condenser = 0;
|
|
||||||
for (int s = 1; s < GetSubsystemCount(); ++s)
|
|
||||||
{
|
|
||||||
Subsystem *sub = GetSubsystem(s);
|
|
||||||
if (sub != 0 && (int)sub->GetClassID() == 0xBBD // Condenser
|
|
||||||
&& sub->GetName() != 0
|
|
||||||
&& stricmp(sub->GetName(), "Condenser5") == 0)
|
|
||||||
{
|
|
||||||
condenser = sub;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (condenser != 0)
|
|
||||||
{
|
|
||||||
++s_v5Sent;
|
|
||||||
DEBUG_STREAM << "[valve-tx] BT_VALVE5 press " << s_v5Sent
|
|
||||||
<< "/" << want << " -> " << condenser->GetName()
|
|
||||||
<< " (frame " << s_v5Frame << ")\n" << std::flush;
|
|
||||||
ReceiverDataMessageOf<ControlsButton> msg(
|
|
||||||
4 /*Condenser::MoveValveMessageID*/,
|
|
||||||
sizeof(ReceiverDataMessageOf<ControlsButton>),
|
|
||||||
(ControlsButton)1 /*press*/);
|
|
||||||
condenser->Dispatch(&msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GYRO IMPULSE BENCH (cadence census item 2, night17):
|
|
||||||
// BT_GYRO_KICK=<frame>[,<amt>] injects EXACTLY ONE deterministic hit
|
|
||||||
// into the gyro's authentic damage fan-out (GyroApplyDamage ->
|
|
||||||
// ApplyDamageResponse @004b2980) at the given master-perf frame
|
|
||||||
// (default 900) -- a FIXED damageForce so the direction never rides
|
|
||||||
// the random fallback, ballistic type, amt default 25. Pure gyro
|
|
||||||
// impulse (no zone damage, no armor change): the [gtrace] receipts
|
|
||||||
// then record the eye/body bounce trajectory for the 28-vs-render
|
|
||||||
// spring-timescale comparison. Env-gated, off by default.
|
|
||||||
if ((Entity *)this == application->GetViewpointEntity()
|
|
||||||
&& getenv("BT_GYRO_KICK"))
|
|
||||||
{
|
|
||||||
static int s_gkFrame = 0;
|
|
||||||
static int s_gkDone = 0;
|
|
||||||
++s_gkFrame;
|
|
||||||
int gkAt = 900;
|
|
||||||
float gkAmt = 25.0f;
|
|
||||||
{
|
|
||||||
char spec[64];
|
|
||||||
strncpy(spec, getenv("BT_GYRO_KICK"), sizeof(spec) - 1);
|
|
||||||
spec[sizeof(spec) - 1] = 0;
|
|
||||||
char *comma = strchr(spec, ',');
|
|
||||||
if (comma != 0) { *comma = 0; gkAmt = (float)atof(comma + 1); }
|
|
||||||
if (spec[0] && atoi(spec) > 0) gkAt = atoi(spec);
|
|
||||||
}
|
|
||||||
if (!s_gkDone && s_gkFrame >= gkAt)
|
|
||||||
{
|
|
||||||
s_gkDone = 1;
|
|
||||||
Damage dmg;
|
|
||||||
dmg.damageType = Damage::BallisticDamageType;
|
|
||||||
dmg.damageAmount = gkAmt;
|
|
||||||
dmg.burstCount = 1;
|
|
||||||
dmg.damageForce = Vector3D(1.0f, 0.0f, 0.3f); // fixed => deterministic dir
|
|
||||||
dmg.impactPoint = localOrigin.linearPosition;
|
|
||||||
extern void GyroApplyDamage(Subsystem *, const Damage &);
|
|
||||||
GyroApplyDamage(gyroSubsystem, dmg);
|
|
||||||
DEBUG_STREAM << "[gyro-kick] frame=" << s_gkFrame
|
|
||||||
<< " amt=" << gkAmt << " (one-shot)\n" << std::flush;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gitea #6 scripted verify (BT_VIEWCYCLE_TEST=<frame>): pulse one
|
// Gitea #6 scripted verify (BT_VIEWCYCLE_TEST=<frame>): pulse one
|
||||||
// secondary-schematic cycle (Damage -> Critical -> Heat -> Damage) at
|
// secondary-schematic cycle (Damage -> Critical -> Heat -> Damage) at
|
||||||
// the given frame and every 300 frames after -- with
|
// the given frame and every 300 frames after -- with
|
||||||
@@ -8787,61 +8463,16 @@ void
|
|||||||
// rotation jitter keeps the change-gate open and the authored N=30
|
// rotation jitter keeps the change-gate open and the authored N=30
|
||||||
// footstep smoother fed (fed per-stride only, it took 10-20 s to warm
|
// footstep smoother fed (fed per-stride only, it took 10-20 s to warm
|
||||||
// from fill=0: the late-footsteps bug).
|
// from fill=0: the late-footsteps bug).
|
||||||
{
|
|
||||||
// #173 RING CADENCE -- DEFAULT 28 Hz (promoted 2026-08-13, user call,
|
|
||||||
// six-curve bench m173v2): push velocity-ring samples at the pod's
|
|
||||||
// BOARD cadence (28 Hz nominal [T1]) instead of once per render
|
|
||||||
// frame. The ring / mean-derivative STRUCTURE is untouched
|
|
||||||
// (byte-faithful); the only change is the sampling clock, which on
|
|
||||||
// the pod WAS the frame clock (28 Hz) and here is ~60. MEASURED
|
|
||||||
// [T2]: at ~59fps the |a| operand carries 64-66% ALIASING NOISE
|
|
||||||
// (|a| 17-26 on flat straight grass); at 28 Hz it drops to 24-32%
|
|
||||||
// and drive-heat generation HALVES at equal speed -- the port was
|
|
||||||
// over-punishing every fast-heavy chassis ~2x. With the authored
|
|
||||||
// loop-5 boost, 28 Hz sustains FULL supercharge speed indefinitely
|
|
||||||
// at a stable Tm ~1500 (leg G: 54-57 u/s held) -- the machine's
|
|
||||||
// valve lever worked, and the manual's printed Super Charged stat
|
|
||||||
// was honest. BT_MYO_RING_HZ=<hz> overrides for bracketing
|
|
||||||
// (=0 or negative restores raw render-cadence sampling).
|
|
||||||
int ringPush = 1;
|
|
||||||
Scalar ringDt = dt;
|
|
||||||
{
|
|
||||||
static Scalar s_ringHz = -2.0f;
|
|
||||||
if (s_ringHz < -1.0f)
|
|
||||||
{
|
|
||||||
const char *rh = getenv("BT_MYO_RING_HZ");
|
|
||||||
s_ringHz = (rh != 0 && *rh != '\0') ? (Scalar)atof(rh) : 28.0f;
|
|
||||||
}
|
|
||||||
if (s_ringHz > 0.0f)
|
|
||||||
{
|
|
||||||
// per-instance dt accumulator -- the heat.cpp #119 static-map
|
|
||||||
// pattern (unbounded, keyed per instance, no layout growth).
|
|
||||||
// Replaces the bench-era 16-slot fixed registry, whose silent
|
|
||||||
// overflow would have reverted a 17th mech to render-cadence
|
|
||||||
// sampling (cadence census item 5, night17).
|
|
||||||
static std::map<const void *, Scalar> s_ringClock;
|
|
||||||
Scalar &acc = s_ringClock[this];
|
|
||||||
acc += dt;
|
|
||||||
if (acc < 1.0f / s_ringHz)
|
|
||||||
ringPush = 0;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ringDt = acc;
|
|
||||||
acc = 0.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ringPush)
|
|
||||||
{
|
{
|
||||||
Scalar rdx = localOrigin.linearPosition.x - accelPrevPos.x;
|
Scalar rdx = localOrigin.linearPosition.x - accelPrevPos.x;
|
||||||
Scalar rdy = localOrigin.linearPosition.y - accelPrevPos.y;
|
Scalar rdy = localOrigin.linearPosition.y - accelPrevPos.y;
|
||||||
Scalar rdz = localOrigin.linearPosition.z - accelPrevPos.z;
|
Scalar rdz = localOrigin.linearPosition.z - accelPrevPos.z;
|
||||||
accelPrevPos = localOrigin.linearPosition;
|
accelPrevPos = localOrigin.linearPosition;
|
||||||
if (ringDt > 1.0e-4f)
|
if (dt > 1.0e-4f)
|
||||||
{
|
{
|
||||||
velRingFwd[velRingCursor] = (Scalar)sqrtf(rdx * rdx + rdz * rdz) / ringDt;
|
velRingFwd[velRingCursor] = (Scalar)sqrtf(rdx * rdx + rdz * rdz) / dt;
|
||||||
velRingVert[velRingCursor] = rdy / ringDt;
|
velRingVert[velRingCursor] = rdy / dt;
|
||||||
velRingDt[velRingCursor] = ringDt;
|
velRingDt[velRingCursor] = dt;
|
||||||
velRingCursor = (velRingCursor + 1) % 15;
|
velRingCursor = (velRingCursor + 1) % 15;
|
||||||
}
|
}
|
||||||
Scalar fwdMean = 0.0f, vertMean = 0.0f, dtMean = 0.0f;
|
Scalar fwdMean = 0.0f, vertMean = 0.0f, dtMean = 0.0f;
|
||||||
@@ -8862,7 +8493,6 @@ void
|
|||||||
accelPrevFwdMean = fwdMean;
|
accelPrevFwdMean = fwdMean;
|
||||||
accelPrevVertMean = vertMean;
|
accelPrevVertMean = vertMean;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// (AUDIO_FIDELITY F7) latch the incoming-missile report accumulated by
|
// (AUDIO_FIDELITY F7) latch the incoming-missile report accumulated by
|
||||||
// Missile::MoveAndCollide since our last frame, then re-arm. The authored
|
// Missile::MoveAndCollide since our last frame, then re-arm. The authored
|
||||||
|
|||||||
@@ -14,21 +14,11 @@
|
|||||||
//
|
//
|
||||||
// Coverage:
|
// Coverage:
|
||||||
// confident : ctor @004be7c4, dtor thunk @004be8bc, TestInstance @004be8e8,
|
// confident : ctor @004be7c4, dtor thunk @004be8bc, TestInstance @004be8e8,
|
||||||
// CreateStreamedSubsystem @004bf8ec,
|
// CreateStreamedSubsystem @004bf8ec
|
||||||
// MissileThrusterSimulation @004be474 -- the STANDALONE
|
// best-effort: MissileThrusterSimulation -- the Performance method pointer is
|
||||||
// Performance body, RECOVERED by the 2026-08-06 re-export
|
// installed by the ctor (PTR_LAB_00512b20) but its standalone body
|
||||||
// (the old "folded into Missile::MoveAndCollide, no distinct
|
// is folded into the host Missile::MoveAndCollide @004bef78 (which
|
||||||
// @ADDR survives" note here was stale). @004be474 does BOTH
|
// samples the thruster acceleration buffer at missile+0x234/+0x250).
|
||||||
// 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').
|
|
||||||
// excluded : the 0x41xxxx engine vtable slots (pure Subsystem base behaviour;
|
// excluded : the 0x41xxxx engine vtable slots (pure Subsystem base behaviour;
|
||||||
// vtable @00512bbc overrides only slot0 = destructor)
|
// vtable @00512bbc overrides only slot0 = destructor)
|
||||||
//
|
//
|
||||||
|
|||||||
+30
-148
@@ -28,14 +28,11 @@
|
|||||||
// ProjectileWeaponSimulation @004bbd04 (Performance -- FULLY RECOVERED
|
// ProjectileWeaponSimulation @004bbd04 (Performance -- FULLY RECOVERED
|
||||||
// by capstone disasm, Gitea #12 2026-07-19; see context/decomp-reference.md
|
// by capstone disasm, Gitea #12 2026-07-19; see context/decomp-reference.md
|
||||||
// s5 -- the old RivetGun-modeled body is retired).
|
// s5 -- the old RivetGun-modeled body is retired).
|
||||||
// FireWeapon @004bc104 (slot 18) -- RECOVERED by the 2026-08-06
|
// best-effort (vtable slot proven, function prologue present, BODY NOT
|
||||||
// re-export (part_013.c:16023-16125; trigger-time hitscan, implemented
|
// recovered by the decompiler -- bodies below are reconstructed from context
|
||||||
// in the #171 batch. The old "BODY NOT recovered" line here was the
|
// and clearly marked):
|
||||||
// stale banner behind the #168 mis-fix -- see the FireWeapon block).
|
// FireWeapon @004bc104 (slot 18), GetStatusFlags @004bbf88 (slot 12),
|
||||||
// best-effort (vtable slot proven, function prologue present, body still
|
// UpdateWeaponState @004bbc20 (slot 16).
|
||||||
// unrecovered -- reconstructed from context and clearly marked):
|
|
||||||
// GetStatusFlags @004bbf88 (slot 12), UpdateWeaponState @004bbc20
|
|
||||||
// (slot 16).
|
|
||||||
// excluded (belong to sibling/derived classes, NOT ProjectileWeapon):
|
// excluded (belong to sibling/derived classes, NOT ProjectileWeapon):
|
||||||
// Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
|
// Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
|
||||||
// MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
|
// MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
|
||||||
@@ -48,9 +45,6 @@
|
|||||||
// _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling)
|
// _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling)
|
||||||
// _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel)
|
// _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel)
|
||||||
// _DAT_004bb3b4 = 0000003f = 0.5f
|
// _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_004e0f74 = {0,0,0} (zero vector)
|
||||||
// DAT_004e0fd4 = {0,0,0} (zero point)
|
// DAT_004e0fd4 = {0,0,0} (zero point)
|
||||||
// 0x40400000 = 3.0f (TotalTimeToEject default)
|
// 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_004bc100 = 1.0f; // time-of-flight bias
|
||||||
static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient
|
static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient
|
||||||
static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling
|
static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling
|
||||||
// Tracer launch-speed constants -- BYTE-CONFIRMED by the 2026-08-13 weapons
|
static const Scalar _DAT_004bc678 = 1.0f; // muzzle-speed term (unresolved)
|
||||||
// audit (WEAPONS_DRIFT_AUDIT.md entry 7 / evidence log 3); the old 1.0f
|
static const Scalar _DAT_004bc67c = 1.0f; // muzzle-speed term (unresolved)
|
||||||
// "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)
|
|
||||||
|
|
||||||
// FUN_00408050 -- uniform [0,1) random (jam roll). The old `return 0.0f` stub ALWAYS
|
// 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
|
// 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
|
minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; // +0x1C8 -> 0x404
|
||||||
tracerCounter = 0; // 0x408
|
tracerCounter = 0; // 0x408
|
||||||
|
|
||||||
// launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed),
|
// 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 = Vector3D(0,0,0); // FUN_00408440(this+0x410, DAT_004e0f74)
|
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)
|
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)
|
totalTimeToEject = 3.0f; // 0x3F0 (0x40400000)
|
||||||
timeToEject = totalTimeToEject; // 0x3F4
|
timeToEject = totalTimeToEject; // 0x3F4
|
||||||
@@ -727,30 +706,6 @@ Logical
|
|||||||
return (p > UniformRandom()) ? True : False; // FUN_00408050
|
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
|
// @004bc06c -- lead-solution time-of-flight. Sample the current target
|
||||||
// position, advance it by launchVelocity, and re-run targeting. With no target
|
// position, advance it by launchVelocity, and re-run targeting. With no target
|
||||||
@@ -760,7 +715,6 @@ void
|
|||||||
Scalar
|
Scalar
|
||||||
ProjectileWeapon::ComputeTimeOfFlight()
|
ProjectileWeapon::ComputeTimeOfFlight()
|
||||||
{
|
{
|
||||||
ResolveLaunchVelocity(); // deferred @004bc3fc z term
|
|
||||||
GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420)
|
GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420)
|
||||||
leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410
|
leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410
|
||||||
leadPosition.y += launchVelocity.y;
|
leadPosition.y += launchVelocity.y;
|
||||||
@@ -1015,27 +969,10 @@ void
|
|||||||
|
|
||||||
//
|
//
|
||||||
// @004bc104 -- slot 18 FireWeapon (overrides MechWeapon's pure-virtual trap
|
// @004bc104 -- slot 18 FireWeapon (overrides MechWeapon's pure-virtual trap
|
||||||
// @004ba45c).
|
// @004ba45c). Body NOT recovered by the decompiler (prologue confirmed at
|
||||||
//
|
// 0x4bc104, immediately following the 1.0f literal @0x4bc100). Spawns the
|
||||||
// STALE-BANNER CORRECTION (2026-08-13, the #171 hunt): this comment said
|
// generic projectile/tracer. MissileLauncher overrides this again at @004bcc60
|
||||||
// "Body NOT recovered by the decompiler" -- TRUE of the 2025 export, FALSE
|
// to launch Missile entities.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Gitea #12: the view/target gate, the ammo pull and the recoil set are
|
// 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
|
// STRIPPED from this body -- they belong to the CALLER (the recovered
|
||||||
@@ -1049,9 +986,10 @@ void
|
|||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
// (ComputeTimeOfFlight @004bc06c is called INSIDE the tracer branch below,
|
// Refresh the lead solution (leadPosition / targeting) for the spawn below
|
||||||
// exactly where @004bc104 calls it -- :16091, between the muzzle resolve and
|
// (@004bc06c; the recovered sim no longer calls it -- best-effort placement
|
||||||
// the range gate. The old top-of-body placement predated the re-export.)
|
// 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
|
// THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the
|
||||||
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
|
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
|
||||||
@@ -1093,82 +1031,27 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
// WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port
|
||||||
// #171 (2026-08-13) -- THE AUTHENTIC AC: TRIGGER-TIME HITSCAN (@004bc104
|
// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
|
||||||
// [T1], the body the stale banner hid; the prior flying-damage round was
|
// Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|.
|
||||||
// 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.)
|
|
||||||
//
|
|
||||||
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
|
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;
|
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
|
||||||
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
|
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
|
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||||
+ launchVelocity.y*launchVelocity.y
|
+ launchVelocity.y*launchVelocity.y
|
||||||
+ launchVelocity.z*launchVelocity.z);
|
+ 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*/,
|
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/,
|
||||||
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
|
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
|
||||||
0 /*splash_burst*/, -1 /*muzzle_seg*/,
|
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
|
// task #61: mark this shot for REPLICATION so the peer sees the enemy's
|
||||||
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
|
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
|
||||||
@@ -1228,7 +1111,6 @@ void
|
|||||||
// plus the DAFC muzzle flash.
|
// plus the DAFC muzzle flash.
|
||||||
Point3D mz;
|
Point3D mz;
|
||||||
GetMuzzlePoint(mz); // @004b9948
|
GetMuzzlePoint(mz); // @004b9948
|
||||||
ResolveLaunchVelocity(); // deferred @004bc3fc z term
|
|
||||||
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||||
+ launchVelocity.y*launchVelocity.y
|
+ launchVelocity.y*launchVelocity.y
|
||||||
+ launchVelocity.z*launchVelocity.z);
|
+ launchVelocity.z*launchVelocity.z);
|
||||||
|
|||||||
@@ -283,14 +283,6 @@ class NotationFile;
|
|||||||
Scalar
|
Scalar
|
||||||
ComputeTimeOfFlight();
|
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 +
|
// --- cross-family isolation helpers (owning Mech subsystem roster +
|
||||||
// cockpit controls live in the mech/controls families). Declared
|
// cockpit controls live in the mech/controls families). Declared
|
||||||
// here so the recovered bodies compile; the mech family wires the
|
// here so the recovered bodies compile; the mech family wires the
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ rem ONE unified build (2026-07-21): the exe carries every layer; the GLASS
|
|||||||
rem platform env arms the desktop cockpit (clickable MFD buttons, trigger
|
rem platform env arms the desktop cockpit (clickable MFD buttons, trigger
|
||||||
rem config, bindings.txt keymap).
|
rem config, bindings.txt keymap).
|
||||||
set BT_PLATFORM=glass
|
set BT_PLATFORM=glass
|
||||||
rem Weapons-verification forensics (2026-08-13): named per-shot receipts in the
|
|
||||||
rem session log -- makes every weapon incident attributable by name in Friday's
|
|
||||||
rem logs (#165/#171 verification net). Costs log verbosity only; SENDLOGS zips
|
|
||||||
rem compress it away. Remove after the weapons-verification cycle closes.
|
|
||||||
set BT_DMG_LOG=1
|
|
||||||
if not exist "build\Release\btl4.exe" goto badpath
|
if not exist "build\Release\btl4.exe" goto badpath
|
||||||
if not exist "content\OPERATOR.EGG" goto badpath
|
if not exist "content\OPERATOR.EGG" goto badpath
|
||||||
set BT_RELAY=107.202.218.169:1500
|
set BT_RELAY=107.202.218.169:1500
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ rem ONE unified build (2026-07-21): the exe carries every layer; the GLASS
|
|||||||
rem platform env arms the desktop cockpit (clickable MFD buttons, trigger
|
rem platform env arms the desktop cockpit (clickable MFD buttons, trigger
|
||||||
rem config, bindings.txt keymap).
|
rem config, bindings.txt keymap).
|
||||||
set BT_PLATFORM=glass
|
set BT_PLATFORM=glass
|
||||||
rem Weapons-verification forensics (2026-08-13): named per-shot receipts in the
|
|
||||||
rem session log -- makes every weapon incident attributable by name in Friday's
|
|
||||||
rem logs (#165/#171 verification net). Costs log verbosity only; SENDLOGS zips
|
|
||||||
rem compress it away. Remove after the weapons-verification cycle closes.
|
|
||||||
set BT_DMG_LOG=1
|
|
||||||
if not exist "build\Release\btl4.exe" goto badpath
|
if not exist "build\Release\btl4.exe" goto badpath
|
||||||
if not exist "content\OPERATOR.EGG" goto badpath
|
if not exist "content\OPERATOR.EGG" goto badpath
|
||||||
set BT_RELAY=10.0.0.46:1500
|
set BT_RELAY=10.0.0.46:1500
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ rem ONE unified build (2026-07-21): the exe carries every layer; the GLASS
|
|||||||
rem platform env arms the desktop cockpit (clickable MFD buttons, trigger
|
rem platform env arms the desktop cockpit (clickable MFD buttons, trigger
|
||||||
rem config, bindings.txt keymap).
|
rem config, bindings.txt keymap).
|
||||||
set BT_PLATFORM=glass
|
set BT_PLATFORM=glass
|
||||||
rem Weapons-verification forensics (2026-08-13): named per-shot receipts in the
|
|
||||||
rem session log -- makes every weapon incident attributable by name in Friday's
|
|
||||||
rem logs (#165/#171 verification net). Costs log verbosity only; SENDLOGS zips
|
|
||||||
rem compress it away. Remove after the weapons-verification cycle closes.
|
|
||||||
set BT_DMG_LOG=1
|
|
||||||
if not exist "build\Release\btl4.exe" goto badpath
|
if not exist "build\Release\btl4.exe" goto badpath
|
||||||
if not exist "content\OPERATOR.EGG" goto badpath
|
if not exist "content\OPERATOR.EGG" goto badpath
|
||||||
set BT_START_INSIDE=1
|
set BT_START_INSIDE=1
|
||||||
|
|||||||
@@ -8,11 +8,6 @@ cd /d %~dp0
|
|||||||
if not exist "build\Release\btl4.exe" goto badpath
|
if not exist "build\Release\btl4.exe" goto badpath
|
||||||
if not exist "content\BTL4.RES" goto badpath
|
if not exist "content\BTL4.RES" goto badpath
|
||||||
set BT_PLATFORM=glass
|
set BT_PLATFORM=glass
|
||||||
rem Weapons-verification forensics (2026-08-13): named per-shot receipts in the
|
|
||||||
rem session log -- makes every weapon incident attributable by name in Friday's
|
|
||||||
rem logs (#165/#171 verification net). Costs log verbosity only; SENDLOGS zips
|
|
||||||
rem compress it away. Remove after the weapons-verification cycle closes.
|
|
||||||
set BT_DMG_LOG=1
|
|
||||||
set BT_STEAM_NET=1
|
set BT_STEAM_NET=1
|
||||||
rem Cockpit view + MFD gauges -- the same client flags the join bats set
|
rem Cockpit view + MFD gauges -- the same client flags the join bats set
|
||||||
rem (field report: players booted 3rd-person without them). Inherited by
|
rem (field report: players booted 3rd-person without them). Inherited by
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #173 wave 2 -- the field-feel gap (3-4s to governor vs bench 12-13s) and
|
|
||||||
# the 28Hz cadence residual, decisive runs:
|
|
||||||
# E) FIELD-SHAPED spawn: no pre-settle, weapons charging + THROTTLED
|
|
||||||
# autofire at scenery from the first frame, sprint at seek-4 --
|
|
||||||
# time-to-degradation + Condenser5/GeneratorD preheat at motion start.
|
|
||||||
# F) 28Hz ring cadence (BT_MYO_RING_HZ=28), balanced valves -- does the
|
|
||||||
# |a| gait-surge term collapse and t1000 stretch?
|
|
||||||
# G) 28Hz ring cadence + PROVEN detent 50 on Condenser5 (BT_VALVE5=2) --
|
|
||||||
# THE FLIP TEST: does boosted seek-4 land UNDER degradation 1000?
|
|
||||||
# Same rig as m173v_bench.sh (solo expert vulture, grass/day, seek-4 forced,
|
|
||||||
# full autodrive, 0.12 turn), 165s each.
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
run_one () { # run_one <tag> <secs> [extra env pairs...]
|
|
||||||
local TAG=$1 SECS=$2; shift 2
|
|
||||||
bt_expert_egg MP.EGG M173V.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=vulture/" M173V.EGG
|
|
||||||
local LOG=m173_${TAG}.log
|
|
||||||
rm -f "$LOG"
|
|
||||||
( export BT_MYO_LOG=1 BT_HEAT_LOG=1 BT_SPEC_LOG=1 BT_MYOMERS_LOG=1 BT_VALVE_LOG=1
|
|
||||||
export BT_FORCE_SEEK=3 BT_AUTODRIVE=1.0 BT_FORCE_TURN=0.12
|
|
||||||
for kv in "$@"; do export "$kv"; done
|
|
||||||
bt_launch "$LOG" M173V.EGG 0x03 )
|
|
||||||
sleep "$SECS"
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 3
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
}
|
|
||||||
|
|
||||||
run_one vul_E_field 165 BT_AUTOFIRE=1 BT_FIRE_AT_ICON=1 BT_AF_PERIOD=4 BT_FIRE_LOG=1
|
|
||||||
run_one vul_F_28bal 165 BT_MYO_RING_HZ=28
|
|
||||||
run_one vul_G_28d50 165 BT_MYO_RING_HZ=28 BT_VALVE5=2
|
|
||||||
|
|
||||||
echo "=================== m173v wave-2 RUNS DONE ==================="
|
|
||||||
for f in m173_vul_E_field.log m173_vul_F_28bal.log m173_vul_G_28d50.log; do
|
|
||||||
echo "--- $f"
|
|
||||||
grep -ac "myotemp" "$f" 2>/dev/null
|
|
||||||
grep -a "valve-tx\|valve -> detent" "$f" 2>/dev/null
|
|
||||||
done
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =========================================================================
|
|
||||||
# #173 DECISIVE EXPERIMENT -- does boosting coolant loop 5's valve sustain
|
|
||||||
# seek-4 on a Vulture, as the equilibrium solver claims, or does it still
|
|
||||||
# cook as the field reported ("loop 5 maxed and couldn't shed")?
|
|
||||||
#
|
|
||||||
# Rig: the m173 measurement pattern (night16 myo173.sh) -- solo expert
|
|
||||||
# vulture, grass/day, BT_FORCE_SEEK=3 (seek-4) + BT_AUTODRIVE=1.0 +
|
|
||||||
# BT_FORCE_TURN=0.12 (stays on-map near top speed), myomer/condenser temp
|
|
||||||
# receipts under the heat-log envs.
|
|
||||||
#
|
|
||||||
# Valve control: the NEW BT_VALVE5=N hook (mech4.cpp) presses Condenser5's
|
|
||||||
# MoveValve N times, spaced 30 frames, starting frame 300. Every press is
|
|
||||||
# receipted by the handler's unconditional "[valve] Condenser5 valve ->
|
|
||||||
# detent D" line, and BT_VALVE_LOG receipts the zero-sum share redistribute
|
|
||||||
# ("[valve] condenser#5 valveState=V flow=F (total=T)") -- the landed
|
|
||||||
# detent is PROVEN, not assumed.
|
|
||||||
#
|
|
||||||
# Three measurement runs, ~150s sustained seek-4 cruise each, identical
|
|
||||||
# otherwise:
|
|
||||||
# A) balanced (no presses) -- expected: degradation ~14s, eq ~1800
|
|
||||||
# B) BT_VALVE5=2 (detent 1->5->50) -- THE QUESTION: stays under 1000?
|
|
||||||
# C) BT_VALVE5=3 (wrap to detent 0) -- expected WORSE than A (share 0)
|
|
||||||
# Plus one visual run:
|
|
||||||
# D) idle mech, 3 presses spaced 600 frames, BT_SHOT_EVERY=90 -- captures
|
|
||||||
# the Heat MFD valve slider at each detent 1/5/50/0 (indicator audit).
|
|
||||||
# =========================================================================
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
rm -f vlv_*.png
|
|
||||||
|
|
||||||
run_one () { # run_one <tag> <secs> [extra env pairs...]
|
|
||||||
local TAG=$1 SECS=$2; shift 2
|
|
||||||
bt_expert_egg MP.EGG M173V.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=vulture/" M173V.EGG
|
|
||||||
local LOG=m173_${TAG}.log
|
|
||||||
rm -f "$LOG"
|
|
||||||
( export BT_MYO_LOG=1 BT_HEAT_LOG=1 BT_SPEC_LOG=1 BT_MYOMERS_LOG=1 BT_VALVE_LOG=1
|
|
||||||
for kv in "$@"; do export "$kv"; done
|
|
||||||
bt_launch "$LOG" M173V.EGG 0x03 )
|
|
||||||
sleep "$SECS"
|
|
||||||
bt_kill_ours
|
|
||||||
sleep 3
|
|
||||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
||||||
sleep 2
|
|
||||||
}
|
|
||||||
|
|
||||||
DRIVE="BT_FORCE_SEEK=3 BT_AUTODRIVE=1.0 BT_FORCE_TURN=0.12"
|
|
||||||
|
|
||||||
run_one vul_A_bal 165 $DRIVE
|
|
||||||
run_one vul_B_d50 165 $DRIVE BT_VALVE5=2
|
|
||||||
run_one vul_C_d0 165 $DRIVE BT_VALVE5=3
|
|
||||||
run_one vul_D_shot 100 BT_VALVE5=3 BT_VALVE5_AT=900 BT_VALVE5_SPACING=600 \
|
|
||||||
BT_SHOT_EVERY=90 BT_SHOT_PREFIX=vlv
|
|
||||||
|
|
||||||
echo "=================== m173v RUNS DONE ==================="
|
|
||||||
for f in m173_vul_A_bal.log m173_vul_B_d50.log m173_vul_C_d0.log m173_vul_D_shot.log; do
|
|
||||||
echo "--- $f"
|
|
||||||
grep -ac "myotemp" "$f" 2>/dev/null
|
|
||||||
grep -a "\[valve" "$f" 2>/dev/null | head -40
|
|
||||||
done
|
|
||||||
ls vlv_*.png 2>/dev/null | head
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# #173 valve experiment digest -- per run: valve receipts (PROOF of detent),
|
|
||||||
# time-to-degradation-1000, equilibrium T, held speed, C5/bank finals.
|
|
||||||
import re, sys, statistics as st
|
|
||||||
|
|
||||||
def digest(path):
|
|
||||||
myotemp = [] # (t, T)
|
|
||||||
myoheat = [] # (v, a, dt)
|
|
||||||
myo = [] # speedEffect samples in file order
|
|
||||||
c5 = []; bank = []
|
|
||||||
presses = [] # ([valve-tx] BT_VALVE5 press lines)
|
|
||||||
detents = [] # ([valve] CondenserN valve -> detent D lines)
|
|
||||||
redist = [] # ([valve] condenser#N valveState=V flow=F (total=T))
|
|
||||||
for ln in open(path, encoding='latin-1', errors='replace'):
|
|
||||||
m = re.search(r"\[myotemp\] t=([\d.eE+-]+) T=([\d.eE+-]+)", ln)
|
|
||||||
if m: myotemp.append((float(m.group(1)), float(m.group(2)))); continue
|
|
||||||
m = re.search(r"\[myoheat\] v=([\d.eE+-]+) a=([\d.eE+-]+) m=[\d.eE+-]+ g=[\d.eE+-]+ ratio=[\d.eE+-]+ dmgGain=[\d.eE+-]+ dt=([\d.eE+-]+)", ln)
|
|
||||||
if m: myoheat.append(tuple(map(float, m.groups()))); continue
|
|
||||||
m = re.search(r"\[myo\] Myomers.* speed=([\d.eE+-]+).* gear=(\d+)", ln)
|
|
||||||
if m: myo.append((float(m.group(1)), int(m.group(2)))); continue
|
|
||||||
m = re.search(r"\[heat-t\] (\S+)( \(bank\))? T=([\d.eE+-]+)", ln)
|
|
||||||
if m:
|
|
||||||
name, isbank, T = m.group(1), m.group(2), float(m.group(3))
|
|
||||||
if name == 'Condenser5': c5.append(T)
|
|
||||||
elif isbank or name == 'HeatSink': bank.append(T)
|
|
||||||
continue
|
|
||||||
if '[valve-tx] BT_VALVE5' in ln: presses.append(ln.strip()); continue
|
|
||||||
m = re.search(r"\[valve\] (Condenser\d+) valve -> detent (\d+)", ln)
|
|
||||||
if m: detents.append((m.group(1), int(m.group(2)))); continue
|
|
||||||
m = re.search(r"\[valve\] condenser#(\d) valveState=(\d+) flow=([\d.eE+-]+) \(total=(\d+)\)", ln)
|
|
||||||
if m: redist.append(tuple(int(x) if i != 2 else float(x) for i, x in enumerate(m.groups()))); continue
|
|
||||||
|
|
||||||
name = path.split('m173_')[-1].replace('.log', '')
|
|
||||||
print("=" * 78)
|
|
||||||
print("RUN %s" % name)
|
|
||||||
if presses:
|
|
||||||
for p in presses: print(" " + p)
|
|
||||||
if detents:
|
|
||||||
print(" detent walk: " + " -> ".join("%s:%d" % d for d in detents))
|
|
||||||
# final redistribute state (last 6 lines)
|
|
||||||
if redist:
|
|
||||||
last6 = redist[-6:]
|
|
||||||
print(" final shares: " + " ".join("#%d v=%d f=%.4f" % (n, v, f) for (n, v, f, t) in last6)
|
|
||||||
+ " (total=%d)" % last6[-1][3])
|
|
||||||
if not myotemp:
|
|
||||||
print(" NO MYOTEMP DATA"); return
|
|
||||||
t_end = myotemp[-1][0]
|
|
||||||
t1000 = next((t for t, T in myotemp if T >= 1000), None)
|
|
||||||
t2000 = next((t for t, T in myotemp if T >= 2000), None)
|
|
||||||
Tpk = max(T for _, T in myotemp)
|
|
||||||
late = [T for t, T in myotemp if t > t_end - 40.0]
|
|
||||||
k = max(2, len(myotemp) // 5)
|
|
||||||
dT = (myotemp[-1][1] - myotemp[-k][1]) / max(1e-6, (myotemp[-1][0] - myotemp[-k][0]))
|
|
||||||
# pair myoheat with myotemp by index (printed in lockstep)
|
|
||||||
n = min(len(myoheat), len(myotemp))
|
|
||||||
vlate = [myoheat[i][0] for i in range(n) if myotemp[i][0] > t_end - 40.0]
|
|
||||||
vpk = max(x[0] for x in myoheat) if myoheat else 0.0
|
|
||||||
selate = [s for s, g in myo[-20:]]
|
|
||||||
gears = set(g for s, g in myo)
|
|
||||||
print(" t1000=%s t2000=%s Tpeak=%.0f eqT(last40s)=%.0f endSlope=%+.2f/s (window %.1fs)"
|
|
||||||
% ("%.1fs" % t1000 if t1000 else "NEVER", "%.1fs" % t2000 if t2000 else "never",
|
|
||||||
Tpk, st.mean(late), dT, t_end))
|
|
||||||
print(" vPeak=%.1f vHeld(last40s)=%.1f u/s speedEffect(last~20 samples)=%.2f gear(s)=%s"
|
|
||||||
% (vpk, st.mean(vlate) if vlate else 0.0, st.mean(selate) if selate else 0.0, sorted(gears)))
|
|
||||||
print(" Condenser5 end T=%.0f bank end T=%.0f" % (c5[-1] if c5 else 0, bank[-1] if bank else 0))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
for f in sys.argv[1:]:
|
|
||||||
digest(f)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# #21 CORRECTION bench (2026-08-13): the faithful overcharge clamp.
|
|
||||||
#
|
|
||||||
# THE OLD SHAPE (21b2bfc, 2026-07-21): 2.5 min of BT_SEEKTEST abuse (dispatches
|
|
||||||
# the SAME ReceiverDataMessageOf id-0xb press the clickable seek button sends,
|
|
||||||
# every ~2s) bricked the laser pre-rescue and produced "38 overcharge rescues"
|
|
||||||
# post-rescue.
|
|
||||||
#
|
|
||||||
# THE NEW PASS (post-correction -- _DAT_004ba830 is a DOUBLE 1.0, overcharge
|
|
||||||
# clamps rechargeLevel to EXACTLY 1.0f):
|
|
||||||
# 1. gear cycling happens, including WRAPS past top (prev > next skips
|
|
||||||
# ResetFiringState -- the overcharge composition);
|
|
||||||
# 2. overcharge compositions observed ([seek] gear-switch lines where the
|
|
||||||
# in-flight level lands ABOVE the new gear's snap window, level > 1.01*seekV);
|
|
||||||
# 3. the weapon KEEPS FIRING after the last overcharge (no dark state);
|
|
||||||
# 4. gauges non-zero: [fire-probe] pct=1 (rechargeLevel, the recharge-dial
|
|
||||||
# source) seen after the last overcharge;
|
|
||||||
# 5. ZERO "rescue" prints anywhere in the log (the rescue block is gone).
|
|
||||||
set -x
|
|
||||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
||||||
cd /c/git/bt411/content || exit 1
|
|
||||||
taskkill //F //IM btl4.exe >/dev/null 2>&1; sleep 3
|
|
||||||
rm -f sk21.log
|
|
||||||
bt_expert_egg MP.EGG SK21.EGG
|
|
||||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=vulture/" SK21.EGG
|
|
||||||
|
|
||||||
( export BT_SEEKTEST=1 BT_AUTOFIRE=1 BT_FIRE_AT_ICON=1
|
|
||||||
export BT_SEEK_LOG=1 BT_FIRE_LOG=1 BT_DMG_LOG=1
|
|
||||||
bt_launch sk21.log SK21.EGG 0x03 )
|
|
||||||
sleep 160
|
|
||||||
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe >/dev/null 2>&1
|
|
||||||
|
|
||||||
python /c/git/bt411/scratchpad/night17/seek21_check.py /c/git/bt411/content/sk21.log
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# #21 correction bench checker -- see seek21_bench.sh for the PASS contract.
|
|
||||||
import io, re, sys
|
|
||||||
|
|
||||||
path = sys.argv[1] if len(sys.argv) > 1 else "/c/git/bt411/content/sk21.log"
|
|
||||||
L = io.open(path, encoding="latin-1", errors="replace").read().splitlines()
|
|
||||||
|
|
||||||
# Emitter gear-switch lines (Myomers [seek] lines lack "seekV["):
|
|
||||||
# [seek] NAME -> gear N seekV[N]=X table={...} max=M level=L genV=...
|
|
||||||
gear_re = re.compile(
|
|
||||||
r"\[seek\] (\S+) -> gear (\d+) seekV\[\d+\]=([-\d.eE+]+) .*level=([-\d.eE+]+)")
|
|
||||||
gears = [] # (line_idx, name, gear, seekV, level)
|
|
||||||
for i, l in enumerate(L):
|
|
||||||
m = gear_re.search(l)
|
|
||||||
if m:
|
|
||||||
gears.append((i, m.group(1), int(m.group(2)),
|
|
||||||
float(m.group(3)), float(m.group(4))))
|
|
||||||
|
|
||||||
names = sorted(set(g[1] for g in gears))
|
|
||||||
print("=== #21 CORRECTION BENCH (faithful overcharge clamp) ===")
|
|
||||||
print("weapon(s) under seek cycling:", names or "NONE")
|
|
||||||
|
|
||||||
ok = True
|
|
||||||
if not gears:
|
|
||||||
print("FAIL: no emitter [seek] gear lines -- rig broken (expert gate? seektest?)")
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
for name in names:
|
|
||||||
G = [g for g in gears if g[1] == name]
|
|
||||||
wraps = sum(1 for a, b in zip(G, G[1:]) if b[2] < a[2])
|
|
||||||
over = [g for g in G if g[3] > 0.0 and g[4] > 1.01 * g[3]]
|
|
||||||
fired_all = [i for i, l in enumerate(L) if ("FIRED '%s'" % name) in l]
|
|
||||||
last_over = over[-1][0] if over else None
|
|
||||||
fired_after = ([i for i in fired_all if i > last_over]
|
|
||||||
if last_over is not None else [])
|
|
||||||
# gauge source: [fire-probe] NAME ... pct=1 after the last overcharge
|
|
||||||
pct1_after = 0
|
|
||||||
if last_over is not None:
|
|
||||||
pr = re.compile(r"\[fire-probe\] %s .*pct=([-\d.eE+]+)" % re.escape(name))
|
|
||||||
for l in L[last_over:]:
|
|
||||||
m = pr.search(l)
|
|
||||||
if m and float(m.group(1)) >= 0.999:
|
|
||||||
pct1_after += 1
|
|
||||||
# tail liveness: still firing in the last 25% of the run
|
|
||||||
tail = int(len(L) * 0.75)
|
|
||||||
fired_tail = sum(1 for i in fired_all if i >= tail)
|
|
||||||
|
|
||||||
print("-- %s: gearSwitches=%d wraps=%d overchargeEvents=%d "
|
|
||||||
"FIRED total=%d afterLastOver=%d pct=1 probes after=%d tailFIRED=%d"
|
|
||||||
% (name, len(G), wraps, len(over), len(fired_all),
|
|
||||||
len(fired_after), pct1_after, fired_tail))
|
|
||||||
|
|
||||||
if wraps < 1:
|
|
||||||
ok = False; print("FAIL(%s): no wrap past top gear" % name)
|
|
||||||
if not over:
|
|
||||||
ok = False; print("FAIL(%s): no overcharge composition ever formed" % name)
|
|
||||||
if last_over is not None and not fired_after:
|
|
||||||
ok = False; print("FAIL(%s): NEVER FIRED after the last overcharge -- dark state" % name)
|
|
||||||
if last_over is not None and pct1_after < 1:
|
|
||||||
ok = False; print("FAIL(%s): recharge gauge (pct) never read full after overcharge" % name)
|
|
||||||
if fired_tail < 1:
|
|
||||||
ok = False; print("FAIL(%s): not firing in the run tail -- possible late brick" % name)
|
|
||||||
|
|
||||||
rescues = sum(1 for l in L if "rescue" in l)
|
|
||||||
print("rescue prints in log:", rescues)
|
|
||||||
if rescues != 0:
|
|
||||||
ok = False; print("FAIL: rescue print still present")
|
|
||||||
|
|
||||||
print("RESULT:", "PASS" if ok else "FAIL")
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""mksoundboard.py -- build SOUNDBOARD_BT411.zip, the distributable tester
|
|
||||||
soundboard (no Python needed on the tester's machine).
|
|
||||||
|
|
||||||
Run: python tools/mksoundboard.py (from the repo root, or anywhere)
|
|
||||||
Output: dist/SOUNDBOARD_BT411.zip containing
|
|
||||||
index.html one self-contained page (inline CSS/JS, works from file://)
|
|
||||||
AUDIO/ every soundbank wav (copies; a few ultra-low-rate zones get
|
|
||||||
their header rate rebased x2^m so browsers can decode them --
|
|
||||||
the page's playbackRate compensates exactly)
|
|
||||||
README.txt tester instructions
|
|
||||||
|
|
||||||
The page plays every sample at its GAME rate:
|
|
||||||
game rate = WAV header rate x 2^((note - 60) / 12)
|
|
||||||
via <audio>.playbackRate with preservesPitch=false (the engine RESAMPLES --
|
|
||||||
pitch and speed shift together, so pitch-preserving time-stretch would be
|
|
||||||
wrong). No fetch()/XHR of the wavs -- plain <audio src> works from file://.
|
|
||||||
|
|
||||||
The trigger-note census is shared with tools/soundboard.py (imported).
|
|
||||||
"""
|
|
||||||
import io, json, os, struct, sys, zipfile
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
sys.path.insert(0, HERE)
|
|
||||||
from soundboard import (GAME_TRIGGERS, VOICE_PRESETS, SEQ_PRIMARY_PATCHES,
|
|
||||||
AUDIO, load_zones, wav_fmt, note_shift)
|
|
||||||
|
|
||||||
OUT = os.path.normpath(os.path.join(HERE, "..", "dist", "SOUNDBOARD_BT411.zip"))
|
|
||||||
MIN_BROWSER_RATE = 3000 # Chromium refuses to decode wav below ~3 kHz
|
|
||||||
|
|
||||||
def build_entries():
|
|
||||||
zones = load_zones()
|
|
||||||
files = sorted(f for f in os.listdir(AUDIO) if f.lower().endswith(".wav"))
|
|
||||||
by_patch = {}
|
|
||||||
for b, p, notes, trig in GAME_TRIGGERS:
|
|
||||||
by_patch.setdefault((b, p), []).extend((n, trig) for n in notes)
|
|
||||||
entries = []
|
|
||||||
for f in files:
|
|
||||||
fmt = wav_fmt(os.path.join(AUDIO, f))
|
|
||||||
hz = fmt[0] if fmt else 0
|
|
||||||
z = zones.get(f)
|
|
||||||
plays, seen = [], set()
|
|
||||||
if z:
|
|
||||||
for n, trig in by_patch.get((z["bank"], z["patch"]), []):
|
|
||||||
if z["keyLo"] <= n <= z["keyHi"] and n not in seen:
|
|
||||||
seen.add(n)
|
|
||||||
plays.append(dict(n=n, s=round(note_shift(n), 6), t=trig))
|
|
||||||
if z["keyLo"] <= 60 <= z["keyHi"] and 60 not in seen:
|
|
||||||
plays.append(dict(n=60, s=1.0, t="default start (note 60)"))
|
|
||||||
# primary: sequence-driven patches lead with their first authored
|
|
||||||
# sequence note; everything else leads with the note-60 default
|
|
||||||
if (z["bank"], z["patch"]) not in SEQ_PRIMARY_PATCHES:
|
|
||||||
plays.sort(key=lambda e: (e["n"] != 60, e["n"]))
|
|
||||||
if not plays:
|
|
||||||
plays = [dict(n=60, s=1.0, t="default start (note 60)")]
|
|
||||||
# zip header rate: rebase ultra-low rates so browsers can decode
|
|
||||||
zh, m = hz, 0
|
|
||||||
while 0 < zh < MIN_BROWSER_RATE:
|
|
||||||
zh, m = zh * 2, m + 1
|
|
||||||
e = dict(f=f, hz=hz, zh=zh,
|
|
||||||
b=z["bank"] if z else 0, p=z["patch"] if z else -1,
|
|
||||||
lo=z["keyLo"] if z else 0, hi=z["keyHi"] if z else 127,
|
|
||||||
v=1 if (z and (z["bank"], z["patch"]) in VOICE_PRESETS) else 0,
|
|
||||||
plays=plays)
|
|
||||||
entries.append(e)
|
|
||||||
return entries
|
|
||||||
|
|
||||||
README = """BT411 SOUNDBOARD -- what every game sound is called
|
|
||||||
=====================================================
|
|
||||||
|
|
||||||
1. Unzip this whole folder anywhere (keep index.html next to the AUDIO
|
|
||||||
folder).
|
|
||||||
2. Double-click index.html -- it opens in your browser. Nothing to
|
|
||||||
install, nothing goes online.
|
|
||||||
3. Type in the filter box to narrow the list, click a sound to hear it.
|
|
||||||
4. Found the sound from your bug? Hit COPY on its card and PASTE the
|
|
||||||
reference string straight into your Discord report. That string is
|
|
||||||
exactly what we need -- no descriptions like "the low buzzy one"
|
|
||||||
required (though those are fun too).
|
|
||||||
|
|
||||||
The board plays each sound the way the GAME plays it -- some sounds are
|
|
||||||
sped up or slowed down in-game from how they are stored (the "x0.25"
|
|
||||||
tags). The RAW toggle lets you hear the stored file instead, in case
|
|
||||||
you are chasing a pitch/speed bug specifically. Sounds with several
|
|
||||||
small note buttons play at several different pitches in-game (warning
|
|
||||||
voice phrases, explosion layers, the weapon ready clunk/blip) -- click
|
|
||||||
each note button to hear each pitch.
|
|
||||||
|
|
||||||
Cards tagged VOICE are the cockpit warning voice. Cards tagged UNUSED
|
|
||||||
are zones no game event ever triggers (we still include them for
|
|
||||||
completeness).
|
|
||||||
|
|
||||||
Thanks for testing!
|
|
||||||
"""
|
|
||||||
|
|
||||||
def esc(s):
|
|
||||||
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
||||||
|
|
||||||
def build_html(entries):
|
|
||||||
data = json.dumps(entries, separators=(",", ":"))
|
|
||||||
n_files = len(entries)
|
|
||||||
n_shift = sum(1 for e in entries for p in e["plays"] if p["s"] != 1.0)
|
|
||||||
return """<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>BT411 Soundboard</title>
|
|
||||||
<style>
|
|
||||||
:root { color-scheme: dark; }
|
|
||||||
body { background:#101418; color:#cfd8e3; font:14px/1.4 "Segoe UI",system-ui,sans-serif; margin:0; }
|
|
||||||
header { padding:14px 18px 10px; border-bottom:1px solid #2a3542; background:#151b22; position:sticky; top:0; z-index:5; }
|
|
||||||
h1 { font-size:18px; margin:0 0 4px; color:#e8eef5; }
|
|
||||||
.sub { color:#8fa3b8; font-size:12.5px; max-width:1000px; }
|
|
||||||
.sub b { color:#c8d6e5; }
|
|
||||||
.controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:10px; }
|
|
||||||
input[type=text] { background:#0c1013; color:#dfe8f2; border:1px solid #33414f; border-radius:4px; padding:5px 8px; width:260px; }
|
|
||||||
button { background:#22303e; color:#dfe8f2; border:1px solid #3a4c5e; border-radius:4px; padding:5px 10px; cursor:pointer; }
|
|
||||||
button:hover { background:#2c3d4e; }
|
|
||||||
label.tog { color:#a9bccd; user-select:none; cursor:pointer; }
|
|
||||||
#count { color:#6f8398; font-size:12px; }
|
|
||||||
#grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:6px; padding:12px 18px 60px; }
|
|
||||||
.card { background:#161d25; border:1px solid #26313d; border-radius:6px; padding:7px 9px; }
|
|
||||||
.card.playing { border-color:#4d8edb; box-shadow:0 0 0 1px #4d8edb inset; }
|
|
||||||
.ref { font-family:Consolas,monospace; font-size:13px; color:#e4ecf4; cursor:pointer; background:none; border:none; padding:0; text-align:left; width:100%; }
|
|
||||||
.ref:hover { color:#8fc1f7; background:none; }
|
|
||||||
.meta { margin-top:4px; display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
|
|
||||||
.tag { font-size:10.5px; padding:1px 6px; border-radius:3px; background:#243240; color:#9db4c9; }
|
|
||||||
.tag.shift { background:#3d2f19; color:#e8b25a; }
|
|
||||||
.tag.voice { background:#3a2440; color:#d79be8; }
|
|
||||||
.tag.unused { background:#3a2626; color:#d99; }
|
|
||||||
.noteb { font-size:11px; padding:1px 7px; background:#1d2a37; border:1px solid #33414f; border-radius:3px; cursor:pointer; color:#bcd; }
|
|
||||||
.noteb:hover { background:#2c3d4e; }
|
|
||||||
.copy { font-size:11px; padding:1px 8px; margin-left:auto; }
|
|
||||||
#status { position:fixed; bottom:0; left:0; right:0; background:#151b22; border-top:1px solid #2a3542;
|
|
||||||
padding:6px 18px; font-family:Consolas,monospace; font-size:12.5px; color:#9db4c9; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>BT411 SOUNDBOARD <span style="color:#6f8398;font-weight:normal">— __NFILES__ samples, played the way the game plays them</span></h1>
|
|
||||||
<div class="sub">
|
|
||||||
Click a sound to hear it <b>at its in-game speed</b> (an <b>x0.25</b> tag means the game
|
|
||||||
plays that file 4x slower than it is stored — the board reproduces that). Small
|
|
||||||
<b>n##</b> buttons appear when one sample fires at several pitches in-game — click each
|
|
||||||
to hear each. <b>When you report a sound in Discord: hit COPY on its card and paste the
|
|
||||||
reference string</b> (it looks like <span style="font-family:Consolas,monospace">[2:113] Warnings01_z0 n16</span>) —
|
|
||||||
that string tells us exactly which sound you mean. VOICE = the cockpit warning voice.
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<input type="text" id="filter" placeholder="filter... (e.g. laser, warn, foot)">
|
|
||||||
<button id="stopb">STOP</button>
|
|
||||||
<button id="playall">Play All (filtered)</button>
|
|
||||||
<label class="tog"><input type="checkbox" id="raw"> RAW file rate (ignore game shift)</label>
|
|
||||||
<span id="count"></span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div id="grid"></div>
|
|
||||||
<div id="status">ready — __NSHIFT__ (sample, trigger) pairs play rate-shifted in-game</div>
|
|
||||||
<script>
|
|
||||||
"use strict";
|
|
||||||
const DATA = __DATA__;
|
|
||||||
|
|
||||||
const grid = document.getElementById("grid");
|
|
||||||
const statusEl = document.getElementById("status");
|
|
||||||
const rawEl = document.getElementById("raw");
|
|
||||||
const filterEl = document.getElementById("filter");
|
|
||||||
const audioCache = new Map();
|
|
||||||
let currentAudio = null, currentCard = null, playAllTimer = null;
|
|
||||||
|
|
||||||
function refString(e, note) {
|
|
||||||
let s = (e.p >= 0 ? "[" + e.b + ":" + e.p + "] " : "[-:-] ") + e.f.replace(/\\.wav$/i, "");
|
|
||||||
if (note !== undefined && note !== 60) s += " n" + note;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
function getAudio(e) {
|
|
||||||
let a = audioCache.get(e.f);
|
|
||||||
if (!a) {
|
|
||||||
a = new Audio("AUDIO/" + e.f); // plain element src: file://-safe, no fetch
|
|
||||||
audioCache.set(e.f, a);
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
function applyNoPreserve(a) {
|
|
||||||
a.preservesPitch = false; // the engine RESAMPLES: pitch+speed together
|
|
||||||
a.mozPreservesPitch = false;
|
|
||||||
a.webkitPreservesPitch = false;
|
|
||||||
}
|
|
||||||
function stopAll() {
|
|
||||||
if (playAllTimer) { clearTimeout(playAllTimer); playAllTimer = null; }
|
|
||||||
if (currentAudio) { currentAudio.pause(); currentAudio.currentTime = 0; }
|
|
||||||
if (currentCard) currentCard.classList.remove("playing");
|
|
||||||
currentAudio = null; currentCard = null;
|
|
||||||
}
|
|
||||||
function playEntry(e, play, card) {
|
|
||||||
stopAll();
|
|
||||||
const a = getAudio(e);
|
|
||||||
applyNoPreserve(a);
|
|
||||||
// zh = the header rate of the wav in this zip (rebased x2^m when the true
|
|
||||||
// rate is too low for browsers to decode); target = what the game outputs
|
|
||||||
const target = rawEl.checked ? e.hz : Math.round(e.hz * play.s);
|
|
||||||
let rate = target / e.zh;
|
|
||||||
const clamped = rate < 0.0625 || rate > 16;
|
|
||||||
rate = Math.min(16, Math.max(0.0625, rate));
|
|
||||||
a.playbackRate = rate;
|
|
||||||
a.currentTime = 0;
|
|
||||||
a.play().catch(err => { statusEl.textContent = "PLAY FAILED " + e.f + " -- " + err; });
|
|
||||||
currentAudio = a; currentCard = card;
|
|
||||||
if (card) card.classList.add("playing");
|
|
||||||
statusEl.textContent = "PLAYING " + refString(e, play.n) + " | " +
|
|
||||||
(rawEl.checked ? "raw " + e.hz + " Hz" : "game " + target + " Hz (x" + play.s + ")") +
|
|
||||||
" | " + play.t + (clamped ? " [browser rate clamp -- approximate]" : "");
|
|
||||||
}
|
|
||||||
function copyText(txt, btn) {
|
|
||||||
const done = () => { const old = btn.textContent; btn.textContent = "copied"; setTimeout(() => btn.textContent = old, 900); };
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
navigator.clipboard.writeText(txt).then(done, () => fallbackCopy(txt, done));
|
|
||||||
} else fallbackCopy(txt, done);
|
|
||||||
}
|
|
||||||
function fallbackCopy(txt, done) {
|
|
||||||
const ta = document.createElement("textarea");
|
|
||||||
ta.value = txt; ta.style.position = "fixed"; ta.style.opacity = "0";
|
|
||||||
document.body.appendChild(ta); ta.select();
|
|
||||||
try { document.execCommand("copy"); } catch (e) {}
|
|
||||||
document.body.removeChild(ta); done();
|
|
||||||
}
|
|
||||||
function shiftTag(s) {
|
|
||||||
return s >= 0.1 ? "x" + s.toFixed(2).replace(/0$/, "") : "x" + s.toFixed(3);
|
|
||||||
}
|
|
||||||
function build() {
|
|
||||||
const f = filterEl.value.toLowerCase();
|
|
||||||
grid.textContent = "";
|
|
||||||
let shown = 0;
|
|
||||||
for (const e of DATA) {
|
|
||||||
if (f && e.f.toLowerCase().indexOf(f) < 0) continue;
|
|
||||||
shown++;
|
|
||||||
const card = document.createElement("div"); card.className = "card";
|
|
||||||
const primary = e.plays[0];
|
|
||||||
const ref = document.createElement("button"); ref.className = "ref";
|
|
||||||
ref.textContent = refString(e, primary.n) + (primary.s !== 1 ? " " + shiftTag(primary.s) : "");
|
|
||||||
ref.title = primary.t + " | stored " + e.hz + " Hz, game " + Math.round(e.hz * primary.s) + " Hz";
|
|
||||||
ref.addEventListener("click", () => playEntry(e, primary, card));
|
|
||||||
card.appendChild(ref);
|
|
||||||
const meta = document.createElement("div"); meta.className = "meta";
|
|
||||||
if (e.v) { const t = document.createElement("span"); t.className = "tag voice"; t.textContent = "VOICE"; meta.appendChild(t); }
|
|
||||||
const seqOnly = !(e.lo <= 60 && 60 <= e.hi);
|
|
||||||
if (seqOnly && e.plays.every(p => p.n === 60)) {
|
|
||||||
const t = document.createElement("span"); t.className = "tag unused"; t.textContent = "UNUSED IN GAME"; meta.appendChild(t);
|
|
||||||
}
|
|
||||||
if (e.plays.length > 1) {
|
|
||||||
for (const p of e.plays) {
|
|
||||||
const nb = document.createElement("button"); nb.className = "noteb";
|
|
||||||
nb.textContent = "n" + p.n + " " + shiftTag(p.s);
|
|
||||||
nb.title = p.t;
|
|
||||||
nb.addEventListener("click", () => playEntry(e, p, card));
|
|
||||||
meta.appendChild(nb);
|
|
||||||
}
|
|
||||||
} else if (primary.s !== 1) {
|
|
||||||
const t = document.createElement("span"); t.className = "tag shift";
|
|
||||||
t.textContent = "game " + shiftTag(primary.s); t.title = primary.t; meta.appendChild(t);
|
|
||||||
}
|
|
||||||
const cp = document.createElement("button"); cp.className = "copy"; cp.textContent = "COPY";
|
|
||||||
cp.addEventListener("click", () => copyText(refString(e, primary.n), cp));
|
|
||||||
meta.appendChild(cp);
|
|
||||||
card.appendChild(meta);
|
|
||||||
grid.appendChild(card);
|
|
||||||
}
|
|
||||||
document.getElementById("count").textContent = shown + " shown / " + DATA.length + " total";
|
|
||||||
}
|
|
||||||
function playAll() {
|
|
||||||
stopAll();
|
|
||||||
const f = filterEl.value.toLowerCase();
|
|
||||||
const list = DATA.filter(e => !f || e.f.toLowerCase().indexOf(f) >= 0);
|
|
||||||
let i = 0;
|
|
||||||
const step = () => {
|
|
||||||
if (i >= list.length) { statusEl.textContent = "play-all done"; playAllTimer = null; return; }
|
|
||||||
const e = list[i++];
|
|
||||||
playEntry(e, e.plays[0], null);
|
|
||||||
playAllTimer = setTimeout(step, 1100);
|
|
||||||
};
|
|
||||||
step();
|
|
||||||
}
|
|
||||||
filterEl.addEventListener("input", build);
|
|
||||||
document.getElementById("stopb").addEventListener("click", () => { stopAll(); statusEl.textContent = "stopped"; });
|
|
||||||
document.getElementById("playall").addEventListener("click", playAll);
|
|
||||||
document.addEventListener("keydown", ev => { if (ev.key === "Escape") { stopAll(); statusEl.textContent = "stopped"; } });
|
|
||||||
build();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""".replace("__DATA__", data).replace("__NFILES__", str(n_files)).replace("__NSHIFT__", str(n_shift))
|
|
||||||
|
|
||||||
def rebased_wav_bytes(path, new_rate):
|
|
||||||
"""the wav with only fmt.dwSamplesPerSec/dwAvgBytesPerSec rewritten."""
|
|
||||||
fmt = wav_fmt(path)
|
|
||||||
data = bytearray(open(path, "rb").read())
|
|
||||||
_, off, block_align = fmt
|
|
||||||
struct.pack_into("<I", data, off + 4, new_rate)
|
|
||||||
struct.pack_into("<I", data, off + 8, new_rate * block_align)
|
|
||||||
return bytes(data)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
entries = build_entries()
|
|
||||||
html = build_html(entries)
|
|
||||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
|
||||||
rebased = []
|
|
||||||
with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
|
|
||||||
zf.writestr("SOUNDBOARD_BT411/index.html", html)
|
|
||||||
zf.writestr("SOUNDBOARD_BT411/README.txt", README)
|
|
||||||
for e in entries:
|
|
||||||
src = os.path.join(AUDIO, e["f"])
|
|
||||||
arc = "SOUNDBOARD_BT411/AUDIO/" + e["f"]
|
|
||||||
if e["zh"] != e["hz"]:
|
|
||||||
zf.writestr(arc, rebased_wav_bytes(src, e["zh"]))
|
|
||||||
rebased.append((e["f"], e["hz"], e["zh"]))
|
|
||||||
else:
|
|
||||||
zf.write(src, arc)
|
|
||||||
size = os.path.getsize(OUT)
|
|
||||||
n_shift = sum(1 for e in entries for p in e["plays"] if p["s"] != 1.0)
|
|
||||||
print("wrote %s (%.1f MB, %d wavs, %d shifted (sample,trigger) pairs)" % (
|
|
||||||
OUT, size / 1e6, len(entries), n_shift))
|
|
||||||
if rebased:
|
|
||||||
print("header-rebased for browser decode (playbackRate compensates):")
|
|
||||||
for f, hz, zh in rebased:
|
|
||||||
print(" %-28s %6d -> %d Hz" % (f, hz, zh))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+32
-205
@@ -1,31 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""soundboard.py -- click-to-play board for the BT soundbank (content/AUDIO/*.wav),
|
"""soundboard.py -- click-to-play board for the BT soundbank (content/AUDIO/*.wav).
|
||||||
playing every sample at its GAME-ACCURATE rate.
|
|
||||||
|
|
||||||
Run: python tools/soundboard.py (from the repo root, or anywhere)
|
Run: python tools/soundboard.py (from the repo root, or anywhere)
|
||||||
|
|
||||||
THE RATE MODEL (see docs/AUDIO_FIDELITY.md F2 + L4AUDIO.cpp:21-24):
|
- Click a button to play that sample (stdlib winsound, async -- click again to
|
||||||
game playback rate = WAV header rate x 2^((note - 60) / 12)
|
restart, click STOP to silence).
|
||||||
The port bakes each zone's authored SoundFont tuning into the extracted WAV's
|
- The label shows [bank:patch] from game/reconstructed/audiopresets.cpp so an
|
||||||
declared rate (sf2extract.py), then applies AL_PITCH = 2^((note-60)/12) for
|
identified sound maps straight back to the game's (bankID, patchID).
|
||||||
the triggering MIDI note. Sources default to note 60 (AUDSRC.cpp DEFAULT_NOTE)
|
- Type in the filter box to narrow (substring, case-insensitive).
|
||||||
so most samples play at their file rate -- but AudioControlSequences streamed
|
- "Play All" steps through the filtered list one per second (ESC/STOP to halt).
|
||||||
from BTL4.RES trigger notes far off 60 (key-splits: the note SELECTS the zone
|
|
||||||
AND transposes it). The authored (patch, note) census below was parsed out of
|
|
||||||
BTL4.RES sequence records and cross-checked against live SetupPatch logs [T1].
|
|
||||||
|
|
||||||
- Click a button: plays the sample at its PRIMARY game rate (header-rewritten
|
|
||||||
temp copy; winsound resamples like the engine does -- pitch and speed shift
|
|
||||||
together).
|
|
||||||
- Small per-note buttons appear when one sample fires at several pitches
|
|
||||||
(explosion splits, the Warnings01 voice zones, weapon ready clunk/blip).
|
|
||||||
- "Raw file rate" toggle: play the untouched WAV for comparison.
|
|
||||||
- The label shows [bank:patch], the shift (e.g. x0.25), and V for the recorded
|
|
||||||
VOICE zones (Warnings01/AllWarning -- the project's Yip recordings).
|
|
||||||
- Filter box narrows (substring, case-insensitive); "Play All" steps through
|
|
||||||
the filtered list; ESC/STOP halts.
|
|
||||||
"""
|
"""
|
||||||
import os, re, struct, sys, tempfile, threading, time
|
import os, re, sys, threading, time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
import winsound
|
import winsound
|
||||||
@@ -33,118 +18,28 @@ import winsound
|
|||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
AUDIO = os.path.normpath(os.path.join(HERE, "..", "content", "AUDIO"))
|
AUDIO = os.path.normpath(os.path.join(HERE, "..", "content", "AUDIO"))
|
||||||
PRESETS_CPP = os.path.normpath(os.path.join(HERE, "..", "game", "reconstructed", "audiopresets.cpp"))
|
PRESETS_CPP = os.path.normpath(os.path.join(HERE, "..", "game", "reconstructed", "audiopresets.cpp"))
|
||||||
TMPDIR = os.path.join(tempfile.gettempdir(), "bt411_soundboard")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def load_mapping():
|
||||||
# The authored trigger-note census [T1]: every AudioControlSequence in
|
"""file -> (bank, patch) from audiopresets.cpp (allPresets[b][p] ... file="X.wav")."""
|
||||||
# BTL4.RES, parsed from the stream records (scratchpad seqparse.py,
|
mapping = {}
|
||||||
# 2026-08-13) and resolved through its mixer chain to the target patch.
|
|
||||||
# Everything NOT listed here is only ever started at DEFAULT_NOTE 60
|
|
||||||
# (AUDSRC.cpp:18), i.e. at its file rate. (bank, patch) are 1-based bank
|
|
||||||
# as logged by SetupPatch. Dynamic pitch-cent modulations (doppler, the
|
|
||||||
# torso-twist pitch scale) are runtime effects and are NOT part of the
|
|
||||||
# base rate.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
GAME_TRIGGERS = [
|
|
||||||
# (bank, patch, [notes in authored order], "trigger")
|
|
||||||
(2, 113, [16, 19, 23], "coolant-leak voice loop (ReportLeak)"),
|
|
||||||
(2, 113, [16, 33, 36], "ammo cook-off countdown voice (FireCountdownStarted)"),
|
|
||||||
(2, 113, [29, 16, 26], "generator-out voice phrase (GeneratorState)"),
|
|
||||||
(2, 113, [29, 16, 40], "SimulationState warning voice phrase"),
|
|
||||||
(2, 115, [30, 18], "death phrase A (random pick at death)"),
|
|
||||||
(2, 115, [49, 43], "death phrase B (random pick at death)"),
|
|
||||||
(2, 115, [95], "death phrase C (random pick at death)"),
|
|
||||||
(2, 83, [30, 48, 46, 96, 44, 63, 66], "mech-explosion sequence (brnext.scp)"),
|
|
||||||
(2, 97, [45, 38], "mech fire-loop sequence (brnext.scp)"),
|
|
||||||
(2, 64, [36, 70], "subsystem-destroyed interior (SimulationState)"),
|
|
||||||
(2, 119, [57, 69, 46, 24], "big explosion sequence (bigexp.scp)"),
|
|
||||||
(2, 120, [57, 69, 46, 24], "big explosion sequence LOD2 (bigexp.scp)"),
|
|
||||||
(2, 119, [69, 51], "medium explosion sequence (medexp.scp)"),
|
|
||||||
(2, 120, [69, 51], "medium explosion sequence LOD2 (medexp.scp)"),
|
|
||||||
(1, 74, [58], "incoming-missile lock beeper (IncomingLock)"),
|
|
||||||
(1, 83, [51], "weapon-configure ticker (ConfigureActivePress)"),
|
|
||||||
(1, 36, [36, 84], "missile misfire (WeaponState)"),
|
|
||||||
(1, 40, [36, 84], "missile loaded/ready (WeaponState)"),
|
|
||||||
(1, 41, [36, 84], "autocannon misfire (WeaponState)"),
|
|
||||||
(1, 56, [36, 84], "laser loaded/ready (WeaponState)"),
|
|
||||||
(1, 70, [36, 84], "autocannon jam (WeaponState)"),
|
|
||||||
(1, 71, [36, 84], "missile jam (WeaponState)"),
|
|
||||||
(1, 75, [36, 84], "projectile loaded/ready (WeaponState)"),
|
|
||||||
]
|
|
||||||
|
|
||||||
VOICE_PRESETS = {(2, 113), (2, 123)} # Warnings01, AllWarning -- Yip's recorded voice
|
|
||||||
|
|
||||||
# Patches the game drives ONLY through their sequences (live SetupPatch census:
|
|
||||||
# no note-60 non-ZONESKIP starts) -- their PRIMARY board rate is the first
|
|
||||||
# authored sequence note, not the note-60 default. ProgramButton01 (1,83) is
|
|
||||||
# deliberately absent: button presses start it at 60 (live-trace evidence);
|
|
||||||
# the configure-ticker 51 stays a secondary note button.
|
|
||||||
SEQ_PRIMARY_PATCHES = {
|
|
||||||
(1, 36), (1, 40), (1, 41), (1, 56), (1, 70), (1, 71), (1, 75), # loaded/jam/misfire
|
|
||||||
(1, 74), # lock beeper (F7: sequence is the only path)
|
|
||||||
(2, 64), (2, 83), (2, 97), (2, 113), (2, 115), (2, 119), (2, 120),
|
|
||||||
}
|
|
||||||
|
|
||||||
def note_shift(note):
|
|
||||||
return 2.0 ** ((note - 60) / 12.0)
|
|
||||||
|
|
||||||
def load_zones():
|
|
||||||
"""file -> dict(bank, patch, keyLo, keyHi) from audiopresets.cpp Z() lines."""
|
|
||||||
zones = {}
|
|
||||||
try:
|
try:
|
||||||
text = open(PRESETS_CPP, encoding="utf-8", errors="replace").read()
|
text = open(PRESETS_CPP, encoding="utf-8", errors="replace").read()
|
||||||
for m in re.finditer(r'Z\((\d+),(\d+),"([^"]+)",(-?\d+),(-?\d+),', text):
|
# blocks look like: allPresets[0][12].samples[0]; ... s.file = "LaserAFire01.wav";
|
||||||
zones[m.group(3)] = dict(bank=int(m.group(1)) + 1, patch=int(m.group(2)),
|
for m in re.finditer(r'allPresets\[(\d+)\]\[(\d+)\]\.samples\[0\];.*?s\.file = "([^"]+)";',
|
||||||
keyLo=int(m.group(4)), keyHi=int(m.group(5)))
|
text, re.S):
|
||||||
|
bank, patch, fname = int(m.group(1)) + 1, int(m.group(2)), m.group(3)
|
||||||
|
mapping[fname] = (bank, patch)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
return zones
|
return mapping
|
||||||
|
|
||||||
def wav_fmt(path):
|
|
||||||
"""(rate, fmt_chunk_file_offset, block_align) from the RIFF header."""
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
head = f.read(12)
|
|
||||||
if head[:4] != b"RIFF" or head[8:12] != b"WAVE":
|
|
||||||
return None
|
|
||||||
while True:
|
|
||||||
ch = f.read(8)
|
|
||||||
if len(ch) < 8:
|
|
||||||
return None
|
|
||||||
cid, sz = ch[:4], struct.unpack("<I", ch[4:])[0]
|
|
||||||
if cid == b"fmt ":
|
|
||||||
off = f.tell()
|
|
||||||
data = f.read(sz)
|
|
||||||
rate, = struct.unpack_from("<I", data, 4)
|
|
||||||
block_align, = struct.unpack_from("<H", data, 12)
|
|
||||||
return rate, off, block_align
|
|
||||||
f.seek(sz + (sz & 1), 1)
|
|
||||||
|
|
||||||
def make_rate_copy(src, dst, new_rate):
|
|
||||||
"""byte-exact copy of src with only the fmt chunk's dwSamplesPerSec and
|
|
||||||
dwAvgBytesPerSec rewritten to new_rate."""
|
|
||||||
fmt = wav_fmt(src)
|
|
||||||
if fmt is None:
|
|
||||||
return False
|
|
||||||
_, off, block_align = fmt
|
|
||||||
data = bytearray(open(src, "rb").read())
|
|
||||||
struct.pack_into("<I", data, off + 4, new_rate) # dwSamplesPerSec
|
|
||||||
struct.pack_into("<I", data, off + 8, new_rate * block_align) # dwAvgBytesPerSec
|
|
||||||
open(dst, "wb").write(data)
|
|
||||||
return True
|
|
||||||
|
|
||||||
class SoundBoard(tk.Tk):
|
class SoundBoard(tk.Tk):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.title("BT411 Soundboard (game rates) -- " + AUDIO)
|
self.title("BT411 Soundboard -- " + AUDIO)
|
||||||
self.geometry("1400x800")
|
self.geometry("1150x760")
|
||||||
os.makedirs(TMPDIR, exist_ok=True)
|
self.mapping = load_mapping()
|
||||||
self.zones = load_zones()
|
|
||||||
self.files = sorted(f for f in os.listdir(AUDIO) if f.lower().endswith(".wav"))
|
self.files = sorted(f for f in os.listdir(AUDIO) if f.lower().endswith(".wav"))
|
||||||
self.rates = {}
|
|
||||||
for f in self.files:
|
|
||||||
fmt = wav_fmt(os.path.join(AUDIO, f))
|
|
||||||
self.rates[f] = fmt[0] if fmt else 0
|
|
||||||
self.plays = self.build_play_table()
|
|
||||||
self.playing_all = False
|
self.playing_all = False
|
||||||
|
|
||||||
top = ttk.Frame(self); top.pack(fill="x", padx=6, pady=4)
|
top = ttk.Frame(self); top.pack(fill="x", padx=6, pady=4)
|
||||||
@@ -155,9 +50,6 @@ class SoundBoard(tk.Tk):
|
|||||||
self.filter_var.trace_add("write", lambda *a: self.rebuild())
|
self.filter_var.trace_add("write", lambda *a: self.rebuild())
|
||||||
ttk.Button(top, text="STOP", command=self.stop).pack(side="left", padx=8)
|
ttk.Button(top, text="STOP", command=self.stop).pack(side="left", padx=8)
|
||||||
ttk.Button(top, text="Play All (filtered)", command=self.play_all).pack(side="left")
|
ttk.Button(top, text="Play All (filtered)", command=self.play_all).pack(side="left")
|
||||||
self.raw_var = tk.BooleanVar(value=False)
|
|
||||||
ttk.Checkbutton(top, text="Raw file rate (ignore game shift)",
|
|
||||||
variable=self.raw_var).pack(side="left", padx=10)
|
|
||||||
self.status = tk.StringVar(value=f"{len(self.files)} samples loaded")
|
self.status = tk.StringVar(value=f"{len(self.files)} samples loaded")
|
||||||
ttk.Label(top, textvariable=self.status, foreground="#06c").pack(side="left", padx=12)
|
ttk.Label(top, textvariable=self.status, foreground="#06c").pack(side="left", padx=12)
|
||||||
|
|
||||||
@@ -177,94 +69,30 @@ class SoundBoard(tk.Tk):
|
|||||||
|
|
||||||
self.rebuild()
|
self.rebuild()
|
||||||
|
|
||||||
def build_play_table(self):
|
|
||||||
"""file -> list of (note, shift, trigger); primary first.
|
|
||||||
|
|
||||||
A zone whose key range contains 60 plays at note 60 (= file rate)
|
|
||||||
from every plain watcher Start; sequence notes are added on top.
|
|
||||||
A zone whose range EXCLUDES 60 is reachable ONLY via its sequence
|
|
||||||
notes -- its file-rate rendering never occurs in game."""
|
|
||||||
by_patch = {}
|
|
||||||
for b, p, notes, trig in GAME_TRIGGERS:
|
|
||||||
by_patch.setdefault((b, p), []).extend((n, trig) for n in notes)
|
|
||||||
plays = {}
|
|
||||||
for f in self.files:
|
|
||||||
z = self.zones.get(f)
|
|
||||||
entries = []
|
|
||||||
if z:
|
|
||||||
seen = set()
|
|
||||||
for n, trig in by_patch.get((z["bank"], z["patch"]), []):
|
|
||||||
if z["keyLo"] <= n <= z["keyHi"] and n not in seen:
|
|
||||||
seen.add(n)
|
|
||||||
entries.append((n, note_shift(n), trig))
|
|
||||||
if z["keyLo"] <= 60 <= z["keyHi"] and 60 not in seen:
|
|
||||||
# default-60 start reaches this zone at file rate
|
|
||||||
entries.append((60, 1.0, "default start (note 60)"))
|
|
||||||
if not entries:
|
|
||||||
entries = [(60, 1.0, "default start (note 60)")]
|
|
||||||
# primary: sequence-driven patches lead with their first authored
|
|
||||||
# sequence note; everything else leads with the note-60 default
|
|
||||||
if not (z and (z["bank"], z["patch"]) in SEQ_PRIMARY_PATCHES):
|
|
||||||
entries.sort(key=lambda e: (e[0] != 60, e[0]))
|
|
||||||
plays[f] = entries
|
|
||||||
return plays
|
|
||||||
|
|
||||||
def label_for(self, f):
|
def label_for(self, f):
|
||||||
z = self.zones.get(f)
|
bp = self.mapping.get(f)
|
||||||
tag = f"[{z['bank']}:{z['patch']}] " if z else "[-:-] "
|
tag = f"[{bp[0]}:{bp[1]}] " if bp else "[-:-] "
|
||||||
if z and (z["bank"], z["patch"]) in VOICE_PRESETS:
|
return tag + f[:-4]
|
||||||
tag += "V "
|
|
||||||
primary = self.plays[f][0]
|
|
||||||
shift_txt = "" if primary[1] == 1.0 else f" x{primary[1]:.2f}" if primary[1] >= 0.1 else f" x{primary[1]:.3f}"
|
|
||||||
return tag + f[:-4] + shift_txt
|
|
||||||
|
|
||||||
def rebuild(self):
|
def rebuild(self):
|
||||||
for w in self.grid_frame.winfo_children():
|
for w in self.grid_frame.winfo_children():
|
||||||
w.destroy()
|
w.destroy()
|
||||||
flt = self.filter_var.get().lower()
|
flt = self.filter_var.get().lower()
|
||||||
shown = [f for f in self.files if flt in f.lower()]
|
shown = [f for f in self.files if flt in f.lower()]
|
||||||
cols = 3
|
cols = 4
|
||||||
for i, f in enumerate(shown):
|
for i, f in enumerate(shown):
|
||||||
cell = ttk.Frame(self.grid_frame)
|
b = ttk.Button(self.grid_frame, text=self.label_for(f), width=38,
|
||||||
cell.grid(row=i // cols, column=i % cols, padx=2, pady=1, sticky="w")
|
|
||||||
b = ttk.Button(cell, text=self.label_for(f), width=44,
|
|
||||||
command=lambda ff=f: self.play(ff))
|
command=lambda ff=f: self.play(ff))
|
||||||
b.pack(side="left")
|
b.grid(row=i // cols, column=i % cols, padx=2, pady=1, sticky="w")
|
||||||
notes = self.plays[f]
|
|
||||||
if len(notes) > 1:
|
|
||||||
for n, sh, trig in notes:
|
|
||||||
txt = f"n{n}"
|
|
||||||
nb = ttk.Button(cell, text=txt, width=4,
|
|
||||||
command=lambda ff=f, nn=n: self.play(ff, nn))
|
|
||||||
nb.pack(side="left")
|
|
||||||
self.status.set(f"{len(shown)} shown / {len(self.files)} total")
|
self.status.set(f"{len(shown)} shown / {len(self.files)} total")
|
||||||
self.canvas.yview_moveto(0)
|
self.canvas.yview_moveto(0)
|
||||||
|
|
||||||
def path_for(self, f, note):
|
def play(self, f):
|
||||||
"""the file to hand winsound: raw, or a header-rewritten game-rate copy."""
|
|
||||||
if self.raw_var.get() or note == 60:
|
|
||||||
return os.path.join(AUDIO, f)
|
|
||||||
game_rate = int(round(self.rates[f] * note_shift(note)))
|
|
||||||
dst = os.path.join(TMPDIR, f"{f[:-4]}@n{note}.wav")
|
|
||||||
if not os.path.exists(dst):
|
|
||||||
if not make_rate_copy(os.path.join(AUDIO, f), dst, game_rate):
|
|
||||||
return os.path.join(AUDIO, f)
|
|
||||||
return dst
|
|
||||||
|
|
||||||
def play(self, f, note=None):
|
|
||||||
self.playing_all = False
|
self.playing_all = False
|
||||||
entries = self.plays[f]
|
path = os.path.join(AUDIO, f)
|
||||||
if note is None:
|
|
||||||
note, shift, trig = entries[0]
|
|
||||||
else:
|
|
||||||
shift, trig = next((s, t) for n, s, t in entries if n == note)
|
|
||||||
path = self.path_for(f, note)
|
|
||||||
winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
||||||
z = self.zones.get(f)
|
bp = self.mapping.get(f)
|
||||||
bp = f"bank {z['bank']}, patch {z['patch']}" if z else "unmapped"
|
self.status.set(f"PLAYING {f}" + (f" (bank {bp[0]}, patch {bp[1]})" if bp else ""))
|
||||||
mode = "RAW" if self.raw_var.get() else f"note {note} x{shift:.4g}"
|
|
||||||
game_rate = int(round(self.rates[f] * (1.0 if self.raw_var.get() else note_shift(note))))
|
|
||||||
self.status.set(f"PLAYING {f} ({bp}; {mode}; {game_rate} Hz; {trig})")
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.playing_all = False
|
self.playing_all = False
|
||||||
@@ -279,10 +107,9 @@ class SoundBoard(tk.Tk):
|
|||||||
for f in shown:
|
for f in shown:
|
||||||
if not self.playing_all:
|
if not self.playing_all:
|
||||||
return
|
return
|
||||||
note = self.plays[f][0][0]
|
|
||||||
path = self.path_for(f, note)
|
|
||||||
self.after(0, lambda ff=f: self.status.set("PLAYING " + ff))
|
self.after(0, lambda ff=f: self.status.set("PLAYING " + ff))
|
||||||
winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
winsound.PlaySound(os.path.join(AUDIO, f),
|
||||||
|
winsound.SND_FILENAME | winsound.SND_ASYNC)
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
self.playing_all = False
|
self.playing_all = False
|
||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|||||||
Reference in New Issue
Block a user