Files
BT411/game/reconstructed/matchlog.cpp
T
arcattackandClaude Opus 4.8 753540de96 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>
2026-07-22 13:22:10 -05:00

143 lines
3.7 KiB
C++

//===========================================================================//
// 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);
}