Core Architecture: Prefab Patching
Note
This article is written for UIExtenderEx maintainers and core contributors. For authoring prefab patches as a mod developer, see the APIv2 Documentation.
UIExtenderEx's prefab patching engine intercepts and modifies Gauntlet UI prefab XML documents before they are parsed into widget trees. Because every prefab runtime—whether the native XML loader, the game's pristine pre-compiled prefabs, or UIExtenderEx's Roslyn-compiled prefabs—derives its structure and fingerprint from this patched XML, modifications remain identical regardless of which runtime serves the movie.
Registration
During module registration, UIExtenderRuntime.Register inspects types decorated with [PrefabExtension] attributes. It instantiates each patch class via its public parameterless constructor and passes it to the corresponding PrefabComponent.RegisterPatch overload.
Each overload encapsulates the patch execution into an Action<XmlDocument> and records it as a PrefabPatch(Type, Patcher) entry inside MoviePatches[movieName], initially in a disabled state.
Supported Patch Types
| Patch Class | Target Selection | Operation |
|---|---|---|
Prefabs2.PrefabExtensionInsertPatch |
Node selected by XPath | Injects nodes according to InsertType (Child, Prepend, Append, ReplaceKeepChildren, Replace), or deletes the target node (Remove). |
Prefabs2.PrefabExtensionSetAttributePatch |
Node selected by XPath | Sets or replaces XML attributes on the target element. It cannot remove one. |
Prefabs.PrefabExtensionInsertPatch (v1 Obsolete) |
Node selected by XPath | Inserts content at a specified child index (Position). |
Prefabs.PrefabExtensionReplacePatch (v1) |
Node selected by XPath | Replaces the selected node and its children with new content. |
Prefabs.PrefabExtensionInsertAsSiblingPatch (v1) |
Node selected by XPath | Inserts content as an adjacent sibling (Append or Prepend). |
Prefabs.PrefabExtensionSetAttributePatch (v1) |
Node selected by XPath | Sets a single XML attribute on the target node. |
Prefabs.CustomPatch<XmlNode> |
Node selected by XPath | Invokes custom callback Apply(XmlNode) on the selected element. |
Prefabs.CustomPatch<XmlDocument> |
Whole XML document | Invokes custom callback Apply(XmlDocument); XPath selector is ignored. |
To maintain unified internal behavior, legacy v1 patches are automatically translated into v2 node placements (PlaceNodes). Both APIs share the same underlying execution pipeline.
XPath Evaluation and XML Sanitization
- Target Node Resolution:
XPath expressions are evaluated against the document using
SelectSingleNode. Only the first matching node is modified. If no matching node is found, UIExtenderEx logs a trace diagnostic, displays an in-game notification ("Failed to apply extension to<movie>: node at<xpath>not found"), and skips the patch without crashing the game. - Automatic XML Comment Removal:
Gauntlet UI's native XML parser crashes if injected XML fragments contain XML comment nodes (
<!-- ... -->). UIExtenderEx's insertion pipeline recursively strips all comment nodes before appending content to the active document. - Structural Integrity Validation:
Operations that break XML document validity—such as attempting to remove the document root element or inserting multiple root elements into a document—are caught and routed through
MessageUtils.Fail. - Target Movie Keys:
The
movieparameter refers to the prefab name (the XML filename without its.xmlextension). Consequently, a patch targeting a prefab applies wherever that prefab is parsed—whether as a top-level movie or as an inlined nested prefab embedded inside another movie.
Patch Execution Pipeline
The prefab patching pipeline intercepts Gauntlet UI movie loading and injects XML transformations seamlessly:
sequenceDiagram
autonumber
participant Game as GauntletMovie.Load
participant Switch as GauntletMoviePatch
participant Runtimes as IPrefabRuntime(s)
participant WF as WidgetFactory
participant LF as WidgetPrefab.LoadFrom
participant PM as WidgetPrefabPatch.ProcessMovie
participant PC as PrefabComponent (per module)
participant Src as PrefabSource
Game->>Switch: LoadPrefix(movieName, dataSource, ref doNotUseGeneratedPrefabs)
loop For each registered runtime (until claimed)
Switch->>Runtimes: TryServe(...)
end
alt Claimed by a Prefab Runtime
Switch-->>Game: Pre-compiled C# variant instantiated (XML parsing bypassed)
else Not Claimed (XML Fallback)
Switch-->>Game: doNotUseGeneratedPrefabs = true
Game->>WF: GetCustomType(name)
alt Registered Runtime Prefab (WidgetFactoryManager)
WF-->>Game: Return mod's custom prefab
else Cached Copy Live
WF-->>Game: Return shared parsed WidgetPrefab
else Uncached (First Load)
WF->>LF: LoadFrom(path)
LF->>LF: Read XML from disk into XmlDocument, instantiate new WidgetPrefab()
LF->>PM: ProcessMovie(prefab, path, document)
loop For each UIExtenderRuntime (in registration order)
PM->>PC: ProcessMovieIfNeeded(name, document)
Note over PC: Apply enabled patches in order<br/>Write DumpXML if configured
end
PM->>Src: RaiseParsed(prefab, name, document)
LF->>LF: Parse patched XmlDocument into Gauntlet widgets
LF-->>WF: Return finished WidgetPrefab
end
end
Detailed Pipeline Stages
- Movie Load Interception (
GauntletMoviePatch):GauntletMoviePatch.LoadPrefixevaluates whether a pre-compiled variant may be served. TheGamePrefabRuntimeclaims a movie only if no mod patches, overrides, or custom registrations touch any prefab in its dependency tree. TheCompiledPrefabRuntimeclaims a movie only if a cached assembly matching the current patched XML fingerprint exists. If neither claims it,doNotUseGeneratedPrefabsis set totrue, forcing the engine through the XML loader. LoadFromTranspiler Hook (WidgetPrefabPatch): WhenWidgetPrefab.LoadFromexecutes, a Harmony transpiler locates thenewobj WidgetPrefabinstruction following file reading and inserts a call toProcessMovie(prefab, path, document). The XML document and path parameters are discovered dynamically by type.- Patch Execution (
ProcessMovieIfNeeded):ProcessMovieiterates across all live runtimes (UIExtender.GetAllRuntimes()) in registration order. If a module contains enabled patches for the prefab, they are executed sequentially. Because runtimes are processed in registration order, later mods observe and build upon modifications made by earlier mods. - Diagnostic XML Dumps:
If the
DumpXMLconfiguration flag is enabled, the fully patchedXmlDocumentis written to<UIExtenderEx Module>/Dumps/<movie>_<moduleName>.xmlfor troubleshooting and inspection. - Publication (
PrefabSource.Parsed): Once all patches are applied,PrefabSource.RaiseParsedis emitted for every parsed prefab (whether patched or untouched).CompiledPrefabRuntimelistens to this event to record and hash each prefab's XML inPrefabXmlRegistry, whilePrefabXmlDumpwrites the XML to disk if theUIEXTENDEREX_DUMP_PREFABSenvironment variable is set. Neither subscriber triggers compilation; code generation begins only when a movie load cannot find a matching cached build (see Pipeline). - Engine Parsing:
The game engine parses the now-modified
XmlDocumentinto its internal widget tree.
Shared Templates and Cache Eviction
WidgetFactory caches parsed WidgetPrefab templates in _liveCustomTypes and _liveInstanceTracker, sharing them across all active movies that reference them. Critical UI elements (such as tooltips and standard headers) are retained in memory indefinitely.
To ensure newly enabled or disabled patches take effect, UIExtender.Enable(), Disable(), and Deregister() invoke WidgetFactoryManager.ReloadOnNextUse(movieNames). This method:
- Evicts the specified prefab names from the factory's live template caches and UIExtenderEx's tracking tables.
- Fires
PrefabSource.ReloadRequestedso the compiled runtime can invalidate stale builds. - Forces the next UI movie referencing the prefab to re-execute
LoadFromand re-apply active patches. - Leaves already-rendered screens undisturbed.
Runtime Registrations: Prefabs, Widgets, and Brushes
Mods can introduce UI resources dynamically in memory without placing XML files on disk via WidgetFactoryManager and BrushFactoryManager.
| API | Mechanism & Architecture |
|---|---|
WidgetFactoryManager.Register(name, Func<WidgetPrefab?>)CreateAndRegister(name, XmlDocument) |
Dynamic Prefabs: Hooks WidgetFactory.IsCustomType and GetCustomType via Harmony prefixes to serve custom prefabs, overriding any game prefab of the same name. Manages reference counting and lifecycle cleanup through an OnUnload hook. Registration clears existing factory copies and raises PrefabSource.Registered. |
WidgetFactoryManager.Create(name, XmlDocument) |
In-Memory Prefab Creation: Constructs a WidgetPrefab directly from an XmlDocument via LoadFromDocument. This uses a Harmony reverse patch that replaces file I/O with in-memory document assignment. Because reverse patches bypass the transpiler hook, LoadFromDocument clones the document, applies enabled patches directly, and raises PrefabSource.Parsed. |
WidgetFactoryManager.Register(Type widgetType) |
Custom C# Widget Classes: Injects custom widget classes into Gauntlet UI. Hooks WidgetFactory.CreateBuiltinWidget to construct the widget via its (UIContext) constructor, adds the type to WidgetInfoTable, and includes it in GetWidgetTypes. Raises PrefabSource.EnvironmentChanged. |
BrushFactoryManager.RegisterCreateAndRegister |
Custom Brushes: Injects mod brushes directly into each BrushFactory._brushes internal dictionary upon factory instantiation and after LoadBrushes reloads. Native game brushes with conflicting names take precedence. |
Resolving JIT Inlining Conflicts
In the .NET runtime, small methods such as BrushFactory.GetBrush, WidgetFactory.IsCustomType, and WidgetFactory.OnUnload are frequently inlined by the JIT compiler into calling methods compiled prior to mod initialization (e.g. WidgetTemplate.CreateWidgets, WidgetTemplate.OnRelease, and GauntletMovie.Release). If inlined, standard Harmony prefixes on those methods would never be reached.
UIExtenderEx overcomes this through two distinct strategies:
- Direct Table Injection for Brushes: Rather than patching lookup methods, brushes are inserted directly into
BrushFactory._brushes. Inlined reads retrieve the mod's brushes seamlessly from the dictionary. - De-inlining via Transpilers for Widgets:
WidgetFactoryManagerapplies an empty Harmony transpiler to caller methods (WidgetTemplate.CreateWidgets,WidgetTemplate.OnRelease,GauntletMovie.Release). Under Harmony, applying a patch causes the target method to be re-compiled by the JIT withMethodImplOptions.NoInliningenforced, guaranteeing that the prefix hooks onIsCustomTypeandOnUnloadare executed. Automated unit tests (PatchInliningTests) verify that all caller methods remain tracked.
Engine XML Loader Fixes
UIExtenderEx applies targeted engine patches to resolve native TaleWorlds Gauntlet UI bugs and limitations:
- Invariant Culture Parsing (
ParsePatch): TaleWorlds' nativeConstantDefinition.GetValueparses numeric constants using the current thread culture. On systems configured with comma decimal separators (e.g. European locales), decimal values such as1.5failed to parse or produced corrupt layout coordinates.ParsePatchtranspiles the parsing call to forceCultureInfo.InvariantCulture. - Dotted Attribute Paths (
WidgetExtensionsPatchinXmlPrefabs): Bannerlord's XML parser natively supported attribute property paths up to two segments deep.WidgetExtensionsPatchextends this to arbitrary depths, allowing nested widget attribute paths likeBrush.Font.CustomScaleto resolve. (Note that these refer strictly to widget properties, not ViewModel paths.) The runtime publishes this capability viaPrefabRuntimes.DottedAttributePathsResolve. - Template Child Allocation Leak (
WidgetTemplatePatchinXmlPrefabs): In native Gauntlet UI, instantiating a custom widget template repeatedly appended child elements to_customTypeChildrenwithout deduplication. On widget release, the engine traversed this list, resulting in quadratic overhead and memory leaks.WidgetTemplatePatchprevents duplicate registrations.
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...