From 1324c81719063a964f04622d672d21b127e20b00 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Fri, 7 Aug 2026 09:52:26 -0500 Subject: [PATCH] scoring: land inflicted credit on the OWNER's machine (Steam + console safe) Completes 2772175/e82f54c. The interceptor restored the credit; this puts it on the right node, so a player's score accumulates again. The operator corrected two of my claims, and both were load-bearing: 1. Scores DID accumulate before build 787. Checked: build 774 already had `currentScore = 0` in the console flush, so the flush was never eating score. My "score zeroed every interval" theory is dropped. The watermark from e82f54c stays only because it is harmless and keeps a master's own total intact across a flush -- it was not fixing a field bug. 2. The reroute works, and 774's own comment says so: the killer's player is a REPLICANT, so Entity::Dispatch reroutes to the owning host (ENTITY.cpp:244-251) and the credit lands on the killer's OWN machine. That is how kill credit has always crossed nodes. So the earlier master-only gate was the right idea and failed for a reason I guessed wrong. A rerouted message arrives over the WIRE through Receive(), which goes straight to the handler table -- the virtual Dispatch override is never called on the receiving side. Type 0 therefore landed in ScoreMessageHandler's arm, which Verify-rejected it: award 0.00. Fix is both halves: * Dispatch intercepts on a MASTER only -- local delivery stays exactly as @004bffa0 does it; * ScoreMessageHandler's type-0 arm DELEGATES to ScoreInflictedMessageHandler instead of Verify-rejecting -- wire delivery gets the same handler. One accumulator, on the machine that owns the score. Works for Steam today (no console tally exists -- btconsole.py/btoperator.py handle no score at all) AND for a real operator console later: the console flush is untouched and still ships authentic deltas under the owner's ownerID. Benched (cross-node zone-walk kill): credit node shooter's master only (victim's node banks 0) running total 253.60 and CLIMBING, no resets (was: peaks ~35, snapping back every few seconds) type-0 rejects 0 LOOSE END: each real award is followed by a duplicate row with award=0.00 (80 real + 80 zero). Harmless -- the total is unaffected -- but it means the report is delivered twice on the owner; not yet explained. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ --- game/reconstructed/btplayer.cpp | 42 ++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/game/reconstructed/btplayer.cpp b/game/reconstructed/btplayer.cpp index 63fec7a..3db4a4f 100644 --- a/game/reconstructed/btplayer.cpp +++ b/game/reconstructed/btplayer.cpp @@ -810,7 +810,20 @@ void && what->messageID == Player::ScoreMessageID && ((BTPlayer::ScoreMessage *)what)->scoreType == BTPlayer::ScoreMessage::DamageInflictedScore - ) + // + // MASTER ONLY. The binary intercepts unconditionally because its + // +0x278 is a console DELTA -- every node's contribution is flushed + // under the scoring player's ownerID and the CONSOLE totals it, so the + // computing node is irrelevant. Our port has no console tally + // (btconsole.py/btoperator.py handle no score at all) and reads +0x278 + // on the OWNING node for the SCORE gauge, CalcRanking and the replicated + // Player__UpdateRecord. Banking on the victim's replicant copy + // therefore loses the credit to the master's next update record. + // Falling through lets Entity::Dispatch reroute to the owner, where the + // wire delivery lands in ScoreMessageHandler's type-0 arm (which now + // delegates back to the inflicted handler). + // + && GetInstance() == Entity::MasterInstance) { ScoreInflictedMessageHandler((BTPlayer::ScoreMessage *)what); return; @@ -987,15 +1000,26 @@ void { case BTPlayer::ScoreMessage::DamageInflictedScore: // 0 // - // Inflicted-damage messages belong to ScoreInflictedMessageHandler. + // Inflicted-damage messages belong to ScoreInflictedMessageHandler, and + // the binary's Dispatch override (@004bffa0) guarantees they never reach + // here -- which is why the original arm is a bare Verify. // - Verify( - False, - "BTPlayer::ScoreMessageHandler should not be " - "given DamageInflictedScoreMessages!", // @0051324a - "d:\\tesla_bt\\bt\\btplayer.cpp", // @0051329a - 0x296 - ); + // PORT DIVERGENCE (2026-08-07), and it is a DELIVERY-PATH difference, + // not a scoring one. Damage is applied on the VICTIM's node, so block B + // dispatches the inflicted report to the SHOOTER's player object there -- + // a REPLICANT -- and Entity::Dispatch reroutes it to the owning host so + // the credit lands on the shooter's OWN machine (the same reroute that + // carries kill credit, ENTITY.cpp:244-251). But a message arriving over + // the WIRE is delivered through Receive(), straight to this handler + // table: the virtual Dispatch override is never called on the receiving + // side. So the rerouted report lands HERE, and Verify-rejecting it + // threw away every cross-node inflicted credit (benched: award=0.00). + // + // Delegate instead. Local deliveries are still intercepted by Dispatch + // exactly as the binary does; wire deliveries land here and get the same + // handler. One accumulator, on the machine that owns the score. + // + ScoreInflictedMessageHandler(message); break; case BTPlayer::ScoreMessage::DamageReceivedScore: // 1