Skip to main content

Replay System

This document explains how the R-Type client records and plays back game sessions.


๐ŸŽฏ Overviewโ€‹

The replay system provides frame-perfect recording and playback of game sessions using a binary event format.

Key Features:

  • โœ… Automatic recording during gameplay
  • โœ… Frame-perfect playback
  • โœ… Playback controls (pause, seek, speed)
  • โœ… Binary .rtr file format
  • โœ… Minimal performance overhead

๐Ÿ“ฆ Architectureโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Replay System โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ ReplayRecord โ”‚ โ”‚ ReplayViewer โ”‚ โ”‚
โ”‚ โ”‚ er โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ–ผ โ–ผ โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ .rtr File Format โ”‚ โ”‚
โ”‚ โ”‚ [Header][Event 1][Event 2]...[EOF] โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ ReplayBrowse โ”‚ โ”‚ ReplayControlโ”‚ โ”‚
โ”‚ โ”‚ r โ”‚ โ”‚ s โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐ŸŽฌ Recording Systemโ€‹

ReplayRecorder Classโ€‹

class ReplayRecorder {
public:
ReplayRecorder(const std::string& filename);
~ReplayRecorder();

void startRecording();
void stopRecording();

void recordEntitySpawn(const EntitySpawnPacket& packet);
void recordEntityPosition(const EntityPositionPacket& packet);
void recordEntityDead(const EntityDeadPacket& packet);
void recordScoreUpdate(uint32_t score);
void recordGameEnd(bool victory);

private:
std::ofstream _file;
std::chrono::steady_clock::time_point _startTime;
bool _isRecording;

void writeHeader();
void writeEvent(uint8_t eventType, const void* data, size_t size);
float getElapsedTime() const;
};

Usage During Gameplayโ€‹

// Game initialization
auto replayRecorder = std::make_unique<ReplayRecorder>(
"replays/game_" + getCurrentTimestamp() + ".rtr"
);
replayRecorder->startRecording();

// During gameplay - record network events
networkClient->setOnEntitySpawnCallback([&replayRecorder](const EntitySpawnPacket& packet) {
replayRecorder->recordEntitySpawn(packet);
// ... handle spawn
});

networkClient->setOnEntityPositionCallback([&replayRecorder](const EntityPositionPacket& packet) {
replayRecorder->recordEntityPosition(packet);
// ... handle position update
});

// Game end
replayRecorder->recordGameEnd(isVictory);
replayRecorder->stopRecording();

Automatic File Namingโ€‹

std::string getCurrentTimestamp() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm = *std::localtime(&time_t);

std::ostringstream oss;
oss << std::put_time(&tm, "%Y%m%d_%H%M%S");
return oss.str();
}

// Example: "game_20260118_143022.rtr"

๐Ÿ“„ File Format (.rtr)โ€‹

File Structureโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ HEADER (32 bytes) โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Magic: "RTYPE" (5 bytes) โ”‚
โ”‚ Version: 1 (uint8_t) โ”‚
โ”‚ Timestamp: Unix timestamp (uint64_t) โ”‚
โ”‚ Duration: Total time (float) โ”‚
โ”‚ PlayerCount: Number of players (uint8_t) โ”‚
โ”‚ Reserved: Padding (13 bytes) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ EVENT 1 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Timestamp: Elapsed time (float) โ”‚
โ”‚ EventType: Type ID (uint8_t) โ”‚
โ”‚ DataSize: Payload size (uint16_t) โ”‚
โ”‚ Data: Event data (variable) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ EVENT 2 โ”‚
โ”‚ ... โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ EVENT N โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Event Typesโ€‹

enum class ReplayEventType : uint8_t {
EntitySpawn = 0x01,
EntityPosition = 0x02,
EntityDead = 0x03,
ScoreUpdate = 0x04,
GameEnd = 0x05
};

Event Data Structuresโ€‹

EntitySpawn Event:

struct ReplayEntitySpawnEvent {
uint32_t entityId;
uint8_t entityType;
float x;
float y;
};

EntityPosition Event:

struct ReplayEntityPositionEvent {
uint32_t entityId;
float x;
float y;
};

EntityDead Event:

struct ReplayEntityDeadEvent {
uint32_t entityId;
};

ScoreUpdate Event:

struct ReplayScoreUpdateEvent {
uint32_t score;
};

GameEnd Event:

struct ReplayGameEndEvent {
uint8_t victory; // 1 = victory, 0 = defeat
};

โ–ถ๏ธ Playback Systemโ€‹

ReplayViewer Classโ€‹

class ReplayViewer {
public:
ReplayViewer(const std::string& filepath);
~ReplayViewer();

bool load();
void update(float deltaTime);
void render(GraphicsSFML& graphics);

void play();
void pause();
void togglePause();
void seek(float deltaTime);
void setSpeed(float speed);
void cycleSpeed();

bool isPaused() const;
bool isFinished() const;
float getCurrentTime() const;
float getTotalDuration() const;
float getSpeed() const;

private:
std::string _filepath;
std::ifstream _file;

std::vector<ReplayEvent> _events;
size_t _currentEventIndex;
float _currentTime;
float _totalDuration;
float _speed;
bool _isPaused;
bool _isFinished;

std::map<uint32_t, Entity> _entities;
uint32_t _currentScore;

bool readHeader();
bool readAllEvents();
void processEventsUntil(float time);
void processEvent(const ReplayEvent& event);
};

Playback Flowโ€‹

// Load replay
auto replayViewer = std::make_unique<ReplayViewer>("replays/game_20260118_143022.rtr");
if (!replayViewer->load()) {
std::cerr << "Failed to load replay" << std::endl;
return;
}

// Main playback loop
while (!replayViewer->isFinished()) {
float deltaTime = clock.restart();

// Update replay state
if (!replayViewer->isPaused()) {
replayViewer->update(deltaTime);
}

// Render current frame
window->clear();
replayViewer->render(graphics);
window->display();

// Handle input
if (input->isKeyPressed(Key::Space)) {
replayViewer->togglePause();
}
if (input->isKeyPressed(Key::Escape)) {
break;
}
}

๐ŸŽฎ Replay Controlsโ€‹

ReplayControls Classโ€‹

class ReplayControls {
public:
ReplayControls(float x, float y, float width, float height);

void update(float deltaTime, bool isPaused, float currentTime,
float totalTime, float speed);
void render(GraphicsSFML& graphics);

bool isRewindClicked(int mouseX, int mouseY, bool isMousePressed);
bool isForwardClicked(int mouseX, int mouseY, bool isMousePressed);
bool isPauseClicked(int mouseX, int mouseY, bool isMousePressed);
bool isSpeedClicked(int mouseX, int mouseY, bool isMousePressed);
bool isExitClicked(int mouseX, int mouseY, bool isMousePressed);

private:
struct Button {
float x, y, width, height;
std::string label;
bool isHovered;
};

Button _pauseButton;
Button _rewindButton;
Button _forwardButton;
Button _speedButton;
Button _exitButton;

float _progressBarX, _progressBarY;
float _progressBarWidth, _progressBarHeight;

void updateButton(Button& button, int mouseX, int mouseY);
void renderButton(GraphicsSFML& graphics, const Button& button);
void renderProgressBar(GraphicsSFML& graphics, float progress);
std::string formatTime(float seconds);
};

Control Featuresโ€‹

Available Controls:

  • Pause/Play: Toggle playback (Spacebar)
  • Rewind: Jump back 10 seconds (Left Arrow)
  • Forward: Jump forward 10 seconds (Right Arrow)
  • Speed: Cycle 0.5x โ†’ 1x โ†’ 2x (S key)
  • Exit: Return to browser (ESC)
  • Progress Bar: Click to seek to specific time

UI Layout:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ [โธ] [โช] [โฉ] [1.0x] [โŒ] โ”‚
โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚
โ”‚ 01:23 / 03:45 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ“‚ Replay Browserโ€‹

ReplayBrowser Classโ€‹

class ReplayBrowser {
public:
ReplayBrowser(WindowSFML& window, GraphicsSFML& graphics, InputSFML& input);

ReplayBrowserAction update(float deltaTime);
void render();
void updateLayout();
void reset();

std::string getSelectedReplayPath() const;

private:
struct ReplayInfo {
std::string filename;
std::string filepath;
std::string timestamp;
float duration;
bool isSelected;
};

std::vector<ReplayInfo> _replays;
int _selectedIndex;
int _scrollOffset;

std::vector<Button> _buttons;

void loadReplayList();
void scanReplayDirectory();
bool parseReplayMetadata(const std::string& filepath, ReplayInfo& info);
};

enum class ReplayBrowserAction {
None,
WatchReplay,
Back
};

Replay List Displayโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ REPLAYS โ”‚
โ”‚ โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ game_20260118_143022.rtr 03:45 โ—€ โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚ โ”‚ game_20260118_140515.rtr 02:12 โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚ โ”‚ game_20260117_210338.rtr 04:03 โ”‚ โ”‚
โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚
โ”‚ โ”‚ game_20260117_195422.rtr 01:58 โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ”‚
โ”‚ [ WATCH ] [ BACK ] โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Replay Scanningโ€‹

void ReplayBrowser::scanReplayDirectory() {
_replays.clear();

if (!std::filesystem::exists("replays")) {
std::filesystem::create_directory("replays");
return;
}

for (const auto& entry : std::filesystem::directory_iterator("replays")) {
if (entry.path().extension() == ".rtr") {
ReplayInfo info;
info.filename = entry.path().filename().string();
info.filepath = entry.path().string();

if (parseReplayMetadata(info.filepath, info)) {
_replays.push_back(info);
}
}
}

// Sort by timestamp (newest first)
std::sort(_replays.begin(), _replays.end(),
[](const ReplayInfo& a, const ReplayInfo& b) {
return a.timestamp > b.timestamp;
}
);
}

โš™๏ธ Implementation Detailsโ€‹

Recording Implementationโ€‹

void ReplayRecorder::startRecording() {
_file.open(_filename, std::ios::binary);
if (!_file.is_open()) {
std::cerr << "Failed to create replay file: " << _filename << std::endl;
return;
}

writeHeader();
_startTime = std::chrono::steady_clock::now();
_isRecording = true;
}

void ReplayRecorder::writeHeader() {
// Magic number
_file.write("RTYPE", 5);

// Version
uint8_t version = 1;
_file.write(reinterpret_cast<const char*>(&version), sizeof(version));

// Timestamp
auto now = std::chrono::system_clock::now();
uint64_t timestamp = std::chrono::system_clock::to_time_t(now);
_file.write(reinterpret_cast<const char*>(&timestamp), sizeof(timestamp));

// Duration (placeholder, updated on stop)
float duration = 0.0f;
_file.write(reinterpret_cast<const char*>(&duration), sizeof(duration));

// Player count (placeholder)
uint8_t playerCount = 1;
_file.write(reinterpret_cast<const char*>(&playerCount), sizeof(playerCount));

// Reserved padding
char padding[13] = {0};
_file.write(padding, 13);
}

void ReplayRecorder::writeEvent(uint8_t eventType, const void* data, size_t size) {
if (!_isRecording) return;

// Timestamp
float elapsed = getElapsedTime();
_file.write(reinterpret_cast<const char*>(&elapsed), sizeof(elapsed));

// Event type
_file.write(reinterpret_cast<const char*>(&eventType), sizeof(eventType));

// Data size
uint16_t dataSize = static_cast<uint16_t>(size);
_file.write(reinterpret_cast<const char*>(&dataSize), sizeof(dataSize));

// Event data
_file.write(static_cast<const char*>(data), size);

_file.flush();
}

void ReplayRecorder::recordEntitySpawn(const EntitySpawnPacket& packet) {
ReplayEntitySpawnEvent event;
event.entityId = packet.entityId;
event.entityType = static_cast<uint8_t>(packet.type);
event.x = packet.x;
event.y = packet.y;

writeEvent(static_cast<uint8_t>(ReplayEventType::EntitySpawn),
&event, sizeof(event));
}

Playback Implementationโ€‹

bool ReplayViewer::load() {
_file.open(_filepath, std::ios::binary);
if (!_file.is_open()) {
return false;
}

if (!readHeader()) {
return false;
}

if (!readAllEvents()) {
return false;
}

_currentEventIndex = 0;
_currentTime = 0.0f;
_isPaused = false;
_isFinished = false;

return true;
}

bool ReplayViewer::readAllEvents() {
while (_file.good() && !_file.eof()) {
ReplayEvent event;

// Read timestamp
_file.read(reinterpret_cast<char*>(&event.timestamp), sizeof(event.timestamp));
if (_file.eof()) break;

// Read event type
_file.read(reinterpret_cast<char*>(&event.type), sizeof(event.type));

// Read data size
uint16_t dataSize;
_file.read(reinterpret_cast<char*>(&dataSize), sizeof(dataSize));

// Read event data
event.data.resize(dataSize);
_file.read(event.data.data(), dataSize);

_events.push_back(std::move(event));
}

return !_events.empty();
}

void ReplayViewer::update(float deltaTime) {
if (_isPaused || _isFinished) return;

_currentTime += deltaTime * _speed;

processEventsUntil(_currentTime);

if (_currentEventIndex >= _events.size()) {
_isFinished = true;
}
}

void ReplayViewer::processEventsUntil(float time) {
while (_currentEventIndex < _events.size() &&
_events[_currentEventIndex].timestamp <= time) {

processEvent(_events[_currentEventIndex]);
_currentEventIndex++;
}
}

๐Ÿš€ Performance Considerationsโ€‹

Recording Performanceโ€‹

Overhead:

  • File I/O: ~1-2 ms per event
  • Event serialization: ~0.1 ms per event
  • Total overhead: < 1% of frame time

Optimization:

  • Events written immediately (no buffering)
  • Binary format (no string serialization)
  • Minimal data per event (8-24 bytes)

Playback Performanceโ€‹

Loading:

  • Read entire file at once: O(n)
  • Parse all events: O(n)
  • Total load time: ~10-50 ms for 5-minute replay

Playback:

  • Sequential event processing: O(k) where k = events per frame
  • Entity lookup: O(1) hash map
  • Rendering: Same as normal gameplay

Memory Usage:

  • ~1 KB per second of gameplay
  • 5-minute replay: ~300 KB
  • All events loaded in memory for fast seeking

๐Ÿ”ง Advanced Featuresโ€‹

Fast Forward/Rewindโ€‹

void ReplayViewer::seek(float deltaTime) {
float targetTime = _currentTime + deltaTime;
targetTime = std::clamp(targetTime, 0.0f, _totalDuration);

if (targetTime < _currentTime) {
// Rewind: Reset and replay from start
reset();
processEventsUntil(targetTime);
} else {
// Fast forward: Process events until target time
processEventsUntil(targetTime);
}

_currentTime = targetTime;
}

void ReplayViewer::reset() {
_currentEventIndex = 0;
_currentTime = 0.0f;
_entities.clear();
_currentScore = 0;
}

Speed Controlโ€‹

void ReplayViewer::cycleSpeed() {
if (_speed == 0.5f) {
_speed = 1.0f;
} else if (_speed == 1.0f) {
_speed = 2.0f;
} else {
_speed = 0.5f;
}
}

Progress Bar Seekingโ€‹

bool ReplayControls::isProgressBarClicked(int mouseX, int mouseY, bool isPressed) {
if (!isPressed) return false;

if (mouseX >= _progressBarX && mouseX <= _progressBarX + _progressBarWidth &&
mouseY >= _progressBarY && mouseY <= _progressBarY + _progressBarHeight) {

// Calculate target time from click position
float progress = (mouseX - _progressBarX) / _progressBarWidth;
float targetTime = progress * totalDuration;

replayViewer->seek(targetTime - replayViewer->getCurrentTime());
return true;
}

return false;
}

๐Ÿ“š Further Readingโ€‹