diff --git a/src/Features/Speedrun/SpeedrunTimer.cpp b/src/Features/Speedrun/SpeedrunTimer.cpp index 1e162594..365a5efb 100644 --- a/src/Features/Speedrun/SpeedrunTimer.cpp +++ b/src/Features/Speedrun/SpeedrunTimer.cpp @@ -27,6 +27,7 @@ #include "Modules/Server.hpp" #include "Scheduler.hpp" #include "Utils.hpp" +#include "Utils/ed25519/ed25519.h" #define SPEEDRUN_PACKET_TYPE "srtimer" #define SYNC_INTERVAL 1.0f // Sync every second, just in case @@ -39,6 +40,8 @@ enum PacketType { STOP, SPLIT, RESET, + ID_REQUEST, // Orange requests speedrun ID + ID_RESPONSE, // Blue responds with speedrun ID }; // TimerAction {{{ @@ -118,6 +121,9 @@ static struct std::vector visitedMaps; std::string lastMap; + + uint8_t speedrunId[16]; // 128-bit unique identifier + bool hasSpeedrunId; // Whether ID has been set } g_speedrun; static std::map g_activeRun; @@ -126,6 +132,8 @@ static std::vector> g_runs; bool g_timePauses = false; static void handleCoopPacket(const void *data, size_t size); +static void sendIdRequest(); +static void sendIdResponse(); ON_INIT { g_timerInterface = new TimerInterface(); @@ -160,8 +168,6 @@ static bool g_coopActuallyReady = false; static int g_coopLastReadyTick = 0; static void handleCoopPacket(const void *data, size_t size) { - if (!engine->IsOrange()) return; - char *data_ = (char *)data; if (size < 5) return; @@ -169,6 +175,17 @@ static void handleCoopPacket(const void *data, size_t size) { PacketType t = (PacketType)data_[0]; int tick = *(int *)(data_ + 1); + // Blue handles ID_REQUEST, Orange handles everything else + if (t == PacketType::ID_REQUEST) { + if (!engine->IsOrange() && g_speedrun.hasSpeedrunId) { + sendIdResponse(); + } + return; + } + + // All other packets are Orange-only + if (!engine->IsOrange()) return; + g_coopLastSyncTick = tick; g_coopLastSyncEngineTick = engine->GetTick(); @@ -179,6 +196,10 @@ static void handleCoopPacket(const void *data, size_t size) { break; case PacketType::START: SpeedrunTimer::Start(); + // Orange requests the speedrun ID + if (g_partnerHasSAR) { + sendIdRequest(); + } break; case PacketType::PAUSE: SpeedrunTimer::Pause(); @@ -196,6 +217,13 @@ static void handleCoopPacket(const void *data, size_t size) { case PacketType::RESET: SpeedrunTimer::Reset(); break; + case PacketType::ID_RESPONSE: + // Orange receives speedrun ID + if (size >= 21) { + memcpy(g_speedrun.speedrunId, data_ + 5, 16); + g_speedrun.hasSpeedrunId = true; + } + break; } } @@ -309,6 +337,32 @@ static void sendCoopPacket(PacketType t, std::string *splitName = NULL, int newS free(buf); } +void SpeedrunTimer::WriteIdToDemo() { + if (!engine->demorecorder->isRecordingDemo) return; + if (!g_speedrun.hasSpeedrunId) return; + + uint8_t data[17]; + data[0] = 0x13; // Demo data type for speedrun identifier + memcpy(data + 1, g_speedrun.speedrunId, 16); + + engine->demorecorder->RecordData(data, sizeof(data)); +} + +static void sendIdRequest() { + char buf[5]; + buf[0] = (char)PacketType::ID_REQUEST; + *(int *)(buf + 1) = getCurrentTick(); + NetMessage::SendMsg(SPEEDRUN_PACKET_TYPE, buf, 5); +} + +static void sendIdResponse() { + char buf[21]; + buf[0] = (char)PacketType::ID_RESPONSE; + *(int *)(buf + 1) = getCurrentTick(); + memcpy(buf + 5, g_speedrun.speedrunId, 16); + NetMessage::SendMsg(SPEEDRUN_PACKET_TYPE, buf, 21); +} + int SpeedrunTimer::GetOffsetTicks() { const char *offset = sar_speedrun_offset.GetString(); @@ -511,6 +565,19 @@ void SpeedrunTimer::Start() { g_speedrun.lastMap = map; g_speedrun.visitedMaps.push_back(map); + // Generate unique speedrun ID + if (ed25519_create_seed(g_speedrun.speedrunId) != 0) { + // Fallback: use timestamp + random if crypto fails + auto now = std::chrono::system_clock::now().time_since_epoch(); + uint64_t timestamp = std::chrono::duration_cast(now).count(); + *(uint64_t *)(g_speedrun.speedrunId) = timestamp; + for (int i = 8; i < 16; i++) { + g_speedrun.speedrunId[i] = (uint8_t)(Math::RandomNumber(0, 255)); + } + } + g_speedrun.hasSpeedrunId = true; + SpeedrunTimer::WriteIdToDemo(); // Write to current demo if recording + sendCoopPacket(PacketType::START); if (!sar_mtrigger_legacy.GetBool()) { toastHud.AddToast(SPEEDRUN_TOAST_TAG, "Speedrun started!"); @@ -793,6 +860,10 @@ void SpeedrunTimer::Reset(bool requested) { g_speedrun.splits.clear(); g_speedrun.visitedMaps.clear(); + // Clear speedrun ID + g_speedrun.hasSpeedrunId = false; + memset(g_speedrun.speedrunId, 0, 16); + if (networkManager.isConnected) { networkManager.splitTicks = -1; } diff --git a/src/Features/Speedrun/SpeedrunTimer.hpp b/src/Features/Speedrun/SpeedrunTimer.hpp index f7443608..8839c6f3 100644 --- a/src/Features/Speedrun/SpeedrunTimer.hpp +++ b/src/Features/Speedrun/SpeedrunTimer.hpp @@ -34,6 +34,7 @@ namespace SpeedrunTimer { bool IsRunning(); void RecordIncompleteSummary(); + void WriteIdToDemo(); void OnLoad(); void CategoryChanged(); diff --git a/src/Modules/EngineDemoPlayer.cpp b/src/Modules/EngineDemoPlayer.cpp index 4f10fb38..86987ede 100644 --- a/src/Modules/EngineDemoPlayer.cpp +++ b/src/Modules/EngineDemoPlayer.cpp @@ -173,6 +173,7 @@ std::string EngineDemoPlayer::GetLevelName() { // 0x10: queued commands // 0x11: VPK internal checksums // 0x12: incomplete speedrun summary +// 0x13: speedrun identifier void EngineDemoPlayer::CustomDemoData(char *data, size_t length) { if (data[0] == 0x03 || data[0] == 0x04) { // Entity input data std::optional slot; diff --git a/src/Modules/EngineDemoRecorder.cpp b/src/Modules/EngineDemoRecorder.cpp index 9e504c7b..249f64cd 100644 --- a/src/Modules/EngineDemoRecorder.cpp +++ b/src/Modules/EngineDemoRecorder.cpp @@ -170,6 +170,7 @@ DETOUR(EngineDemoRecorder::SetSignonState, int state) { if (state == SIGNONSTATE_FULL && needToRecordInitialVals) { needToRecordInitialVals = false; RecordTimestamp(); + SpeedrunTimer::WriteIdToDemo(); // Write speedrun ID to every demo segment RecordQueuedCommands(); SpeedrunTimer::RecordIncompleteSummary(); engine->ExecuteCommand("echo \"SAR " SAR_VERSION " (Built " SAR_BUILT ")\"", true);