Add telemetry system: protobuf over Unix socket

- TelemetrySocket: non-blocking Unix domain socket writer (fire-and-forget)
- TelemetryEmitter: high-level event emitter with protobuf serialization
- Hooks into: World::update (snapshots), World::onGo (race start),
  ServerLobby (race finish, player join/leave),
  LinearWorld::newLap (laps, kart finish),
  NetworkItemManager (item collection, switch),
  Flyable (projectile hits)
- CMake: finds protobuf, generates C++ from proto schema, links libprotobuf
- Configurable via STK_TELEMETRY_SOCKET env (disabled if unset)
- Snapshot rate configurable via STK_TELEMETRY_SNAPSHOT_HZ (default 10)
This commit is contained in:
Benjamin
2026-06-11 20:29:04 +02:00
parent eebbfac15f
commit 4a962014a2
11 changed files with 1058 additions and 0 deletions
+30
View File
@@ -530,6 +530,35 @@ endif()
# Provides list of source and header files (STK_SOURCES and STK_HEADERS)
include(sources.cmake)
# --- Telemetry (protobuf) ---------------------------------------------------
# Generate C++ code from the telemetry proto file. Only needed for server builds;
# the telemetry emitter is compiled in unconditionally but is a runtime no-op
# unless STK_TELEMETRY_SOCKET is set.
find_package(Protobuf REQUIRED)
set(TELEMETRY_PROTO "${CMAKE_CURRENT_SOURCE_DIR}/../proto/telemetry.proto")
# If building inside Docker, the proto file is copied to the source tree.
if(NOT EXISTS "${TELEMETRY_PROTO}")
set(TELEMETRY_PROTO "${CMAKE_CURRENT_SOURCE_DIR}/proto/telemetry.proto")
endif()
set(TELEMETRY_PROTO_OUT "${CMAKE_CURRENT_BINARY_DIR}/telemetry_proto")
file(MAKE_DIRECTORY ${TELEMETRY_PROTO_OUT})
set(TELEMETRY_PB_CC "${TELEMETRY_PROTO_OUT}/telemetry.pb.cc")
set(TELEMETRY_PB_H "${TELEMETRY_PROTO_OUT}/telemetry.pb.h")
add_custom_command(
OUTPUT ${TELEMETRY_PB_CC} ${TELEMETRY_PB_H}
COMMAND protobuf::protoc
--cpp_out=${TELEMETRY_PROTO_OUT}
--proto_path=${CMAKE_CURRENT_SOURCE_DIR}/../proto
--proto_path=${CMAKE_CURRENT_SOURCE_DIR}/proto
telemetry.proto
DEPENDS ${TELEMETRY_PROTO}
COMMENT "Generating telemetry protobuf C++"
)
set(STK_SOURCES ${STK_SOURCES} ${TELEMETRY_PB_CC})
set(STK_HEADERS ${STK_HEADERS} ${TELEMETRY_PB_H})
include_directories(${TELEMETRY_PROTO_OUT})
include_directories(${Protobuf_INCLUDE_DIRS})
if (USE_APPLE_NETWORK_LIBRARIES)
add_definitions(-DAPPLE_NETWORK_LIBRARIES)
set(STK_SOURCES
@@ -703,6 +732,7 @@ target_link_libraries(supertuxkart
stkirrlicht
${Angelscript_LIBRARIES}
${MCPP_LIBRARY}
${Protobuf_LIBRARIES}
)
if (USE_APPLE_NETWORK_LIBRARIES)
+12
View File
@@ -45,6 +45,7 @@
#include "network/network_config.hpp"
#include "network/network_string.hpp"
#include "network/rewind_manager.hpp"
#include "network/telemetry/telemetry_emitter.hpp"
#include "physics/physics.hpp"
#include "tracks/track.hpp"
#include "utils/constants.hpp"
@@ -603,6 +604,17 @@ void Flyable::explode(AbstractKart *kart_hit, PhysicalObject *object,
world->kartHit(kart->getWorldKartId(),
m_owner->getWorldKartId());
// Telemetry: kart hit by projectile.
if (TelemetryEmitter::get() != nullptr)
{
Vec3 hit_pos = getXYZ();
TelemetryEmitter::get()->onKartHit(
kart->getWorldKartId(),
m_owner->getWorldKartId(),
static_cast<int>(m_type),
hit_pos.x(), hit_pos.y(), hit_pos.z());
}
if (m_owner->getController()->canGetAchievements())
{
if (m_owner->getWorldKartId() != kart->getWorldKartId())
+15
View File
@@ -25,6 +25,7 @@
#include "network/protocols/game_protocol.hpp"
#include "network/rewind_manager.hpp"
#include "network/stk_host.hpp"
#include "network/telemetry/telemetry_emitter.hpp"
#include "network/stk_peer.hpp"
bool NetworkItemManager::m_network_item_debugging = false;
@@ -102,6 +103,16 @@ void NetworkItemManager::collectedItem(ItemState *item, AbstractKart *kart)
kart->getWorldKartId(),
item->getTicksTillReturn());
m_item_events.unlock();
// Telemetry: item collected.
if (TelemetryEmitter::get() != nullptr)
{
Vec3 xyz = item->getXYZ();
TelemetryEmitter::get()->onItemCollected(
kart->getWorldKartId(), item->getItemId(),
static_cast<int>(item->getType()),
xyz.x(), xyz.y(), xyz.z());
}
}
else
{
@@ -125,6 +136,10 @@ void NetworkItemManager::switchItems()
m_item_events.getData()
.emplace_back(World::getWorld()->getTicksSinceStart());
m_item_events.unlock();
// Telemetry: items switched.
if (TelemetryEmitter::get() != nullptr)
TelemetryEmitter::get()->onItemsSwitched();
}
ItemManager::switchItems();
} // switchItems
+11
View File
@@ -262,6 +262,8 @@ extern "C" {
#include "network/socket_address.hpp"
#include "network/stk_host.hpp"
#include "network/stk_peer.hpp"
#include "network/telemetry/telemetry_emitter.hpp"
#include "network/telemetry/telemetry_socket.hpp"
#include "online/profile_manager.hpp"
#include "online/request_manager.hpp"
#include "race/grand_prix_manager.hpp"
@@ -1496,6 +1498,11 @@ int handleCmdLine(bool has_server_config, bool has_parent_process)
else
NetworkConfig::get()->setIsLAN();
STKHost::create();
// Initialize telemetry system (no-op if STK_TELEMETRY_SOCKET not set).
TelemetrySocket::create();
TelemetryEmitter::create();
if (!GUIEngine::isNoGraphics())
NetworkingLobby::getInstance()->setJoinedServer(server);
else if (NetworkConfig::get()->isClient())
@@ -2718,6 +2725,10 @@ static void cleanSuperTuxKart()
delete main_loop;
// Shutdown telemetry before network cleanup.
TelemetryEmitter::destroy();
TelemetrySocket::destroy();
if(Online::RequestManager::isRunning())
Online::RequestManager::get()->stopNetworkThread();
+17
View File
@@ -39,6 +39,7 @@
#include "network/server_config.hpp"
#include "network/stk_host.hpp"
#include "network/stk_peer.hpp"
#include "network/telemetry/telemetry_emitter.hpp"
#include "race/history.hpp"
#include "states_screens/race_gui_base.hpp"
#include "tracks/check_manager.hpp"
@@ -409,6 +410,17 @@ void LinearWorld::newLap(unsigned int kart_index)
m_kart_info[kart_index].m_finished_laps
* Track::getCurrentTrack()->getTrackLength()
+ getDistanceDownTrackForKart(kart->getWorldKartId(), true);
// Telemetry: lap completed.
if (TelemetryEmitter::get() != nullptr && kart_info.m_finished_laps > 0)
{
float lap_time_ms = stk_config->ticks2Time(
getTimeTicks() - kart_info.m_lap_start_ticks) * 1000.0f;
float total_time_ms = stk_config->ticks2Time(getTimeTicks()) * 1000.0f;
TelemetryEmitter::get()->onLapCompleted(
kart_index, kart_info.m_finished_laps,
lap_time_ms, total_time_ms);
}
}
// Last lap message (kart_index's assert in previous block already)
if (raceHasLaps() && kart_info.m_finished_laps+1 == lap_count)
@@ -522,6 +534,11 @@ void LinearWorld::newLap(unsigned int kart_index)
m_finish_timeout = finish_time * 0.25f + 15.0f;
}
kart->finishedRace(finish_time);
// Telemetry: kart finished the race.
if (TelemetryEmitter::get() != nullptr)
TelemetryEmitter::get()->onKartFinished(
kart_index, kart->getPosition(), finish_time);
}
}
int ticks_per_lap;
+9
View File
@@ -59,6 +59,7 @@
#include "network/network_config.hpp"
#include "network/rewind_manager.hpp"
#include "network/stk_host.hpp"
#include "network/telemetry/telemetry_emitter.hpp"
#include "physics/btKart.hpp"
#include "physics/physics.hpp"
#include "physics/triangle_mesh.hpp"
@@ -728,6 +729,10 @@ void World::onGo()
}
}
}
// Telemetry: race has started (all karts released).
if (TelemetryEmitter::get() != nullptr)
TelemetryEmitter::get()->onRaceStarted();
} // onGo
//-----------------------------------------------------------------------------
@@ -1219,6 +1224,10 @@ void World::update(int ticks)
Physics::get()->update(ticks);
PROFILER_POP_CPU_MARKER();
// Telemetry: emit periodic snapshots after all game state is updated.
if (TelemetryEmitter::get() != nullptr)
TelemetryEmitter::get()->onWorldUpdate(ticks);
PROFILER_POP_CPU_MARKER();
updateTimeTargetSound();
+20
View File
@@ -20,6 +20,7 @@
#include "addons/addon.hpp"
#include "network/race_result_logger.hpp"
#include "network/telemetry/telemetry_emitter.hpp"
#include "config/user_config.hpp"
#include "items/network_item_manager.hpp"
#include "items/powerup_manager.hpp"
@@ -2299,6 +2300,8 @@ void ServerLobby::checkRaceFinished()
Log::info("ServerLobby", "The game is considered finished.");
RaceResultLogger::logResult();
if (TelemetryEmitter::get() != nullptr)
TelemetryEmitter::get()->onRaceFinished();
// notify the network world that it is stopped
RaceEventManager::get()->stop();
@@ -2435,6 +2438,9 @@ void ServerLobby::clientDisconnected(Event* event)
std::string name = StringUtils::wideToUtf8(p->getName());
msg->encodeString(name);
Log::info("ServerLobby", "%s disconnected", name.c_str());
if (TelemetryEmitter::get() != nullptr)
TelemetryEmitter::get()->onPlayerLeft(name, p->getOnlineId(),
event->getPeer()->getHostId(), "disconnected");
}
// Don't show waiting peer disconnect message to in game player
@@ -2871,6 +2877,20 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr<STKPeer> peer,
peer->setValidated(true);
// Telemetry: notify player joined.
if (TelemetryEmitter::get() != nullptr)
{
auto profiles = peer->getPlayerProfiles();
for (auto& prof : profiles)
{
TelemetryEmitter::get()->onPlayerJoined(
StringUtils::wideToUtf8(prof->getName()),
prof->getOnlineId(), peer->getHostId(),
prof->getCountryCode(), peer->isAIPeer(),
peer->getAveragePing());
}
}
// send a message to the one that asked to connect
NetworkString* server_info = getNetworkString();
server_info->setSynchronous(true);
+602
View File
@@ -0,0 +1,602 @@
// telemetry_emitter.cpp — Serializes game state into protobuf and sends it.
#include "network/telemetry/telemetry_emitter.hpp"
#include "network/telemetry/telemetry_socket.hpp"
#include "network/telemetry/telemetry.pb.h"
#include "config/stk_config.hpp"
#include "items/attachment.hpp"
#include "items/powerup.hpp"
#include "karts/abstract_kart.hpp"
#include "karts/kart.hpp"
#include "karts/skidding.hpp"
#include "modes/linear_world.hpp"
#include "modes/world.hpp"
#include "modes/world_status.hpp"
#include "network/remote_kart_info.hpp"
#include "network/server_config.hpp"
#include "race/race_manager.hpp"
#include "tracks/track.hpp"
#include "tracks/track_sector.hpp"
#include "tracks/check_manager.hpp"
#include "utils/log.hpp"
#include "utils/string_utils.hpp"
#include <chrono>
#include <cstdlib>
#include <random>
namespace telemetry = stk::telemetry;
TelemetryEmitter* TelemetryEmitter::s_instance = nullptr;
// Default: emit snapshots every 12 ticks = 10 Hz at 120 tick/s.
static constexpr int DEFAULT_SNAPSHOT_INTERVAL = 12;
static int64_t wallClockMs()
{
using namespace std::chrono;
return duration_cast<milliseconds>(
system_clock::now().time_since_epoch()).count();
}
// ----------------------------------------------------------------------------
TelemetryEmitter* TelemetryEmitter::get()
{
return s_instance;
}
void TelemetryEmitter::create()
{
if (s_instance == nullptr)
s_instance = new TelemetryEmitter();
}
void TelemetryEmitter::destroy()
{
delete s_instance;
s_instance = nullptr;
}
TelemetryEmitter::TelemetryEmitter()
: m_seq(0), m_snapshot_interval(DEFAULT_SNAPSHOT_INTERVAL),
m_ticks_since_snapshot(0)
{
// Check for custom snapshot rate.
const char* rate = std::getenv("STK_TELEMETRY_SNAPSHOT_HZ");
if (rate != nullptr && rate[0] != '\0')
{
int hz = std::atoi(rate);
if (hz > 0 && hz <= 120)
{
m_snapshot_interval = 120 / hz;
Log::info("Telemetry", "Snapshot rate: %d Hz (every %d ticks)",
hz, m_snapshot_interval);
}
}
}
bool TelemetryEmitter::isEnabled() const
{
TelemetrySocket* sock = TelemetrySocket::get();
return sock != nullptr && sock->isEnabled();
}
// ----------------------------------------------------------------------------
std::string TelemetryEmitter::generateGameId()
{
// Simple random hex string for game identification.
static std::mt19937 rng(std::random_device{}());
static const char hex[] = "0123456789abcdef";
std::string id;
id.reserve(16);
for (int i = 0; i < 16; i++)
id += hex[rng() % 16];
return id;
}
void TelemetryEmitter::sendFrame(telemetry::TelemetryFrame& frame)
{
TelemetrySocket* sock = TelemetrySocket::get();
if (sock == nullptr || !sock->isEnabled())
return;
frame.set_seq(m_seq++);
frame.set_timestamp_ms(wallClockMs());
World* world = World::getWorld();
if (world != nullptr)
frame.set_game_tick(world->getTicksSinceStart());
std::string serialized;
if (!frame.SerializeToString(&serialized))
{
Log::warn("Telemetry", "Failed to serialize frame (seq=%llu)",
(unsigned long long)m_seq - 1);
return;
}
sock->sendFrame(serialized);
}
// ----------------------------------------------------------------------------
// Race lifecycle
// ----------------------------------------------------------------------------
void TelemetryEmitter::onRaceStarted()
{
if (!isEnabled()) return;
m_game_id = generateGameId();
m_ticks_since_snapshot = 0;
RaceManager* rm = RaceManager::get();
World* world = World::getWorld();
Track* track = Track::getCurrentTrack();
if (rm == nullptr || world == nullptr || track == nullptr)
return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_race_started();
ev->set_game_id(m_game_id);
ev->set_track_ident(rm->getTrackName());
ev->set_mode(static_cast<telemetry::GameMode>(rm->getMinorRaceMode()));
ev->set_difficulty(rm->getDifficulty());
ev->set_num_laps(rm->getNumLaps());
ev->set_reverse(rm->getReverseTrack());
ev->set_track_length(track->getTrackLength());
CheckManager* cm = track->getCheckManager();
ev->set_num_checkpoints(cm ? cm->getCheckStructureCount() : 0);
unsigned int num_karts = world->getNumKarts();
ev->set_num_karts(num_karts);
for (unsigned int i = 0; i < num_karts; i++)
{
AbstractKart* kart = world->getKart(i);
const RemoteKartInfo& ki = rm->getKartInfo(i);
auto* info = ev->add_karts();
info->set_kart_id(i);
info->set_kart_ident(ki.getKartName());
info->set_player_name(StringUtils::wideToUtf8(ki.getPlayerName()));
info->set_online_id(ki.getOnlineId());
info->set_country(ki.getCountryCode());
info->set_is_ai(ki.isNetworkPlayer() == false && ki.getOnlineId() == 0);
info->set_handicap(static_cast<int>(ki.getHandicap()));
if (kart != nullptr)
{
auto* pos = info->mutable_start_position();
pos->set_x(kart->getXYZ().x());
pos->set_y(kart->getXYZ().y());
pos->set_z(kart->getXYZ().z());
}
}
sendFrame(frame);
Log::info("Telemetry", "Race started: game_id=%s track=%s karts=%d",
m_game_id.c_str(), rm->getTrackName().c_str(), num_karts);
}
void TelemetryEmitter::onRaceFinished()
{
if (!isEnabled()) return;
RaceManager* rm = RaceManager::get();
World* world = World::getWorld();
if (rm == nullptr || world == nullptr) return;
LinearWorld* lw = rm->modeHasLaps()
? dynamic_cast<LinearWorld*>(world) : nullptr;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_race_finished();
ev->set_game_id(m_game_id);
ev->set_total_time(world->getTime());
unsigned int n = rm->getNumPlayers();
for (unsigned int i = 0; i < n; i++)
{
AbstractKart* kart = world->getKart(i);
const RemoteKartInfo& ki = rm->getKartInfo(i);
auto* result = ev->add_results();
result->set_kart_id(i);
result->set_player_name(StringUtils::wideToUtf8(ki.getPlayerName()));
result->set_position(kart ? kart->getPosition() : -1);
result->set_time_ms(rm->getKartRaceTime(i) * 1000.0f);
result->set_score(rm->getKartScore(i));
result->set_eliminated(kart ? kart->isEliminated() : false);
}
if (lw != nullptr)
{
int flt = lw->getFastestLapTicks();
if (flt >= 0)
{
ev->set_fastest_lap_ms(stk_config->ticks2Time(flt) * 1000.0f);
ev->set_fastest_lap_by(
StringUtils::wideToUtf8(lw->getFastestLapKartName()));
}
}
sendFrame(frame);
Log::info("Telemetry", "Race finished: game_id=%s", m_game_id.c_str());
}
void TelemetryEmitter::onRaceCountdown(float seconds_left)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_race_countdown();
ev->set_game_id(m_game_id);
ev->set_seconds_left(seconds_left);
sendFrame(frame);
}
// ----------------------------------------------------------------------------
// Per-tick update
// ----------------------------------------------------------------------------
void TelemetryEmitter::onWorldUpdate(int ticks)
{
if (!isEnabled()) return;
m_ticks_since_snapshot += ticks;
if (m_ticks_since_snapshot >= m_snapshot_interval)
{
m_ticks_since_snapshot = 0;
buildSnapshot();
}
}
void TelemetryEmitter::buildSnapshot()
{
World* world = World::getWorld();
if (world == nullptr) return;
telemetry::TelemetryFrame frame;
auto* snap = frame.mutable_snapshot();
snap->set_game_id(m_game_id);
snap->set_race_time(world->getTime());
snap->set_phase(static_cast<int>(world->getPhase()));
unsigned int num_karts = world->getNumKarts();
for (unsigned int i = 0; i < num_karts; i++)
{
AbstractKart* kart = world->getKart(i);
if (kart == nullptr) continue;
auto* ks = snap->add_karts();
populateKartState(ks, kart, i);
}
sendFrame(frame);
}
void TelemetryEmitter::populateKartState(
telemetry::KartState* ks, AbstractKart* kart, unsigned int kart_id)
{
RaceManager* rm = RaceManager::get();
const RemoteKartInfo& ki = rm->getKartInfo(kart_id);
ks->set_kart_id(kart_id);
ks->set_kart_ident(ki.getKartName());
ks->set_player_name(StringUtils::wideToUtf8(ki.getPlayerName()));
ks->set_online_id(ki.getOnlineId());
ks->set_is_ai(ki.isNetworkPlayer() == false && ki.getOnlineId() == 0);
// Transform
auto* pos = ks->mutable_position();
pos->set_x(kart->getXYZ().x());
pos->set_y(kart->getXYZ().y());
pos->set_z(kart->getXYZ().z());
auto* vel = ks->mutable_velocity();
const btVector3& v = kart->getVelocity();
vel->set_x(v.x());
vel->set_y(v.y());
vel->set_z(v.z());
auto* rot = ks->mutable_rotation();
btQuaternion q = kart->getRotation();
rot->set_x(q.x());
rot->set_y(q.y());
rot->set_z(q.z());
rot->set_w(q.w());
ks->set_heading(kart->getHeading());
ks->set_speed(kart->getSpeed());
// Race progress
ks->set_race_position(kart->getPosition());
ks->set_finished(kart->hasFinishedRace());
if (kart->hasFinishedRace())
ks->set_finish_time(kart->getFinishTime());
// LinearWorld-specific progress
World* world = World::getWorld();
LinearWorld* lw = dynamic_cast<LinearWorld*>(world);
if (lw != nullptr)
{
ks->set_laps_completed(lw->getFinishedLapsOfKart(kart_id));
ks->set_overall_distance(lw->getOverallDistance(kart_id));
ks->set_race_time(rm->getKartRaceTime(kart_id));
// Track sector data
const TrackSector* sector = lw->getTrackSector(kart_id);
if (sector != nullptr)
{
ks->set_distance_along_track(sector->getDistanceFromStart());
ks->set_distance_to_center(sector->getDistanceToCenter());
ks->set_current_graph_node(sector->getCurrentGraphNode());
ks->set_on_road(sector->isOnRoad());
ks->set_last_triggered_checkline(sector->getLastTriggeredCheckline());
}
}
// Physics
Kart* concrete_kart = dynamic_cast<Kart*>(kart);
if (concrete_kart != nullptr)
{
ks->set_on_ground(concrete_kart->isOnGround());
ks->set_is_jumping(concrete_kart->isJumping());
ks->set_steer_percent(kart->getSteerPercent());
// Skidding
const Skidding* skid = concrete_kart->getSkidding();
if (skid != nullptr)
{
ks->set_is_skidding(skid->isSkidding());
ks->set_skid_state(static_cast<telemetry::SkidState>(skid->getSkidState()));
}
// Status effects
ks->set_invulnerable(concrete_kart->isInvulnerable());
ks->set_shielded(concrete_kart->isShielded());
if (concrete_kart->isShielded())
ks->set_shield_time(concrete_kart->getShieldTime());
ks->set_squashed(concrete_kart->isSquashed());
ks->set_plunger_ticks(concrete_kart->getBlockedByPlungerTicks());
}
// Powerup
const Powerup* powerup = kart->getPowerup();
if (powerup != nullptr)
{
ks->set_current_powerup(static_cast<telemetry::PowerupType>(powerup->getType()));
ks->set_powerup_count(powerup->getNum());
}
// Nitro
ks->set_nitro_energy(kart->getEnergy());
// Attachment
const Attachment* att = kart->getAttachment();
if (att != nullptr)
{
ks->set_attachment(static_cast<telemetry::AttachmentType>(att->getType()));
ks->set_attachment_ticks_left(att->getTicksLeft());
}
}
// ----------------------------------------------------------------------------
// Discrete events
// ----------------------------------------------------------------------------
void TelemetryEmitter::onLapCompleted(unsigned int kart_id, int lap,
float lap_time_ms, float total_time_ms)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_lap_completed();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
ev->set_lap_number(lap);
ev->set_lap_time_ms(lap_time_ms);
ev->set_total_time_ms(total_time_ms);
sendFrame(frame);
}
void TelemetryEmitter::onCheckpointPassed(unsigned int kart_id, int checkpoint_id)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_checkpoint_passed();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
ev->set_checkpoint_id(checkpoint_id);
sendFrame(frame);
}
void TelemetryEmitter::onItemCollected(unsigned int kart_id, int item_id,
int item_type, float x, float y, float z)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_item_collected();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
ev->set_item_id(item_id);
ev->set_item_type(static_cast<telemetry::ItemType>(item_type));
auto* pos = ev->mutable_position();
pos->set_x(x); pos->set_y(y); pos->set_z(z);
sendFrame(frame);
}
void TelemetryEmitter::onItemUsed(unsigned int kart_id, int powerup_type,
float px, float py, float pz,
float dx, float dy, float dz,
unsigned int target_kart_id)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_item_used();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
ev->set_type(static_cast<telemetry::PowerupType>(powerup_type));
auto* pos = ev->mutable_position();
pos->set_x(px); pos->set_y(py); pos->set_z(pz);
auto* dir = ev->mutable_direction();
dir->set_x(dx); dir->set_y(dy); dir->set_z(dz);
ev->set_target_kart_id(target_kart_id);
sendFrame(frame);
}
void TelemetryEmitter::onItemDropped(unsigned int kart_id, int item_type,
float x, float y, float z)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_item_dropped();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
ev->set_item_type(static_cast<telemetry::ItemType>(item_type));
auto* pos = ev->mutable_position();
pos->set_x(x); pos->set_y(y); pos->set_z(z);
sendFrame(frame);
}
void TelemetryEmitter::onItemsSwitched()
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_items_switched();
ev->set_game_id(m_game_id);
sendFrame(frame);
}
void TelemetryEmitter::onKartHit(unsigned int victim_id, unsigned int hitter_id,
int weapon_type, float x, float y, float z)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_kart_hit();
ev->set_game_id(m_game_id);
ev->set_victim_kart_id(victim_id);
ev->set_hitter_kart_id(hitter_id);
ev->set_weapon(static_cast<telemetry::PowerupType>(weapon_type));
auto* pos = ev->mutable_position();
pos->set_x(x); pos->set_y(y); pos->set_z(z);
sendFrame(frame);
}
void TelemetryEmitter::onKartCollision(unsigned int kart_a, unsigned int kart_b,
float x, float y, float z, float impact_speed)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_kart_collision();
ev->set_game_id(m_game_id);
ev->set_kart_a_id(kart_a);
ev->set_kart_b_id(kart_b);
auto* pos = ev->mutable_position();
pos->set_x(x); pos->set_y(y); pos->set_z(z);
ev->set_impact_speed(impact_speed);
sendFrame(frame);
}
void TelemetryEmitter::onKartEliminated(unsigned int kart_id, float race_time)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_kart_eliminated();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
ev->set_race_time(race_time);
sendFrame(frame);
}
void TelemetryEmitter::onKartRespawned(unsigned int kart_id,
float x, float y, float z)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_kart_respawned();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
auto* pos = ev->mutable_respawn_position();
pos->set_x(x); pos->set_y(y); pos->set_z(z);
sendFrame(frame);
}
void TelemetryEmitter::onKartFinished(unsigned int kart_id, int position,
float finish_time)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_kart_finished();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
ev->set_position(position);
ev->set_finish_time(finish_time);
sendFrame(frame);
}
void TelemetryEmitter::onKartRescued(unsigned int kart_id,
float x, float y, float z)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_kart_rescued();
ev->set_game_id(m_game_id);
ev->set_kart_id(kart_id);
auto* pos = ev->mutable_position();
pos->set_x(x); pos->set_y(y); pos->set_z(z);
sendFrame(frame);
}
// ----------------------------------------------------------------------------
// Lobby events
// ----------------------------------------------------------------------------
void TelemetryEmitter::onPlayerJoined(const std::string& name, unsigned int online_id,
unsigned int host_id, const std::string& country,
bool is_ai, int ping_ms)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_player_joined();
ev->set_player_name(name);
ev->set_online_id(online_id);
ev->set_host_id(host_id);
ev->set_country(country);
ev->set_is_ai(is_ai);
ev->set_avg_ping_ms(ping_ms);
sendFrame(frame);
}
void TelemetryEmitter::onPlayerLeft(const std::string& name, unsigned int online_id,
unsigned int host_id, const std::string& reason)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_player_left();
ev->set_player_name(name);
ev->set_online_id(online_id);
ev->set_host_id(host_id);
ev->set_reason(reason);
sendFrame(frame);
}
void TelemetryEmitter::onLobbyStateChanged(int phase, int num_players, int max_players,
const std::string& server_name,
const std::string& track,
int mode, int difficulty)
{
if (!isEnabled()) return;
telemetry::TelemetryFrame frame;
auto* ev = frame.mutable_lobby_state();
ev->set_phase(static_cast<telemetry::LobbyPhase>(phase));
ev->set_num_players(num_players);
ev->set_max_players(max_players);
ev->set_server_name(server_name);
ev->set_track_ident(track);
ev->set_mode(static_cast<telemetry::GameMode>(mode));
ev->set_difficulty(difficulty);
sendFrame(frame);
}
@@ -0,0 +1,81 @@
// telemetry_emitter.hpp — High-level telemetry event emitter.
//
// Hooks into STK game events (World::update, item collection, laps, etc.)
// and emits protobuf-serialized TelemetryFrame messages via TelemetrySocket.
#ifndef HEADER_TELEMETRY_EMITTER_HPP
#define HEADER_TELEMETRY_EMITTER_HPP
#include <cstdint>
#include <string>
#include "network/telemetry/telemetry.pb.h"
class AbstractKart;
class TelemetryEmitter
{
public:
static TelemetryEmitter* get();
static void create();
static void destroy();
/** Returns true if telemetry is active (socket enabled). */
bool isEnabled() const;
// --- Configuration ---
/** Set snapshot rate (default: every 12 ticks = 10 Hz at 120 tick/s). */
void setSnapshotInterval(int ticks) { m_snapshot_interval = ticks; }
// --- Race lifecycle ---
void onRaceStarted();
void onRaceFinished();
void onRaceCountdown(float seconds_left);
// --- Per-tick update (call from World::update) ---
void onWorldUpdate(int ticks);
// --- Discrete events ---
void onLapCompleted(unsigned int kart_id, int lap, float lap_time_ms, float total_time_ms);
void onCheckpointPassed(unsigned int kart_id, int checkpoint_id);
void onItemCollected(unsigned int kart_id, int item_id, int item_type, float x, float y, float z);
void onItemUsed(unsigned int kart_id, int powerup_type, float px, float py, float pz,
float dx, float dy, float dz, unsigned int target_kart_id);
void onItemDropped(unsigned int kart_id, int item_type, float x, float y, float z);
void onItemsSwitched();
void onKartHit(unsigned int victim_id, unsigned int hitter_id, int weapon_type,
float x, float y, float z);
void onKartCollision(unsigned int kart_a, unsigned int kart_b,
float x, float y, float z, float impact_speed);
void onKartEliminated(unsigned int kart_id, float race_time);
void onKartRespawned(unsigned int kart_id, float x, float y, float z);
void onKartFinished(unsigned int kart_id, int position, float finish_time);
void onKartRescued(unsigned int kart_id, float x, float y, float z);
// --- Lobby events ---
void onPlayerJoined(const std::string& name, unsigned int online_id,
unsigned int host_id, const std::string& country, bool is_ai, int ping_ms);
void onPlayerLeft(const std::string& name, unsigned int online_id,
unsigned int host_id, const std::string& reason);
void onLobbyStateChanged(int phase, int num_players, int max_players,
const std::string& server_name, const std::string& track,
int mode, int difficulty);
private:
TelemetryEmitter();
~TelemetryEmitter() = default;
void sendFrame(stk::telemetry::TelemetryFrame& frame);
void buildSnapshot();
void populateKartState(stk::telemetry::KartState* ks, AbstractKart* kart, unsigned int kart_id);
std::string generateGameId();
static TelemetryEmitter* s_instance;
uint64_t m_seq;
std::string m_game_id;
int m_snapshot_interval; // ticks between snapshots
int m_ticks_since_snapshot;
};
#endif // HEADER_TELEMETRY_EMITTER_HPP
+204
View File
@@ -0,0 +1,204 @@
// telemetry_socket.cpp — Unix domain socket writer implementation.
#include "network/telemetry/telemetry_socket.hpp"
#include "utils/log.hpp"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <chrono>
#ifdef __linux__
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#endif
TelemetrySocket* TelemetrySocket::s_instance = nullptr;
// Reconnect cooldown (don't spam connect attempts).
static constexpr int64_t RECONNECT_COOLDOWN_MS = 2000;
static int64_t nowMs()
{
using namespace std::chrono;
return duration_cast<milliseconds>(
steady_clock::now().time_since_epoch()).count();
}
// ----------------------------------------------------------------------------
TelemetrySocket* TelemetrySocket::get()
{
return s_instance;
}
void TelemetrySocket::create()
{
if (s_instance == nullptr)
s_instance = new TelemetrySocket();
}
void TelemetrySocket::destroy()
{
delete s_instance;
s_instance = nullptr;
}
// ----------------------------------------------------------------------------
TelemetrySocket::TelemetrySocket()
: m_enabled(false), m_fd(-1),
m_last_connect_attempt_ms(0),
m_frames_sent(0), m_frames_dropped(0)
{
#ifdef __linux__
const char* path = std::getenv("STK_TELEMETRY_SOCKET");
if (path == nullptr || path[0] == '\0')
{
Log::info("Telemetry", "STK_TELEMETRY_SOCKET not set, telemetry disabled.");
return;
}
m_socket_path = path;
m_enabled = true;
Log::info("Telemetry", "Telemetry enabled, socket: %s", m_socket_path.c_str());
tryConnect();
#else
Log::info("Telemetry", "Telemetry only supported on Linux.");
#endif
}
TelemetrySocket::~TelemetrySocket()
{
disconnect();
if (m_frames_sent > 0 || m_frames_dropped > 0)
{
Log::info("Telemetry", "Session stats: sent=%llu dropped=%llu",
(unsigned long long)m_frames_sent,
(unsigned long long)m_frames_dropped);
}
}
// ----------------------------------------------------------------------------
void TelemetrySocket::tryConnect()
{
#ifdef __linux__
if (m_fd >= 0)
return; // already connected
int64_t now = nowMs();
if (now - m_last_connect_attempt_ms < RECONNECT_COOLDOWN_MS)
return; // cooldown
m_last_connect_attempt_ms = now;
m_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (m_fd < 0)
{
Log::warn("Telemetry", "socket() failed: %s", strerror(errno));
return;
}
// Set non-blocking.
int flags = fcntl(m_fd, F_GETFL, 0);
fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, m_socket_path.c_str(), sizeof(addr.sun_path) - 1);
if (connect(m_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
{
if (errno != EINPROGRESS)
{
// Connection failed immediately (collector not running yet).
close(m_fd);
m_fd = -1;
// Only log once per cooldown cycle to avoid spam.
return;
}
// EINPROGRESS: connection pending — we'll detect success/failure on next write.
}
Log::info("Telemetry", "Connected to collector at %s", m_socket_path.c_str());
#endif
}
void TelemetrySocket::disconnect()
{
#ifdef __linux__
if (m_fd >= 0)
{
close(m_fd);
m_fd = -1;
}
#endif
}
// ----------------------------------------------------------------------------
void TelemetrySocket::sendFrame(const uint8_t* data, size_t size)
{
#ifdef __linux__
if (!m_enabled)
return;
if (m_fd < 0)
{
tryConnect();
if (m_fd < 0)
{
m_frames_dropped++;
return;
}
}
// Wire format: [4 bytes LE length][payload]
uint32_t len = static_cast<uint32_t>(size);
uint8_t header[4];
header[0] = (len ) & 0xFF;
header[1] = (len >> 8) & 0xFF;
header[2] = (len >> 16) & 0xFF;
header[3] = (len >> 24) & 0xFF;
// Use writev to send header + payload in one syscall.
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 4;
iov[1].iov_base = const_cast<uint8_t*>(data);
iov[1].iov_len = size;
ssize_t written = writev(m_fd, iov, 2);
if (written < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// Collector is slow; drop the frame.
m_frames_dropped++;
return;
}
// Broken pipe or other error — disconnect and retry later.
disconnect();
m_frames_dropped++;
return;
}
if ((size_t)written < size + 4)
{
// Partial write on non-blocking socket — treat as dropped.
// (In practice, small frames < 64KB rarely partial-write on Unix sockets.)
disconnect();
m_frames_dropped++;
return;
}
m_frames_sent++;
#else
(void)data;
(void)size;
#endif
}
@@ -0,0 +1,57 @@
// telemetry_socket.hpp — Non-blocking Unix domain socket writer for telemetry.
//
// The writer connects to a Unix socket path (from env $STK_TELEMETRY_SOCKET).
// If the socket is unavailable or the collector is slow, frames are silently
// dropped — telemetry NEVER blocks the game loop.
//
// Wire format: [4-byte LE length][protobuf TelemetryFrame payload]
//
// Thread safety: all writes happen on the game thread (World::update path).
// No locking needed.
#ifndef HEADER_TELEMETRY_SOCKET_HPP
#define HEADER_TELEMETRY_SOCKET_HPP
#include <cstdint>
#include <string>
#include <vector>
class TelemetrySocket
{
public:
static TelemetrySocket* get();
static void create();
static void destroy();
/** Returns true if telemetry is enabled (socket path configured). */
bool isEnabled() const { return m_enabled; }
/** Send a serialized protobuf frame. Non-blocking; drops if can't write. */
void sendFrame(const uint8_t* data, size_t size);
void sendFrame(const std::vector<uint8_t>& data)
{
sendFrame(data.data(), data.size());
}
void sendFrame(const std::string& data)
{
sendFrame(reinterpret_cast<const uint8_t*>(data.data()), data.size());
}
private:
TelemetrySocket();
~TelemetrySocket();
void tryConnect();
void disconnect();
static TelemetrySocket* s_instance;
bool m_enabled;
std::string m_socket_path;
int m_fd;
int64_t m_last_connect_attempt_ms;
uint64_t m_frames_sent;
uint64_t m_frames_dropped;
};
#endif // HEADER_TELEMETRY_SOCKET_HPP