├── lib ├── 0Harmony.dll └── 0Harmony_2.0.0.10.dll ├── docs ├── Runtime │ ├── Overview.png │ ├── Registration.png │ ├── VMPatching.png │ ├── PrefabPatching.png │ ├── ExtendedVMRuntime.png │ ├── PrefabPatching.puml │ ├── ExtendedVMRuntime.puml │ ├── Overview.puml │ ├── Registration.puml │ └── VMPatching.puml └── Interface │ ├── Overview.png │ ├── PrefabExtension.png │ ├── ViewModelMixin.png │ ├── ViewModelMixin.puml │ ├── Overview.puml │ └── PrefabExtension.puml ├── .idea └── .idea.UIExtenderLib │ └── .idea │ ├── codeStyles │ └── codeStyleConfig.xml │ ├── encodings.xml │ ├── vcs.xml │ ├── projectSettingsUpdater.xml │ └── indexLayout.xml ├── UIExtenderLib ├── UIExtenderLib.nuspec ├── CodePatcher │ ├── BuiltInPatches │ │ ├── CorePatches.cs │ │ └── ViewModelPatches.cs │ ├── StaticLibrary │ │ ├── UIExtenderRuntimeLib.cs │ │ └── UIExtenderPatchLib.cs │ ├── CodePatchesAssemblyBuilder.cs │ └── CodePatcherComponent.cs ├── Properties │ └── AssemblyInfo.cs ├── Interface │ ├── UIExtensionAttributes.cs │ ├── BaseViewModelMixin.cs │ └── PrefabPatchInterface.cs ├── UIExtender.cs ├── Utils.cs ├── UIExtenderLib.csproj ├── UIExtenderRuntime.cs ├── Prefab │ └── PrefabComponent.cs └── ViewModel │ └── ViewModelComponent.cs ├── Package.nuspec ├── ExampleModule ├── Properties │ └── AssemblyInfo.cs ├── SubModule.cs └── ExampleModule.csproj ├── AnotherExampleModule ├── Properties │ └── AssemblyInfo.cs ├── SubModule.cs └── AnotherExampleModule.csproj ├── UIExtenderLib.sln ├── UIExtenderLib.sln.DotSettings ├── README.md ├── LICENSE └── .gitignore /lib/0Harmony.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/lib/0Harmony.dll -------------------------------------------------------------------------------- /docs/Runtime/Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Runtime/Overview.png -------------------------------------------------------------------------------- /lib/0Harmony_2.0.0.10.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/lib/0Harmony_2.0.0.10.dll -------------------------------------------------------------------------------- /docs/Interface/Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Interface/Overview.png -------------------------------------------------------------------------------- /docs/Runtime/Registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Runtime/Registration.png -------------------------------------------------------------------------------- /docs/Runtime/VMPatching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Runtime/VMPatching.png -------------------------------------------------------------------------------- /docs/Runtime/PrefabPatching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Runtime/PrefabPatching.png -------------------------------------------------------------------------------- /docs/Interface/PrefabExtension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Interface/PrefabExtension.png -------------------------------------------------------------------------------- /docs/Interface/ViewModelMixin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Interface/ViewModelMixin.png -------------------------------------------------------------------------------- /docs/Runtime/ExtendedVMRuntime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/UIExtenderLib/HEAD/docs/Runtime/ExtendedVMRuntime.png -------------------------------------------------------------------------------- /.idea/.idea.UIExtenderLib/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/.idea.UIExtenderLib/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/Interface/ViewModelMixin.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | class BaseViewModelMixin { 3 | 4 | } 5 | 6 | interface IViewModelMixin { 7 | 8 | } 9 | 10 | BaseViewModelMixin --|> IViewModelMixin 11 | 12 | @enduml -------------------------------------------------------------------------------- /.idea/.idea.UIExtenderLib/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.idea.UIExtenderLib/.idea/projectSettingsUpdater.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/.idea.UIExtenderLib/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/Interface/Overview.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | SubModule --* UIExtender 4 | SubModule --* ViewModelMixinClasses 5 | SubModule --* PrefabExtensionClasses 6 | 7 | class ViewModelMixinClasses { 8 | [ViewModelMixin] 9 | } 10 | 11 | class PrefabExtensionClasses { 12 | [PrefabExtension("Movie", "XPath")] 13 | } 14 | 15 | ViewModelMixinClasses --|> BaseViewModelMixin 16 | PrefabExtensionClasses --|> IPrefabExtension 17 | 18 | @enduml -------------------------------------------------------------------------------- /docs/Interface/PrefabExtension.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | interface IPrefabPatch { 4 | } 5 | 6 | abstract class InsertPatch { 7 | + Position 8 | } 9 | 10 | class PrefabExtensionInsertAsSiblingPatch { 11 | + Type 12 | + Name 13 | } 14 | 15 | class PrefabExtensionInsertPatch { 16 | + Name 17 | } 18 | 19 | class PrefabExtensionReplacePatch { 20 | + Name 21 | } 22 | 23 | class CustomPatch { 24 | } 25 | 26 | CustomPatch --|> IPrefabPatch 27 | InsertPatch --|> IPrefabPatch 28 | 29 | PrefabExtensionInsertPatch --|> InsertPatch 30 | PrefabExtensionReplacePatch --|> IPrefabPatch 31 | PrefabExtensionInsertAsSiblingPatch --|> IPrefabPatch 32 | 33 | @enduml -------------------------------------------------------------------------------- /UIExtenderLib/UIExtenderLib.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | UIExtenderLib 5 | $version$ 6 | UIExtenderLib 7 | shdwp 8 | shdwp 9 | false 10 | LGPL-3.0-or-later 11 | https://github.com/shdwp/UIExtenderLib 12 | $description$ 13 | Initial v2 release. 14 | Copyright 2020 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/Runtime/PrefabPatching.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | loop for each Patch 3 | CodePatcher -> Patch: apply with Harmony 4 | alt widget load patch transpiler 5 | Patch -> Patch: find marks of existing patch 6 | alt existing patch found 7 | Patch -> Patch: replace existing call with new DynamicMethod\nwhich calls both both previous and current `ProcessMovieDocumentIfNeeded` 8 | else otherwise 9 | Patch -> Patch: replace beginning of method with mark,\ncall to `LoadXmlDocument` and `ProcessMovieDocumentIfNeeded` 10 | end 11 | else otherwise 12 | note over Patch 13 | See `VMPatching` diagram. 14 | endnote 15 | end 16 | end 17 | @enduml -------------------------------------------------------------------------------- /Package.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Package 5 | 2.0.0.0 6 | shdwp 7 | shdwp 8 | false 9 | LGPLv3 10 | https://github.com/shdwp/UIExtenderLib 11 | Library for Mount And Blade II: Bannerlord mods to enable multiple mods changing standard UI of the game 12 | Initial v2.0.0 release 13 | Copyright 2020 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /UIExtenderLib/CodePatcher/BuiltInPatches/CorePatches.cs: -------------------------------------------------------------------------------- 1 | using TaleWorlds.GauntletUI.PrefabSystem; 2 | 3 | namespace UIExtenderLib.CodePatcher.BuiltInPatches 4 | { 5 | /// 6 | /// Set of patches that provide core functionality of UIExtender. 7 | /// If any of those fail UIExtender lib will not function at all. 8 | /// 9 | /// Two patches in total is required in order for the library to function: 10 | /// - Patch to `WidgetPrefab.LoadFrom`, which apply prefab XML extensions 11 | /// - Patch to `ViewModel.ExecuteCommand`, which fixes inheritance problems 12 | /// 13 | internal class CorePatches 14 | { 15 | internal static bool AddTo(CodePatcherComponent comp) 16 | { 17 | var widgetLoadMethod = typeof(WidgetPrefab).GetMethod(nameof(WidgetPrefab.LoadFrom)); 18 | var executeCommandMethod = typeof(TaleWorlds.Library.ViewModel).GetMethod(nameof(TaleWorlds.Library.ViewModel.ExecuteCommand)); 19 | 20 | if (widgetLoadMethod == null || executeCommandMethod == null) 21 | { 22 | return false; 23 | } 24 | 25 | comp.AddWidgetLoadPatch(widgetLoadMethod); 26 | comp.AddViewModelExecutePatch(executeCommandMethod); 27 | return true; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /docs/Runtime/ExtendedVMRuntime.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | actor Game 4 | 5 | Game -> ExtendedVM: Instantiation (constructor called) 6 | ExtendedVM -> BaseVM: base constructor 7 | BaseVM --> ExtendedVM 8 | 9 | ExtendedVM -> ViewModelComponent: InitializeMixinsForVMInstance() 10 | ViewModelComponent -> ViewModelComponent: instantiate all mixins associated with this VM 11 | ViewModelComponent --> ExtendedVM 12 | ExtendedVM --> Game 13 | 14 | ... 15 | Game -> ExtendedVM: Destructor 16 | ExtendedVM -> BaseVM: original destructor 17 | BaseVM --> ExtendedVM 18 | 19 | ExtendedVM -> ViewModelComponent: DestructMixinsForVMInstance 20 | ViewModelComponent --> ExtendedVM 21 | ExtendedVM --> Game 22 | ... 23 | 24 | ... 25 | Game -> ExtendedVM: OnFinalize 26 | ExtendedVM -> BaseVM: original OnFinalize 27 | BaseVM --> ExtendedVM 28 | 29 | ExtendedVM -> ViewModelComponent: FinalizeMixinsForVMInstance 30 | ViewModelComponent --> ExtendedVM 31 | ExtendedVM --> Game 32 | ... 33 | 34 | Game -> ExtendedVM: method access (and getters/setters) 35 | alt original method 36 | ExtendedVM -> BaseVM: property access 37 | else mixin method 38 | ExtendedVM -> ViewModelComponent: MixinInstanceForVMInstance 39 | ViewModelComponent -> ExtendedVM 40 | ExtendedVM -> Mixin: property access 41 | Mixin --> ExtendedVM 42 | end 43 | ExtendedVM --> Game 44 | 45 | @enduml -------------------------------------------------------------------------------- /docs/Runtime/Overview.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | class UIExtender { 4 | - _runtime 5 | } 6 | 7 | class UIExtenderRuntime { 8 | + ModuleName 9 | - _userMessages 10 | } 11 | 12 | class PrefabComponent { 13 | - _moviePatches 14 | ~ RegisterPatch() 15 | ~ ProcessMovieIfNeeded() 16 | ~ ForceReloadMovies() 17 | } 18 | 19 | class ViewModelComponent { 20 | - _assemblyBuilder 21 | - _mixins 22 | - _mixinInstanceCache 23 | ~ RegisterViewModelMixin() 24 | ~ ExtendsViewModelType() 25 | ~ ExtendedViewModelForType() 26 | ~ MixinInstanceForObject() 27 | ~ InitializeMixinsForVMInstance() 28 | ~ RefreshMixinsForVMInstance() 29 | ~ FinalizeMixinsForVMInstance() 30 | ~ DestructMixinsForVMInstance() 31 | } 32 | 33 | class CodePatcherComponent { 34 | - _harmony 35 | - _patchesAssemblyBuilder 36 | - _transpilers 37 | - _postfixes 38 | ~ AddViewModelInstantiationPatch() 39 | ~ AddViewModelRefreshPatch() 40 | ~ AddWidgetLoadPatch() 41 | ~ AddViewModelExecutePatch() 42 | ~ ApplyPatches() 43 | } 44 | 45 | class CodePatchesAssemblyBuilder { 46 | - _assemblyBuilder 47 | ~ AddTranspiler() 48 | ~ AddPostfix() 49 | ~ SaveAndLoadLibraryType() 50 | } 51 | 52 | UIExtender --* UIExtenderRuntime 53 | UIExtenderRuntime --* PrefabComponent 54 | UIExtenderRuntime --* ViewModelComponent 55 | UIExtenderRuntime --* CodePatcherComponent 56 | 57 | CodePatcherComponent --* CodePatchesAssemblyBuilder 58 | 59 | @enduml -------------------------------------------------------------------------------- /docs/Runtime/Registration.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | actor UserLibrary 4 | 5 | UserLibrary -> UIExtender: Register() 6 | 7 | UIExtender -> UIExtender: Find attributed types in calling assembly 8 | UIExtender -> UIExtender: Create runtime for module and store it 9 | UIExtender -> UIExtenderRuntime: Register() 10 | loop for each type 11 | alt prefab extension 12 | UIExtenderRuntime -> PrefabComponent: RegisterPatch() 13 | PrefabComponent --> UIExtenderRuntime 14 | PrefabComponent -> PrefabComponent: store extension for later 15 | PrefabComponent --> UIExtenderRuntime 16 | else if view model extension 17 | UIExtenderRuntime -> ViewModelComponent: RegisterViewModelMixin() 18 | ViewModelComponent -> ViewModelComponent: store mixin for later 19 | ViewModelComponent --> UIExtenderRuntime 20 | end 21 | end 22 | UIExtenderRuntime -> UIExtenderRuntime: Patching (see separate diagram) 23 | note over UIExtenderRuntime 24 | See `VMPatching` and `PrefabPatching` diagrams. 25 | endnote 26 | UIExtenderRuntime --> UIExtender 27 | UIExtender --> UserLibrary 28 | 29 | ... 30 | 31 | UserLibrary -> UIExtender: Verify() 32 | UIExtender -> UIExtenderRuntime: Verify() 33 | alt any errors encountered during registration 34 | UIExtenderRuntime -> UIExtenderRuntime: display errors to the user 35 | end 36 | UIExtenderRuntime --> UIExtender 37 | UIExtender --> UserLibrary 38 | 39 | 40 | @enduml -------------------------------------------------------------------------------- /ExampleModule/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("ExampleModule")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("ExampleModule")] 12 | [assembly: AssemblyCopyright("Copyright © 2020")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("109D437D-F030-4B47-B64B-E668884018B0")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /AnotherExampleModule/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("AnotherExampleModule")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("AnotherExampleModule")] 12 | [assembly: AssemblyCopyright("Copyright © 2020")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("AE5A7DED-AA14-4F54-9566-AC6A8323CB41")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /UIExtenderLib/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("UIExtenderLib")] 8 | [assembly: AssemblyDescription("Library for MnB2: Bannerlord mods to enable multiple mods changing standard UI of the game")] 9 | [assembly: AssemblyConfiguration("Debug")] 10 | [assembly: AssemblyCompany("shdwp")] 11 | [assembly: AssemblyProduct("UIExtenderLib")] 12 | [assembly: AssemblyCopyright("Copyright © 2020")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("F17CCC8F-1E87-4B3C-B4EF-E03DD062322E")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("2.0.0.3")] 35 | [assembly: AssemblyFileVersion("2.0.0.3")] -------------------------------------------------------------------------------- /docs/Runtime/VMPatching.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | UIExtenderRuntime -> StandardPatchLibrary: AddPatches() 3 | StandardPatchLibrary -> CodePatcher: add standard patches 4 | CodePatcher --> StandardPatchLibrary 5 | StandardPatchLibrary --> UIExtenderRuntime 6 | alt errors encountered 7 | UIExtenderRuntime -> UIExtenderRuntime: store user error message for later 8 | end 9 | 10 | UIExtenderRuntime -> CodePatcher: ApplyPatches() 11 | 12 | loop for each Patch 13 | CodePatcher -> Patch: apply with Harmony 14 | alt view model instantiation callsite transpiler 15 | Patch -> Patch: search for `newobj` calls in code 16 | Patch -> ViewModelComponent: ExtendsViewModelType() 17 | ViewModelComponent --> Patch 18 | alt component extends this view model type 19 | Patch -> ViewModelComponent: ExtendedViewModelTypeForType() 20 | ViewModelComponent -> ViewModelComponent: generate extended view model type 21 | ViewModelComponent --> Patch 22 | Patch -> Patch: replace original VM type in `newobj`\nwith extended VM type, making game\ncreate extended VM instances in place\nof original (base) ones 23 | end 24 | else view model refresh callsite postfix 25 | Patch -> Patch: add refresh postfix 26 | note over Patch 27 | See `ExtendedVMRuntime` diagram. 28 | endnote 29 | else otherwise 30 | note over Patch 31 | See `PrefabPatching` diagram. 32 | endnote 33 | end 34 | end 35 | 36 | CodePatcher --> UIExtenderRuntime 37 | 38 | UIExtenderRuntime -> ViewModelComponent: SaveDebugImages() 39 | ViewModelComponent --> UIExtenderRuntime 40 | 41 | @enduml -------------------------------------------------------------------------------- /UIExtenderLib/Interface/UIExtensionAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UIExtenderLib.Interface 4 | { 5 | /// 6 | /// Base class for extensions attributes 7 | /// 8 | public class UIExtenderLibExtension: Attribute { } 9 | 10 | /// 11 | /// Attribute for mixin methods to be added to view models. 12 | /// Only methods specified by this attribute will actually end up in extended view model 13 | /// 14 | public class DataSourceMethod : Attribute { } 15 | 16 | /// 17 | /// Attribute to mark view model mixins. 18 | /// Mixin classes should extend from `BaseViewModelMixin` and should be marked with this attribute 19 | /// 20 | public class ViewModelMixin : UIExtenderLibExtension { } 21 | 22 | /// 23 | /// Attribute for prefab XML extensions. 24 | /// Extension classes should inherit from one of the `IPrefabPatch` base classes and should be marked with this attribute 25 | /// 26 | public class PrefabExtension : UIExtenderLibExtension 27 | { 28 | /// 29 | /// Gauntlet Movie name to extend 30 | /// 31 | public string Movie { get; } 32 | 33 | /// 34 | /// XPath of the node to operate against (optional) 35 | /// 36 | public string XPath { get; } 37 | 38 | /// 39 | /// Constructor 40 | /// 41 | /// Gauntlet Movie name to extend 42 | /// XPath of the node to operate against (optional) 43 | public PrefabExtension(string movie, string xpath = null) : base() 44 | { 45 | Movie = movie; 46 | XPath = xpath; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /UIExtenderLib.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIExtenderLib", "UIExtenderLib\UIExtenderLib.csproj", "{F17CCC8F-1E87-4B3C-B4EF-E03DD062322E}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleModule", "ExampleModule\ExampleModule.csproj", "{109D437D-F030-4B47-B64B-E668884018B0}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnotherExampleModule", "AnotherExampleModule\AnotherExampleModule.csproj", "{AE5A7DED-AA14-4F54-9566-AC6A8323CB41}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {F17CCC8F-1E87-4B3C-B4EF-E03DD062322E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {F17CCC8F-1E87-4B3C-B4EF-E03DD062322E}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {F17CCC8F-1E87-4B3C-B4EF-E03DD062322E}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {F17CCC8F-1E87-4B3C-B4EF-E03DD062322E}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {109D437D-F030-4B47-B64B-E668884018B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {109D437D-F030-4B47-B64B-E668884018B0}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {109D437D-F030-4B47-B64B-E668884018B0}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {109D437D-F030-4B47-B64B-E668884018B0}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {AE5A7DED-AA14-4F54-9566-AC6A8323CB41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {AE5A7DED-AA14-4F54-9566-AC6A8323CB41}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {AE5A7DED-AA14-4F54-9566-AC6A8323CB41}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {AE5A7DED-AA14-4F54-9566-AC6A8323CB41}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /UIExtenderLib.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | NEXT_LINE 3 | NEXT_LINE 4 | NEXT_LINE 5 | NEXT_LINE 6 | NEXT_LINE 7 | NEXT_LINE 8 | NEXT_LINE 9 | NEXT_LINE 10 | True 11 | True 12 | True 13 | True -------------------------------------------------------------------------------- /AnotherExampleModule/SubModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using TaleWorlds.CampaignSystem; 4 | using TaleWorlds.CampaignSystem.ViewModelCollection; 5 | using TaleWorlds.CampaignSystem.ViewModelCollection.Map; 6 | using TaleWorlds.Core; 7 | using TaleWorlds.Core.ViewModelCollection; 8 | using TaleWorlds.Engine; 9 | using TaleWorlds.Library; 10 | using TaleWorlds.MountAndBlade; 11 | using UIExtenderLib; 12 | using UIExtenderLib.Interface; 13 | 14 | namespace AnotherExampleModule 15 | { 16 | [PrefabExtension("MapBar", "descendant::ListPanel[@Id='BottomInfoBar']/Children")] 17 | public class BottomBarExtension : PrefabExtensionInsertPatch 18 | { 19 | public override int Position => PositionLast; 20 | public override string Name => "AnotherExampleExtension"; 21 | } 22 | 23 | [ViewModelMixin] 24 | public class MapInfoMixin : BaseViewModelMixin 25 | { 26 | private string _hintText; 27 | 28 | [DataSourceProperty] public BasicTooltipViewModel AnotherExampleExtensionHint => new BasicTooltipViewModel(() => _hintText); 29 | 30 | public MapInfoMixin(MapInfoVM vm) : base(vm) 31 | { 32 | } 33 | 34 | public override void OnRefresh() 35 | { 36 | _hintText = "AnotherExampleExtension Tick " + Utilities.GetDeltaTime(0); 37 | } 38 | } 39 | 40 | [ViewModelMixin] 41 | public class MapInfoAdditionalMixin : BaseViewModelMixin 42 | { 43 | [DataSourceProperty] public string AnotherExampleExtensionValue => "AEE"; 44 | 45 | public MapInfoAdditionalMixin(MapInfoVM vm) : base(vm) 46 | { 47 | } 48 | } 49 | 50 | public class SubModule: MBSubModuleBase 51 | { 52 | private UIExtender _uiExtender = new UIExtender("AnotherExampleModule"); 53 | 54 | protected override void OnSubModuleLoad() 55 | { 56 | base.OnSubModuleLoad(); 57 | 58 | _uiExtender.Register(); 59 | } 60 | 61 | protected override void OnBeforeInitialModuleScreenSetAsRoot() 62 | { 63 | base.OnBeforeInitialModuleScreenSetAsRoot(); 64 | 65 | _uiExtender.Verify(); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /ExampleModule/SubModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using TaleWorlds.CampaignSystem; 4 | using TaleWorlds.CampaignSystem.ViewModelCollection; 5 | using TaleWorlds.CampaignSystem.ViewModelCollection.Map; 6 | using TaleWorlds.Core; 7 | using TaleWorlds.Core.ViewModelCollection; 8 | using TaleWorlds.Engine; 9 | using TaleWorlds.Library; 10 | using TaleWorlds.Localization; 11 | using TaleWorlds.MountAndBlade; 12 | using TaleWorlds.MountAndBlade.ViewModelCollection.Multiplayer; 13 | using UIExtenderLib; 14 | using UIExtenderLib.Interface; 15 | using Debug = System.Diagnostics.Debug; 16 | 17 | namespace ExampleModule 18 | { 19 | [PrefabExtension("MapBar", "descendant::ListPanel[@Id='BottomInfoBar']/Children")] 20 | public class BottomBarExtension : PrefabExtensionInsertPatch 21 | { 22 | public override int Position => PositionLast; 23 | public override string Name => "ExampleExtension"; 24 | } 25 | 26 | [ViewModelMixin] 27 | public class MapInfoMixin : BaseViewModelMixin 28 | { 29 | private string _hintText; 30 | 31 | [DataSourceProperty] public BasicTooltipViewModel ExampleExtensionHint => new BasicTooltipViewModel(() => _hintText); 32 | [DataSourceProperty] public string ExampleExtensionValue => "EE"; 33 | 34 | public MapInfoMixin(MapInfoVM vm) : base(vm) 35 | { 36 | } 37 | 38 | public override void OnRefresh() 39 | { 40 | _hintText = "ExampleExtension Tick " + Utilities.GetDeltaTime(0); 41 | if (_vm.TryGetTarget(out var vm)) 42 | { 43 | vm.OnPropertyChanged(); 44 | } 45 | } 46 | } 47 | 48 | public class SubModule: MBSubModuleBase 49 | { 50 | private UIExtender _uiExtender = new UIExtender("ExampleModule"); 51 | 52 | protected override void OnSubModuleLoad() 53 | { 54 | base.OnSubModuleLoad(); 55 | 56 | _uiExtender.Register(); 57 | } 58 | 59 | protected override void OnBeforeInitialModuleScreenSetAsRoot() 60 | { 61 | base.OnBeforeInitialModuleScreenSetAsRoot(); 62 | 63 | _uiExtender.Verify(); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /UIExtenderLib/Interface/BaseViewModelMixin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using TaleWorlds.Library; 4 | using Debug = System.Diagnostics.Debug; 5 | 6 | namespace UIExtenderLib.Interface 7 | { 8 | /// 9 | /// Interface for ViewModel mixins 10 | /// Should not be used directly, ViewModelMixin should be used as base class 11 | /// 12 | public interface IViewModelMixin 13 | { 14 | /// 15 | /// Called when ViewModel is refreshed (specifics are based on ViewModel patch) 16 | /// 17 | void OnRefresh(); 18 | 19 | /// 20 | /// Called when ViewModel's `OnFinalized` called (supported on models game actually call `OnFinalized`) 21 | /// 22 | void OnFinalize(); 23 | } 24 | 25 | /// 26 | /// Base class for ViewModelMixin. 27 | /// Generic parameter T will be used to determine which VM to extend. 28 | /// You can use protected _vm to access fields of the original view model. 29 | /// 30 | /// child of ViewModel this mixin is extending 31 | public class BaseViewModelMixin: IViewModelMixin where T: TaleWorlds.Library.ViewModel 32 | { 33 | /// 34 | /// ViewModel instance this mixin is attached to 35 | /// 36 | protected WeakReference _vm; 37 | 38 | public BaseViewModelMixin(T vm) 39 | { 40 | _vm = new WeakReference(vm); 41 | } 42 | 43 | /// 44 | /// Called when ViewModel is refreshed (specifics are based on ViewModel patch). 45 | /// Defaults to empty method. 46 | /// 47 | public virtual void OnRefresh() { } 48 | 49 | /// 50 | /// Called when ViewModel's `OnFinalized` called (supported on models game actually call `OnFinalized`). 51 | /// Defaults to empty method. 52 | /// 53 | public virtual void OnFinalize() { } 54 | 55 | /// 56 | /// Helper method to get private value from attached view model instance 57 | /// 58 | /// name of the field 59 | /// type 60 | /// 61 | protected V GetPrivate(string name) 62 | { 63 | return _vm.PrivateValue(name); 64 | } 65 | 66 | /// 67 | /// Helper method to set private value of attached view model instance 68 | /// 69 | /// name of the field 70 | /// new value 71 | /// type 72 | protected void SetPrivate(string name, V value) 73 | { 74 | _vm.PrivateValueSet(name, value); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /UIExtenderLib/Interface/PrefabPatchInterface.cs: -------------------------------------------------------------------------------- 1 | using System.Xml; 2 | 3 | namespace UIExtenderLib.Interface 4 | { 5 | /// 6 | /// General interface for XML prefab patch 7 | /// 8 | public interface IPrefabPatch 9 | { 10 | } 11 | 12 | /// 13 | /// Custom patch on either whole XmlDocument (if T is XmlDocument) or Xpath specified node (if XmlNode is the generic argument) 14 | /// 15 | /// 16 | public abstract class CustomPatch : IPrefabPatch where T: XmlNode 17 | { 18 | /// 19 | /// Apply this patch to obj 20 | /// 21 | /// 22 | public abstract void Apply(T obj); 23 | } 24 | 25 | /// 26 | /// Base class for insert patches 27 | /// 28 | public abstract class InsertPatch : IPrefabPatch 29 | { 30 | /// 31 | /// Constant that will insert snippet at the very beginning 32 | /// 33 | public static int PositionFirst = 0; 34 | 35 | /// 36 | /// Constant that will insert snippet at the very end 37 | /// 38 | public static int PositionLast = int.MaxValue; 39 | 40 | /// 41 | /// Position to insert snippet at 42 | /// 43 | public abstract int Position { get; } 44 | } 45 | 46 | /// 47 | /// Patch that inserts prefab extension (specified by `Name`) as a child in XPath specified node, at specific position (`Position` property) 48 | /// Extension snippet should be named as `{Name}.xml` and located at module's `GUI/PrefabExtensions` folder. 49 | /// 50 | public abstract class PrefabExtensionInsertPatch: InsertPatch 51 | { 52 | /// 53 | /// Name of the extension snippet, without `.xml` 54 | /// 55 | public abstract string Name { get; } 56 | } 57 | 58 | /// 59 | /// Patch that replaces node specified by XPath with node from prefab extension 60 | /// 61 | public abstract class PrefabExtensionReplacePatch : IPrefabPatch 62 | { 63 | /// 64 | /// Name of the extension snippet, without `.xml` 65 | /// 66 | public abstract string Name { get; } 67 | } 68 | 69 | /// 70 | /// Patch that inserts prefab extension as a sibling to node specified by Xpath. 71 | /// Order is controlled by `Type` property. 72 | /// 73 | public abstract class PrefabExtensionInsertAsSiblingPatch: IPrefabPatch 74 | { 75 | /// 76 | /// Insert type enum - Prepend inserts snippet before sibling, Append - after 77 | /// 78 | public enum InsertType 79 | { 80 | Prepend, 81 | Append, 82 | } 83 | 84 | /// 85 | /// Type of the insert 86 | /// 87 | public virtual InsertType Type => InsertType.Append; 88 | 89 | /// 90 | /// Name of the extension snippet, without `.xml` 91 | /// 92 | public abstract string Name { get; } 93 | } 94 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Library for Mount & Blade: Bannerlord that enables multiple mods to alter standard game interface. 2 | 3 | ### Installation 4 | Install from NuGet: package name `UIExtenderLib`. 5 | 6 | Alternatively you can download package from [NuGet page](https://www.nuget.org/packages/UIExtenderLib/) and open it to find `dll` in the `lib/` folder. 7 | 8 | ### Updating from version 1.0.x 9 | UIExtenderLib doesn't include `UIExtenderLibModule` now, meaning that all you need to do is add `dll` as a dependency. Mixing v1 modules and v2 modules are not supported, meaning that you absolutely need to update dependent mods. API stayed the same except for small changes in registration routine. 10 | 11 | ### When you should use it 12 | If you change any of the game standard _prefab_ `.xml` files you should use this library or similar approach in order to not overwrite changes to the same elements by other mods. 13 | You don't need to use this if you are adding a completely new screen or a menu item in the encounter overlay, since things like that are already handled correctly by the game API. 14 | 15 | ### Quickstart 16 | You mark your _prefab extensions_ based on one of the `IPrefabPatch` descendants and marking it with `PrefabExtension` attribute, therefore enabling you to make additions to the specified Movie's XML data. 17 | 18 | ```cs 19 | [PrefabExtension("MapBar", "descendant::ListPanel[@Id='BottomInfoBar']/Children")] 20 | public class PrefabExtension : PrefabExtensionInsertPatch 21 | { 22 | public override int Position => PositionLast; 23 | public override string Name => "HorseAmountIndicator"; 24 | } 25 | ``` 26 | This specific extension will load prefab `HorseAmountIndicator.xml` from `MODULE/GUI/PrefabExtensions/` folder. 27 | 28 | In order to add data to the prefab, you need to add properties to the target datasource class, which in case of `BottomInfoBar` is `MapInfoVM`, this is done by making a _mixin_ class, inheriting from `BaseViewModelMixin` and marking it with `ViewModelMixin` attribute. This class will be mixed in to the target view model `T`, making fields and methods accessible in the prefab: 29 | 30 | ```cs 31 | [ViewModelMixin] 32 | public class ViewModelMixin : BaseViewModelMixin 33 | { 34 | private int _horsesAmount; 35 | private string _horsesTooltip; 36 | 37 | [DataSourceProperty] public BasicTooltipViewModel HorsesAmountHint => new BasicTooltipViewModel(() => _horsesTooltip); 38 | [DataSourceProperty] public string HorsesAmount => "" + _horsesAmount; 39 | 40 | public ViewModelMixin(MapInfoVM vm) : base(vm) 41 | { 42 | } 43 | 44 | public override void OnRefresh() 45 | { 46 | var horses = MobileParty.MainParty.ItemRoster.Where(i => i.EquipmentElement.Item.ItemCategory.Id == new MBGUID(671088673)); 47 | var newTooltip = horses.Aggregate("Horses: ", (s, element) => $"{s}\n{element.EquipmentElement.Item.Name}: {element.Amount}"); 48 | 49 | if (newTooltip != _horsesTooltip) 50 | { 51 | _horsesAmount = horses.Sum(item => item.Amount); 52 | _horsesTooltip = newTooltip; 53 | 54 | if (_vm.TryGetTarget(out var vm)) 55 | { 56 | vm.OnPropertyChanged(nameof(HorsesAmount)); 57 | } 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | The last thing is to call `UIExtender.Register` and `UIExtender.Verify` to apply your extensions: 64 | ```cs 65 | 66 | protected override void OnSubModuleLoad() 67 | { 68 | base.OnSubModuleLoad(); 69 | 70 | _extender = new UIExtender("ModuleName"); 71 | _extender.Register(); 72 | } 73 | 74 | protected override void OnBeforeInitialScreenSetAsRoot() { 75 | _extender.Verify(); 76 | } 77 | ``` 78 | 79 | ### Documentation 80 | Rest of the documentation is located on [wiki](https://github.com/shdwp/UIExtenderLib/wiki). 81 | 82 | ### Examples 83 | * [CampMod](https://github.com/shdwp/BannerlordCampMod) - mod that adds player camps 84 | * [HorseAmountIndicator](https://github.com/shdwp/BannerlordHorseAmountIndicatorMod) - mod that adds horse amount indicator 85 | -------------------------------------------------------------------------------- /UIExtenderLib/UIExtender.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | using TaleWorlds.Engine; 5 | using TaleWorlds.Library; 6 | using UIExtenderLib.Interface; 7 | using Debug = System.Diagnostics.Debug; 8 | 9 | namespace UIExtenderLib 10 | { 11 | /// 12 | /// Client class instance of which should be created for each module using this library 13 | /// 14 | public class UIExtender 15 | { 16 | /// 17 | /// Cache or runtime objects that will be accessed from patched code 18 | /// 19 | /// 20 | private static readonly Dictionary RuntimeInstances = new Dictionary(); 21 | 22 | /// 23 | /// Name of the module this instance is assigned to 24 | /// 25 | private string _moduleName; 26 | 27 | /// 28 | /// Runtime instance of this extender 29 | /// 30 | private UIExtenderRuntime _runtime; 31 | 32 | /// 33 | /// Default constructor. `moduleName` should match module folder because it will be used to look-up resources 34 | /// 35 | /// Module name, should match module folder 36 | public UIExtender(string moduleName) 37 | { 38 | _moduleName = moduleName; 39 | } 40 | 41 | /// 42 | /// Register extension types from calling assembly. 43 | /// Should be called during `OnSubModuleLoad` 44 | /// 45 | public void Register() 46 | { 47 | Register(Assembly.GetCallingAssembly()); 48 | } 49 | 50 | /// 51 | /// Register extension types from specified assembly 52 | /// Should be called during `OnSubModuleLoad`, called by `Register` 53 | /// 54 | /// 55 | public void Register(Assembly assembly) 56 | { 57 | CheckNotCompatibleVersions(); 58 | 59 | var types = assembly 60 | .GetTypes() 61 | .Where(t => t.CustomAttributes.Any(a => a.AttributeType.IsSubclassOf(typeof(UIExtenderLibExtension)))); 62 | 63 | if (RuntimeInstances.ContainsKey(_moduleName)) 64 | { 65 | Utils.DisplayUserError($"Failed to load extension module {_moduleName} - already loaded!"); 66 | return; 67 | } 68 | 69 | var runtime = new UIExtenderRuntime(_moduleName); 70 | _runtime = runtime; 71 | RuntimeInstances[_moduleName] = runtime; 72 | 73 | runtime.Register(types); 74 | } 75 | 76 | /// 77 | /// Verify registration and emit user messages if needed 78 | /// Required to be called during `OnBeforeInitialScreenSetAsRoot` 79 | /// 80 | public void Verify() 81 | { 82 | if (Utils.SoftAssert(_runtime != null, $"Verify() called before Register()!")) 83 | { 84 | _runtime.Verify(); 85 | } 86 | } 87 | 88 | /// 89 | /// Get specified module runtime from the cache. Called from patches 90 | /// 91 | /// name of the module that produced this patch 92 | /// 93 | internal static UIExtenderRuntime RuntimeFor(string moduleName) 94 | { 95 | return RuntimeInstances[moduleName]; 96 | } 97 | 98 | /// 99 | /// Check for previous not compatible versions of UIExtenderLib 100 | /// 101 | private void CheckNotCompatibleVersions() 102 | { 103 | var mods = Utilities.GetModulesNames(); 104 | if (mods.Contains("0UIExtenderLibModule") || mods.Contains("UIExtenderLibModule")) 105 | { 106 | Debug.Fail($"UIExtender version >= 2 is not supported with UIExtenderLibModule.\n" + 107 | $"Please disable UIExtenderLibModule.\n\n" + 108 | $"Modules doesn't need to use it anymore and should be updated to newer version.'"); 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /UIExtenderLib/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Reflection; 6 | using TaleWorlds.Core; 7 | using TaleWorlds.Engine; 8 | using TaleWorlds.Library; 9 | using Debug = System.Diagnostics.Debug; 10 | using Path = System.IO.Path; 11 | 12 | namespace UIExtenderLib 13 | { 14 | public static class Utils 15 | { 16 | /// 17 | /// Non-critical failure, should only crash on debug 18 | /// 19 | /// 20 | public static void SoftFail(string text) 21 | { 22 | if (Debugger.IsAttached) 23 | { 24 | Debug.Fail(text); 25 | } 26 | } 27 | 28 | /// 29 | /// Non-critical assert, should only crash on debug, otherwise return bool of the condition 30 | /// 31 | /// 32 | /// 33 | /// 34 | public static bool SoftAssert(bool condition, string text) 35 | { 36 | if (Debugger.IsAttached) 37 | { 38 | Debug.Assert(condition, text); 39 | } 40 | 41 | return condition; 42 | } 43 | 44 | /// 45 | /// Critical runtime compatibility assert. Used when Bannerlord version is not compatible and it 46 | /// prevents runtime from functioning 47 | /// 48 | /// 49 | /// 50 | public static void CompatAssert(bool condition, string text = "no description") 51 | { 52 | Debug.Assert(condition, $"Bannerlord compatibility failure: {text}."); 53 | } 54 | 55 | /// 56 | /// Display error message to the end user 57 | /// 58 | /// 59 | /// 60 | public static void DisplayUserError(string text, params object[] args) 61 | { 62 | InformationManager.DisplayMessage(new InformationMessage("UIExtender: " + String.Format(text, args), Colors.Red)); 63 | } 64 | 65 | /// 66 | /// Display warning message to the end user 67 | /// 68 | /// 69 | /// 70 | public static void UserWarning(string text, params object[] args) 71 | { 72 | InformationManager.DisplayMessage(new InformationMessage("UIExtender: " + String.Format(text, args), Colors.Yellow)); 73 | } 74 | 75 | public static string GeneratedDllDirectory() 76 | { 77 | var path = Path.Combine(Utilities.GetBasePath(), "bin", "UIExtenderLib_gen_dll"); 78 | Directory.CreateDirectory(path); 79 | return path; 80 | } 81 | } 82 | 83 | public static class IDictionaryExtensions 84 | { 85 | /// 86 | /// Extension method on IDictionary that will either return value for key (if exist), 87 | /// or call `Func def` to create it, store and then return 88 | /// 89 | /// 90 | /// key to use 91 | /// function that returns value if not exists 92 | /// 93 | /// 94 | /// 95 | public static V Get(this IDictionary o, K key, Func def) 96 | { 97 | V value; 98 | if (o.TryGetValue(key, out value)) 99 | { 100 | return value; 101 | } 102 | else 103 | { 104 | value = def(); 105 | o[key] = value; 106 | return value; 107 | } 108 | } 109 | } 110 | 111 | public static class ReflectionHelpers 112 | { 113 | public static T PrivateValue(this object o, string fieldName) 114 | { 115 | var field = o.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 116 | Utils.CompatAssert(field != null, $"private value getter on {o}.{fieldName}"); 117 | return (T) field.GetValue(o); 118 | } 119 | 120 | public static void PrivateValueSet(this object o, string fieldName, T value) 121 | { 122 | var field = o.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 123 | Utils.CompatAssert(field != null, $"private value setter on {o}.{fieldName}"); 124 | field.SetValue(o, value); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /UIExtenderLib/CodePatcher/StaticLibrary/UIExtenderRuntimeLib.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Xml; 6 | using TaleWorlds.Library; 7 | 8 | namespace UIExtenderLib.CodePatcher.StaticLibrary 9 | { 10 | /// 11 | /// Library of general functions that are called in generated IL code (in order for it to be more simplistic). 12 | /// Most of this methods are simple proxy-calls to methods further down in hierarchy. Most are called in generated extended VM classes. 13 | /// 14 | public static class UIExtenderRuntimeLib 15 | { 16 | /// 17 | /// Proxy method to ViewModelComponent.MixinInstanceForObject 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static object MixinInstanceForVMInstance(string moduleName, Type mixinType, object instance) 24 | { 25 | return UIExtender.RuntimeFor(moduleName).ViewModelComponent.MixinInstanceForVMInstance(mixinType, instance); 26 | } 27 | 28 | /// 29 | /// Proxy method to ViewModelComponent.InitializeMixinsForViewModelInstance 30 | /// 31 | /// 32 | /// 33 | /// 34 | public static void InitializeMixinsForVMInstance(string moduleName, Type t, object instance) 35 | { 36 | var runtime = UIExtender.RuntimeFor(moduleName); 37 | 38 | // Verify should have been called at this point 39 | Utils.SoftAssert(runtime.VerifyCalled, $"UIExtender.Verify was not called!"); 40 | 41 | runtime.ViewModelComponent.InitializeMixinsForVMInstance(t, instance); 42 | } 43 | 44 | /// 45 | /// Proxy method to ViewModelComponent.DestructMixinsForInstance 46 | /// 47 | /// 48 | /// 49 | public static void DestructMixinsForVMInstance(string moduleName, object instance) 50 | { 51 | UIExtender.RuntimeFor(moduleName).ViewModelComponent.DestructMixinsForVMInstance(instance); 52 | } 53 | 54 | /// 55 | /// Proxy method to ViewModelComponent.FinalizeMixinsForInstance 56 | /// 57 | /// 58 | /// 59 | public static void FinalizeMixinsForVMInstance(string moduleName, object instance) 60 | { 61 | UIExtender.RuntimeFor(moduleName).ViewModelComponent.FinalizeMixinForVMInstance(instance); 62 | } 63 | 64 | /// 65 | /// Load XmlDocument with file at path. 66 | /// Equivalent XML code from `WidgetPrefab.LoadFrom`, which is called from patch. 67 | /// Original loading code was removed in order to make space for patch instructions. 68 | /// 69 | /// 70 | /// 71 | /// 72 | public static void LoadXmlDocument(string moduleName, string path, XmlDocument document) 73 | { 74 | using (XmlReader xmlReader = XmlReader.Create(path, new XmlReaderSettings 75 | { 76 | IgnoreComments = true, 77 | IgnoreWhitespace = true, 78 | })) 79 | { 80 | document.Load(xmlReader); 81 | } 82 | } 83 | 84 | /// 85 | /// Method inserted into `WidgetFactory.LoadFrom`, which patches XmlDocument. 86 | /// A number of those are run in sequence for each extension. 87 | /// 88 | /// 89 | /// 90 | /// 91 | public static void ProcessMovieDocumentIfNeeded(string moduleName, string path, XmlDocument document) 92 | { 93 | // currently replicates exactly what WidgetFactory is doing 94 | var movieName = Path.GetFileNameWithoutExtension(path); 95 | 96 | // proxy call to ProcessMovieIfNeeded 97 | UIExtender.RuntimeFor(moduleName).PrefabComponent.ProcessMovieIfNeeded(movieName, document); 98 | } 99 | 100 | /// 101 | /// Method that recursively searches for MethodInfo of method named `name`. Used in `ViewModel.ExecuteCommand` in order 102 | /// for it to recognize base class methods. 103 | /// 104 | /// 105 | /// 106 | /// 107 | /// 108 | public static MethodInfo FindExecuteCommandTargetMethod(Type t, string name, BindingFlags flags) 109 | { 110 | if (t == null) 111 | { 112 | return null; 113 | } 114 | 115 | var method = t.GetMethod(name, flags | BindingFlags.FlattenHierarchy); 116 | return method != null ? method : FindExecuteCommandTargetMethod(t.BaseType, name, flags); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /UIExtenderLib/CodePatcher/BuiltInPatches/ViewModelPatches.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Reflection; 5 | using SandBox.GauntletUI; 6 | using SandBox.GauntletUI.Map; 7 | using TaleWorlds.CampaignSystem; 8 | using TaleWorlds.CampaignSystem.ViewModelCollection; 9 | using TaleWorlds.CampaignSystem.ViewModelCollection.CharacterDeveloper; 10 | using TaleWorlds.CampaignSystem.ViewModelCollection.Map; 11 | using TaleWorlds.Core; 12 | using TaleWorlds.MountAndBlade.GauntletUI; 13 | using TaleWorlds.MountAndBlade.ViewModelCollection.Multiplayer; 14 | 15 | namespace UIExtenderLib.CodePatcher.BuiltInPatches 16 | { 17 | /// 18 | /// Set of view model specific patches. 19 | /// Some of them may fail because of outdated library, but given that the mod in question 20 | /// doesn't use that specific view model classes it should still function just fine (albeit 21 | /// user warning will be displayed on the main menu). 22 | /// 23 | internal class ViewModelPatches 24 | { 25 | internal enum Result: int 26 | { 27 | Success = 1, 28 | Failure = 2, 29 | Partial = 3, 30 | } 31 | 32 | internal static Result AddTo(CodePatcherComponent comp) 33 | { 34 | int value = 0; 35 | 36 | value |= (int)ThreeMapVMPatches(comp); 37 | value |= (int)PartyVMPatch(comp); 38 | value |= (int)MissionAgentStatusVMPatch(comp); 39 | value |= (int)MapVMPatch(comp); 40 | value |= (int)CharacterVMPatch(comp); 41 | 42 | if (Enum.IsDefined(typeof(Result), value)) 43 | { 44 | return (Result) value; 45 | } 46 | else 47 | { 48 | Debug.Fail($"Invalid enum usage!"); 49 | return Result.Failure; 50 | } 51 | } 52 | 53 | private static Result ThreeMapVMPatches(CodePatcherComponent comp) 54 | { 55 | var callsite = typeof(MapVM).GetConstructor(new Type[] 56 | { 57 | typeof(INavigationHandler), typeof(IMapStateHandler), typeof(MapBarShortcuts), typeof(Action) 58 | }); 59 | 60 | if (callsite == null) 61 | { 62 | return Result.Failure; 63 | } 64 | 65 | var dependentRefreshMethods = new[] 66 | { 67 | typeof(MapInfoVM).GetMethod(nameof(MapInfoVM.Refresh)), 68 | typeof(MapTimeControlVM).GetMethod(nameof(MapTimeControlVM.Refresh)), 69 | typeof(MapNavigationVM).GetMethod(nameof(MapNavigationVM.Refresh)), 70 | }; 71 | 72 | if (dependentRefreshMethods.Any(e => e == null)) 73 | { 74 | return Result.Failure; 75 | } 76 | 77 | comp.AddViewModelInstantiationPatch(callsite); 78 | foreach (var method in dependentRefreshMethods) 79 | { 80 | comp.AddViewModelRefreshPatch(method); 81 | } 82 | 83 | return Result.Success; 84 | } 85 | 86 | private static Result PartyVMPatch(CodePatcherComponent comp) 87 | { 88 | Type type = typeof(GauntletPartyScreen); 89 | MethodInfo interfaceMethod = type.GetInterface(nameof(IGameStateListener)).GetMethod("OnActivate"); 90 | if (interfaceMethod == null) 91 | { 92 | return Result.Failure; 93 | } 94 | 95 | InterfaceMapping map = type.GetInterfaceMap(interfaceMethod.DeclaringType ?? throw new NullReferenceException("Cannot find GauntletPartyScreen IGameStateListener.OnActivate method")); 96 | var callsite = map.TargetMethods[Array.IndexOf(map.InterfaceMethods, interfaceMethod)]; 97 | if (callsite == null) 98 | { 99 | return Result.Failure; 100 | } 101 | 102 | comp.AddViewModelInstantiationPatch(callsite); 103 | return Result.Success; 104 | } 105 | 106 | private static Result MissionAgentStatusVMPatch(CodePatcherComponent comp) 107 | { 108 | var callsite = typeof(MissionGauntletAgentStatus).GetMethod(nameof(MissionGauntletAgentStatus.EarlyStart)); 109 | if (callsite == null) 110 | { 111 | return Result.Failure; 112 | } 113 | 114 | var refresh = typeof(MissionAgentStatusVM).GetMethod(nameof(MissionAgentStatusVM.Tick)); 115 | if (refresh == null) 116 | { 117 | return Result.Failure; 118 | } 119 | 120 | comp.AddViewModelInstantiationPatch(callsite); 121 | comp.AddViewModelRefreshPatch(refresh); 122 | return Result.Success; 123 | } 124 | 125 | private static Result MapVMPatch(CodePatcherComponent comp) 126 | { 127 | var callsite = typeof(GauntlerMapBarGlobalLayer).GetMethod(nameof(GauntlerMapBarGlobalLayer.Initialize)); 128 | if (callsite == null) 129 | { 130 | return Result.Failure; 131 | } 132 | 133 | var refresh = typeof(MapVM).GetMethod(nameof(MapVM.OnRefresh)); 134 | if (refresh == null) 135 | { 136 | return Result.Failure; 137 | } 138 | 139 | comp.AddViewModelInstantiationPatch(callsite); 140 | comp.AddViewModelRefreshPatch(refresh); 141 | return Result.Success; 142 | } 143 | 144 | private static Result CharacterVMPatch(CodePatcherComponent comp) 145 | { 146 | var callsite = typeof(CharacterDeveloperVM).GetConstructor(new Type[] {typeof(Action)}); 147 | if (callsite == null) 148 | { 149 | return Result.Failure; 150 | } 151 | 152 | var refresh = typeof(CharacterVM).GetMethod(nameof(CharacterVM.RefreshValues)); 153 | if (refresh == null) 154 | { 155 | return Result.Failure; 156 | } 157 | 158 | comp.AddViewModelInstantiationPatch(callsite); 159 | comp.AddViewModelRefreshPatch(refresh); 160 | return Result.Success; 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /UIExtenderLib/CodePatcher/CodePatchesAssemblyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Reflection.Emit; 8 | using HarmonyLib; 9 | using TaleWorlds.Engine; 10 | using Path = System.IO.Path; 11 | 12 | namespace UIExtenderLib.PatchAssembly 13 | { 14 | /// 15 | /// AssemblyBuilder-like class which represents mod-specific assembly 16 | /// containing extended view models (generated by ViewModelComponent) and 17 | /// Harmony patches (generated by CodePatcherComponent into UIExtender_ModAssembly_MODULENAME_StaticLibrary class). 18 | /// 19 | internal class CodePatchesAssemblyBuilder 20 | { 21 | internal static string TypeNamePrefix = "UIExtender_"; 22 | 23 | private readonly string _moduleName; 24 | private readonly string _dllName; 25 | private readonly AssemblyBuilder _assemblyBuilder; 26 | private readonly ModuleBuilder _moduleBuilder; 27 | 28 | private readonly TypeBuilder _staticLibType; 29 | private readonly string _staticLibName; 30 | 31 | internal CodePatchesAssemblyBuilder(string moduleName) 32 | { 33 | _moduleName = moduleName; 34 | 35 | _dllName = $"UIExtender_PatchAssembly_{_moduleName}.dll"; 36 | var assembly = new AssemblyName($"UIExtender_PatchAssembly_{_moduleName}"); 37 | _assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assembly, AssemblyBuilderAccess.Run | AssemblyBuilderAccess.Save); 38 | _moduleBuilder = _assemblyBuilder.DefineDynamicModule( 39 | $"UIExtender_PatchAssemblyModule_{_moduleName}", 40 | _dllName 41 | ); 42 | 43 | _staticLibName = $"UIExtender_ModAssembly_{_moduleName}_StaticLibrary"; 44 | _staticLibType = _moduleBuilder.DefineType(_staticLibName); 45 | } 46 | 47 | /// 48 | /// Add transpiler to library class. `staticArgs` will be added in front of usual IEnumerable argument. 49 | /// 50 | /// 51 | /// transpiler this method will be proxied to 52 | /// array of static arguments that will prepend Harmony's CodeInstruction array. Only strings, integers, Types and array of Types are supported as of now 53 | internal void AddTranspiler(string name, MethodInfo proxyTo, params object[] staticArgs) 54 | { 55 | var method = _staticLibType.DefineMethod( 56 | name, 57 | MethodAttributes.Static | MethodAttributes.Public, 58 | typeof(IEnumerable), 59 | new Type[] {typeof(IEnumerable)} 60 | ); 61 | 62 | var gen = method.GetILGenerator(); 63 | ILLoadArguments(gen, staticArgs); 64 | gen.Emit(OpCodes.Ldarg_0); 65 | gen.EmitCall(OpCodes.Call, proxyTo, null); 66 | gen.Emit(OpCodes.Ret); 67 | } 68 | 69 | /// 70 | /// Add postfix to library class. `staticArgs` will be added in front of __instance argument. 71 | /// 72 | /// 73 | /// postfix this method will be proxied to. Required to accept __instance argument 74 | /// array of static arguments that will prepend Harmony's CodeInstruction array. Only strings and int's are supported as of now 75 | internal void AddPostfix(string name, MethodInfo proxyTo, params object[] staticArgs) 76 | { 77 | var method = _staticLibType.DefineMethod( 78 | name, 79 | MethodAttributes.Static | MethodAttributes.Public, 80 | null, 81 | new Type[] {typeof(object)} 82 | ); 83 | 84 | // state name of the parameter in order for Harmony to recognize it 85 | method.DefineParameter(1, ParameterAttributes.None, "__instance"); 86 | 87 | var gen = method.GetILGenerator(); 88 | ILLoadArguments(gen, staticArgs); 89 | gen.Emit(OpCodes.Ldarg_0); 90 | gen.EmitCall(OpCodes.Call, proxyTo, null); 91 | gen.Emit(OpCodes.Ret); 92 | } 93 | 94 | /// 95 | /// Save generated dll and load static library class from it. 96 | /// Finalizes dll meaning `AddX` methods can no longer be used. 97 | /// 98 | /// 99 | internal Type SaveAndLoadLibraryType() 100 | { 101 | _staticLibType.CreateType(); 102 | _assemblyBuilder.Save(_dllName); 103 | 104 | var targetPath = Path.Combine(Utils.GeneratedDllDirectory(), _dllName); 105 | if (File.Exists(targetPath)) 106 | { 107 | File.Delete(targetPath); 108 | } 109 | 110 | File.Move(_dllName, targetPath); 111 | var assembly = Assembly.LoadFile(Path.GetFullPath(targetPath)); 112 | return assembly.GetType(_staticLibName); 113 | } 114 | 115 | private static void ILLoadArguments(ILGenerator gen, object[] args) 116 | { 117 | foreach (var arg in args) 118 | { 119 | switch (arg) 120 | { 121 | case string str: 122 | gen.Emit(OpCodes.Ldstr, str); 123 | break; 124 | case int num: 125 | gen.Emit(OpCodes.Ldc_I4, num); 126 | break; 127 | case Type t: 128 | gen.Emit(OpCodes.Ldtoken, t); 129 | gen.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)), null); 130 | break; 131 | case IEnumerable ta: 132 | gen.Emit(OpCodes.Ldc_I4, ta.Count()); 133 | gen.Emit(OpCodes.Newarr, typeof(Type)); 134 | gen.Emit(OpCodes.Dup); 135 | for (int i = 0; i < ta.Count(); i++) 136 | { 137 | var value = ta.ElementAt(i); 138 | gen.Emit(OpCodes.Ldc_I4, i); 139 | gen.Emit(OpCodes.Ldtoken, value); 140 | gen.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)), null); 141 | gen.Emit(OpCodes.Stelem_Ref); 142 | } 143 | break; 144 | default: 145 | Debug.Fail($"Type of {arg} is not supported by this method!"); 146 | break; 147 | } 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /UIExtenderLib/CodePatcher/CodePatcherComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Reflection.Emit; 8 | using System.Security.Principal; 9 | using System.Xml; 10 | using HarmonyLib; 11 | using TaleWorlds.Core; 12 | using TaleWorlds.GauntletUI.PrefabSystem; 13 | using UIExtenderLib.CodePatcher.StaticLibrary; 14 | using UIExtenderLib.PatchAssembly; 15 | using UIExtenderLib.ViewModel; 16 | 17 | namespace UIExtenderLib.CodePatcher 18 | { 19 | /// 20 | /// Component that deals with code patching using Harmony. 21 | /// Adds static patches to PatchAssemblyBuilder later passing them to Harmony. 22 | /// 23 | internal class CodePatcherComponent 24 | { 25 | private readonly Harmony _harmony; 26 | private readonly UIExtenderRuntime _runtime; 27 | private readonly CodePatchesAssemblyBuilder _patchesAssemblyBuilder; 28 | 29 | private readonly Dictionary _transpilers = new Dictionary(); 30 | private readonly Dictionary _postfixes = new Dictionary(); 31 | 32 | internal CodePatcherComponent(UIExtenderRuntime runtime) 33 | { 34 | // check runtime harmony version 35 | CheckHarmonyVersion(); 36 | 37 | _harmony = new Harmony("net.uiextenderlib." + runtime.ModuleName); 38 | _runtime = runtime; 39 | _patchesAssemblyBuilder = new CodePatchesAssemblyBuilder(runtime.ModuleName); 40 | } 41 | 42 | /// 43 | /// Adds Harmony patch running InstantiationCallsiteTranspiler from PatchLibrary, 44 | /// which swaps original view model constructors with extended ones. 45 | /// Doesn't do actual patching, which is done in `Apply()` method. 46 | /// 47 | /// 48 | internal void AddViewModelInstantiationPatch(MethodBase callsite) 49 | { 50 | var patchName = nameof(UIExtenderPatchLib.InstantiationCallsiteTranspiler); 51 | var name = patchName + Guid.NewGuid().ToString(); 52 | _patchesAssemblyBuilder.AddTranspiler(name, GetPatchLibMethod(patchName), new object[] {_runtime.ModuleName}); 53 | _transpilers[name] = callsite; 54 | } 55 | 56 | /// 57 | /// Adds Harmony patch running RefreshPostfix from PatchLibrary, which appends mixin refresh code to view model refresh methods. 58 | /// Doesn't do actual patching, which is done in `Apply()` method. 59 | /// 60 | /// 61 | internal void AddViewModelRefreshPatch(MethodBase callsite) 62 | { 63 | var patchName = nameof(UIExtenderPatchLib.RefreshPostfix); 64 | var name = patchName + Guid.NewGuid().ToString(); 65 | 66 | _patchesAssemblyBuilder.AddPostfix( 67 | name, 68 | GetPatchLibMethod(patchName), 69 | new object[] {_runtime.ModuleName} 70 | ); 71 | _postfixes[name] = callsite; 72 | } 73 | 74 | /// 75 | /// Adds Harmony patch running PrefabLoadTranspiler from PatchLib, patching `WidgetFactory.LoadFrom` in order for it 76 | /// to load extensions. 77 | /// Doesn't do actual patching, which is done in `Apply()` method. 78 | /// 79 | /// 80 | internal void AddWidgetLoadPatch(MethodBase callsite) 81 | { 82 | var patchName = nameof(UIExtenderPatchLib.PrefabLoadTranspiler); 83 | var name = patchName + Guid.NewGuid().ToString(); 84 | 85 | _patchesAssemblyBuilder.AddTranspiler(name, GetPatchLibMethod(patchName), new object[] {_runtime.ModuleName}); 86 | _transpilers[name] = callsite; 87 | } 88 | 89 | /// 90 | /// Adds Harmony patch running ViewModelExecuteTranspiler from PatchLib, fixing it's lookup issues brought by inheritance. 91 | /// Doesn't do actual patching, which is done in `Apply()` method. 92 | /// 93 | /// 94 | internal void AddViewModelExecutePatch(MethodBase callsite) 95 | { 96 | var patchName = nameof(UIExtenderPatchLib.ViewModelExecuteTranspiler); 97 | var name = patchName + Guid.NewGuid().ToString(); 98 | _patchesAssemblyBuilder.AddTranspiler(name, GetPatchLibMethod(patchName)); 99 | _transpilers[name] = callsite; 100 | } 101 | 102 | /// 103 | /// Apply added patches (actually patch game code). 104 | /// After this call underlying assembly will be finalized and `AddX` methods can no longer be used. 105 | /// 106 | internal void ApplyPatches() 107 | { 108 | // finalize assembly builder and load static library class from it 109 | var staticLibType = _patchesAssemblyBuilder.SaveAndLoadLibraryType(); 110 | 111 | foreach (var kv in _transpilers) 112 | { 113 | var method = staticLibType.GetMethod(kv.Key); 114 | _harmony.Patch(kv.Value, transpiler: new HarmonyMethod(method)); 115 | } 116 | 117 | foreach (var kv in _postfixes) 118 | { 119 | var method = staticLibType.GetMethod(kv.Key); 120 | _harmony.Patch(kv.Value, postfix: new HarmonyMethod(method)); 121 | } 122 | } 123 | 124 | /// 125 | /// Helper method to find MethodInfo of specified patch from PatchLib 126 | /// 127 | /// 128 | /// 129 | private MethodInfo GetPatchLibMethod(string name) 130 | { 131 | return typeof(UIExtenderPatchLib).GetMethod(name, BindingFlags.Static | BindingFlags.Public); 132 | } 133 | 134 | /// 135 | /// Check loaded Harmony version. 136 | /// Bannerlord only loads first dll it can find without regards to it's version, meaning that the library can 137 | /// end up with outdated harmony at runtime. 138 | /// 139 | private void CheckHarmonyVersion() 140 | { 141 | var version = typeof(Harmony).Assembly.ImageRuntimeVersion; 142 | var majorVersion = int.Parse("" + version.Skip(1).ElementAt(0)); 143 | if (majorVersion < 2) 144 | { 145 | Debug.Fail($"Loaded Harmony version {version} is not supported by UIExtenderLib.\n"+ 146 | "You need to get rid of modules using Harmony versions < 2.0.0 since they're not compatible.\n"+ 147 | "See https://github.com/shdwp/UIExtenderLib/wiki/Mismatched-Harmony-versions."); 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /UIExtenderLib/UIExtenderLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {F17CCC8F-1E87-4B3C-B4EF-E03DD062322E} 8 | Library 9 | Properties 10 | UIExtenderLib 11 | UIExtenderLib_2.0.0.3 12 | v4.7.2 13 | 512 14 | 15 | 16 | x64 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\lib\0Harmony_2.0.0.10.dll 37 | False 38 | 39 | 40 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\Modules\SandBox\bin\Win64_Shipping_Client\SandBox.GauntletUI.dll 41 | 42 | 43 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\Modules\SandBox\bin\Win64_Shipping_Client\SandBox.View.dll 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.CampaignSystem.dll 52 | 53 | 54 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.CampaignSystem.ViewModelCollection.dll 55 | 56 | 57 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Core.dll 58 | 59 | 60 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.dll 61 | 62 | 63 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.GauntletUI.dll 64 | 65 | 66 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.PrefabSystem.dll 67 | 68 | 69 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Library.dll 70 | False 71 | 72 | 73 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.dll 74 | 75 | 76 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\Modules\Native\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.GauntletUI.dll 77 | 78 | 79 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\Modules\Native\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.View.dll 80 | 81 | 82 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.ViewModelCollection.dll 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 110 | -------------------------------------------------------------------------------- /UIExtenderLib/UIExtenderRuntime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Xml; 4 | using TaleWorlds.Core; 5 | using TaleWorlds.Library; 6 | using UIExtenderLib.CodePatcher; 7 | using UIExtenderLib.CodePatcher.BuiltInPatches; 8 | using UIExtenderLib.Interface; 9 | using UIExtenderLib.Prefab; 10 | using UIExtenderLib.ViewModel; 11 | using Debug = System.Diagnostics.Debug; 12 | 13 | namespace UIExtenderLib 14 | { 15 | /// 16 | /// Actual runtime of UIExtender, assigned to each module's instance of `UIExtender` 17 | /// 18 | internal class UIExtenderRuntime 19 | { 20 | /// 21 | /// Name of the module this runtime is assigned to 22 | /// 23 | internal readonly string ModuleName; 24 | 25 | /// 26 | /// Instance of PrefabComponent, which deals with XML files 27 | /// 28 | internal readonly PrefabComponent PrefabComponent; 29 | 30 | /// 31 | /// Instance of ViewModelComponent, which deals with child classes of `ViewModel` 32 | /// 33 | internal readonly ViewModelComponent ViewModelComponent; 34 | 35 | /// 36 | /// Instance of CodePatcherComponent, which deals with Harmony 37 | /// 38 | internal readonly CodePatcherComponent CodePatcher; 39 | 40 | /// 41 | /// Whether `Verify()` was actually called 42 | /// 43 | internal bool VerifyCalled = false; 44 | 45 | /// 46 | /// User messages to be displayed during `Verify()` call 47 | /// 48 | private List _userMessages = new List(); 49 | 50 | /// 51 | /// Constructor 52 | /// 53 | /// Name of the module this runtime is assigned to 54 | internal UIExtenderRuntime(string moduleName) 55 | { 56 | ModuleName = moduleName; 57 | 58 | PrefabComponent = new PrefabComponent(moduleName); 59 | ViewModelComponent = new ViewModelComponent(moduleName); 60 | CodePatcher = new CodePatcherComponent(this); 61 | } 62 | 63 | /// 64 | /// Display user warning when `Verify()` will be called 65 | /// 66 | /// 67 | internal void AddUserWarning(string text) 68 | { 69 | _userMessages.Add(new InformationMessage(text, Colors.Yellow)); 70 | } 71 | 72 | /// 73 | /// Display user error when `Verify()` will be called 74 | /// 75 | /// 76 | internal void AddUserError(string text) 77 | { 78 | _userMessages.Add(new InformationMessage(text, Colors.Red)); 79 | } 80 | 81 | /// 82 | /// Register types attributed with `UIExtenderLibExtension`: 83 | /// 1. will add extensions to their respective components 84 | /// 2. will add standard patches and patch game 85 | /// 3. will force game to reload affected XMLs 86 | /// 87 | /// 88 | internal void Register(IEnumerable types) 89 | { 90 | foreach (var extensionType in types) 91 | { 92 | var baseAttribute = Attribute.GetCustomAttribute(extensionType, typeof(UIExtenderLibExtension)); 93 | switch (baseAttribute) 94 | { 95 | case PrefabExtension xmlExtension: 96 | { 97 | var constructor = extensionType.GetConstructor(new Type[] { }); 98 | if (constructor == null) 99 | { 100 | Debug.Fail("Failed to find appropriate constructor for patch!"); 101 | } 102 | 103 | // gauntlet xml extension 104 | switch (constructor.Invoke(new object[]{})) 105 | { 106 | case PrefabExtensionInsertPatch patch: 107 | PrefabComponent.RegisterPatch(xmlExtension.Movie, xmlExtension.XPath, patch); 108 | break; 109 | 110 | case PrefabExtensionReplacePatch patch: 111 | PrefabComponent.RegisterPatch(xmlExtension.Movie, xmlExtension.XPath, patch); 112 | break; 113 | 114 | case PrefabExtensionInsertAsSiblingPatch patch: 115 | PrefabComponent.RegisterPatch(xmlExtension.Movie, xmlExtension.XPath, patch); 116 | break; 117 | 118 | case CustomPatch patch: 119 | PrefabComponent.RegisterPatch(xmlExtension.Movie, patch.Apply); 120 | break; 121 | 122 | case CustomPatch patch: 123 | PrefabComponent.RegisterPatch(xmlExtension.Movie, xmlExtension.XPath, patch.Apply); 124 | break; 125 | 126 | default: 127 | Debug.Fail($"Patch class is unsupported - {extensionType}!"); 128 | break; 129 | } 130 | 131 | break; 132 | } 133 | 134 | case ViewModelMixin _: 135 | // view model mixin 136 | ViewModelComponent.RegisterViewModelMixin(extensionType); 137 | break; 138 | 139 | default: 140 | Debug.Fail($"Failed to find appropriate clause for base type {extensionType} with attribute {baseAttribute}!"); 141 | break; 142 | } 143 | } 144 | 145 | var patchingResult = ViewModelPatches.Result.Success; 146 | // add core patches (in order for UIExtender to actually work) 147 | if (!CorePatches.AddTo(CodePatcher)) 148 | { 149 | _userMessages.Add(new InformationMessage($"Failed to patch {ModuleName} (outdated).", Colors.Red)); 150 | return; 151 | } 152 | 153 | patchingResult = ViewModelPatches.AddTo(CodePatcher); 154 | switch (patchingResult) 155 | { 156 | case ViewModelPatches.Result.Success: 157 | break; 158 | 159 | case ViewModelPatches.Result.Partial: 160 | AddUserWarning($"There were errors on {ModuleName} patching. Some functionality may not work (module outdated)."); 161 | break; 162 | 163 | case ViewModelPatches.Result.Failure: 164 | AddUserError($"Failed to patch {ModuleName} (outdated)."); 165 | break; 166 | } 167 | 168 | // finalize code patcher and let harmony apply patches 169 | CodePatcher.ApplyPatches(); 170 | 171 | // save .dll for troubleshooting 172 | ViewModelComponent.SaveDebugImages(); 173 | 174 | // force reload movies that should be patched by extensions 175 | PrefabComponent.ForceReloadMovies(); 176 | } 177 | 178 | /// 179 | /// Verify registration and emit user warnings/errors if needed 180 | /// 181 | internal void Verify() 182 | { 183 | VerifyCalled = true; 184 | 185 | foreach (var message in _userMessages) 186 | { 187 | InformationManager.DisplayMessage(message); 188 | } 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Common IntelliJ Platform excludes 2 | 3 | # User specific 4 | **/.idea/**/workspace.xml 5 | **/.idea/**/tasks.xml 6 | **/.idea/shelf/* 7 | **/.idea/dictionaries 8 | 9 | # Sensitive or high-churn files 10 | **/.idea/**/dataSources/ 11 | **/.idea/**/dataSources.ids 12 | **/.idea/**/dataSources.xml 13 | **/.idea/**/dataSources.local.xml 14 | **/.idea/**/sqlDataSources.xml 15 | **/.idea/**/dynamic.xml 16 | 17 | # Rider 18 | # Rider auto-generates .iml files, and contentModel.xml 19 | **/.idea/**/*.iml 20 | **/.idea/**/contentModel.xml 21 | **/.idea/**/modules.xml 22 | 23 | *.suo 24 | *.user 25 | .vs/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | _UpgradeReport_Files/ 29 | [Pp]ackages/ 30 | 31 | Thumbs.db 32 | Desktop.ini 33 | .DS_Store 34 | 35 | ## Ignore Visual Studio temporary files, build results, and 36 | ## files generated by popular Visual Studio add-ons. 37 | ## 38 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 39 | 40 | # User-specific files 41 | *.rsuser 42 | *.suo 43 | *.user 44 | *.userosscache 45 | *.sln.docstates 46 | 47 | # User-specific files (MonoDevelop/Xamarin Studio) 48 | *.userprefs 49 | 50 | # Mono auto generated files 51 | mono_crash.* 52 | 53 | # Build results 54 | [Dd]ebug/ 55 | [Dd]ebugPublic/ 56 | [Rr]elease/ 57 | [Rr]eleases/ 58 | x64/ 59 | x86/ 60 | [Aa][Rr][Mm]/ 61 | [Aa][Rr][Mm]64/ 62 | bld/ 63 | [Bb]in/ 64 | [Oo]bj/ 65 | [Ll]og/ 66 | [Ll]ogs/ 67 | 68 | # Visual Studio 2015/2017 cache/options directory 69 | .vs/ 70 | # Uncomment if you have tasks that create the project's static files in wwwroot 71 | #wwwroot/ 72 | 73 | # Visual Studio 2017 auto generated files 74 | Generated\ Files/ 75 | 76 | # MSTest test Results 77 | [Tt]est[Rr]esult*/ 78 | [Bb]uild[Ll]og.* 79 | 80 | # NUnit 81 | *.VisualState.xml 82 | TestResult.xml 83 | nunit-*.xml 84 | 85 | # Build Results of an ATL Project 86 | [Dd]ebugPS/ 87 | [Rr]eleasePS/ 88 | dlldata.c 89 | 90 | # Benchmark Results 91 | BenchmarkDotNet.Artifacts/ 92 | 93 | # .NET Core 94 | project.lock.json 95 | project.fragment.lock.json 96 | artifacts/ 97 | 98 | # StyleCop 99 | StyleCopReport.xml 100 | 101 | # Files built by Visual Studio 102 | *_i.c 103 | *_p.c 104 | *_h.h 105 | *.ilk 106 | *.meta 107 | *.obj 108 | *.iobj 109 | *.pch 110 | *.pdb 111 | *.ipdb 112 | *.pgc 113 | *.pgd 114 | *.rsp 115 | *.sbr 116 | *.tlb 117 | *.tli 118 | *.tlh 119 | *.tmp 120 | *.tmp_proj 121 | *_wpftmp.csproj 122 | *.log 123 | *.vspscc 124 | *.vssscc 125 | .builds 126 | *.pidb 127 | *.svclog 128 | *.scc 129 | 130 | # Chutzpah Test files 131 | _Chutzpah* 132 | 133 | # Visual C++ cache files 134 | ipch/ 135 | *.aps 136 | *.ncb 137 | *.opendb 138 | *.opensdf 139 | *.sdf 140 | *.cachefile 141 | *.VC.db 142 | *.VC.VC.opendb 143 | 144 | # Visual Studio profiler 145 | *.psess 146 | *.vsp 147 | *.vspx 148 | *.sap 149 | 150 | # Visual Studio Trace Files 151 | *.e2e 152 | 153 | # TFS 2012 Local Workspace 154 | $tf/ 155 | 156 | # Guidance Automation Toolkit 157 | *.gpState 158 | 159 | # ReSharper is a .NET coding add-in 160 | _ReSharper*/ 161 | *.[Rr]e[Ss]harper 162 | *.DotSettings.user 163 | 164 | # TeamCity is a build add-in 165 | _TeamCity* 166 | 167 | # DotCover is a Code Coverage Tool 168 | *.dotCover 169 | 170 | # AxoCover is a Code Coverage Tool 171 | .axoCover/* 172 | !.axoCover/settings.json 173 | 174 | # Coverlet is a free, cross platform Code Coverage Tool 175 | coverage*[.json, .xml, .info] 176 | 177 | # Visual Studio code coverage results 178 | *.coverage 179 | *.coveragexml 180 | 181 | # NCrunch 182 | _NCrunch_* 183 | .*crunch*.local.xml 184 | nCrunchTemp_* 185 | 186 | # MightyMoose 187 | *.mm.* 188 | AutoTest.Net/ 189 | 190 | # Web workbench (sass) 191 | .sass-cache/ 192 | 193 | # Installshield output folder 194 | [Ee]xpress/ 195 | 196 | # DocProject is a documentation generator add-in 197 | DocProject/buildhelp/ 198 | DocProject/Help/*.HxT 199 | DocProject/Help/*.HxC 200 | DocProject/Help/*.hhc 201 | DocProject/Help/*.hhk 202 | DocProject/Help/*.hhp 203 | DocProject/Help/Html2 204 | DocProject/Help/html 205 | 206 | # Click-Once directory 207 | publish/ 208 | 209 | # Publish Web Output 210 | *.[Pp]ublish.xml 211 | *.azurePubxml 212 | # Note: Comment the next line if you want to checkin your web deploy settings, 213 | # but database connection strings (with potential passwords) will be unencrypted 214 | *.pubxml 215 | *.publishproj 216 | 217 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 218 | # checkin your Azure Web App publish settings, but sensitive information contained 219 | # in these scripts will be unencrypted 220 | PublishScripts/ 221 | 222 | # NuGet Packages 223 | *.nupkg 224 | # NuGet Symbol Packages 225 | *.snupkg 226 | # The packages folder can be ignored because of Package Restore 227 | **/[Pp]ackages/* 228 | # except build/, which is used as an MSBuild target. 229 | !**/[Pp]ackages/build/ 230 | # Uncomment if necessary however generally it will be regenerated when needed 231 | #!**/[Pp]ackages/repositories.config 232 | # NuGet v3's project.json files produces more ignorable files 233 | *.nuget.props 234 | *.nuget.targets 235 | 236 | # Microsoft Azure Build Output 237 | csx/ 238 | *.build.csdef 239 | 240 | # Microsoft Azure Emulator 241 | ecf/ 242 | rcf/ 243 | 244 | # Windows Store app package directories and files 245 | AppPackages/ 246 | BundleArtifacts/ 247 | Package.StoreAssociation.xml 248 | _pkginfo.txt 249 | *.appx 250 | *.appxbundle 251 | *.appxupload 252 | 253 | # Visual Studio cache files 254 | # files ending in .cache can be ignored 255 | *.[Cc]ache 256 | # but keep track of directories ending in .cache 257 | !?*.[Cc]ache/ 258 | 259 | # Others 260 | ClientBin/ 261 | ~$* 262 | *~ 263 | *.dbmdl 264 | *.dbproj.schemaview 265 | *.jfm 266 | *.pfx 267 | *.publishsettings 268 | orleans.codegen.cs 269 | 270 | # Including strong name files can present a security risk 271 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 272 | #*.snk 273 | 274 | # Since there are multiple workflows, uncomment next line to ignore bower_components 275 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 276 | #bower_components/ 277 | 278 | # RIA/Silverlight projects 279 | Generated_Code/ 280 | 281 | # Backup & report files from converting an old project file 282 | # to a newer Visual Studio version. Backup files are not needed, 283 | # because we have git ;-) 284 | _UpgradeReport_Files/ 285 | Backup*/ 286 | UpgradeLog*.XML 287 | UpgradeLog*.htm 288 | ServiceFabricBackup/ 289 | *.rptproj.bak 290 | 291 | # SQL Server files 292 | *.mdf 293 | *.ldf 294 | *.ndf 295 | 296 | # Business Intelligence projects 297 | *.rdl.data 298 | *.bim.layout 299 | *.bim_*.settings 300 | *.rptproj.rsuser 301 | *- [Bb]ackup.rdl 302 | *- [Bb]ackup ([0-9]).rdl 303 | *- [Bb]ackup ([0-9][0-9]).rdl 304 | 305 | # Microsoft Fakes 306 | FakesAssemblies/ 307 | 308 | # GhostDoc plugin setting file 309 | *.GhostDoc.xml 310 | 311 | # Node.js Tools for Visual Studio 312 | .ntvs_analysis.dat 313 | node_modules/ 314 | 315 | # Visual Studio 6 build log 316 | *.plg 317 | 318 | # Visual Studio 6 workspace options file 319 | *.opt 320 | 321 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 322 | *.vbw 323 | 324 | # Visual Studio LightSwitch build output 325 | **/*.HTMLClient/GeneratedArtifacts 326 | **/*.DesktopClient/GeneratedArtifacts 327 | **/*.DesktopClient/ModelManifest.xml 328 | **/*.Server/GeneratedArtifacts 329 | **/*.Server/ModelManifest.xml 330 | _Pvt_Extensions 331 | 332 | # Paket dependency manager 333 | .paket/paket.exe 334 | paket-files/ 335 | 336 | # FAKE - F# Make 337 | .fake/ 338 | 339 | # CodeRush personal settings 340 | .cr/personal 341 | 342 | # Python Tools for Visual Studio (PTVS) 343 | __pycache__/ 344 | *.pyc 345 | 346 | # Cake - Uncomment if you are using it 347 | # tools/** 348 | # !tools/packages.config 349 | 350 | # Tabs Studio 351 | *.tss 352 | 353 | # Telerik's JustMock configuration file 354 | *.jmconfig 355 | 356 | # BizTalk build output 357 | *.btp.cs 358 | *.btm.cs 359 | *.odx.cs 360 | *.xsd.cs 361 | 362 | # OpenCover UI analysis results 363 | OpenCover/ 364 | 365 | # Azure Stream Analytics local run output 366 | ASALocalRun/ 367 | 368 | # MSBuild Binary and Structured Log 369 | *.binlog 370 | 371 | # NVidia Nsight GPU debugger configuration file 372 | *.nvuser 373 | 374 | # MFractors (Xamarin productivity tool) working folder 375 | .mfractor/ 376 | 377 | # Local History for Visual Studio 378 | .localhistory/ 379 | 380 | # BeatPulse healthcheck temp database 381 | healthchecksdb 382 | 383 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 384 | MigrationBackup/ 385 | 386 | # Ionide (cross platform F# VS Code tools) working folder 387 | .ionide/ 388 | -------------------------------------------------------------------------------- /UIExtenderLib/CodePatcher/StaticLibrary/UIExtenderPatchLib.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Reflection.Emit; 7 | using System.Xml; 8 | using HarmonyLib; 9 | using TaleWorlds.GauntletUI.PrefabSystem; 10 | 11 | namespace UIExtenderLib.CodePatcher.StaticLibrary 12 | { 13 | /// 14 | /// Library of general transpilers and postfixes. Used in both CorePatches and ViewModelPatches. 15 | /// Most of those methods require additional positional argument `moduleName`, hence they're not usable 16 | /// in itself with Harmony. Instead they are used in generated assembly managed by `CodePatcherComponent`. 17 | /// 18 | public class UIExtenderPatchLib 19 | { 20 | /// 21 | /// Transpiler which replaces constructors of view models with their expanded counterparts. 22 | /// Only replaces constructors which are affected by current mod. 23 | /// 24 | /// 25 | /// 26 | /// 27 | public static IEnumerable InstantiationCallsiteTranspiler(string moduleName, IEnumerable input) 28 | { 29 | return input.Select(op => 30 | { 31 | var component = UIExtender.RuntimeFor(moduleName).ViewModelComponent; 32 | if (op.opcode != OpCodes.Newobj) 33 | { 34 | return op; 35 | } 36 | 37 | var constructor = op.operand as ConstructorInfo; 38 | var type = constructor.DeclaringType; 39 | 40 | if (type == null || !type.IsSubclassOf(typeof(TaleWorlds.Library.ViewModel))) 41 | { 42 | return op; 43 | } 44 | 45 | var baseType = component.BaseTypeForPossiblyExtendedType(type); 46 | if (baseType != null && component.ExtendsViewModelType(baseType)) 47 | { 48 | op.operand = component.ExtendedViewModelTypeForType(baseType, type).GetConstructor(constructor.GetParameters().Types()); 49 | } 50 | 51 | return op; 52 | }); 53 | } 54 | 55 | /// 56 | /// Postfix that is used to call `OnRefresh()` on attached mixins 57 | /// 58 | /// 59 | /// 60 | public static void RefreshPostfix(string moduleName, object __instance) 61 | { 62 | UIExtender.RuntimeFor(moduleName).ViewModelComponent.RefreshMixinForVMInstance(__instance); 63 | } 64 | 65 | /// 66 | /// Transpiler for `LoadFrom` method that apply patches to loaded XML file 67 | /// 68 | /// 69 | /// 70 | /// 71 | public static IEnumerable PrefabLoadTranspiler(string moduleName, IEnumerable input) 72 | { 73 | var nopMarkName = "UIExtenderLib"; 74 | 75 | var loadXmlMethod = typeof(UIExtenderRuntimeLib).GetMethod(nameof(UIExtenderRuntimeLib.LoadXmlDocument)); 76 | var list = new List(input); 77 | 78 | DynamicMethod CreateDynamicMethod(MethodInfo parentMethod) 79 | { 80 | var method = new DynamicMethod($"{Guid.NewGuid()}", null, new [] { typeof(string), typeof(string), typeof(XmlDocument) }); 81 | var gen = method.GetILGenerator(); 82 | 83 | if (parentMethod != null) 84 | { 85 | gen.Emit(OpCodes.Ldarg_0); 86 | gen.Emit(OpCodes.Ldarg_1); 87 | gen.Emit(OpCodes.Ldarg_2); 88 | gen.EmitCall(OpCodes.Call, parentMethod, null); 89 | } 90 | 91 | var processMethod = typeof(UIExtenderRuntimeLib).GetMethod(nameof(UIExtenderRuntimeLib.ProcessMovieDocumentIfNeeded)); 92 | Debug.Assert(processMethod != null); 93 | gen.Emit(OpCodes.Ldstr, moduleName); 94 | gen.Emit(OpCodes.Ldarg_1); 95 | gen.Emit(OpCodes.Ldarg_2); 96 | gen.EmitCall(OpCodes.Call, processMethod, null); 97 | gen.Emit(OpCodes.Ret); 98 | 99 | return method; 100 | } 101 | 102 | int FindAlreadyPatchedIndex() 103 | { 104 | for (var i = 0; i < list.Count(); i++) 105 | { 106 | if (list[i].opcode == OpCodes.Nop && (string) list[i].operand == nopMarkName) 107 | { 108 | return i; 109 | } 110 | } 111 | 112 | return -1; 113 | } 114 | 115 | void ApplyInitialPatch(DynamicMethod method) 116 | { 117 | var additions = new List(); 118 | 119 | additions.Add(new CodeInstruction(OpCodes.Ldstr, moduleName)); 120 | additions.Add(new CodeInstruction(OpCodes.Ldarg_2)); 121 | additions.Add(new CodeInstruction(OpCodes.Ldloc_0)); 122 | additions.Add(new CodeInstruction(OpCodes.Call, loadXmlMethod)); 123 | 124 | // @TODO: replace NOP with Label? 125 | additions.Add(new CodeInstruction(OpCodes.Nop, nopMarkName)); 126 | additions.Add(new CodeInstruction(OpCodes.Ldstr, moduleName)); 127 | additions.Add(new CodeInstruction(OpCodes.Ldarg_2)); 128 | additions.Add(new CodeInstruction(OpCodes.Ldloc_0)); 129 | additions.Add(new CodeInstruction(OpCodes.Call, method)); 130 | 131 | // @TODO: reformat/rewrite 132 | var from = list.TakeWhile(i => !(i.opcode == OpCodes.Newobj && (i.operand as ConstructorInfo).DeclaringType == typeof(XmlReaderSettings))).Count() + 2; 133 | var to = from + list.Skip(from).TakeWhile(i => !(i.opcode == OpCodes.Newobj && (i.operand as ConstructorInfo).DeclaringType == typeof(WidgetPrefab))).Count(); 134 | var count = to - from; 135 | 136 | list.RemoveRange(from, count); 137 | list.InsertRange(from, additions); 138 | 139 | from = from + additions.Count; 140 | count = to - from; 141 | 142 | if (Utils.SoftAssert(count > 0, "Don't have enought NOP space in the IL!")) 143 | { 144 | list.InsertRange(from, Enumerable.Repeat(new CodeInstruction(OpCodes.Nop), count)); 145 | } 146 | else 147 | { 148 | UIExtender.RuntimeFor(moduleName).AddUserError($"Failed to patch {moduleName} (outdated)."); 149 | } 150 | } 151 | 152 | var index = FindAlreadyPatchedIndex(); 153 | if (index == -1) 154 | { 155 | var method = CreateDynamicMethod(null); 156 | ApplyInitialPatch(method); 157 | } 158 | else 159 | { 160 | var offset = list.Skip(index).TakeWhile(i => i.opcode != OpCodes.Call).Count(); 161 | var instruction = list[index + offset]; 162 | 163 | if (Utils.SoftAssert(instruction.opcode == OpCodes.Call, $"Invalid instruction found at marker!")) 164 | { 165 | instruction.operand = CreateDynamicMethod((MethodInfo)instruction.operand); 166 | } 167 | else 168 | { 169 | UIExtender.RuntimeFor(moduleName).AddUserError($"Failed to patch {moduleName} (outdated)."); 170 | } 171 | } 172 | 173 | return list; 174 | } 175 | 176 | /// 177 | /// Transpiler which fixes `ViewModel.ExecuteCommand` method to not only look at top-level functions, but also recursively 178 | /// look in base classes. 179 | /// 180 | /// 181 | /// 182 | public static IEnumerable ViewModelExecuteTranspiler(IEnumerable input) 183 | { 184 | var replacedMethod = typeof(Type).GetMethod(nameof(Type.GetMethod), new Type[] {typeof(string), typeof(BindingFlags)}); 185 | var targetMethod = typeof(UIExtenderRuntimeLib).GetMethod(nameof(UIExtenderRuntimeLib.FindExecuteCommandTargetMethod)); 186 | 187 | var index = input.TakeWhile(i => !(i.opcode == OpCodes.Callvirt && (i.operand as MethodInfo) == replacedMethod)).Count(); 188 | if (index >= input.Count()) 189 | { 190 | // already patched 191 | return input; 192 | } 193 | 194 | var list = new List(input); 195 | list[index].opcode = OpCodes.Call; 196 | list[index].operand = targetMethod; 197 | return list; 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /UIExtenderLib/Prefab/PrefabComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Reflection; 6 | using System.Xml; 7 | using TaleWorlds.Core; 8 | using TaleWorlds.Engine; 9 | using TaleWorlds.Engine.GauntletUI; 10 | using TaleWorlds.GauntletUI.PrefabSystem; 11 | using UIExtenderLib.Interface; 12 | using NonGenericCollections = System.Collections; 13 | using Path = System.IO.Path; 14 | 15 | namespace UIExtenderLib.Prefab 16 | { 17 | /// 18 | /// Component that deals with Gauntlet prefab XML files 19 | /// 20 | public class PrefabComponent 21 | { 22 | private readonly string _moduleName; 23 | 24 | /// 25 | /// Registered movie patches 26 | /// 27 | private Dictionary>> _moviePatches = new Dictionary>>(); 28 | 29 | public PrefabComponent(string moduleName) 30 | { 31 | _moduleName = moduleName; 32 | } 33 | 34 | /// 35 | /// Register general XmlDocument patch 36 | /// 37 | /// 38 | /// 39 | internal void RegisterPatch(string movie, Action patcher) 40 | { 41 | Debug.Assert(movie != null && !movie.IsEmpty(), $"Invalid movie name: {movie}!"); 42 | 43 | _moviePatches.Get(movie, () => new List>()).Add(patcher); 44 | } 45 | 46 | /// 47 | /// Register patch operating at node specified by XPath 48 | /// 49 | /// 50 | /// 51 | /// 52 | internal void RegisterPatch(string movie, string xpath, Action patcher) 53 | { 54 | RegisterPatch(movie, (document) => 55 | { 56 | var node = document.SelectSingleNode(xpath); 57 | if (node == null) 58 | { 59 | Utils.DisplayUserError($"Failed to apply extension to {movie}: node at {xpath} not found."); 60 | return; 61 | } 62 | 63 | patcher(node); 64 | }); 65 | } 66 | 67 | /// 68 | /// Register snippet insert patch 69 | /// 70 | /// 71 | /// 72 | /// 73 | internal void RegisterPatch(string movie, string xpath, PrefabExtensionInsertPatch patch) 74 | { 75 | RegisterPatch(movie, xpath, (node) => 76 | { 77 | var extensionNode = LoadPrefabExtension(patch.Name); 78 | var importedExtensionNode = node.OwnerDocument.ImportNode(extensionNode, true); 79 | var position = Math.Min(patch.Position, node.ChildNodes.Count - 1); 80 | position = Math.Max(position, 0); 81 | Debug.Assert(position >= 0 && position < node.ChildNodes.Count, $"Invalid position ({position}) for insert (patching in {patch.Name})"); 82 | 83 | node.InsertAfter(importedExtensionNode, node.ChildNodes[position]); 84 | }); 85 | } 86 | 87 | /// 88 | /// Register snippet replace patch 89 | /// 90 | /// 91 | /// 92 | /// 93 | internal void RegisterPatch(string movie, string xpath, PrefabExtensionReplacePatch patch) 94 | { 95 | RegisterPatch(movie, xpath, (node) => 96 | { 97 | var extensionNode = LoadPrefabExtension(patch.Name); 98 | var importedExtensionNode = node.OwnerDocument.ImportNode(extensionNode, true); 99 | 100 | node.ParentNode.ReplaceChild(importedExtensionNode, node); 101 | }); 102 | } 103 | 104 | /// 105 | /// Register snippet insert as sibling patch 106 | /// 107 | /// 108 | /// 109 | /// 110 | internal void RegisterPatch(string movie, string xpath, PrefabExtensionInsertAsSiblingPatch patch) 111 | { 112 | RegisterPatch(movie, xpath, (node) => 113 | { 114 | var extensionNode = LoadPrefabExtension(patch.Name); 115 | var importedExtensionNode = node.OwnerDocument.ImportNode(extensionNode, true); 116 | 117 | switch (patch.Type) 118 | { 119 | case PrefabExtensionInsertAsSiblingPatch.InsertType.Append: 120 | node.ParentNode.InsertAfter(importedExtensionNode, node); 121 | break; 122 | 123 | case PrefabExtensionInsertAsSiblingPatch.InsertType.Prepend: 124 | node.ParentNode.InsertBefore(importedExtensionNode, node); 125 | break; 126 | } 127 | }); 128 | } 129 | 130 | /** 131 | * Load and parse prefab extension 132 | */ 133 | /// 134 | /// Load snippet extension XML. Will search for the file in MODULE\GUI\PrefabExtensions\NAME.xml 135 | /// 136 | /// name of the extension (without .xml) 137 | /// document element 138 | private XmlNode LoadPrefabExtension(string name) 139 | { 140 | var path = Path.Combine(Utilities.GetBasePath(), "Modules", _moduleName, "GUI", "PrefabExtensions", name + ".xml"); 141 | var doc = new XmlDocument(); 142 | 143 | using (var reader = XmlReader.Create(path, new XmlReaderSettings 144 | { 145 | IgnoreComments = true, 146 | IgnoreWhitespace = true, 147 | })) 148 | { 149 | doc.Load(reader); 150 | } 151 | 152 | Debug.Assert(doc.HasChildNodes, $"Failed to parse extension ({name}) XML!"); 153 | return doc.DocumentElement; 154 | } 155 | 156 | /// 157 | /// Make WidgetFactory reload Movies that were extended by _moviePatches. 158 | /// WidgetFactory loads Movies during SandBox module loading phase, which occurs even before 159 | /// our module gets loaded, hence once we get control we need to force it to reload XMLs that 160 | /// are getting patched by extensions. 161 | /// 162 | internal void ForceReloadMovies() 163 | { 164 | // @TODO: figure out a method more prone to game updates 165 | 166 | // get internal dict of loaded Widgets 167 | var dict = UIResourceManager.WidgetFactory.PrivateValue("_customTypes"); 168 | Utils.CompatAssert(dict != null, "WidgetFactory._customTypes == null"); 169 | 170 | foreach (var movie in _moviePatches.Keys) 171 | { 172 | Debug.Assert(dict.Contains(movie), $"Movie {movie} to be patched was not found in the WidgetFactory!"); 173 | 174 | // remove widget from previously loaded Widgets 175 | dict.Remove(movie); 176 | 177 | // re-add it, forcing Factory to call now-patched `LoadFrom` method 178 | UIResourceManager.WidgetFactory.AddCustomType(movie, PathForMovie(movie)); 179 | } 180 | } 181 | 182 | /// 183 | /// Get path for movie from WidgetFactory 184 | /// 185 | /// 186 | /// 187 | private string PathForMovie(string movie) 188 | { 189 | // @TODO: figure out a method more prone to game updates 190 | var prefabNamesMethod = typeof(WidgetFactory).GetMethod("GetPrefabNamesAndPathsFromCurrentPath", BindingFlags.Instance | BindingFlags.NonPublic); 191 | Utils.CompatAssert(prefabNamesMethod != null, "WidgetFactory.GetPrefabNamesAndPathsFromCurrentPath"); 192 | 193 | // get names and paths of loaded Widgets 194 | var paths = prefabNamesMethod.Invoke(UIResourceManager.WidgetFactory, new object[] { }) as Dictionary; 195 | Utils.CompatAssert(paths != null, "WidgetFactory.GetPrefabNamesAndPathsFromCurrentPath == null"); 196 | 197 | return paths[movie]; 198 | } 199 | 200 | /// 201 | /// Apply patches to movie (if any is registered) 202 | /// 203 | /// 204 | /// 205 | public void ProcessMovieIfNeeded(string movie, XmlDocument document) 206 | { 207 | if (_moviePatches.ContainsKey(movie)) 208 | { 209 | foreach (var patch in _moviePatches[movie]) 210 | { 211 | patch(document); 212 | } 213 | } 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /ExampleModule/ExampleModule.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {109D437D-F030-4B47-B64B-E668884018B0} 8 | Library 9 | Properties 10 | ExampleModule 11 | ExampleModule 12 | v4.7.2 13 | 512 14 | 15 | 16 | x64 17 | true 18 | full 19 | false 20 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\Modules\ExampleModule\bin\Win64_Shipping_Client\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.CampaignSystem.dll 41 | 42 | 43 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.CampaignSystem.ViewModelCollection.dll 44 | 45 | 46 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Core.dll 47 | 48 | 49 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Core.ViewModelCollection.dll 50 | 51 | 52 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.dll 53 | 54 | 55 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.AccessProvider.Epic.dll 56 | 57 | 58 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.AccessProvider.Steam.dll 59 | 60 | 61 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.AccessProvider.Test.dll 62 | 63 | 64 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.DotNet.dll 65 | 66 | 67 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.DotNet.AutoGenerated.dll 68 | 69 | 70 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.dll 71 | 72 | 73 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.AutoGenerated.dll 74 | 75 | 76 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.GauntletUI.dll 77 | 78 | 79 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.dll 80 | 81 | 82 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.Data.dll 83 | 84 | 85 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.ExtraWidgets.dll 86 | 87 | 88 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.PrefabSystem.dll 89 | 90 | 91 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.TooltipExtensions.dll 92 | 93 | 94 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.InputSystem.dll 95 | 96 | 97 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Library.dll 98 | 99 | 100 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Localization.dll 101 | 102 | 103 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.dll 104 | 105 | 106 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.AutoGenerated.dll 107 | 108 | 109 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.Diamond.dll 110 | 111 | 112 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.GauntletUI.Widgets.dll 113 | 114 | 115 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.Helpers.dll 116 | 117 | 118 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.ViewModelCollection.dll 119 | 120 | 121 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.NavigationSystem.dll 122 | 123 | 124 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Network.dll 125 | 126 | 127 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlatformService.dll 128 | 129 | 130 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlatformService.Epic.dll 131 | 132 | 133 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlatformService.Steam.dll 134 | 135 | 136 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlayerServices.dll 137 | 138 | 139 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PSAI.dll 140 | 141 | 142 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.SaveSystem.dll 143 | 144 | 145 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Starter.DotNetCore.dll 146 | 147 | 148 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Starter.Library.dll 149 | 150 | 151 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.TwoDimension.dll 152 | 153 | 154 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.TwoDimension.Standalone.dll 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | {f17ccc8f-1e87-4b3c-b4ef-e03dd062322e} 164 | UIExtenderLib 165 | 166 | 167 | 168 | 175 | -------------------------------------------------------------------------------- /AnotherExampleModule/AnotherExampleModule.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {AE5A7DED-AA14-4F54-9566-AC6A8323CB41} 8 | Library 9 | Properties 10 | AnotherExampleModule 11 | AnotherExampleModule 12 | v4.7.2 13 | 512 14 | 15 | 16 | x64 17 | true 18 | full 19 | false 20 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\Modules\AnotherExampleModule\bin\Win64_Shipping_Client\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.CampaignSystem.dll 41 | 42 | 43 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.CampaignSystem.ViewModelCollection.dll 44 | 45 | 46 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Core.dll 47 | 48 | 49 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Core.ViewModelCollection.dll 50 | 51 | 52 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.dll 53 | 54 | 55 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.AccessProvider.Epic.dll 56 | 57 | 58 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.AccessProvider.Steam.dll 59 | 60 | 61 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Diamond.AccessProvider.Test.dll 62 | 63 | 64 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.DotNet.dll 65 | 66 | 67 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.DotNet.AutoGenerated.dll 68 | 69 | 70 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.dll 71 | 72 | 73 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.AutoGenerated.dll 74 | 75 | 76 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Engine.GauntletUI.dll 77 | 78 | 79 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.dll 80 | 81 | 82 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.Data.dll 83 | 84 | 85 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.ExtraWidgets.dll 86 | 87 | 88 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.PrefabSystem.dll 89 | 90 | 91 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.GauntletUI.TooltipExtensions.dll 92 | 93 | 94 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.InputSystem.dll 95 | 96 | 97 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Library.dll 98 | 99 | 100 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Localization.dll 101 | 102 | 103 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.dll 104 | 105 | 106 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.AutoGenerated.dll 107 | 108 | 109 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.Diamond.dll 110 | 111 | 112 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.GauntletUI.Widgets.dll 113 | 114 | 115 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.Helpers.dll 116 | 117 | 118 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.ViewModelCollection.dll 119 | 120 | 121 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.NavigationSystem.dll 122 | 123 | 124 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Network.dll 125 | 126 | 127 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlatformService.dll 128 | 129 | 130 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlatformService.Epic.dll 131 | 132 | 133 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlatformService.Steam.dll 134 | 135 | 136 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PlayerServices.dll 137 | 138 | 139 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.PSAI.dll 140 | 141 | 142 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.SaveSystem.dll 143 | 144 | 145 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Starter.DotNetCore.dll 146 | 147 | 148 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.Starter.Library.dll 149 | 150 | 151 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.TwoDimension.dll 152 | 153 | 154 | F:\SteamFastLibrary\steamapps\common\Mount & Blade II Bannerlord\bin\Win64_Shipping_Client\TaleWorlds.TwoDimension.Standalone.dll 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | {f17ccc8f-1e87-4b3c-b4ef-e03dd062322e} 164 | UIExtenderLib 165 | 166 | 167 | 168 | 175 | -------------------------------------------------------------------------------- /UIExtenderLib/ViewModel/ViewModelComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Reflection.Emit; 7 | using TaleWorlds.Library; 8 | using UIExtenderLib.Interface; 9 | using UIExtenderLib.CodePatcher.StaticLibrary; 10 | using UIExtenderLib.PatchAssembly; 11 | using Debug = System.Diagnostics.Debug; 12 | 13 | namespace UIExtenderLib.ViewModel 14 | { 15 | /// 16 | /// Component that deals with extended VM generation and runtime support 17 | /// 18 | internal class ViewModelComponent 19 | { 20 | private readonly string _moduleName; 21 | private readonly string _typeNamePrefix = "UIExtender_"; 22 | private readonly AssemblyBuilder _assemblyBuilder; 23 | private readonly string _dllName; 24 | private readonly ModuleBuilder _moduleBuilder; 25 | 26 | /// 27 | /// List of registered mixin types 28 | /// 29 | private readonly Dictionary> _mixins = new Dictionary>(); 30 | 31 | /// 32 | /// Cache dictionary of extended VM types 33 | /// 34 | private readonly Dictionary _extendedTypeCache = new Dictionary(); 35 | 36 | /// 37 | /// Cache of mixin instances. Key is generated by `mixinCacheKey`. Instances are removed when original view model is deallocated 38 | /// 39 | private readonly Dictionary> _mixinInstanceCache = new Dictionary>(); 40 | 41 | internal ViewModelComponent(string moduleName) 42 | { 43 | _moduleName = moduleName; 44 | 45 | _dllName = $"UIExtender_VMAssembly_{_moduleName}.dll"; 46 | var assembly = new AssemblyName($"UIExtender_VMAssembly_{_moduleName}"); 47 | 48 | _assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assembly, AssemblyBuilderAccess.RunAndSave); 49 | _moduleBuilder = _assemblyBuilder.DefineDynamicModule( 50 | $"UIExtender_VMAssemblyModule_{_moduleName}", 51 | _dllName 52 | ); 53 | } 54 | 55 | /// 56 | /// Register mixin type. 57 | /// 58 | /// mixin type, should be a subclass of ViewModelMixin where T specify view model to extend 59 | internal void RegisterViewModelMixin(Type mixinType) 60 | { 61 | Type viewModelType = null; 62 | var node = mixinType; 63 | while (node != null) 64 | { 65 | if (typeof(IViewModelMixin).IsAssignableFrom(node)) 66 | { 67 | viewModelType = node.GetGenericArguments().FirstOrDefault(); 68 | if (viewModelType != null) 69 | { 70 | break; 71 | } 72 | } 73 | 74 | node = node.BaseType; 75 | } 76 | 77 | Debug.Assert(viewModelType != null, $"Failed to find base type for mixin {mixinType}, should be specialized as T of ViewModelMixin!"); 78 | _mixins.Get(viewModelType, () => new List()).Add(mixinType); 79 | } 80 | 81 | /// 82 | /// Check if any mixins are extending type t 83 | /// 84 | /// 85 | /// 86 | internal bool ExtendsViewModelType(Type t) 87 | { 88 | return _mixins.ContainsKey(t); 89 | } 90 | 91 | /// 92 | /// Find base type for type that was possibly extended by other instances. 93 | /// Recursively looks at parents skipping ones starting with standard `_typeNamePrefix`. 94 | /// Returns same type if it's not one of the extended types. 95 | /// 96 | /// 97 | /// 98 | internal Type BaseTypeForPossiblyExtendedType(Type t) 99 | { 100 | while (t != null) 101 | { 102 | if (t.Name.StartsWith(_typeNamePrefix)) 103 | { 104 | t = t.BaseType; 105 | } 106 | else 107 | { 108 | break; 109 | } 110 | } 111 | 112 | return t; 113 | } 114 | 115 | /// 116 | /// Fetch mixin instance from cache for specified mixin type and extended view model instance. 117 | /// Used in extended VM methods to proxy calls to mixin that yielded that method. 118 | /// Requires extended VM constructor to run first (which will create instances). 119 | /// 120 | /// type of a mixin 121 | /// instance of VM 122 | /// 123 | internal IViewModelMixin MixinInstanceForVMInstance(Type mixinType, object instance) 124 | { 125 | var mixinInstance = mixinCacheList(instance).FirstOrDefault(m => m.GetType() == mixinType); 126 | Debug.Assert(mixinInstance != null, $"Mixin instance (of type {mixinType}) not (yet) created for {instance}!"); 127 | return mixinInstance; 128 | } 129 | 130 | /// 131 | /// Get extended view model type for specified base type (view model). 132 | /// Generates that type if it is not found in cache, setting it's parent type to the `parentType` 133 | /// (which can be another extended VM type). 134 | /// Should only be called if `ExtendsViewModelType()` returned true for type. 135 | /// Used to swap constructors in transcribed instantiation callsites for view models. 136 | /// 137 | /// original type of VM (found in game) 138 | /// parent type for newly generated type (can either equal to base to to extended VM from another instance) 139 | /// 140 | internal Type ExtendedViewModelTypeForType(Type baseType, Type parentType) 141 | { 142 | return _extendedTypeCache.Get(baseType, () => 143 | { 144 | var type = GenerateExtendedVMTypeFor(baseType, parentType); 145 | return type; 146 | }); 147 | } 148 | 149 | /// 150 | /// Initialize mixin instances for specified view model instance, called in extended VM constructor. 151 | /// 152 | /// base type of VM (as found in game) 153 | /// instance of extended VM 154 | internal void InitializeMixinsForVMInstance(Type baseType, object instance) 155 | { 156 | var list = mixinCacheList(instance); 157 | 158 | foreach (var mixinType in _mixins[baseType]) 159 | { 160 | list.Add((IViewModelMixin)Activator.CreateInstance(mixinType, new [] { instance })); 161 | } 162 | } 163 | 164 | /// 165 | /// Calls `OnRefresh` on mixins associated with view model instance, 166 | /// specifics are determined by VM model patch (usually during `OnRefresh`-like method in VM). 167 | /// 168 | /// 169 | internal void RefreshMixinForVMInstance(object instance) 170 | { 171 | foreach (var mixin in mixinCacheList(instance)) 172 | { 173 | mixin.OnRefresh(); 174 | } 175 | } 176 | 177 | /// 178 | /// Calls `OnFinalize` on mixins associated with view model instance, 179 | /// called during extended view models `OnFinalize` (Gauntlet method). 180 | /// 181 | /// 182 | internal void FinalizeMixinForVMInstance(object instance) 183 | { 184 | foreach (var mixin in mixinCacheList(instance)) 185 | { 186 | mixin.OnFinalize(); 187 | } 188 | } 189 | 190 | /// 191 | /// Destruct mixins associated with view model instance, 192 | /// called when respective VM destructor is called. 193 | /// 194 | /// 195 | internal void DestructMixinsForVMInstance(object instance) 196 | { 197 | _mixinInstanceCache.Remove(mixinCacheKey(instance)); 198 | } 199 | 200 | /// 201 | /// Generate extended VM type in _assemblyBuilder for base type (found in game), with parent set as `parentType` 202 | /// (can be either base type or other instance extended type). 203 | /// 204 | /// 205 | /// 206 | /// 207 | private Type GenerateExtendedVMTypeFor(Type baseType, Type parentType) 208 | { 209 | Debug.Assert(_mixins.ContainsKey(baseType), $"Invalid extended vm generation call - type {baseType} is not registered!"); 210 | 211 | var getTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)); 212 | var getMixinInstance = typeof(UIExtenderRuntimeLib).GetMethod(nameof(UIExtenderRuntimeLib.MixinInstanceForVMInstance)); 213 | Debug.Assert(getTypeFromHandle != null && getMixinInstance != null); 214 | 215 | var builder = _moduleBuilder.DefineType( 216 | _typeNamePrefix + _moduleName + baseType.Name, 217 | TypeAttributes.Class | TypeAttributes.Public, 218 | parentType 219 | ); 220 | 221 | { 222 | // constructor 223 | var defaultConstructor = parentType.GetConstructors().First(); 224 | var constructorSignature = defaultConstructor.GetParameters().Select(p => p.ParameterType).ToArray(); 225 | 226 | var constructor = builder.DefineConstructor( 227 | MethodAttributes.Public, 228 | CallingConventions.Standard, 229 | constructorSignature 230 | ); 231 | 232 | { 233 | var gen = constructor.GetILGenerator(); 234 | 235 | // call base constructor 236 | for (int i = 0; i < constructorSignature.Length + 1; i++) 237 | { 238 | gen.Emit(OpCodes.Ldarg, i); 239 | } 240 | 241 | gen.Emit(OpCodes.Call, defaultConstructor); 242 | 243 | // instaniate mixins of this type 244 | var instantiateMixins = typeof(UIExtenderRuntimeLib).GetMethod(nameof(UIExtenderRuntimeLib.InitializeMixinsForVMInstance)); 245 | gen.Emit(OpCodes.Ldstr, _moduleName); 246 | gen.Emit(OpCodes.Ldtoken, baseType); 247 | gen.Emit(OpCodes.Ldarg_0); 248 | gen.Emit(OpCodes.Call, instantiateMixins); 249 | 250 | gen.Emit(OpCodes.Ret); 251 | } 252 | } 253 | 254 | { 255 | // destructor 256 | var finalizer = builder.DefineMethod("Finalize", MethodAttributes.Family | MethodAttributes.ReuseSlot | MethodAttributes.HideBySig | MethodAttributes.Virtual); 257 | var existingFinalizer = parentType.GetMethod("Finalize", BindingFlags.Instance | BindingFlags.NonPublic); 258 | 259 | var gen = finalizer.GetILGenerator(); 260 | 261 | if (existingFinalizer != null) 262 | { 263 | gen.Emit(OpCodes.Ldarg_0); 264 | gen.EmitCall(OpCodes.Call, existingFinalizer, null); 265 | } 266 | 267 | var destructMixins = typeof(UIExtenderRuntimeLib).GetMethod(nameof(UIExtenderRuntimeLib.DestructMixinsForVMInstance)); 268 | gen.Emit(OpCodes.Ldstr, _moduleName); 269 | gen.Emit(OpCodes.Ldarg_0); 270 | gen.EmitCall(OpCodes.Call, destructMixins, null); 271 | gen.Emit(OpCodes.Ret); 272 | } 273 | 274 | { 275 | // OnFinalize 276 | var method = builder.DefineMethod("OnFinalize", MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.HideBySig | MethodAttributes.Virtual); 277 | var existingMethod = parentType.GetMethod("OnFinalize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); 278 | 279 | var gen = method.GetILGenerator(); 280 | if (existingMethod != null) 281 | { 282 | gen.Emit(OpCodes.Ldarg_0); 283 | gen.EmitCall(OpCodes.Call, existingMethod, null); 284 | } 285 | 286 | var finalizeMixins = typeof(UIExtenderRuntimeLib).GetMethod(nameof(UIExtenderRuntimeLib.FinalizeMixinsForVMInstance)); 287 | gen.Emit(OpCodes.Ldstr, _moduleName); 288 | gen.Emit(OpCodes.Ldarg_0); 289 | gen.EmitCall(OpCodes.Call, finalizeMixins, null); 290 | gen.Emit(OpCodes.Ret); 291 | } 292 | 293 | foreach (var mixin in _mixins[baseType]) 294 | { 295 | // properties 296 | foreach (var property in mixin.GetProperties().Where(p => p.CustomAttributes.Any(a => a.AttributeType == typeof(DataSourceProperty)))) 297 | { 298 | var newProperty = builder.DefineProperty(property.Name, PropertyAttributes.None, property.PropertyType, new Type[] {}); 299 | var attributeConstructor = typeof(DataSourceProperty).GetConstructor(new Type[] { }); 300 | var customBuilder = new CustomAttributeBuilder(attributeConstructor, new object[] {}); 301 | newProperty.SetCustomAttribute(customBuilder); 302 | 303 | // getter 304 | MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; 305 | var newMethod = builder.DefineMethod("get_" + property.Name, getSetAttr, property.PropertyType, Type.EmptyTypes); 306 | var gen = newMethod.GetILGenerator(); 307 | 308 | // body 309 | var method = property.GetGetMethod(); 310 | 311 | gen.Emit(OpCodes.Ldstr, _moduleName); 312 | gen.Emit(OpCodes.Ldtoken, mixin); 313 | gen.Emit(OpCodes.Call, getTypeFromHandle); 314 | gen.Emit(OpCodes.Ldarg_0); 315 | gen.Emit(OpCodes.Call, getMixinInstance); 316 | gen.Emit(OpCodes.Call, method); 317 | gen.Emit(OpCodes.Ret); 318 | 319 | newProperty.SetGetMethod(newMethod); 320 | } 321 | 322 | // methods 323 | foreach (var method in mixin.GetMethods().Where(m => m.CustomAttributes.Any(a => a.AttributeType == typeof(DataSourceMethod)))) 324 | { 325 | var newMethod = builder.DefineMethod(method.Name, MethodAttributes.Public, null, new Type[] { }); 326 | var gen = newMethod.GetILGenerator(); 327 | 328 | // body 329 | gen.Emit(OpCodes.Ldstr, _moduleName); 330 | gen.Emit(OpCodes.Ldtoken, mixin); 331 | gen.Emit(OpCodes.Call, getTypeFromHandle); 332 | gen.Emit(OpCodes.Ldarg_0); 333 | gen.Emit(OpCodes.Call, getMixinInstance); 334 | gen.Emit(OpCodes.Call, method); 335 | gen.Emit(OpCodes.Ret); 336 | } 337 | } 338 | 339 | return builder.CreateType(); 340 | } 341 | 342 | /// 343 | /// Save dll files (used only for debugging). 344 | /// Finalizes dll meaning new types can no longer be added. 345 | /// 346 | internal void SaveDebugImages() 347 | { 348 | _assemblyBuilder.Save(_dllName); 349 | 350 | var targetPath = Path.Combine(Utils.GeneratedDllDirectory(), _dllName); 351 | if (File.Exists(targetPath)) 352 | { 353 | File.Delete(targetPath); 354 | } 355 | 356 | File.Move(_dllName, targetPath); 357 | } 358 | 359 | /// 360 | /// Construct string key for _mixinInstanceCache 361 | /// 362 | /// 363 | /// 364 | private string mixinCacheKey(object instance) 365 | { 366 | return instance.GetType() + "_" + instance.GetHashCode(); 367 | } 368 | 369 | /// 370 | /// Get list of mixin instances from _mixinInstanceCache associated with VM instance 371 | /// 372 | /// 373 | /// 374 | private List mixinCacheList(object instance) 375 | { 376 | var key = mixinCacheKey(instance); 377 | return _mixinInstanceCache.Get(key, () => new List()); 378 | } 379 | } 380 | } --------------------------------------------------------------------------------