velocity/dt audit: DRAG WAS THE GOVERNOR -- the binary applies -0.001*v^2 every frame, burn AND coast (@004bef78 + thruster @004be474, ctor precomputes terminal sqrt(T/COD) into +0x348 as the seeker lead base; authored COD 0.001 byte-read from BTL4.RES type-15 srm/lrm/strk/nrk). The port omitted it: LRM hit 3030 u/s at burnout vs the machine's 547.7 terminal (5.5x), 3912u downrange at t=5s vs 2108u -- a major slice of the 'missiles reach anything' field feel. Pool now integrates speed += (accel - 0.001*v^2)*dt. The dt MODEL itself verified FAITHFUL (Mover zeroes accel per frame + integrates x dt in seconds, matched line-for-line to T0 MOVER.cpp -- the 28Hz myomer idiom does NOT apply; open-question closed). AC cosmetic tracer TOF constants landed (2.0f @0x4bc678, 1/9.8 x87 EXTENDED @0x4bc67c, muzzleHeight term via ResolveLaunchVelocity deferred-first-use). Stale banners swept (misthrst @004be474 recovered; projweap #168 flight-contact banner gone). KB: decomp-reference Mover-gravity fix + flight-model bullet, coast gravity = 6.5 not 9.8. Drift audit: SSVELOCITY/DT + SSRANGE AUTHENTICITY (manual cross-check: PPC/AFC100 MATCH, SRM 450-vs-800 authoring drift; rangefinder ladder NOT miscalibrated -- fixed 0-1200 + authored pips both sides; -100m range-bias state unimplemented, parked). Build clean

This commit is contained in:
Joe DiPrima
2026-08-13 14:58:04 -05:00
parent 6517ebe505
commit b94636b607
9 changed files with 401 additions and 45 deletions
+22 -3
View File
@@ -289,6 +289,20 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
**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 —
@@ -603,9 +617,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
+15 -7
View File
@@ -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
+8
View File
@@ -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).
+4 -1
View File
@@ -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):
+204
View File
@@ -382,3 +382,207 @@ tap-eject unjam, NoAmmo latch with ammo remaining (§9); gear-wrap brick
(§2); incoming-missile warning (§10); generator switching mid-charge,
out-of-range beam (§1); rack-name quirks, homing agility + Narc-did-
nothing (§8).
---
## RANGE AUTHENTICITY — printed manual cross-check + the rangefinder scale (2026-08-13)
**Why:** playtesters report ranges "feel wrong" and hits landing "beyond
the maximum range on the range scale." Two legs: (1) diff every weapon
range/damage number the original player manual prints against the
authored content values (Appendix A of the field guide — this audit's
numbers table); (2) pin down how the HUD rangefinder's displayed maximum
relates to authored WeaponRange.
### Leg 1 — the manual cross-check
Source: `reference/manual/Tesla40_BT_manual.pdf` (34 pp). Finding about
the source itself [T1 — full-text sweep + page renders, this audit]: the
manual prints **no weapons table**. The per-mech stat sheets (pp. 25-29)
carry loadouts, coolant loops, tonnage/armor/reservoir/speeds — no
per-weapon range or damage column. The ONLY printed range figures are
MFD screenshot mockups (eng-data panels), four values total; **no
per-weapon damage number appears anywhere in the manual** (the p. 4
scoring page says damage is delivered "the same way their BattleTech
boardgame counterparts do" — a fidelity claim with no numbers; the
authored 4.10 values do NOT numerically track boardgame values, e.g.
PPC 12 vs the boardgame's 10 — treat the sentence as blurb, not spec).
| Weapon | MANUAL prints | CONTENT authored | Verdict |
|---|---|---|---|
| PPC | RANGE 900M (p. 20, PPC eng-data screen) | 900 | **MATCH** |
| AFC100 | RANGE 400M (p. 21, AFC eng-data screen) | 400 | **MATCH** |
| SRM4 | RANGE 450M (p. 22, Engineering MFD mockup) | 800 (display-only) | **MISMATCH** |
| SRM6 | RANGE 450M (p. 22 AND p. 23, two independent screens) | 800 (display-only) | **MISMATCH** |
| SLaser | — | 1.5 dmg / 150 | manual-silent |
| ERSLaser | — | 2 / 225 | manual-silent |
| MLaser | — | 2.5 / 350 | manual-silent |
| ERMLaser | — | 3.5 / 500 | manual-silent |
| LLaser | — | 5 / 600 | manual-silent |
| ERLLaser | — | 6 / 750 | manual-silent |
| ERPPC | — | 16 / 950 | manual-silent |
| AFC25 | — | 7 / 900 | manual-silent |
| AFC50 | — | 13 / 750 | manual-silent |
| GAUSS | — | 20 / 900 | manual-silent (pip-color note below) |
| LRM5/10/15/20, NRK5 | — | 6000 (display-only) | manual-silent |
| ALL weapons, damage | — (zero numbers printed) | Appendix A | manual-silent |
All four manual figures verified by page RENDER, not OCR alone (the
450M reads twice on p. 22 and once on p. 23) [T1].
**The SRM mismatch read:** 450 (manual) vs 800 (shipped 4.10 content,
BLH live pip dump [T2] + field-guide Appendix A). Two mitigations:
(a) the manual's panels are mockups from the earlier software revision —
the same 4.0→4.10 drift already proven on the Loki loadout rework,
the small-laser loop redistribution, and supercharge
(`context/pod-hardware.md` §Manual); (b) for MISSILE racks,
WeaponRange is fire-control display ONLY (lane 10 [T1]) — real reach is
flight physics on both values, so the 450→800 change altered the pip
position and panel text, never a trajectory. Verdict: authoring drift
between revisions, NOT a port defect; the port displays the shipped
4.10 content values.
**Pip-color side note:** manual p. 10 codes the readiness pips PPC=Blue,
Laser=Red, LRM=Green, SRM=Brown, AFC=Orange, **Gauss=Grey** — blue/red
match our dumps exactly, SRM "brown" is the authored amber (0.6,0.4,0),
but the shipped GAUSS record authors a YELLOW pip (lane 8 [T1]). Same
drift class as the SRM range; cosmetic.
### Leg 2 — the rangefinder scale (what the ladder max actually is)
The tester model behind the complaint — "the scale maxes at the selected
weapon's range" — is FALSE in both the binary and the port. What the
HUD right ladder actually is (full detail `context/gauges-hud.md`
§Cockpit HUD reticle):
- **The ladder is a FIXED 01200 m scale** — the ctor's hardcoded
calibration (@004cc40c, ctor param 11 = 0x44960000) [T1]; port
identical (`btl4vid.cpp:2382` kRetMaxRange = 1200.0f). It is never
derived from any weapon's WeaponRange or live effectiveRange.
- **Each weapon marks its own reach as a PIP** at its authored
WeaponRange, CLAMPED into [0..1200] (`AddWeapon` @004cdac0; port
transcription `btl4vid.cpp:3139-3147`) [T1]. Caret below a pip = that
weapon reaches the target. An LRM/NRK rack authored 6000 therefore
pins its pip AT THE LADDER TOP — authentic on both sides.
- **The caret is the displayed target range**, sliding at 500 m/s toward
the true pick range (HudSimulation part_013.c:5652 [T1]; port
mech4.cpp targeting step [T2]); pegs at 1200 with no target. During
depth-discontinuity walks the displayed value legitimately lags true
range by hundreds of meters (the Gitea #4 measurement).
- **The ONE displayed-range divergence:** the binary subtracts
`_DAT_004b7ecc` = 100.0f from RangeToTarget (@0x1EC) every frame the
timed flag @0x22C is set (timer @0x21C to limit @0x1D8) — the port
does NOT implement this state (`hud.cpp:79-82` tracks it;
`context/open-questions.md` §HUD range-bias) [T1 read, unimplemented].
Direction: the shipped machine UNDER-reads range by up to 100 m in
that state; the port never does. So when the state is active a
true-850 m missile hit against an 800 pip DISPLAYED as in-range on
the pod but displays as beyond-the-pip in the port — a bounded
(≤100 m) port-side contributor to "hit beyond the scale" reports.
Whether the state ever triggers in the field is unknown (what sets
@0x22C is the open question) [T4 for field prevalence].
**Rangefinder verdict:** the displayed scale is AUTHENTIC — no port
miscalibration. "Hits beyond the maximum range on the range scale" is
expected, authentic behavior with three drivers, largest first:
(1) missile reach is flight physics, not WeaponRange — SRMs genuinely
hit past their 800 pip and LRMs past the 1200 ladder top [T1];
(2) energy/AC pips mark AUTHORED range while the live fire gate is
effectiveRange = (1 zoneDamage) × authored — a damaged weapon's real
reach sits BELOW its pip, never above, so those lanes cannot produce
beyond-pip hits; (3) the 500 m/s caret slide lags true range during
fast picks. The single true divergence is the unimplemented 100 m
bias state (above) — worth reconstructing if Friday's reports cluster
in the 0100 m band past a pip; anything farther past the pip is
missiles being missiles.
**Guide action:** MISMATCHES exist (SRM display ranges + the Gauss pip
color) → per the audit rule the field guide was NOT touched; no
reassurance paragraph shipped.
---
## VELOCITY/DT INTEGRATION — term-by-term audit (2026-08-13)
**Scope:** the projectile velocity integration, binary vs port pool
(`mech4.cpp BTUpdateProjectiles` / `BTPushProjectile`), plus the AC tracer
launch-speed constants. Binary side read end-to-end this audit:
Missile::MoveAndCollide @004bef78, Seeker::FindTarget @004be9a0 /
LeadTarget @004beae4, **MissileThrusterSimulation @004be474 — the
standalone Performance body, RECOVERED** (misthrst.cpp's "folded into
MoveAndCollide, no distinct @ADDR survives" note predated the 2026-08-06
re-export and is corrected), Missile ctor @004bf5b4, launch composer
@004bcc60, and the engine Mover lane, decomp↔T0-source matched
line-for-line: **FUN_00422360 = Mover::PerformAndWatch**,
FUN_00421bac = ApplyWorldAccelerations, FUN_00421e2c =
ApplyAirResistanceAndGravity(power), FUN_00421ca8 = CalculateDrag,
FUN_00421b2c/FUN_00421b6c = UpdateWorldMotion/UpdateLocalMotion;
mover+0x250 = localEnvironment, +0x254 = deadReckoner (MOVER.h/.cpp).
**The dt model [T1/T0]:** every Performance slice is variable-step REAL
SECONDS (the #96 sim-time result), and `Mover::PerformAndWatch`
**ZEROES `localAcceleration` at the top of every frame**
(@00422360: `FUN_0040a7f4(+0x1dc, &DAT_004e0fd4)` == T0 MOVER.cpp:672
`localAcceleration = Motion::Identity`). Thrust, drag and gravity are
re-added FRESH each frame and then integrated ×dt (@00421bac:
`p += v·dt + ½a·dt²; v += a·dt`). Net missile physics is the clean ODE
`dv/dt = ThrusterAccel COD·ρ·v²` — **there is NO myomer-style 28 Hz
per-tick accumulate anywhere in this lane**, so the port's
`speed += accel·dt` needs no ×(dt·28) rescale. (The myomer idiom applies
to PERSISTENT accumulators; the Mover acceleration is not one.)
**The headline: drag was wrongly waved off.** All four missile models
author linear drag CODs **0.001 on every axis** (BTL4.RES type-15 raw
floats read this audit: srm rid=315 / lrm 319 / strk 323 / nrk 327;
airDensity = 1.0, engine default). 0.001 multiplies **v²**: it is the
flight model's GOVERNOR, and the binary says so itself — the Missile
ctor @004bf5b4 (part_013.c:18416) **precomputes terminal speed
`sqrt(ThrusterAccel / negCOD.z)` into missile+0x348** and the seeker
uses it as the lead time base. Terminal speeds: LRM/Streak/NRK
**547.7 u/s**, SRM **774.6 u/s**. The port pool (per #84's "drag
~0.001 = negligible") flew undamped: an LRM reached **3030 u/s** by
burnout — 5.5× the binary cap. Simulated head-to-head (28 Hz binary
loop vs the port step): 800u arrival LRM 2.54 s @ 492 u/s (binary) vs
2.22 s @ 695 (undamped port); at t=5 s the undamped LRM had covered
3912u @ 1530 u/s vs the binary's 2108u @ 544. **Fixed this build**:
the pool integrates `speed += (accel_burn 0.001·speed²)·dt` for
thruster rounds, burn AND coast (a burned-out round decelerates,
`v(t) = v0/(1+0.001·v0·t)` — Streak at t=5 s: 256 u/s, was 1005
frozen). AC tracers (p.accel==0) untouched — the binary's visible AC
round is a renderer cosmetic with no Mover physics.
| Term | Binary [T1] | Port (pre-audit) | Verdict | Player-visible consequence |
|---|---|---|---|---|
| dt model | variable slice, SECONDS; accel accumulator zeroed/frame (@00422360); integrate ×dt (@00421bac) | `speed += accel·dt`, `pos += vel·dt` | **FAITHFUL** (no per-tick accumulate here) | none |
| Launch velocity | authored MuzzleVelocity (z negated) + shooter velocity (@004bcc60 / FUN_004b9cbc) | same (#67) | **FAITHFUL** | none |
| Launch acceleration | ZERO in the make message (@004bcc60 authors the DAT_004e0fd4 zero Motion) | thrust applies from frame 1 (binary thruster adds the same frame) | **FAITHFUL** | none |
| Thrust | @004be474: while burning, `burnLeft = dt`, `localAccel += (0,0,ThrusterAccel)` onto the fresh accumulator ⇒ constant accel | `speed += accel·burn_dt` | **FAITHFUL** | none |
| **Drag** | `COD·ρ·sign(v)·v²` per body axis, EVERY frame, burn + coast (@004bef78, power-2 inline; plain-Projectile dead class uses power 1.0 @004bddec); CODs 0.001 [T1 bytes]; terminal `sqrt(T/c)` precomputed @004bf5b4→+0x348 | **OMITTED** ("negligible") | **WAS DIVERGENT → FIXED-THIS-BUILD** | was: LRM 3030 u/s (5.5× cap), 13% time-to-800u, +40% impact speeds, map-crossing runaways past 2.5 s; now governed at 547.7/774.6 |
| Speed clamp | none — drag IS the clamp | none — drag now is | **FAITHFUL** (post-fix) | — |
| Velocity↔seeker slaving | burning: lateral local vel/accel zeroed each frame ⇒ velocity ≡ speed×nose; nose slerps ≤ MaxThrusterRotationRate·dt (@004be474); at burnout slaving STOPS (ballistic coast) | rotate-toward-aim at port gains 4/8·dt, speed-normalized; steers through coast | KNOWN-DIVERGENCE [T3] (lane 11) | turn dynamics shape; coast homing (real impacts are in-burn) |
| Gravity | none during burn; coast: `worldAccel.y = 6.5` fresh each frame (env default) + y<1 kill plane | absent (loft arc + ttl only) | DIVERGENT [T3 **minor**, unfixed] | post-burnout flights only (beyond ~1400u SRM / ~4800u LRM); missed rounds fly level instead of dropping |
| Position step | `p += v_old·dt + ½a·dt²` | `p += v_new·dt` (semi-implicit) | FAITHFUL-approx | ≤ ~12u lead over a full SRM burn @60fps; imperceptible |
| Expiry | burn+10 s, y<1 (@004bef78 tail) | same (#168) | **FAITHFUL** | none |
| Seeker lead | `aim += targetVel × range/terminal(+0x348)`; loft climb-gated (thruster+0xF0 > _DAT_004bec1c) | live-position re-lead; loft constants exact | KNOWN-DIVERGENCE [T3] (lane 11) | small lead-shape difference |
| AC tracer speed | `launchVelocity.z = effRange/sqrt(2.0·(1/9.8)·muzzleHeight)` @004bc3fc:16157-16164 (bytes @0x4bc678 = `00000040` 2.0f; @0x4bc67c = `8a6c379943c6fad0fb3f` 1/9.8 x87 EXTENDED) | 1.0f stand-ins, no muzzleHeight | **WAS DIVERGENT (cosmetic) → FIXED-THIS-BUILD** | tracer pace now the authentic fall-time scale (~1 s to effRange at a ~5u mount) |
**Port changes landed (this audit, build clean):**
1. `mech4.cpp BTUpdateProjectiles` — quadratic drag `0.001·v²` on thruster
rounds, burn and coast; stale "drag negligible / range-capped" comment
replaced with the byte-cited model.
2. `projweap.cpp``_DAT_004bc678 = 2.0f`, `_DAT_004bc67c = 1/9.8`
(bytes above), muzzleHeight term restored via
`ResolveLaunchVelocity()` (deferred to first use — the port ctor runs
mid-roster-build; z==0 sentinel; complete-Mech-TU bridge
`BTWeaponMountHeight` in mech4.cpp). Stale "unresolved" constant
comments, the :990 "#168 flight-contact model" banner, and the header
"FireWeapon BODY NOT recovered" line all swept.
3. `misthrst.cpp` — banner corrected: @004be474 IS the standalone
MissileThrusterSimulation (steer + burn + thrust-add), recovered.
**KB corollary (swept):** the "Mover gravity is dt-less per-frame → may
need 28 Hz rescale at our frame rate" open question is CLOSED-NO: the
per-frame gravity/drag adds land on an accumulator that PerformAndWatch
zeroes every frame — they are per-frame *acceleration definitions*, not
accumulating increments, and integrate ×dt correctly at ANY frame rate.
(The port's separate "environment gravity reads 0" gap stays open.)
+59 -14
View File
@@ -817,6 +817,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
@@ -1800,22 +1822,45 @@ 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. Still
// unported [T3 minor]: coast gravity (-6.5 world-Y, @4bef78
// else-branch) and the binary's burnout end to velocity-slaving --
// both live 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;
+15 -5
View File
@@ -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)
//
+66 -15
View File
@@ -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;
@@ -985,10 +1031,11 @@ void
// 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). Reconstruction pending the #171 group
// decision (Oracle's HOLD) -- the current port body below still carries
// the #168 flight-contact model. MissileLauncher (@004bcc60) is the
// genuinely-flying family (Model B) and overrides this slot.
// 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
@@ -1110,7 +1157,10 @@ void
// 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.
// 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);
@@ -1178,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);
+8
View File
@@ -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