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:
@@ -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
|
**Proximity fuse**: seeker rangeToTarget(+0x10C) < `_DAT_004bf5a4` = **4.0** → detonate on
|
||||||
targetEntity without a geometry hit. Seeker drops a destroyed target (movementMode 2|9,
|
targetEntity without a geometry hit. Seeker drops a destroyed target (movementMode 2|9,
|
||||||
FUN_0049fb54).
|
FUN_0049fb54).
|
||||||
|
- **Missile FLIGHT MODEL (velocity/dt audit 2026-08-13) [T1]:** per frame:
|
||||||
|
`MissileThrusterSimulation` **@004be474** (the standalone Performance body — recovered;
|
||||||
|
misthrst.cpp's "folded" note was stale) slerps the missile toward the seeker aim
|
||||||
|
≤ MaxThrusterRotationRate·dt and, while burning, adds `(0,0,−ThrusterAccel)` to the
|
||||||
|
frame-zeroed accumulator; @4bef78 adds quadratic drag `−COD·ρ·sign(v)·v²` per body axis
|
||||||
|
(burn AND coast; the dead plain-Projectile @4bddec uses power **1.0** via
|
||||||
|
ApplyAirResistanceAndGravity(1.0)), zeroes LATERAL local vel/accel while burning (velocity
|
||||||
|
slaved to the nose; slaving STOPS at burnout), applies gravity (env, default **6.5**) ONLY
|
||||||
|
when coasting, then integrates ×dt (@00421bac). Net: `dv/dt = T − COD·v²`. **Authored
|
||||||
|
CODs = 0.001 ALL axes, all four models** (BTL4.RES type-15 rid 315 srm / 319 lrm /
|
||||||
|
323 strk / 327 nrk, raw floats); ctor @4bf5b4 (part_013.c:18416) precomputes **terminal
|
||||||
|
speed `sqrt(T/negCOD.z)` → missile+0x348** (the seeker lead-time base): LRM/Streak/NRK
|
||||||
|
**547.7 u/s**, SRM **774.6**. Port pool carries the same ODE since the 2026-08-13 drag
|
||||||
|
fix (mech4.cpp; drag was previously omitted as "negligible" — it is the speed governor).
|
||||||
- **Performance split** (Projectile ctor @4be1bc / Missile ctor @4bf5b4): instance
|
- **Performance split** (Projectile ctor @4be1bc / Missile ctor @4bf5b4): instance
|
||||||
(flags&0xC)==ReplicantInstance(4) → FUN_004221c0 dead-reckoning smoother (no physics);
|
(flags&0xC)==ReplicantInstance(4) → FUN_004221c0 dead-reckoning smoother (no physics);
|
||||||
master → 4bddec/4bef78. (CLASSMAP's old authoritative/ghost labels were swapped —
|
master → 4bddec/4bef78. (CLASSMAP's old authoritative/ghost labels were swapped —
|
||||||
@@ -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
|
(`FUN_0041bd98`) — **VARIABLE-STEP, slice in real SECONDS**, line-for-line the
|
||||||
WinTesla SIMULATE.cpp body. Corollaries: every Performance's `time_slice` is
|
WinTesla SIMULATE.cpp body. Corollaries: every Performance's `time_slice` is
|
||||||
seconds in BOTH binary and port (no hidden tick/second unit gap); a dt-LESS
|
seconds in BOTH binary and port (no hidden tick/second unit gap); a dt-LESS
|
||||||
per-frame term (myomer kinetic @4b8d18's middle term; the Mover gravity
|
per-frame term into PERSISTENT state (myomer kinetic @4b8d18's middle term)
|
||||||
`-= **(mover+0x250)` @0x421e77) fires at the FRAME cadence = the tick rate,
|
fires at the FRAME cadence = the tick rate, 28 Hz nominal, sagging under load.
|
||||||
28 Hz nominal, sagging under load. Mover velocity is u/s (the replicant
|
⚠ 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
|
dead-reckoner @0x421f7c does `pos += vel × ticksΔ/DAT_0052140c`). The myomer
|
||||||
heat term's ONE caller is the **unexported MyomersSimulation body
|
heat term's ONE caller is the **unexported MyomersSimulation body
|
||||||
@0x4b8b9a-0x4b8d0d** (E8-scan + raw disasm; the export gap hid it): calls
|
@0x4b8b9a-0x4b8d0d** (E8-scan + raw disasm; the export gap hid it): calls
|
||||||
|
|||||||
@@ -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).**
|
- **Environment gravity unwired — the myomer CLIMB-heat term is inert (found 2026-07-31, #85).**
|
||||||
`Mover::localEnvironment` (MOVER.h:271) is declared and never populated anywhere in the port;
|
`Mover::localEnvironment` (MOVER.h:271) is declared and never populated anywhere in the port;
|
||||||
`GetEnvironment()->gravityConstant` is the named analog of the original Mover's gravity POINTER at
|
`GetEnvironment()->gravityConstant` is the named analog of the original Mover's gravity POINTER at
|
||||||
`+0x250` (`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`)
|
integrator uses for its `m·g·|vy|·dt` climb-work term. The bridge (`BTMechMyomerMotionSample`)
|
||||||
null-guards it, so climbing currently generates no myomer heat (flat maps: moot; cavern slopes:
|
null-guards it, so climbing currently generates no myomer heat (flat maps: moot; cavern slopes:
|
||||||
a missing cost). EnvironmentZone resources (RES type 23) exist in BTL4.RES — wiring them (or at
|
a missing cost). EnvironmentZone resources (RES type 23) exist in BTL4.RES — wiring them (or at
|
||||||
@@ -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
|
the Mover holds to the live gravity cell). Harmless on flat ground (|vy|≈0 anyway) but the
|
||||||
climb-work heat never accrues on slopes. Find where our Environment gravity actually lives
|
climb-work heat never accrues on slopes. Find where our Environment gravity actually lives
|
||||||
and re-point the bridge. [T2 measured]
|
and re-point the bridge. [T2 measured]
|
||||||
- **Engine Mover gravity is dt-LESS per-frame in the binary** (`@0x421e77: vy -= **(+0x250)`,
|
- **✅ Engine Mover gravity needs NO 28Hz rescale — CLOSED 2026-08-13 (velocity/dt audit) [T1/T0].**
|
||||||
raw, once per Perform — same class as the myomer kinetic term). If our linked WinTesla
|
The worry was that the per-frame `worldAccel.y -= **(+0x250)` (@0x421e77, no dt) would scale
|
||||||
MOVER.cpp kept that form, gravity acceleration in the port scales with OUR ~59Hz frame rate
|
with our frame rate. It cannot: **`Mover::PerformAndWatch` = `FUN_00422360` ZEROES
|
||||||
(≈2.1× the pod's 28) — jump arcs, falls, missile droop all stiffer than the pod. CHECK the
|
`localAcceleration` at the top of every frame** (`FUN_0040a7f4(+0x1dc, &DAT_004e0fd4)` ==
|
||||||
WinTesla source's gravity line; if per-frame, it needs the same 28Hz reference-rate
|
T0 MOVER.cpp:672 `localAcceleration = Motion::Identity`), so gravity/drag/thrust adds are
|
||||||
normalization the myomer kinetic term got. [T1 binary side / T4 port impact until read]
|
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
|
- **Gait-noise phantom acceleration feeds the myomer accel term** (`[myoheat] a=4..33` at
|
||||||
steady cruise — our per-frame velocity re-derivation jitters; the binary read the same
|
steady cruise — our per-frame velocity re-derivation jitters; the binary read the same
|
||||||
`AccelerationLastFrame` cell off ITS OWN gait, noise level unknown). Bounded contributor
|
`AccelerationLastFrame` cell off ITS OWN gait, noise level unknown). Bounded contributor
|
||||||
|
|||||||
@@ -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)
|
(loadout rework on Loki, small-laser redistribution elsewhere), NOT a bug. `BT_SPEC_LOG` (mech4)
|
||||||
re-dumps on demand.
|
re-dumps on demand.
|
||||||
|
|
||||||
|
### Weapon-range cross-check RESULT (2026-08-13) [T1] — manual prints only 4 range figures
|
||||||
|
The manual has NO weapons table and prints NO per-weapon damage number anywhere; the per-mech
|
||||||
|
stat sheets carry no range/damage column. The only printed ranges are MFD screenshot mockups:
|
||||||
|
PPC 900M (p20) and AFC100 400M (p21) MATCH the shipped 4.10 content; SRM4/SRM6 450M (p22/p23)
|
||||||
|
vs the shipped 800 is the same 4.0→4.10 drift class as the Loki rework (and missile WeaponRange
|
||||||
|
is display-only — no physics change either way). Full table + the rangefinder-scale verdict:
|
||||||
|
`docs/WEAPONS_DRIFT_AUDIT.md` §RANGE AUTHENTICITY.
|
||||||
|
|
||||||
## Key Relationships
|
## Key Relationships
|
||||||
- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view).
|
- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view).
|
||||||
- Input: [[locomotion]] (the mapper).
|
- Input: [[locomotion]] (the mapper).
|
||||||
|
|||||||
@@ -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 =
|
(mech4.cpp): `+0x1C4` vec = `localVelocity.linearMotion`; `+0x82C` vec =
|
||||||
`localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame);
|
`localAcceleration.linearMotion` (the authentic 15-ring smoothed AccelerationLastFrame);
|
||||||
`+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong);
|
`+0x20C` = `moverMass` (the collision divert's cell — the old "motion gain" label was wrong);
|
||||||
`*(+0x250)` = the environment **gravity pointer** (`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
|
Physics: `heat += ratio²·(1+dmg)·[0.005·½mv² + 0.2·m·g·|vy|·dt + 0.2·m|v||a|·dt]` — kinetic work
|
||||||
+ climb power + acceleration power. The binary `fabs`es v.y (the port's old draft didn't).
|
+ climb power + acceleration power. The binary `fabs`es v.y (the port's old draft didn't).
|
||||||
**Authored tuning extracted from BTL4.RES** (all 18 mech variants share one record):
|
**Authored tuning extracted from BTL4.RES** (all 18 mech variants share one record):
|
||||||
|
|||||||
@@ -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,
|
(§2); incoming-missile warning (§10); generator switching mid-charge,
|
||||||
out-of-range beam (§1); rack-name quirks, homing agility + Narc-did-
|
out-of-range beam (§1); rack-name quirks, homing agility + Narc-did-
|
||||||
nothing (§8).
|
nothing (§8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RANGE AUTHENTICITY — printed manual cross-check + the rangefinder scale (2026-08-13)
|
||||||
|
|
||||||
|
**Why:** playtesters report ranges "feel wrong" and hits landing "beyond
|
||||||
|
the maximum range on the range scale." Two legs: (1) diff every weapon
|
||||||
|
range/damage number the original player manual prints against the
|
||||||
|
authored content values (Appendix A of the field guide — this audit's
|
||||||
|
numbers table); (2) pin down how the HUD rangefinder's displayed maximum
|
||||||
|
relates to authored WeaponRange.
|
||||||
|
|
||||||
|
### Leg 1 — the manual cross-check
|
||||||
|
|
||||||
|
Source: `reference/manual/Tesla40_BT_manual.pdf` (34 pp). Finding about
|
||||||
|
the source itself [T1 — full-text sweep + page renders, this audit]: the
|
||||||
|
manual prints **no weapons table**. The per-mech stat sheets (pp. 25-29)
|
||||||
|
carry loadouts, coolant loops, tonnage/armor/reservoir/speeds — no
|
||||||
|
per-weapon range or damage column. The ONLY printed range figures are
|
||||||
|
MFD screenshot mockups (eng-data panels), four values total; **no
|
||||||
|
per-weapon damage number appears anywhere in the manual** (the p. 4
|
||||||
|
scoring page says damage is delivered "the same way their BattleTech
|
||||||
|
boardgame counterparts do" — a fidelity claim with no numbers; the
|
||||||
|
authored 4.10 values do NOT numerically track boardgame values, e.g.
|
||||||
|
PPC 12 vs the boardgame's 10 — treat the sentence as blurb, not spec).
|
||||||
|
|
||||||
|
| Weapon | MANUAL prints | CONTENT authored | Verdict |
|
||||||
|
|---|---|---|---|
|
||||||
|
| PPC | RANGE 900M (p. 20, PPC eng-data screen) | 900 | **MATCH** |
|
||||||
|
| AFC100 | RANGE 400M (p. 21, AFC eng-data screen) | 400 | **MATCH** |
|
||||||
|
| SRM4 | RANGE 450M (p. 22, Engineering MFD mockup) | 800 (display-only) | **MISMATCH** |
|
||||||
|
| SRM6 | RANGE 450M (p. 22 AND p. 23, two independent screens) | 800 (display-only) | **MISMATCH** |
|
||||||
|
| SLaser | — | 1.5 dmg / 150 | manual-silent |
|
||||||
|
| ERSLaser | — | 2 / 225 | manual-silent |
|
||||||
|
| MLaser | — | 2.5 / 350 | manual-silent |
|
||||||
|
| ERMLaser | — | 3.5 / 500 | manual-silent |
|
||||||
|
| LLaser | — | 5 / 600 | manual-silent |
|
||||||
|
| ERLLaser | — | 6 / 750 | manual-silent |
|
||||||
|
| ERPPC | — | 16 / 950 | manual-silent |
|
||||||
|
| AFC25 | — | 7 / 900 | manual-silent |
|
||||||
|
| AFC50 | — | 13 / 750 | manual-silent |
|
||||||
|
| GAUSS | — | 20 / 900 | manual-silent (pip-color note below) |
|
||||||
|
| LRM5/10/15/20, NRK5 | — | 6000 (display-only) | manual-silent |
|
||||||
|
| ALL weapons, damage | — (zero numbers printed) | Appendix A | manual-silent |
|
||||||
|
|
||||||
|
All four manual figures verified by page RENDER, not OCR alone (the
|
||||||
|
450M reads twice on p. 22 and once on p. 23) [T1].
|
||||||
|
|
||||||
|
**The SRM mismatch read:** 450 (manual) vs 800 (shipped 4.10 content,
|
||||||
|
BLH live pip dump [T2] + field-guide Appendix A). Two mitigations:
|
||||||
|
(a) the manual's panels are mockups from the earlier software revision —
|
||||||
|
the same 4.0→4.10 drift already proven on the Loki loadout rework,
|
||||||
|
the small-laser loop redistribution, and supercharge
|
||||||
|
(`context/pod-hardware.md` §Manual); (b) for MISSILE racks,
|
||||||
|
WeaponRange is fire-control display ONLY (lane 10 [T1]) — real reach is
|
||||||
|
flight physics on both values, so the 450→800 change altered the pip
|
||||||
|
position and panel text, never a trajectory. Verdict: authoring drift
|
||||||
|
between revisions, NOT a port defect; the port displays the shipped
|
||||||
|
4.10 content values.
|
||||||
|
|
||||||
|
**Pip-color side note:** manual p. 10 codes the readiness pips PPC=Blue,
|
||||||
|
Laser=Red, LRM=Green, SRM=Brown, AFC=Orange, **Gauss=Grey** — blue/red
|
||||||
|
match our dumps exactly, SRM "brown" is the authored amber (0.6,0.4,0),
|
||||||
|
but the shipped GAUSS record authors a YELLOW pip (lane 8 [T1]). Same
|
||||||
|
drift class as the SRM range; cosmetic.
|
||||||
|
|
||||||
|
### Leg 2 — the rangefinder scale (what the ladder max actually is)
|
||||||
|
|
||||||
|
The tester model behind the complaint — "the scale maxes at the selected
|
||||||
|
weapon's range" — is FALSE in both the binary and the port. What the
|
||||||
|
HUD right ladder actually is (full detail `context/gauges-hud.md`
|
||||||
|
§Cockpit HUD reticle):
|
||||||
|
|
||||||
|
- **The ladder is a FIXED 0–1200 m scale** — the ctor's hardcoded
|
||||||
|
calibration (@004cc40c, ctor param 11 = 0x44960000) [T1]; port
|
||||||
|
identical (`btl4vid.cpp:2382` kRetMaxRange = 1200.0f). It is never
|
||||||
|
derived from any weapon's WeaponRange or live effectiveRange.
|
||||||
|
- **Each weapon marks its own reach as a PIP** at its authored
|
||||||
|
WeaponRange, CLAMPED into [0..1200] (`AddWeapon` @004cdac0; port
|
||||||
|
transcription `btl4vid.cpp:3139-3147`) [T1]. Caret below a pip = that
|
||||||
|
weapon reaches the target. An LRM/NRK rack authored 6000 therefore
|
||||||
|
pins its pip AT THE LADDER TOP — authentic on both sides.
|
||||||
|
- **The caret is the displayed target range**, sliding at 500 m/s toward
|
||||||
|
the true pick range (HudSimulation part_013.c:5652 [T1]; port
|
||||||
|
mech4.cpp targeting step [T2]); pegs at 1200 with no target. During
|
||||||
|
depth-discontinuity walks the displayed value legitimately lags true
|
||||||
|
range by hundreds of meters (the Gitea #4 measurement).
|
||||||
|
- **The ONE displayed-range divergence:** the binary subtracts
|
||||||
|
`_DAT_004b7ecc` = 100.0f from RangeToTarget (@0x1EC) every frame the
|
||||||
|
timed flag @0x22C is set (timer @0x21C to limit @0x1D8) — the port
|
||||||
|
does NOT implement this state (`hud.cpp:79-82` tracks it;
|
||||||
|
`context/open-questions.md` §HUD range-bias) [T1 read, unimplemented].
|
||||||
|
Direction: the shipped machine UNDER-reads range by up to 100 m in
|
||||||
|
that state; the port never does. So when the state is active a
|
||||||
|
true-850 m missile hit against an 800 pip DISPLAYED as in-range on
|
||||||
|
the pod but displays as beyond-the-pip in the port — a bounded
|
||||||
|
(≤100 m) port-side contributor to "hit beyond the scale" reports.
|
||||||
|
Whether the state ever triggers in the field is unknown (what sets
|
||||||
|
@0x22C is the open question) [T4 for field prevalence].
|
||||||
|
|
||||||
|
**Rangefinder verdict:** the displayed scale is AUTHENTIC — no port
|
||||||
|
miscalibration. "Hits beyond the maximum range on the range scale" is
|
||||||
|
expected, authentic behavior with three drivers, largest first:
|
||||||
|
(1) missile reach is flight physics, not WeaponRange — SRMs genuinely
|
||||||
|
hit past their 800 pip and LRMs past the 1200 ladder top [T1];
|
||||||
|
(2) energy/AC pips mark AUTHORED range while the live fire gate is
|
||||||
|
effectiveRange = (1 − zoneDamage) × authored — a damaged weapon's real
|
||||||
|
reach sits BELOW its pip, never above, so those lanes cannot produce
|
||||||
|
beyond-pip hits; (3) the 500 m/s caret slide lags true range during
|
||||||
|
fast picks. The single true divergence is the unimplemented −100 m
|
||||||
|
bias state (above) — worth reconstructing if Friday's reports cluster
|
||||||
|
in the 0–100 m band past a pip; anything farther past the pip is
|
||||||
|
missiles being missiles.
|
||||||
|
|
||||||
|
**Guide action:** MISMATCHES exist (SRM display ranges + the Gauss pip
|
||||||
|
color) → per the audit rule the field guide was NOT touched; no
|
||||||
|
reassurance paragraph shipped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VELOCITY/DT INTEGRATION — term-by-term audit (2026-08-13)
|
||||||
|
|
||||||
|
**Scope:** the projectile velocity integration, binary vs port pool
|
||||||
|
(`mech4.cpp BTUpdateProjectiles` / `BTPushProjectile`), plus the AC tracer
|
||||||
|
launch-speed constants. Binary side read end-to-end this audit:
|
||||||
|
Missile::MoveAndCollide @004bef78, Seeker::FindTarget @004be9a0 /
|
||||||
|
LeadTarget @004beae4, **MissileThrusterSimulation @004be474 — the
|
||||||
|
standalone Performance body, RECOVERED** (misthrst.cpp's "folded into
|
||||||
|
MoveAndCollide, no distinct @ADDR survives" note predated the 2026-08-06
|
||||||
|
re-export and is corrected), Missile ctor @004bf5b4, launch composer
|
||||||
|
@004bcc60, and the engine Mover lane, decomp↔T0-source matched
|
||||||
|
line-for-line: **FUN_00422360 = Mover::PerformAndWatch**,
|
||||||
|
FUN_00421bac = ApplyWorldAccelerations, FUN_00421e2c =
|
||||||
|
ApplyAirResistanceAndGravity(power), FUN_00421ca8 = CalculateDrag,
|
||||||
|
FUN_00421b2c/FUN_00421b6c = UpdateWorldMotion/UpdateLocalMotion;
|
||||||
|
mover+0x250 = localEnvironment, +0x254 = deadReckoner (MOVER.h/.cpp).
|
||||||
|
|
||||||
|
**The dt model [T1/T0]:** every Performance slice is variable-step REAL
|
||||||
|
SECONDS (the #96 sim-time result), and `Mover::PerformAndWatch`
|
||||||
|
**ZEROES `localAcceleration` at the top of every frame**
|
||||||
|
(@00422360: `FUN_0040a7f4(+0x1dc, &DAT_004e0fd4)` == T0 MOVER.cpp:672
|
||||||
|
`localAcceleration = Motion::Identity`). Thrust, drag and gravity are
|
||||||
|
re-added FRESH each frame and then integrated ×dt (@00421bac:
|
||||||
|
`p += v·dt + ½a·dt²; v += a·dt`). Net missile physics is the clean ODE
|
||||||
|
`dv/dt = ThrusterAccel − COD·ρ·v²` — **there is NO myomer-style 28 Hz
|
||||||
|
per-tick accumulate anywhere in this lane**, so the port's
|
||||||
|
`speed += accel·dt` needs no ×(dt·28) rescale. (The myomer idiom applies
|
||||||
|
to PERSISTENT accumulators; the Mover acceleration is not one.)
|
||||||
|
|
||||||
|
**The headline: drag was wrongly waved off.** All four missile models
|
||||||
|
author linear drag CODs **0.001 on every axis** (BTL4.RES type-15 raw
|
||||||
|
floats read this audit: srm rid=315 / lrm 319 / strk 323 / nrk 327;
|
||||||
|
airDensity = 1.0, engine default). 0.001 multiplies **v²**: it is the
|
||||||
|
flight model's GOVERNOR, and the binary says so itself — the Missile
|
||||||
|
ctor @004bf5b4 (part_013.c:18416) **precomputes terminal speed
|
||||||
|
`sqrt(ThrusterAccel / negCOD.z)` into missile+0x348** and the seeker
|
||||||
|
uses it as the lead time base. Terminal speeds: LRM/Streak/NRK
|
||||||
|
**547.7 u/s**, SRM **774.6 u/s**. The port pool (per #84's "drag
|
||||||
|
~0.001 = negligible") flew undamped: an LRM reached **3030 u/s** by
|
||||||
|
burnout — 5.5× the binary cap. Simulated head-to-head (28 Hz binary
|
||||||
|
loop vs the port step): 800u arrival LRM 2.54 s @ 492 u/s (binary) vs
|
||||||
|
2.22 s @ 695 (undamped port); at t=5 s the undamped LRM had covered
|
||||||
|
3912u @ 1530 u/s vs the binary's 2108u @ 544. **Fixed this build**:
|
||||||
|
the pool integrates `speed += (accel_burn − 0.001·speed²)·dt` for
|
||||||
|
thruster rounds, burn AND coast (a burned-out round decelerates,
|
||||||
|
`v(t) = v0/(1+0.001·v0·t)` — Streak at t=5 s: 256 u/s, was 1005
|
||||||
|
frozen). AC tracers (p.accel==0) untouched — the binary's visible AC
|
||||||
|
round is a renderer cosmetic with no Mover physics.
|
||||||
|
|
||||||
|
| Term | Binary [T1] | Port (pre-audit) | Verdict | Player-visible consequence |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| dt model | variable slice, SECONDS; accel accumulator zeroed/frame (@00422360); integrate ×dt (@00421bac) | `speed += accel·dt`, `pos += vel·dt` | **FAITHFUL** (no per-tick accumulate here) | none |
|
||||||
|
| Launch velocity | authored MuzzleVelocity (z negated) + shooter velocity (@004bcc60 / FUN_004b9cbc) | same (#67) | **FAITHFUL** | none |
|
||||||
|
| Launch acceleration | ZERO in the make message (@004bcc60 authors the DAT_004e0fd4 zero Motion) | thrust applies from frame 1 (binary thruster adds the same frame) | **FAITHFUL** | none |
|
||||||
|
| Thrust | @004be474: while burning, `burnLeft −= dt`, `localAccel += (0,0,−ThrusterAccel)` onto the fresh accumulator ⇒ constant accel | `speed += accel·burn_dt` | **FAITHFUL** | none |
|
||||||
|
| **Drag** | `−COD·ρ·sign(v)·v²` per body axis, EVERY frame, burn + coast (@004bef78, power-2 inline; plain-Projectile dead class uses power 1.0 @004bddec); CODs 0.001 [T1 bytes]; terminal `sqrt(T/c)` precomputed @004bf5b4→+0x348 | **OMITTED** ("negligible") | **WAS DIVERGENT → FIXED-THIS-BUILD** | was: LRM 3030 u/s (5.5× cap), −13% time-to-800u, +40% impact speeds, map-crossing runaways past 2.5 s; now governed at 547.7/774.6 |
|
||||||
|
| Speed clamp | none — drag IS the clamp | none — drag now is | **FAITHFUL** (post-fix) | — |
|
||||||
|
| Velocity↔seeker slaving | burning: lateral local vel/accel zeroed each frame ⇒ velocity ≡ speed×nose; nose slerps ≤ MaxThrusterRotationRate·dt (@004be474); at burnout slaving STOPS (ballistic coast) | rotate-toward-aim at port gains 4/8·dt, speed-normalized; steers through coast | KNOWN-DIVERGENCE [T3] (lane 11) | turn dynamics shape; coast homing (real impacts are in-burn) |
|
||||||
|
| Gravity | none during burn; coast: `worldAccel.y −= 6.5` fresh each frame (env default) + y<−1 kill plane | absent (loft arc + ttl only) | DIVERGENT [T3 **minor**, unfixed] | post-burnout flights only (beyond ~1400u SRM / ~4800u LRM); missed rounds fly level instead of dropping |
|
||||||
|
| Position step | `p += v_old·dt + ½a·dt²` | `p += v_new·dt` (semi-implicit) | FAITHFUL-approx | ≤ ~12u lead over a full SRM burn @60fps; imperceptible |
|
||||||
|
| Expiry | burn+10 s, y<−1 (@004bef78 tail) | same (#168) | **FAITHFUL** | none |
|
||||||
|
| Seeker lead | `aim += targetVel × range/terminal(+0x348)`; loft climb-gated (thruster+0xF0 > _DAT_004bec1c) | live-position re-lead; loft constants exact | KNOWN-DIVERGENCE [T3] (lane 11) | small lead-shape difference |
|
||||||
|
| AC tracer speed | `launchVelocity.z = −effRange/sqrt(2.0·(1/9.8)·muzzleHeight)` @004bc3fc:16157-16164 (bytes @0x4bc678 = `00000040` 2.0f; @0x4bc67c = `8a6c379943c6fad0fb3f` 1/9.8 x87 EXTENDED) | 1.0f stand-ins, no muzzleHeight | **WAS DIVERGENT (cosmetic) → FIXED-THIS-BUILD** | tracer pace now the authentic fall-time scale (~1 s to effRange at a ~5u mount) |
|
||||||
|
|
||||||
|
**Port changes landed (this audit, build clean):**
|
||||||
|
1. `mech4.cpp BTUpdateProjectiles` — quadratic drag `0.001·v²` on thruster
|
||||||
|
rounds, burn and coast; stale "drag negligible / range-capped" comment
|
||||||
|
replaced with the byte-cited model.
|
||||||
|
2. `projweap.cpp` — `_DAT_004bc678 = 2.0f`, `_DAT_004bc67c = 1/9.8`
|
||||||
|
(bytes above), muzzleHeight term restored via
|
||||||
|
`ResolveLaunchVelocity()` (deferred to first use — the port ctor runs
|
||||||
|
mid-roster-build; z==0 sentinel; complete-Mech-TU bridge
|
||||||
|
`BTWeaponMountHeight` in mech4.cpp). Stale "unresolved" constant
|
||||||
|
comments, the :990 "#168 flight-contact model" banner, and the header
|
||||||
|
"FireWeapon BODY NOT recovered" line all swept.
|
||||||
|
3. `misthrst.cpp` — banner corrected: @004be474 IS the standalone
|
||||||
|
MissileThrusterSimulation (steer + burn + thrust-add), recovered.
|
||||||
|
|
||||||
|
**KB corollary (swept):** the "Mover gravity is dt-less per-frame → may
|
||||||
|
need 28 Hz rescale at our frame rate" open question is CLOSED-NO: the
|
||||||
|
per-frame gravity/drag adds land on an accumulator that PerformAndWatch
|
||||||
|
zeroes every frame — they are per-frame *acceleration definitions*, not
|
||||||
|
accumulating increments, and integrate ×dt correctly at ANY frame rate.
|
||||||
|
(The port's separate "environment gravity reads 0" gap stays open.)
|
||||||
|
|||||||
@@ -817,6 +817,28 @@ void
|
|||||||
out = m->localOrigin.linearPosition; // safe non-garbage fallback (owner origin)
|
out = m->localOrigin.linearPosition; // safe non-garbage fallback (owner origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// BTWeaponMountHeight -- the @004bc3fc ctor's muzzle-height operand (velocity/
|
||||||
|
// dt integration audit 2026-08-13): the ProjectileWeapon ctor computes
|
||||||
|
// launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleTransform.y)
|
||||||
|
// (@004bc3fc part_013.c:16157-16164 -- FUN_004b9948 muzzle transform, then
|
||||||
|
// FUN_0040a968 extracts the translation and local_5c = its Y). The binary
|
||||||
|
// takes that Y at CTOR time = the bind-pose mount height; the port resolves
|
||||||
|
// lazily at first use (ctor runs mid-roster-build), so return the mount's
|
||||||
|
// height ABOVE THE MECH ORIGIN -- the same quantity for a standing mech,
|
||||||
|
// and terrain-independent. Complete-Mech-TU bridge per the databinding rule.
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Scalar
|
||||||
|
BTWeaponMountHeight(void *ownerMech, int segIndex)
|
||||||
|
{
|
||||||
|
Mech *m = (Mech *)ownerMech;
|
||||||
|
if (m == 0)
|
||||||
|
return 0.0f;
|
||||||
|
Point3D p;
|
||||||
|
BTResolveWeaponMuzzle(ownerMech, segIndex, p);
|
||||||
|
return p.y - m->localOrigin.linearPosition.y;
|
||||||
|
}
|
||||||
|
|
||||||
// First vital damage-zone index (Mech__DamageZone::vitalDamageZone is protected; Mech has access).
|
// First vital damage-zone index (Mech__DamageZone::vitalDamageZone is protected; Mech has access).
|
||||||
int
|
int
|
||||||
Mech::FirstVitalZone() const
|
Mech::FirstVitalZone() const
|
||||||
@@ -1800,22 +1822,45 @@ static void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #84 THRUSTER BURN: the binary Missile hosts a MissileThruster whose
|
// #84 THRUSTER BURN + QUADRATIC DRAG (velocity/dt integration audit
|
||||||
// authored acceleration drives the round while BurnTime remains
|
// 2026-08-13). Binary model [T1]: Mover::PerformAndWatch @00422360
|
||||||
// (MISTHRST: acceleration = (0,0,-thrusterAccel) in the missile frame,
|
// ZEROES localAcceleration EVERY frame (`FUN_0040a7f4(+0x1dc,
|
||||||
// integrated by Missile::MoveAndCollide @4bef78). The pool flew at
|
// &DAT_004e0fd4)` = the T0 MOVER.cpp:672 `localAcceleration =
|
||||||
// CONSTANT |MuzzleVelocity| -- the rack-eject speed (SRM 100, LRM 30
|
// Motion::Identity`), the MissileThruster's own Performance @004be474
|
||||||
// u/s) -- which is why field missiles crawled (Oracle/Rajel, #84).
|
// (the standalone MissileThrusterSimulation body -- recovered by the
|
||||||
// Authored (BTL4.RES type-15 missile models, +0x44/+0x48): SRM
|
// 2026-08-06 re-export) re-adds (0,0,-ThrusterAccel) while burning,
|
||||||
// burn 2.5s @ 600 u/s^2, LRM 10s @ 300, Streak 3s @ 300. Drag is
|
// and Missile::MoveAndCollide @4bef78 adds the Mover quadratic drag
|
||||||
// authored ~0.001 (negligible at combat ranges) -- integrate accel
|
// -COD*airDensity*sign(v)*v^2 per body axis, then integrates with dt
|
||||||
// along the current heading, un-damped, range-capped as before. [T1
|
// in SECONDS (@00421bac ApplyWorldAccelerations: v += a*dt). Net:
|
||||||
// values / T2 integration]
|
// dv/dt = ThrusterAccel - COD*v^2 (burn; lateral vel zeroed)
|
||||||
if (p.burnLeft > 0.0f && p.accel > 0.0f)
|
// -- 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;
|
Scalar burn_dt = (dt < p.burnLeft) ? dt : p.burnLeft;
|
||||||
p.burnLeft -= burn_dt;
|
if (burn_dt > 0.0f)
|
||||||
Scalar ns = p.speed + p.accel * burn_dt;
|
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)
|
if (p.speed > 0.01f)
|
||||||
{
|
{
|
||||||
Scalar k = ns / p.speed;
|
Scalar k = ns / p.speed;
|
||||||
|
|||||||
@@ -14,11 +14,21 @@
|
|||||||
//
|
//
|
||||||
// Coverage:
|
// Coverage:
|
||||||
// confident : ctor @004be7c4, dtor thunk @004be8bc, TestInstance @004be8e8,
|
// confident : ctor @004be7c4, dtor thunk @004be8bc, TestInstance @004be8e8,
|
||||||
// CreateStreamedSubsystem @004bf8ec
|
// CreateStreamedSubsystem @004bf8ec,
|
||||||
// best-effort: MissileThrusterSimulation -- the Performance method pointer is
|
// MissileThrusterSimulation @004be474 -- the STANDALONE
|
||||||
// installed by the ctor (PTR_LAB_00512b20) but its standalone body
|
// Performance body, RECOVERED by the 2026-08-06 re-export
|
||||||
// is folded into the host Missile::MoveAndCollide @004bef78 (which
|
// (the old "folded into Missile::MoveAndCollide, no distinct
|
||||||
// samples the thruster acceleration buffer at missile+0x234/+0x250).
|
// @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;
|
// excluded : the 0x41xxxx engine vtable slots (pure Subsystem base behaviour;
|
||||||
// vtable @00512bbc overrides only slot0 = destructor)
|
// vtable @00512bbc overrides only slot0 = destructor)
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -28,11 +28,14 @@
|
|||||||
// ProjectileWeaponSimulation @004bbd04 (Performance -- FULLY RECOVERED
|
// ProjectileWeaponSimulation @004bbd04 (Performance -- FULLY RECOVERED
|
||||||
// by capstone disasm, Gitea #12 2026-07-19; see context/decomp-reference.md
|
// by capstone disasm, Gitea #12 2026-07-19; see context/decomp-reference.md
|
||||||
// s5 -- the old RivetGun-modeled body is retired).
|
// s5 -- the old RivetGun-modeled body is retired).
|
||||||
// best-effort (vtable slot proven, function prologue present, BODY NOT
|
// FireWeapon @004bc104 (slot 18) -- RECOVERED by the 2026-08-06
|
||||||
// recovered by the decompiler -- bodies below are reconstructed from context
|
// re-export (part_013.c:16023-16125; trigger-time hitscan, implemented
|
||||||
// and clearly marked):
|
// in the #171 batch. The old "BODY NOT recovered" line here was the
|
||||||
// FireWeapon @004bc104 (slot 18), GetStatusFlags @004bbf88 (slot 12),
|
// stale banner behind the #168 mis-fix -- see the FireWeapon block).
|
||||||
// UpdateWeaponState @004bbc20 (slot 16).
|
// 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):
|
// excluded (belong to sibling/derived classes, NOT ProjectileWeapon):
|
||||||
// Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
|
// Emitter family @004bb120/@004bb888/@004ba4d0-@004bb478 (energy weapons),
|
||||||
// MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
|
// MissileLauncher @004bcff0/@004bd060/@004bd08c & FireWeapon @004bcc60,
|
||||||
@@ -45,6 +48,9 @@
|
|||||||
// _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling)
|
// _DAT_004bc068 = 0000803f = 1.0f (jam-chance clamp ceiling)
|
||||||
// _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel)
|
// _DAT_004bb3b0 = 000080bf = -1.0f (resource "unset" sentinel)
|
||||||
// _DAT_004bb3b4 = 0000003f = 0.5f
|
// _DAT_004bb3b4 = 0000003f = 0.5f
|
||||||
|
// _DAT_004bc678 = 00000040 = 2.0f (tracer fall-time factor; audit 2026-08-13)
|
||||||
|
// _DAT_004bc67c = 8a6c379943c6fad0fb3f = 1/9.8 as 80-bit x87 EXTENDED
|
||||||
|
// (tracer gravity term; audit 2026-08-13)
|
||||||
// DAT_004e0f74 = {0,0,0} (zero vector)
|
// DAT_004e0f74 = {0,0,0} (zero vector)
|
||||||
// DAT_004e0fd4 = {0,0,0} (zero point)
|
// DAT_004e0fd4 = {0,0,0} (zero point)
|
||||||
// 0x40400000 = 3.0f (TotalTimeToEject default)
|
// 0x40400000 = 3.0f (TotalTimeToEject default)
|
||||||
@@ -145,8 +151,18 @@ namespace {
|
|||||||
static const Scalar _DAT_004bc100 = 1.0f; // time-of-flight bias
|
static const Scalar _DAT_004bc100 = 1.0f; // time-of-flight bias
|
||||||
static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient
|
static const Scalar _DAT_004bc064 = 0.41f; // heat -> jam-chance coefficient
|
||||||
static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling
|
static const Scalar _DAT_004bc068 = 1.0f; // jam-chance clamp ceiling
|
||||||
static const Scalar _DAT_004bc678 = 1.0f; // muzzle-speed term (unresolved)
|
// Tracer launch-speed constants -- BYTE-CONFIRMED by the 2026-08-13 weapons
|
||||||
static const Scalar _DAT_004bc67c = 1.0f; // muzzle-speed term (unresolved)
|
// 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
|
// FUN_00408050 -- uniform [0,1) random (jam roll). The old `return 0.0f` stub ALWAYS
|
||||||
// jammed (0 < any positive jam chance), so a projectile weapon could never fire. A real
|
// jammed (0 < any positive jam chance), so a projectile weapon could never fire. A real
|
||||||
@@ -311,11 +327,16 @@ ProjectileWeapon::ProjectileWeapon(
|
|||||||
minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; // +0x1C8 -> 0x404
|
minVoltagePercentToFire = subsystem_resource->minVoltagePercentToFire; // +0x1C8 -> 0x404
|
||||||
tracerCounter = 0; // 0x408
|
tracerCounter = 0; // 0x408
|
||||||
|
|
||||||
// launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed)
|
// launchVelocity seed = (0,0,0) then z = -(effectiveRange / muzzleSpeed),
|
||||||
|
// muzzleSpeed = sqrt((1/9.8) * 2.0 * muzzleHeight) -- @004bc3fc:16157-16164
|
||||||
|
// resolves the muzzle transform (FUN_004b9948) INSIDE the ctor and uses its
|
||||||
|
// translation Y. The port DEFERS the z term to first use
|
||||||
|
// (ResolveLaunchVelocity below): our ctor runs mid-roster-build, where the
|
||||||
|
// segment resolve can fall back to the mech origin and would bake a garbage
|
||||||
|
// constant. z == 0 is the unresolved sentinel (the binary's z is always
|
||||||
|
// negative after its ctor).
|
||||||
launchVelocity = Vector3D(0,0,0); // FUN_00408440(this+0x410, DAT_004e0f74)
|
launchVelocity = Vector3D(0,0,0); // FUN_00408440(this+0x410, DAT_004e0f74)
|
||||||
Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678); // FUN_004dd138
|
|
||||||
tracerOrigin = Vector3D(0,0,0); // FUN_00408440(this+0x42c, DAT_004e0f74)
|
tracerOrigin = Vector3D(0,0,0); // FUN_00408440(this+0x42c, DAT_004e0f74)
|
||||||
launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed)
|
|
||||||
|
|
||||||
totalTimeToEject = 3.0f; // 0x3F0 (0x40400000)
|
totalTimeToEject = 3.0f; // 0x3F0 (0x40400000)
|
||||||
timeToEject = totalTimeToEject; // 0x3F4
|
timeToEject = totalTimeToEject; // 0x3F4
|
||||||
@@ -706,6 +727,30 @@ Logical
|
|||||||
return (p > UniformRandom()) ? True : False; // FUN_00408050
|
return (p > UniformRandom()) ? True : False; // FUN_00408050
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The @004bc3fc ctor launch-speed expression, deferred to first use (see the
|
||||||
|
// ctor note). Byte-grounded constants (audit 2026-08-13): @0x4bc678 = 2.0f,
|
||||||
|
// @0x4bc67c = 1/9.8 (80-bit x87 extended `8a6c379943c6fad0fb3f`):
|
||||||
|
// launchVelocity.z = -effectiveRange / sqrt((1/9.8) * 2.0 * muzzleHeight)
|
||||||
|
// The binary computes it once at ctor time (bind pose, effectiveRange still ==
|
||||||
|
// authored WeaponRange); the port computes it once at first use with the mount
|
||||||
|
// height above the mech origin -- the same quantity for a standing mech.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
ProjectileWeapon::ResolveLaunchVelocity()
|
||||||
|
{
|
||||||
|
if (launchVelocity.z != 0.0f)
|
||||||
|
return; // already resolved
|
||||||
|
extern Scalar BTWeaponMountHeight(void *ownerMech, int segIndex);
|
||||||
|
Scalar muzzleHeight = BTWeaponMountHeight(owner, GetSegmentIndex());
|
||||||
|
if (muzzleHeight < 0.5f)
|
||||||
|
muzzleHeight = 0.5f; // port guard: unresolved segment (muzzle==origin
|
||||||
|
// fallback) -- the binary's bind-pose height is
|
||||||
|
// always positive; keeps sqrt/divide sane
|
||||||
|
Scalar muzzleSpeed = (Scalar)Sqrt(_DAT_004bc67c * _DAT_004bc678 * muzzleHeight); // FUN_004dd138
|
||||||
|
launchVelocity.z = -(effectiveRange / muzzleSpeed); // this[0x106] = -(this[0xca]/speed)
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// @004bc06c -- lead-solution time-of-flight. Sample the current target
|
// @004bc06c -- lead-solution time-of-flight. Sample the current target
|
||||||
// position, advance it by launchVelocity, and re-run targeting. With no target
|
// position, advance it by launchVelocity, and re-run targeting. With no target
|
||||||
@@ -715,6 +760,7 @@ Logical
|
|||||||
Scalar
|
Scalar
|
||||||
ProjectileWeapon::ComputeTimeOfFlight()
|
ProjectileWeapon::ComputeTimeOfFlight()
|
||||||
{
|
{
|
||||||
|
ResolveLaunchVelocity(); // deferred @004bc3fc z term
|
||||||
GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420)
|
GetTargetPosition(leadPosition); // FUN_004b9cbc(this, this+0x420)
|
||||||
leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410
|
leadPosition.x += launchVelocity.x; // this+0x420 += this+0x410
|
||||||
leadPosition.y += launchVelocity.y;
|
leadPosition.y += launchVelocity.y;
|
||||||
@@ -985,10 +1031,11 @@ void
|
|||||||
// single-panel Damage (amount = batchedCount x DamageAmount) to the LOCKED
|
// single-panel Damage (amount = batchedCount x DamageAmount) to the LOCKED
|
||||||
// target *(mech+0x388) via SendDamageMessage @004b9728 -- at fire time.
|
// target *(mech+0x388) via SendDamageMessage @004b9728 -- at fire time.
|
||||||
// A locked AC inside range cannot miss; out of range the batch silently
|
// A locked AC inside range cannot miss; out of range the batch silently
|
||||||
// discards (tracer still flies). Reconstruction pending the #171 group
|
// discards (tracer still flies). The body below IMPLEMENTS that hitscan
|
||||||
// decision (Oracle's HOLD) -- the current port body below still carries
|
// (landed in the #171 batch, bbb3b4e; the earlier "still carries the #168
|
||||||
// the #168 flight-contact model. MissileLauncher (@004bcc60) is the
|
// flight-contact model" sentence here outlived the change -- the exact
|
||||||
// genuinely-flying family (Model B) and overrides this slot.
|
// stale-banner genus this block documents). MissileLauncher (@004bcc60)
|
||||||
|
// is the genuinely-flying family (Model B) and overrides this slot.
|
||||||
//
|
//
|
||||||
// Gitea #12: the view/target gate, the ammo pull and the recoil set are
|
// Gitea #12: the view/target gate, the ammo pull and the recoil set are
|
||||||
// STRIPPED from this body -- they belong to the CALLER (the recovered
|
// STRIPPED from this body -- they belong to the CALLER (the recovered
|
||||||
@@ -1110,7 +1157,10 @@ void
|
|||||||
// The VISIBLE round: the binary's 0xBCD renderer tracer, cosmetic only
|
// The VISIBLE round: the binary's 0xBCD renderer tracer, cosmetic only
|
||||||
// (the shell entity 0xBD1 is dead code -- nothing shipped ever spawns a
|
// (the shell entity 0xBD1 is dead code -- nothing shipped ever spawns a
|
||||||
// damage-carrying ballistic round). Fly the pool round as that visual:
|
// 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
|
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||||
+ launchVelocity.y*launchVelocity.y
|
+ launchVelocity.y*launchVelocity.y
|
||||||
+ launchVelocity.z*launchVelocity.z);
|
+ launchVelocity.z*launchVelocity.z);
|
||||||
@@ -1178,6 +1228,7 @@ void
|
|||||||
// plus the DAFC muzzle flash.
|
// plus the DAFC muzzle flash.
|
||||||
Point3D mz;
|
Point3D mz;
|
||||||
GetMuzzlePoint(mz); // @004b9948
|
GetMuzzlePoint(mz); // @004b9948
|
||||||
|
ResolveLaunchVelocity(); // deferred @004bc3fc z term
|
||||||
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||||
+ launchVelocity.y*launchVelocity.y
|
+ launchVelocity.y*launchVelocity.y
|
||||||
+ launchVelocity.z*launchVelocity.z);
|
+ launchVelocity.z*launchVelocity.z);
|
||||||
|
|||||||
@@ -283,6 +283,14 @@ class NotationFile;
|
|||||||
Scalar
|
Scalar
|
||||||
ComputeTimeOfFlight();
|
ComputeTimeOfFlight();
|
||||||
|
|
||||||
|
// The @004bc3fc ctor launch-speed expression
|
||||||
|
// (launchVelocity.z = -effRange / sqrt((1/9.8) * 2.0 * muzzleHeight);
|
||||||
|
// constants byte-confirmed @0x4bc678/@0x4bc67c, audit 2026-08-13),
|
||||||
|
// deferred to first use because the port ctor runs mid-roster-build.
|
||||||
|
// z == 0 is the unresolved sentinel; no data member added (layout locked).
|
||||||
|
void
|
||||||
|
ResolveLaunchVelocity();
|
||||||
|
|
||||||
// --- cross-family isolation helpers (owning Mech subsystem roster +
|
// --- cross-family isolation helpers (owning Mech subsystem roster +
|
||||||
// cockpit controls live in the mech/controls families). Declared
|
// cockpit controls live in the mech/controls families). Declared
|
||||||
// here so the recovered bodies compile; the mech family wires the
|
// here so the recovered bodies compile; the mech family wires the
|
||||||
|
|||||||
Reference in New Issue
Block a user