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:
co-authored by
Claude Opus 4.8
parent
3097d269b4
commit
753540de96
@@ -0,0 +1,142 @@
|
||||
//===========================================================================//
|
||||
// matchlog.cpp -- per-peer MP forensic combat log (PORT tooling, 2026-07-22)//
|
||||
//---------------------------------------------------------------------------//
|
||||
// See matchlog.hpp for the contract + event catalogue. This TU only owns
|
||||
// the file + the line writer; the event call sites live at the authoritative
|
||||
// combat hooks (mech.cpp DMG, mechweap.cpp FIRE, mech4.cpp DEATH/PROJ/
|
||||
// SPLASH/RAM, btplayer.cpp VEHICLE/PLAYER_DEAD/SCORE, mechdmg.cpp CRIT) and
|
||||
// two engine seams (APP.cpp MISSION, L4NET.CPP PEER_UP/DOWN via extern).
|
||||
//===========================================================================//
|
||||
|
||||
#include <bt.hpp>
|
||||
#include <app.hpp> // Application/application (the st= line prefix)
|
||||
#include <matchlog.hpp>
|
||||
#include <btversion.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
int
|
||||
BTMatchHostOf(const EntityID &id)
|
||||
{
|
||||
EntityID copy(id); // GetHostID() is non-const
|
||||
return (int)copy.GetHostID();
|
||||
}
|
||||
|
||||
int
|
||||
BTMatchLocalOf(const EntityID &id)
|
||||
{
|
||||
return (int)id; // operator int() const == localID
|
||||
}
|
||||
|
||||
static FILE *s_matchLog = 0;
|
||||
static int s_matchState = -1; // -1 unresolved, 0 off, 1 armed
|
||||
static char s_matchPath[160] = "";
|
||||
|
||||
const char *
|
||||
BTMatchLogPath()
|
||||
{
|
||||
return s_matchPath;
|
||||
}
|
||||
|
||||
void
|
||||
BTMatchLogClose()
|
||||
{
|
||||
if (s_matchLog != 0)
|
||||
{
|
||||
fflush(s_matchLog);
|
||||
fclose(s_matchLog);
|
||||
s_matchLog = 0;
|
||||
s_matchState = 0; // further events: no-op
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
BTMatchLogActive()
|
||||
{
|
||||
if (s_matchState >= 0)
|
||||
return s_matchState;
|
||||
|
||||
//
|
||||
// Resolve the gate once. BT_MATCHLOG overrides; the default arms for
|
||||
// any "-net" launch (every MP path) and stays silent for solo.
|
||||
//
|
||||
int on;
|
||||
const char *e = getenv("BT_MATCHLOG");
|
||||
if (e != 0)
|
||||
{
|
||||
on = (atoi(e) != 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char *cmd = GetCommandLineA();
|
||||
on = (cmd != 0 && strstr(cmd, "-net") != 0);
|
||||
}
|
||||
|
||||
if (on)
|
||||
{
|
||||
SYSTEMTIME now;
|
||||
GetLocalTime(&now);
|
||||
|
||||
sprintf(s_matchPath, "matchlog_%04u%02u%02u_%02u%02u%02u_%lu.txt",
|
||||
(unsigned)now.wYear, (unsigned)now.wMonth, (unsigned)now.wDay,
|
||||
(unsigned)now.wHour, (unsigned)now.wMinute, (unsigned)now.wSecond,
|
||||
(unsigned long)GetCurrentProcessId());
|
||||
s_matchLog = fopen(s_matchPath, "w");
|
||||
if (s_matchLog == 0)
|
||||
s_matchPath[0] = '\0';
|
||||
}
|
||||
s_matchState = (s_matchLog != 0) ? 1 : 0;
|
||||
|
||||
if (s_matchState)
|
||||
{
|
||||
char machine[MAX_COMPUTERNAME_LENGTH + 1] = "?";
|
||||
DWORD machine_length = sizeof(machine);
|
||||
GetComputerNameA(machine, &machine_length);
|
||||
|
||||
const char *self = getenv("BT_SELF");
|
||||
const char *relay = getenv("BT_RELAY");
|
||||
SYSTEMTIME now;
|
||||
GetLocalTime(&now);
|
||||
BTMatchLog("HDR",
|
||||
"ver=\"%s\" date=%04u-%02u-%02u pid=%lu machine=\"%s\" "
|
||||
"self=\"%s\" relay=\"%s\" cmd=\"%s\"",
|
||||
BT_VERSION_FULL,
|
||||
(unsigned)now.wYear, (unsigned)now.wMonth, (unsigned)now.wDay,
|
||||
(unsigned long)GetCurrentProcessId(), machine,
|
||||
self != 0 ? self : "",
|
||||
relay != 0 ? relay : "",
|
||||
GetCommandLineA() != 0 ? GetCommandLineA() : "");
|
||||
}
|
||||
return s_matchState;
|
||||
}
|
||||
|
||||
void
|
||||
BTMatchLog(const char *tag, const char *format, ...)
|
||||
{
|
||||
if (!BTMatchLogActive())
|
||||
return;
|
||||
|
||||
char body[640];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
_vsnprintf(body, sizeof(body) - 1, format, args);
|
||||
body[sizeof(body) - 1] = 0;
|
||||
va_end(args);
|
||||
|
||||
SYSTEMTIME now;
|
||||
GetLocalTime(&now);
|
||||
|
||||
// The app state rides every line (WaitingForEgg .. RunningMission ..)
|
||||
// so the tool can window events to the live mission without a separate
|
||||
// state-transition hook.
|
||||
int app_state = (application != 0)
|
||||
? (int)application->GetApplicationState() : -1;
|
||||
|
||||
fprintf(s_matchLog, "%s t=%lu w=%02u:%02u:%02u.%03u st=%d %s\n",
|
||||
tag, (unsigned long)GetTickCount(),
|
||||
(unsigned)now.wHour, (unsigned)now.wMinute,
|
||||
(unsigned)now.wSecond, (unsigned)now.wMilliseconds,
|
||||
app_state, body);
|
||||
fflush(s_matchLog);
|
||||
}
|
||||
Reference in New Issue
Block a user