GDReplayFormat is a replay format meant to be usable by all bots.
Replay data is stored in a compact binary format using:
- Variable-length integer encoding (
varint
) to reduce storage size. - Delta encoding for frame data to reduce storage size.
- Input packing for optimal storage (single byte in best-case scenario)
- Optional extension support for custom replay metadata and input types.
Bot developers like you can add your own fields to GDR (like position data/frame fixes if you want) using the Extensions feature
If extensions are present, they are parsed only if they match the expected input tag.
std::string author; /* Replay author. Usually the nickname of the person who recorded it. */
std::string description; /* Replay description. Not required. */
float duration{}; /* Duration of the replay in seconds. */
int gameVersion{}; /* Game version the replay was recorded on. (Example: 22074 for 2.2074, refer to GEODE_COMP_GD_VERSION) */
double framerate = 240.0; /* Framerate (ticks per second) of the replay. 240 by default, change if replay is recorded using physics bypass. */
int seed = 0; /* Random seed set when at the start of the attempt. */
int coins = 0; /* Number of coins collected in the level. */
bool ldm = false; /* Whether the replay was recorded in low detail mode. */
bool platformer = false; /* Whether the replay was recorded in platformer mode. */
Bot botInfo{}; /* Information about the bot that recorded the replay. */
Level levelInfo{}; /* Information about the level the replay was recorded on. */
std::vector<InputType> inputs; /* Refer to inputs section. */
std::vector<uint64_t> deaths; /* List of frames where a death should happen. */
std::string name; /* Name of the bot that recorded the replay. */
int version = 1; /* Version of the bot that recorded the replay. */
uint32_t id{}; /* ID of the level the replay was recorded on. */
std::string name; /* Name of the level the replay was recorded on. */
uint64_t frame{}; /* Frame that the input was recorded on. */
uint8_t button{}; /* Pressed button. 1 = Jump, 2 = Left, 3 = Right. Can be converted directly to PlayerButton enum. */
bool player2{}; /* Whether the input was for player 2. */
bool down{}; /* Whether the button was pressed or released. */
Please stick to these point to guarantee replay compatibility with other bots:
- Use a frame counter to store frames. Multiplying time can lead to inaccuracy in frame counting.
- The first frame of a replay should be 0.
- When saving the replay to a file, use the
.gdr2
extension to better identify newer replays.
Assuming you are using CPM as it is already in all Geode mods
Add CPM dependency (make sure it is added AFTER the add_subdirectory
line and BEFORE the setup_geode_mod
line)
CPMAddPackage("gh:maxnut/GDReplayFormat#commithash") # REPLACE commithash with a specific commit hash
target_link_libraries(${PROJECT_NAME} libGDR)
Add this to your geode mod and add your bot's name and version to the Replay
#include <gdr/gdr.hpp>
struct MyBot : gdr::Replay<MyBot, gdr::Input<>> {
MyBot() : Replay("MyBot", 1) {}
};
The GDR Extensions system allows any bot to add it's own fields to a whole replay or the individual inputs in a replay for it's own use while still keeping GDR standardized and usable for every other bot that may use it. The most common use of this system is to add position and velocity data to inputs to allow frame corrections.
If you plan on using extensions to add physics fixes to your replays, please use the already provided PhysicsInput!
Put simply, if you want to add X position to an input in my replays, you need to add it to your Input struct and add a way to save/parse it, like this:
struct MyInput : Input<"MyInput"> {
float xpos;
MyInput() = default;
MyInput(uint64_t frame, uint8_t button, bool player2, bool down, float xpos)
: Input(frame, button, player2, down), xpos(xpos) {}
void parseExtension(binary_reader& reader) override {
reader >> xpos;
}
void saveExtension(binary_writer& writer) const override {
writer << xpos;
}
};
Now, if you wanted to add a field (for this example, we will use Attempts) to your whole replay, the process would be very similar:
struct MyReplay : Replay<MyReplay, MyInput> {
int attempts;
MyReplay() : Replay("TestBot", 1) {}
void parseExtension(binary_reader& reader) override {
reader >> attempts;
}
void saveExtension(binary_writer& writer) const override {
writer << attempts;
}
bool shouldParseExtension() const override {
return botInfo.name == "TestBot" && botInfo.version == 1;
}
};
These extensions can be accessed the same way you access regular fields
- The file format has completely changed. If you want to keep GDR 1 support we suggest using the converter.
- To check if the GDR file is newer than 1.0, you can check if the first three bytes of the data equal to "GDR".
- The
importData
andexportData
methods now return a result type, for better error handling.
Each GDR file consists of the following sections:
- Header
- Replay Metadata
- Death Frames
- Input Data
Field | Type | Description |
---|---|---|
Magic | 3 Bytes (ASCII) | File identifier ("GDR") |
Version | varint |
Replay format version |
Input Tag | string |
Identifier for custom input types |
Field | Type | Description |
---|---|---|
Author | string |
Author of the replay |
Description | string |
Description of the replay |
Duration | varint |
Length of the replay in frames |
Game Version | varint |
Version of the game |
Framerate | varint |
FPS of the recording |
Seed | varint |
Random seed used in gameplay |
Coins | varint |
Number of coins collected |
LDM Enabled | bool |
Whether "Low Detail Mode" was enabled |
Platformer | bool |
Whether the replay is platformer mode |
Bot Name | string |
Name of the bot used to record the replay |
Bot Version | varint |
Version of the bot |
Level ID | varint |
Level identifier |
Level Name | string |
Name of the level |
Extension Size | varint |
Size of the extension block |
Extension Data | bytes |
Custom extension data (if applicable) |
Field | Type | Description |
---|---|---|
Death Count | varint |
Number of deaths recorded |
Death Frames | varint[] |
Frame numbers where deaths occurred (delta-encoded) |
Field | Type | Description |
---|---|---|
Input Count | varint |
Number of input records |
P1 Input Count | varint |
Number of player1 input records |
Inputs | varint + extension (optional) |
Input events |
Each input entry consists of:
Field | Type | Description |
---|---|---|
Packed | varint |
Input frame delta and button press bitmask packed in a single value |
Extension Size (Optional) | varint |
Size of custom extension data |
Extension Data (Optional) | bytes |
Custom input extension data |
If the replay is not platformer mode, the delta can go up to 63 frames and the input will fit in a single byte. Otherwise, the delta can go up to 15 frames.
The bitmask is a single uint8_t
value encoding the input state:
Bit Position | Meaning |
---|---|
0 | Button press state (1 = down, 0 = up) |
1-2 (Optional) | Button ID (00 = None, 01 = Jump, 10 = Left, 11 = Right) |