Core Architecture: Overview
Note
This article is written for UIExtenderEx maintainers and core contributors. Mod authors looking for general usage guidelines and tutorials should start with the General Overview and the API documentation instead. For details on the runtime compilation pipeline that compiles UI movies into C# assemblies, see Compiled Prefabs: Architecture.
The core assembly, Bannerlord.UIExtenderEx.dll, provides the foundational infrastructure of UIExtenderEx. It manages the registration of mod UI extensions, installs the Harmony patches required to intercept and extend TaleWorlds' Gauntlet UI engine, coordinates prefab XML modifications and ViewModel mixins, and exposes the runtime hosting APIs used by specialized prefab runtimes.
This section covers the core architecture across four dedicated articles:
- Overview: Architectural layers, global Harmony patches, the
Runtimeshosting seam, configuration, and threading models. - Lifecycle: Engine initialization, registration sequence, enabling/disabling extensions, and clean teardown.
- Prefab Patching: How XML patches are intercepted, applied, cached, and coordinated with the prefab system.
- ViewModel Patching: How mixins, property bindings, method overrides, and accessor stubs are attached to ViewModels.
Tip
Disambiguating "Runtime": In this codebase, the term runtime has two distinct meanings:
UIExtenderRuntime(Core Runtime): The internal per-module container managing a mod's registered patches, mixins, and activation states.- Prefab Runtime (
IPrefabRuntime): A movie loading strategy that determines how a UI movie is constructed—such as the game's native pre-compiled prefabs (GamePrefabs), UIExtenderEx's dynamic Roslyn-compiled prefabs (CompiledPrefabs), or the XML prefab parser (XmlPrefabs).
Architectural Layers: Process-Wide vs. Per-Module
To achieve maximum performance and avoid redundant hooks, the core divides its responsibilities into two distinct architectural layers:
Process-Wide Layer (Static & Global):
- Managed globally by static members on
UIExtender. - Initializes a single, shared Harmony instance (
bannerlord.uiextender.ex). - Pre-loads required TaleWorlds assemblies and installs global engine hooks (intercepting movie loading, prefab deserialization, brush/widget managers, etc.).
- Maintains an immutable, lock-free array of all active module runtimes (
UIExtender.GetAllRuntimes()). - Exposes the host
RuntimesAPI to pluggable prefab runtime assemblies.
- Managed globally by static members on
Per-Module Layer (Scoped & Isolated):
- Each mod obtains an isolated
UIExtenderinstance viaUIExtender.Create("ModId"). - Registering an extender creates an internal
UIExtenderRuntime, composed of two specialized components:PrefabComponent: Stores the mod's prefab patches, XPath selectors, and per-patch enable states.ViewModelComponent: Stores the mod's ViewModel mixins, method overrides, accessor stubs, and mixin enable states.
- When a global hook triggers, it iterates through all registered runtimes in registration order. If a mod does not target a particular movie or ViewModel, it incurs no processing overhead.
- Each mod obtains an isolated
classDiagram
direction TB
class UIExtender {
<<Public API>>
+Create(moduleName)$ UIExtender
+GetUIExtenderFor(moduleName)$ UIExtender
~GetAllRuntimes()$ UIExtenderRuntime[]
~Harmony$ Harmony
-Instances$ Dictionary
-_runtimes$ UIExtenderRuntime[]
-_runtime UIExtenderRuntime
+Register(Assembly)
+Register(IEnumerable)
+Enable()
+Disable()
+Enable(Type)
+Disable(Type)
+Deregister()
}
class UIExtenderRuntime {
<<Internal Coordinator>>
+ModuleName : string
+PrefabComponent : PrefabComponent
+ViewModelComponent : ViewModelComponent
+Register(types)
+Enable()
+Disable()
+Enable(Type)
+Disable(Type)
+Deregister()
}
class PrefabComponent {
<<Internal Prefab Manager>>
~MoviePatches : Dictionary
-_enabledPatches : ConcurrentDictionary
+RegisterPatch(movie, xpath, patch)
+GetMoviesToPatch()
+ProcessMovieIfNeeded(movie, XmlDocument)
}
class ViewModelComponent {
<<Internal ViewModel Manager>>
+Mixins : Dictionary
~MixinInstanceCache : ConditionalWeakTable
~MixinInstanceRefreshFromConstructorCache : ConditionalWeakTable
-_mixinTypeEnabled : ConcurrentDictionary
-_mixinOverrides : Dictionary
+RegisterViewModelMixin(type, refreshMethodName, handleDerived)
+InitializeMixinsForVMInstance(ViewModel)
~TryGetOverride(mixinType, method)
~GetEnabledMixinTypes(viewModelType)
}
UIExtender "1" *-- "0..1" UIExtenderRuntime : manages
UIExtenderRuntime "1" *-- "1" PrefabComponent : owns
UIExtenderRuntime "1" *-- "1" ViewModelComponent : owns
ABI Stability and Internal Encapsulation
UIExtender is the sole public-facing type in the core runtime registration pipeline. Its public methods (Create, Register, Enable, Disable, Deregister) define the binary interface (ABI) used by mod assemblies and must remain strictly backwards-compatible.
In contrast, UIExtenderRuntime, PrefabComponent, and ViewModelComponent are entirely internal. Their data structures, caching mechanisms, and method signatures can be refactored freely without breaking dependent mods.
Global Harmony Patches
During engine startup, UIExtender's static constructor initializes the global patching layer.
Assembly Pre-Loading
Because Harmony patch target resolution searches only among assemblies currently loaded in the AppDomain, and TaleWorlds loads UI assemblies on-demand (for example, TaleWorlds.GauntletUI.Data is not loaded until the first UI movie is requested), UIExtender first executes LoadPatchedAssemblies(). This method explicitly forces the loading of required assemblies:
TaleWorlds.LibraryTaleWorlds.GauntletUITaleWorlds.GauntletUI.PrefabSystemTaleWorlds.GauntletUI.DataTaleWorlds.Engine.GauntletUI
If any assembly fails to load, UIExtenderEx logs a warning and informs the user, while continuing to load remaining components.
Global Engine Patches
Once target assemblies are available, UIExtenderEx applies its primary suite of engine patches using a single shared Harmony instance (bannerlord.uiextender.ex):
| Patch | Target Method | Patch Type | Purpose |
|---|---|---|---|
GauntletMoviePatch |
GauntletMovie.Load |
Prefix | Movie Load Interception: Acts as the movie switch. Queries registered IPrefabRuntime instances (Game and Compiled—XmlPrefabs registers none) to determine whether a pre-compiled or compiled variant can serve the movie. Falls back to the game's XML loader if neither runtime claims it. See Pipeline. |
WidgetPrefabPatch |
WidgetPrefab.LoadFrom |
Transpiler + Reverse Patch | XML Patch Application: Transpiles the game's file loader to inject ProcessMovie, executing all registered mod XML patches immediately after the XML document is loaded from disk. Provides a reverse patch (LoadFromDocument) allowing prefabs to be built directly from mod-supplied XmlDocument instances. See Prefab Patching. |
ParsePatch |
ConstantDefinition.GetValue |
Transpiler | Culture Invariant Parsing: Forces decimal constants in prefab XML to parse using CultureInfo.InvariantCulture, preventing crashes and layout corruption on systems configured with comma decimal separators. |
ViewModelPatch |
ViewModel.ExecuteCommand |
Prefix | Internal ViewModel Extensions: Forwards commands to the instance an internal ViewModelWrapper wraps. See ViewModel Patching: Internal ViewModel Types. |
BrushFactoryManager |
BrushFactory..ctorBrushFactory.LoadBrushes |
Postfix | Custom Brush Registration: Injects runtime-registered brushes into the brush factory's internal lookup tables upon creation and whenever brushes are reloaded. |
WidgetFactoryManager |
WidgetFactory.GetCustomTypeWidgetFactory.IsCustomTypeWidgetFactory.CreateBuiltinWidgetWidgetFactory.GetWidgetTypesWidgetFactory.OnUnload |
Prefix / Postfix | Custom Widget & Prefab Serving: Intercepts widget factory queries to inject mod-registered custom widget types, custom prefabs, and handle resource reference counting and lifecycle cleanup. |
EventManagerPatch |
EventManager.OnWidgetDisconnectedFromRoot (1.3.12+)EventManager.UnRegisterWidgetForEvent (up to 1.3.11) |
Transpiler | Engine Leak Fix: Fixes engine bugs where widgets stay registered in the visual definition (and TweenPosition) update tables after they no longer belong there. From 1.3.12, widgets with a visual definition stay registered after disconnection from the UI root; up to 1.3.11, widgets whose visual definition is taken away stay registered, also after disconnection. |
UIConfigPatch |
UIConfig.DoNotUseGeneratedPrefabs (setter)UIConfig.SetUsingGeneratedPrefabs |
Postfix | Configuration Synchronization: Listens for changes to the game's pre-compiled prefab flag (triggered via code or console commands) and syncs the state with UIExtenderEx's persistent settings. |
Patch Resilience and Safe Execution
Global patches are installed through an internal TryPatch helper. Each patch is treated as an isolated unit:
- If a patch fails to apply (e.g. if an engine update alters a method signature), an in-game warning notification or trace diagnostic is emitted detailing which feature will be disabled.
- Failure of one patch does not prevent subsequent patches from installing.
- Transpilers locate instructions by pattern matching IL opcode sequences rather than assuming fixed instruction offsets. If a target pattern cannot be resolved, the original method body is left intact.
Per-Registration (Dynamic) Patches
In addition to the global patches installed once at startup, UIExtenderEx dynamically applies targeted Harmony patches as mods register mixins and extensions:
| Dynamic Patch | Application Trigger | Purpose |
|---|---|---|
ViewModelWithMixinPatch |
Applied once per ViewModel type targeted by a mixin |
Hooks constructors, OnFinalize, and the designated refresh method (RefreshValues or custom) to instantiate, refresh, and dispose attached mixin instances. |
ViewModelOverridePatch |
Applied once per ViewModel method targeted by a [BUTRViewModelOverride] |
Replaces the target method with a chain of delegates, allowing mixin methods to intercept, alter, or replace the original method execution. |
UnsafeAccessorPatch |
Applied once per [BUTRUnsafeAccessor] stub |
Transpiles stub methods into high-performance IL instructions (call, callvirt, ldflda, ldsflda) that bypass member accessibility checks. |
The Runtimes Hosting Seam
The Bannerlord.UIExtenderEx.Runtimes namespace defines the public contract between the core assembly and specialized prefab runtime implementations (XmlPrefabs, GamePrefabs, and CompiledPrefabs).
Runtimes are compiled into separate assemblies to decouple the core modding API from heavy dependencies (such as Roslyn compiler packages). The core defines the hosting contracts; runtimes consume them.
| Seam Type | Description and Role |
|---|---|
IPrefabRuntime / PrefabRuntimes |
Manages the ordered registry of prefab runtimes (derived from SubModule.xml load order). Exposes engine capabilities such as IsMovieSwitchInstalled (verifying GauntletMoviePatch is active) and DottedAttributePathsResolve (indicating dotted property path support). |
PrefabSource |
Central event bus and source of truth for prefab data. Raises Parsed, Registered, ReloadRequested, and EnvironmentChanged events. Tracks patched prefab names and runtime-registered widgets/brushes. |
MixinSource |
Provides access to active mixin types registered for a given ViewModel type in binding resolution order. |
ViewModelSource |
Exposes member resolution helpers so prefab runtimes can resolve property bindings and commands against live ViewModel instances. |
RuntimeSettings |
Exposes core module configuration flags (such as whether compiled prefabs or debug dumps are enabled) to runtime assemblies. |
Additionally, ViewModelMixins.Get<TMixin>() and ViewModelMixins.Select() in the Bannerlord.UIExtenderEx.ViewModels namespace provide the public runtime lookup mechanism that compiled prefab C# code uses to resolve mixin instances from bound ViewModels.
Configuration and Settings
UIExtenderEx settings are defined in the <Settings> section of the module's SubModule.xml and loaded via SettingsSubModuleXml. When Mod Configuration Menu (MCM) is present, it exposes these settings in the user interface and writes changes directly back to SubModule.xml. The configuration file is automatically reloaded when its last-modified timestamp changes.
| Setting | Consumed By | Architectural Effect |
|---|---|---|
DisableGeneratedPrefabs |
SubModule (Static constructor) |
Sets UIConfig.DoNotUseGeneratedPrefabs = true. Forces all UI movies to load via XML, restoring pre-v3.0.0 behavior. Used primarily as a compatibility escape hatch. |
DumpXML |
PrefabComponent.ProcessMovieIfNeeded |
Diagnostic dump: saves the fully patched XML document of every movie to <UIExtenderEx Module>/Dumps/<movie>_<moduleName>.xml. |
CompiledPrefabs |
GameCompiledPrefabEnvironment.IsEnabled |
Enables runtime C# source code generation, Roslyn background compilation, and dynamic prefab assembly caching. |
DumpGeneratedCode |
Bannerlord.UIExtenderEx.CompiledPrefabs |
Emits generated C# source code files to disk for debugging and inspection. |
RecordTimings |
GameCompiledPrefabEnvironment.RecordTimings |
PrefabTimings writes every movie load (timed by GauntletMovieTimingPatch) and the compile, load and warm-up timings to CompiledPrefabs/Timings/, one tab-separated file per session. |
Concurrency and Thread Safety
Because UI movie loading and prefab parsing can occur concurrently across multiple threads (e.g. background worker threads, asynchronous compilation, and scene transitions), the core runtime enforces strict thread-safety invariants:
Lock-Free Published Snapshots
Static runtime registries—such as UIExtender._runtimes and PrefabRuntimes._runtimes—use an atomic published array pattern:
- Write Path: When a mod registers or deregisters, a new array is constructed and published atomically using
Volatile.Write. - Read Path: Calling
UIExtender.GetAllRuntimes()performs a lock-free snapshot viaVolatile.Read. - Performance: Because ViewModel constructors, refresh hooks, and finalize callbacks query the active runtimes continuously, iteration operates over a fixed array reference with index loops. This incurs zero heap allocations and ensures readers never observe partially populated lists.
Concurrent Lookups and Instance Lifetimes
- Thread-Safe Dictionaries: Component-level lookups and patch enablement flags use
ConcurrentDictionary<TKey, TValue>, allowing safe concurrent access across worker and UI threads. - Garbage Collection Safety: Instance-level state (such as
MixinInstanceCacheand deferred constructor refresh queues inViewModelComponent) is stored inConditionalWeakTable<TKey, TValue>. This guarantees that mixin instances and caches are automatically cleaned up when the owningViewModelis garbage collected, preventing memory leaks. - Main-Thread Registration: The top-level
UIExtender.Instancesregistry uses a standardDictionary<string, UIExtender>because mod registration and deregistration occur sequentially during module startup (OnSubModuleLoad) on the main thread.
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...