Table of Contents

Compiled Prefabs

Note

This article is written for mod authors. If you are interested in the internal architecture and code generation pipeline of UIExtenderEx, see the Compiled Prefabs Internals section.

In Mount & Blade II: Bannerlord, Gauntlet UI can construct a screen (a "movie") using two different mechanisms:

  1. Dynamic Runtime XML Parsing: The game reads .xml prefab files from disk and parses them into a widget hierarchy on the fly.
  2. Pre-Compiled C# Classes: The game executes optimized C# classes generated ahead of time by TaleWorlds and shipped in Modules/<Module>/bin/Win64_Shipping_Client/*.AutoGenerated*.dll.

Pre-compiled C# classes are substantially faster than XML parsing and represent the game's default, preferred way to load user interfaces.

However, pre-compiled prefabs act as an immutable snapshot. The game's GeneratedPrefabContext indexes these pre-compiled classes by movie name, then by ViewModel variant. If a pre-compiled class exists for a given movie, the engine loads it immediately and never looks at the XML file on disk. Consequently, any modifications made by mods to the underlying XML are completely bypassed.

The Evolution of Prefab Patching in UIExtenderEx

  • Legacy Behavior (Pre-v3.0.0):
    UIExtenderEx previously resolved this conflict by forcing UIConfig.DoNotUseGeneratedPrefabs = true and patching the engine setter to prevent the game from re-enabling it. This forced every screen in the game to fall back to the slow runtime XML parser. While all modded XML patches worked, it resulted in longer load times and noticeable UI micro-stutters across the entire game.

  • Modern Behavior (v3.0.0+):
    UIExtenderEx retains the game's fast pre-compiled prefabs for untouched screens, while introducing an on-demand, in-memory C# compilation pipeline for modified screens. Whenever a screen is touched by a mod, UIExtenderEx compiles an optimized C# assembly directly from the patched XML and caches it for future opens.

A movie is compiled when:

  • The movie has a vanilla pre-compiled variant, but its XML has changed: A mod's prefab extension modified the XML tree, or a mod shipped a replacement XML file.
  • The movie has no pre-compiled variant at all: TaleWorlds only generates pre-compiled classes for specific (Movie, ViewModel) pairs. If a mod subclasses a screen's ViewModel, the game fails to match the pre-compiled variant and falls back to XML. UIExtenderEx detects this and generates a dedicated compiled variant for the subclass.

When a modified screen opens for the first time, it loads immediately via XML while UIExtenderEx compiles the new C# variant on a background worker thread. Once compilation finishes, the compiled assembly is cached in Modules/Bannerlord.UIExtenderEx/CompiledPrefabs and used for all subsequent opens.


What This Means for Your Mod

In short: nothing changes in how you write patches and mixins. UIExtenderEx handles compilation, caching, and loading transparently.

What Works Automatically

  • All Prefab Patches: Your PrefabExtensionInsertPatch, PrefabExtensionSetAttributePatch, v1 patches, and custom patches are applied to the XML exactly as before. The resulting patched XML is compiled into C#.
  • Direct Typed Mixin Access: Properties and methods contributed by your ViewModel mixins are bound using direct, strongly-typed C# calls rather than slow reflection.
  • ViewModel Subclasses: If you subclass an existing game ViewModel, UIExtenderEx generates a dedicated compiled variant for your subclass instead of leaving the screen on slow XML parsing.
  • Custom XML Replacements: If your mod ships an XML file to override a vanilla prefab (e.g., UI reskins), UIExtenderEx detects the override and compiles it automatically.
  • Polymorphic / Dynamic Bindings: Bindings to properties that exist only on derived types (such as custom subclasses in list items) fall back gracefully to dynamic by-name bindings, matching XML loader flexibility.

Mod Author Checklist

  1. Keep patch fragments out of GUI/Prefabs: Store partial XML patch snippets in a separate folder (e.g., GUI/PrefabExtensions) so TaleWorlds' WidgetFactory does not mistake them for full standalone prefabs.
  2. Register runtime widgets and prefabs early: If you create custom C# widget classes or generate XML prefabs in code, register them through WidgetFactoryManager before the target movie opens.
  3. Ensure bound properties are public: Properties exposed to Gauntlet must have public getters. A setter only needs to be public if the UI writes the value back.
  4. Test screens twice: The first open after a code or XML change loads via XML while compilation runs in the background. The second open uses the compiled C# variant.

How a Patched Movie Is Loaded

Whenever GauntletMovie.Load is called, UIExtenderEx evaluates the movie and target ViewModel type to choose the optimal loading path:

Scenario What Loads Description
Untouched Vanilla Movie Vanilla Pre-Compiled C# The game's original pre-compiled class is executed. This applies to the majority of vanilla UI screens.
Modified Movie (Cache Hit) UIExtenderEx Compiled C# A patch, replacement XML, or custom ViewModel touched the movie, and a cached assembly matching the current fingerprint exists.
Modified Movie (First Open / Cache Miss) Dynamic XML Fallback No matching compiled build exists yet (or inputs changed). The screen loads immediately from XML, while a background thread compiles the C# variant for future opens.

Architecture and Isolation

All mod-facing APIs reside in Bannerlord.UIExtenderEx.dll targeting netstandard2.0. The background compiler and runtime modules are internal to UIExtenderEx. Whether a screen loads via pre-compiled C# or fallback XML, it is built from the exact same patched XML tree and looks visually identical; the only difference is the open speed.

Resilience Against Engine Updates

To leverage TaleWorlds' pre-compiled prefabs, UIExtenderEx validates TaleWorlds' internal generated class names at startup. If a future game update alters these naming patterns, UIExtenderEx detects the mismatch and safely falls back to compiled/XML loading for all screens. Your patches will never be skipped or ignored due to game updates.


Settings

UIExtenderEx provides configurable settings declared in the <Settings> section of its SubModule.xml. These can be adjusted manually or configured in-game via the Mod Configuration Menu (MCM):

Setting Default Effect
CompiledPrefabs true Enables compiling patched movies to C# at runtime. If set to false, patched movies always load via XML, while untouched vanilla movies continue using the game's pre-compiled prefabs.
DisableGeneratedPrefabs false Reverts to legacy pre-v3.0 behavior: disables all pre-compiled prefabs across the entire game, forcing every movie to load via XML. Useful only for diagnostic debugging.
DumpGeneratedCode false Writes generated C# source code to Modules/Bannerlord.UIExtenderEx/CompiledPrefabs/Sources/<Movie>/<ViewModel>/<Prefab>.gen.cs for inspection.
DumpXML false Writes the final patched XML of every modified movie to Modules/Bannerlord.UIExtenderEx/Dumps/<movie>_<module>.xml.
RecordTimings false Writes how long each movie took to open, and the compile, load and warm-up times behind it, to CompiledPrefabs/Timings/, one file per session. For investigating load times; recorded with compiled prefabs off as well, as a baseline.

ViewModel Mixins under Compiled Prefabs

ViewModel mixins work identically under compiled prefabs: annotate your class with [ViewModelMixin], derive from BaseViewModelMixin<TViewModel>, and expose members using [DataSourceProperty] and [DataSourceMethod].

Under compiled prefabs, member access is optimized:

  • Generation-Time Resolution: Bindings to mixin properties or methods are resolved during code generation against all registered mixins for the exact target ViewModel type.
  • Direct Mixin Access: The generated C# retrieves the mixin instance via Bannerlord.UIExtenderEx.ViewModels.ViewModelMixins.Get<TMixin>(vm) and invokes its members directly. If an instance lacks the mixin, it safely evaluates to the member type's default value rather than throwing an exception.
  • Strict Attribute Requirement: Only properties and methods decorated with [DataSourceProperty] and [DataSourceMethod] are visible to the code generator, matching runtime mixin rules.
  • Internal Accessibility: Your mixin classes and custom ViewModels do not need to be public. UIExtenderEx configures IgnoresAccessChecksTo so the generated assembly can bind to internal types and members.
  • Automatic Invalidation: Enabling, disabling, or deregistering a mixin automatically invalidates the fingerprint for all movies whose ViewModel hierarchy touches that mixin.

Replacing a Prefab by Shipping Your Own XML

If your mod includes a file like GUI/Prefabs/InitialScreen.xml, it replaces Native's copy because Gauntlet's resource manager resolves paths relative to each module's GUI folder, giving precedence to the latest loaded module. Many UI reskin mods use this approach without writing any C# patches.

Because vanilla's pre-compiled InitialScreen was generated from Native's original XML, the game would normally ignore your replacement file. UIExtenderEx detects when multiple modules provide a prefab of the same name and treats those movies as modified, routing them through the compilation pipeline.

Note

Disabled backup files (such as InitialScreen.xml_dev or InitialScreen.xml.bak) are ignored by both the game and UIExtenderEx. Only valid .xml files are registered.


Binding to a Property the Declared Type Does Not Have

Gauntlet's dynamic XML loader resolves @PropertyName bindings at runtime using ViewModel.GetPropertyValue(name). Because it performs lookup by string, it does not care about compile-time types. Two common modding patterns rely on this dynamic behavior:

  1. ViewModel Mixins: Adding properties and commands dynamically to vanilla ViewModels.
  2. Polymorphic Collections: Placing custom derived ViewModel subclasses into a collection declared with a base type. For example, CharacterDeveloperVM.CharacterList is a SelectorVM<SelectorItemVM>. A mod can populate it with instances of MySelectorItemVM : SelectorItemVM and bind an item template widget to IsVisible="@HasCustomPerk".

Standard ahead-of-time code generators cannot resolve properties missing from the declared base type and will drop those bindings.

UIExtenderEx solves this by generating dynamic by-name bindings for any member that cannot be resolved on the declared type. The generated code queries the ViewModel's binding table and executes commands via ViewModel.ExecuteCommand, mirroring XML loader flexibility while preserving direct typed access for all standard bindings.

Rules and Limitations for Dynamic Bindings

  • Bound Properties Must Be Public: Gauntlet's GetPropertyValue uses MethodInfo.GetGetMethod(), which returns null for non-public getters. If a property is not public, neither the XML loader nor the compiled prefab can bind to it. Non-public setters cannot be written back.
  • Missing Members Fall Back to Default Values: If a binding references a property that does not exist on a runtime instance, value-typed properties take their default value (e.g., boolean flags evaluate to false), matching XML behavior. UIExtenderEx logs a diagnostic trace line when this occurs.
  • Indexed List Paths (DataSource="{Items\0}"): Under compiled prefabs, indexing into a list dynamically tracks whichever item is currently at that index when the list changes.
  • Unrecognized XML Tags: Any XML element that is neither a registered widget class nor a prefab is instantiated as a generic Widget (matching XML loader fallback). This is flagged at build time by UIX0029.

Where a Compiled Movie Differs from XML

UIExtenderEx compiled prefabs match XML loader behavior, but intentionally resolve several long-standing engine quirks and bugs where the XML loader leaves stale data or crashes unexpectedly:

  • Child ViewModel Updates: In compiled prefabs, replacing a child ViewModel updates all widgets bound to it. In the XML loader, only widgets bound directly via a single-step DataSource="{Child}" refresh (UIX0026).
  • Logical Children Parameter Scoping: In compiled prefabs, *Parameter references on children passed into a <LogicalChildrenLocation /> correctly read the parameters of the declaring prefab (UIX0027).
  • Type Incompatibilities: If a bound value cannot be converted to the widget property's type, the compiled prefab skips the initial assignment and retains the widget's XML default value instead of throwing an exception. However, any subsequent property change notification carrying an incompatible value will still throw, matching the XML loader's behavior (UIX0025).
  • Widget Recycling and Gamepad Navigation: When list items update without visual changes, compiled prefabs avoid rebuilding widget instances unnecessarily, preserving gamepad focus and active animation states.
  • Widget Tags: Widget.Tag is initialized to null (matching vanilla pre-compiled prefabs) rather than generating a new Guid on every parse.

For a full technical analysis of these differences, see Deviations from the XML Loader.


Keep Patch Fragments Out of GUI/Prefabs

Important

If you load patch fragments from XML files using [PrefabExtensionFileName], do not store them inside GUI/Prefabs/.

Why This Matters

A patch fragment is an incomplete XML snippet whose root element is the widget being inserted (such as <Widget> or <DummyRoot>), rather than a full <Prefab> document with a <Window> root.

The game's WidgetFactory automatically registers every .xml file inside GUI/Prefabs/ as an independent prefab. When the code generator encounters these fragment files, TaleWorlds' internal parser throws an unhandled NullReferenceException. While UIExtenderEx catches this and falls back to XML, the affected movie will not be able to use compiled prefabs.

Place your patch fragments in a dedicated folder under GUI/ that is not named Prefabs. For example:

MyMod/
└── GUI/
    ├── Prefabs/             <-- Full standalone prefabs only
    │   └── MyCustomScreen.xml
    └── PrefabExtensions/   <-- Patch fragments go here!
        └── CustomButtonPatch.xml

[PrefabExtensionFileName] searches your entire GUI/ directory recursively by filename, so your patch will find the file without triggering premature registration in WidgetFactory.


Widget Classes and Prefabs Registered at Runtime

The game builds its internal widget registry once at startup from assemblies loaded at that moment. Custom widget classes loaded later or prefabs constructed dynamically in memory are unknown to WidgetFactory.

You can register dynamic widgets and in-memory prefabs through WidgetFactoryManager:

using Bannerlord.UIExtenderEx.ResourceManager;

// Register a custom widget class from an assembly loaded after startup
WidgetFactoryManager.Register(typeof(MyCustomWidget));

// Register an in-memory XML document as a prefab
WidgetFactoryManager.CreateAndRegister("MyGeneratedPrefab", xmlDocument);

// Register a factory delegate called on first use or upon reload
WidgetFactoryManager.Register("MyGeneratedPrefab", () => WidgetFactoryManager.Create("MyGeneratedPrefab", BuildDocument()));
  • Registration Timing: Register custom widgets before the movie that references them loads (for instance, in your SubModule or screen constructor). If an unregistered widget name is encountered, Gauntlet falls back to a base Widget and logs a warning.
  • Overriding Existing Prefabs: Registering a runtime prefab whose name matches a vanilla prefab replaces the vanilla prefab for both XML loading and compilation.

Changing a Prefab You Registered at Runtime

When you register a prefab using a factory delegate via WidgetFactoryManager.Register(name, () => ...), UIExtenderEx caches the generated result as long as any screen references it.

Modifying the underlying data or document will not automatically update screens that have already parsed the prefab. To force an update:

WidgetFactoryManager.ReloadOnNextUse(new[] { "MyGeneratedPrefab" });

This clears the parsed cache and invalidates the compiled assembly for any movie embedding that prefab. The next time the movie loads, your factory method is called again and a fresh assembly is compiled.


Constants That Read a Sprite or a Brush

In Gauntlet XML, constants can measure the dimensions of a sprite or brush layer:

<Constants>
  <Constant Name="Icon.Width" SpriteName="General\Icons\Coin" SpriteValueType="Width" />
  <Constant Name="Panel.Width" BrushName="Standard.Panel" BrushLayer="Default" BrushValueType="Width" />
  <Constant Name="Icon.WidthWithGap" Value="!Icon.Width" Additive="8" />
</Constants>

TaleWorlds' ahead-of-time compiler evaluates these constants on the developer machine and hardcodes the numbers into C#, meaning pre-compiled prefabs ignore texture packs or reskins that alter sprite dimensions.

UIExtenderEx handles these constants dynamically:

  • SpriteWidth, SpriteHeight, BrushLayerWidth, BrushLayerHeight, and any math derived from them are resolved dynamically when the widget is constructed against the active UIContext.
  • If a player installs a custom texture pack or UI reskin, widgets automatically adapt to the updated asset dimensions without requiring recompilation.

Enabling, Disabling and Deregistering

When you toggle extensions using UIExtender.Enable, UIExtender.Disable, or UIExtender.Deregister():

  • Prefab Patches: UIExtenderEx marks all affected prefabs for re-parsing. The next time a screen opens, it loads with the updated patch set. Screens currently open remain unchanged until closed and reopened.
  • ViewModel Mixins: Active mixin member signatures change, invalidating the cached fingerprint for affected movies. Those movies fall back to XML for one open while a new assembly compiles in the background.

You do not need to manually flush caches or trigger reloads; UIExtenderEx manages invalidation automatically.


Which Changes Rebuild a Movie

UIExtenderEx generates a SHA-256 fingerprint for each movie based on:

  1. The patched XML content of all prefabs in the movie's hierarchy.
  2. The resolved widget classes and Harmony patches applied to those widgets.
  3. The root ViewModel type and all active mixins attached to reachable ViewModels.
  4. A generation hash of UIExtenderEx's compiler assemblies and the game's UI assemblies.

All other assemblies are excluded from this fingerprint. Instead, each build tracks the specific assemblies it actually depends on, and the cached build is invalidated if any of those dependencies change. See The Prefab Fingerprint and Assembly Dependency Tracking.

Recompilation Triggers

A movie is automatically recompiled when:

  • You modify your prefab patch, XML fragment, or replacement XML file.
  • You rebuild an assembly the compiled movie depends on, such as one containing its mixins or custom widget classes.
  • Another mod alters a prefab, widget class, or mixin used by the movie.
  • A mixin or patch reaching the movie is enabled, disabled, or deregistered.
  • The game or UIExtenderEx is updated.

A movie is not rebuilt when unrelated screens open, unrelated mods register extensions, a library the movie does not depend on (such as Harmony) is updated, or texture packs modify sprite dimensions.


Verifying and Troubleshooting

All compiled prefabs and build diagnostics are stored in Modules/Bannerlord.UIExtenderEx/CompiledPrefabs:

Path Description
CompiledPrefabs.zip The primary cache containing compiled assemblies and cache.txt metadata.
Sources/<Movie>/<ViewModel>/ Generated C# source files (.gen.cs), generated when DumpGeneratedCode is enabled.
Failed/<Movie>/<ViewModel>/ Contains errors.txt explaining why a movie failed generation or compilation. Cleared automatically upon a successful build.
Timings/<yyyyMMdd-HHmmss>-<pid>.tsv With RecordTimings, one tab-separated file per session: time, thread, event (load movie, decide, compile, load assembly, register, preload, warm-up generator, warm-up compiler), movie, variant, ms, detail. At most 5000 lines a session; the 10 newest sessions are kept. Starting the cache over leaves it alone.

How to Verify That Your Movie Is Compiled

  1. Open the target screen in-game once, close it, and open it a second time.
  2. Check Modules/Bannerlord.UIExtenderEx/CompiledPrefabs/CompiledPrefabs.zip. A .dll matching <Movie>/<ViewModel>.dll should be present inside the zip archive.
  3. If compilation failed, inspect Modules/Bannerlord.UIExtenderEx/CompiledPrefabs/Failed/<Movie>/<ViewModel>/errors.txt.

Key Diagnostic Trace Messages

UIExtenderEx logs detailed diagnostics through System.Diagnostics.Trace (accessible in debug logs or ButterLib crash reports):

  • using compiled prefab '<movie>' for '<ViewModel>': Confirms the screen successfully loaded a compiled C# variant.
  • prefab '<movie>': <n> ms on the main thread, <outcome>: Explains the exact decision path taken for the movie load.
  • the build of '<movie>' for '<ViewModel>' depends on an assembly that changed: A dependency DLL was updated, prompting an automatic rebuild.
  • Failed to generate code for prefab '<movie>', falling back to XML: Indicates generation failed; check the Failed/ directory for full diagnostics.
  • compiled prefab binding '<name>' found no member on <RuntimeType>: A dynamic binding could not resolve the property on the target instance at runtime.

Forcing a Cache Reset

To force a full rebuild of all compiled prefabs, close the game and delete Modules/Bannerlord.UIExtenderEx/CompiledPrefabs/CompiledPrefabs.zip.

Shipping Pre-Compiled Caches with Mod Packs

Curated mod packs that lock specific game and mod versions can distribute a pre-warmed CompiledPrefabs.zip placed in their module root (Modules/<ModPackModule>/CompiledPrefabs.zip). UIExtenderEx will load matching builds directly, saving players from initial background compilations.


Known Limitations

  • Self-Contained Compiler: Roslyn and its dependencies are embedded directly inside Bannerlord.UIExtenderEx.Compiler.dll. It has zero external dependencies, works across all game distributions (Steam, GOG, Epic, Xbox Game Pass), and avoids version conflicts with other mods.
  • First-Open Background Compilation: The first time a modified screen opens in a new setup, it loads via XML while compilation runs in the background (from under 0.1 seconds for a small screen to about 1.6 seconds for the largest; see measured timings). Subsequent opens are instantaneous.
  • Build-Specific Cache: Compiled assemblies are pinned to exact assembly MVIDs and game builds. Individual mods should not distribute CompiledPrefabs.zip, as the cache will not match end users' distinct environments.

This page was last modified at 10/05/2026 13:34:58 +03:00 (UTC).

Commit Message
Author:    Vitalii Mikhailov
Commit:    f3b725a97c3f299c20e56b5f206e27142ec15edd
* Added C# Auto-Generated XML prefab compilation for patched and dynamic movies, matching vanilla UI performance

* Added name-based property binding fallback for ViewModel subtypes not declared on the parent
* Improved unparseable prefab error logging with name and file details
* Fixed game crash with 3-segment attribute paths
* Added Bannerlord.UIExtenderE...