Compiled Prefabs: Architecture
Note
This article is written for UIExtenderEx maintainers and core contributors. Mod authors looking for general usage guidelines and troubleshooting should read Compiled Prefabs in the General section instead.
This document details the architectural pipeline in UIExtenderEx (introduced in v3.0.0) that intercepts Gauntlet UI movie loading, transforms patched prefab XML into C# at runtime, compiles it in the background, caches the resulting assemblies, and registers the generated widgets with the game engine.
For why the assemblies are split as they are, see Why the split is shaped this way.
Architecture Overview
The Problem
TaleWorlds' Gauntlet UI framework supports two mechanisms for instantiating UI movies:
- XML Prefab Loader: Reads and parses prefab XML files from disk at runtime.
- Pre-compiled C# Prefabs: Executes pre-compiled C# widget classes registered in
GeneratedPrefabContextfor specific(Movie, ViewModel)pairs.
When a pre-compiled widget class exists for a given movie and ViewModel type, GauntletMovie.Load instantiates it directly and bypasses the XML on disk entirely.
Because UIExtenderEx's primary objective is to modify prefab XML (via inserts, replacements, attribute adjustments, and ViewModel mixin bindings), the pre-compiled classes would completely ignore modded changes.
The Historical Solution (Pre-v3.0.0)
In versions prior to v3.0.0, UIExtenderEx solved this issue aggressively by setting UIConfig.DoNotUseGeneratedPrefabs = true globally and patching the configuration setter. This forced all UI movies through the XML loader. While this ensured all mod patches were applied, it introduced noticeable UI load stutter across the entire game.
The Modern Pipeline (v3.0.0+)
Starting with v3.0.0, UIExtenderEx uses a dynamic, per-load evaluation strategy:
- Untouched Movies: If a movie and all prefabs it inlines remain untouched by any patch or override, the game's original pre-compiled C# class is preserved for maximum performance.
- Patched or Subclassed Movies: If a movie is patched, uses an overridden XML file, or is bound to an unsupported/subclassed ViewModel, UIExtenderEx:
- Serves the first load via the XML loader to prevent blocking the UI thread.
- Generates equivalent C# source code from the patched XML using an alternative code generator implementation.
- Compiles the generated source asynchronously on a background worker thread using an embedded Roslyn compiler.
- Caches the compiled assembly to disk (
CompiledPrefabs.zip). - Registers the newly generated prefab creator into
GeneratedPrefabContext, replacing the game's variant for all subsequent loads.
Prefab Runtimes Architecture
To separate concerns and preserve extensibility, UI loading strategies are decoupled into modular prefab runtimes.
The core assembly (Bannerlord.UIExtenderEx) manages mixin registries, applies XML patches, and installs the interception hook in GauntletMovie.Load (GauntletMoviePatch). Any component that serves a movie through a mechanism other than the default XML loader implements IPrefabRuntime and registers with the core via Bannerlord.UIExtenderEx.Runtimes.PrefabRuntimes.
| Runtime | SubModule Assembly | Responsibility |
|---|---|---|
| XML Runtime | Bannerlord.UIExtenderEx.XmlPrefabs |
Delegates directly to the game's GauntletMovie XML loader while applying core bug fixes (such as dotted attribute path resolution in WidgetExtensionsPatch and template child deduplication in WidgetTemplatePatch). Publishes runtime loader capabilities via PrefabRuntimes.DottedAttributePathsResolve. |
| Game Runtime | Bannerlord.UIExtenderEx.GamePrefabs |
Preserves and serves the game's native pre-compiled C# variants when no mod patches, file overrides, or runtime registrations affect any prefabs in the movie's dependency tree. Verifies TaleWorlds naming conventions prior to trusting inlined prefabs. |
| Compiled Runtime | Bannerlord.UIExtenderEx.CompiledPrefabs |
Orchestrates runtime code generation, Roslyn background compilation, assembly caching, and creator registration for modified or newly introduced prefabs. |
If no specialized runtime is active or claims a given movie, GauntletMovie.Load defaults to the standard XML loader. Consequently, each registered runtime acts strictly as a performance optimization; correctness and patch application are guaranteed even if a runtime fails or is disabled.
Assembly Structure & Responsibilities
The codebase enforces a strict inward dependency model: runtime assemblies depend on the core assembly, but the core assembly never references any runtime assembly directly. This decoupling is verified by automated architecture tests (RuntimeSplitTests).
┌─────────────────────────────────────────┐
│ Bannerlord.UIExtenderEx │
│ (Core) │
└────▲───────────────▲───────────────▲────┘
│ │ │
┌───────────────┴───┐ ┌───────┴───────┐ ┌───┴────────────────────────┐
│ XmlPrefabs │ │ GamePrefabs │ │ CompiledPrefabs │
│ (Runtime) │ │ (Runtime) │ │ (Runtime) │
└───────────────────┘ └───────────────┘ └────▲──────────────────▲────┘
│ │
┌────────────┴─────┐ ┌────────┴────┐
│ CodeGenerator │ │ Compiler │
└──────────────────┘ └─────────────┘
| Assembly | Root Namespace | Target Frameworks | Responsibilities |
|---|---|---|---|
Bannerlord.UIExtenderEx |
Bannerlord.UIExtenderEx |
netstandard2.0 |
Public API for mod authors, prefab patch execution, ViewModel mixin registry, WidgetFactoryManager, configuration settings, runtime switch hook (GauntletMoviePatch), and the runtime host surface (Bannerlord.UIExtenderEx.Runtimes). |
Bannerlord.UIExtenderEx.XmlPrefabs |
Bannerlord.UIExtenderEx.XmlPrefabs |
netstandard2.0 |
XmlPrefabRuntime implementation along with two critical engine fixes: WidgetExtensionsPatch (resolves dotted attribute property paths correctly) and WidgetTemplatePatch (prevents duplicate template allocations in _customTypeChildren). |
Bannerlord.UIExtenderEx.GamePrefabs |
Bannerlord.UIExtenderEx.GamePrefabs |
netstandard2.0 |
GamePrefabRuntime, PrefabNames, and PrefabOverrideRegistry. Tracks TaleWorlds pre-compiled variants and determines whether prefabs remain pristine. |
Bannerlord.UIExtenderEx.CompiledPrefabs |
Bannerlord.UIExtenderEx.CompiledPrefabs |
net472net6.0 |
CompiledPrefabRuntime, CompiledPrefabManager, fingerprint calculation, dependency tracking, cache management, GeneratedPrefabContextPatch, and host interface implementations for the code generator. |
Bannerlord.UIExtenderEx.CodeGenerator |
Bannerlord.UIExtenderEx.GauntletUI.CodeGenerator |
netstandard2.0 |
Alternative implementation of the Gauntlet UI code generator with UIExtenderEx enhancements (mixin bindings, fallback dynamic member access via Bannerlord.UIExtenderEx.GauntletUI.CodeGenerator.Runtime.DynamicMember). Emits C# source code. |
Bannerlord.UIExtenderEx.Compiler |
Bannerlord.UIExtenderEx.CompiledPrefabs.Compilation |
net472net6.0 |
Roslyn compiler wrapper (ICSharpCompiler, RoslynCompiler), assembly publicization (ReferencePublicizer), access check suppression (IgnoresAccessChecksSource), metadata inspection (PrefabAssemblyReference), and VectorsBinding. Uses ILRepack to internalize Roslyn dependencies. |
Note
Bannerlord.UIExtenderEx.Module is a packaging project rather than a loaded assembly; it references the required projects to output the final module layout for distribution.
Why the split is shaped this way
- The compiled runtime is isolated from the core framework: The core is the public API package (
netstandard2.0, single DLL), validated against previous releases on every build. It must build, load, and function independently without any specialized runtime present (providing slow but reliable XML fallback). The compiler assembly targetsnet472/net6.0and bundles 12 MB of Roslyn; because it is the component most susceptible to antivirus quarantine or missing installation files, failure is contained strictly to the compiled runtime without impacting core mod features. - The game runtime resides in its own assembly: It is the only assembly that relies on internal TaleWorlds generator conventions (
_generatedPrefabs, class-name heuristics), none of which are public APIs. If a game update alters these conventions,ConventionsHoldcleanly turns off the runtime. Even withoutBannerlord.UIExtenderEx.GamePrefabs.dll, all movies continue opening with their patches applied via XML or compiled builds. - Mixin hooks remain in the core, not the XML runtime:
ViewModelWithMixinPatchserves both runtimes. Generated code accesses mixin instances viaViewModelMixins.Get<T>(backed byMixinInstanceCache), andDynamicMemberbinds through the same per-instance table (ViewModelSource) used byGauntletView. Because ViewModels are instantiated before the engine determines which runtime will render the movie, mixin state must remain globally available across XML fallbacks, background compiles, and third-party mod queries.
Target Frameworks & Game Runtime Compatibility
Mount & Blade II: Bannerlord runs on two different CLR environments depending on the distribution platform:
- Steam / GOG / Epic Games: Runs on the desktop .NET Framework 4.7.2 CLR (reported internally by TaleWorlds as
ApplicationPlatform.CurrentRuntimeLibrary == RuntimeLibrary.Mono). Binaries reside inbin/Win64_Shipping_Client. - Microsoft Store / Xbox App (GDK): Runs on .NET 6 (
RuntimeLibrary.DotNetCore). Binaries reside inbin/Gaming.Desktop.x64_Shipping_Client.
Bannerlord.BuildResources automatically outputs the appropriate build targets into their corresponding game directories:
| Target Framework | Build Property | Target Game Folder |
|---|---|---|
net472 |
BuildForWindows |
bin/Win64_Shipping_Client |
net6.0 |
BuildForWindowsStore |
bin/Gaming.Desktop.x64_Shipping_Client |
Because the Roslyn compiler engine requires platform-specific dependencies, Bannerlord.UIExtenderEx.Compiler and Bannerlord.UIExtenderEx.CompiledPrefabs are multi-targeted for both net472 and net6.0. All other assemblies (Core, XmlPrefabs, GamePrefabs, and CodeGenerator) target netstandard2.0 and share a single unified build.
Host Interfaces and the Runtimes API
Code Generator Isolation
The code generator (Bannerlord.UIExtenderEx.CodeGenerator) has no compile-time dependencies on the compiled runtime or the core game assemblies. It declares abstract host interfaces for all external services it requires. During startup (CompiledPrefabRuntime.Install), the compiled runtime injects concrete implementations:
| Generator Interface | Implemented By (CompiledPrefabs) |
Registration Point | Purpose |
|---|---|---|---|
IWidgetRegistrations |
WidgetFactoryRegistrations |
CodeGeneratorEnvironment.Registrations |
Resolves custom widget types registered via WidgetFactoryManager. |
IViewModelMemberResolver |
MixinMemberResolver |
ViewModelMemberResolution.Resolver |
Resolves properties and methods introduced by ViewModel mixins. |
IDynamicMemberHost |
DynamicMemberHost |
DynamicMember.Host |
Supplies runtime reflection and binding fallback when properties are not statically declared. |
ICompiledPrefabEnvironment |
GameCompiledPrefabEnvironment |
Passed to CompiledPrefabManager.Create(...) |
Encapsulates game environment interactions, settings, and creator class instantiation. |
Core Runtime APIs (Bannerlord.UIExtenderEx.Runtimes)
The core assembly exposes the runtime environment through dedicated query sources:
IPrefabRuntime&PrefabRuntimes: Handles runtime registration, priority queries (TryServe), ownership checks (IsOwnVariant), and synchronization flags.PrefabSource: Central event hub for prefab parsing (Parsed), registrations (Registered), reload notifications (ReloadRequested), and environment modifications (EnvironmentChanged). Also exposesAddWidgetTypesto make late-loaded widget types visible to Gauntlet's internal widget registry. Exceptions in event handlers are safely logged without interrupting game execution.MixinSource: Provides ordered access to registered and active mixins for any target ViewModel type.ViewModelSource: Exposes property and command resolution logic matching active ViewModel instances.RuntimeSettings: Unified settings provider for runtime configuration options (CompiledPrefabs,DumpGeneratedCode,RecordTimings).
Environmental Synchronization Flags
Two critical environment settings operate outside interface abstractions:
CodeGeneratorEnvironment.DottedPathsResolveCorrectly: Synchronized withPrefabRuntimes.DottedAttributePathsResolve. This is validated at startup and before every served load because third-party Harmony patches onWidgetExtensions.GetObjectAndPropertycan potentially overwrite or revert UIExtenderEx's transpiler fix. The generator must replicate the exact path resolution behavior of the active loader. This flag is also factored into prefab fingerprints.CodeGeneratorEnvironment.IsPatched: Set by the compiled runtime when Harmony patches are detected on widget property notification methods. The code generator analyzes method IL to discover change notifications; detecting patches ensures that modified IL does not produce invalid notification bindings.
When running standalone unit tests, these environment hooks default to safe no-op or empty implementations.
Runtime Dependencies & Isolation
UIExtenderEx requires only a single external runtime dependency: Bannerlord.Harmony.
All other external dependencies are compiled into source packages (Bannerlord.BUTR.Shared, Bannerlord.ModuleManager.Source, Harmony.Extensions, BUTR.MessageBoxPInvoke) or bundled directly into the assemblies:
- Bundled Compiler: Roslyn and its required support libraries are merged into
Bannerlord.UIExtenderEx.Compilerusing ILRepack with full type internalization. This isolates Roslyn completely from other mods and prevents assembly binding redirects or version collisions. ButterLib is not required. - Fault-Tolerant Loading: Bannerlord invokes
Assembly.GetTypes()on each SubModule assembly prior to instantiation and treats anyReflectionTypeLoadExceptionas a critical failure. Consequently, no type inBannerlord.UIExtenderEx.CompiledPrefabs.dllmay inherit from, implement, or expose types fromBannerlord.UIExtenderEx.Compiler.dllin its public type signatures. If the compiler assembly is missing (e.g. due to antivirus quarantine or partial installation), the runtime installs in a disabled state, allowing patched movies to fall back gracefully to XML (CompilerMissingTestsverifies this across both frameworks). If a runtime SubModule DLL is missing altogether, the engine displays a standard warning dialog and continues execution without that runtime. - Generated Code References: Generated assemblies reference
Bannerlord.UIExtenderEx.CodeGenerator(for runtime dynamic member dispatch) andBannerlord.UIExtenderEx(forViewModelMixins.Get<T>). Both assemblies are explicitly registered as compilation references. - Generation Invalidation: The cache archive generation hash (
PrefabFingerprint.ComputeGeneration) combines the Module Version IDs (MVIDs) ofCompiledPrefabs,CodeGenerator,Compiler, and the core TaleWorlds Gauntlet and Library assemblies. Any update to the game or UIExtenderEx automatically invalidates stale cached assemblies.
Lifecycle & Startup Sequence
Note
For the broader framework lifecycle (including mod registration phases, enable/disable toggling, and teardown), see Core Runtime Lifecycle.
The module's SubModule.xml registers four SubModules in strict execution order:
Bannerlord.UIExtenderEx(Core)Bannerlord.UIExtenderEx.XmlPrefabs(XML Runtime)Bannerlord.UIExtenderEx.GamePrefabs(Game Runtime)Bannerlord.UIExtenderEx.CompiledPrefabs(Compiled Runtime)
The TaleWorlds engine instantiates all SubModules first, then calls their OnSubModuleLoad methods sequentially in the declared order:
[Start-up]
│
├─► 1. Core Static Constructor
│ Checks 'DisableGeneratedPrefabs'. If true, enables UIConfig.DoNotUseGeneratedPrefabs.
│
├─► 2. Runtime Constructors
│ - XmlPrefabs: Applies WidgetExtensionsPatch & WidgetTemplatePatch.
│ - GamePrefabs: Registers with PrefabRuntimes; monitors pristine game variants.
│ - CompiledPrefabs: Registers host interfaces, hooks PrefabSource events,
│ patches GeneratedPrefabContext.CollectPrefabs, registers runtime.
│
├─► 3. Core OnSubModuleLoad
│ Executes UIExtender static constructor; applies GauntletMoviePatch hook
│ (skipped when DisableGeneratedPrefabs is on).
│
└─► 4. CompiledPrefabs OnSubModuleLoad
Initializes CompiledPrefabManager and schedules background warm-up worker
(cache preloading, JIT warming, throwaway Roslyn compile).
Detailed Startup Steps
- Core Static Initialization: Checks the
DisableGeneratedPrefabssetting. If enabled, it force-loadsTaleWorlds.Engine.GauntletUIand setsUIConfig.DoNotUseGeneratedPrefabs = true, disabling all pre-compiled prefabs globally. Any subsequent modifications toUIConfig.DoNotUseGeneratedPrefabs(via MCM or the developer console commandui.use_generated_prefabs) are captured byUIConfigPatchand persisted across sessions. - Runtime Construction:
XmlPrefabRuntimeapplies its XML loader patches.GamePrefabRuntimeregisters withPrefabRuntimesand begins tracking TaleWorlds variant collections.CompiledPrefabRuntimeinstalls generator host implementations, subscribes toPrefabSourceevents, installsGeneratedPrefabContextPatch(to re-register cached variants after an engine resource refresh), and registers withPrefabRuntimes. Because runtimes are queried in registration order, the Game runtime is checked first, ensuring unpatched prefabs are never unnecessarily compiled.
- Core Patch Application:
OnSubModuleLoadin the core assembly triggers the static constructor ofUIExtender, immediately applying the Harmony patches (includingGauntletMoviePatch). This ensures the load switch is active before any game UI screen can open. WhenDisableGeneratedPrefabsis on, this step is skipped: no generated prefab is used, so nothing needs the switch early, and the patches are applied on the first access toUIExtender, normally a mod'sUIExtender.Create. - Compiled Runtime Warm-up:
OnSubModuleLoadcreates the singletonCompiledPrefabManagerviaCompiledPrefabManager.Create(...). If environment validation fails (such as a missing compiler assembly), a fallbackDisabledCompiledPrefabEnvironmentis used, leaving all patched movies on the XML loader. If enabled, a background worker is dispatched to preload cached assemblies, JIT-compile critical code generator routines, and execute a warm-up Roslyn compilation (see Pipeline: Warm-up).
Life of a Movie Load
The diagram below illustrates the decision flow when GauntletMovie.Load is called:
sequenceDiagram
autonumber
participant Game as GauntletMovie.Load
participant Switch as GauntletMoviePatch (Core)
participant GameRt as GamePrefabRuntime
participant Mgr as CompiledPrefabManager
participant FP as PrefabFingerprint
participant Gen as PrefabCodeGenerator
participant Worker as Background Worker
Game->>Switch: LoadPrefix(movieName, dataSource, ref doNotUseGeneratedPrefabs)
Switch->>GameRt: TryServe(movieName, dataSource)
Note over GameRt: Check if movie or inlined dependencies<br/>are touched by patches/overrides
alt Movie is pristine (unmodified)
GameRt-->>Switch: true (use game's pre-compiled class)
else Movie is modified or custom
GameRt-->>Switch: false
Switch->>Mgr: TryServe(movieName, dataSource)
Mgr->>Mgr: DrainFinished() (register finished background builds)
Mgr->>FP: Compute fast fingerprint
alt Cached assembly matches fingerprint
Mgr-->>Switch: true (use compiled variant)
else Compilation pending or previously failed
Mgr-->>Switch: false (fallback to XML loader)
else Build required
Mgr->>FP: BeginSnapshot() (pin prefab XML tree)
Mgr->>Gen: GenerateSources(snapshot)
Mgr->>FP: CollectInputs() (compute full build hash)
Mgr->>Worker: Queue CompileInBackground(sources, refs, fingerprint)
Mgr-->>Switch: false (fallback to XML loader for this load)
Worker->>Worker: Roslyn compile, write cache, load assembly
Worker->>Mgr: Enqueue completed build
end
end
Switch-->>Game: doNotUseGeneratedPrefabs = !servedByRuntime
Post-Load Worker Draining
When a background compilation job finishes on the worker thread, its result is pushed to a thread-safe queue. On the next movie load request (for any movie), DrainFinished executes on the main thread and registers the newly compiled creator into GeneratedPrefabContext. The next time that specific movie and ViewModel pair is loaded, TryServe immediately finds the registered creator and serves the compiled class.
Threading Model
To ensure thread safety and avoid race conditions with Gauntlet UI's single-threaded architecture, execution responsibilities are partitioned across threads:
Main Thread (UI Thread)
- All
GauntletMovie.Loadinterception logic and runtime selection. - Prefab XML snapshotting and code generation (
PrefabCodeGenerator.GenerateSources). - Draining completed worker results and registering creators in
GeneratedPrefabContext. - Mixin resolution and dynamic member reflection dispatch.
Background Worker Threads
- Roslyn Compilation:
CompileInBackgroundinvokes the C# compiler asynchronously. - Assembly Loading: Executing
Assembly.Loadoff the UI thread avoids UI stutters caused by third-party mod assembly-load listeners (a load took 90–210 ms in a modded game, against 1–15 ms for the load itself; see Off-Thread Assembly Loading). - Disk I/O: Reading and writing
.dllfiles toCompiledPrefabs.zipand flushing the cache index (PrefabCache.Flush). - Initial Warm-up: Preloading cached assemblies and pre-compiling code generator methods.
Only immutable data structures (source text strings, reference file paths, and type dependency descriptors) cross the boundary to worker threads. The prefab XML snapshot is released on the main thread as soon as code generation finishes.
Naming Conventions & Identifiers
Generated assemblies and types follow deterministic naming rules:
- Root Namespace:
Bannerlord.UIExtenderEx.AutoGenerated. All generated assemblies share this prefix. The prefix is checked byIsGeneratedAssemblyandIsOwnVariantto ensure generated assemblies are never treated as external compilation references. - Assembly Name: Formatted as
Bannerlord.UIExtenderEx.AutoGenerated.<Movie>.<ViewModelName>.<BuildTagHex>, where<BuildTagHex>represents the first 16 hexadecimal characters of the build tag hash (combining the input fingerprint, inspected types, and reference assembly MVIDs). This ensures a rebuilt variant receives a distinct assembly identity even if the old version remains in memory. - Root Widget Class: Formatted as
<Prefab>__<Variant>, where<Variant>is the sanitized full name of the ViewModel. Names are normalized usingGeneratedNaming.GetUsableName: dots become underscores, and characters invalid in C# identifiers (such as+for nested classes or hyphens in filenames) are replaced with their hexadecimal unicode values delimited by underscores (e.g.,_002B_). - Inlined Dependency Classes:
- Standard child prefab:
<Root>_Dependency_<Index>_<Prefab>__DependendPrefab. - Inherited base prefab:
<Root>_Dependency_<Index>_<Prefab>__InheritedPrefab. - List item template:
<Root>_Dependency_<Index>_<Identifier>(without suffix).
- Standard child prefab:
- Prefab Creator Class:
GeneratedUIPrefabCreatorcontainingCollectGeneratedPrefabDefinitions(GeneratedPrefabContext). Bound by name dynamically viaGameCompiledPrefabEnvironment.CreateCreator. It intentionally does not implementIGeneratedUIPrefabCreatordirectly to prevent Gauntlet's eager assembly scanner from registering it prematurely.
Related Documentation
- Pipeline: Detailed breakdown of the load decision tree,
CompiledPrefabManager, fingerprint calculations, and cache serialization. - Code Generator: Alternative code generator implementation details, mixin binding generation, and by-name binding fallback strategies.
- Handling Differences: Managing structural and schema discrepancies in Gauntlet prefabs.
- Deviations from XML: Documented differences between XML loader behavior and compiled C# execution.
- Compilation: Deep dive into Roslyn bundling, ILRepack internalization, reference publicizing, and access check suppression.
- Testing: Architectural test suites, runtime verification harnesses, and benchmark suites.
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...