Skip to content

Instantly share code, notes, and snippets.

@becked
Last active February 11, 2026 02:00
Show Gist options
  • Select an option

  • Save becked/f6f2c434762d18148e7d3ffe621d9c5d to your computer and use it in GitHub Desktop.

Select an option

Save becked/f6f2c434762d18148e7d3ffe621d9c5d to your computer and use it in GitHub Desktop.
Old World Bug: External Scenario Mods with StrictModeDeferred Info Types Cause Version Mismatch

Old World Bug: External Scenario Mods with StrictModeDeferred Info Types Cause "Version Mismatch"

TL;DR

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.

Reproduction

  1. Create an external scenario mod with scenario=true in ModInfo.xml and mzModName set in scenario-add.xml
  2. Add any -add.xml file for a StrictModeDeferred info type (e.g., bonus-add.xml, goal-add.xml)
  3. Start the scenario from the scenario setup screen
  4. Expected: Game starts normally
  5. Actual: Game creates an auto-save then immediately returns to the main menu. Player.log shows "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.

High-Level Flow

1. Scenario Setup Screen (Controller) — strict mode ON

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, calls GetModdedXML(name, ADD)OpenModdedXML(path, bAddCRC=true) → each -add.xml file's CRC32 is XORed into the accumulator
  • ReadInfoListData(items, deferredPass=false)thisPass() in strict mode returns item.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.

2. Server — inherits non-zero CRC, strict mode OFF

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.

3. Client — computes CRC = 0, strict mode OFF

JoinGame() creates another new ModPath (also strictMode=false by default) and creates fresh Infos. During Infos.init():

  • ReadInfoListTypes() — all files XORed once
  • ReadInfoListData(items, deferredPass=false) — in non-strict mode, thisPass() returns !false = true for ALL items → all files XORed again → everything cancels

Result: Client CRC = 0

4. CRC Check — mismatch

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

Why DLC Mods Are Unaffected

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.

Suggested Fix

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 line

This 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.

Detailed Code References

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/.

CRC Accumulator: HashCombinerAlt

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

Strict Mode Set for Scenarios: UpdateScenarioMods

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

thisPass() in ReadInfoListData — The Asymmetry

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()
}

Server Creation — Missing SetStrictMode

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=false

Client Creation — Also No SetStrictMode

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

CRC Check on Connection

DLL line ~9480 (GameServerBehaviour.OnConnection)

if (true && msg.VersionCRC != -1 && msg.VersionCRC != base.ModSettings.ModPath.GetCRC()) {
    // Sends ClientVersionFail → client logs "Version mismatch" → exitClient()
}

OpenModdedXML — Where CRC Is Accumulated

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);
    }
    // ...
}

Internal vs External Mod CRC Path

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
}

StrictModeDeferred Flag on Info Types

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.

Environment

  • Old World version 1.0.81366 (Steam, macOS)
  • External scenario mod with scenario=true in ModInfo.xml
  • Tested with bonus-add.xml and goal-add.xml — both trigger the issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment