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:
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user