#84: the replicant salvo now HOMES AT THE LIVE TARGET -- the stale-point double detonation is gone
Oracle (674): "missile appear to register hit explosions twice, once where
target was and again where the target is". Rajel (693): "once where the mech
was when I fired and on the expected impact".
MECHANISM, measured on two nodes (BT_PROJ_LOG "DET at" now logs EVERY contact
detonation, including damage-0 rounds, which were invisible before): the
replicant salvo mirror pushed rounds with NO entity handle at the FROZEN
fire-time aim point, and the pool's contact test is proximity to p.targetPos --
so mirror rounds "contacted" the empty air where the target USED to be and
detonated there, on every peer, every salvo (45 salvos -> 234 frozen-point
air bursts in a 200s bench). The true impact appears at the live position
(the shooter's own homing rounds on his node; the victim's damage reaction on
peers) -- two sites per salvo, exactly as reported.
THE BINARY'S MODEL (CLASSMAP, Missile flight-entity cluster): missiles are
ENTITIES with authoritative/GHOST Performance variants (Projectile
PTR_LAB_005129e8 / 005129f4) and their own WriteUpdateRecord (@4bef4c, slot 7)
-- peers ran ghost missiles updated from the wire and saw the TRUE trajectory.
One explosion, correct place, every node. The frozen-point mirror is the port
infidelity (the entity Missile itself stays blocked by the documented 2007
Entity-base mismatch; mislanch.cpp:301).
FIX: the MissileLauncher update record now carries the locked target's EntityID
(salvoTargetID; EntityID::Null = point fire). The mirror resolves it on the
receiving node via HostManager::GetEntityPointer (engine T0, an index-socket
Find -- NULL-safe on any ID), refuses non-mechs via BTIsRegisteredMech, and
pushes the mirror rounds WITH the handle -- they re-lead on the live local
replicant exactly as the master's own rounds do, and detonate at the true
position. Guards: record-length gate (a short record from an older build has
no ID field -- its tail would be garbage and must not resolve) + Null check +
registered-mech check.
VERIFIED (two nodes, 200s, missile autofire both ways):
* every record arrives len=52/52 with sane aims ([mlrec] diag)
* 22/46 salvos resolve (tgtID=3:22 -> the victim's local master) and their
mirror rounds home: the frozen-air DET population fell 234 -> 102, the
live-homing population rose to 155/207 per node
* the unresolved remainder is BENIGN and correct: EntityID::Null point fire
plus hostID=-1 LOCAL entities -- i.e. the victim's WRECK, which autofire
keeps shooting after the kill; a wreck does not move, so the frozen aim IS
its true position
Downstream expectations: Ronin's smoke-screen (#114) should drop by ~the mirror
half, and the audio census (#32) loses the duplicate Static3DPatchSource
explosion sounds on peers -- both to be read from the next field logs.
Also in this commit: [projectile] DET diagnostic (every contact detonation with
position/damage/handle/aim), [mlrec] record-arrival diagnostic (len/recID/
subsys/counter/aim, capped 200), scratchpad/night9/mp_double.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3354db4bd1
commit
da70bd58b2
@@ -220,8 +220,13 @@ i860 firmware** `content/VREND.MNG` (same decode workflow as the task-#55 'damag
|
||||
- Remaining [T3]: sfx cone shape (v_bias as up-bias), size ×0.5 + 1.25m textured floor +
|
||||
hot-phase occlusion 0.45..0.9, burst scatter ×0.2; trail
|
||||
density is frame-rate-dependent (2/frame @60fps = 2× pod density); .PFX 'repeats' unconsumed
|
||||
(all shipped files author 1); replicant visual salvo chases the stale fire-time aim point
|
||||
(no re-lead without a target handle).
|
||||
(all shipped files author 1); replicant visual salvo FIXED (2026-08-02, #84): the
|
||||
update record now carries the locked target's EntityID; the mirror resolves it
|
||||
locally (HostManager::GetEntityPointer, engine T0) and homes at the LIVE
|
||||
replicant, as the binary's GHOST missiles did (CLASSMAP: Projectile
|
||||
PTR_LAB_005129e8 authoritative / 005129f4 ghost performances) -- peers now see
|
||||
detonations at the true position. Point fire / local-only targets (wrecks,
|
||||
hostID -1) still fly to the frozen aim, which is correct for them.
|
||||
|
||||
## Weapon beams (the unported dpl_* layer)
|
||||
The `dpl_*` beam renderable was never ported → beams are drawn by `BTPushBeam`/`BTDrawBeams`
|
||||
|
||||
@@ -1676,6 +1676,16 @@ static void
|
||||
}
|
||||
if (contact)
|
||||
{
|
||||
// #84 diag: EVERY contact detonation, incl. damage-0 visual/mirror
|
||||
// rounds (the IMPACT line below only covers damage-carrying rounds,
|
||||
// which hid the replicant mirror's stale-point bursts entirely).
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] DET at(" << hitPos.x << ","
|
||||
<< hitPos.y << "," << hitPos.z << ")"
|
||||
<< " dmg=" << p.damage
|
||||
<< " tgt=" << (void *)p.target
|
||||
<< " aim(" << p.targetPos.x << "," << p.targetPos.y
|
||||
<< "," << p.targetPos.z << ")" << std::endl;
|
||||
BTSpawnRoundDetonation(p.shooter, p.weaponSubsys, hitPos);
|
||||
Entity *tgt = p.target;
|
||||
// Deliver to the projectile's target mech -- the launcher set p.target
|
||||
|
||||
@@ -63,6 +63,10 @@
|
||||
#if !defined(TESTBT_HPP)
|
||||
# include <testbt.hpp>
|
||||
#endif
|
||||
#if !defined(APP_HPP)
|
||||
# include <app.hpp> // #84: application -> host manager
|
||||
#endif
|
||||
#include <hostmgr.hpp> // #84: HostManager::GetEntityPointer (salvo target resolve)
|
||||
#include <math.h>
|
||||
|
||||
// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp, a
|
||||
@@ -99,6 +103,7 @@ namespace {
|
||||
int fired; // master: salvos launched
|
||||
int seen; // replicant: last counter applied (-1 = unsynced)
|
||||
Point3D target; // master: the last salvo's aim point
|
||||
EntityID targetID; // master: the locked target (#84; Null = point fire)
|
||||
};
|
||||
BTSalvoState gSalvoTable[64];
|
||||
|
||||
@@ -117,6 +122,7 @@ namespace {
|
||||
s.fired = 0;
|
||||
s.seen = -1;
|
||||
s.target = Point3D(0.0f, 0.0f, 0.0f);
|
||||
s.targetID = EntityID::Null;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -362,6 +368,8 @@ void MissileLauncher::FireWeapon()
|
||||
BTSalvoState &s = BTSalvoOf(this);
|
||||
++s.fired;
|
||||
s.target = targetPos;
|
||||
s.targetID = (target != 0)
|
||||
? ((Entity *)target)->GetEntityID() : EntityID::Null;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
@@ -384,6 +392,7 @@ void
|
||||
rec->salvoCounter = s.fired;
|
||||
rec->salvoRounds = missileCount;
|
||||
rec->salvoTarget = s.target;
|
||||
rec->salvoTargetID = s.targetID;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -392,6 +401,19 @@ void
|
||||
MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply)
|
||||
|
||||
MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message;
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
{
|
||||
static int s_rr = 0;
|
||||
if (s_rr++ < 200)
|
||||
DEBUG_STREAM << "[mlrec] len=" << (int)rec->recordLength
|
||||
<< "/" << (int)sizeof(MissileLauncher__UpdateRecord)
|
||||
<< " recID=" << (int)rec->recordID
|
||||
<< " subsys=" << (int)rec->subsystemID
|
||||
<< " ctr=" << rec->salvoCounter
|
||||
<< " n=" << rec->salvoRounds
|
||||
<< " aim=(" << rec->salvoTarget.x << "," << rec->salvoTarget.y
|
||||
<< "," << rec->salvoTarget.z << ")" << std::endl;
|
||||
}
|
||||
BTSalvoState &s = BTSalvoOf(this);
|
||||
if (s.seen < 0)
|
||||
{
|
||||
@@ -422,8 +444,27 @@ void
|
||||
if (mid >= 0)
|
||||
BTMissileThrustOf(mid, &tBurn, &tAccel);
|
||||
}
|
||||
// #84 -- home the mirror at the LIVE target, as the binary's GHOST
|
||||
// missiles did. The record carries the master's locked-target entity
|
||||
// ID; resolve it against THIS node's registry (the victim's local
|
||||
// master or replicant -- both track the true position here). The
|
||||
// frozen salvoTarget remains the launch aim + the fallback for point
|
||||
// fire, unresolved IDs (target died / not yet replicated here), and
|
||||
// short records from older builds (length-gated: a stale-build record
|
||||
// has no ID field and its tail would be garbage).
|
||||
Entity *mirror_tgt = 0;
|
||||
if (message->recordLength >= (int)sizeof(MissileLauncher__UpdateRecord)
|
||||
&& !(rec->salvoTargetID == EntityID::Null)
|
||||
&& application != 0 && application->GetHostManager() != 0)
|
||||
{
|
||||
mirror_tgt = application->GetHostManager()->
|
||||
GetEntityPointer(rec->salvoTargetID);
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (mirror_tgt != 0 && !BTIsRegisteredMech(mirror_tgt))
|
||||
mirror_tgt = 0; // resolved to a non-mech: refuse
|
||||
}
|
||||
for (int i = 0; i < n; ++i)
|
||||
BTPushProjectile(mz, owner, 0 /*no entity: aim point only*/,
|
||||
BTPushProjectile(mz, owner, mirror_tgt /*#84 live target; 0 -> point*/,
|
||||
rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity, 1,
|
||||
subsystemID /*per-round detonation resolve on this node*/,
|
||||
0, GetSegmentIndex() /*task #67 mount frame*/,
|
||||
@@ -431,7 +472,9 @@ void
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n
|
||||
<< " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y
|
||||
<< "," << rec->salvoTarget.z << ")\n" << std::flush;
|
||||
<< "," << rec->salvoTarget.z << ")"
|
||||
<< " tgtID=" << rec->salvoTargetID
|
||||
<< " resolved=" << (void *)mirror_tgt << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,15 @@ class Missile;
|
||||
int salvoCounter; // fires since spawn (edge-detected by peers)
|
||||
int salvoRounds; // missiles in the salvo (authored missileCount)
|
||||
Point3D salvoTarget; // the aim point the salvo flew at
|
||||
// #84: the LOCKED TARGET's entity ID (EntityID::Null = point fire).
|
||||
// The binary replicated the whole Missile entity, and its peers ran
|
||||
// a GHOST performance updated from the wire (CLASSMAP: Projectile
|
||||
// PTR_LAB_005129e8 authoritative / 005129f4 ghost) -- so peers saw
|
||||
// the TRUE trajectory. The pool's mirror flew at the frozen
|
||||
// fire-time point instead, detonating "where the target WAS": the
|
||||
// reported double explosion. Carrying the target ID lets the
|
||||
// mirror home at the LIVE local replicant, like the ghost did.
|
||||
EntityID salvoTargetID;
|
||||
};
|
||||
typedef MissileLauncher__UpdateRecord UpdateRecord;
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# #84: measure the DOUBLE DETONATION on two real nodes.
|
||||
#
|
||||
# A and B chase and fire missiles at each other. Every contact detonation now
|
||||
# logs position + aim + target handle (BT_PROJ_LOG "DET at"). READ:
|
||||
# * on节点 B, [projectile] REPLICANT salvo lines = A's mirrored fire, aim = the
|
||||
# FROZEN fire-time point; their DETs should land at that frozen point
|
||||
# (tgt=00000000) -- the stale half of the double.
|
||||
# * DETs with tgt!=0 = own homing rounds -> the true impact position.
|
||||
# * matchlog VEHICLE rows give both mechs' positions to measure staleness.
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
bt_assert_player_env
|
||||
rm -f mpd_a.log mpd_b.log
|
||||
bt_expert_egg MP.EGG MPD.EGG
|
||||
|
||||
launch () { # $1=log $2=affinity $3=port
|
||||
BT_MP_LOG=1 BT_DMG_LOG=1 BT_MATCHLOG=1 BT_PROJ_LOG=1 \
|
||||
BT_GOTO=enemy BT_GOTO_STOP=100 BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=5 \
|
||||
bt_launch "$1" MPD.EGG "$2" -net "$3"
|
||||
sleep 2
|
||||
}
|
||||
|
||||
launch mpd_b.log 0x0C 1601
|
||||
launch mpd_a.log 0x03 1501
|
||||
sleep 5
|
||||
python ../tools/btconsole.py MPD.EGG 127.0.0.1:1501 127.0.0.1:1601 &
|
||||
PC=$!
|
||||
|
||||
sleep 200
|
||||
kill $PC 2>/dev/null
|
||||
bt_kill_ours
|
||||
rm -f MPD.EGG
|
||||
echo "=== DONE ==="
|
||||
Reference in New Issue
Block a user