Versioning & Migrations
The versioning module (src/Engine/Versioning/) answers one question at startup: the player’s save was written by version X, the game is now version Y — what should happen? It ships semantic-version comparison, a game manifest with changelog, and a save-migration registry.
The manifest
Your game declares its version in a manifest.json (path set by EngineConfig::manifestPath, default manifest.json):
{
"version": "1.1.0",
"description": "Factory automation game",
"changelog": [
{ "version": "1.1.0", "changes": ["Added conveyor tiers", "Balance pass"] },
{ "version": "1.0.0", "changes": ["Initial release"] }
]
}
A missing or malformed manifest falls back to 1.0.0 — versioning is opt-in and never blocks startup.
What happens at startup
pg::Engine runs VersionManager::initialize during ECS setup. It reads the version stamped in the save file (key __pg_game_version), compares against the manifest, and acts according to two EngineConfig flags (both default true):
Situation |
Action |
|---|---|
New install |
stamp current version, nothing else |
Same version |
nothing |
Major bump |
|
Minor / patch bump |
|
Downgrade |
nothing (restamp only) |
The check result also carries relevantChangelog — the entries between the saved and current version — ready to show the player a “what’s new” screen.
Migrations
Register a callback per target version; on update, all callbacks with oldVersion < target <= currentVersion run in ascending version order:
manifest.registerMigration(SemanticVersion{1, 1, 0}, [](SaveManager& save)
{
// rename a key introduced in 1.1.0
auto old = save.getValue("score");
save.onProcessEvent(SaveElementEvent("highscore", old));
});
This is the intended path for evolving key-value save data across releases. (Per-system SaveSys blobs get field-level tolerance for free from defaultDeserialize — see Serialization — so most save-format drift needs no migration at all.)
SemanticVersion
SemanticVersion (header-only) is the comparison workhorse: parses "M.m.p" strings (anything unparseable resets to 1.0.0), full ordering operators, and the bump predicates isMajorBumpFrom / isMinorBumpFrom / isPatchBumpFrom. It’s fully covered by test/semanticversion.cc — extend those tests if you touch it.
Practical guidance
Bump patch/minor for save-compatible releases; migrations cover renames and derived data.
Reserve major for genuinely incompatible saves — with the default flags, a major bump deletes player saves. If that’s not what you want, set
autoWipeSaveOnMajorBump = falsein yourEngineConfigand handle it yourself from theVersionCheckResult.Keep the changelog array current; the engine sorts and slices it for you.