DemoReplay

The .gdemo format of the main-menu demos and the player's recordings: a resume log under its own header, with a simulation revision and its world's hash.

Location

Headersrc/GalaxyEggbert/Game/DemoReplay.hpp
Sourcessrc/GalaxyEggbert/Game/DemoReplay.cpp; playing and recording in src/GalaxyEggbertCNA/GalaxyEggbertGameDemo.cpp
NamespaceGalaxyEggbert::Game (free functions, DemoRecording, DemoLiveInput, DemoIdleTimer)
Engine dependencyNone. The host feeds it live input and plays the recording through the normal simulation.

Purpose

Speedy Blupi played recorded games in its main menu after a while without input. Galaxy Eggbert does the same (BD-0115): the menu's Demo button, or 40 seconds without input in the settled main menu, plays ten demo positions in turn, the first again after the last. A demo is drawn and heard like play ("Demo" in the top-left corner) and any new key, button, wheel step or touch stops it; moving the mouse does not. Players can record their own demo into any of the ten positions from the Pause screen (BD-0116), name it, and export or import it as a file.

A demo holds the same data as a resume log: the start state, the seed and the raw input of every 1/60 s tick. The difference is compatibility. A resume file is accepted only by the identical build; a demo must survive new builds, so instead it names the simulation revision it was recorded with and the hash of its world, plus a signature of the end state so a changed simulation can be detected. A demo whose format, revision or world differs is skipped, never replayed into a different game.

Collaborators

Uses

Used by

Key members

A selection of the public API declared in src/GalaxyEggbert/Game/DemoReplay.hpp.

MemberSignatureWhat it does
ConstantskDemoFormatVersion = 1, kDemoSimulationRevision = 1, kDemoIdleSeconds = 40.0f, kDemoSlotCount = 10, kDemoMaxTicks (10 minutes of ticks), kDemoNameMax = 24Format and revision numbers, idle delay, positions, length and name limits.
DemoRecordingstruct DemoRecording { int simulationRevision; std::string title; ResumeLog log; std::vector<std::pair<std::string, std::uint64_t>> endSignature; std::string Serialize() const; }One demo in memory.
ParseDemoDemoLoadStatus ParseDemo(const std::string& text, int expectedRevision, DemoRecording& out)Validates header, checksum, every field, normal game speed and length. Transactional.
DemoWorldStatusDemoLoadStatus DemoWorldStatus(const DemoRecording& demo, const std::string& path, const std::optional<std::string>& worldBytes)Ok, WorldMissing or WorldChanged for the file the host would load.
DemoLoadStatusenum class DemoLoadStatus { Ok, Missing, Corrupt, IncompatibleFormat, IncompatibleRevision, WorldMissing, WorldChanged }Why a demo can or cannot play; DemoLoadStatusText() gives the log text.
DemoPath, UserDemoPath, UserDemoDir, SharedDemoDirstd::string DemoPath(int number) (and similar)Content/demos/demoNNN.gdemo; demos/demoNNN.gdemo; demos; demos/shared (the last three in the player's folder).
ExportDemo, ImportDemostd::string ExportDemo(const std::string& source, int gamer, int slot)
DemoLoadStatus ImportDemo(const std::string& source, int slot)
Copy a position's demo into demos/shared/ (never replacing a file), or a valid shared demo into a position.
DemoExportName, ListSharedDemosstd::string DemoExportName(int gamer, int slot, const std::string& title = {})A-demo-5.gdemo, or <name>.gdemo for a named demo; the shared list is newest first.
SanitizeDemoName, RenameDemo, DemoTitlebool RenameDemo(const std::string& path, const std::string& name)Names: letters, digits, spaces, - and ', at most 24 characters.
HashDemoSignature, DemoSignatureDifferencesstd::vector<std::string> DemoSignatureDifferences(expected, actual)Hash the end-state signature; name the parts that differ.
DemoOrderAfterstd::vector<int> DemoOrderAfter(int current, int count)The next demos to try, wrapping after the last.
DemoActivity, DemoNewPress, DemoInputHeldbool DemoNewPress(const DemoLiveInput& previous, const DemoLiveInput& now) noexceptIdle-timer reset, the stop rule (a new press only, not mouse movement, auto-repeat or a release), and the input latch after a demo.
DemoIdleTimerbool Update(float elapsedSeconds, bool eligible, bool activity) noexceptCounts idle time only while eligible; true once when 40 s are reached.

The .gdemo file

Text, written by DemoRecording::Serialize(): a header, the end signature, then the resume-log body and its END <FNV-1a> footer. The start of the shipped Content/demos/demo001.gdemo:

GALAXY_DEMO 1
1
"World 1: pillars, mine pits and lifts"
"Nov  8 2026 19:35:31 fnv1a64=17098780557396544612"
9
"Blupi" 5723123326820972394
"Hide trail" 14695981039346656037
"blocks" 16646699624160265678
...
"worlds3d/world011.vwr" 17476187514555174857
11 3 1 3279155381 14 10 10 -0 0 0 0 0 0 0 1
"soundEnabled=1
...

Line by line: magic and format version; simulation revision; title; the recording build (for information only, never compared); the number of signature entries and each entry's name and hash; then the world path and the FNV-1a hash of its bytes, the start record, the settings and the input runs exactly as in a resume log. A demo must be at normal game speed, non-empty and at most ten minutes long. The revision is checked last, so a damaged file is always reported as damaged.

Where demos live

FileWhat
Content/demos/demo001.gdemo ... demo010.gdemoThe shipped demos, one per position (missions 011, 074, 105, 023, 033, 055, 065, 081, 112, 123, as of 2026-11-10).
demos/demoNNN.gdemo (player's folder; /save/demos/ on the web)The player's recording for position NNN; when present it plays instead of the shipped one ("Original" gives the shipped one back).
demos/shared/*.gdemoExports and files to import (on the web, an export is also downloaded and the file picker writes imports here).

How it is used

GalaxyEggbertGame::StartDemoFile() checks everything before touching the player's state: the file must parse, its world must be found (the campaign's file, or a galaxy world with the recorded hash in any player's galaxy) and load. Only then does it set aside the player's profile, settings and cheats, apply the recording's profile with the player's own sound, volumes and display settings, and play. Each update feeds one recorded tick into the normal UpdateTick() (src/GalaxyEggbertCNA/GalaxyEggbertGameDemo.cpp):

const auto& run = demo_.log.runs[demoRun_];
const std::uint32_t count = run.count;
tickInput_ = run.input;
try
{
    UpdateTick();
}
catch (const std::exception& error)
{
    // A failure inside the replayed game takes the player back to the menu, never further.
    ...
    StopDemo();
    return false;
}

At the end EndOfDemo() compares the end signature (a difference is only logged in the menu) and starts the next position; StopDemo() restores the player's state. A player recording (StartPlayerRecording()) restarts the current mission from its start at normal speed, titled "Recorded by player A: mission 11" until renamed, and ends when Play or the mission is left or after ten minutes. Developers record and check demos from the command line: --record-demo <file> with --mission N (optionally --autoplay, --demo-ticks N, --demo-title), and --verify-demo <file>, which replays undrawn and prints DEMO VERIFY <file>: equal or different: .... The wrappers are tools/record_demo.sh and tools/verify_demos.sh.

Rules and invariants

Tests

Related pages