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>
143 lines
3.7 KiB
C++
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);
|
|
}
|