External (user) scenario mods that include -add.xml files for StrictModeDeferred info types (bonus, goal, event, eventOption, eventStory, goalReq, etc.) silently fail at game start with "Version mismatch" in the log. The root cause is that AppMain.StartGame() creates a new ModPath for the server but never copies the strictMode flag from the controller's ModPath, causing an asymmetric CRC computation between server and client.
Internal (DLC) scenario mods are unaffected because their files bypass OpenModdedXML/AddCRC entirely.
- Create an external scenario mod with
scenario=truein ModInfo.xml andmzModNameset inscenario-add.xml - Add any
-add.xmlfile for aStrictModeDeferredinfo type (e.g.,bonus-add.xml,goal-add.xml) - Start the scenario from the scenario setup screen
- Expected: Game starts normally
- Actual: Game creates an auto-save then immediately returns to the main menu.
Player.logshows "Version mismatch"
Removing the StrictModeDeferred -add.xml file fixes the issue. Non-deferred types (tribe-add.xml, unit-add.xml, genderedText-add.xml, etc.) work fine.
When a scenario with mzModName is selected, UpdateScenarioMods() sets strict mode = true on the controller's ModPath. The controller then creates ModSettings, which creates Infos, which calls init():
ReadInfoListTypes()— iterates ALL info types, callsGetModdedXML(name, ADD)→OpenModdedXML(path, bAddCRC=true)→ each-add.xmlfile's CRC32 is XORed into the accumulatorReadInfoListData(items, deferredPass=false)—thisPass()in strict mode returnsitem.HasFlag(StrictModeDeferred) == deferredPass, so StrictModeDeferred items are skipped → their CRCs are NOT XORed a second time
Result: Controller CRC = XOR of all StrictModeDeferred files' CRC32 values (non-zero)
Non-deferred files are XORed twice (once in each pass) and cancel via XOR. Deferred files are XORed only once.
AppMain.StartGame() creates a new ModPath for the server. It reuses the controller's Infos (no re-initialization) and copies the CRC:
ModPath modPath = new ModPath();
modPath.InitMods(controller.CurrentModSettings.GetMods()); // CRC = 0
gameServerBehaviour.ModSettings = modPath.CreateModSettings(Application, controller.CurrentModSettings.Infos);
modPath.CRC = controller.CurrentModSettings.ModPath.GetCRC(); // CRC = non-zero
// !! Missing: modPath.SetStrictMode(controller.CurrentModSettings.ModPath.IsStrictMode())CreateServerGame() then calls Infos.PreCreateGame(), which would XOR the deferred files again (canceling them) if the server were in strict mode. But the server's ModPath defaults to strictMode=false, so thisPass() returns !deferredPass = false for all items → no-op. Server CRC stays non-zero.
JoinGame() creates another new ModPath (also strictMode=false by default) and creates fresh Infos. During Infos.init():
ReadInfoListTypes()— all files XORed onceReadInfoListData(items, deferredPass=false)— in non-strict mode,thisPass()returns!false = truefor ALL items → all files XORed again → everything cancels
Result: Client CRC = 0
The client sends its CRC to the server via SendClientGameReadyToServer(). The server's OnConnection() compares:
Server CRC (non-zero) ≠ Client CRC (0) → "Version mismatch" → game aborted
In GetModdedXML, internal mods load XML directly from Unity's Resources system, bypassing OpenModdedXML and AddCRC entirely:
if (isInternal) {
xmlDocument2 = new XmlDocument();
xmlDocument2.LoadXml(cacheDict(modRecord)[text3][j].text); // No CRC
} else {
xmlDocument2 = OpenModdedXML(text5, searchType != ModdedXMLType.ADD_ALWAYS); // AddCRC called
}Internal mod files contribute zero to the CRC accumulator, so the asymmetry never manifests.
Copy strict mode from the controller to the server's ModPath in AppMain.StartGame():
modPath.CRC = controller.CurrentModSettings.ModPath.GetCRC();
modPath.SetStrictMode(controller.CurrentModSettings.ModPath.IsStrictMode()); // Add this lineThis way, Infos.PreCreateGame() on the server would run in strict mode, XORing the deferred files a second time and canceling them to 0 — matching the client's CRC.
Alternatively, the same SetStrictMode could be applied in JoinGame() so the client also computes the non-zero CRC, but fixing it on the server side seems more correct since that's where the CRC originates.
All line numbers refer to the decompiled Assembly-CSharp.dll (version 1.0.81366) via ILSpy, and to the game's published source files in Source/.
File: Source/Base/SystemCore/HashCombinerAlt.cs
Uses XOR — commutative and self-canceling (a ^ a = 0):
public struct HashCombinerAlt {
public static HashCombinerAlt Zero = new HashCombinerAlt(0);
private readonly int hash;
public HashCombinerAlt Add(int value) { return new HashCombinerAlt(hash ^ value); }
}DLL line ~97648
protected virtual void UpdateScenarioMods(ScenarioType eScenario) {
InfoScenario scenario = Infos.scenario(eScenario);
bool strictMode = false;
if (!string.IsNullOrEmpty(scenario.mzModName)) {
list.Add(scenario.mzModName);
strictMode = true; // <-- Strict mode enabled for scenario mods
}
Controller.CurrentMods.SetStrictMode(strictMode);
Controller.CurrentMods.SetMods(list, null, showPopup: false);
}File: Source/Base/Game/GameCore/Infos.cs, line 822
bool thisPass(XmlDataListItemBase item) {
if (!mModSettings.ModPath.IsStrictMode())
return !deferredPass; // Non-strict: ALL items in init(), NONE in PreCreateGame()
return item.GetFlags().HasFlag(XmlDataListFlags.StrictModeDeferred) == deferredPass;
// Strict: deferred items ONLY in PreCreateGame(), non-deferred ONLY in init()
}DLL line ~3664 (AppMain.StartGame)
ModPath modPath = new ModPath();
modPath.InitMods(controller.CurrentModSettings.GetMods());
gameServerBehaviour.ModSettings = modPath.CreateModSettings(Application, controller.CurrentModSettings.Infos);
modPath.CRC = controller.CurrentModSettings.ModPath.GetCRC();
// No SetStrictMode() call — server defaults to strictMode=falseDLL line ~2164 (AppMain.JoinGame)
ModPath modPath = new ModPath();
modPath.SetMods(list, null, showPopup: true);
// No SetStrictMode() — defaults to false
// Creates fresh Infos → init() → all CRCs cancel → CRC = 0
LoadingManager.ModSettings = modPath.CreateModSettings(Application, infos);DLL line ~9480 (GameServerBehaviour.OnConnection)
if (true && msg.VersionCRC != -1 && msg.VersionCRC != base.ModSettings.ModPath.GetCRC()) {
// Sends ClientVersionFail → client logs "Version mismatch" → exitClient()
}DLL line ~62963
private XmlDocument OpenModdedXML(string filePath, bool bAddCRC) {
if (moddedFileCRC.ContainsKey(filePath)) {
if (bAddCRC) AddCRC(moddedFileCRC[filePath]); // Cached CRC
} else {
byte[] bytes = File.ReadAllBytes(filePath);
int crc = CRC32.Compute(bytes);
moddedFileCRC[filePath] = crc;
if (bAddCRC) AddCRC(crc);
}
// ...
}DLL line ~62600 (inside GetModdedXML)
if (isInternal) {
xmlDocument2 = new XmlDocument();
xmlDocument2.LoadXml(cacheDict(modRecord)[text3][j].text); // No OpenModdedXML, no CRC
} else {
xmlDocument2 = OpenModdedXML(text5, searchType != ModdedXMLType.ADD_ALWAYS); // CRC accumulated
}File: Source/Base/Game/GameCore/Infos.cs, BuildListOfInfoFiles() (line ~529+)
Types with StrictModeDeferred: bonus, effectPlayer, effectCity, event, eventOption, eventStory, eventStoryOption, goal, goalReq, and others. These are the types that trigger the bug when used in external scenario mods.
- Old World version 1.0.81366 (Steam, macOS)
- External scenario mod with
scenario=truein ModInfo.xml - Tested with
bonus-add.xmlandgoal-add.xml— both trigger the issue