diff --git a/src/network/protocols/server_lobby.cpp b/src/network/protocols/server_lobby.cpp index 2006512..18d1abf 100644 --- a/src/network/protocols/server_lobby.cpp +++ b/src/network/protocols/server_lobby.cpp @@ -19,6 +19,7 @@ #include "network/protocols/server_lobby.hpp" #include "addons/addon.hpp" +#include "network/race_result_logger.hpp" #include "config/user_config.hpp" #include "items/network_item_manager.hpp" #include "items/powerup_manager.hpp" @@ -2297,6 +2298,7 @@ void ServerLobby::checkRaceFinished() if (!RaceEventManager::get()->isRaceOver()) return; Log::info("ServerLobby", "The game is considered finished."); + RaceResultLogger::logResult(); // notify the network world that it is stopped RaceEventManager::get()->stop(); diff --git a/src/network/race_result_logger.cpp b/src/network/race_result_logger.cpp new file mode 100644 index 0000000..093c8da --- /dev/null +++ b/src/network/race_result_logger.cpp @@ -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 +#include +#include +#include +#include +#include + +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(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 diff --git a/src/network/race_result_logger.hpp b/src/network/race_result_logger.hpp new file mode 100644 index 0000000..07dcf24 --- /dev/null +++ b/src/network/race_result_logger.hpp @@ -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