Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions src/Features/Speedrun/SpeedrunTimer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +40,8 @@ enum PacketType {
STOP,
SPLIT,
RESET,
ID_REQUEST, // Orange requests speedrun ID
ID_RESPONSE, // Blue responds with speedrun ID
};

// TimerAction {{{
Expand Down Expand Up @@ -118,6 +121,9 @@ static struct

std::vector<std::string> visitedMaps;
std::string lastMap;

uint8_t speedrunId[16]; // 128-bit unique identifier
bool hasSpeedrunId; // Whether ID has been set
} g_speedrun;

static std::map<std::string, int> g_activeRun;
Expand All @@ -126,6 +132,8 @@ static std::vector<std::map<std::string, int>> 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();
Expand Down Expand Up @@ -160,15 +168,24 @@ 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;

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();

Expand All @@ -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();
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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<std::chrono::milliseconds>(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!");
Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/Features/Speedrun/SpeedrunTimer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace SpeedrunTimer {
bool IsRunning();

void RecordIncompleteSummary();
void WriteIdToDemo();

void OnLoad();
void CategoryChanged();
Expand Down
1 change: 1 addition & 0 deletions src/Modules/EngineDemoPlayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> slot;
Expand Down
1 change: 1 addition & 0 deletions src/Modules/EngineDemoRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down