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
| Header | src/GalaxyEggbert/Game/DemoReplay.hpp |
|---|---|
| Sources | src/GalaxyEggbert/Game/DemoReplay.cpp; playing and recording in src/GalaxyEggbertCNA/GalaxyEggbertGameDemo.cpp |
| Namespace | GalaxyEggbert::Game (free functions, DemoRecording, DemoLiveInput, DemoIdleTimer) |
| Engine dependency | None. 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
- ResumeLog: body (
WriteBody/ReadBody), checksum footer and FNV-1a hashing. UserFolderfordemos/, andFreeFileName.hppfor numbered export names.
Used by
- GalaxyEggbertGame:
StartDemo,StartDemoFile,ResolveDemoWorld,DemoTick,UpdateDemo,EndOfDemo,StopDemo,UpdateDemoMenu,StartPlayerRecording,FinishDemoRecording, and the demo positions screen. - InputPad draws the positions screen (
InputPad::DemoSlotInfo). - CustomWorldStorage: a galaxy pack carries the player's recordings made in that galaxy.
Key members
A selection of the public API declared in src/GalaxyEggbert/Game/DemoReplay.hpp.
| Member | Signature | What it does |
|---|---|---|
| Constants | kDemoFormatVersion = 1, kDemoSimulationRevision = 1, kDemoIdleSeconds = 40.0f, kDemoSlotCount = 10, kDemoMaxTicks (10 minutes of ticks), kDemoNameMax = 24 | Format and revision numbers, idle delay, positions, length and name limits. |
DemoRecording | struct 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. |
ParseDemo | DemoLoadStatus ParseDemo(const std::string& text, int expectedRevision, DemoRecording& out) | Validates header, checksum, every field, normal game speed and length. Transactional. |
DemoWorldStatus | DemoLoadStatus 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. |
DemoLoadStatus | enum class DemoLoadStatus { Ok, Missing, Corrupt, IncompatibleFormat, IncompatibleRevision, WorldMissing, WorldChanged } | Why a demo can or cannot play; DemoLoadStatusText() gives the log text. |
DemoPath, UserDemoPath, UserDemoDir, SharedDemoDir | std::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, ImportDemo | std::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, ListSharedDemos | std::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, DemoTitle | bool RenameDemo(const std::string& path, const std::string& name) | Names: letters, digits, spaces, - and ', at most 24 characters. |
HashDemoSignature, DemoSignatureDifferences | std::vector<std::string> DemoSignatureDifferences(expected, actual) | Hash the end-state signature; name the parts that differ. |
DemoOrderAfter | std::vector<int> DemoOrderAfter(int current, int count) | The next demos to try, wrapping after the last. |
DemoActivity, DemoNewPress, DemoInputHeld | bool DemoNewPress(const DemoLiveInput& previous, const DemoLiveInput& now) noexcept | Idle-timer reset, the stop rule (a new press only, not mouse movement, auto-repeat or a release), and the input latch after a demo. |
DemoIdleTimer | bool Update(float elapsedSeconds, bool eligible, bool activity) noexcept | Counts 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
| File | What |
|---|---|
Content/demos/demo001.gdemo ... demo010.gdemo | The 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/*.gdemo | Exports 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
- While a demo plays the player's data is frozen:
savedata.txtandresume.txtare neither written nor deleted, and focus loss stops the demo. - The input that stops a demo never reaches the menu: input is ignored until nothing is held.
- Demos run only from the settled main menu; never in Play, Pause, Setup, the editor or during a menu fade.
- When a simulation change makes
tools/verify_demos.shreport a shipped demo as different, bumpkDemoSimulationRevisionand record the demos again; the old files are then skipped as incompatible. - F-keys and Tab are left out of a recording; exports never overwrite (
A-demo-5-2.gdemo...).
Tests
VerifyDemoReplay(tools/VerifyDemoReplay.cpp): header and round trip, refusal of another revision or format, of truncated, damaged, trailing, empty, fast, over-long and invalid demos, demo versus resume file, world status, paths, export/import and naming, signature differences, the play order, the 40-second idle timer and the stop rules.VerifyDemoFiles(VerifyDemoReplay --shipped, from the repository root): ten shipped demos, numbered without gaps, each parsing with the current revision, matching its world, with a title and an end signature.VerifyDemoReplays(tools/verify_demos.sh): the game itself replays every shipped demo undrawn and silent under Xvfb, compares the end state, and checks that the player'ssavedata.txtandresume.txtare untouched. Registered only on native non-Windows builds wherexvfb-runis found.