From da70bd58b2b033ff02a4d903cebd2746aa5d4495 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sun, 2 Aug 2026 02:29:58 -0500 Subject: [PATCH] #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) --- context/rendering.md | 9 +++++-- game/reconstructed/mech4.cpp | 10 +++++++ game/reconstructed/mislanch.cpp | 47 +++++++++++++++++++++++++++++++-- game/reconstructed/mislanch.hpp | 9 +++++++ scratchpad/night9/mp_double.sh | 35 ++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 scratchpad/night9/mp_double.sh diff --git a/context/rendering.md b/context/rendering.md index 92b2dc6..e12433e 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -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` diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 47ffded..4e0127e 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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 diff --git a/game/reconstructed/mislanch.cpp b/game/reconstructed/mislanch.cpp index e09a3c0..870a93e 100644 --- a/game/reconstructed/mislanch.cpp +++ b/game/reconstructed/mislanch.cpp @@ -63,6 +63,10 @@ #if !defined(TESTBT_HPP) # include #endif +#if !defined(APP_HPP) +# include // #84: application -> host manager +#endif +#include // #84: HostManager::GetEntityPointer (salvo target resolve) #include // 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; } } diff --git a/game/reconstructed/mislanch.hpp b/game/reconstructed/mislanch.hpp index e22b53d..236421c 100644 --- a/game/reconstructed/mislanch.hpp +++ b/game/reconstructed/mislanch.hpp @@ -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; diff --git a/scratchpad/night9/mp_double.sh b/scratchpad/night9/mp_double.sh new file mode 100644 index 0000000..544679d --- /dev/null +++ b/scratchpad/night9/mp_double.sh @@ -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 ==="