-
Notifications
You must be signed in to change notification settings - Fork 5
Home
Set up Bannerlord mod project as usual, then add UIExtenderLib
as a dependency from NuGet.
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 .xml
s (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 fromPrefabExtensions
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 byType
field) -
PrefabExtensionReplacePatch
- replaces node specified by XPath with element from prefab extension -
CustomPatch<T>
- gives you full control over specifiedXmlNode
to manually patch
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.
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.
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.
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 clearRefresh
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).
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.
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.
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();
}
You can refer to interface class diagrams here.
- CampMod - mod that adds player camps
- HorseAmountIndicator - mod that adds horse amount indicator