BetaDeps v0.7.3 - SaveShield
BetaDeps v0.7.3 — SaveShield + ON-by-default swallow-mode
Headline release. SaveShield is BetaDeps's second defensive layer, joining PatchShield. The two now work side-by-side: PatchShield wraps Harmony-patched methods; SaveShield wraps the engine-level entry points where consumer-mod handlers run during save deserialization and battle init.
SaveShield core
Shields seven Bannerlord entry points by default: MBSaveLoad.LoadSaveGameData, SaveManager.Load (both overloads), SandBoxSaveHelper.LoadGameAction, MissionState.FinishMissionLoading, Mission.SetMissionMode, Mission.OnInitialize, Mission.SpawnTroop. When a consumer-mod handler throws from any of these, SaveShield catches the throw, writes a full diagnostic block to runtime.log, and — by default — drops the throw so the load continues. Saves that crashed on load now load. Battles that crashed on entry now enter.
Swallow-mode ON by default
This is the big behavioral change. v0.7.2 shipped swallow-mode as opt-in; v0.7.3 makes it the protective default. Naming convention now matches PatchShield exactly — both default ON, both opt-out via a *-disabled.flag file. Click Toggle SaveShield Swallow in Mod Config to opt out (creates saveshield-swallow-disabled.flag next to runtime.log). Migration: the legacy v0.7.2 saveshield-swallow.flag file is auto-deleted on first launch so it doesn't sit stale.
The swallow only fires when the deepest non-engine, non-BetaDeps stack frame is from a mod assembly — so engine bugs and BetaDeps bugs continue to propagate unmodified.
Mod-creator diagnostics (the SaveShield FAILURE block)
Every caught exception writes a labeled block to runtime.log with all of the following:
- CULPRIT — name of the assembly that owns the deepest non-engine frame. Mod authors get a one-line answer about which mod to investigate.
- Current API signature probe — when
MissingMethodExceptionorMissingFieldExceptionfires, SaveShield reflects the named type on the current build and prints every current overload. The mod author sees exactly what to migrate to. (Real example shipped: ReinforcementSystem'sMission.GetFormationSpawnFramecrash. SaveShield printed the current 6-parameter signature against the 5-parameter signature the mod called, pinpointing the API drift.) - CULPRIT manifest — reads the offending mod's
SubModule.xmlfor Name/Id/Version/Author/DependedModules, plus the DLL'sAssemblyVersionand everyTaleWorlds.*referenced assembly. Single-stop "what version shipped against what API". - Cecil import scan — walks the mod DLL's
MemberReferencesvia Mono.Cecil and lists everyTaleWorlds.*member referenced whose name matches the failing call. Distinguishes compile-time-bound calls from reflection-bound ones. - Finalizer call chain — frames that led TO the patched method (not just the exception's throw stack). Often reveals the engine code path that triggered the failure.
- Parsed frames — exception's
System.Diagnostics.StackTracewalked frame-by-frame with IL offsets. - First-arg summary — for
LoadSaveGameDatathis is the save name; forLoadGameActionit's the fullSaveGameFileInfodump including the mod-list captured at save time; forSetMissionModeit's theMissionModevalue, etc.
selftest.log + selftest.json
selftest.log gets a new SaveShield status section between PatchShield and the installed-vs-enabled list. Counters (Methods shielded, Duplicate-key hits, Other load failures, Swallow-mode state, Exceptions swallowed) plus the full text of every FAILURE block recorded this session, plus a copy-paste-ready GitHub-issue markdown snippet of the most-recent failure.
New selftest.json sidecar is written next to selftest.log — schema-versioned, same data in machine-readable form. AI assistants and CI tooling can parse it without interpreting the human-readable layout.
Send-to-GitHub button enrichment
The pre-fill URL on the "Send to GitHub" button now embeds the most-recent SaveShield FAILURE block as inline markdown (CULPRIT table, current API signatures, manifest probe, stack frames). Bug reports come with the diagnosis already filled in.
failed-mods-catalog.txt
New append-only ledger at Modules\BetaDeps\failed-mods-catalog.txt. Each session, the first time a (CULPRIT, ExceptionType, OwnerMethod) triple is seen, a one-line entry is appended. Build up a personal incompatibility ledger across sessions.
PatchShield owner-counts
PatchShield now tracks which Harmony owner IDs got auto-unpatched and how many of each. selftest.log surfaces this as "AIInfluence: 4 patches unpatched" rows, so you see at a glance which mods are bleeding.
Incompatibility list updates
Two more mods moved to the Known incompatible list, both with detailed cause notes:
- Retinues —
TypeLoadException: Could not load type 'Attribute'at module load. Retinues's ownSafety.Attributebase class fails to load due to value-type mismatch with one of its referenced assemblies. Downstream symptom is the "Retinues Dependency Error: Harmony: Error" dialog. Needs a Retinues mod-author recompile. - ReinforcementSystem — calls
Mission.GetFormationSpawnFramewith the v1.2.x 5-parameter signature; current Bannerlord added a 6th parameter (Boolean useDefaultClassIfNotFound). SaveShield swallows the throw, but the broken handler was setting up team state that the engine then expected, soMissionCombatantsLogicstill trips anMBIllegalValueExceptiondownstream. Needs a ReinforcementSystem mod-author one-line fix.
Caveats
Swallow-mode validated end-to-end against ReinforcementSystem. It works as designed for mods whose broken handler is non-essential (drops the handler, game keeps running). It can't rescue battles when the broken handler is doing critical engine-state setup — in that case you'll see a SAVE-LOAD FAILURE block, then a follow-up MISSION-INIT FAILURE from inside TaleWorlds itself (with CULPRIT: (no non-engine frame found)) because the engine noticed inconsistent state. Disable the named CULPRIT mod from the first block and the engine-level follow-up goes away.