Tutorial Other [VB.net] How to load another C#/VB.net project or mod dependecy (such as Bannerlib), and Harmony

Users who are viewing this thread

Let's do some advanced VB.net modding, this involves C# projects: Bannerlib and Harmony.
Bannerlib is a easy Bannerlord API wrapper, we will use it for the Input system to spawn a console for testing
Harmony is a .Net runtime patcher, You can redirect a method procedure/function using this library

Without further ado, let's create our first patch based mod. We also going to learn how to use git along the way

0. first create your project.
follows the tutorial if you're a new starter https://forums.taleworlds.com/index...-to-write-bannerlord-mod-using-vb-net.404953/

73Htld.jpg


1. create a local empty git repo
it doesn't matter how you do it, it can be via command line or a frontend
I use TortoiseGit, because i'm really used to it at work
mwOMuL.jpg


2. copy code below, create .gitignore file on your .sln directory, and paste the content to the file. This file prevents you accidentally push intermediate garbage into your git repository.

https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

3r5Fnj.jpg

3. now, add your source code and commit them into your git repo.

7gwiIW.jpg


4. add Harmony and bannerlib as external git module.

https://github.com/sirdoombox/BannerLib.git

https://github.com/pardeike/Harmony.git
Warning!
If your project statically linked a third party library, you have to adhere to the license. For example Bannerlib.Input happens to be AGPL and I'm using the library statically, then my source code should be licenced under AGPL as well.
Don't ask me more about licence, I'm not a ****ing lawyer. Just go to the stackoverflow, if you want debate XD

I use Bannerlib to map certain key to do a custom action in the game, we're going to use it to bind a hidden console developer in the game as patch testing later.
If you're writing a mod, .I think it's a must to make it open because, you know this mods run full permission on your behalf. If the sources are open we can inspect and screen them

NTDnOX.jpg

now you should have 2 project in your externals directory

5. we need to create a directory where our compiled dlls will be placed, module manifest, and also write build script to copy our module manifests to the bannerlord module directory (you can skip this and just put your module manifest directly like in the older tutorial, but this is more ideal)

We need to create 2 directory of module manifest: Our mod, and Bannerlib.Input

6. write Submodule.xml manifest in each module directory

Ny8ipE.jpg

XML:
<Module>
     <Name value="MyVBPatchProject"/>
     <Id value="MyVBPatchProject"/>
     <Version value="e1.0.3"/>
     <SingleplayerModule value="true"/>
     <MultiplayerModule value="false"/>
     <DependedModules>
        <DependedModule Id="BannerLib.Input"/>
    </DependedModules>
     <SubModules>
         <SubModule>
             <Name value="MyVBPatchProject"/>
             <DLLName value="MyVBPatchProject.dll"/>
             <SubModuleClassType value="MyVBPatchProject.Main"/>
             <Tags>
                 <Tag key="DedicatedServerType" value="none" />
                 <Tag key="IsNoRenderModeElement" value="false" />
             </Tags>
         </SubModule>
     </SubModules>
     <Xmls>
  </Xmls>
</Module>
MLdtka.jpg

XML:
<Module>
     <Name value="BannerLib.Input"/>
     <Id value="BannerLib.Input"/>
     <Version value="e1.0.3"/>
     <SingleplayerModule value="true"/>
     <MultiplayerModule value="false"/>
     <DependedModules>
    </DependedModules>
     <SubModules>
         <SubModule>
             <Name value="BannerLib.Input"/>
             <DLLName value="BannerLib.Input.dll"/>
             <SubModuleClassType value="BannerLib.Input.InputSubModule"/>
             <Tags>
                 <Tag key="DedicatedServerType" value="none" />
                 <Tag key="IsNoRenderModeElement" value="false" />
             </Tags>
         </SubModule>
     </SubModules>
     <Xmls>
  </Xmls>
</Module>

So there are 2 module manifest, we need to make sure these directories get copied during compilation, that is on next step.
MyVBPatchProject has to depend on Bannerlib thats why we put <DependedModule Id="BannerLib.Input"/> in MyVBPatchProject manifest
6. now open your solution and right click in your Project (in this case MyVBPatchProject) > properties...
Go to compile and click on Build Events. Now this is where we put script to copy Module manifest directory to bannerlord module directory

2FANW6.jpg

in pre build textbox

Bash:
xcopy /F /R /Y /I  "$(SolutionDir)<your mod manifest directory folder name>" "<bannerlord module directory>\<your mod manifest directory folder name>"
xcopy /F /R /Y /I  "$(SolutionDir)BannerLib.Input" "<bannerlord module directory>\BannerLib.Input"

for example:
Bash:
xcopy /F /R /Y /I  "$(SolutionDir)MyVBPatchProject" "F:\Steam\steamapps\common\Mount & Blade II Bannerlord\Modules\MyVBPatchProject"
xcopy /F /R /Y /I  "$(SolutionDir)BannerLib.Input" "F:\Steam\steamapps\common\Mount & Blade II Bannerlord\Modules\BannerLib.Input"

In build output path:
Code:
<your bannerlord module directory>\<your mod manifest directory folder name>
example:
Code:
F:\Steam\steamapps\common\Mount & Blade II Bannerlord\Modules\MyVBPatchProject\bin\Win64_Shipping_Client

whenever you build the program any changes in your module manifest in project directory will be copied automatically to bannerlord module directory :grin:

Don't forget to setup your debug parameters (see previous tutorial)

Add your module manifest folder to git and commit any changes.

7. from this onward is the meaty stuff, we will create an entry point for our program mod
EvNyML.jpg


C#:
Imports TaleWorlds.CampaignSystem
Imports TaleWorlds.Core
Imports TaleWorlds.MountAndBlade

Namespace Global.MyVBPatchProject
    Public Class Main
        Inherits MBSubModuleBase

        Protected Overrides Sub OnSubModuleLoad()
            MyBase.OnSubModuleLoad()
        End Sub

        Protected Overrides Sub OnGameStart(game As Game, gameStarterObject As IGameStarter)
            Dim campaign = game.GameType
            If (campaign Is Nothing) Then
                'Debug.WriteLine("oops!")
                Exit Sub
            End If
            Dim campaignStarter = CType(gameStarterObject, CampaignGameStarter)
            AddBehaviour(campaignStarter)
        End Sub

        Private Sub AddBehaviour(gameInit As CampaignGameStarter)
            'gameInit.AddBehavior(New SimpleDayCounter)
        End Sub

        Protected Overrides Sub OnBeforeInitialModuleScreenSetAsRoot()
            MyBase.OnBeforeInitialModuleScreenSetAsRoot()
            Dim ver = System.Environment.Version
            InformationManager.ShowInquiry(New InquiryData(
                "Net Enviroment",
                $"running on version {ver}",
                True,
                False,
                "Accept",
                "",
                Sub()
                    'Environment.Exit(1)
                End Sub,
                Sub()

                End Sub))
        End Sub

    End Class

End Namespace

OnGameStart procedure is your CampaignBase entry point.
AddBehaviour procedure is where we load our CampaignBase modules (tutorial coming soon)
If you got reference error add Taleworlds.* dll assemblies. don't forget to set On Copy to False so you don't copy bunch of Taleworlds.* dll to your mod binary directory.

7. Now lets add our libraries. Right click on solution and add existing project
7.a add Harmony library first. Your solution look like this. Yeah I know, don't worry about yellow warning in the project, We'll fix that
8l5LH1.jpg


7.b to add bannerlib, we have to create a paths.csproj where README.MD reside in externals/Bannerlib

XML:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
    <GameBins>Path to the main bin/Win64_Shipping_Client folder\</GameBins>
    <BuildPath>Path to the desired build folder\</BuildPath>
</PropertyGroup>
</Project>

pQuFOg.jpg


this is what my paths.csproj looks like (& char is &amp; in xml):
XML:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
    <GameBins>F:\Steam\steamapps\common\Mount &amp; Blade II Bannerlord\bin\Win64_Shipping_Client\</GameBins>
    <BuildPath> F:\Steam\steamapps\common\Mount &amp; Blade II Bannerlord\Modules\BannerLib.Input\bin\Win64_Shipping_Client\</BuildPath>
</PropertyGroup>
</Project>

don't forget to append \ at the end, otherwise you won't be able to load the library into the solution.
After that we can import Bannerlib.Input csproj.

HXsJMu.jpg


In bannerlib input project, we must add reference Taleworlds.* libraries

7.c Now, let's configure our Harmony project.

z32g8C.jpg


Harmony also targets to .NETCore platform, Bannerlord doesn't support that. To remove .NETCore target we need edit csproj manually. To do that we have to unload Harmony project by right clicking on the project > Unload

Now right click again and edit.

XML:
<TargetFrameworks>net35;net45;net472;net48;netcoreapp3.0;netcoreapp3.1</TargetFrameworks>
Replace that line with this
XML:
<TargetFrameworks>net35;net45;net472;net48;</TargetFrameworks>
and in
XML:
<ItemGroup></ItemGroup>
add this line
XML:
<Reference Include="netstandard" />
so it looks like this
XML:
    <ItemGroup>
        <None Include="..\LICENSE" Pack="true" PackagePath="" />
        <None Include="..\HarmonyLogo.png" Pack="true" Visible="false" PackagePath="" />
    <Reference Include="netstandard" />
    </ItemGroup>
Now this libraries only targets to net framework 3.5, 4.5, 4.7.2, and 4.8.
Save it and reload, now Harmony is ready to be use

7.d Let's link Bannerlib.Input and harmony to our mod project.

lULiT8.jpg


Now build the whole solution. There will be 2 folder, BannerLib.Input contains BannerLib.Input.dll and MyVBPatchProject will contain 0Harmony.dll and our dll

hWOKAZ.jpg


8. Add console developer binding to your keyboard.
Create new Developer console Module
C#:
Imports System.Runtime.InteropServices
Imports BannerLib.Input
Imports TaleWorlds.InputSystem

Namespace Global.MyVBPatchProject
    Public Module DeveloperConsole
        Private m_IsStarted = False
        Private Declare Sub toggle_imgui_console_visibility Lib "Rgl.dll" Alias "?toggle_imgui_console_visibility@rglCommand_line_manager@@QEAAXXZ" (x As UIntPtr)
        Public Sub LoadTheHotkeys()
            If (m_IsStarted) Then Exit Sub
            m_IsStarted = True
            Dim hotkey = HotKeys.Create("MyVBPatchProject")
            Dim key = hotkey.Add("MyVBPatchProject_DeveloperConsole",
                InputKey.Tilde,
                HotKeyCategory.Action,
                "Developer console",
                "Toggles a developer console"
            )

            Dim lambda As Func(Of Boolean) = Function()
                Return True
            End Function
            key.WithPredicate( lambda )
            key.WithOnPressedAction(
                Sub()
                    toggle_imgui_console_visibility(New UIntPtr(1))
                End Sub)
            hotkey.Build()
        End Sub
    End Module
End Namespace

Now in OnBeforeInitialModuleScreenSetAsRoot() In main your entry point class add the functionality to load the hotkey like this

C#:
        Protected Overrides Sub OnBeforeInitialModuleScreenSetAsRoot()
            MyBase.OnBeforeInitialModuleScreenSetAsRoot()
            DeveloperConsole.LoadTheHotkeys() '<----
            Dim ver = System.Environment.Version
            InformationManager.ShowInquiry(New InquiryData(
                "Net Enviroment",
                $"running on version {ver}",
                True,
                False,
                "Accept",
                "",
                Sub()
                    'Environment.Exit(1)
                End Sub,
                Sub()

                End Sub))
        End Sub

Let's test the module to see if your the console key works.
qnxW2E.jpg


Make sure to tick both module
pbeYyW.jpg

kz1EHD.jpg


Now the console and input system are working we can use this to debug and test our Harmony patch.

9. We're going to pick a simple procedure to patch. I think ShowPartySizeDetail at Taleworlds.CampaignSystem.Sandbox.GameComponent.Party is easy enough to debug and test.
Before working with Harmony library, we need to a nuget package for MonoMod.Common -Version 20.4.3.1 in package manager, otherwise it will throw Mono.Cecil not found error during patching. (related issue: https://github.com/pardeike/Harmony/pull/263 )

run this in package manager
Bash:
Install-Package MonoMod.Common -Version 20.4.3.1

QKqbUn.jpg



Create a friend class, name it Patch
C#:
Imports HarmonyLib
Imports TaleWorlds.CampaignSystem.SandBox.GameComponents.Party
Imports TaleWorlds.Core

Namespace Global.MyVBPatchProject
    <HarmonyPatch(GetType(DefaultPartySizeLimitModel), "ShowPartySizeDetail")>
    Friend Class Patch
        Public Shared Sub Postfix(strings As List(Of String), ByRef __result As String)
            InformationManager.ShowInquiry(New InquiryData(
                "Patch works",
                "if you see this, the patch works",
                True,
                False,
                "Accept",
                "",
                Sub()
                    'Environment.Exit(1)
                End Sub,
                Sub()

                End Sub))
            __result = "hello world"
        End Sub
    End Class
End Namespace
Prefix means this procedure will run first before the original, Postfix the original runs first then followed by your patch
If we assign byRef parameter by any value, Harmony will assume the function returned a value, thus exiting the function. Remember, if you wish to modify a function return value add a byref parameter with matching data type on the last parameter declaration

In main.vb change procedure OnSubModule change it to this

C#:
Protected Overrides Sub OnSubModuleLoad()
            MyBase.OnSubModuleLoad()
            Dim harmony = New Harmony("calradia.MyVBPatchProject.example")
            harmony.PatchAll()
        End Sub


Compile....and finger crossed.....
Now Load a campaign or create a new one.

If you are in campaign, open the devcon with tilde and type this command campaign.show_party_size_limit_detail test
if a messagebox pops up, congrats! your patch works

0JxOIQ.jpg


Ach, that was quite long tutorial, good luck VB brethren.

here's my repo:


If possible, please avoid using Harmony pacther and stick with provided API as this can cause mod conflict or instablity!

.
 
Last edited:
Back
Top Bottom