Add race result logger (existing patch from stk-server-docker)

This commit is contained in:
Benjamin
2026-06-11 20:08:59 +02:00
parent 2957e51aaa
commit eebbfac15f
3 changed files with 154 additions and 0 deletions
+2
View File
@@ -19,6 +19,7 @@
#include "network/protocols/server_lobby.hpp" #include "network/protocols/server_lobby.hpp"
#include "addons/addon.hpp" #include "addons/addon.hpp"
#include "network/race_result_logger.hpp"
#include "config/user_config.hpp" #include "config/user_config.hpp"
#include "items/network_item_manager.hpp" #include "items/network_item_manager.hpp"
#include "items/powerup_manager.hpp" #include "items/powerup_manager.hpp"
@@ -2297,6 +2298,7 @@ void ServerLobby::checkRaceFinished()
if (!RaceEventManager::get()->isRaceOver()) return; if (!RaceEventManager::get()->isRaceOver()) return;
Log::info("ServerLobby", "The game is considered finished."); Log::info("ServerLobby", "The game is considered finished.");
RaceResultLogger::logResult();
// notify the network world that it is stopped // notify the network world that it is stopped
RaceEventManager::get()->stop(); RaceEventManager::get()->stop();
+137
View File
@@ -0,0 +1,137 @@
// SuperTuxKart dedicated-server PATCH (not part of upstream STK).
//
// Captures the per-player classification of a finished networked game and
// appends it as one JSON object (newline-delimited) to $STK_RESULTS_FILE.
// A separate "result-shipper" sidecar tails that file and POSTs each line to
// the stats dashboard. Writing to a local file (rather than doing network I/O
// here) keeps the game loop free of blocking/error-prone calls.
//
// Called from ServerLobby::checkRaceFinished() while World/RaceManager are
// still alive (see stk-patches/0001-race-result-hook.patch).
#include "network/race_result_logger.hpp"
#include "config/stk_config.hpp"
#include "karts/abstract_kart.hpp"
#include "modes/linear_world.hpp"
#include "modes/world.hpp"
#include "network/remote_kart_info.hpp"
#include "network/server_config.hpp"
#include "race/race_manager.hpp"
#include "utils/log.hpp"
#include "utils/string_utils.hpp"
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <sstream>
#include <string>
namespace
{
std::string jsonEscape(const std::string& in)
{
std::string out;
out.reserve(in.size() + 8);
for (unsigned char c : in)
{
switch (c)
{
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20)
{
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", (int)c);
out += buf;
}
else
{
out += (char)c;
}
}
}
return out;
}
} // namespace
void RaceResultLogger::logResult()
{
const char* path = std::getenv("STK_RESULTS_FILE");
if (path == NULL || path[0] == '\0')
return; // feature disabled -> behave like stock STK
World* world = World::getWorld();
RaceManager* rm = RaceManager::get();
if (world == NULL || rm == NULL)
return;
LinearWorld* lw = rm->modeHasLaps()
? static_cast<LinearWorld*>(world) : NULL;
std::ostringstream o;
o << "{";
o << "\"schema\":1";
o << ",\"ts\":" << (long long)std::time(NULL);
o << ",\"server\":\""
<< jsonEscape((std::string)ServerConfig::m_server_name) << "\"";
o << ",\"track\":\"" << jsonEscape(rm->getTrackName()) << "\"";
o << ",\"mode\":\"" << jsonEscape(rm->getMinorModeName()) << "\"";
o << ",\"difficulty\":" << (int)rm->getDifficulty();
o << ",\"laps\":" << rm->getNumLaps();
o << ",\"reverse\":" << (rm->getReverseTrack() ? "true" : "false");
if (lw != NULL)
{
int flt = lw->getFastestLapTicks();
long long flms = (flt >= 0)
? (long long)(stk_config->ticks2Time(flt) * 1000.0f) : -1;
o << ",\"fastest_lap_ms\":" << flms;
o << ",\"fastest_lap_by\":\""
<< jsonEscape(StringUtils::wideToUtf8(lw->getFastestLapKartName()))
<< "\"";
}
o << ",\"players\":[";
const unsigned n = rm->getNumPlayers();
bool first = true;
for (unsigned i = 0; i < n; i++)
{
const RemoteKartInfo& ki = rm->getKartInfo(i);
AbstractKart* kart = world->getKart(i);
if (!first) o << ",";
first = false;
o << "{";
o << "\"name\":\""
<< jsonEscape(StringUtils::wideToUtf8(ki.getPlayerName())) << "\"";
o << ",\"online_id\":" << (unsigned)ki.getOnlineId();
o << ",\"country\":\"" << jsonEscape(ki.getCountryCode()) << "\"";
o << ",\"kart\":\"" << jsonEscape(ki.getKartName()) << "\"";
o << ",\"position\":" << (kart != NULL ? kart->getPosition() : -1);
o << ",\"time_ms\":"
<< (long long)(rm->getKartRaceTime(i) * 1000.0f);
o << ",\"score\":" << rm->getKartScore(i);
o << ",\"eliminated\":"
<< ((kart != NULL && kart->isEliminated()) ? "true" : "false");
o << "}";
}
o << "]}";
std::ofstream f(path, std::ios::out | std::ios::app);
if (!f.is_open())
{
Log::warn("RaceResultLogger",
"Could not open results file '%s'", path);
return;
}
f << o.str() << "\n";
f.flush();
Log::info("RaceResultLogger",
"Wrote race result: track=%s mode=%s players=%u",
rm->getTrackName().c_str(), rm->getMinorModeName().c_str(), n);
} // logResult
+15
View File
@@ -0,0 +1,15 @@
// SuperTuxKart dedicated-server PATCH (not part of upstream STK).
// Emits one JSON line per finished game so an external service can build
// leaderboards / history / records. See apps/stk-server/stk-patches/README.md.
#ifndef HEADER_RACE_RESULT_LOGGER_HPP
#define HEADER_RACE_RESULT_LOGGER_HPP
namespace RaceResultLogger
{
/** Append a single JSON object describing the just-finished game to the
* file named by the STK_RESULTS_FILE environment variable. No-op when the
* variable is unset/empty, so an unpatched-feeling default is preserved. */
void logResult();
}
#endif