#84: missiles ACCELERATE like the binary -- the pool flew at constant

rack-eject speed; the authored MissileThruster was never applied

The binary Missile hosts a MissileThruster subsystem: authored
acceleration for BurnTime seconds after the MuzzleVelocity eject.
Authored values decoded from BTL4.RES (type-15 missile model records,
reached from the AmmoBin's ammoModelFile via the type-1 MODELLIST
indirection): SRM 2.5s @ 600 u/s^2, LRM 10s @ 300 (climb 50), Streak
3s @ 300 (turn 360deg/s), NARC 10s @ 300; splash 30 all (port constant
was already right); authored drag ~0.001 = negligible.

The port pool flew every round at CONSTANT |MuzzleVelocity| -- SRMs 100
u/s, LRMs 30 u/s (10x slower than authored) -- the entirety of the
night-7 "missiles are very slow" report, and the driver of the
"explosion before the missiles arrive" perception (the salvo-lead
detonates while the slow spread rounds straggle in).

Fix: BTMissileThrustOf (mech4.cpp) lazily parses + caches the RES
thruster table (ModelList ids aliased to their type-15 member's
burn/accel); both launch paths (master FireWeapon + the replicant salvo
mirror -- peers see the same speed) resolve through the launcher's bin
and pass burn/accel into the pool; the advance integrates speed +=
accel*dt while burn remains, heading preserved.  Ballistic rounds
(autocannon/gauss) pass 0/0 -- unchanged.  Impact log now prints
v=/burnLeft= evidence.

Verified live (solo, BT_SPAWN_ENEMY + BT_AF_MISSILE): thrust table 8
entries cached; Black Hawk Streak resolve burn=3 accel=300; impacts at
v=277 u/s (was a constant 100).  Fun authenticity note: the Black
Hawk's "SRM6" fires strk (STREAK) ammo per its authored bin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
This commit is contained in:
Joe DiPrima
2026-07-31 10:41:39 -05:00
co-authored by Claude Fable 5
parent ac7c46b87a
commit 4704ab3d41
6 changed files with 196 additions and 5 deletions
+129 -1
View File
@@ -843,6 +843,8 @@ struct BTProjectile {
// (matches the ONE arcade cluster missile). 0 on every
// other round (a straight tracer / non-lead volley round)
// so splash is NOT re-applied + floored-at-1 per round.
Scalar accel; // #84: authored ThrusterAcceleration (u/s^2; 0 = unpowered round)
Scalar burnLeft; // #84: authored BurnTime remaining (s); accel applies while > 0
int active;
};
static BTProjectile gProjectiles[64];
@@ -882,6 +884,105 @@ void BTProjectilesClearAll(void)
gProjectiles[i].active = 0;
}
//###########################################################################
// #84: the missile-model thruster table (BTL4.RES type-15 "GameModel" 0x54
// records; the same records the binary's Missile ctor copies +0x40..0x4C
// from). The launcher's AmmoBin carries the model's RESOURCE ID
// (ammoModelFile); resolve burn/accel from the RES directory, parsed once
// 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)
{
enum { kMax = 32 };
static int s_loaded = 0, s_n = 0;
static int s_id[kMax];
static float s_burn[kMax], s_accel[kMax];
if (!s_loaded)
{
s_loaded = 1;
FILE *f = fopen("BTL4.RES", "rb"); // cwd == content\ at runtime
if (f != 0)
{
int hdr[3];
if (fread(hdr, 4, 3, f) == 3 && hdr[2] > 0 && hdr[2] < 65536)
{
int maxID = hdr[2];
unsigned *dir = (unsigned *)malloc(maxID * 4);
if (dir != 0 && (int)fread(dir, 4, maxID, f) == maxID)
{
// pass 1: the type-15 GameModel 0x54 records (the missile models)
for (int i = 0; i < maxID && s_n < kMax; ++i)
{
if (dir[i] == 0) continue;
int rh[2]; // rid, rtype
unsigned meta[4]; // prio, flags, off, len
if (fseek(f, dir[i], SEEK_SET) != 0) continue;
if (fread(rh, 4, 2, f) != 2) continue;
if (rh[1] != 15) continue; // GameModel records only
if (fseek(f, dir[i] + 40, SEEK_SET) != 0) continue;
if (fread(meta, 4, 4, f) != 4) continue;
if (meta[3] != 0x54) continue; // the missile model record
float tail[5]; // +0x40: rot,burn,accel,climb,splash
if (fseek(f, meta[2] + 0x40, SEEK_SET) != 0) continue;
if (fread(tail, 4, 5, f) != 5) continue;
s_id[s_n] = rh[0];
s_burn[s_n] = tail[1];
s_accel[s_n] = tail[2];
++s_n;
}
// pass 2: the AmmoBin's ammoModelFile references the TYPE-1
// MODELLIST ('srm'/'lrm'/'strk'/'nrk'), which redirects to the
// model family (type 10/3/15/18 members). Alias each list id
// to its type-15 member's thruster values.
const int nModels = s_n;
for (int i = 0; i < maxID && s_n < kMax; ++i)
{
if (dir[i] == 0) continue;
int rh[2];
unsigned meta[4];
if (fseek(f, dir[i], SEEK_SET) != 0) continue;
if (fread(rh, 4, 2, f) != 2) continue;
if (rh[1] != 1) continue; // ModelList only
if (fseek(f, dir[i] + 40, SEEK_SET) != 0) continue;
if (fread(meta, 4, 4, f) != 4) continue;
int lst[9]; // count + up to 8 member ids
if (meta[3] < 8 || meta[3] > 36) continue;
if (fseek(f, meta[2], SEEK_SET) != 0) continue;
int cnt = (int)(meta[3] / 4) - 1;
if (fread(lst, 4, cnt + 1, f) != (size_t)(cnt + 1)) continue;
if (lst[0] < 1 || lst[0] < cnt) cnt = lst[0] < cnt ? lst[0] : cnt;
for (int m = 1; m <= cnt; ++m)
for (int k = 0; k < nModels; ++k)
if (s_id[k] == lst[m])
{
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_n;
m = cnt + 1; // one alias per list
break;
}
}
}
if (dir != 0) free(dir);
}
fclose(f);
DEBUG_STREAM << "[projectile] thrust table: " << s_n
<< " missile model(s) cached from BTL4.RES\n" << std::flush;
}
}
for (int i = 0; i < s_n; ++i)
{
if (s_id[i] == model_id)
{
*out_burn = s_burn[i]; *out_accel = s_accel[i];
return 1;
}
}
return 0;
}
extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, float);
//###########################################################################
@@ -1207,7 +1308,8 @@ void
void
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided,
int weapon_subsys, int splash_burst, int muzzle_seg, int damage_type)
int weapon_subsys, int splash_burst, int muzzle_seg, int damage_type,
Scalar thrust_accel, Scalar thrust_burn)
{
// MUZZLE (muzzle wave, 2026-07-12): the passed muzzle is now the weapon's
// AUTHENTIC mount segment (GetMuzzlePoint reads the real segmentIndex --
@@ -1336,6 +1438,8 @@ void
p.shooter = (Entity *)shooter;
p.weaponSubsys = weapon_subsys;
p.splashBurst = splash_burst; // >0 only on a salvo-lead round
p.accel = (thrust_accel > 0.0f) ? thrust_accel : 0.0f; // #84 thruster
p.burnLeft = (thrust_burn > 0.0f) ? thrust_burn : 0.0f;
// RACK-TUBE SPREAD [T3, physically grounded]: the binary's Missile
// entities each launch from their own rack tube (per-tube authored
@@ -1435,6 +1539,29 @@ 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)
{
Scalar burn_dt = (dt < p.burnLeft) ? dt : p.burnLeft;
p.burnLeft -= burn_dt;
Scalar ns = p.speed + p.accel * burn_dt;
if (p.speed > 0.01f)
{
Scalar k = ns / p.speed;
p.vel.x *= k; p.vel.y *= k; p.vel.z *= k;
}
p.speed = ns;
}
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt;
p.traveled += p.speed * dt;
@@ -1590,6 +1717,7 @@ static void
DEBUG_STREAM << "[projectile] IMPACT damage=" << p.damage
<< " subsys=" << p.weaponSubsys
<< " v=" << p.speed << " burnLeft=" << p.burnLeft // #84 thrust evidence
<< (mgr ? " (msgmgr bundled)" : " (direct)")
<< " (zone cyl-resolved)\n" << std::flush;