Skip to content

BetaDeps v0.7.0 — PatchShield + dependency-conflict fixes

Choose a tag to compare

@Trashpanda62 Trashpanda62 released this 24 May 05:24
· 51 commits to main since this release

BetaDeps v0.7.0 — PatchShield + dependency-conflict fixes + opt-in recovery

Released 2026-05-24.

Headline

PatchShield: a generic Harmony finalizer that catches the entire class of "consumer mod built against a stale TaleWorlds API" crashes. When a mod's prefix throws MissingMethodException, MissingFieldException, or TypeLoadException because TaleWorlds renamed/removed something the mod was patching, BetaDeps now (a) logs it, (b) synthesizes a sensible default return value so downstream callers don't NRE, and (c) auto-unpatches the offending mod's prefix so the broken patch never fires again. Before v0.7 these crashed the game during campaign init. With the shield, the offending patch becomes a logged warning and the game continues.

This solves real-world cases like the AIInfluence + NavalDLC campaign-load crash chain, plus a category of issues every time TaleWorlds ships a Bannerlord patch and not every mod author updates immediately.

Auto-disable also stays opt-in. Click Toggle Auto-Disable in Mod Config to enable single-launch recovery for the small set of known-CTD mods (HotScenes, RBM, etc.). All other recovery is now done live by the shield without any flag.

New

  • PatchShield — installed at OnBeforeInitialModuleScreenSetAsRoot and re-run at every late lifecycle hook (OnGameInitializationFinished, OnGameStart, OnAfterGameInitializationFinished, OnNewGameCreated) so consumer mods that defer their PatchAll past OnSubModuleLoad are also covered. Idempotent: each pass only wraps newly-patched methods. Reports shield pass: +N new, X already-shielded, Y skipped per pass.
  • Auto-unpatch on first throw. When the shield catches one of the three swallowable exceptions, it enumerates Harmony.GetPatchInfo(originalMethod) and removes every non-BetaDeps owner from the patch chain via Harmony.Unpatch(method, HarmonyPatchType.Prefix, owner). Subsequent calls run the unmodified original. Before this, a broken prefix would fire hundreds of times during NPC generation; now it fires once and is removed.
  • MCM.Abstractions.Base.PerSave.PerSaveSettings<TSelf> — generic singleton base class consumer mods like Detailed Character Creation reference at JIT/type-load time. Without it the CLR throws TypeLoadException on DccPerSaveSettings.get_SaveInstance(), taking out the campaign init for anyone running DCC plus another mod that patches OnGameInitializationFinished. AttributePerSaveSettings<TSelf> re-parented to inherit from it so the full upstream inheritance chain resolves.
  • ISettingsBuilder.CreatePreset(string, string, Action<ISettingsPresetBuilder>) + concrete SettingsPresetBuilderImpl — closes a missing-method gap that broke Retinues' fluent-builder MCM panel.
  • ISettingsPropertyGroupBuilder.SetGroupOrder(int) — second missing-method gap on the same panel.
  • Auto-disable opt-in (carried forward from earlier v0.7 work). All recovery features that modify LauncherData.xml ship OFF by default; click Toggle Auto-Disable in Mod Config to opt in. PatchShield runs regardless because it doesn't touch the launcher's modlist — it just defangs broken patches in memory.

Fixed

  • Character Development Editor "dependency conflict" dialog. Traced via IL inspection: our impersonated Bannerlord.UIExtenderEx.Prefabs.PrefabExtensionReplacePatch.GetPrefabExtension() declared a return type of XmlNode, but the upstream BUTR ABI declares it as XmlDocument. Consumer mods compile their override GetPrefabExtension() against the upstream signature; the CLR rejected those overrides at type-load time with "does not have an implementation" because the return types differed. Both PrefabExtensionReplacePatch.GetPrefabExtension() and PrefabExtensionInsertAsSiblingPatch.GetPrefabExtension() now return XmlDocument. CDE and any mod hitting the same ABI mismatch now load cleanly.
  • Defense-in-depth shim on CollectModuleAssemblyTypes. Transpiler that, when a consumer assembly's GetTypes() throws ReflectionTypeLoadException, performs a lenient pass and signals Success to the engine if the specific SubModule class named in SubModule.xml loaded cleanly and has a usable parameterless constructor. Catches the same class of issue for any future ABI mismatch we haven't found yet, without papering over genuinely-broken mods.
  • IDontCare 658/658 self-test pass. McmSelfTest now classifies properties type-first (checking MCM.Common.Dropdown<T> ancestry) before falling back to [SettingProperty*] attribute classification. IDontCare's GlobalFilterMode is declared as Dropdown<string> but decorated with [SettingPropertyBool], which used to misroute the round-trip; now it's classified correctly. Done-semantics also re-snapshots expected values after all mutations to handle computed properties (AdvancedFilteringStringsToFilter recomputes from other dropdowns).
  • Alias-folder versions widened to v2.4.99 / v2.10.99 / v2.14.99 (Harmony / ButterLib / UIExtenderEx). Satisfies modern consumer-mod minimum-version checks.
  • Stale launcher entries no longer treated as suspects. BannerFix-style ghost entries (in LauncherData.xml but folder missing) are correctly skipped instead of triggering a phantom disable.

Diagnosed incompatible community mods

While shipping v0.7 we manually traced three community mods that are genuinely incompatible with the current Bannerlord build, beyond what BetaDeps can shield. These are mod-authoring issues, not BetaDeps bugs — listed here so users can find them:

  • HotScenes — crashes the game before the main menu loads.
  • RBM (Realistic Battle Mod) — crashes during campaign load, before the tutorial mission.
  • JoinAnyBattle — crashes during the tutorial → world-map transition.

If you use any of these and your campaign won't load, disable them in BLSE's Mods tab. PatchShield catches a lot of patch-API mismatches but cannot rewrite a mod's own internal logic.

Changed defaults (vs. v0.6.0)

  • Auto-disable on first run: OFF (was ON in v0.6.0). PatchShield runs regardless.

Known limitations

  • Mod Organizer 2 is not officially supported. MO2's virtual filesystem layer can hide the alias folders BetaDeps creates in Modules\. Install BetaDeps directly into <Bannerlord>\Modules\.
  • Legacy strict-wildcard mods declaring v2.4.0.* Harmony minimums (e.g. older CargoHolds, if still maintained) will not match the bumped alias version v2.4.99.0. Modern modlist support takes precedence.

Files in this release

  • BetaDeps-v0.7.0.zip — drop into Modules\ (unzip → Modules\BetaDeps\)

Verifying the install

After first launch, your Modules\BetaDeps\runtime.log should contain lines like:

[BetaDeps.PatchShield] shield pass: +510 new, 0 already-shielded, 0 skipped (total shielded: 510)
[BetaDeps.PatchShield] shield pass: +142 new, 510 already-shielded, 0 skipped (total shielded: 652)

If a consumer mod's prefix throws during campaign load, you'll see:

[BetaDeps.PatchShield] swallowed MissingMethodException from a patch on <class>.<method>: ...
[BetaDeps.PatchShield] unpatched prefixes from owner '<modid>' on <class>::<method>

That's the shield doing its job. The game continues; the broken patch is gone for the rest of the session.