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);