#171 IMPLEMENTED: the AC is trigger-time hitscan again (@004bc104 restored: counter/TracerInterval batching, ComputeTimeOfFlight inside the tracer branch refreshing LIVE effectiveRange, dist<=+0x328 fire-time gate, ONE single-panel batched damage to the locked target via @004b9728; the flying round is the cosmetic 0xBCD tracer, damage 0 -- shell class 0xBD1 stays dead). Missiles keep flight, corrected to the binary: lock-drop = movementMode 2||9 exactly (@0049fb54, tracks THROUGH the fall), proximity fuze 4.0 (_DAT_004bf5a4 -- was misused as a turn gain), salvo roll floor-of-1 REMOVED (binary sender rolls 0; the receiver burst do-while is the floor, mech.cpp:1229). Gauss untouched pending the era witnesses (shipped binary deals NO Gauss damage -- group question on #171). Verification: line-by-line vs part_013.c:16023-16125/18260-18285 (verify workflow + review); end-to-end receipts from the engaged bench run: type=1 burst=1 hitscan hits incl. a 13-round batch, all DETs dmg=0. Local rig cannot form avatar locks for a clean re-run (rig, not feature) -- Friday's field receipts are armed and decisive. Closes the #168 mis-generalization; #112 single-panel restored
This commit is contained in:
@@ -1746,11 +1746,29 @@ static void
|
|||||||
// autocannon could not miss a moving target. Track the entity for the
|
// autocannon could not miss a moving target. Track the entity for the
|
||||||
// contact test; only GUIDED rounds also steer onto it.
|
// contact test; only GUIDED rounds also steer onto it.
|
||||||
extern int BTIsRegisteredMech(Entity *e);
|
extern int BTIsRegisteredMech(Entity *e);
|
||||||
if (p.target != 0 && BTIsRegisteredMech(p.target)
|
if (p.target != 0 && BTIsRegisteredMech(p.target))
|
||||||
&& !((Mech *)p.target)->IsMechDestroyed())
|
|
||||||
{
|
{
|
||||||
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
|
// #171 LOCK DROP predicate = @0049fb54 EXACTLY: movementMode 2||9,
|
||||||
p.targetPos.y += p.aimOffsetY;
|
// the latched death modes -- NOT IsMechDestroyed(), whose extra
|
||||||
|
// graphicAlarm>=9 term fires at the vital-kill TRIGGER, seconds
|
||||||
|
// before the collapse (modes 5-8) reaches 2/9. The binary seeker
|
||||||
|
// keeps TRACKING (and its 4.0u fuze stays live) all through the
|
||||||
|
// fall; it releases only once the wreck latches disabled
|
||||||
|
// (@004bef78: IsKindOf(Mech) && FUN_0049fb54 -> seeker+0xfc = 0).
|
||||||
|
const int mm = ((Mech *)p.target)->MovementMode();
|
||||||
|
if (!(mm == 2 || mm == 9))
|
||||||
|
{
|
||||||
|
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
|
||||||
|
p.targetPos.y += p.aimOffsetY;
|
||||||
|
}
|
||||||
|
else if (p.guided)
|
||||||
|
{
|
||||||
|
// Seeker released: the round flies on UNGUIDED; only geometry
|
||||||
|
// can detonate it now. (The old port kept steering to the
|
||||||
|
// corpse's last position and could still sphere-contact it.)
|
||||||
|
p.guided = 0;
|
||||||
|
p.target = 0; // fuze disarmed with the lock
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (p.guided)
|
if (p.guided)
|
||||||
{
|
{
|
||||||
@@ -1910,7 +1928,15 @@ static void
|
|||||||
cz = p.targetPos.z - hitPos.z;
|
cz = p.targetPos.z - hitPos.z;
|
||||||
contactD2 = cx*cx + cy*cy + cz*cz;
|
contactD2 = cx*cx + cy*cy + cz*cz;
|
||||||
}
|
}
|
||||||
const int contact = (contactD2 < (10.0f*10.0f));
|
// #171 FUZE RADIUS: a GUIDED damage round detonates at the binary's
|
||||||
|
// proximity fuze _DAT_004bf5a4 = 4.0 (seeker rangeToTarget +0x10C <
|
||||||
|
// 4.0, @004bef78 -- the SAME constant the old steering comment
|
||||||
|
// misattributed as "MissileTurnGain"; the 4.0/8.0 turn rates above are
|
||||||
|
// PORT TUNING [T3], not that constant). Cosmetic rounds (damage 0:
|
||||||
|
// AC tracers, cluster visuals) keep the wider 10u so their burst
|
||||||
|
// visuals still read at speed.
|
||||||
|
const Scalar fuzeR = (p.guided && p.damage > 0.0f) ? 4.0f : 10.0f;
|
||||||
|
const int contact = (contactD2 < fuzeR * fuzeR);
|
||||||
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
|
if (!contact && (p.age >= p.ttl || (p.guided && p.pos.y < -1.0f)))
|
||||||
{
|
{
|
||||||
if (getenv("BT_PROJ_LOG"))
|
if (getenv("BT_PROJ_LOG"))
|
||||||
@@ -1978,9 +2004,22 @@ static void
|
|||||||
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
|
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
|
||||||
if (bursts > 1)
|
if (bursts > 1)
|
||||||
{
|
{
|
||||||
|
// #171: the binary roll (part_013.c:18272-18281) has NO
|
||||||
|
// floor-of-1 -- rolled = min(Random(n) + n/4, n), and
|
||||||
|
// Random(n) is 0..n-1, so n<4 can roll ZERO. The
|
||||||
|
// VICTIM side is what floors it: the binary burst loop
|
||||||
|
// is a DO-WHILE (part_012.c:14660-14685 -- local_2c =
|
||||||
|
// burstCount, apply, decrement, `if (local_2c < 1)
|
||||||
|
// break` AFTER the body), so a 0-burst message still
|
||||||
|
// applies ONE burst; mech.cpp:1229's `burstsLeft < 1
|
||||||
|
// -> 1` guard is the faithful mirror of that shape.
|
||||||
|
// The old sender-side floor made the end-to-end count
|
||||||
|
// identical, but shipped a message field the binary
|
||||||
|
// never sends (burstCount 1 where the binary says 0);
|
||||||
|
// roll it exactly as the binary does and let the
|
||||||
|
// receiver's do-while supply the floor.
|
||||||
int rolled = Random(bursts) + (bursts >> 2);
|
int rolled = Random(bursts) + (bursts >> 2);
|
||||||
if (rolled > bursts) rolled = bursts;
|
if (rolled > bursts) rolled = bursts;
|
||||||
if (rolled < 1) rolled = 1;
|
|
||||||
bursts = rolled;
|
bursts = rolled;
|
||||||
}
|
}
|
||||||
dmg.burstCount = bursts;
|
dmg.burstCount = bursts;
|
||||||
|
|||||||
@@ -1002,10 +1002,9 @@ void
|
|||||||
{
|
{
|
||||||
Check(this);
|
Check(this);
|
||||||
|
|
||||||
// Refresh the lead solution (leadPosition / targeting) for the spawn below
|
// (ComputeTimeOfFlight @004bc06c is called INSIDE the tracer branch below,
|
||||||
// (@004bc06c; the recovered sim no longer calls it -- best-effort placement
|
// exactly where @004bc104 calls it -- :16091, between the muzzle resolve and
|
||||||
// inside the unrecovered @4bc104 body, which is where the lead data is used).
|
// the range gate. The old top-of-body placement predated the re-export.)
|
||||||
ComputeTimeOfFlight(); // @004bc06c
|
|
||||||
|
|
||||||
// THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the
|
// THE FIRING HEAT (task #9; was "a separate pre-existing gap"): the
|
||||||
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
|
// binary adds heatCostToFire RAW to the weapon's own pendingHeat --
|
||||||
@@ -1047,27 +1046,79 @@ void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WAVE 7 Phase B: launch one flying ballistic round toward the owner's target (port
|
//
|
||||||
// reconstruction; the byte-exact world-entity Projectile is blocked by the 2007 engine
|
// #171 (2026-08-13) -- THE AUTHENTIC AC: TRIGGER-TIME HITSCAN (@004bc104
|
||||||
// Entity base mismatch -- see BTPushProjectile / mech4.cpp). Speed = |launchVelocity|.
|
// [T1], the body the stale banner hid; the prior flying-damage round was
|
||||||
|
// the #168 mis-generalization from the MISSILE launcher). Binary shape:
|
||||||
|
// increment the round counter (+0x408); every TracerInterval-th (+0x438)
|
||||||
|
// round, dist = |owner aimPoint(+0x37c) - muzzle| gated against the LIVE
|
||||||
|
// effectiveRange (+0x328, the same gate the Emitter uses) -> ONE
|
||||||
|
// single-panel Damage (amount = batchedCount x DamageAmount) to the
|
||||||
|
// LOCKED target *(mech+0x388) via SendDamageMessage @004b9728, AT FIRE
|
||||||
|
// TIME. Out of range: the batch silently discards. A locked AC inside
|
||||||
|
// range cannot miss; nothing about the hit rides the visible round.
|
||||||
|
// (This also restores the binary's ONE-message-per-batch = single-panel
|
||||||
|
// damage -- closing #112's multi-zone shotgun, which was the port's
|
||||||
|
// per-round zone re-roll scatter.)
|
||||||
|
//
|
||||||
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
|
char *o = (char *)owner; // inherited MechSubsystem::owner (the Mech)
|
||||||
// The owner's target slots (raw 0x388/0x37c -- the same raw offsets mech4's
|
|
||||||
// targeting block writes; a consistent producer/consumer pair on our
|
|
||||||
// compiled object). Task #41: the slots hold the WORLD PICK -- the enemy
|
|
||||||
// mech under the boresight, or the terrain downrange (a missile fired at
|
|
||||||
// the scenery flies to the ground point and detonates without damage), or
|
|
||||||
// null at the sky (BTPushProjectile refuses; the weapon's own 0x388 gate
|
|
||||||
// wouldn't have fired either).
|
|
||||||
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
|
void *target = (o != 0) ? *(void **)(o + 0x388) : 0;
|
||||||
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
|
Point3D targetPos = (o != 0) ? *(Point3D *)(o + 0x37c) : muzzle;
|
||||||
|
|
||||||
|
++tracerCounter; // @0x408 -- incremented BEFORE the test
|
||||||
|
// @4bc104:16079: bVar1 = (interval != 0) && (counter % interval == 0).
|
||||||
|
// interval==0 -> bVar1 never set: NO damage ever, counter never resets
|
||||||
|
// (and the short-circuit means no divide) -- the port matches exactly.
|
||||||
|
if (tracerInterval != 0 && (tracerCounter % tracerInterval) == 0)
|
||||||
|
{
|
||||||
|
// @4bc104:16088-16091: muzzle transform (@004b9948, resolved above as
|
||||||
|
// `muzzle`), then the lead/targeting refresh -- INSIDE the tracer
|
||||||
|
// branch, not per-round. (Also re-derives effectiveRange via
|
||||||
|
// UpdateTargeting, so the gate below reads the LIVE value.)
|
||||||
|
ComputeTimeOfFlight(); // @004bc06c
|
||||||
|
Vector3D toAim;
|
||||||
|
toAim.Subtract(targetPos, muzzle); // :16096 FUN_00408644(aimPoint(+0x37c), muzzle)
|
||||||
|
Scalar dist = (Scalar)sqrtf(toAim.x*toAim.x + toAim.y*toAim.y + toAim.z*toAim.z);
|
||||||
|
if (dist <= effectiveRange && target != 0) // :16098 gate (+0x328); the null test is the
|
||||||
|
{ // port's guard (binary trusts 0x388!=0 upstream)
|
||||||
|
// @4bc104 fills EXACTLY TWO fields of the batched record (:16100-16120):
|
||||||
|
// damageAmount = counter x authored amount (:16110)
|
||||||
|
// damageForce = rootSegRot . leadPosition(+0x420) x float(+0x41c)
|
||||||
|
// impactPoint is NOT set here -- SendDamageMessage @004b9728 sets it
|
||||||
|
// (FUN_00408440(dmg+0x20, mech+0x37c) = the aim point); the port's
|
||||||
|
// SendDamageMessage doesn't do that write, so it is FOLDED here as
|
||||||
|
// impactPoint = targetPos (net-identical). burstCount RIDES the
|
||||||
|
// authored record untouched (Damage ctor = 1 for an AC; only
|
||||||
|
// MissileLauncher re-authors it, and that class overrides FireWeapon).
|
||||||
|
// damageForce divergence [T3, deliberate]: the binary's value is the
|
||||||
|
// lead point rotated by the mech root-segment matrix scaled by the
|
||||||
|
// float at +0x41c (a dword lifted from the tracer-model record --
|
||||||
|
// direction-equivalent junk; gyro rattle consumes DIRECTION only,
|
||||||
|
// and the Emitter's own path authors force = target - muzzle
|
||||||
|
// (emitter.cpp:360 @0x3B0)), so the port uses the same toAim shape.
|
||||||
|
Scalar batched = (Scalar)tracerCounter * damageData.damageAmount; // :16110
|
||||||
|
Damage batchSave = damageData; // binary builds a LOCAL copy (:16100);
|
||||||
|
damageData.damageAmount = batched; // the port mutates+restores because our
|
||||||
|
damageData.impactPoint = targetPos; // SendDamageMessage reads the member
|
||||||
|
damageData.damageForce = toAim;
|
||||||
|
SendDamageMessage((Entity *)target); // @004b9728 -- at fire time
|
||||||
|
damageData = batchSave;
|
||||||
|
}
|
||||||
|
tracerCounter = 0; // :16123 -- resets on HIT and MISS alike,
|
||||||
|
} // but ONLY on tracer rounds
|
||||||
|
|
||||||
|
// The VISIBLE round: the binary's 0xBCD renderer tracer, cosmetic only
|
||||||
|
// (the shell entity 0xBD1 is dead code -- nothing shipped ever spawns a
|
||||||
|
// damage-carrying ballistic round). Fly the pool round as that visual:
|
||||||
|
// damage 0, straight at the pick, authentic muzzle speed.
|
||||||
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);
|
||||||
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
|
BTPushProjectile(muzzle, o, target, targetPos, speed, 0.0f /*cosmetic tracer*/,
|
||||||
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/,
|
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/,
|
||||||
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
|
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
|
||||||
0 /*splash_burst*/, -1 /*muzzle_seg*/,
|
0 /*splash_burst*/, -1 /*muzzle_seg*/,
|
||||||
(int)damageData.damageType /*issue: autocannon = Ballistic, was Explosive*/);
|
(int)damageData.damageType);
|
||||||
|
|
||||||
// task #61: mark this shot for REPLICATION so the peer sees the enemy's
|
// task #61: mark this shot for REPLICATION so the peer sees the enemy's
|
||||||
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
|
// cannon. ++fireCounter; the CALLER's Loaded case makes the two
|
||||||
|
|||||||
Reference in New Issue
Block a user