Skip to content

BetaDeps v0.7.3 - SaveShield

Choose a tag to compare

@Trashpanda62 Trashpanda62 released this 25 May 07:54
· 51 commits to main since this release

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 MissingMethodException or MissingFieldException fires, 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's Mission.GetFormationSpawnFrame crash. 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.xml for Name/Id/Version/Author/DependedModules, plus the DLL's AssemblyVersion and every TaleWorlds.* referenced assembly. Single-stop "what version shipped against what API".
  • Cecil import scan — walks the mod DLL's MemberReferences via Mono.Cecil and lists every TaleWorlds.* 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.StackTrace walked frame-by-frame with IL offsets.
  • First-arg summary — for LoadSaveGameData this is the save name; for LoadGameAction it's the full SaveGameFileInfo dump including the mod-list captured at save time; for SetMissionMode it's the MissionMode value, 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:

  • RetinuesTypeLoadException: Could not load type 'Attribute' at module load. Retinues's own Safety.Attribute base 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.GetFormationSpawnFrame with 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, so MissionCombatantsLogic still trips an MBIllegalValueException downstream. 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.