From 866d6f052776bd3ce70f2fee02df89edbfb339ff Mon Sep 17 00:00:00 2001 From: Itms Date: Mon, 20 Oct 2025 13:54:34 +0200 Subject: [PATCH] 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 dae7a8c39403af3a6f5ef7f059b9b07421e849a6 --- .../Persistence/PersistentMatchSettings.js | 3 +- .../mods/public/gui/loadgame/SavegameList.js | 4 +- .../public/gui/loadgame/SavegameLoader.js | 17 ++++---- .../public/gui/replaymenu/replay_actions.js | 7 ++-- .../mods/public/gui/replaymenu/replay_menu.js | 6 +-- source/lib/build_version.h | 42 ++++++++++++++++++- source/lib/sysdep/os/win/pyrogenesis.rc | 8 ++-- source/lobby/XmppClient.cpp | 2 +- source/main.cpp | 4 +- source/network/NetProtocol.h | 2 +- .../network/scripting/JSInterface_Network.cpp | 4 +- source/ps/CLogger.cpp | 4 +- source/ps/Pyrogenesis.cpp | 2 +- source/ps/Replay.cpp | 2 +- source/ps/SavedGame.cpp | 3 +- source/ps/VisualReplay.cpp | 2 +- source/ps/scripting/JSInterface_Mod.cpp | 3 +- 17 files changed, 81 insertions(+), 34 deletions(-) diff --git a/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js b/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js index d45fc7c00e..97419ad196 100644 --- a/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js +++ b/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js @@ -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 || {}; diff --git a/binaries/data/mods/public/gui/loadgame/SavegameList.js b/binaries/data/mods/public/gui/loadgame/SavegameList.js index d39cc181aa..8c28d35f15 100644 --- a/binaries/data/mods/public/gui/loadgame/SavegameList.js +++ b/binaries/data/mods/public/gui/loadgame/SavegameList.js @@ -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); } diff --git a/binaries/data/mods/public/gui/loadgame/SavegameLoader.js b/binaries/data/mods/public/gui/loadgame/SavegameLoader.js index c8de3ee389..154df3ca1f 100644 --- a/binaries/data/mods/public/gui/loadgame/SavegameLoader.js +++ b/binaries/data/mods/public/gui/loadgame/SavegameLoader.js @@ -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) { diff --git a/binaries/data/mods/public/gui/replaymenu/replay_actions.js b/binaries/data/mods/public/gui/replaymenu/replay_actions.js index be3a701b4d..b81dec4d3d 100644 --- a/binaries/data/mods/public/gui/replaymenu/replay_actions.js +++ b/binaries/data/mods/public/gui/replaymenu/replay_actions.js @@ -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")); diff --git a/binaries/data/mods/public/gui/replaymenu/replay_menu.js b/binaries/data/mods/public/gui/replaymenu/replay_menu.js index f13193c2cd..ad14071eb4 100644 --- a/binaries/data/mods/public/gui/replaymenu/replay_menu.js +++ b/binaries/data/mods/public/gui/replaymenu/replay_menu.js @@ -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; } diff --git a/source/lib/build_version.h b/source/lib/build_version.h index 304359034c..b38463674d 100644 --- a/source/lib/build_version.h +++ b/source/lib/build_version.h @@ -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 diff --git a/source/lib/sysdep/os/win/pyrogenesis.rc b/source/lib/sysdep/os/win/pyrogenesis.rc index deb27f234a..7da833fb96 100644 --- a/source/lib/sysdep/os/win/pyrogenesis.rc +++ b/source/lib/sysdep/os/win/pyrogenesis.rc @@ -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" diff --git a/source/lobby/XmppClient.cpp b/source/lobby/XmppClient.cpp index 8f39534750..646b86b942 100644 --- a/source/lobby/XmppClient.cpp +++ b/source/lobby/XmppClient.cpp @@ -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); diff --git a/source/main.cpp b/source/main.cpp index 184509f411..1fa5308112 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -561,7 +561,9 @@ static void RunGameOrAtlas(const std::span 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; } diff --git a/source/network/NetProtocol.h b/source/network/NetProtocol.h index 54f1a967f2..96074c9964 100644 --- a/source/network/NetProtocol.h +++ b/source/network/NetProtocol.h @@ -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()) { diff --git a/source/network/scripting/JSInterface_Network.cpp b/source/network/scripting/JSInterface_Network.cpp index c3247cf167..29b437681e 100644 --- a/source/network/scripting/JSInterface_Network.cpp +++ b/source/network/scripting/JSInterface_Network.cpp @@ -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); diff --git a/source/ps/CLogger.cpp b/source/ps/CLogger.cpp index 79c640bd1e..5642da8b08 100644 --- a/source/ps/CLogger.cpp +++ b/source/ps/CLogger.cpp @@ -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() diff --git a/source/ps/Pyrogenesis.cpp b/source/ps/Pyrogenesis.cpp index 15d2228151..e6255630b6 100644 --- a/source/ps/Pyrogenesis.cpp +++ b/source/ps/Pyrogenesis.cpp @@ -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"; diff --git a/source/ps/Replay.cpp b/source/ps/Replay.cpp index 6f49f6bc12..19aadb4088 100644 --- a/source/ps/Replay.cpp +++ b/source/ps/Replay.cpp @@ -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); diff --git a/source/ps/SavedGame.cpp b/source/ps/SavedGame.cpp index 793fb349ff..6e7ea7bc1b 100644 --- a/source/ps/SavedGame.cpp +++ b/source/ps/SavedGame.cpp @@ -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(now), "playerID", g_Game->GetPlayerID(), "mods", mods, diff --git a/source/ps/VisualReplay.cpp b/source/ps/VisualReplay.cpp index d1d3701e79..61c1a09e70 100644 --- a/source/ps/VisualReplay.cpp +++ b/source/ps/VisualReplay.cpp @@ -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() diff --git a/source/ps/scripting/JSInterface_Mod.cpp b/source/ps/scripting/JSInterface_Mod.cpp index 2d6443c949..9d1b713774 100644 --- a/source/ps/scripting/JSInterface_Mod.cpp +++ b/source/ps/scripting/JSInterface_Mod.cpp @@ -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);