diff --git a/CMakeLists.txt b/CMakeLists.txt index ee4ab78..2b28593 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/items/flyable.cpp b/src/items/flyable.cpp index d22b6e6..5d89f94 100644 --- a/src/items/flyable.cpp +++ b/src/items/flyable.cpp @@ -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(m_type), + hit_pos.x(), hit_pos.y(), hit_pos.z()); + } + if (m_owner->getController()->canGetAchievements()) { if (m_owner->getWorldKartId() != kart->getWorldKartId()) diff --git a/src/items/network_item_manager.cpp b/src/items/network_item_manager.cpp index 63af067..44ae245 100644 --- a/src/items/network_item_manager.cpp +++ b/src/items/network_item_manager.cpp @@ -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(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 diff --git a/src/main.cpp b/src/main.cpp index 5bd1979..d261e68 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(); diff --git a/src/modes/linear_world.cpp b/src/modes/linear_world.cpp index f0b115f..a071cdf 100644 --- a/src/modes/linear_world.cpp +++ b/src/modes/linear_world.cpp @@ -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; diff --git a/src/modes/world.cpp b/src/modes/world.cpp index d61405b..9365292 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -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(); diff --git a/src/network/protocols/server_lobby.cpp b/src/network/protocols/server_lobby.cpp index 18d1abf..a82b882 100644 --- a/src/network/protocols/server_lobby.cpp +++ b/src/network/protocols/server_lobby.cpp @@ -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 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); diff --git a/src/network/telemetry/telemetry_emitter.cpp b/src/network/telemetry/telemetry_emitter.cpp new file mode 100644 index 0000000..45f76a9 --- /dev/null +++ b/src/network/telemetry/telemetry_emitter.cpp @@ -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 +#include +#include + +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( + 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(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(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(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(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(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); + 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(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(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(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(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(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(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(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(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(mode)); + ev->set_difficulty(difficulty); + sendFrame(frame); +} diff --git a/src/network/telemetry/telemetry_emitter.hpp b/src/network/telemetry/telemetry_emitter.hpp new file mode 100644 index 0000000..70602e3 --- /dev/null +++ b/src/network/telemetry/telemetry_emitter.hpp @@ -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 +#include + +#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 diff --git a/src/network/telemetry/telemetry_socket.cpp b/src/network/telemetry/telemetry_socket.cpp new file mode 100644 index 0000000..9bdfee1 --- /dev/null +++ b/src/network/telemetry/telemetry_socket.cpp @@ -0,0 +1,204 @@ +// telemetry_socket.cpp — Unix domain socket writer implementation. + +#include "network/telemetry/telemetry_socket.hpp" +#include "utils/log.hpp" + +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#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( + 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(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(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 +} diff --git a/src/network/telemetry/telemetry_socket.hpp b/src/network/telemetry/telemetry_socket.hpp new file mode 100644 index 0000000..fc3e667 --- /dev/null +++ b/src/network/telemetry/telemetry_socket.hpp @@ -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 +#include +#include + +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& data) + { + sendFrame(data.data(), data.size()); + } + void sendFrame(const std::string& data) + { + sendFrame(reinterpret_cast(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