#113 follow-on: missiles fly their AUTHORED agility -- the turn-rate cap is wired (model+0x40 deg/s, the field the #113 audit identified: pi/180-scaled into the binary thruster @004be7c4[0x3a], _DAT_004be8b8 byte-verified [T1]). The thrust table now reads tail[0] (it always fetched and dropped it); mislanch passes the rate via a salvo-scoped latch (BTPushSetTurnRate -- deliberately NOT a BTPushProjectile signature change: six call sites + three externs under /FORCE is the silent-AV churn class), master AND replicant mirror so peers see the same arcs; the steering keeps the field-verified exponential blend [T3] and CAPS this frame's heading change at turnDps*dt -- below the cap the old behavior is bit-identical, above it the round slews at the authored rate. BT_TURN_AUTH=0 restores uncapped; [projturn] CAP receipts under BT_PROJ_LOG. A/B bench (2-node standoff, scratchpad/night17/turn_bench.sh): AUTH runs 43/69 CAP receipts, ALL at 60dps (the blend wants <=~220 so 360 rounds are authentically unconstrained; LRMs slew ~3.6x slower during corrections -- the era 'sluggish 60 deg/sec' look); OFF runs ZERO CAP lines (negative control); impacts 25/9 vs 7/4 -- no accuracy collapse. Data bonus: mad2's 'LRM15'-labeled racks resolve 360dps (Narc-profile rounds, the guide's authored-mislabeling lore confirmed by receipts); madcat's are true 60dps LRMs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bw71WVsccjwW7uKRg4aWD
This commit is contained in:
Joe DiPrima
2026-08-14 12:43:24 -05:00
co-authored by Claude Fable 5
parent 569b8d31b5
commit cec64583db
2 changed files with 610 additions and 533 deletions
+74 -3
View File
@@ -875,6 +875,8 @@ struct BTProjectile {
Scalar aimOffsetY; // vertical aim offset vs the target's origin (live re-lead)
Scalar damage; // <= 0 -> VISUAL-ONLY round (replicant-side salvo mirror)
int guided; // 1 = missile (seeker loft + steering); 0 = ballistic (straight)
float turnDps; // (#113) authored turn-rate cap, deg/s (model+0x40 [T1]);
// 0 = uncapped (legacy tuned steering only)
Entity *shooter; // the firing mech (messmgr explosion bundling at impact)
int damageType; // the FIRING WEAPON's authentic damageData.damageType
// (issue: was hardcoded Explosive at impact, so
@@ -950,12 +952,19 @@ void BTProjectilesClearAll(void)
// and cached. Reading the shipped RES directly mirrors the binary's stream
// (the engine-side in-memory path has no raw-record accessor in the port).
//###########################################################################
int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel)
int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel,
float *out_turn_dps)
{
enum { kMax = 32 };
static int s_loaded = 0, s_n = 0;
static int s_id[kMax];
static float s_burn[kMax], s_accel[kMax];
// (#113 audit, 2026-08-14) model+0x40 is the AUTHORED TURN RATE in deg/s
// (60 lrm / 360 strk+nrk) -- the Missile ctor @004bf5b4 stack-builds the
// thruster record from the model and @004be7c4 scales this field by
// _DAT_004be8b8 = pi/180 into thruster[0x3a] rad/s [T1 raw bytes]. The
// tail read below always fetched it as tail[0] ("rot") and dropped it.
static float s_turn[kMax];
if (!s_loaded)
{
s_loaded = 1;
@@ -987,6 +996,7 @@ int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel)
s_id[s_n] = rh[0];
s_burn[s_n] = tail[1];
s_accel[s_n] = tail[2];
s_turn[s_n] = tail[0]; // authored turn rate, deg/s
++s_n;
}
// pass 2: the AmmoBin's ammoModelFile references the TYPE-1
@@ -1017,6 +1027,7 @@ int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel)
s_id[s_n] = rh[0]; // the LIST id aliases the model
s_burn[s_n] = s_burn[k];
s_accel[s_n] = s_accel[k];
s_turn[s_n] = s_turn[k];
++s_n;
m = cnt + 1; // one alias per list
break;
@@ -1035,12 +1046,24 @@ int BTMissileThrustOf(int model_id, float *out_burn, float *out_accel)
if (s_id[i] == model_id)
{
*out_burn = s_burn[i]; *out_accel = s_accel[i];
if (out_turn_dps != 0)
*out_turn_dps = s_turn[i];
return 1;
}
}
return 0;
}
// (#113) Salvo-scoped turn-rate hand-off: mislanch sets the authored rate
// just before pushing a salvo (master AND replicant mirror), BTPushProjectile
// consumes it for guided rounds. A latch instead of a BTPushProjectile
// signature change ON PURPOSE: six call sites + three extern decls under
// /FORCE is exactly the silent-AV churn the conventions warn about. Only
// mislanch pushes guided rounds in production (the BT_JUMP test hook's
// guided push inherits whatever was last set -- test-only, harmless).
static float gPushTurnDps = 0.0f; // 0 = uncapped (legacy steering)
void BTPushSetTurnRate(float dps) { gPushTurnDps = dps; }
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
//###########################################################################
@@ -1694,6 +1717,7 @@ void
p.damage = damage;
p.damageType = damage_type; // weapon's authentic type (issue fix)
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
p.turnDps = (guided != 0 && gPushTurnDps > 0.0f) ? gPushTurnDps : 0.0f; // (#113) authored agility
p.shooter = (Entity *)shooter;
p.weaponSubsys = weapon_subsys;
p.splashBurst = splash_burst; // >0 only on a salvo-lead round
@@ -1825,7 +1849,8 @@ static void
if (range > 0.001f)
{
Scalar inv = 1.0f / range;
Scalar cx = p.vel.x / p.speed, cy = p.vel.y / p.speed, cz = p.vel.z / p.speed;
Scalar ox = p.vel.x / p.speed, oy = p.vel.y / p.speed, oz = p.vel.z / p.speed;
Scalar cx = ox, cy = oy, cz = oz;
// MissileTurnGain (4.0, _DAT_004bf5a4) far out; DOUBLED inside
// the no-loft radius so the dive converges onto the contact
// sphere (the port shape of the binary's squared-error terminal
@@ -1835,7 +1860,53 @@ static void
Scalar cl = (Scalar)sqrtf(cx*cx + cy*cy + cz*cz);
if (cl > 0.001f)
{
p.vel.x = cx/cl*p.speed; p.vel.y = cy/cl*p.speed; p.vel.z = cz/cl*p.speed;
cx /= cl; cy /= cl; cz /= cl;
// (#113, 2026-08-14) THE AUTHORED TURN-RATE CAP. The binary
// flies each round at its authored agility: model+0x40 deg/s
// (lrm 60, strk/nrk 360), scaled pi/180 into the thruster
// (@004bf5b4 stack record -> @004be7c4 [0x3a], _DAT_004be8b8
// byte-verified) [T1]. The exponential blend above is the
// field-verified port shape [T3]; the CAP is where the
// per-round personality lives -- limit this frame's heading
// change to turnDps*dt. Below the cap the old behavior is
// bit-identical; above it the round slews at the authored
// rate (the LRM's slow arc-over, the Streak/Narc "angry
// bees"). BT_TURN_AUTH=0 restores uncapped (A/B lever).
{
static int s_turnAuth = -1;
if (s_turnAuth < 0)
{
const char *tv = getenv("BT_TURN_AUTH");
s_turnAuth = (tv != 0 && *tv == '0') ? 0 : 1;
}
if (s_turnAuth && p.turnDps > 0.0f)
{
Scalar dot = ox*cx + oy*cy + oz*cz;
if (dot > 1.0f) dot = 1.0f;
if (dot < -1.0f) dot = -1.0f;
Scalar ang = (Scalar)acosf((float)dot);
Scalar lim = p.turnDps * 0.01745329f * dt;
if (ang > lim && ang > 1e-4f)
{
if (getenv("BT_PROJ_LOG"))
{
static int s_tc = 0;
if ((s_tc++ % 61) == 0) // 61: coprime sampler
DEBUG_STREAM << "[projturn] CAP "
<< p.turnDps << " dps (wanted "
<< (float)(ang / dt * 57.29578f)
<< ") dmg=" << p.damage << "\n" << std::flush;
}
Scalar t = lim / ang; // partial slew (small
cx = ox + (cx - ox) * t; // angles: lerp+renorm
cy = oy + (cy - oy) * t; // ~= slerp)
cz = oz + (cz - oz) * t;
cl = (Scalar)sqrtf(cx*cx + cy*cy + cz*cz);
if (cl > 0.001f) { cx /= cl; cy /= cl; cz /= cl; }
}
}
}
p.vel.x = cx*p.speed; p.vel.y = cy*p.speed; p.vel.z = cz*p.speed;
}
}
}
File diff suppressed because it is too large Load Diff