Skip to content
Vasyl Horbachenko edited this page Apr 22, 2020 · 10 revisions

Setting up development

Set up Bannerlord mod project as usual, then add UIExtenderLib as a dependency from NuGet.

Prefab XML patching

Prefab patching is done by editing target .xml file on the fly applying patches one by one. This approach was selected in order to support loading extensions from .xmls (just like the game originally does) and to give finer control over the process.

In order to patch one of game prefab .xml files you need to create new class and decorate it with one of the IPrefabPatch descendants:

    [PrefabExtension("PREFAB_NAME")]
    public class ExamplePrefabExtension: CustomPatch<XmlDocument>
    {
    }

Currently there are following classes for prefab extensions:

  • PrefabExtensionInsertPatch - inserts prefab extension from PrefabExtensions folder as a child of XPath specified node and at specified position
  • PrefabExtensionInsertAsSiblingPatch - inserts prefab extension as a sibling to node specified by XPath (either after or before judging by Type field)
  • PrefabExtensionReplacePatch - replaces node specified by XPath with element from prefab extension
  • CustomPatch<T> - gives you full control over specified XmlNode to manually patch

PrefabExtensions insert

Using PrefabExtensionInsertPatch as a base class for your extensions which will load, parse and insert an XML element from a file in GUI/PrefabExtensions folder at specified position and at specified node. You need to override Prefab and Position getters to provide file name of the extension and position in among the children to insert it to. Following example will apply to SandBox/GUI/Prefab/Map/MapBar.xml and insert contents of Module/GUI/PrefabExtensions/ExampleButton.xml into bottom-left navigation bar (XML XPath is specified by second argument):

    [PrefabExtension("MapBar", "descendant::ListPanel[@Sprite='mapbar_left_canvas']/Children")]
    public class ExamplePrefabExtension: PrefabExtensionInsertPatch
    {
        public override int Position => 1;
        public override string Name=> "ExampleButton";
    }

All loaded modules are scanned for for files in GUI/PrefabExtensions folder recursively (similar to what game does for GUI/Prefabs), so when you specify name like ExampleButton it could be located in any mod's GUI/PrefabExtensions. Nested folders are supported as well, but only name of the file will be taken into account to figure out its name.

Custom patches

Using CustomPatch<T> you can patch the tree yourself. Generic argument T should either be an XmlDocument (if you want to operate on whole document) or XmlNode (if you want single node specified by xpath). During patching library will call your void Patch(T obj) so you can operate on the tree.

How it works

During UIExtender.Register() call library will fetch all registered extensions and iterate over them, registering them in internal component called PrefabComponent. After all extensions from all modules has been registered and patches have been applied, component will force Gauntlet's WidgetFactory to reload movies that for which patches were registered. At the time of force-reloading WidgetPrefab/LoadFrom method has already been patches, therefore this time extensions will be applied in order they were registered, just after WidgetPrefab parsed the XML and before it processed it.

Currently I haven't found a way to alter the very initial parsing of the prefabs, since it happens even before any user modules are loaded into the memory. I'm discouraged to modify game files, therefore the only way is to ask WidgetFactory to reload affected movies after hooks has been set up.

You may refer to UML diagrams here in order to get a better grasp on it.

ViewModel patching

In order to populate prefabs Gaunlet uses descendants of ViewModel class. Since you will be expanding existing prefabs by adding elements you also need to expand respective view models to actually populate new elements with data. This is done by using View Model Mixins: classes inheriting from BaseViewModelMixin and decorated with ViewModelMixin attribute:

    [ViewModelMixin]
    public class ExampleMixin: BaseViewModelMixin<MapNavigationVM>
    {
        [DataSourceProperty] public bool IsExampleButtonEnabled => true;
        [DataSourceProperty] public HintViewModel ExampleButtonHint => new HintViewModel("Example button");

        public ExampleMixin(MapNavigationVM vm): base(vm) {}

        [DataSourceMethod]
        public void ExecuteOpenExample()
        {
            // open example panel
        }
    }

Properties and methods marked with respective DataSourceX attributes will then be added to the target view model (specified by generic argument of BaseViewModelMixin), so that you can use them just as any other property from the datasource in your prefab extensions.

In the mixins you can use protected WeakReference<T> _vm in order to access original view model, the lifetime of your mixin should be the same as respective view model, the mixin will be created at the bottom of the constructor of the original view model.

Mixins have two methods you can override:

  • OnRefresh() called whenever original view model is refreshed. Keep in mind that not all of supported view models have a clear Refresh method, therefore specifics on when and how often this is called differs from patch to patch
  • OnFinalize() called whenever original VM implementation is called. This is dependend on game code and usually called whenever respective widget goes offscreen
  • You can also use desctructor, mixin instance should be deallocated in the original view model destructor

Your mixins should have the same lifetime of view model and shouldn't outlive the view models. Keep in mind that game doesn't deallocate instances immediately due to how GC works. In order to track Gauntlet-related lifetime you can use OnFinalize() method in mixins, semantics of which will follow original view model.

Keep in mind that patching is currently done on a later stage of loading (when main menu appears) so view models created before that stage will not be extended with your mixins (same goes for prefabs too).

Supported models

Since UIExtenderLibModule patches specific callsites in order to replace original view models with extended ones support for them has to be included in the library. Currently following view models are supported:

  • MapVM
  • MapInfoVM
  • MapTimeControlVM
  • MapNavigationVM
  • PartyVM
  • MissionAgentStatusVM
  • CharacterVM

Please note that it's quite easy to add support for more models, for that head onto wiki page.

How it works

Similarly to the prefab extensions mixins will be added to the internal ViewModelComponent, which will generate new view model class (extended class), which will inherit from target view model and add all methods from registered mixins.

Then component will patch constructor callsites (where original models were created) to call extended class constructors instead of original ones.

Additionally, since view models doesn't have reliably called refresh method this functionality is manually patched in as well.

You may refer to UML diagrams here in order to get a better grasp on it.

Wrapping it up

After your extensions has been set up you need to call UIExtender.Register in order to actually register it. This is normally done on submodule load:

        protected override void OnSubModuleLoad()
        {
            base.OnSubModuleLoad();
            _extender = new UIExtender("ModuleName");
            _extender.Register();
        }

Constructor argument should match your module's name since it will be used to lookup assets in Modules folder.

Note that you also have to call Verify() on later stages:

    protected override void OnBeforeInitialModuleScreenSetAsRoot()
    {
        base.OnBeforeInitialModuleScreenSetAsRoot();
        
        _extender.Verify();
    }

Interface diagrams

You can refer to interface class diagrams here.

Example mods