#95: REVERT the salvo-damage hack; deliver the cluster the way the binary does
Supersedes the mislanch change inefc3e9f, which multiplied the lead round by missileCount. That produced roughly the right average total but as ONE lump on ONE zone, with no scatter and no variance -- not what the arcade does. WHAT THE BINARY DOES (all byte-verified): * MissileLauncher ctor @004bcff0: burstCount = missileCount; damageAmount /= missileCount * FireWeapon @004bcc60 spawns exactly ONE Missile -- no loop, missileCount is never read there. The salvo IS one cluster round. * Missile::Perform rolls how many of the cluster connect right before it dispatches (part_013.c:10082): b = Random(n) + n/4, clamped to n -- i.e. between a quarter of the salvo and all of it. * It then dispatches DIRECTLY at the struck entity (FUN_004be078: param_2->Dispatch(&msg)) -- NOT through the shooter's message manager. * Mech::TakeDamageMessageHandler @0x4a0439-0x4a04d8 applies the hit burstCount times, RE-ROLLING the struck zone per burst. Our handler already implements that loop faithfully (mech.cpp, task #80) -- I wrongly believed it was missing, because the KB asserts as [T1] that "burstCount is cosmetic for zone damage". That is half right: DamageZone::TakeDamage really does ignore burstCount (verified @0041e4e0), but the HANDLER honours it by calling that function repeatedly. KB corrected separately. The actual defect was that the projectile impact path hardcoded burstCount = 1, and -- worse -- routed damage through the SubsystemMessageManager, whose consolidation rebuilds the record from a stream carrying only {damageType, damageAmount, subsystemID}. DamageInformation has no burstCount field, so the cluster count was DROPPED in transit no matter what the round carried. So: dispatch projectile damage directly, as the binary does, carrying the rolled burst. This also retires the #84 double explosion architecturally -- the second blast came from the manager's bundled explosion, and projectiles no longer go through the manager at all. The MarkRoundDetonated suppression added inefc3e9fis therefore dead and is removed. ALL-WEAPON-CLASS AUDIT (scratchpad/night8/allweap.{sh,py}), one run, all classes firing at once: EXPLOSIVE (missile) 38 zone applications, bursts spread 1x2,2x6,3x9,4x4,5x5, 6x12 across ELEVEN zones -- the [n/4..n] roll plus the per-burst zone re-roll, i.e. the cluster scattering ENERGY (beam) 8 applications, burst 1 -- unchanged, no regression type4 8 applications, burst 1 -- unchanged COLLISION 0 applications reached armour -- divert intact Explosions: 10 projectile impacts produce no bundled blast; direct-fire still queues its own (11 made). NOT yet verified: cross-pod delivery. Dispatch reroutes to a replicant on its own (and the binary relies on exactly that), but the manager routing is gone, so MP damage should get a two-node run before this ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
efc3e9ff1a
commit
3b7c19c232
@@ -1694,41 +1694,61 @@ static void
|
||||
Damage dmg;
|
||||
dmg.damageType = (Enumeration)p.damageType; // issue: was hardcoded Explosive
|
||||
dmg.damageAmount = p.damage;
|
||||
dmg.burstCount = 1;
|
||||
dmg.impactPoint = hitPos; // nearest approach, not the step end
|
||||
// Route through the SHOOTER's SubsystemMessageManager with the
|
||||
// firing launcher's roster index (task #7 bundling): the
|
||||
// consolidation resolves roster[id]+0x3E4 = the weapon's
|
||||
// ExplosionModelFile (mslhit/acanhit) and fires it AT the
|
||||
// impact point -- the missing missile-hit explosion (the
|
||||
// laser path always did this via SendDamageMessage; the
|
||||
// projectile path bypassed the manager entirely).
|
||||
SubsystemMessageManager *mgr = 0;
|
||||
if (p.shooter != 0 && p.weaponSubsys >= 0 && BTIsRegisteredMech(p.shooter))
|
||||
mgr = (SubsystemMessageManager *)((Mech *)p.shooter)->GetMessageManager();
|
||||
if (mgr != 0)
|
||||
//
|
||||
// CLUSTER BURST (issue #95). A missile salvo is ONE arcade
|
||||
// Missile entity carrying the PER-MISSILE amount (the launcher
|
||||
// ctor does `damageAmount /= missileCount; burstCount =
|
||||
// missileCount` @004bcff0) -- the salvo total is reconstituted
|
||||
// at the RECEIVER, where Mech::TakeDamageMessageHandler applies
|
||||
// the hit `burstCount` times, re-rolling the struck zone each
|
||||
// burst (@0x4a0439-0x4a04d8; mech.cpp, already faithful).
|
||||
//
|
||||
// This site hardcoded burstCount = 1, so every salvo delivered
|
||||
// one missile's worth -- an LRM 15 landing ~3 points instead of
|
||||
// 50, exactly as players measured.
|
||||
//
|
||||
// How many of the cluster connect is ROLLED at impact, in the
|
||||
// binary's Missile::Perform right before it dispatches
|
||||
// (part_013.c:10082):
|
||||
// n = burstCount
|
||||
// b = Random(n) + n/4, clamped to n
|
||||
// i.e. between a quarter of the salvo and all of it. That
|
||||
// variance + the handler's per-burst zone re-roll IS the
|
||||
// authentic spread; pre-multiplying the amount instead would
|
||||
// dump the whole salvo on one zone with no scatter.
|
||||
int bursts = (p.splashBurst > 0) ? p.splashBurst : 1;
|
||||
if (bursts > 1)
|
||||
{
|
||||
// issue #84: this round already spawned its OWN detonation
|
||||
// at hitPos above. Tell the manager, so the consolidation
|
||||
// does not ALSO queue this weapon's explosion at the bundle's
|
||||
// consolidated point a frame later -- the double blast
|
||||
// players see as "once where the target was, again where it
|
||||
// is". Direct-fire weapons never mark, so lasers/AC keep
|
||||
// the bundled explosion they depend on.
|
||||
mgr->MarkRoundDetonated(p.weaponSubsys);
|
||||
Entity::TakeDamageMessage take_damage(
|
||||
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||
p.shooter->GetEntityID(), -1 /*unaimed -> cylinder resolves*/,
|
||||
dmg, p.weaponSubsys);
|
||||
mgr->AddDamageMessage(tgt, &take_damage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity::TakeDamageMessage take_damage(
|
||||
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||
0 /*inflictor id: bring-up*/, -1 /*unaimed -> cylinder resolves*/, dmg);
|
||||
tgt->Dispatch(&take_damage);
|
||||
int rolled = Random(bursts) + (bursts >> 2);
|
||||
if (rolled > bursts) rolled = bursts;
|
||||
if (rolled < 1) rolled = 1;
|
||||
bursts = rolled;
|
||||
}
|
||||
dmg.burstCount = bursts;
|
||||
// DISPATCH DIRECTLY, as the binary's Missile does
|
||||
// (Missile::Perform -> FUN_004be078: `param_2->Dispatch(&msg)`
|
||||
// straight at the struck entity). A projectile hit does NOT go
|
||||
// through the shooter's SubsystemMessageManager in the arcade.
|
||||
//
|
||||
// The port used to route it there (task #7) purely to borrow the
|
||||
// manager's impact explosion, and that cost us both defects
|
||||
// players reported this session:
|
||||
// * the consolidation rebuilds the damage record from a stream
|
||||
// carrying only {damageType, damageAmount, subsystemID} --
|
||||
// DamageInformation has no burstCount field, so the CLUSTER
|
||||
// COUNT was dropped in transit and every salvo applied once
|
||||
// (issue #95); and
|
||||
// * it queued a SECOND explosion at the bundle's consolidated
|
||||
// point a frame later -- the double blast (issue #84).
|
||||
// The round spawns its own detonation at hitPos above, so the
|
||||
// manager buys us nothing here. Dispatch reroutes cross-pod for
|
||||
// a replicant victim on its own.
|
||||
Entity::TakeDamageMessage take_damage(
|
||||
Entity::TakeDamageMessageID, sizeof(Entity::TakeDamageMessage),
|
||||
(p.shooter != 0) ? p.shooter->GetEntityID() : EntityID::Null,
|
||||
-1 /*unaimed -> cylinder resolves*/, dmg, p.weaponSubsys);
|
||||
tgt->Dispatch(&take_damage);
|
||||
// gauge scoring wave (Step 6): a projectile hit credits SCORE too
|
||||
// (tgt == gEnemyMech here; local player is the viewpoint shooter).
|
||||
BTPostDamageScore((Entity *)tgt, p.damage);
|
||||
@@ -1755,7 +1775,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)")
|
||||
<< " burst=" << dmg.burstCount << " (direct dispatch)"
|
||||
<< " (zone cyl-resolved)\n" << std::flush;
|
||||
|
||||
// SPLASH (task #62): a detonating missile SALVO damages every
|
||||
|
||||
@@ -164,8 +164,6 @@ SubsystemMessageManager::SubsystemMessageManager(
|
||||
commonDamageInformation.damageZoneIndex = -1; // this[0x3A]
|
||||
commonDamageInformation.impactPoint = Point3D::Identity; // this[0x3B] = DAT_004e0f80
|
||||
|
||||
selfDetonatedCount = 0; // port-only (issue #84)
|
||||
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -390,19 +388,7 @@ void
|
||||
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
|
||||
<< " weapon=" << (firingWeapon ? firingWeapon->GetName() : "<none>")
|
||||
<< " explosionID=" << explosionID << std::endl;
|
||||
//
|
||||
// issue #84: skip the bundled explosion for a weapon whose round
|
||||
// already detonated itself at its own impact point. Queueing it
|
||||
// too puts a second blast at the CONSOLIDATED point a frame later
|
||||
// -- the "explosion where the target was, and again where it is".
|
||||
//
|
||||
if (DidRoundDetonate(info->subsystemID))
|
||||
{
|
||||
if (getenv("BT_FIRE_LOG"))
|
||||
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
|
||||
<< " SKIPPED (round detonated itself)" << std::endl;
|
||||
}
|
||||
else if (explosionID != ResourceDescription::NullResourceID
|
||||
if (explosionID != ResourceDescription::NullResourceID
|
||||
&& !weaponExplosions.Find(explosionID)) // chain+0xFC, slot 0xC
|
||||
{
|
||||
ResourceIDPlug *plug = new ResourceIDPlug(explosionID);
|
||||
@@ -447,7 +433,6 @@ void
|
||||
commonDamageInformation.entityHit = 0; // this[0x39]
|
||||
commonDamageInformation.damageZoneIndex = -1; // this[0x3A]
|
||||
commonDamageInformation.impactPoint = Point3D::Identity; // this[0x3B]
|
||||
selfDetonatedCount = 0; // port-only (issue #84): marks are per-flush
|
||||
}
|
||||
|
||||
|
||||
@@ -528,32 +513,6 @@ ResourceDescription::ResourceID
|
||||
return ResourceDescription::NullResourceID;
|
||||
}
|
||||
|
||||
//
|
||||
// PORT-ONLY (issue #84) -- see the header note. A projectile that spawned its
|
||||
// own round detonation marks its weapon here so the consolidation does not queue
|
||||
// a SECOND, differently-placed explosion for the same hit.
|
||||
//
|
||||
void
|
||||
SubsystemMessageManager::MarkRoundDetonated(int subsystem_id)
|
||||
{
|
||||
Check(this);
|
||||
if (subsystem_id < 0 || DidRoundDetonate(subsystem_id))
|
||||
return;
|
||||
if (selfDetonatedCount >= kMaxSelfDetonated)
|
||||
return; // full: fall back to the bundled blast
|
||||
selfDetonated[selfDetonatedCount++] = subsystem_id;
|
||||
}
|
||||
|
||||
Logical
|
||||
SubsystemMessageManager::DidRoundDetonate(int subsystem_id) const
|
||||
{
|
||||
Check(this);
|
||||
for (int i = 0; i < selfDetonatedCount; ++i)
|
||||
if (selfDetonated[i] == subsystem_id)
|
||||
return True;
|
||||
return False;
|
||||
}
|
||||
|
||||
//
|
||||
// SubmitExplosion -- the real spawn (task #7 tail). The binary posts a
|
||||
// Registry::MakeEntityMessage (id 3, class 0x31 Explosion, flags 0x100) at
|
||||
|
||||
@@ -163,36 +163,6 @@ class
|
||||
ResourceDescription::ResourceID
|
||||
terrainHitExplosionID; //@0x12C (resource +0x30)
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// PORT-ONLY -- appended AFTER every layout-locked member (issue #84).
|
||||
//
|
||||
// A projectile spawns its OWN per-round detonation at its exact impact point
|
||||
// (BTSpawnRoundDetonation), which is the arcade's rippling volley look. It then
|
||||
// routes its damage through this manager, which ALSO queues the firing weapon's
|
||||
// explosion -- a second blast, at the bundle's CONSOLIDATED impact point and on
|
||||
// a later frame. Two explosions, two places: "once where the target was and
|
||||
// again where the target is" (Oracle, 4.11.674).
|
||||
//
|
||||
// That duplicate was known and thought harmless -- "among a rippled volley it is
|
||||
// invisible" -- which held while a salvo landed N detonations. It stopped being
|
||||
// true when the cluster collapsed to ONE damaging round.
|
||||
//
|
||||
// So: a weapon that already detonated its own round this frame is marked here,
|
||||
// and the consolidation skips queueing its explosion. Direct-fire weapons
|
||||
// (lasers/AC) never mark, so they keep the bundled explosion they rely on.
|
||||
// Cleared every flush. Marked by roster index -- exact, because a weapon is
|
||||
// either a projectile launcher or an emitter, never both in one frame.
|
||||
//
|
||||
public:
|
||||
void
|
||||
MarkRoundDetonated(int subsystem_id);
|
||||
Logical
|
||||
DidRoundDetonate(int subsystem_id) const;
|
||||
protected:
|
||||
enum { kMaxSelfDetonated = 16 };
|
||||
int selfDetonated[kMaxSelfDetonated];
|
||||
int selfDetonatedCount;
|
||||
|
||||
void
|
||||
CreateWeaponExplosions(
|
||||
Logical terrain_hit,
|
||||
|
||||
@@ -334,21 +334,16 @@ void MissileLauncher::FireWeapon()
|
||||
// carries the missile's damageAmount + the cluster splash; the rest are
|
||||
// VISUAL (damage 0) -- the ripple of tracers, no extra damage.
|
||||
//
|
||||
// ...but it must carry the WHOLE SALVO's damage (issue #95). The ctor
|
||||
// above does what the binary's MissileLauncher ctor does:
|
||||
// damageData.burstCount = missileCount; // @0x3d4
|
||||
// damageData.damageAmount /= missileCount; // @0x3ac
|
||||
// i.e. the record holds PER-MISSILE amount plus the count, and the arcade
|
||||
// reconstitutes the total as amount x burstCount when it applies the hit.
|
||||
// Our application (DamageZone::TakeDamage) applies `amount * scale` ONCE
|
||||
// and drops burstCount -- so handing the lead round the already-divided
|
||||
// amount delivered amount/missileCount, i.e. 1/N of the authored salvo.
|
||||
// Two individually-correct changes composed into a silent N-fold shortfall:
|
||||
// players measured an LRM 15 landing ~3 points instead of 50.
|
||||
// Multiply the count back in here, where the port collapses the cluster to
|
||||
// one damaging round, so the salvo delivers exactly its authored total.
|
||||
// The lead round carries the PER-MISSILE amount, exactly as the binary's
|
||||
// ctor prepared it (damageAmount /= missileCount, burstCount = missileCount).
|
||||
// The salvo total is reconstituted at IMPACT, where the round hands the
|
||||
// handler a burstCount and the handler applies the hit that many times
|
||||
// (Mech::TakeDamageMessageHandler @0x4a0423-0x4a04d8, mech.cpp) -- see the
|
||||
// cluster-burst note at the impact site in mech4.cpp. Do NOT pre-multiply
|
||||
// here: that would deliver the salvo as one lump on one zone, losing both
|
||||
// the per-burst zone re-roll and the arcade's hit-count variance.
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed,
|
||||
(i == 0) ? damageData.damageAmount * (Scalar)nmiss : 0.0f,
|
||||
(i == 0) ? damageData.damageAmount : 0.0f,
|
||||
&launchVelocity, 1,
|
||||
subsystemID /*messmgr explosion bundling at impact (task #7)*/,
|
||||
(i == 0) ? nmiss : 0 /*salvo-lead: cluster splash baseBurst*/,
|
||||
|
||||
Reference in New Issue
Block a user