MP match forensics: per-peer matchlog + relay auto-upload + matchcheck

For the 8-player playtest.  Damage authority is VICTIM-side, so no single
peer sees the whole match: every -net instance now writes a compact
machine-parseable matchlog_<date>_<time>_<pid>.txt (auto-armed by -net;
BT_MATCHLOG=1/0 overrides; solo silent), and at mission end a relay-mode
pod dials the relay game port (the seat-request throwaway-dial pattern,
envelope route -9) and streams the file back -- the relay saves every
peer's copy under matchlogs/ on the operator's machine, so testers send
nothing by hand.  tools/matchcheck.py <dir> reconciles all of them:
damage claimed (FIRE/PROJ/SPLASH/RAM, shooter-side) vs applied (DMG,
victim-side authoritative), kill/death replication across observers,
PLAYER_DEAD counts, scores, version skew, replicant-application and
unattributed-damage anomalies, mid-mission disconnects.

Hooks at the authoritative sites: Mech::TakeDamageMessageHandler (DMG,
post-base, zone + post-application damageLevel), MechWeapon::
SendDamageMessage (FIRE), projectile impact/splash/collision dispatch
(PROJ/SPLASH/RAM), wreck entry (DEATH -- a ONE-SHOT latch at
MovementMode 9, NOT the transition: a replicant arrives at 9 straight
from the type-6 record header and never runs the transition branch),
BTPlayer vehicle/death/score handlers (VEHICLE/PLAYER_DEAD/SCORE), zone
crit cascade (CRIT), APP.cpp RunningMission (MISSION), L4NET host
handlers (PEER_UP/PEER_DOWN).  Every line t=/w=/st=-stamped + fflushed.

Verified 2-node: mesh run pairs A's 22 FIRE 1:1 with B's 22 DMG (same
amounts, cylinder-resolved zones, ~200ms latency); force-damage kill run
logs 103 DMG + 3 DEATH inst=M + 3 PLAYER_DEAD across three respawn
cycles victim-side and the kill SCOREs shooter-side; relay-mode run
auto-uploaded BOTH peers' files at the mission-clock stop and matchcheck
produced the full report with exactly one (correct) anomaly -- the force
hook's claim-less damage, the unattributed-damage detector working.
Relay gained a route-specific 8MB cap for the upload frame (game frames
stay capped at 1600).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-22 13:22:10 -05:00
co-authored by Claude Opus 4.8
parent 3097d269b4
commit 753540de96
17 changed files with 947 additions and 2 deletions
+40
View File
@@ -70,6 +70,7 @@
//
#include <bt.hpp>
#include <matchlog.hpp> // MP match forensics (VEHICLE/PLAYER_DEAD/SCORE)
#pragma hdrstop
#if !defined(BTPLAYER_HPP)
@@ -414,6 +415,12 @@ void
//
++deathCount; // this+0x200
message->deathCount = deathCount; // message+0x38
// MP MATCH FORENSICS (matchlog.hpp): the owning player's death
// notification (fires exactly once per death, deduped above).
if (BTMatchLogActive())
BTMatchLog("PLAYER_DEAD", "player=%d:%d deaths=%d",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(), deathCount);
// TODO(bring-up): scenarioRole is the scoring role looked up from the mission's
// role registry (btplayer.cpp:815, currently deferred), so it is NULL for the
// minimal dev missions. Only debit a life when a role is present.
@@ -521,6 +528,14 @@ void
//
currentScore +=
(MECH_TONNAGE(target_mech) / MECH_TONNAGE(our_mech)) * award; // (+0x4bc / +0x4bc), this+0x278
// MP MATCH FORENSICS (matchlog.hpp): inflicted-damage score (type=0),
// logged with the post-accumulate total.
if (BTMatchLogActive())
BTMatchLog("SCORE",
"player=%d:%d type=0 award=%.2f total=%.2f kills=%d",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
(float)award, (float)currentScore, killCount);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -712,6 +727,17 @@ void
// straggler score lands the award without the vehicle response.
//
message->scoreAward = award; // msg+0x1c
// MP MATCH FORENSICS (matchlog.hpp): every folded score award. total is
// the score BEFORE this award lands (the base applies it right after) --
// matchcheck sums the awards; kills counts ride along.
if (BTMatchLogActive())
BTMatchLog("SCORE",
"player=%d:%d type=%d award=%.2f total=%.2f kills=%d",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
(int)message->scoreType, (float)award, (float)currentScore,
killCount);
if (playerVehicle == 0)
{
currentScore += message->scoreAward; // the base's award apply
@@ -920,6 +946,20 @@ void
Register_Object(playerVehicle);
}
// MP MATCH FORENSICS (matchlog.hpp): the player has a vehicle (initial
// spawn AND every drop-zone respawn route here) -- the line that binds a
// player id to a mech id for the whole analysis.
if (BTMatchLogActive() && playerVehicle != 0)
BTMatchLog("VEHICLE",
"player=%d:%d mech=%d:%d deaths=%d pos=%.1f,%.1f,%.1f",
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
BTMatchHostOf(playerVehicle->GetEntityID()),
(int)playerVehicle->GetEntityID(),
deathCount,
(float)mech_location.linearPosition.x,
(float)mech_location.linearPosition.y,
(float)mech_location.linearPosition.z);
// --- BRING-UP: spawn a stationary target/enemy mech (gameplay scaffolding) ---
// The mission system is networked (one pilot per host) and BT's bot/AI code
// is absent, so a solo mission has nothing to fight. When BT_SPAWN_ENEMY is