Add an engine "compatible" version

When a patch version is released, it must declare compatibility with the
previous patch versions of the same main release. This allows players to
keep replaying their games and to keep playing online with users of
other patches of the same main release.

This should have anticipated for dae7a8c394
This commit is contained in:
Itms 2025-10-20 13:54:34 +02:00
parent 50f6da2a13
commit 866d6f0527
No known key found for this signature in database
GPG key ID: C7E52BD14CE14E09
17 changed files with 81 additions and 34 deletions

View file

@ -24,7 +24,8 @@ class PersistentMatchSettings
Engine.FileExists(this.filename) &&
Engine.ReadJSONFile(this.filename);
const persistedSettings = data?.engine_info?.engine_version == this.engineInfo.engine_version &&
const persistedSettings = data?.engine_info?.engine_serialization_version &&
data.engine_info.engine_serialization_version == this.engineInfo.engine_serialization_version &&
hasSameMods(data?.engine_info?.mods, this.engineInfo.mods) &&
data.attributes || {};

View file

@ -174,8 +174,8 @@ class SavegameList
isCompatibleSavegame(metadata, engineInfo)
{
return engineInfo &&
metadata.engine_version &&
metadata.engine_version == engineInfo.engine_version &&
metadata.engine_serialization_version &&
metadata.engine_serialization_version == engineInfo.engine_serialization_version &&
hasSameMods(metadata.mods, engineInfo.mods);
}

View file

@ -24,9 +24,9 @@ class SavegameLoader
// Check compatibility before really loading it
const engineInfo = Engine.GetEngineInfo();
const sameMods = hasSameMods(metadata.mods, engineInfo.mods);
const sameEngineVersion = metadata.engine_version && metadata.engine_version == engineInfo.engine_version;
const compatibleEngineVersions = metadata.engine_serialization_version && metadata.engine_serialization_version == engineInfo.engine_serialization_version;
if (sameEngineVersion && sameMods)
if (compatibleEngineVersions && sameMods)
{
this.closePageCallback(gameId);
return;
@ -35,14 +35,17 @@ class SavegameLoader
// Version not compatible ... ask for confirmation
let message = "";
if (!sameEngineVersion)
if (metadata.engine_version)
message += sprintf(translate("This savegame needs 0 A.D. version %(requiredVersion)s, while you are running version %(currentVersion)s."), {
"requiredVersion": metadata.engine_version,
"currentVersion": engineInfo.engine_version
if (!compatibleEngineVersions)
{
if (metadata.engine_serialization_version)
message += sprintf(translate("This savegame needs 0 A.D. version %(requiredCompatibleVersion)s or compatible. You are running version %(currentVersion)s, compatible down to %(compatibleVersion)s."), {
"requiredCompatibleVersion": metadata.engine_serialization_version,
"currentVersion": engineInfo.engine_version,
"compatibleVersion": engineInfo.engine_serialization_version,
}) + "\n";
else
message += translate("This savegame needs an older version of 0 A.D.") + "\n";
}
if (!sameMods)
{

View file

@ -84,7 +84,7 @@ function reallyStartVisualReplay(replayDirectory)
function displayReplayCompatibilityError(replay)
{
var errMsg;
if (replayHasSameEngineVersion(replay))
if (replayHasCompatibleEngineVersion(replay))
{
const gameMods = replay.attribs.mods || [];
errMsg = translate("This replay needs a different sequence of mods:") + "\n" +
@ -93,8 +93,9 @@ function displayReplayCompatibilityError(replay)
else
{
errMsg = translate("This replay is not compatible with your version of the game!") + "\n";
errMsg += sprintf(translate("Your version: %(version)s"), { "version": g_EngineInfo.engine_version }) + "\n";
errMsg += sprintf(translate("Required version: %(version)s"), { "version": replay.attribs.engine_version });
errMsg += sprintf(translate("Your version: %(version)s, compatible down to %(compatibleVersion)s"), { "version": g_EngineInfo.engine_version, "compatibleVersion": g_EngineInfo.engine_serialization_version }) + "\n";
if (replay.attribs.engine_serialization_version)
errMsg += sprintf(translate("Replay version: %(version)s"), { "version": replay.attribs.engine_serialization_version });
}
messageBox(500, 200, errMsg, translate("Incompatible replay"));

View file

@ -360,13 +360,13 @@ function getReplayDuration(replay)
*/
function isReplayCompatible(replay)
{
return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods);
return replayHasCompatibleEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods);
}
/**
* True if we can start the given replay with the currently loaded mods.
*/
function replayHasSameEngineVersion(replay)
function replayHasCompatibleEngineVersion(replay)
{
return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version;
return replay.attribs.engine_serialization_version && replay.attribs.engine_serialization_version == g_EngineInfo.engine_serialization_version;
}

View file

@ -23,8 +23,46 @@
#ifndef INCLUDED_BUILDVERSION
#define INCLUDED_BUILDVERSION
#define PYROGENESIS_VERSION "0.28.0"
#define PYROGENESIS_VERSION_WORD 0,28,0,0
/*
* The version of the game is built as MAJOR.MINOR.PATCH, where
* - MAJOR is 0
* - MINOR is incremented for each main release
* - PATCH is incremented whenever we release a bugfix patch release
*
* TODO: This does not respect semver.
*/
#define PS_VERSION_MAJOR 0
#define PS_VERSION_MINOR 28
#define PS_VERSION_PATCH 0
/*
* When a patch version is released, it should stay simulation-compatible with
* all the previous patch releases of the same main release. This allows players
* to keep replaying their games and playing online against other versions of the
* same main release.
*
* The "compatible patch version" is the earliest patch version with which the
* current version is compatible. It should ideally stay at 0 all the time.
* If the compatible patch version is bumped:
* - a new lobby room must be opened
* - the version of the public '0ad' mod must be bumped
* - incompatible replays and savegames will be rotated as new folders are created
*
* This does not describe compatibility with the modding API. Modders are advised
* to rely on the actual engine version.
*/
#define PS_SERIALIZATION_COMPATIBLE_PATCH 0
#define STR(ver) #ver
#define DOTCONCAT(v1, v2, v3) STR(v1) "." STR(v2) "." STR(v3)
// See definition of PS_VERSION_* for details
#define PS_VERSION DOTCONCAT(PS_VERSION_MAJOR, PS_VERSION_MINOR, PS_VERSION_PATCH)
// Used in Windows .rc file
#define PS_VERSION_WORD PS_VERSION_MAJOR,PS_VERSION_MINOR,PS_VERSION_PATCH,0
// See definition of PS_SERIALIZATION_COMPATIBLE_PATCH for details
#define PS_SERIALIZATION_VERSION DOTCONCAT(PS_VERSION_MAJOR, PS_VERSION_MINOR, PS_SERIALIZATION_COMPATIBLE_PATCH)
extern wchar_t build_version[];
#endif // INCLUDED_BUILDVERSION

View file

@ -9,8 +9,8 @@
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION PYROGENESIS_VERSION_WORD
PRODUCTVERSION PYROGENESIS_VERSION_WORD
FILEVERSION PS_VERSION_WORD
PRODUCTVERSION PS_VERSION_WORD
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS VER_DEBUG
FILEOS VOS_NT_WINDOWS32
@ -23,12 +23,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "Wildfire Games"
VALUE "FileDescription", "Pyrogenesis engine"
VALUE "FileVersion", PYROGENESIS_VERSION
VALUE "FileVersion", PS_VERSION
VALUE "InternalName", "pyrogenesis.rc"
VALUE "LegalCopyright", "Copyright (C) 2025 Wildfire Games"
VALUE "OriginalFilename", "pyrogenesis.rc"
VALUE "ProductName", "Pyrogenesis"
VALUE "ProductVersion", PYROGENESIS_VERSION
VALUE "ProductVersion", PS_VERSION
END
END
BLOCK "VarFileInfo"

View file

@ -130,7 +130,7 @@ XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string
m_client->registerConnectionListener(this);
m_client->setPresence(gloox::Presence::Available, -1);
m_client->disco()->setVersion("Pyrogenesis", PYROGENESIS_VERSION);
m_client->disco()->setVersion("Pyrogenesis", PS_SERIALIZATION_VERSION);
m_client->disco()->setIdentity("client", "bot");
m_client->setCompression(false);

View file

@ -561,7 +561,9 @@ static void RunGameOrAtlas(const std::span<const char* const> argv)
if (args.Has("version"))
{
debug_printf("Pyrogenesis %s\n", PYROGENESIS_VERSION);
debug_printf("Pyrogenesis %s\n", PS_VERSION);
if (std::strcmp(PS_VERSION, PS_SERIALIZATION_VERSION) != 0)
debug_printf("Compatible down to patch %s\n", PS_SERIALIZATION_VERSION);
return;
}

View file

@ -44,7 +44,7 @@ Message CreateHandshake() {
handshake.m_Magic = PS_PROTOCOL_MAGIC;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_EngineVersion = PYROGENESIS_VERSION;
handshake.m_EngineVersion = PS_SERIALIZATION_VERSION;
for (const Mod::ModData* mod : Mod::Instance().GetEnabledModsData())
{

View file

@ -128,7 +128,7 @@ void StartNetworkHost(const CStrW& playerName, const u16 serverPort, const CStr&
* TODO: it should be possible to implement SRP or something along those lines to completely protect from this,
* but the cost/benefit ratio is probably not worth it.
*/
CStr hashedPass = HashCryptographically(password, hostJID + password + PYROGENESIS_VERSION);
CStr hashedPass = HashCryptographically(password, hostJID + password + PS_SERIALIZATION_VERSION);
g_NetServer->SetPassword(hashedPass);
g_NetClient->SetHostJID(hostJID);
g_NetClient->SetGamePassword(hashedPass);
@ -176,7 +176,7 @@ void StartNetworkJoinLobby(const CStrW& playerName, const CStr& hostJID, const C
ENSURE(!g_NetServer);
ENSURE(!g_Game);
CStr hashedPass = HashCryptographically(password, hostJID + password + PYROGENESIS_VERSION);
CStr hashedPass = HashCryptographically(password, hostJID + password + PS_SERIALIZATION_VERSION);
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);

View file

@ -77,8 +77,8 @@ CLogger::CLogger(std::ostream& mainLog, std::ostream& interestingLog, const bool
m_InterestingLog{interestingLog},
m_UseDebugPrintf{useDebugPrintf}
{
m_MainLog << html_header0 << PYROGENESIS_VERSION << ") Main log" << html_header1;
m_InterestingLog << html_header0 << PYROGENESIS_VERSION << ") Main log (warnings and errors only)" << html_header1;
m_MainLog << html_header0 << PS_VERSION << ") Main log" << html_header1;
m_InterestingLog << html_header0 << PS_VERSION << ") Main log (warnings and errors only)" << html_header1;
}
CLogger::~CLogger()

View file

@ -57,7 +57,7 @@ static void AppendAsciiFile(FILE* out, const OsPath& pathname)
void psBundleLogs(FILE* f)
{
fwprintf(f, L"Build Version: %ls\n\n", build_version);
fwprintf(f, L"Engine Version: %hs\n\n", PYROGENESIS_VERSION);
fwprintf(f, L"Engine Version: %hs\n\n", PS_VERSION);
fwprintf(f, L"System info:\n\n");
OsPath path1 = psLogDir()/"system_info.txt";

View file

@ -80,7 +80,7 @@ void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
Script::SetProperty(rq, attribs, "timestamp", (double)std::time(nullptr));
// Add engine version and currently loaded mods for sanity checks when replaying
Script::SetProperty(rq, attribs, "engine_version", PYROGENESIS_VERSION);
Script::SetProperty(rq, attribs, "engine_serialization_version", PS_SERIALIZATION_VERSION);
JS::RootedValue mods(rq.cx);
Script::ToJSVal(rq, &mods, g_Mods.GetEnabledModsData());
Script::SetProperty(rq, attribs, "mods", mods);

View file

@ -112,7 +112,8 @@ Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation
Script::CreateObject(
rq,
&metadata,
"engine_version", PYROGENESIS_VERSION,
"engine_version", PS_VERSION,
"engine_serialization_version", PS_SERIALIZATION_VERSION,
"time", static_cast<double>(now),
"playerID", g_Game->GetPlayerID(),
"mods", mods,

View file

@ -61,7 +61,7 @@ const u8 minimumReplayDuration = 3;
OsPath VisualReplay::GetDirectoryPath()
{
return Paths(g_CmdLineArgs).UserData() / "replays" / PYROGENESIS_VERSION;
return Paths(g_CmdLineArgs).UserData() / "replays" / PS_SERIALIZATION_VERSION;
}
OsPath VisualReplay::GetCacheFilePath()

View file

@ -135,7 +135,8 @@ JS::Value GetEngineInfo(const ScriptInterface& scriptInterface)
Script::CreateObject(
rq,
&metainfo,
"engine_version", PYROGENESIS_VERSION,
"engine_version", PS_VERSION,
"engine_serialization_version", PS_SERIALIZATION_VERSION,
"mods", mods);
Script::DeepFreezeObject(rq, metainfo);