Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32e5e8c6a6 | ||
|
|
3013cf86c5 | ||
|
|
65b30d4633 | ||
|
|
ad5fc7b524 | ||
|
|
87e45547c0 | ||
|
|
f450f9cce0 | ||
|
|
a0a365462b | ||
|
|
bca93e181e | ||
|
|
7b120531ea | ||
|
|
df188e737f | ||
|
|
b00fa39af7 | ||
|
|
929d6267c2 | ||
|
|
2949e190fe | ||
|
|
a6cc8d63d2 | ||
|
|
f9546d254f | ||
|
|
b355844847 | ||
|
|
567ffacaae | ||
|
|
f811c64590 | ||
|
|
2c6dc001e3 | ||
|
|
ca9e5ff5c0 | ||
|
|
0f5c87dc84 | ||
|
|
25ed1ca627 | ||
|
|
47826018cf | ||
|
|
ef90b6af8c | ||
|
|
8e6697312f | ||
|
|
e4546923f8 | ||
|
|
bb4094fe7e | ||
|
|
5272a3db64 | ||
|
|
510f4f4c1b | ||
|
|
b94636b607 | ||
|
|
6517ebe505 | ||
|
|
bbb3b4ec82 | ||
|
|
80037a7c78 | ||
|
|
3c1decdcc6 |
+10
@@ -61,3 +61,13 @@ content/*.log
|
||||
content/matchlog_*.txt
|
||||
content/btl4.exe
|
||||
content/*.EGG
|
||||
|
||||
# scratchpad: session working files -- NEVER bulk-add (the 2026-08-13 6a744ef
|
||||
# incident: a stray `git add scratchpad` pushed a session handoff carrying the
|
||||
# gitea credential). Bench .sh scripts are added INDIVIDUALLY by full path.
|
||||
scratchpad/**/*.out
|
||||
scratchpad/**/*.exe
|
||||
scratchpad/**/*.dll
|
||||
scratchpad/**/SESSION_HANDOFF*.md
|
||||
scratchpad/**/AGENT_FINDINGS*.md
|
||||
scratchpad/**/message*.txt
|
||||
|
||||
@@ -199,14 +199,23 @@ Fixed-torso mechs (the BLH: `TorsoHorizontalEnabled=0` — no jointtorso in the
|
||||
boresight dead-ahead. Once locked, `Emitter::FireWeapon` converges with NO aim/arc test
|
||||
(part_013.c:7758). `MechWeapon::UpdateTargetState` (`FUN_004b9bdc` [T1]): `targetWithinRange =
|
||||
dist < (1 − hostZoneDamage) × weaponRange`. The 0x388 WRITER is in the same un-exported gap.
|
||||
⚠ `weaponRange` is **fire-control data only** (#168 audit [T1]): projectile FLIGHT is never
|
||||
range-capped — a plain shell (AFC) lives exactly 5.0 s (@4bddec) and a Missile lives
|
||||
BurnTime+10 s with a y<−1 kill-plane (@4bef78); FireWeapon @4bcc60 has no range gate, so an
|
||||
AFC50 (authored range 750) landing hits at 2000+ u is AUTHENTIC arcade behavior. Constants +
|
||||
the whole flight/expiry chain: [[decomp-reference]] §5 "Projectile/Missile flight & expiry".
|
||||
The same audit killed the pool's stale-point auto-hit (unguided contact was tested against
|
||||
the fire-time pick, so a locked AC shell could not miss a moving target — fixed 2026-08-11:
|
||||
the contact test now tracks the target's live position on every round kind).
|
||||
⚠ ~~`weaponRange` is fire-control data only~~ **RETRACTED 2026-08-13 (the #171 hunt): that
|
||||
was MISSILE-only truth mis-generalized to ballistics.** The AC's own FireWeapon @004bc104
|
||||
(recovered by the 2026-08-06 re-export; #168 worked from a stale "not recovered" banner and
|
||||
generalized from the missile launcher) is TRIGGER-TIME HITSCAN: batched rounds, dist vs LIVE
|
||||
effectiveRange at fire time (the same gate energy uses), ONE single-panel damage message to
|
||||
the locked target, nothing spawned (the shell class 0xBD1 is dead code; the visible round is
|
||||
the 0xBCD renderer tracer). So: an AFC50 landing hits beyond its authored range is a PORT
|
||||
DEVIATION, not arcade behavior; "a locked AC could not miss a mover" IS the arcade behavior
|
||||
(#168's "stale-point auto-hit" kill removed an authentic design while fixing its stale-point
|
||||
detail). Missiles: genuinely flying + seeker-led (the machine leads for the player), 4.0u
|
||||
proximity fuze, swept contact, salvo randomization — the #168 flight direction stands THERE.
|
||||
Gauss CORRECTED (2026-08-13 audit): the FIELDED Gauss record is CLASS 0xBCD (ProjectileWeapon)
|
||||
— full AC hitscan WITH damage; port agrees. GaussRifle 0xBCE = dead code both sides (zero
|
||||
shipped records). The earlier no-damage claim was class-level truth mis-applied to the field.
|
||||
#112 (AFC multi-zone shotgun) is the port's per-round scatter vs the binary's one batched
|
||||
message. Full chain + evidence: [[decomp-reference]] §5 (corrected); port fix pending the
|
||||
#171 group decision (Oracle's HOLD).
|
||||
⚠ **`hostZoneDamage` is `Subsystem::damageZone->damageLevel` (weapon `@0xE0 → +0x158`), NOT
|
||||
heatLoad** — the port originally computed `effectiveRange = (1 − heatLoad) × weaponRange`
|
||||
(mechweap.cpp), the SAME `@0xE0`-DamageZone-vs-heat misattribution corrected in
|
||||
|
||||
+85
-22
@@ -253,20 +253,69 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
||||
@004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30
|
||||
(generator client capacity @0x1e4/count @0x1e8).
|
||||
|
||||
### Projectile/Missile flight & expiry (#168 audit, 2026-08-11) [T1]
|
||||
- **No range cap exists anywhere in the flight chain.** WeaponRange feeds fire-control only
|
||||
(effectiveRange @0x328 = (1−hostZoneDamage)×WeaponRange — targeting/reticle "in range" call).
|
||||
FireWeapon @4bcc60 reads no +0x328: the trigger fires at any target distance.
|
||||
- **Plain Projectile (AFC shell) live integrator @4bddec** (master); expiry: age since
|
||||
### Projectile/Missile flight & expiry (#168 audit 2026-08-11; ⚠ CORRECTED by the #171 hunt 2026-08-13) [T1]
|
||||
- ⚠ **#168's "no range cap / WeaponRange is fire-control only" was MISSILE-ONLY truth,
|
||||
mis-generalized.** The AC's own FireWeapon @004bc104 (recovered by the 2026-08-06 re-export;
|
||||
the old projweap.cpp "body NOT recovered" banner was stale) IS range-gated AND hitscan:
|
||||
batch counter +0x408, every TracerInterval-th (+0x438) round → dist = |aimPoint(+0x37c) −
|
||||
muzzle| vs LIVE effectiveRange +0x328 (dynamic: WeaponRange × (K − owningZone structure),
|
||||
recomputed @004b9bdc; K @_DAT_004b9c98 presumed 1.0 [T4-unread]) → ONE single-panel Damage
|
||||
(batchedCount × DamageAmount) to the LOCKED target *(mech+0x388) via @004b9728 AT FIRE TIME.
|
||||
Out of range = batch silently discarded (tracer still renders). The visible AC round is the
|
||||
0xBCD renderer TRACER (tracerModel+0x41C, TOF @004bc06c) — cosmetic. MissileLauncher
|
||||
@004bcc60 (no fire-time damage, no range read) is the genuinely-flying family.
|
||||
**GAUSS CORRECTED (weapons audit 2026-08-13): the FIELDED Gauss is a 0xBCD ProjectileWeapon
|
||||
record** (BTL4.RES @0xf16e6, bytes cd0b verified raw — 20 pts Ballistic, 900 range, 8s cycle,
|
||||
16 rounds, yellow pip) — it runs the FULL AC hitscan chain WITH damage; port and binary agree.
|
||||
The GaussRifle class 0xBCE (@004bdca4 charge-dump-only FireWeapon) + shell 0xBD1 are real code
|
||||
with ZERO shipped content records — dead on both sides. (The earlier "port Gauss damage is an
|
||||
invention" claim was wrong at the fielded level and is retracted.)
|
||||
**Every FireWeapon (all classes) requires a LOCKED target**: both sims gate the trigger on
|
||||
*(mech+0x388) != 0 — no lock, no fire [T1].
|
||||
- **INCOMING-MISSILE ALARM (#83, 2026-08-13) [T1]:** the Missile CTOR tail (@004bf5b4,
|
||||
part_013.c:18426-18431) registers the missile into its target's inbound SOCKET LIST — target
|
||||
= this+0x34c, class-checked as Mech (@0041a1a4 vs 0x50bdb4), Add on target+0x418 (list ctor'd
|
||||
in the mech reset at int-index 0x106, right after the 0x3FC/0x400 attribute init 0/FLT_MAX =
|
||||
DAT_005209d0). The TARGET'S OWN tick (@004a9b5c, file=bt/mech4.cpp, part_013.c:3218-3232)
|
||||
reads the FIRST list member: empty → IncomingLock(+0x3FC)=0 (distance left stale); present →
|
||||
lock=1, DistanceToMissile(+0x400) = |missilePos−mechPos| (entity positions, sqrt of the
|
||||
component squares). The mech DEATH path (part_012.c:14812-14834, states 9/10 branch) walks +
|
||||
empties the list. Consumers: the authored watchers (1→Start/0→Stop looped IncomingAlarm01
|
||||
[1:74] n58; DistanceToMissile scalecfg ctlID=12 Tempo, 100..800→600..10). PORT: the feed
|
||||
rides the BTPushProjectile simulator's salvo-LEAD rounds (mech4.cpp) — the world-entity
|
||||
Missile is a blocked transcription (gotcha 33); mirror leads report on peer nodes (the
|
||||
victim's inbound missiles are always mirrors).
|
||||
- **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²
|
||||
vs predicted pos(+0x260, predictor FUN_00422060 via +0x254) > `_DAT_004be050` = 0.1 →
|
||||
updateModel(+0x18)|=1 (ForceUpdate, replication-dirty — NOT a kill).
|
||||
updateModel(+0x18)|=1 (ForceUpdate, replication-dirty — NOT a kill). Contact: swept test
|
||||
@0042291c per tick; on hit → explosion msg 0x5C at live pos + direct TakeDamage (zone −1)
|
||||
to the struck entity via @004be078; at EXPIRY silent removal, no damage.
|
||||
- **Missile live integrator @4bef78** (master); expiry: age > burnTime(+0x340, model rec
|
||||
+0x44) + `_DAT_004bf5ac` = **10.0 s** coast margin, OR pos.y < `_DAT_004bf5b0` = **−1.0**
|
||||
kill-plane → FUN_0042061c. Keepalive: no update for `_DAT_004bf594`=2.0 s → dirty bit.
|
||||
**Proximity fuse**: seeker rangeToTarget(+0x10C) < `_DAT_004bf5a4` = **4.0** → detonate on
|
||||
targetEntity without a geometry hit. Seeker drops a destroyed target (movementMode 2|9,
|
||||
FUN_0049fb54).
|
||||
- **Missile FLIGHT MODEL (velocity/dt audit 2026-08-13) [T1]:** per frame:
|
||||
`MissileThrusterSimulation` **@004be474** (the standalone Performance body — recovered;
|
||||
misthrst.cpp's "folded" note was stale) slerps the missile toward the seeker aim
|
||||
≤ MaxThrusterRotationRate·dt and, while burning, adds `(0,0,−ThrusterAccel)` to the
|
||||
frame-zeroed accumulator; @4bef78 adds quadratic drag `−COD·ρ·sign(v)·v²` per body axis
|
||||
(burn AND coast; the dead plain-Projectile @4bddec uses power **1.0** via
|
||||
ApplyAirResistanceAndGravity(1.0)), zeroes LATERAL local vel/accel while burning (velocity
|
||||
slaved to the nose; slaving STOPS at burnout), applies gravity (env, default **6.5**) ONLY
|
||||
when coasting, then integrates ×dt (@00421bac). Net: `dv/dt = T − COD·v²`. **Authored
|
||||
CODs = 0.001 ALL axes, all four models** (BTL4.RES type-15 rid 315 srm / 319 lrm /
|
||||
323 strk / 327 nrk, raw floats); ctor @4bf5b4 (part_013.c:18416) precomputes **terminal
|
||||
speed `sqrt(T/negCOD.z)` → missile+0x348** (the seeker lead-time base): LRM/Streak/NRK
|
||||
**547.7 u/s**, SRM **774.6**. Port pool carries the same ODE since the 2026-08-13 drag
|
||||
fix (mech4.cpp; drag was previously omitted as "negligible" — it is the speed governor).
|
||||
- **Performance split** (Projectile ctor @4be1bc / Missile ctor @4bf5b4): instance
|
||||
(flags&0xC)==ReplicantInstance(4) → FUN_004221c0 dead-reckoning smoother (no physics);
|
||||
master → 4bddec/4bef78. (CLASSMAP's old authoritative/ghost labels were swapped —
|
||||
@@ -400,18 +449,19 @@ 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
|
||||
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).
|
||||
**LATENT ARCADE BUG found + diverged (issue #21, 2026-07-21) [T1 byte-verified + T2 repro]:**
|
||||
the Emitter charge integrates toward the GENERATOR voltage (@004ba838 `fld [src+0x1dc]`), full
|
||||
is detected only inside the +-1% snap window around `seekVoltage[idx]` (ComputeOutputVoltage
|
||||
@004ba738: `|rl-1|<=0.01 -> 1.0`), and OVERCHARGE reads ZERO (`rl>1.01 -> 0`,
|
||||
`_DAT_004ba830 = 0.0` byte-verified). Toggle the seek gear while a charge is in flight and
|
||||
the level lands ABOVE the new gear's window -> rechargeLevel pinned 0, the `==1.0` Loaded test
|
||||
(`_DAT_004bac04`) never fires, charging continues to the generator ceiling where EVERY gear
|
||||
reads overcharged -> the weapon is PERMANENTLY bricked (arc dark, ready-dot dark, no fire; gen
|
||||
re-select cannot help). The arcade shipped this (locked 60 fps + rare seek use hid it); the
|
||||
port's clickable seek button triggers it in seconds. **Deliberate divergence** (emitter.cpp
|
||||
Loading tick): overcharge (`currentLevel > seekVoltage[idx]`) counts as fully charged ->
|
||||
Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro +
|
||||
**#21 RETRACTED 2026-08-13 (era testimony -> adversarial re-audit; witnesses 6-0):** the
|
||||
"LATENT ARCADE BUG / permanently bricked / the arcade shipped this" claim that stood here was
|
||||
a CONSTANT-WIDTH MISREAD. `_DAT_004ba830` is a **DOUBLE 1.0** (bytes 00..00 f0 3f @0x4ba830 --
|
||||
the old "0.0 byte-verified" read the low DWORD of a QWORD: 1.0's mantissa zeros; Ghidra's own
|
||||
"globals overlap smaller symbols" warning applies). ComputeOutputVoltage @004ba738 truth [T1
|
||||
raw disasm]: ratio = charge/seekV[idx]; snap |rl-1|<=0.01 -> 1.0f; else CLAMP to [0,1] --
|
||||
overcharge stores EXACTLY 0x3f800000, the `==1.0` Loaded test passes next tick, the weapon
|
||||
FIRES the stored charge (damage divisor is the RECOMMENDED gear, so a wrap-fired charge deals
|
||||
normal top-gear damage). ToggleSeekVoltage @004ba478 genuinely skips ResetFiringState on wrap
|
||||
[T1] -- but nothing downstream ever bricks. The machine never had a dark laser; the port's
|
||||
"rescue" was a patch over OUR misread (removed with the clamp fix, same date). Constants
|
||||
extracted: 0x4ba818=0.0f 0x4ba81c=1e-4f 0x4ba820=1.0f 0x4ba824=0.01f 0x4ba828=dbl 0.0
|
||||
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,
|
||||
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`
|
||||
@@ -581,9 +631,14 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
|
||||
(`FUN_0041bd98`) — **VARIABLE-STEP, slice in real SECONDS**, line-for-line the
|
||||
WinTesla SIMULATE.cpp body. Corollaries: every Performance's `time_slice` is
|
||||
seconds in BOTH binary and port (no hidden tick/second unit gap); a dt-LESS
|
||||
per-frame term (myomer kinetic @4b8d18's middle term; the Mover gravity
|
||||
`-= **(mover+0x250)` @0x421e77) fires at the FRAME cadence = the tick rate,
|
||||
28 Hz nominal, sagging under load. Mover velocity is u/s (the replicant
|
||||
per-frame term into PERSISTENT state (myomer kinetic @4b8d18's middle term)
|
||||
fires at the FRAME cadence = the tick rate, 28 Hz nominal, sagging under load.
|
||||
⚠ The Mover gravity `-= **(mover+0x250)` @0x421e77 is NOT in that class
|
||||
(corrected 2026-08-13, velocity/dt audit): `Mover::PerformAndWatch` =
|
||||
**`FUN_00422360`** zeroes `localAcceleration` every frame (== T0
|
||||
MOVER.cpp:672), so gravity/drag/thrust adds define a fresh per-frame
|
||||
ACCELERATION integrated ×dt by `ApplyWorldAccelerations` @00421bac —
|
||||
frame-rate-independent, no 28 Hz rescale. Mover velocity is u/s (the replicant
|
||||
dead-reckoner @0x421f7c does `pos += vel × ticksΔ/DAT_0052140c`). The myomer
|
||||
heat term's ONE caller is the **unexported MyomersSimulation body
|
||||
@0x4b8b9a-0x4b8d0d** (E8-scan + raw disasm; the export gap hid it): calls
|
||||
@@ -732,7 +787,15 @@ 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_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_TRACE` | per-frame hit-bounce integrator trace |
|
||||
| `BT_GYRO_TRACE` | per-frame hit-bounce integrator trace (`[gtrace]`, carries wall-ms `t=`) |
|
||||
| `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_INCOMING_LOG` | `[incoming]` missile-alarm receipt (#83): LOCK/clear edges + ~1Hz held lines with distance (mech4.cpp PerformAndWatch latch) |
|
||||
| `BT_DESIGNATE=enemy` | bench designator (#83 rig): stamps the production target slots (+0x388/+0x37c/+0x38c) with the nearest live mech on frames where the boresight pick found no mech — standoff locks; a real pick still wins; HUD ring untouched |
|
||||
| `BT_WORLDHIT=0` | disables the missile flight sweep's static-solid-tree half (#175) — restores the heightfield-only world test (A/B lever; default on) |
|
||||
| `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_GOTO="enemy"\|"x z"` / `BT_GOTO_LOG` | self-driving beeline (to the enemy or a map coordinate) + its log |
|
||||
|
||||
@@ -55,7 +55,11 @@ then unconditionally `+0x264 = +0x268 = mission->advancedDamageOn(+0xf0)` (both
|
||||
combat-ineffective evaluation (part_012.c:9364: mech state ∈ {3,4} && novice is an extra OR-term
|
||||
setting `mech+0x414`). The `#if 0` block in surviving PPC.CPP:63-95 (novice PPCs deal no damage)
|
||||
shows the original intent of the tier.
|
||||
- **+0x260 — the HEAT-MODEL master switch, ON for veteran+expert.** This is `FUN_004ad7d4`
|
||||
- **+0x260 — the HEAT-MODEL master switch, ON for veteran+expert.** Writer pinned 2026-08-13
|
||||
[T1]: the BTPlayer ctor @004c0bc8 (part_013.c:19397-19426) stamps the whole flag block from the
|
||||
session experience level — `+0x260 = (level>=2)`, `+0x25c = (level>=1)`, `+0x270 = (level>=1)`,
|
||||
`+0x26c = (level==1||level==2)`, `+0x274 = the raw level`, `+0x264/+0x268 = record+0xf0`
|
||||
(advancedDamage). Read via `FUN_004ad7d4`
|
||||
(`*(*(sub+0xD0)+0x190)+0x260`), the single most-called gate in the subsystem family: Myomers
|
||||
work→heat (movement heat, MyomersSimulation @4b8d18), Emitter/energy-weapon firing heat, missile
|
||||
launch heat, the heat.cpp simulation, projectile-weapon heat, and the heat-scaled part of the jam
|
||||
@@ -89,8 +93,13 @@ the `Tesla40_BT_manual.pdf` scan along with p3):
|
||||
- **The viewscreen hunting-aid gate is GONE in 4.10**: part_014 (HUD/btl4vid TU) reads none of the
|
||||
player flags — the target hotbox/edge arrows (@4cdf6f) are gated on lock state only, every mode.
|
||||
Fits the 4.10 arcade-cities positioning.
|
||||
- **Movement heat is veteran+expert in 4.10** (the +0x260 gate), not expert-only as the 4.0 manual
|
||||
states. Whether a coefficient still differentiates them is open.
|
||||
- **Movement heat is veteran+expert in 4.10** (the +0x260 gate; stamp table read at @004c0bc8
|
||||
[T1]), not expert-only as the 4.0 manual states. ERA-WITNESS CROSS-CHECK (Oracle's guide
|
||||
return, 2026-08-13, Gitea #177): his pod memory says expert-only, agreeing with the MANUAL and
|
||||
against the 4.10 code — consistent with heat-at-veteran being a genuine 4.0→4.10 balance
|
||||
change (his hands-on spans the earlier revision). Tester-notes line warranted so veteran
|
||||
heat isn't reported as a bug. Whether a coefficient still differentiates vet/expert heat
|
||||
magnitude is open [T4].
|
||||
|
||||
## Port state (as of 2026-07-20) — WIRED (issue #2) [T2]
|
||||
|
||||
|
||||
+132
-8
@@ -194,7 +194,8 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
- **Environment gravity unwired — the myomer CLIMB-heat term is inert (found 2026-07-31, #85).**
|
||||
`Mover::localEnvironment` (MOVER.h:271) is declared and never populated anywhere in the port;
|
||||
`GetEnvironment()->gravityConstant` is the named analog of the original Mover's gravity POINTER at
|
||||
`+0x250` (`FUN_00421e2c` does `vy -= **(+0x250)` per tick), which the myomer drive-heat
|
||||
`+0x250` = `localEnvironment` (MOVER.h; `FUN_00421e2c` = ApplyAirResistanceAndGravity does
|
||||
`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`)
|
||||
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
|
||||
@@ -715,12 +716,19 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
|
||||
the Mover holds to the live gravity cell). Harmless on flat ground (|vy|≈0 anyway) but the
|
||||
climb-work heat never accrues on slopes. Find where our Environment gravity actually lives
|
||||
and re-point the bridge. [T2 measured]
|
||||
- **Engine Mover gravity is dt-LESS per-frame in the binary** (`@0x421e77: vy -= **(+0x250)`,
|
||||
raw, once per Perform — same class as the myomer kinetic term). If our linked WinTesla
|
||||
MOVER.cpp kept that form, gravity acceleration in the port scales with OUR ~59Hz frame rate
|
||||
(≈2.1× the pod's 28) — jump arcs, falls, missile droop all stiffer than the pod. CHECK the
|
||||
WinTesla source's gravity line; if per-frame, it needs the same 28Hz reference-rate
|
||||
normalization the myomer kinetic term got. [T1 binary side / T4 port impact until read]
|
||||
- **✅ Engine Mover gravity needs NO 28Hz rescale — CLOSED 2026-08-13 (velocity/dt audit) [T1/T0].**
|
||||
The worry was that the per-frame `worldAccel.y -= **(+0x250)` (@0x421e77, no dt) would scale
|
||||
with our frame rate. It cannot: **`Mover::PerformAndWatch` = `FUN_00422360` ZEROES
|
||||
`localAcceleration` at the top of every frame** (`FUN_0040a7f4(+0x1dc, &DAT_004e0fd4)` ==
|
||||
T0 MOVER.cpp:672 `localAcceleration = Motion::Identity`), so gravity/drag/thrust adds are
|
||||
per-frame *acceleration definitions* on a fresh accumulator — a constant `a` integrated ×dt
|
||||
by `ApplyWorldAccelerations` @00421bac (`v += a·dt`), correct at ANY frame rate. The port's
|
||||
linked MOVER.cpp has the identical structure. (Distinct from the myomer per-tick accumulates,
|
||||
which land in PERSISTENT state and did need the ×(dt·28) fix.) The separate gap — the port's
|
||||
`GetEnvironment()->gravityConstant` sampling 0 (unwired Environment, entry above) — is still
|
||||
open; note the target constant is **6.5** (LATTICE.cpp:413 default), not 9.8 (9.8 appears only
|
||||
in the AC tracer fire-control math @0x4bc67c). Full audit:
|
||||
`docs/WEAPONS_DRIFT_AUDIT.md` §VELOCITY/DT INTEGRATION.
|
||||
- **Gait-noise phantom acceleration feeds the myomer accel term** (`[myoheat] a=4..33` at
|
||||
steady cruise — our per-frame velocity re-derivation jitters; the binary read the same
|
||||
`AccelerationLastFrame` cell off ITS OWN gait, noise level unknown). Bounded contributor
|
||||
@@ -1225,7 +1233,18 @@ 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,
|
||||
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.
|
||||
- **#173 -- chicken-walker seek-4 myomer overheat** (see [[subsystems]] night-16 note).
|
||||
- **#173 -- chicken-walker seek-4 myomer overheat: ⚑ SETTLED AUTHENTIC (2026-08-13, three
|
||||
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
|
||||
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,
|
||||
@@ -1245,3 +1264,108 @@ consistent with internals-only, see #103). Logs: `scratchpad/night16/` + Oracle
|
||||
wanted for the pair-leak confirmation.
|
||||
- Nanook lost the coolant MFD glass window, recovered only by config edit -> #76 comment
|
||||
(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.
|
||||
|
||||
## Oracle's weapons-guide return — the first curated .txt (2026-08-13, docs/feedback/WEAPONS_GUIDE_RETURN_Oracle_20260813.txt)
|
||||
Fourteen answers; the two CONTRADICTS are tracked as Gitea #176 (seek-gear damage swing — read
|
||||
the authored seekVoltage gear table; the (charge/seekV[rec])² law and his memory may both hold
|
||||
if the authored gears are closely spaced) and #177 (veteran heat — witness + 4.0 manual say
|
||||
EXPERT-only; the player+0x260 WRITER has never been read). Settled/confirmed [witness]:
|
||||
- Out-of-range laser fires but deals nothing — the range cliff CONFIRMED (he trigger-grouped
|
||||
weapons by range bands to avoid useless heat; Thor noted all-energy-but-one).
|
||||
- Generator reassignment mid-charge PRESERVES the charge (resumes where it left off; pre-round
|
||||
gen/seek shuffling was routine; a killed generator left weapons mid-charge, resumed on
|
||||
reassign). VERIFY the port preserves charge across reassignment — audit item.
|
||||
- Gear wrap harmless (re-confirms #21); down-shifting used deliberately for RATE of fire.
|
||||
- PPC victim-side: MFD collapse-to-a-vertical-line (CRT deflection collapse + phosphor bloom,
|
||||
then re-expand) on ALL MFDs incl. secondary — reference photo ARCHIVED at
|
||||
reference/field/oracle_mfd_collapse_ppchit.jpg (his Discord link expires) — THE visual target
|
||||
for Cyd's ppc-sync-distortion pass. Also: PPC hits sometimes flipped AUTO-mode generator
|
||||
assignments on the victim (momentary trip, never saw one stay offline) — matches the brownout
|
||||
rider.
|
||||
- AFC: NO visible projectile in his memory — muzzle flash + impact flash only, no tracer, both
|
||||
perspectives ("no pace"). Era question OPEN: our 0xBCD tracer code is real [T1], but whether
|
||||
the shipped machine DREW it visibly needs corroboration (ask Draco/SAURON Friday). Recoil
|
||||
kick scaled with caliber — matches dmg/16 [T1].
|
||||
- Gauss carrier = "the Mischief" (variant name) — cross-check the content roster.
|
||||
- Missile speed-drop pre-endorsed ("direction I expect"); splash MATCHES; rack "personality"
|
||||
(Thor/Madcat loft high, Avatar flatter) = the authored per-rack MuzzleVelocity up-tilt we
|
||||
already launch with + range-scaled seeker loft — likely already faithful, no action.
|
||||
- SRM pip 800 agrees with the VWE site capture — the 450/800 question is CLOSED at 800.
|
||||
- Jam eject = single tap (matches tap=one round [T1]; the 3s hold = bay dump, separate).
|
||||
- Incoming alarm: the beeper was the WHOLE annunciation (no lamp/voice rider) + two YouTube
|
||||
references with the real alarm audible (on #83, comment 1797) — the ear-A/B target for the
|
||||
b00fa39 fix.
|
||||
|
||||
### Oracle-return follow-ups resolved same night (2026-08-13, late)
|
||||
- **#176 seek-gear damage: RESOLVED, no port change.** Law confirmed from the Emitter ctor's own
|
||||
calibration (@004bb120: EC = energyTotal/(0.5 x seekV[REC]^2) -> damage = authored x
|
||||
(V/seekV[rec])^2 [T1]); the authored gear table read from content (LLaser: {6000,7000,8000,
|
||||
9900} vs rated 10000; recommended = the 8000 gear = "seek 3" = the published calibration
|
||||
point, EXACTLY as Oracle said). Real ladder: 56%/77%/100%/153% -- seek 4 is an OVERCHARGE
|
||||
gear (no clamp; a logged 5.80-damage LLaser shot from an 8618V store proves >100% ships;
|
||||
~2.9x charge time from the exponential curve). The guide's "half-charge = quarter" example
|
||||
was the wrong part -- corrected in the master guide. OPEN [T3]: whether other emitters
|
||||
author different gear fractions than LLaser's 0.60/0.70/0.80/0.99 (dump [seek] per weapon).
|
||||
- **#177 veteran heat: RESOLVED-FAITHFUL, closed.** The BTPlayer ctor stamp table read verbatim
|
||||
(@004c0bc8 [T1]): +0x260 heat = veteran+expert in 4.10. Oracle + the 4.0 manual say
|
||||
expert-only -> best read: a genuine 4.0->4.10 balance change (his hands-on spans the earlier
|
||||
machine). Tester-notes line warranted (veteran heat is not a bug). The vet-vs-expert heat
|
||||
MAGNITUDE coefficient stays open [T4].
|
||||
- **NARC/STREAK per-type seeker maneuverability (Oracle: the "angry swarm of bees" at high
|
||||
aspect angles near a maneuvering target)** [T3→T4]: the port's steering gains (4.0 turn /
|
||||
8.0 loft) are PORT-TUNED constants applied uniformly to every missile family; Oracle's
|
||||
memory says the maneuverable types behaved differently. Candidate: an authored per-model
|
||||
turn-rate field in the missile model record (we consume +0x44 maxTimeOfFlight, +0x48
|
||||
thrust, +0x50 detonationMode — +0x4c is UNREAD). Read +0x4c across the 8 cached missile
|
||||
models; if it differentiates NARC/STREAK, wire it as the turn gain. Post-Friday.
|
||||
- **Gauss carrier confirmed vs content (Oracle: "the Mischief")**: Mischief = lok2 (the exe's
|
||||
variant-name table: vul1="Mad Dog", lok2="Mischief", mad2="Zanin Neko"), and the Loki
|
||||
family's L4GAUGE.CFG section authors GAUSS + AmmoBinGAUSS gauge rows — consistent. (A
|
||||
separate section authors GAUSS_1/GAUSS_2 twin rows at letters H/O — which family that is
|
||||
remains unidentified; low priority.)
|
||||
|
||||
@@ -635,6 +635,14 @@ CONCLUSION: the coolant-loop reconstruction is faithful; every difference is 4.0
|
||||
(loadout rework on Loki, small-laser redistribution elsewhere), NOT a bug. `BT_SPEC_LOG` (mech4)
|
||||
re-dumps on demand.
|
||||
|
||||
### Weapon-range cross-check RESULT (2026-08-13) [T1] — manual prints only 4 range figures
|
||||
The manual has NO weapons table and prints NO per-weapon damage number anywhere; the per-mech
|
||||
stat sheets carry no range/damage column. The only printed ranges are MFD screenshot mockups:
|
||||
PPC 900M (p20) and AFC100 400M (p21) MATCH the shipped 4.10 content; SRM4/SRM6 450M (p22/p23)
|
||||
vs the shipped 800 is the same 4.0→4.10 drift class as the Loki rework (and missile WeaponRange
|
||||
is display-only — no physics change either way). Full table + the rangefinder-scale verdict:
|
||||
`docs/WEAPONS_DRIFT_AUDIT.md` §RANGE AUTHENTICITY.
|
||||
|
||||
## Key Relationships
|
||||
- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view).
|
||||
- Input: [[locomotion]] (the mapper).
|
||||
|
||||
@@ -1064,3 +1064,45 @@ 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
|
||||
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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
## 33. A FIX WIRED INTO A BLOCKED TRANSCRIPTION: code that compiles, reads faithfully, and never runs (#83, 2026-08-13)
|
||||
The port carries two kinds of reconstructed code: LIVE paths (what the game executes) and
|
||||
TRANSCRIPTIONS -- byte-faithful class reconstructions kept for reference or blocked on an
|
||||
engine mismatch. The world-entity `Missile` (missile.cpp) is the archetype: blocked by the
|
||||
2007 engine Entity-base mismatch, it never flies; live missile flight is the BTPushProjectile
|
||||
simulator (mech4.cpp). The July F7 fix (cc2b109) wired the incoming-missile alarm feed into
|
||||
`Missile::MoveAndCollide` -- correct logic, faithful comments, clean build, marked FIXED in
|
||||
the doc header -- and it was DEAD CODE for a month. #83 ("alarm missing entirely") was filed
|
||||
by an era witness two weeks after the "fix" shipped, and the doc's FIXED claim steered this
|
||||
audit wrong for half a session (the F7 body text still described the pre-fix state; the
|
||||
header said fixed; NEITHER was field truth).
|
||||
**Rules:** (1) when a subsystem has a transcription AND a live path, a fix lands in the LIVE
|
||||
path -- grep for what actually executes (receipts in field logs are the arbiter), not what
|
||||
reads correctly. (2) Mark inert transcriptions with an explicit *** TRANSCRIPTION ONLY --
|
||||
THIS NEVER RUNS *** banner naming the live site (missile.cpp now does). (3) A doc header's
|
||||
FIXED is a claim, not evidence -- a fix without a FIELD receipt ([incoming], [seqloop], etc.)
|
||||
is T3 at best. (4) The fix's own bench must fail first: the first #83 bench (solo) produced
|
||||
zero receipts because solo seats never fill -- proving the rig, then the fix, in that order.
|
||||
|
||||
+172
-8
@@ -122,7 +122,10 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
|
||||
(mech4.cpp): `+0x1C4` vec = `localVelocity.linearMotion`; `+0x82C` vec =
|
||||
`localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame);
|
||||
`+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong);
|
||||
`*(+0x250)` = the environment **gravity pointer** (`FUN_00421e2c` does `vy -= **(+0x250)`/tick).
|
||||
`*(+0x250)` = the environment **gravity pointer** (= `Mover::localEnvironment`, MOVER.h;
|
||||
`FUN_00421e2c` = ApplyAirResistanceAndGravity does `worldAccel.y -= **(+0x250)` each frame —
|
||||
onto an accumulator `Mover::PerformAndWatch` @00422360 zeroes every frame, so it is a constant
|
||||
acceleration integrated ×dt, NOT an accumulating per-tick term; velocity/dt audit 2026-08-13).
|
||||
Physics: `heat += ratio²·(1+dmg)·[0.005·½mv² + 0.2·m·g·|vy|·dt + 0.2·m|v||a|·dt]` — kinetic work
|
||||
+ climb power + acceleration power. The binary `fabs`es v.y (the port's old draft didn't).
|
||||
**Authored tuning extracted from BTL4.RES** (all 18 mech variants share one record):
|
||||
@@ -339,17 +342,178 @@ 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
|
||||
myomers) is authentic and remains.
|
||||
|
||||
## Spawn-discharge loop heat + per-chassis generator bias (night-16 field, 2026-08-12) [T3 field-observed, mechanism plausible-authentic]
|
||||
## Spawn-discharge loop heat + per-chassis generator bias — ✅ CONFIRMED (2026-08-13, #173 bench lane) [T2 live receipts + T1 authored data]
|
||||
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
|
||||
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
|
||||
cooks off first even if the player fires only missiles. Not yet verified against the authored
|
||||
data -- verify via BTL4.RES generator/loop assignments before treating as established. [T3]
|
||||
cooks off first even if the player fires only missiles. **All three links verified:**
|
||||
(1) *Weapons spawn discharged* — Emitter ctor @004bb120 AND ResetToInitialState @004ba4d0 both
|
||||
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)
|
||||
cannot run seek-4 myomers without immediate overheat, loop 5 pegged, heat only sheds near
|
||||
standstill (Elengil; Oracle repro). Investigate myomer heat rate + loop-5 capacity vs authored
|
||||
data for those chassis; authored-balance vs port-artifact undecided. The myomer drive-heat
|
||||
FORMULA is verified faithful (#137, above) -- if the rate is right, the question moves to the
|
||||
authored loop capacity / condenser assignments.
|
||||
standstill (Elengil; Oracle repro). ⚑ SETTLED 2026-08-13 by the three lanes below (authored
|
||||
data + cooling-side audit + live bench matrix): AUTHENTIC authored balance, reproduced and
|
||||
quantified; one bounded [T3] severity residual (accel-term ring cadence) and the locomotion
|
||||
top-speed cross-check remain flagged. See the BENCH lane block for the numbers.
|
||||
|
||||
## #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,3 +234,29 @@ and asserts [crit] DESTROYED + [techstat] X + zero named FIRED after + REFUSED>0
|
||||
fires; P2 partial-crits to 0.5 and asserts the weapon KEEPS firing (freeze regression, the #164
|
||||
upstream writer). LESSON (gotcha SS30): BT_KILL_SUBSYS writes the cells directly and can only
|
||||
validate READERS -- state benches must drive the real writer path.
|
||||
|
||||
## weapons_sweep (night16) -- the all-families weapons bench (#171/#174/lane-12, PASS 2026-08-13)
|
||||
`scratchpad/night16/weapons_sweep.sh` + `weapons_check.py`: ONE 2-node bench proving every
|
||||
weapons family on one tree. Rig: **A=mad2 "Zanin Neko"** (AFC50 + ERPPC + LRM15 + SRM6 x2 +
|
||||
ERMLaser x2 -- all three families on one chassis, the AC carrier per L4GAUGE.CFG) vs **B=madcat**
|
||||
(AFC100 + LRM15 x2 + ER lasers), grass/day, `BT_GOTO=enemy` + throttled BT_AUTOFIRE +
|
||||
BT_AF_MISSILE (periods 4/7), 300s, env `BT_DMG_LOG BT_PROJ_LOG BT_DEATH_LOG BT_AMMO_LOG`.
|
||||
Asserts per family: energy named [emitter] FIRED + type-3/4 dmghits; AC type=1 dmghits ALL
|
||||
burst=1 + ballistic (guided=0) PUSHes ALL dmg=0 (#171 hitscan; damage DETs reconciled to guided
|
||||
pushes per log); missiles [ammo] FIRED + IMPACT dmg>0 with burst>1 + type=2 dmghits (contact
|
||||
through the 4.0u fuze at drag-governed speeds); #174 cascade-per-life<=1; bystander receipts
|
||||
individually classified; hard rig floors. `--selftest` feeds the detector pre-#171/pre-#174
|
||||
shapes + bystander false-positive shapes and requires each to FAIL (run first, gated).
|
||||
Rig lessons [T2]:
|
||||
- **avatar-vs-avatar forms NO locks** (three prior runs, 0 dmghits even from lasers) while the
|
||||
madcat family fights fine -- dodge with madcat-family chassis; `mad2` is the AC carrier.
|
||||
- Node->pilot-page mapping: `-net 1501` claims pilot `127.0.0.1:1502`, `-net 1601` claims
|
||||
`:1602` (net port + 1) -- sed each pilot page's vehicle= line EXACTLY for mixed pairings.
|
||||
- **BYSTANDER receipts legitimately fire in a 2-mech fight**: the production aim ray designates
|
||||
SCENERY constantly (the ac_bench BT_FIRE_AT_ICON lesson generalizes to the real pick), and a
|
||||
mech crossing a scenery-locked round's path detonates it -- the binary world-sweep shape
|
||||
(FUN_0042291c via @004bef78 [T1]). All 11 receipts of the PASS run were true positives
|
||||
(struck = the other live mech, aim 146-1016u away, one paired IMPACT each;
|
||||
`scratchpad/night16/bys_forensics.py`). A zero-receipt assertion is miscalibrated; assert
|
||||
the false-positive conditions instead (self-strike / phantom id / det-at-lock-aim / missing
|
||||
paired delivery / print-cap 24 reached).
|
||||
|
||||
@@ -6,7 +6,7 @@ source_sections: "multiplayer.md (lifecycle + trigger wiring, task #52); referen
|
||||
related_topics: [rendering, multiplayer, combat-damage, reconstruction-gotchas, asset-formats, bgf-format]
|
||||
key_terms: [material-ramp]
|
||||
open_questions:
|
||||
- "Peer warp IS wired + visible -- observer sees a peer's un-wreck warp (world-anchored BTStartWarpEffect, 160b78e). Remaining nuance [T3]: anchor to the peer's authentic DropZoneLocation (not replicated) vs the peer's world position."
|
||||
- "Peer warp RETIRED (#111, 2026-08-14): the world-anchored peer sphere was a port extension; the authentic effect is POV-only. Observers see the un-wreck model swap. The old DropZoneLocation-anchor nuance is moot."
|
||||
- "In the 2-node force-damage test only the EXPAND (respawn) warp logs reliably; confirm the COLLAPSE (death-side) fires on every MP death path."
|
||||
---
|
||||
|
||||
@@ -118,20 +118,17 @@ it impossible to ignore. Generalized in [[reconstruction-gotchas]] §13.
|
||||
## Trigger wiring (summary; full detail in [[multiplayer]]) [T2]
|
||||
Local player's own death → `BTStartWarpCollapsePOV()` (btplayer.cpp `VehicleDeadMessageHandler`,
|
||||
`deathCount==-1`, guarded `this==GetMissionPlayer()`). Respawn `DropZoneReply` → `BTStartWarpExpandPOV()`
|
||||
(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. **Peer warp IS wired + visible
|
||||
(`160b78e`):** an observer sees a peer's death/respawn warp — the replicant un-wreck fires the
|
||||
world-anchored `BTStartWarpEffect(x,y,z)` at the peer's position (`MechDeathHandler::Tick`,
|
||||
since the #94 fix triggered by the death-state EXIT edge of the replicated `MovementMode` —
|
||||
cause-agnostic, covers zone-less collision deaths; gated to
|
||||
`ReplicantInstance`; the effect machine is a SINGLE global slot, and since 2026-07-30 the local
|
||||
pilot's own POV lifecycle OWNS it — `BTStartWarpEffect` self-skips while `gWarpPhase!=0 && gWarpPOV`
|
||||
(`[tloc] peer warp SKIPPED`), because a peer's un-wreck landing mid-collapse/wait/expand used to
|
||||
overwrite the machine and kill the POV vortex ("my respawn had no blue whirlwind"). Invisible
|
||||
before the #81 respawn fix only because overlapping respawns barely existed; 2-node bench: 20/20
|
||||
POV collapse+expand pairs, 9 peer spheres played, 11 correctly skipped; `simulationState` rides every update-record header so the observer tracks the
|
||||
peer's state). The ONLY remaining nuance [T3, non-gating]: the observer's peer sphere is anchored to
|
||||
the peer's WORLD position rather than the peer's authentic `DropZoneLocation` (that attribute isn't
|
||||
replicated) — a fidelity refinement, not a missing effect.
|
||||
(local-guarded). `BTWarpForceUnmask()` on mission-end / no-DropZones. **Peer warp RETIRED
|
||||
(#111, 3013cf8, 2026-08-14):** the world-anchored `BTStartWarpEffect` peer sphere (`160b78e`,
|
||||
fired on the replicant un-wreck edge) was a PORT EXTENSION — the authentic effect is POV-ONLY,
|
||||
and the field read a blue bubble on another player's mech as a wrong effect ("should just be
|
||||
the big boom from other POV" — Lynx, night 9, confirmed). Observers now see exactly what the
|
||||
pods showed: the un-wreck model swap itself (pfx cleanup + rebuild + the ungated `[respawn]`
|
||||
receipt all stay; only the sphere call is gone). `BTStartWarpEffect` remains defined,
|
||||
caller-less, as documentation of the one-warp-slot guard; the old drop-zone-anchor nuance and
|
||||
the slot-contention skip are both moot with it. `simulationState` still rides every
|
||||
update-record header so the observer tracks the peer's state (night-16 field receipts: all
|
||||
un-wreck edges clean `mode 9->1`).
|
||||
|
||||
## How it was solved (method) + verification [T2]
|
||||
1. **Dump the real asset** (`dump_tsphere.py`) — established it's a bicone with cylindrical UVs, ending
|
||||
|
||||
@@ -428,6 +428,29 @@ 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
|
||||
(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).
|
||||
|
||||
**TRULY FIXED 2026-08-13 (b00fa39) — the July "fix" was DEAD CODE (gotcha 33).** The cc2b109
|
||||
driver lived in `Missile::MoveAndCollide` — but the world-entity Missile is a blocked
|
||||
transcription that never flies; live flight is the BTPushProjectile simulator (mech4.cpp). So
|
||||
the attributes stayed 0/FLT_MAX in every field build while this file's header said F7 fixed —
|
||||
which is exactly what Oracle reported as #83. Binary update rule recovered [T1]: the missile
|
||||
CTOR registers into the target mech's inbound socket list (`@004bf5b4` tail: this+0x34c
|
||||
class-checked vs 0x50bdb4 → target+0x418 Add); the TARGET'S tick (`@004a9b5c`, bt/mech4.cpp)
|
||||
reads the FIRST list member — lock=1 + dist=|missilePos−mechPos| entity positions, lock=0 on
|
||||
empty; the death path empties the list (and reset writes 0/FLT_MAX — `DAT_005209d0`).
|
||||
Port fix: the feed now rides the push simulator's per-frame update, salvo LEAD only (the
|
||||
port's lead round = the binary's ONE cluster Missile per trigger, so one report per salvo =
|
||||
one registration); replicant mirror leads report identically → the alarm reaches the VICTIM's
|
||||
cockpit in MP, where inbound missiles are always mirrors. Bench (2-node standoff LRM duel):
|
||||
54/55 lock edges per node, dist closing 792→87 over ~2.2s, beeper STARTs at live authored
|
||||
tempo 10 (far) → 435 (close) — on mirrors, i.e. the field case. Receipts: `[incoming]`
|
||||
(BT_INCOMING_LOG) + the always-on looped `[seqloop]`. Field-verify Friday.
|
||||
|
||||
<a name="f5"></a>
|
||||
#### F5. FootStep is a contact LEVEL, not a clip-transition pulse (High, CONFIRMED)
|
||||
|
||||
@@ -617,6 +640,60 @@ 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>
|
||||
## 4. Validations and KB corrections (no fidelity gap)
|
||||
|
||||
@@ -683,3 +760,5 @@ reduce source-pool starvation.
|
||||
**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
|
||||
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.**
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
# 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).
|
||||
- **#175 addendum (2026-08-13, 7b12053): the STATIC-WORLD half landed too.**
|
||||
The flight step now also rays the static collision solid tree
|
||||
(`WorldStructurePick` → `Mover::FindStaticSolidHitBy`) beside the visual
|
||||
heightfield march, earliest hit wins — rounds burst on hills/structures
|
||||
instead of tunneling (user-observed on grass: an LRM duel through a
|
||||
mound; ALL 345 bench WORLD-bursts were struct-class — the mound was
|
||||
solid-tree geometry the heightfield test was blind to). Struck solid's
|
||||
owner takes the binary's zone=-1 direct dispatch (@004be078); salvo-lead
|
||||
world bursts splash (near-miss vs cover). [T3 minor, pre-existing]:
|
||||
world hits are tested before the same frame's fuze/bystander contact
|
||||
rather than in one unified earliest-t arbitration — mixed ordering only
|
||||
matters when a round crosses cover AND reaches its fuze point within a
|
||||
single ~10-16u frame step. BT_WORLDHIT=0 = rollback lever.
|
||||
|
||||
### 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.)
|
||||
@@ -0,0 +1,584 @@
|
||||
=============================================================================
|
||||
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 different CHARGE GEAR (section 2,
|
||||
veteran/expert only), and the REAL ladder -- read from the machine's
|
||||
own gear table after Oracle's notes called out our old example --
|
||||
is: seek 1 = 56% damage (fastest), seek 2 = 77%, seek 3 = 100%
|
||||
(THE published number -- the calibration gear), seek 4 = 153%
|
||||
(an OVERCHARGE: more damage AND more heat per shot, at roughly
|
||||
triple the charge time). Downshift for rate of fire, seek 4 for
|
||||
the single big hit. 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.
|
||||
=============================================================================
|
||||
@@ -0,0 +1,531 @@
|
||||
=============================================================================
|
||||
BT411 -- WEAPONS FIELD GUIDE: EVERY WEAPON, AS THE ORIGINAL BUILT IT
|
||||
Updated 2026-08-13 -- reconstruction review edition, for the whole group.
|
||||
=============================================================================
|
||||
WHAT THIS IS: we have now read the original machine's firing code for
|
||||
every weapon family and compared it, behavior by behavior, against what
|
||||
our port does. This document is the result. For each family you get:
|
||||
what the shipped machine really did, what our port does today, what
|
||||
changes in the next build, and -- where the code alone cannot settle
|
||||
it -- what we need from your memory.
|
||||
|
||||
HOW TO REVIEW: you flew the real pods; we only read their code. Read
|
||||
each section and mark anything that CONTRADICTS your memory of the
|
||||
machine -- and anything that confirms it. Silence is not confirmation:
|
||||
if a section simply matches what you remember, a one-word "matches" is
|
||||
genuinely useful data.
|
||||
|
||||
HOW TO REPLY: EDIT THIS FILE. Every section that needs your input
|
||||
ends with a YOUR NOTES block -- write directly in it (callsign first,
|
||||
then MATCHES / CONTRADICTS / CAN'T RECALL, then your detail) and post
|
||||
the edited file back when you're done. Which floor, which revision,
|
||||
which mech you flew -- machine-room details turn weak memories into
|
||||
strong evidence.
|
||||
|
||||
ONE RETRACTION UP FRONT: the last issues handout (#27) told you
|
||||
ballistic rounds could now "genuinely MISS a moving target". The
|
||||
deeper dig behind this document proved that framing wrong: the
|
||||
original decides a cannon hit AT THE TRIGGER, and the round you watch
|
||||
fly is a tracer drawn for show. We were wrong; details in the
|
||||
AUTOCANNONS section. Your review of that section is part of the
|
||||
group's sign-off on the correction.
|
||||
=============================================================================
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
1. LASERS -- SMALL / MEDIUM / LARGE, STANDARD AND ER
|
||||
-----------------------------------------------------------------------------
|
||||
THE ORIGINAL'S BEHAVIOR
|
||||
- A charge-and-release energy weapon. No ammo, no jam. Six marks, one
|
||||
mechanism -- only the numbers differ (Appendix A). No pulse lasers
|
||||
exist anywhere in the shipped content.
|
||||
- Firing needs three things at the trigger: a LOCKED target, your
|
||||
view actually on the target (the weapon only arms while you are
|
||||
looking where it points), and the target inside effective range.
|
||||
- The beam is instant. The hit is decided the moment you fire -- no
|
||||
travel time, no leading. What you see is the discharge drawing
|
||||
itself, plus a 0.2-second afterglow that tracks the target.
|
||||
- Damage scales with the SQUARE of the charge: a full-charge shot
|
||||
does the listed number; a half-charge release does a quarter.
|
||||
- Out of range: the trigger works and the beam draws, but the damage
|
||||
is silently thrown away. No hit, no denied tone we can find.
|
||||
- Effective range shrinks in proportion to damage on the body zone
|
||||
carrying the weapon: a half-wrecked arm fires at half reach. An
|
||||
undamaged weapon reaches exactly its listed range -- confirmed
|
||||
down to the byte this week.
|
||||
- Overheat is a hard stop: a heat-failed laser quits outright. There
|
||||
is no gradual weakening on the way down.
|
||||
- Each hit lands on ONE panel of the target.
|
||||
|
||||
OUR PORT TODAY
|
||||
Same machine, verified live end to end. The beam is drawn from the
|
||||
real laser-tube art. Charge timing is smoothed for modern frame
|
||||
rates -- a robustness measure, not a behavior change. (The #164
|
||||
stuck-beam guards are already in.)
|
||||
|
||||
CHANGING IN THE NEXT BUILD
|
||||
Nothing mechanical.
|
||||
|
||||
WE NEED YOUR MEMORY
|
||||
- Firing past range: do you remember shots that visibly fired at a
|
||||
distant target and just did nothing? That is what the machine did.
|
||||
- Generator switching mid-charge: did switching or re-routing
|
||||
generators while a laser was charging COST you the in-flight
|
||||
charge (with an electrical alarm)? A yes pins down a control path
|
||||
we have not been able to identify from the code alone.
|
||||
|
||||
|
||||
YOUR NOTES (callsign first, one entry per line):
|
||||
> ¡Oracle! CONTRADICTS Square/inverse square law damage output change on seek level changes is too large a change between seek levels. Note the values presently depicted in the MFDs and Seek 3 damage output agrees with published data from the VWE site capture.
|
||||
> ¡Oracle! Cannot comment on effective range reducing due to section damage. This was unknown to me and never came up in discussion among players
|
||||
> ¡Oracle! Yes, firing at targets beyond the lasers maximum range discharged the laser but no damage was dealt to the target. I assigned triggers on all mechs based on range groups so that I would not heat up the mech with useless firing of weapons outside their effective range. Important on the Thor where all but one weapon is energy based.
|
||||
> ¡Oracle! No switching a generator mid-charge did not cause a loss of charge, the weapon would continue charging from where it left off when reconnected to a generator. It was very common to change generator assignments and seek levels before mission start and there was no loss of weapon charge state due to such changes as all weapons initialized without a charge and were in the process of charging at mission start. Losing a generator due to damage or overheating left weapons in mid-charge state and they resumed charging when assigned to a new generator.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
2. THE CHARGE GEAR (ALL ENERGY WEAPONS) -- AND THE BRICK THE ARCADE SHIPPED
|
||||
-----------------------------------------------------------------------------
|
||||
THE ORIGINAL'S BEHAVIOR
|
||||
Veteran/expert pilots could cycle an energy weapon's charge gear
|
||||
(four settings). The machine shipped with a latent flaw: cycling
|
||||
PAST the top gear wrapped to the bottom without resetting the firing
|
||||
state. The stored charge now sat above the new gear's release
|
||||
window, the output read zero, and the weapon went permanently dark
|
||||
-- charged, lit, and unable to fire until re-arm. The arcade
|
||||
shipped this.
|
||||
|
||||
OUR PORT TODAY
|
||||
We deliberately rescue it: an overcharged weapon counts as loaded
|
||||
and fires (#21; 38 rescues proven on the bench). This is the port's
|
||||
ONE deliberate deviation in the energy lane, and it stays on the
|
||||
deviations list so nobody mistakes it for the original.
|
||||
|
||||
CHANGING IN THE NEXT BUILD
|
||||
Nothing -- unless the group votes for the authentic brick.
|
||||
|
||||
WE NEED YOUR MEMORY
|
||||
- Did cycling a laser's gear past the top setting ever leave the
|
||||
weapon DARK -- charged but refusing to fire until re-arm? If pods
|
||||
genuinely ate weapons this way, some of you will remember it.
|
||||
|
||||
|
||||
YOUR NOTES (callsign first, one entry per line):
|
||||
> ¡Oracle! CONTRADICTS - Going from Seek 4 to Seek 1 did not fail a weapon and prevent it from firing. I routinely changed seek levels in game to lower levels to increase rate of fire without negative impact to weapon state. I cannot recall a single instance of a weapon failing to recharge or fire unless it was destroyed or had no generator power to recharge.
|
||||
>
|
||||
>
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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):
|
||||
> ¡Oracle! MATCHES
|
||||
> ¡Oracle! On all MFDs including the Secondary display would rapidly and briefly collapse into a vertical line in the center if the display and then rapidly expand back to normal display. As these are CRTs the phosphor would glow very intensely when a single vertical line was displayed and the MFD image would not instantly collapse to the line not expand back to the normal display. The collapsed image looked like this: https://cdn.discordapp.com/attachments/1529538330738561085/1537570522164887592/image0.jpg?ex=6a7f85a3&is=6a7e3423&hm=91c50efca3ada435598d24400be44b280e821987ea166e095950d130ee77a5fa&
|
||||
> ¡Oracle! A PPC of either type did sometimes cause generators assignment to switch on individual susb-systems if they were assigned to auto mode. I never noticed a generator actually going offline, but the switching of generator assignment suggests a generator had momentarily tripped offline.
|
||||
>
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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):
|
||||
> ¡Oracle! CONTRADICTS
|
||||
> ¡Oracle! I never recall seeing a projectile animation at all for an AFC. There was a muzzle flash and an impact flash. There was no tracer in my memory so there was no "pace". The projectiles were completely invisible as far as I'm concerned both from the firing mech's and the target mech's perspectives.
|
||||
> ¡Oracle! CAN'T RECALL AFC25 "short stroking" and not firing. I didn't use mechs equipped with those very often. There was a kickback when AFCs fired and it was relative to the caliber
|
||||
>
|
||||
>
|
||||
>
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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):
|
||||
> ¡Oracle! MATCHES
|
||||
> ¡Oracle! The Mischief was equipped with a Gauss rifle.
|
||||
>
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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.
|
||||
|
||||
> ¡Oracle! MATCHES the description and coming changes are in the direction I expect.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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):
|
||||
> ¡Oracle! MATCHES on nearby missile splash damage, but I never conducted a controlled test firing into terrain or nerfed up against a mech that wasn't shooting back.
|
||||
>
|
||||
>
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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):
|
||||
> ¡Oracle! MATCHES The racks on different mechs did appear to have a "personality" to them. The Thor and Madcat chassis tended to loft the missiles up. The Avatar tended to have a flatter trajectory. However range also seemed to play a factor with very long range shots sometimes lofting the missiles on a higher arc. The manueverability of the NARCs and STREAKS often caused an angry swarm of bees effect when the missiles came near a manuevering target at a high aspect angle.
|
||||
> ¡Oracle! CAN'T RECALL I never noticed a discrepancy on SRM/LRM racks, never paid attention to the score to notice there may have been an issue. I know that the mechs with twin LRM20s and LRM15s were particularly effective at causing crits even on a fairly fresh mech.
|
||||
> ¡Oracle! CAN'T RECALL the SRM Range pip, but it does agree with the 800m range published on the VWE site for v4.10
|
||||
>
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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):
|
||||
> ¡Oracle! CONTRADICTS there being heat in Veteran mode and the manual agrees with this. Expert mode was the only mode with heat.
|
||||
> ¡Oracle! A jammed round was ejected with a single tap of the eject button. No holding down was required.
|
||||
> ¡Oracle! I have never seen a non-damaged (x'd out) non-jammed weapon stop recycling and firing.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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):
|
||||
> ¡Oracle! There was only a missile warning horn sound, no other warning. This is the missile warning alarm from the sound board "[1:74] IncomingAlarm01 n58". Can be heard in this video: https://youtu.be/zcJ1Xf_le4Y?is=HbEp2KXO9OXw6kIX at 43 seconds. Even better in this video at 1:10 https://youtu.be/eWb4bZSRkWY?is=YUm8b0bThkWA_pZ6
|
||||
|
||||
>
|
||||
>
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
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.
|
||||
=============================================================================
|
||||
+49
-1
@@ -1,4 +1,5 @@
|
||||
#include <cstdlib>
|
||||
#include <map> // (BT411) the 28Hz smoother-intake clock (cadence census item 3)
|
||||
#include "munga.h"
|
||||
#pragma hdrstop
|
||||
|
||||
@@ -958,7 +959,54 @@ void
|
||||
|
||||
if (control_ID == controlID)
|
||||
{
|
||||
audioControlAverage.Add(control_value);
|
||||
// (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);
|
||||
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 ((controlID == 100 || controlID == 101) && s_sa++ < 3000)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#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"
|
||||
#pragma hdrstop
|
||||
|
||||
@@ -442,6 +444,45 @@ void
|
||||
);
|
||||
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_temp,
|
||||
head_entity->localToWorld
|
||||
@@ -473,6 +514,19 @@ void
|
||||
{
|
||||
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
|
||||
{
|
||||
|
||||
@@ -3786,6 +3786,11 @@ void BTStartWarpExpandPOV()
|
||||
// World-anchored warp (a PORT EXTENSION -- the authentic effect is POV only): an
|
||||
// OBSERVER seeing a peer respawn over THERE. No world mask (the observer is alive);
|
||||
// expand-reveal at the peer's world point.
|
||||
// *** RETIRED (#111, 2026-08-14): no caller. The field read the peer-visible
|
||||
// bubble as a wrong effect on the dying/respawning mech ("should just be the
|
||||
// big boom from other POV"); the authentic effect is the POV-only translocation,
|
||||
// and observers now see the un-wreck model swap itself (mechdmg.cpp). Kept as
|
||||
// documentation of the one-warp-slot guard. ***
|
||||
//
|
||||
void BTStartWarpEffect(float x, float y, float z)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
// _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_004ba828 = 00 00 00 00 = 0.0f (below-range clamp result)
|
||||
// _DAT_004ba830 = 00 00 00 00 = 0.0f (above-range clamp result)
|
||||
// _DAT_004ba830 = 00 00 00 00 00 00 f0 3f = DOUBLE 1.0 (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_004bac04 = 00 00 80 3f = 1.0f (outputVoltage "fully charged" test)
|
||||
// _DAT_004bac08 = 00 00 00 00 = 0.0f (firing dischargeTimer expiry threshold)
|
||||
@@ -498,21 +501,16 @@ void
|
||||
// generator + calibrated voltageScale/EC; TrackSeekVoltage integrates
|
||||
// the charge over the authored RechargeRate seconds).
|
||||
//
|
||||
// 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
|
||||
// POD-FRAME SUB-STEPPING (task #11): run the binary's own Loading
|
||||
// tick -- TrackSeekVoltage + ComputeOutputVoltage + the Loaded test --
|
||||
// at the binary's own frame rate. This also keeps the I^2R generator
|
||||
// feed integral accurate (big steps evaluate seekRate at a stale level
|
||||
// and OVER-heat the generators). Sub-step count is bounded; leftover
|
||||
// time under extreme throttling just resumes next frame.
|
||||
// at the binary's locked 60 fps. Justification: the I^2R generator
|
||||
// feed integral -- TrackSeekVoltage's seekRate^2 heat evaluated over
|
||||
// big variable-dt steps uses a stale level and OVER-heats the
|
||||
// generators. (The former "weapon-brick fix" rationale was FALSE --
|
||||
// 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;
|
||||
Scalar remaining = time_slice;
|
||||
@@ -531,32 +529,13 @@ void
|
||||
weaponAlarm.SetLevel(2); // -> Loaded
|
||||
break;
|
||||
}
|
||||
// OVERCHARGE RESCUE (issue #21, 2026-07-21) -- a DELIBERATE
|
||||
// divergence from the binary, which DEADLOCKS here: change the
|
||||
// seek gear while a charge is in flight and the level can land
|
||||
// ABOVE the new gear's snap window (level/seekV > 1.01) -- the
|
||||
// byte-verified ComputeOutputVoltage clamp then zeroes
|
||||
// rechargeLevel (_DAT_004ba830 = 0.0) and the ==1.0 Loaded test
|
||||
// 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;
|
||||
}
|
||||
// The old OVERCHARGE RESCUE (issue #21, 2026-07-21) lived here.
|
||||
// REMOVED 2026-08-13: it patched an artifact of the
|
||||
// _DAT_004ba830 width misread (double 1.0 dword-read as float
|
||||
// 0.0). With the faithful clamp in ComputeOutputVoltage an
|
||||
// overcharged weapon snaps rechargeLevel to exactly 1.0f, so
|
||||
// the ==1.0f test above takes it to Loaded naturally -- the
|
||||
// binary never deadlocked here and neither do we.
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -801,7 +780,17 @@ void
|
||||
}
|
||||
else if (rechargeLevel > 1.0f) // _DAT_004ba820
|
||||
{
|
||||
rechargeLevel = 0.0f; // _DAT_004ba830 (NB: also 0.0f)
|
||||
// #21 CORRECTION (2026-08-13, adversarial re-audit, raw-byte disasm):
|
||||
// _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,6 +53,8 @@
|
||||
#endif
|
||||
#include <JOINT.hpp> // Joint, JointSubsystem (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)
|
||||
# include <app.hpp>
|
||||
#endif
|
||||
@@ -478,8 +480,51 @@ void
|
||||
if (swayAngle > maxAnimationNoise) swayAngle = maxAnimationNoise;
|
||||
if (swayAngle < minAnimationNoise) swayAngle = minAnimationNoise;
|
||||
|
||||
IntegrateEyeJoint(time_slice); // FUN_004b2ec0
|
||||
IntegrateBody(time_slice); // FUN_004b30ec
|
||||
// (cadence census item 2, gotcha 32) THE INTEGRATOR CLOCK: the binary
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE (task #56, byte-verified): the binary Performance @004b275c ENDS here.
|
||||
// WriteEyeJoint/WriteMechJoint are NOT called from the gyro -- they are called
|
||||
@@ -659,6 +704,8 @@ void
|
||||
+ bodyOrientation.z*bodyOrientation.z;
|
||||
if (m2 > 1e-9f)
|
||||
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
|
||||
<< " " << (float)eyePosition.z
|
||||
<< " body=" << (float)bodyOrientation.x << " " << (float)bodyOrientation.y
|
||||
|
||||
@@ -1019,9 +1019,14 @@ protected:
|
||||
public:
|
||||
int MovementMode() { return (int)GetSimulationState(); }
|
||||
void SetMovementMode(int m) { SetSimulationState((unsigned)m); }
|
||||
// (AUDIO_FIDELITY F7) called by each homing Missile per MoveAndCollide
|
||||
// tick; PerformAndWatch latches the accumulators into the published
|
||||
// IncomingLock/DistanceToMissile attributes each frame.
|
||||
// (AUDIO_FIDELITY F7 / #83) called by each inbound Missile per
|
||||
// MoveAndCollide tick; PerformAndWatch latches the accumulators into
|
||||
// the published IncomingLock/DistanceToMissile attributes each frame.
|
||||
// [T3 micro-semantic] the binary (@004a9b5c) reads the FIRST member
|
||||
// of the mech's inbound socket list, not the minimum; the list's
|
||||
// insertion order is unresolved (Node/Plug layer), so we keep MIN --
|
||||
// observable only with 2+ simultaneous inbound missiles, where min
|
||||
// = nearest = fastest beep (the conservative reading).
|
||||
void ReportIncomingMissile(Scalar range)
|
||||
{
|
||||
incomingLockNext = 1;
|
||||
|
||||
+593
-45
@@ -159,6 +159,7 @@
|
||||
#include <matchlog.hpp> // MP match forensics (DEATH/PROJ/SPLASH/RAM event lines)
|
||||
#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 <map> // the ring 28Hz dt-accumulator registry (heat.cpp #119 pattern, census item 5)
|
||||
#if !defined(PLAYER_HPP)
|
||||
# include <player.hpp> // Player::VehicleDeadMessage -- the death->respawn notification (task #52)
|
||||
#endif
|
||||
@@ -817,6 +818,28 @@ void
|
||||
out = m->localOrigin.linearPosition; // safe non-garbage fallback (owner origin)
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// BTWeaponMountHeight -- the @004bc3fc ctor's muzzle-height operand (velocity/
|
||||
// dt integration audit 2026-08-13): the ProjectileWeapon ctor computes
|
||||
// launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleTransform.y)
|
||||
// (@004bc3fc part_013.c:16157-16164 -- FUN_004b9948 muzzle transform, then
|
||||
// FUN_0040a968 extracts the translation and local_5c = its Y). The binary
|
||||
// takes that Y at CTOR time = the bind-pose mount height; the port resolves
|
||||
// lazily at first use (ctor runs mid-roster-build), so return the mount's
|
||||
// height ABOVE THE MECH ORIGIN -- the same quantity for a standing mech,
|
||||
// and terrain-independent. Complete-Mech-TU bridge per the databinding rule.
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Scalar
|
||||
BTWeaponMountHeight(void *ownerMech, int segIndex)
|
||||
{
|
||||
Mech *m = (Mech *)ownerMech;
|
||||
if (m == 0)
|
||||
return 0.0f;
|
||||
Point3D p;
|
||||
BTResolveWeaponMuzzle(ownerMech, segIndex, p);
|
||||
return p.y - m->localOrigin.linearPosition.y;
|
||||
}
|
||||
|
||||
// First vital damage-zone index (Mech__DamageZone::vitalDamageZone is protected; Mech has access).
|
||||
int
|
||||
Mech::FirstVitalZone() const
|
||||
@@ -1746,11 +1769,46 @@ static void
|
||||
// autocannon could not miss a moving target. Track the entity for the
|
||||
// contact test; only GUIDED rounds also steer onto it.
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (p.target != 0 && BTIsRegisteredMech(p.target)
|
||||
&& !((Mech *)p.target)->IsMechDestroyed())
|
||||
if (p.target != 0 && BTIsRegisteredMech(p.target))
|
||||
{
|
||||
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
|
||||
p.targetPos.y += p.aimOffsetY;
|
||||
// #171 LOCK DROP predicate = @0049fb54 EXACTLY: movementMode 2||9,
|
||||
// the latched death modes -- NOT IsMechDestroyed(), whose extra
|
||||
// graphicAlarm>=9 term fires at the vital-kill TRIGGER, seconds
|
||||
// before the collapse (modes 5-8) reaches 2/9. The binary seeker
|
||||
// keeps TRACKING (and its 4.0u fuze stays live) all through the
|
||||
// fall; it releases only once the wreck latches disabled
|
||||
// (@004bef78: IsKindOf(Mech) && FUN_0049fb54 -> seeker+0xfc = 0).
|
||||
const int mm = ((Mech *)p.target)->MovementMode();
|
||||
if (!(mm == 2 || mm == 9))
|
||||
{
|
||||
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
|
||||
p.targetPos.y += p.aimOffsetY;
|
||||
}
|
||||
else if (p.guided)
|
||||
{
|
||||
// Seeker released: the round flies on UNGUIDED; only geometry
|
||||
// can detonate it now. (The old port kept steering to the
|
||||
// corpse's last position and could still sphere-contact it.)
|
||||
p.guided = 0;
|
||||
p.target = 0; // fuze disarmed with the lock
|
||||
}
|
||||
}
|
||||
// (#83/F7) THE INCOMING-MISSILE ALARM FEED lives HERE, in the live
|
||||
// flight simulator -- NOT in missile.cpp (the world-entity Missile is
|
||||
// a blocked transcription that never flies; the July F7 driver rode
|
||||
// it, which is why the alarm stayed silent in every field build).
|
||||
// Binary contract: ONE cluster Missile per trigger registers into its
|
||||
// target mech's inbound socket list at ctor (@004bf5b4 tail) and the
|
||||
// TARGET's tick reads it (@004a9b5c: lock=1, dist=|missile-mech|).
|
||||
// The salvo LEAD is the port's cluster (task #62), so lead-only =
|
||||
// one report per salvo = the binary's one registration; mirror leads
|
||||
// on peer nodes report identically (mislanch.cpp resolves the live
|
||||
// target by replicated ID), which is what puts the alarm in the
|
||||
// VICTIM's cockpit -- inbound missiles are always mirrors there.
|
||||
if (p.guided && p.salvoLead && p.target != 0)
|
||||
{
|
||||
extern void BTReportIncomingMissile(Entity *target, const Point3D &missile_position);
|
||||
BTReportIncomingMissile((Entity *)p.target, p.pos);
|
||||
}
|
||||
if (p.guided)
|
||||
{
|
||||
@@ -1782,28 +1840,74 @@ static void
|
||||
}
|
||||
}
|
||||
|
||||
// #84 THRUSTER BURN: the binary Missile hosts a MissileThruster whose
|
||||
// authored acceleration drives the round while BurnTime remains
|
||||
// (MISTHRST: acceleration = (0,0,-thrusterAccel) in the missile frame,
|
||||
// integrated by Missile::MoveAndCollide @4bef78). The pool flew at
|
||||
// CONSTANT |MuzzleVelocity| -- the rack-eject speed (SRM 100, LRM 30
|
||||
// u/s) -- which is why field missiles crawled (Oracle/Rajel, #84).
|
||||
// Authored (BTL4.RES type-15 missile models, +0x44/+0x48): SRM
|
||||
// burn 2.5s @ 600 u/s^2, LRM 10s @ 300, Streak 3s @ 300. Drag is
|
||||
// authored ~0.001 (negligible at combat ranges) -- integrate accel
|
||||
// along the current heading, un-damped, range-capped as before. [T1
|
||||
// values / T2 integration]
|
||||
if (p.burnLeft > 0.0f && p.accel > 0.0f)
|
||||
// #84 THRUSTER BURN + QUADRATIC DRAG (velocity/dt integration audit
|
||||
// 2026-08-13). Binary model [T1]: Mover::PerformAndWatch @00422360
|
||||
// ZEROES localAcceleration EVERY frame (`FUN_0040a7f4(+0x1dc,
|
||||
// &DAT_004e0fd4)` = the T0 MOVER.cpp:672 `localAcceleration =
|
||||
// Motion::Identity`), the MissileThruster's own Performance @004be474
|
||||
// (the standalone MissileThrusterSimulation body -- recovered by the
|
||||
// 2026-08-06 re-export) re-adds (0,0,-ThrusterAccel) while burning,
|
||||
// and Missile::MoveAndCollide @4bef78 adds the Mover quadratic drag
|
||||
// -COD*airDensity*sign(v)*v^2 per body axis, then integrates with dt
|
||||
// in SECONDS (@00421bac ApplyWorldAccelerations: v += a*dt). Net:
|
||||
// dv/dt = ThrusterAccel - COD*v^2 (burn; lateral vel zeroed)
|
||||
// -- a clean variable-step ODE. There is NO myomer-style 28Hz
|
||||
// per-tick accumulate in this lane: `speed += accel*dt` is the
|
||||
// faithful form and needs no (dt*28) rescale.
|
||||
// AUTHORED [T1, raw floats read from BTL4.RES type-15 records
|
||||
// 2026-08-13]: linear CODs = 0.001 on ALL axes of ALL four models
|
||||
// (srm rid=315, lrm 319, strk 323, nrk 327); airDensity = 1.0
|
||||
// (engine default, LATTICE.cpp:413). Drag is NOT negligible -- it is
|
||||
// the flight model's GOVERNOR: the Missile ctor @004bf5b4 precomputes
|
||||
// terminal speed sqrt(ThrusterAccel/negCOD.z) into missile+0x348 for
|
||||
// the seeker's lead time (part_013.c:18416). Terminal: LRM/Streak/
|
||||
// NRK 547.7 u/s, SRM 774.6. The old "drag ~0.001 negligible" note
|
||||
// (#84) let an LRM reach 3030 u/s by burnout -- 5.5x the binary cap
|
||||
// (sim: binary 492 u/s at 800u vs undamped 695; port t+15% to 800u).
|
||||
// Drag runs during burn AND coast (a burned-out round DECELERATES,
|
||||
// v(t)=v0/(1+COD*v0*t)); thruster rounds only -- the AC round is the
|
||||
// 0xBCD renderer tracer, a cosmetic with no Mover physics. Coast
|
||||
// gravity ported below (audit close 2026-08-13). Still unported
|
||||
// [T3 minor]: the binary's burnout end to velocity-slaving (lives
|
||||
// beyond practical impact ranges -- see WEAPONS_DRIFT_AUDIT
|
||||
// 'VELOCITY/DT INTEGRATION').
|
||||
if (p.accel > 0.0f)
|
||||
{
|
||||
Scalar burn_dt = (dt < p.burnLeft) ? dt : p.burnLeft;
|
||||
p.burnLeft -= burn_dt;
|
||||
Scalar ns = p.speed + p.accel * burn_dt;
|
||||
if (burn_dt > 0.0f)
|
||||
p.burnLeft -= burn_dt;
|
||||
Scalar ns = p.speed + p.accel * burn_dt
|
||||
- 0.001f * p.speed * p.speed * dt; // authored COD x airDensity 1.0 [T1]
|
||||
if (ns < 1.0f) ns = 1.0f; // guard: drag alone never reverses flight
|
||||
if (p.speed > 0.01f)
|
||||
{
|
||||
Scalar k = ns / p.speed;
|
||||
p.vel.x *= k; p.vel.y *= k; p.vel.z *= k;
|
||||
}
|
||||
p.speed = ns;
|
||||
// COAST GRAVITY (WEAPONS_DRIFT_AUDIT dt-table row closed
|
||||
// 2026-08-13). The binary applies the environment gravity
|
||||
// (default 6.5, FUN_00421e2c `vy -= g`) ONLY while COASTING --
|
||||
// the @4bef78 else-branch; during burn the thruster re-adds
|
||||
// thrust with NO gravity term onto the frame-zeroed accumulator
|
||||
// [T1, decomp-reference flight-model bullet]. It is a fresh
|
||||
// per-frame ACCELERATION (not an accumulate), so `vy -= 6.5*dt`
|
||||
// is the faithful integration at any frame rate. coast_dt is
|
||||
// this frame's non-burning remainder (exact at the burnout
|
||||
// boundary). Applies to EVERY thruster round, lock or no lock
|
||||
// -- it is Mover physics, not seeker logic (a lock-dropped
|
||||
// round coasts ballistic and now DROPS instead of flying level).
|
||||
Scalar coast_dt = dt - burn_dt;
|
||||
if (p.burnLeft <= 0.0f && coast_dt > 0.0f)
|
||||
{
|
||||
p.vel.y -= 6.5f * coast_dt; // env gravityConstant default [T1]
|
||||
Scalar sv = (Scalar)sqrtf((float)(p.vel.x*p.vel.x
|
||||
+ p.vel.y*p.vel.y + p.vel.z*p.vel.z));
|
||||
if (sv > 0.01f)
|
||||
p.speed = sv; // keep speed == |vel| (the step
|
||||
// length + normalizations below
|
||||
// divide by p.speed)
|
||||
}
|
||||
}
|
||||
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt;
|
||||
p.age += dt;
|
||||
@@ -1812,6 +1916,19 @@ static void
|
||||
// query every frame, FUN_0042291c, and DETONATES on geometry). Ray the
|
||||
// flight step against the terrain/cave solids -- a lofted round in a
|
||||
// low cavern bursts on the CEILING instead of punching through it.
|
||||
// (#175) TWO queries, earliest hit wins: the visual-heightfield march
|
||||
// (BTGroundRayHit) alone let rounds tunnel through everything that
|
||||
// blocks the WALK but is not heightfield -- the arena1-garage class
|
||||
// (open-questions: the visual mesh is not the collision truth), and
|
||||
// the field case that filed the ticket (LRM duel through a mound).
|
||||
// The STATIC COLLISION SOLID TREE (WorldStructurePick ->
|
||||
// Mover::FindStaticSolidHitBy) is the same static-world tail of the
|
||||
// engine collider FUN_0042291c swept, so the round now detonates on
|
||||
// exactly the geometry that stops a walking mech. A struck solid's
|
||||
// OWNING entity takes the direct dispatch (binary missile contact
|
||||
// @004be078 dispatches at the struck entity -- trucks/props are
|
||||
// missile-killable); plain terrain owners ignore it.
|
||||
// BT_WORLDHIT=0 restores the heightfield-only behavior (A/B lever).
|
||||
{
|
||||
extern bool BTGroundRayHit(float,float,float, float,float,float,
|
||||
float, float*,float*,float*);
|
||||
@@ -1822,8 +1939,47 @@ static void
|
||||
Vector3D rd;
|
||||
rd.x = p.vel.x/p.speed; rd.y = p.vel.y/p.speed; rd.z = p.vel.z/p.speed;
|
||||
float hx, hy, hz;
|
||||
int worldHit = 0; // 1 = heightfield, 2 = static solid
|
||||
Entity *structOwner = 0;
|
||||
if (BTGroundRayHit(prev.x, prev.y, prev.z, rd.x, rd.y, rd.z,
|
||||
step + 1.0f, &hx, &hy, &hz))
|
||||
worldHit = 1;
|
||||
{
|
||||
static int s_wh = -1;
|
||||
if (s_wh < 0)
|
||||
{
|
||||
const char *wv = getenv("BT_WORLDHIT");
|
||||
s_wh = (wv != 0 && *wv == '0') ? 0 : 1;
|
||||
}
|
||||
if (s_wh && p.shooter != 0 && BTIsRegisteredMech(p.shooter))
|
||||
{
|
||||
Point3D sHit;
|
||||
Entity *sOwn = 0;
|
||||
Vector3D sd(rd.x, rd.y, rd.z);
|
||||
if (((Mech *)p.shooter)->WorldStructurePick(prev, sd,
|
||||
step + 1.0f, &sHit, &sOwn))
|
||||
{
|
||||
// earlier than the heightfield hit (or the only hit)?
|
||||
int take = 1;
|
||||
if (worldHit == 1)
|
||||
{
|
||||
const float gd2 = (hx-prev.x)*(hx-prev.x)
|
||||
+ (hy-prev.y)*(hy-prev.y) + (hz-prev.z)*(hz-prev.z);
|
||||
const float sd2 = (float)((sHit.x-prev.x)*(sHit.x-prev.x)
|
||||
+ (sHit.y-prev.y)*(sHit.y-prev.y)
|
||||
+ (sHit.z-prev.z)*(sHit.z-prev.z));
|
||||
take = (sd2 < gd2);
|
||||
}
|
||||
if (take)
|
||||
{
|
||||
worldHit = 2;
|
||||
structOwner = sOwn;
|
||||
hx = (float)sHit.x; hy = (float)sHit.y; hz = (float)sHit.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (worldHit != 0)
|
||||
{
|
||||
// burst on the rock: the round's own DETONATION (the binary
|
||||
// missile detonates on ANY geometry, @004bef78) + a tight
|
||||
@@ -1851,12 +2007,40 @@ static void
|
||||
BTApplySplashDamage(p.shooter, p.weaponSubsys, hp, 0, sdmg);
|
||||
}
|
||||
}
|
||||
// (#175) a struck STRUCTURE's owning entity takes the direct
|
||||
// dispatch, exactly as the binary missile contact does
|
||||
// (@004be078: zone=-1 TakeDamageMessage straight at the
|
||||
// struck entity, no class test) -- missiles kill trucks/
|
||||
// props. Plain terrain owners have no zones and ignore it.
|
||||
int ownerDispatched = 0;
|
||||
if (worldHit == 2 && structOwner != 0
|
||||
&& !BTIsRegisteredMech(structOwner)
|
||||
&& structOwner->damageZoneCount > 0
|
||||
&& p.damage > 0.0f)
|
||||
{
|
||||
ownerDispatched = 1;
|
||||
Damage wdmg;
|
||||
wdmg.damageType = (Enumeration)p.damageType;
|
||||
wdmg.damageAmount = p.damage;
|
||||
wdmg.burstCount = 1;
|
||||
wdmg.impactPoint.x = hx; wdmg.impactPoint.y = hy;
|
||||
wdmg.impactPoint.z = hz;
|
||||
Entity::TakeDamageMessage take_damage(
|
||||
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||
(p.shooter != 0) ? p.shooter->GetEntityID() : EntityID::Null,
|
||||
-1 /*icon handler maps -1 -> zone 0*/, wdmg);
|
||||
structOwner->Dispatch(&take_damage);
|
||||
}
|
||||
extern void BTPfxTrailPuff(int, float, float, float, float, float, float, int);
|
||||
for (int pf = 0; pf < 4; ++pf)
|
||||
BTPfxTrailPuff(0, hx, hy, hz, -rd.x, -rd.y, -rd.z, 2);
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] WORLD burst at(" << hx << ","
|
||||
<< hy << "," << hz << ")" << std::endl;
|
||||
DEBUG_STREAM << "[projectile] WORLD burst ("
|
||||
<< ((worldHit == 2) ? "struct" : "terrain")
|
||||
<< (ownerDispatched ? " owner-dispatched" : "")
|
||||
<< ") at(" << hx << "," << hy << "," << hz << ")"
|
||||
<< " dmg=" << p.damage << " lead=" << p.salvoLead
|
||||
<< std::endl;
|
||||
p.active = 0;
|
||||
continue;
|
||||
}
|
||||
@@ -1892,25 +2076,154 @@ static void
|
||||
// detonation being flung past the target at high speed.
|
||||
Point3D hitPos = p.pos;
|
||||
Scalar contactD2;
|
||||
Scalar tTgt = 0.0f; // target's nearest-approach segment param
|
||||
// (the bystander sweep compares against it)
|
||||
{
|
||||
const Scalar sx = p.pos.x - prev.x, sy = p.pos.y - prev.y, sz = p.pos.z - prev.z;
|
||||
const Scalar seg2 = sx*sx + sy*sy + sz*sz;
|
||||
Scalar t = 0.0f;
|
||||
if (seg2 > 1.0e-6f)
|
||||
{
|
||||
const Scalar wx = p.targetPos.x - prev.x,
|
||||
wy = p.targetPos.y - prev.y,
|
||||
wz = p.targetPos.z - prev.z;
|
||||
t = (wx*sx + wy*sy + wz*sz) / seg2;
|
||||
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
|
||||
tTgt = (wx*sx + wy*sy + wz*sz) / seg2;
|
||||
if (tTgt < 0.0f) tTgt = 0.0f; else if (tTgt > 1.0f) tTgt = 1.0f;
|
||||
}
|
||||
hitPos.x = prev.x + t*sx; hitPos.y = prev.y + t*sy; hitPos.z = prev.z + t*sz;
|
||||
hitPos.x = prev.x + tTgt*sx; hitPos.y = prev.y + tTgt*sy; hitPos.z = prev.z + tTgt*sz;
|
||||
const Scalar cx = p.targetPos.x - hitPos.x,
|
||||
cy = p.targetPos.y - hitPos.y,
|
||||
cz = p.targetPos.z - hitPos.z;
|
||||
contactD2 = cx*cx + cy*cy + cz*cz;
|
||||
}
|
||||
const int contact = (contactD2 < (10.0f*10.0f));
|
||||
// BYSTANDER SWEEP (WEAPONS_DRIFT_AUDIT lane 12, closed 2026-08-13).
|
||||
// The binary's per-tick contact test is a WORLD sweep (FUN_0042291c,
|
||||
// called from Missile::MoveAndCollide @004bef78): every solid in the
|
||||
// flight path can detonate the round -- including a third mech between
|
||||
// shooter and target [T1]. The port pool tested only the locked
|
||||
// target sphere + the terrain ray, so a mech standing in someone
|
||||
// else's crossfire was flown through (splash alone could touch it).
|
||||
// Port shape [T3 -- radius approximation of the real solid sweep]:
|
||||
// nearest-approach of this frame's flight segment vs a VERTICAL
|
||||
// CAPSULE per other registered mech. The capsule is derived from the
|
||||
// mech's OWN collision template (Mover::GetCollisionTemplate, the same
|
||||
// BoxedSolid whose maxY is CylinderReferenceHeight): axis = mech
|
||||
// origin up template minY..maxY, radius = the template's larger
|
||||
// horizontal half-extent (the axis ignores the template's near-zero
|
||||
// horizontal center offset, so no yaw rotation is needed). Fallback
|
||||
// when the template is unresolved: 3.5u x 14u [T3 fixed constants,
|
||||
// fielded-mech scale -- CylinderReferenceHeight ~14]. Wrecks stay in
|
||||
// the sweep: the binary detonates on ANY solid, and the victim-side
|
||||
// zone-state guard (#174) keeps a dead zone from cascading or scoring.
|
||||
// Cheap by construction: damage-carrying GUIDED rounds only (= the one
|
||||
// salvo lead; the N-1 visual tracers are damage-0), per-mech
|
||||
// distance cull, shooter excluded by the registry helper, locked
|
||||
// target excluded (its own 4.0u fuze above is the authentic test).
|
||||
Entity *bysMech = 0;
|
||||
Scalar bysT = 2.0f;
|
||||
Point3D bysHit = p.pos;
|
||||
if (p.guided && p.damage > 0.0f)
|
||||
{
|
||||
extern int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut);
|
||||
Entity *cand[32];
|
||||
const int nc = BTGetTargetCandidates(p.shooter, cand, 32); // excludes the shooter
|
||||
const Scalar sx = p.pos.x - prev.x, sy = p.pos.y - prev.y,
|
||||
sz = p.pos.z - prev.z;
|
||||
const Scalar seg2 = sx*sx + sy*sy + sz*sz;
|
||||
const Scalar step = p.speed * dt;
|
||||
for (int ci = 0; ci < nc && seg2 > 1.0e-6f; ++ci)
|
||||
{
|
||||
Entity *e = cand[ci];
|
||||
if (e == 0 || e == p.target || !BTIsRegisteredMech(e))
|
||||
continue;
|
||||
Mech *m = (Mech *)e;
|
||||
Point3D mp = m->localOrigin.linearPosition;
|
||||
// distance cull: farther than this frame's step + the largest
|
||||
// plausible capsule reach -> untouchable this frame
|
||||
const Scalar cdx = mp.x - prev.x, cdz = mp.z - prev.z;
|
||||
const Scalar cull = step + 24.0f;
|
||||
if (cdx*cdx + cdz*cdz > cull*cull)
|
||||
continue;
|
||||
Scalar rr, cy0, cy1;
|
||||
BoxedSolid *tmpl = m->GetCollisionTemplate();
|
||||
if (tmpl != 0 && tmpl->maxY > tmpl->minY)
|
||||
{
|
||||
const Scalar hx = 0.5f * (tmpl->maxX - tmpl->minX);
|
||||
const Scalar hz = 0.5f * (tmpl->maxZ - tmpl->minZ);
|
||||
rr = (hx > hz) ? hx : hz;
|
||||
cy0 = mp.y + tmpl->minY;
|
||||
cy1 = mp.y + tmpl->maxY;
|
||||
}
|
||||
else
|
||||
{
|
||||
rr = 3.5f; cy0 = mp.y; cy1 = mp.y + 14.0f; // [T3] fixed fallback
|
||||
}
|
||||
if (!(rr > 0.5f)) rr = 0.5f; // degenerate/garbage template
|
||||
if (rr > 10.0f) rr = 10.0f; // guards (NaN falls to 0.5)
|
||||
// closest points between the flight segment prev+t*(sx,sy,sz)
|
||||
// and the vertical axis segment (mp.x, cy0..cy1, mp.z) --
|
||||
// standard segment-segment closest point (RTCD 5.1.9 with
|
||||
// d2 = (0,h,0))
|
||||
const Scalar h = cy1 - cy0;
|
||||
const Scalar rx = prev.x - mp.x, ry = prev.y - cy0,
|
||||
rz = prev.z - mp.z;
|
||||
const Scalar ee = h*h;
|
||||
const Scalar f = h*ry;
|
||||
const Scalar c = sx*rx + sy*ry + sz*rz;
|
||||
const Scalar b = sy*h;
|
||||
const Scalar den = seg2*ee - b*b;
|
||||
Scalar t = (den > 1.0e-6f) ? (b*f - c*ee) / den : 0.0f;
|
||||
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
|
||||
Scalar s = (ee > 1.0e-6f) ? (b*t + f) / ee : 0.0f;
|
||||
if (s < 0.0f) { s = 0.0f; t = -c / seg2; }
|
||||
else if (s > 1.0f) { s = 1.0f; t = (b - c) / seg2; }
|
||||
if (t < 0.0f) t = 0.0f; else if (t > 1.0f) t = 1.0f;
|
||||
const Scalar qx = prev.x + t*sx - mp.x;
|
||||
const Scalar qy = prev.y + t*sy - (cy0 + s*h);
|
||||
const Scalar qz = prev.z + t*sz - mp.z;
|
||||
if (qx*qx + qy*qy + qz*qz < rr*rr && t < bysT)
|
||||
{
|
||||
bysT = t;
|
||||
bysMech = e;
|
||||
bysHit.x = prev.x + t*sx;
|
||||
bysHit.y = prev.y + t*sy;
|
||||
bysHit.z = prev.z + t*sz;
|
||||
}
|
||||
}
|
||||
}
|
||||
// #171 FUZE RADIUS: a GUIDED damage round detonates at the binary's
|
||||
// proximity fuze _DAT_004bf5a4 = 4.0 (seeker rangeToTarget +0x10C <
|
||||
// 4.0, @004bef78 -- the SAME constant the old steering comment
|
||||
// misattributed as "MissileTurnGain"; the 4.0/8.0 turn rates above are
|
||||
// PORT TUNING [T3], not that constant). Cosmetic rounds (damage 0:
|
||||
// AC tracers, cluster visuals) keep the wider 10u so their burst
|
||||
// visuals still read at speed.
|
||||
const Scalar fuzeR = (p.guided && p.damage > 0.0f) ? 4.0f : 10.0f;
|
||||
int contact = (contactD2 < fuzeR * fuzeR);
|
||||
Entity *victim = p.target;
|
||||
if (bysMech != 0 && (!contact || bysT < tTgt))
|
||||
{
|
||||
// a third mech sits EARLIER on this frame's flight segment than
|
||||
// the locked target's fuze point -- it takes the round (the
|
||||
// binary's sweep detonates on the FIRST solid in the path). The
|
||||
// struck mech becomes the DIRECT victim; the salvo cluster roll
|
||||
// and the splash package below run unchanged against it.
|
||||
contact = 1;
|
||||
victim = bysMech;
|
||||
hitPos = bysHit;
|
||||
static int s_bysPrints = 0; // capped receipt (field forensics)
|
||||
if (s_bysPrints < 24)
|
||||
{
|
||||
++s_bysPrints;
|
||||
DEBUG_STREAM << "[projectile] BYSTANDER id="
|
||||
<< (int)bysMech->GetEntityID()
|
||||
<< " t=" << bysT
|
||||
<< " at(" << bysHit.x << "," << bysHit.y << "," << bysHit.z
|
||||
<< ") dmg=" << p.damage
|
||||
<< " lockedTgt=" << (void *)p.target
|
||||
<< ((s_bysPrints == 24) ? " (receipt cap reached)" : "")
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
|
||||
{
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
@@ -1935,10 +2248,13 @@ static void
|
||||
<< "," << p.targetPos.z << ")" << std::endl;
|
||||
if (p.salvoLead) // ONE Explosion per salvo (@004bcc60)
|
||||
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hitPos);
|
||||
Entity *tgt = p.target;
|
||||
// Deliver to the projectile's target mech -- the launcher set p.target
|
||||
// from the shooter's 0x388 slot (the picked victim; any peer mech in
|
||||
// MP, task #46). A replicant target reroutes cross-pod via Dispatch.
|
||||
Entity *tgt = victim;
|
||||
// Deliver to the struck mech -- normally the launcher's locked
|
||||
// target (the shooter's 0x388 slot; any peer mech in MP, task #46),
|
||||
// or the BYSTANDER the sweep above found first on the flight
|
||||
// segment (lane 12: it becomes the direct victim, exactly as if it
|
||||
// had been the lock). A replicant victim reroutes cross-pod via
|
||||
// Dispatch.
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (tgt != 0 && BTIsRegisteredMech(tgt) && p.damage > 0.0f)
|
||||
{
|
||||
@@ -1978,9 +2294,22 @@ static void
|
||||
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
|
||||
if (bursts > 1)
|
||||
{
|
||||
// #171: the binary roll (part_013.c:18272-18281) has NO
|
||||
// floor-of-1 -- rolled = min(Random(n) + n/4, n), and
|
||||
// Random(n) is 0..n-1, so n<4 can roll ZERO. The
|
||||
// VICTIM side is what floors it: the binary burst loop
|
||||
// is a DO-WHILE (part_012.c:14660-14685 -- local_2c =
|
||||
// burstCount, apply, decrement, `if (local_2c < 1)
|
||||
// break` AFTER the body), so a 0-burst message still
|
||||
// applies ONE burst; mech.cpp:1229's `burstsLeft < 1
|
||||
// -> 1` guard is the faithful mirror of that shape.
|
||||
// The old sender-side floor made the end-to-end count
|
||||
// identical, but shipped a message field the binary
|
||||
// never sends (burstCount 1 where the binary says 0);
|
||||
// roll it exactly as the binary does and let the
|
||||
// receiver's do-while supply the floor.
|
||||
int rolled = Random(bursts) + (bursts >> 2);
|
||||
if (rolled > bursts) rolled = bursts;
|
||||
if (rolled < 1) rolled = 1;
|
||||
bursts = rolled;
|
||||
}
|
||||
dmg.burstCount = bursts;
|
||||
@@ -6356,12 +6685,59 @@ void
|
||||
targetReticle.rayIntersection = farPt;
|
||||
rlPickClass = 4; // BT_RANGE_LOG: max-range default (issue #4)
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
MECH_TARGET_ENTITY(this) = 0; // no world sentinel -> no target
|
||||
MECH_TARGET_SUBIDX(this) = -1;
|
||||
}
|
||||
|
||||
// BENCH DESIGNATOR (#83 rig, env-gated, off by default):
|
||||
// BT_DESIGNATE=enemy overrides the frame's designation with the
|
||||
// nearest live mech whenever the boresight pick found NO mech --
|
||||
// a standoff missile duel needs locks at ranges where the enemy
|
||||
// subtends too small an angle for the self-driven aim to hold.
|
||||
// A real mech pick still wins (this fills no-mech frames only),
|
||||
// and the stamped state is exactly the production designation
|
||||
// (entity slot + aim point + zone -1), so everything downstream
|
||||
// -- fire gates, seeker, incoming-alarm feed -- runs authentic.
|
||||
// The HUD reticle is left alone (no fake lock ring).
|
||||
{
|
||||
static int s_desig = -1;
|
||||
if (s_desig < 0)
|
||||
{
|
||||
const char *dv = getenv("BT_DESIGNATE");
|
||||
s_desig = (dv != 0 && !stricmp(dv, "enemy")) ? 1 : 0;
|
||||
}
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (s_desig == 1
|
||||
&& (pickTarget == 0 || !BTIsRegisteredMech(pickTarget)))
|
||||
{
|
||||
extern int BTGetTargetCandidates(Entity *shooter, Entity **out, int maxOut);
|
||||
Entity *ec[32];
|
||||
const int enc = BTGetTargetCandidates((Entity *)this, ec, 32);
|
||||
float best = 1e30f;
|
||||
Mech *bm = 0;
|
||||
for (int ei = 0; ei < enc; ++ei)
|
||||
{
|
||||
Mech *em = (Mech *)ec[ei];
|
||||
if (em == 0 || em->IsMechDestroyed()) continue;
|
||||
float edx = (float)em->localOrigin.linearPosition.x
|
||||
- (float)localOrigin.linearPosition.x;
|
||||
float edz = (float)em->localOrigin.linearPosition.z
|
||||
- (float)localOrigin.linearPosition.z;
|
||||
float ed2 = edx*edx + edz*edz;
|
||||
if (ed2 < best) { best = ed2; bm = em; }
|
||||
}
|
||||
if (bm != 0)
|
||||
{
|
||||
MECH_TARGET_ENTITY(this) = (Entity *)bm;
|
||||
MECH_TARGET_SUBIDX(this) = -1;
|
||||
MECH_TARGET_POS(this) = bm->localOrigin.linearPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [parallax] diag (issue #16): project the pick point back into the
|
||||
// RENDER view -- the frame the crosshair is drawn in. Reticle +y is
|
||||
// down, so ry > 0 = the pick lands BELOW the crosshair. A healthy
|
||||
@@ -6585,10 +6961,16 @@ void
|
||||
// there, moving caret gone". Re-seed instead of propagating.
|
||||
if (!(trueRange == trueRange) || !(sShownRange == sShownRange))
|
||||
{
|
||||
if (getenv("BT_RANGE_LOG"))
|
||||
// #147: ALWAYS-ON (capped) -- the trap is the ONLY evidence
|
||||
// that can name the NaN source in a field log, and Friday's
|
||||
// fleet does not set BT_RANGE_LOG. Fires once per poisoning
|
||||
// (the re-seed clears it); the #172 rare-forensic precedent.
|
||||
static int s_nanPrints = 0;
|
||||
if (s_nanPrints < 20 && ++s_nanPrints)
|
||||
DEBUG_STREAM << "[range] NaN TRAPPED (true=" << trueRange
|
||||
<< " shown=" << sShownRange << ") -- re-seeded to 1200\n"
|
||||
<< std::flush;
|
||||
<< " shown=" << sShownRange << ") -- re-seeded to 1200"
|
||||
<< ((s_nanPrints == 20) ? " (cap reached)" : "")
|
||||
<< "\n" << std::flush;
|
||||
trueRange = 1200.0f;
|
||||
sShownRange = 1200.0f;
|
||||
}
|
||||
@@ -7604,6 +7986,98 @@ void
|
||||
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
|
||||
// secondary-schematic cycle (Damage -> Critical -> Heat -> Damage) at
|
||||
// the given frame and every 300 frames after -- with
|
||||
@@ -8464,15 +8938,60 @@ void
|
||||
// footstep smoother fed (fed per-stride only, it took 10-20 s to warm
|
||||
// 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 rdy = localOrigin.linearPosition.y - accelPrevPos.y;
|
||||
Scalar rdz = localOrigin.linearPosition.z - accelPrevPos.z;
|
||||
accelPrevPos = localOrigin.linearPosition;
|
||||
if (dt > 1.0e-4f)
|
||||
if (ringDt > 1.0e-4f)
|
||||
{
|
||||
velRingFwd[velRingCursor] = (Scalar)sqrtf(rdx * rdx + rdz * rdz) / dt;
|
||||
velRingVert[velRingCursor] = rdy / dt;
|
||||
velRingDt[velRingCursor] = dt;
|
||||
velRingFwd[velRingCursor] = (Scalar)sqrtf(rdx * rdx + rdz * rdz) / ringDt;
|
||||
velRingVert[velRingCursor] = rdy / ringDt;
|
||||
velRingDt[velRingCursor] = ringDt;
|
||||
velRingCursor = (velRingCursor + 1) % 15;
|
||||
}
|
||||
Scalar fwdMean = 0.0f, vertMean = 0.0f, dtMean = 0.0f;
|
||||
@@ -8492,12 +9011,27 @@ void
|
||||
-((fwdMean - accelPrevFwdMean) / dtMean));
|
||||
accelPrevFwdMean = fwdMean;
|
||||
accelPrevVertMean = vertMean;
|
||||
}
|
||||
}
|
||||
|
||||
// (AUDIO_FIDELITY F7) latch the incoming-missile report accumulated by
|
||||
// Missile::MoveAndCollide since our last frame, then re-arm. The authored
|
||||
// beeper matches incomingLock 1/0 and the tempo scale reads the range --
|
||||
// both polled by ExecuteWatchers below.
|
||||
// (#83 receipt) BT_INCOMING_LOG: lock edges always; ~1Hz while locked.
|
||||
if (getenv("BT_INCOMING_LOG"))
|
||||
{
|
||||
static int s_inCount = 0;
|
||||
if ((incomingLock == 0) != (incomingLockNext == 0))
|
||||
DEBUG_STREAM << "[incoming] " << (incomingLockNext ? "LOCK" : "clear")
|
||||
<< " mech=" << (void *)this
|
||||
<< " dist=" << (float)distanceToMissileNext
|
||||
<< " t=" << (GetTickCount() % 1000000) << "\n" << std::flush;
|
||||
else if (incomingLockNext != 0 && (s_inCount++ % 60) == 0)
|
||||
DEBUG_STREAM << "[incoming] held mech=" << (void *)this
|
||||
<< " dist=" << (float)distanceToMissileNext
|
||||
<< " t=" << (GetTickCount() % 1000000) << "\n" << std::flush;
|
||||
}
|
||||
incomingLock = incomingLockNext;
|
||||
distanceToMissile = distanceToMissileNext;
|
||||
incomingLockNext = 0;
|
||||
@@ -9350,18 +9884,32 @@ void
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// BTReportIncomingMissile -- complete-type bridge (F7)
|
||||
// BTReportIncomingMissile -- complete-type bridge (F7/#83)
|
||||
//
|
||||
// Missile::MoveAndCollide (missile.cpp, forward-declared Mech only) reports
|
||||
// its target + range each tick; the missile-alarm attributes latch in
|
||||
// Mech::PerformAndWatch. Seeker targets are always mechs in this game
|
||||
// (the launcher's lock); null-guarded.
|
||||
// its CONSTRUCTION target each tick; the missile-alarm attributes latch in
|
||||
// Mech::PerformAndWatch. Binary contract (@004bf5b4 ctor registration +
|
||||
// @004a9b5c target-tick read): the ctor class-checks the target as a Mech
|
||||
// (vs 0x50bdb4) before the socket Add -- seeker targets are always mechs
|
||||
// in this game; distance = |missilePos - mechPos| from the ENTITY
|
||||
// positions (not the seeker lead); a dead target's inbound list is
|
||||
// emptied on the death path (states 9/10), mirrored here as a skip.
|
||||
//###########################################################################
|
||||
void BTReportIncomingMissile(Entity *target, Scalar range)
|
||||
void BTReportIncomingMissile(Entity *target, const Point3D &missile_position)
|
||||
{
|
||||
if (target != 0)
|
||||
{
|
||||
((Mech *)target)->ReportIncomingMissile(range);
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (target != 0 && BTIsRegisteredMech(target)) // the binary class-checks the
|
||||
{ // ctor target vs Mech (0x50bdb4)
|
||||
Mech *mech = (Mech *)target;
|
||||
const int mm = mech->MovementMode();
|
||||
if (mm == 2 || mm == 9) // the port's latched death modes
|
||||
return; // (@0049fb54 predicate) -- the
|
||||
// binary empties the inbound list
|
||||
// on the death path
|
||||
Vector3D to_missile;
|
||||
to_missile.Subtract(missile_position,
|
||||
mech->localOrigin.linearPosition);
|
||||
mech->ReportIncomingMissile(to_missile.Length());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1387,7 +1387,6 @@ void
|
||||
&& owner->GetInstance() == Entity::ReplicantInstance)
|
||||
{
|
||||
extern void BTRebuildMechModel(Entity *entity);
|
||||
extern void BTStartWarpEffect(float x, float y, float z);
|
||||
// #129 (fresh spawns smoking, peers' screens only): the wreck plume
|
||||
// is re-armed LOCALLY on every node (entity-attached, 10s windows),
|
||||
// and only the master's Mech::Reset ran the @004d0c14 effect
|
||||
@@ -1397,13 +1396,20 @@ void
|
||||
extern void BTStopEntityPfx(void *entity_v);
|
||||
BTStopEntityPfx((void *)owner);
|
||||
BTRebuildMechModel((Entity *)owner);
|
||||
BTStartWarpEffect((float)owner->localOrigin.linearPosition.x,
|
||||
(float)owner->localOrigin.linearPosition.y,
|
||||
(float)owner->localOrigin.linearPosition.z);
|
||||
// (#111) the peer-visible warp sphere that used to fire here is
|
||||
// RETIRED. It was a deliberate port extension (160b78e, "observer
|
||||
// sees peer un-wreck") -- but the authentic effect is POV-ONLY
|
||||
// (POVTranslocateRenderable, the dying pilot's own translocation;
|
||||
// btl4vid.cpp's banner says so twice), and the field read the blue
|
||||
// bubble on another mech as a wrong effect ("should just be the
|
||||
// big boom from other POV" -- Lynx, night 9, confirmed). The
|
||||
// observer sees exactly what the pods showed: the un-wreck model
|
||||
// swap itself. This also retires the one-slot contention skip
|
||||
// (a peer landing mid-POV-warp no longer competes for the slot).
|
||||
// #108 forensics: UNGATED -- one line per peer respawn; a field
|
||||
// night's ghost triage needs these without asking players for envs.
|
||||
DEBUG_STREAM << "[respawn] replicant " << owner->GetEntityID()
|
||||
<< " un-wrecked + warp (mode "
|
||||
<< " un-wrecked (mode "
|
||||
<< oldMode << "->" << mode << ") at ("
|
||||
<< owner->localOrigin.linearPosition.x << ","
|
||||
<< owner->localOrigin.linearPosition.z << ")\n" << std::flush;
|
||||
|
||||
@@ -365,7 +365,17 @@ MechControlsMapper::MechControlsMapper(
|
||||
lookRight = 0;
|
||||
lookDown = 0;
|
||||
lookBehind = 0;
|
||||
lookState = LookNone;
|
||||
lookState = -1; // IMPOSSIBLE state (#179): the commit below fires
|
||||
// only on lookState EDGES, and both cells started
|
||||
// equal (LookNone) -- so the spawn state was never
|
||||
// COMMITTED: the reticle's pip GROUP mask kept its
|
||||
// construction bits and the Nova showed its REAR
|
||||
// laser's pips in the forward view until the first
|
||||
// look toggle (Oracle, night 17). Seeding -1 makes
|
||||
// the mapper's first tick an edge into LookNone,
|
||||
// stamping the forward view exactly as the machine
|
||||
// showed it at spawn (weapon enables were already
|
||||
// ctor-faithful: viewFireEnable = !rearFiring).
|
||||
|
||||
torsoUp = 0;
|
||||
torsoDown = 0;
|
||||
|
||||
@@ -235,15 +235,28 @@ void Missile::MoveAndCollide(Scalar time_slice)
|
||||
if (thruster != 0)
|
||||
thruster->MissileThrusterSimulation(time_slice); // bleed burnTimeRemaining
|
||||
|
||||
// (AUDIO_FIDELITY F7) report the threat to the homing target: the mech's
|
||||
// IncomingLock/DistanceToMissile attributes (binary @0x3fc/0x400) drive
|
||||
// the authored missile-alarm beeper whose TEMPO rises as we close.
|
||||
if (seeker != 0 && seeker->targetEntity != 0 && (simulationFlags & 1) == 0)
|
||||
// (AUDIO_FIDELITY F7 / #83) *** TRANSCRIPTION ONLY -- THIS NEVER RUNS ***
|
||||
// The world-entity Missile is blocked (2007 engine Entity-base mismatch);
|
||||
// live flight is the BTPushProjectile simulator, and the WORKING alarm
|
||||
// feed is there (mech4.cpp, the salvo-lead report in the push update
|
||||
// loop). The July F7 driver lived only here -- a month of field silence
|
||||
// (#83) because the reporter rode a class that never flies. Kept for
|
||||
// the day the entity unblocks:
|
||||
// report the threat to the CONSTRUCTION target,
|
||||
// not the seeker's live target: the binary registers the missile into the
|
||||
// target mech's inbound socket list in the ctor tail (@004bf5b4:
|
||||
// this+0x34c class-checked as Mech, then target+0x418 list Add) and the
|
||||
// TARGET'S OWN tick (@004a9b5c) reads that list -- IncomingLock=1 +
|
||||
// DistanceToMissile = |missilePos - mechPos| (entity positions, NOT the
|
||||
// seeker lead point). Registration survives a seeker lock-drop; it ends
|
||||
// only with the missile (socket weak-ref) or the target's death (the
|
||||
// death path walks + empties the list -- the bridge mirrors that with a
|
||||
// dead-target skip). The old seeker-gated lead-point report understated
|
||||
// range and went silent on lock-drop.
|
||||
if (targetConnection != 0 && (simulationFlags & 1) == 0)
|
||||
{
|
||||
extern void BTReportIncomingMissile(Entity *target, Scalar range);
|
||||
Vector3D to_target;
|
||||
to_target.Subtract(seeker->targetPosition, localOrigin.linearPosition);
|
||||
BTReportIncomingMissile(seeker->targetEntity, to_target.Length());
|
||||
extern void BTReportIncomingMissile(Entity *target, const Point3D &missile_position);
|
||||
BTReportIncomingMissile((Entity *)targetConnection, localOrigin.linearPosition);
|
||||
}
|
||||
|
||||
// --- guidance: build steering toward the Seeker's lead point ----------
|
||||
|
||||
@@ -14,11 +14,21 @@
|
||||
//
|
||||
// Coverage:
|
||||
// confident : ctor @004be7c4, dtor thunk @004be8bc, TestInstance @004be8e8,
|
||||
// CreateStreamedSubsystem @004bf8ec
|
||||
// best-effort: MissileThrusterSimulation -- the Performance method pointer is
|
||||
// installed by the ctor (PTR_LAB_00512b20) but its standalone body
|
||||
// is folded into the host Missile::MoveAndCollide @004bef78 (which
|
||||
// samples the thruster acceleration buffer at missile+0x234/+0x250).
|
||||
// CreateStreamedSubsystem @004bf8ec,
|
||||
// MissileThrusterSimulation @004be474 -- the STANDALONE
|
||||
// Performance body, RECOVERED by the 2026-08-06 re-export
|
||||
// (the old "folded into Missile::MoveAndCollide, no distinct
|
||||
// @ADDR survives" note here was stale). @004be474 does BOTH
|
||||
// jobs each frame: (1) slerp the owner Missile's orientation
|
||||
// toward the Seeker aim point (+0xE4), rate-capped at
|
||||
// maxThrusterRotationRate*dt; (2) while ThrusterBurning:
|
||||
// burnTimeRemaining -= dt and owner->localAcceleration +=
|
||||
// this->acceleration (0,0,-thrusterAccel) -- onto the
|
||||
// accumulator Mover::PerformAndWatch @00422360 zeroes every
|
||||
// frame, i.e. a constant acceleration, dt-correct. The
|
||||
// simplified body below remains the dormant-TU stand-in;
|
||||
// the LIVE port path is the mech4.cpp pool (see
|
||||
// docs/WEAPONS_DRIFT_AUDIT.md 'VELOCITY/DT INTEGRATION').
|
||||
// excluded : the 0x41xxxx engine vtable slots (pure Subsystem base behaviour;
|
||||
// vtable @00512bbc overrides only slot0 = destructor)
|
||||
//
|
||||
|
||||
+148
-30
@@ -28,11 +28,14 @@
|
||||
// ProjectileWeaponSimulation @004bbd04 (Performance -- FULLY RECOVERED
|
||||
// by capstone disasm, Gitea #12 2026-07-19; see context/decomp-reference.md
|
||||
// s5 -- the old RivetGun-modeled body is retired).
|
||||
// best-effort (vtable slot proven, function prologue present, BODY NOT
|
||||
// recovered by the decompiler -- bodies below are reconstructed from context
|
||||
// and clearly marked):
|
||||
// FireWeapon @004bc104 (slot 18), GetStatusFlags @004bbf88 (slot 12),
|
||||
// UpdateWeaponState @004bbc20 (slot 16).
|
||||
// FireWeapon @004bc104 (slot 18) -- RECOVERED by the 2026-08-06
|
||||
// re-export (part_013.c:16023-16125; trigger-time hitscan, implemented
|
||||
// in the #171 batch. The old "BODY NOT recovered" line here was the
|
||||
// stale banner behind the #168 mis-fix -- see the FireWeapon block).
|
||||
// best-effort (vtable slot proven, function prologue present, body still
|
||||
// unrecovered -- reconstructed from context and clearly marked):
|
||||
// GetStatusFlags @004bbf88 (slot 12), UpdateWeaponState @004bbc20
|
||||
// (slot 16).
|
||||
// excluded (belong to sibling/derived classes, NOT ProjectileWeapon):
|
||||
// Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
|
||||
// MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
|
||||
@@ -45,6 +48,9 @@
|
||||
// _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling)
|
||||
// _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel)
|
||||
// _DAT_004bb3b4 = 0000003f = 0.5f
|
||||
// _DAT_004bc678 = 00000040 = 2.0f (tracer fall-time factor; audit 2026-08-13)
|
||||
// _DAT_004bc67c = 8a6c379943c6fad0fb3f = 1/9.8 as 80-bit x87 EXTENDED
|
||||
// (tracer gravity term; audit 2026-08-13)
|
||||
// DAT_004e0f74 = {0,0,0} (zero vector)
|
||||
// DAT_004e0fd4 = {0,0,0} (zero point)
|
||||
// 0x40400000 = 3.0f (TotalTimeToEject default)
|
||||
@@ -145,8 +151,18 @@ namespace {
|
||||
static const Scalar _DAT_004bc100 = 1.0f; // time-of-flight bias
|
||||
static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient
|
||||
static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling
|
||||
static const Scalar _DAT_004bc678 = 1.0f; // muzzle-speed term (unresolved)
|
||||
static const Scalar _DAT_004bc67c = 1.0f; // muzzle-speed term (unresolved)
|
||||
// Tracer launch-speed constants -- BYTE-CONFIRMED by the 2026-08-13 weapons
|
||||
// audit (WEAPONS_DRIFT_AUDIT.md entry 7 / evidence log 3); the old 1.0f
|
||||
// "unresolved" stand-ins are RETIRED:
|
||||
// @0x4bc678 = 40000000 = 2.0f (float32)
|
||||
// @0x4bc67c = 8a6c379943c6fad0fb3f = 0.10204081... = 1/9.8, stored as an
|
||||
// 80-bit x87 EXTENDED (not float32-decodable -- why it sat
|
||||
// "unresolved"). Together with the ctor's muzzle-height term:
|
||||
// launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleHeight)
|
||||
// i.e. the tracer crosses effectiveRange in the time a shell would fall
|
||||
// from muzzle height under g=9.8 (~1s for a ~5u mount).
|
||||
static const Scalar _DAT_004bc678 = 2.0f; // fall-time factor (2h/g)
|
||||
static const Scalar _DAT_004bc67c = 0.10204081f; // 1/9.8 (x87 extended in the image)
|
||||
|
||||
// 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
|
||||
@@ -311,11 +327,16 @@ ProjectileWeapon::ProjectileWeapon(
|
||||
minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; // +0x1C8 -> 0x404
|
||||
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)
|
||||
Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678); // FUN_004dd138
|
||||
tracerOrigin = Vector3D(0,0,0); // FUN_00408440(this+0x42c, DAT_004e0f74)
|
||||
launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed)
|
||||
|
||||
totalTimeToEject = 3.0f; // 0x3F0 (0x40400000)
|
||||
timeToEject = totalTimeToEject; // 0x3F4
|
||||
@@ -706,6 +727,30 @@ Logical
|
||||
return (p > UniformRandom()) ? True : False; // FUN_00408050
|
||||
}
|
||||
|
||||
//
|
||||
// The @004bc3fc ctor launch-speed expression, deferred to first use (see the
|
||||
// ctor note). Byte-grounded constants (audit 2026-08-13): @0x4bc678 = 2.0f,
|
||||
// @0x4bc67c = 1/9.8 (80-bit x87 extended `8a6c379943c6fad0fb3f`):
|
||||
// launchVelocity.z = -effectiveRange / sqrt((1/9.8) * 2.0 * muzzleHeight)
|
||||
// The binary computes it once at ctor time (bind pose, effectiveRange still ==
|
||||
// authored WeaponRange); the port computes it once at first use with the mount
|
||||
// height above the mech origin -- the same quantity for a standing mech.
|
||||
//
|
||||
void
|
||||
ProjectileWeapon::ResolveLaunchVelocity()
|
||||
{
|
||||
if (launchVelocity.z != 0.0f)
|
||||
return; // already resolved
|
||||
extern Scalar BTWeaponMountHeight(void *ownerMech, int segIndex);
|
||||
Scalar muzzleHeight = BTWeaponMountHeight(owner, GetSegmentIndex());
|
||||
if (muzzleHeight < 0.5f)
|
||||
muzzleHeight = 0.5f; // port guard: unresolved segment (muzzle==origin
|
||||
// fallback) -- the binary's bind-pose height is
|
||||
// always positive; keeps sqrt/divide sane
|
||||
Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678 * muzzleHeight); // FUN_004dd138
|
||||
launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed)
|
||||
}
|
||||
|
||||
//
|
||||
// @004bc06c -- lead-solution time-of-flight. Sample the current target
|
||||
// position, advance it by launchVelocity, and re-run targeting. With no target
|
||||
@@ -715,6 +760,7 @@ Logical
|
||||
Scalar
|
||||
ProjectileWeapon::ComputeTimeOfFlight()
|
||||
{
|
||||
ResolveLaunchVelocity(); // deferred @004bc3fc z term
|
||||
GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420)
|
||||
leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410
|
||||
leadPosition.y += launchVelocity.y;
|
||||
@@ -969,10 +1015,27 @@ void
|
||||
|
||||
//
|
||||
// @004bc104 -- slot 18 FireWeapon (overrides MechWeapon's pure-virtual trap
|
||||
// @004ba45c). Body NOT recovered by the decompiler (prologue confirmed at
|
||||
// 0x4bc104, immediately following the 1.0f literal @0x4bc100). Spawns the
|
||||
// generic projectile/tracer. MissileLauncher overrides this again at @004bcc60
|
||||
// to launch Missile entities.
|
||||
// @004ba45c).
|
||||
//
|
||||
// STALE-BANNER CORRECTION (2026-08-13, the #171 hunt): this comment said
|
||||
// "Body NOT recovered by the decompiler" -- TRUE of the 2025 export, FALSE
|
||||
// since the 2026-08-06 re-export (part_013.c:16023-16125), and the stale
|
||||
// banner is the ROOT CAUSE of the #168 mis-fix: the ballistic model was
|
||||
// generalized from the MISSILE launcher @004bcc60 instead of this body.
|
||||
// The RECOVERED truth [T1]: the AC FireWeapon is TRIGGER-TIME HITSCAN --
|
||||
// it spawns NOTHING (no shell entity; class 0xBD1 is dead code; the visible
|
||||
// round is the 0xBCD renderer tracer). It batches the round counter
|
||||
// (+0x408) and every TracerInterval-th round (+0x438) computes
|
||||
// dist = |mech aimPoint(+0x37c) - muzzle| and, gated dist <= LIVE
|
||||
// effectiveRange (+0x328, the same gate shape as the Emitter), sends ONE
|
||||
// single-panel Damage (amount = batchedCount x DamageAmount) to the LOCKED
|
||||
// target *(mech+0x388) via SendDamageMessage @004b9728 -- at fire time.
|
||||
// A locked AC inside range cannot miss; out of range the batch silently
|
||||
// discards (tracer still flies). The body below IMPLEMENTS that hitscan
|
||||
// (landed in the #171 batch, bbb3b4e; the earlier "still carries the #168
|
||||
// flight-contact model" sentence here outlived the change -- the exact
|
||||
// stale-banner genus this block documents). MissileLauncher (@004bcc60)
|
||||
// is the genuinely-flying family (Model B) and overrides this slot.
|
||||
//
|
||||
// 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
|
||||
@@ -986,10 +1049,9 @@ void
|
||||
{
|
||||
Check(this);
|
||||
|
||||
// Refresh the lead solution (leadPosition / targeting) for the spawn below
|
||||
// (@004bc06c; the recovered sim no longer calls it -- best-effort placement
|
||||
// inside the unrecovered @4bc104 body, which is where the lead data is used).
|
||||
ComputeTimeOfFlight(); // @004bc06c
|
||||
// (ComputeTimeOfFlight @004bc06c is called INSIDE the tracer branch below,
|
||||
// exactly where @004bc104 calls it -- :16091, between the muzzle resolve and
|
||||
// the range gate. The old top-of-body placement predated the re-export.)
|
||||
|
||||
// THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the
|
||||
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
|
||||
@@ -1031,27 +1093,82 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port
|
||||
// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
|
||||
// Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|.
|
||||
//
|
||||
// #171 (2026-08-13) -- THE AUTHENTIC AC: TRIGGER-TIME HITSCAN (@004bc104
|
||||
// [T1], the body the stale banner hid; the prior flying-damage round was
|
||||
// the #168 mis-generalization from the MISSILE launcher). Binary shape:
|
||||
// increment the round counter (+0x408); every TracerInterval-th (+0x438)
|
||||
// round, dist = |owner aimPoint(+0x37c) - muzzle| gated against the LIVE
|
||||
// effectiveRange (+0x328, the same gate the Emitter uses) -> ONE
|
||||
// single-panel Damage (amount = batchedCount x DamageAmount) to the
|
||||
// LOCKED target *(mech+0x388) via SendDamageMessage @004b9728, AT FIRE
|
||||
// TIME. Out of range: the batch silently discards. A locked AC inside
|
||||
// range cannot miss; nothing about the hit rides the visible round.
|
||||
// (This also restores the binary's ONE-message-per-batch = single-panel
|
||||
// damage -- closing #112's multi-zone shotgun, which was the port's
|
||||
// per-round zone re-roll scatter.)
|
||||
//
|
||||
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
|
||||
// The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's
|
||||
// targeting block writes; a consistent producer/consumer pair on our
|
||||
// compiled object). Task #41: the slots hold the WORLD PICK -- the enemy
|
||||
// mech under the boresight, or the terrain downrange (a missile fired at
|
||||
// the scenery flies to the ground point and detonates without damage), or
|
||||
// null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate
|
||||
// wouldn't have fired either).
|
||||
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
|
||||
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
|
||||
|
||||
++tracerCounter; // @0x408 -- incremented BEFORE the test
|
||||
// @4bc104:16079: bVar1 = (interval != 0) && (counter % interval == 0).
|
||||
// interval==0 -> bVar1 never set: NO damage ever, counter never resets
|
||||
// (and the short-circuit means no divide) -- the port matches exactly.
|
||||
if (tracerInterval != 0 && (tracerCounter % tracerInterval) == 0)
|
||||
{
|
||||
// @4bc104:16088-16091: muzzle transform (@004b9948, resolved above as
|
||||
// `muzzle`), then the lead/targeting refresh -- INSIDE the tracer
|
||||
// branch, not per-round. (Also re-derives effectiveRange via
|
||||
// UpdateTargeting, so the gate below reads the LIVE value.)
|
||||
ComputeTimeOfFlight(); // @004bc06c
|
||||
Vector3D toAim;
|
||||
toAim.Subtract(targetPos, muzzle); // :16096 FUN_00408644(aimPoint(+0x37c), muzzle)
|
||||
Scalar dist = (Scalar)sqrtf(toAim.x*toAim.x + toAim.y*toAim.y + toAim.z*toAim.z);
|
||||
if (dist <= effectiveRange && target != 0) // :16098 gate (+0x328); the null test is the
|
||||
{ // port's guard (binary trusts 0x388!=0 upstream)
|
||||
// @4bc104 fills EXACTLY TWO fields of the batched record (:16100-16120):
|
||||
// damageAmount = counter x authored amount (:16110)
|
||||
// damageForce = rootSegRot . leadPosition(+0x420) x float(+0x41c)
|
||||
// impactPoint is NOT set here -- SendDamageMessage @004b9728 sets it
|
||||
// (FUN_00408440(dmg+0x20, mech+0x37c) = the aim point); the port's
|
||||
// SendDamageMessage doesn't do that write, so it is FOLDED here as
|
||||
// impactPoint = targetPos (net-identical). burstCount RIDES the
|
||||
// authored record untouched (Damage ctor = 1 for an AC; only
|
||||
// MissileLauncher re-authors it, and that class overrides FireWeapon).
|
||||
// damageForce divergence [T3, deliberate]: the binary's value is the
|
||||
// lead point rotated by the mech root-segment matrix scaled by the
|
||||
// float at +0x41c (a dword lifted from the tracer-model record --
|
||||
// direction-equivalent junk; gyro rattle consumes DIRECTION only,
|
||||
// and the Emitter's own path authors force = target - muzzle
|
||||
// (emitter.cpp:360 @0x3B0)), so the port uses the same toAim shape.
|
||||
Scalar batched = (Scalar)tracerCounter * damageData.damageAmount; // :16110
|
||||
Damage batchSave = damageData; // binary builds a LOCAL copy (:16100);
|
||||
damageData.damageAmount = batched; // the port mutates+restores because our
|
||||
damageData.impactPoint = targetPos; // SendDamageMessage reads the member
|
||||
damageData.damageForce = toAim;
|
||||
SendDamageMessage((Entity *)target); // @004b9728 -- at fire time
|
||||
damageData = batchSave;
|
||||
}
|
||||
tracerCounter = 0; // :16123 -- resets on HIT and MISS alike,
|
||||
} // but ONLY on tracer rounds
|
||||
|
||||
// The VISIBLE round: the binary's 0xBCD renderer tracer, cosmetic only
|
||||
// (the shell entity 0xBD1 is dead code -- nothing shipped ever spawns a
|
||||
// damage-carrying ballistic round). Fly the pool round as that visual:
|
||||
// damage 0, straight at the pick, authentic muzzle speed
|
||||
// (= effRange / sqrt(2*(1/9.8)*muzzleHeight), the recovered @004bc3fc
|
||||
// constants -- see ResolveLaunchVelocity).
|
||||
ResolveLaunchVelocity();
|
||||
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||
+ launchVelocity.y*launchVelocity.y
|
||||
+ launchVelocity.z*launchVelocity.z);
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, 0.0f /*cosmetic tracer*/,
|
||||
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/,
|
||||
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
|
||||
0 /*splash_burst*/, -1 /*muzzle_seg*/,
|
||||
(int)damageData.damageType /*issue: autocannon = Ballistic, was Explosive*/);
|
||||
(int)damageData.damageType);
|
||||
|
||||
// task #61: mark this shot for REPLICATION so the peer sees the enemy's
|
||||
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
|
||||
@@ -1111,6 +1228,7 @@ void
|
||||
// plus the DAFC muzzle flash.
|
||||
Point3D mz;
|
||||
GetMuzzlePoint(mz); // @004b9948
|
||||
ResolveLaunchVelocity(); // deferred @004bc3fc z term
|
||||
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||
+ launchVelocity.y*launchVelocity.y
|
||||
+ launchVelocity.z*launchVelocity.z);
|
||||
|
||||
@@ -283,6 +283,14 @@ class NotationFile;
|
||||
Scalar
|
||||
ComputeTimeOfFlight();
|
||||
|
||||
// The @004bc3fc ctor launch-speed expression
|
||||
// (launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleHeight);
|
||||
// constants byte-confirmed @0x4bc678/@0x4bc67c, audit 2026-08-13),
|
||||
// deferred to first use because the port ctor runs mid-roster-build.
|
||||
// z == 0 is the unresolved sentinel; no data member added (layout locked).
|
||||
void
|
||||
ResolveLaunchVelocity();
|
||||
|
||||
// --- cross-family isolation helpers (owning Mech subsystem roster +
|
||||
// cockpit controls live in the mech/controls families). Declared
|
||||
// here so the recovered bodies compile; the mech family wires the
|
||||
|
||||
@@ -6,6 +6,11 @@ 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 config, bindings.txt keymap).
|
||||
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 "content\OPERATOR.EGG" goto badpath
|
||||
set BT_RELAY=107.202.218.169:1500
|
||||
|
||||
@@ -9,6 +9,11 @@ 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 config, bindings.txt keymap).
|
||||
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 "content\OPERATOR.EGG" goto badpath
|
||||
set BT_RELAY=10.0.0.46:1500
|
||||
|
||||
@@ -8,6 +8,11 @@ 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 config, bindings.txt keymap).
|
||||
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 "content\OPERATOR.EGG" goto badpath
|
||||
set BT_START_INSIDE=1
|
||||
|
||||
@@ -8,6 +8,11 @@ cd /d %~dp0
|
||||
if not exist "build\Release\btl4.exe" goto badpath
|
||||
if not exist "content\BTL4.RES" goto badpath
|
||||
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
|
||||
rem Cockpit view + MFD gauges -- the same client flags the join bats set
|
||||
rem (field report: players booted 3rd-person without them). Inherited by
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,88 @@
|
||||
#!/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"))
|
||||
@@ -0,0 +1,533 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,34 @@
|
||||
# Thursday dev-day state (2026-08-13 ~late afternoon) — pre-compaction anchor
|
||||
|
||||
COMMITTED/PUSHED through 2c6dc00: #173 cadence fix (ring default 28Hz) + gotcha 32 + census
|
||||
checklist; #21 clamp fix + retraction sweep; launcher BT_DMG_LOG=1 forensics net; guide 2B
|
||||
confession rewrite; all of Wed/Thu's weapons batch (#170/#165/#172/#174/#171 + drag/bystander/
|
||||
gravity/tracer; single-log + SENDLOGS; soundboard shipped + ear-verified).
|
||||
|
||||
CADENCE QUEUE: ALL FIVE ITEMS DONE, reviewed + committed (f811c64 gyro / 567ffac registry /
|
||||
b355844 footstep / f9546d2 doppler; sway check = verification only). Receipts:
|
||||
scratchpad/night17/CADENCE_STATE.md. Census checklist + env-gate table updated in context/.
|
||||
Doppler perceptual verdict is T3 (solo rig near-field dominated) — needs ear A/B or 2-node.
|
||||
NEW census row: child-entity worldLinearVelocity=0 → self-sound cents −200 (pre-existing,
|
||||
untouched, needs a binary-behavior ruling first).
|
||||
|
||||
EVENING ADDITIONS (both verified + pushed): #83 incoming-missile alarm (b00fa39 — the July F7
|
||||
driver was DEAD CODE in the blocked Missile transcription, gotcha 33; feed now rides the push
|
||||
simulator's salvo leads incl. replicant mirrors = the victim's cockpit in MP; 2-node bench:
|
||||
54/55 lock cycles, beeper tempo 10→435 live; Oracle era-confirmed the sample same day) and
|
||||
#175 missile world collision (7b12053 — the static-solid-tree half of FUN_0042291c's sweep;
|
||||
345 struct-class WORLD bursts on the reported mound, user-witnessed; owner dispatch = trucks
|
||||
missile-killable; BT_WORLDHIT=0 lever). New hooks: BT_INCOMING_LOG, BT_DESIGNATE=enemy,
|
||||
BT_WORLDHIT. Both tickets commented; release notes updated with both.
|
||||
|
||||
TOMORROW (Friday): cut zip via python tools/mkdist.py (clean tree first; sweep bench artifacts
|
||||
from content/ incl. INC*.EGG + inc_*.log/incoming_*.log); finalize
|
||||
dist/RELEASE_NOTES_NEXT_discord.md (swap build number); post zip + notes + soundboard zip +
|
||||
remind SENDLOGS. Field-verify list rides the notes' checklist (+ alarm tempo ramp by ear,
|
||||
missiles bursting on cover, near-miss splash).
|
||||
|
||||
WAITING ON OTHERS: annotated field-guide copies (diff each against the version the tester
|
||||
received); Elengil's pre-rotation steam_20260812.log; era answers (valve-boost lore, SRM
|
||||
450/800 pip, PPC-hit visuals for Cyd's scramble); Cyd PPC-scramble merge = post-Friday
|
||||
(rebase over palette work + dirty-skip token fix + 2-node bench). NEXT WEEK: BTSimClock
|
||||
migration per the census checklist (gyro item may land today instead).
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,68 @@
|
||||
# #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)
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,68 @@
|
||||
# #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")
|
||||
@@ -0,0 +1,327 @@
|
||||
#!/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()
|
||||
+205
-32
@@ -1,16 +1,31 @@
|
||||
#!/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)
|
||||
|
||||
- Click a button to play that sample (stdlib winsound, async -- click again to
|
||||
restart, click STOP to silence).
|
||||
- The label shows [bank:patch] from game/reconstructed/audiopresets.cpp so an
|
||||
identified sound maps straight back to the game's (bankID, patchID).
|
||||
- Type in the filter box to narrow (substring, case-insensitive).
|
||||
- "Play All" steps through the filtered list one per second (ESC/STOP to halt).
|
||||
THE RATE MODEL (see docs/AUDIO_FIDELITY.md F2 + L4AUDIO.cpp:21-24):
|
||||
game playback rate = WAV header rate x 2^((note - 60) / 12)
|
||||
The port bakes each zone's authored SoundFont tuning into the extracted WAV's
|
||||
declared rate (sf2extract.py), then applies AL_PITCH = 2^((note-60)/12) for
|
||||
the triggering MIDI note. Sources default to note 60 (AUDSRC.cpp DEFAULT_NOTE)
|
||||
so most samples play at their file rate -- but AudioControlSequences streamed
|
||||
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, sys, threading, time
|
||||
import os, re, struct, sys, tempfile, threading, time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import winsound
|
||||
@@ -18,28 +33,118 @@ import winsound
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
AUDIO = os.path.normpath(os.path.join(HERE, "..", "content", "AUDIO"))
|
||||
PRESETS_CPP = os.path.normpath(os.path.join(HERE, "..", "game", "reconstructed", "audiopresets.cpp"))
|
||||
TMPDIR = os.path.join(tempfile.gettempdir(), "bt411_soundboard")
|
||||
|
||||
def load_mapping():
|
||||
"""file -> (bank, patch) from audiopresets.cpp (allPresets[b][p] ... file="X.wav")."""
|
||||
mapping = {}
|
||||
# ---------------------------------------------------------------------------
|
||||
# The authored trigger-note census [T1]: every AudioControlSequence in
|
||||
# BTL4.RES, parsed from the stream records (scratchpad seqparse.py,
|
||||
# 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:
|
||||
text = open(PRESETS_CPP, encoding="utf-8", errors="replace").read()
|
||||
# blocks look like: allPresets[0][12].samples[0]; ... s.file = "LaserAFire01.wav";
|
||||
for m in re.finditer(r'allPresets\[(\d+)\]\[(\d+)\]\.samples\[0\];.*?s\.file = "([^"]+)";',
|
||||
text, re.S):
|
||||
bank, patch, fname = int(m.group(1)) + 1, int(m.group(2)), m.group(3)
|
||||
mapping[fname] = (bank, patch)
|
||||
for m in re.finditer(r'Z\((\d+),(\d+),"([^"]+)",(-?\d+),(-?\d+),', text):
|
||||
zones[m.group(3)] = dict(bank=int(m.group(1)) + 1, patch=int(m.group(2)),
|
||||
keyLo=int(m.group(4)), keyHi=int(m.group(5)))
|
||||
except OSError:
|
||||
pass
|
||||
return mapping
|
||||
return zones
|
||||
|
||||
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):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("BT411 Soundboard -- " + AUDIO)
|
||||
self.geometry("1150x760")
|
||||
self.mapping = load_mapping()
|
||||
self.title("BT411 Soundboard (game rates) -- " + AUDIO)
|
||||
self.geometry("1400x800")
|
||||
os.makedirs(TMPDIR, exist_ok=True)
|
||||
self.zones = load_zones()
|
||||
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
|
||||
|
||||
top = ttk.Frame(self); top.pack(fill="x", padx=6, pady=4)
|
||||
@@ -50,6 +155,9 @@ class SoundBoard(tk.Tk):
|
||||
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="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")
|
||||
ttk.Label(top, textvariable=self.status, foreground="#06c").pack(side="left", padx=12)
|
||||
|
||||
@@ -69,30 +177,94 @@ class SoundBoard(tk.Tk):
|
||||
|
||||
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):
|
||||
bp = self.mapping.get(f)
|
||||
tag = f"[{bp[0]}:{bp[1]}] " if bp else "[-:-] "
|
||||
return tag + f[:-4]
|
||||
z = self.zones.get(f)
|
||||
tag = f"[{z['bank']}:{z['patch']}] " if z else "[-:-] "
|
||||
if z and (z["bank"], z["patch"]) in VOICE_PRESETS:
|
||||
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):
|
||||
for w in self.grid_frame.winfo_children():
|
||||
w.destroy()
|
||||
flt = self.filter_var.get().lower()
|
||||
shown = [f for f in self.files if flt in f.lower()]
|
||||
cols = 4
|
||||
cols = 3
|
||||
for i, f in enumerate(shown):
|
||||
b = ttk.Button(self.grid_frame, text=self.label_for(f), width=38,
|
||||
cell = ttk.Frame(self.grid_frame)
|
||||
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))
|
||||
b.grid(row=i // cols, column=i % cols, padx=2, pady=1, sticky="w")
|
||||
b.pack(side="left")
|
||||
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.canvas.yview_moveto(0)
|
||||
|
||||
def play(self, f):
|
||||
def path_for(self, f, note):
|
||||
"""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
|
||||
path = os.path.join(AUDIO, f)
|
||||
entries = self.plays[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)
|
||||
bp = self.mapping.get(f)
|
||||
self.status.set(f"PLAYING {f}" + (f" (bank {bp[0]}, patch {bp[1]})" if bp else ""))
|
||||
z = self.zones.get(f)
|
||||
bp = f"bank {z['bank']}, patch {z['patch']}" if z else "unmapped"
|
||||
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):
|
||||
self.playing_all = False
|
||||
@@ -107,9 +279,10 @@ class SoundBoard(tk.Tk):
|
||||
for f in shown:
|
||||
if not self.playing_all:
|
||||
return
|
||||
note = self.plays[f][0][0]
|
||||
path = self.path_for(f, note)
|
||||
self.after(0, lambda ff=f: self.status.set("PLAYING " + ff))
|
||||
winsound.PlaySound(os.path.join(AUDIO, f),
|
||||
winsound.SND_FILENAME | winsound.SND_ASYNC)
|
||||
winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
||||
time.sleep(1.0)
|
||||
self.playing_all = False
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
Reference in New Issue
Block a user