Main Tools you will need:
- An IDE that can compile C# code, mainly Class Libraries (.DLL) that use .NET Framework (4.6.2) [Personally I like to use Visual Studio Community (it's free) 2019 is good, and 2022 is good as well]
- An Assembly viewer, like ILSpy (that's i-l-spy) or DnSpy. I believe some IDE's have an option to view Assemblies as well, so if you want to use that feature feel free, as long as you can open and view the code in a .DLL
- An Assembly Publicizer, I prefer to use CabbageCrow's AssemblyPublicizer, this is to convert all private and protected methods, classes, and fields to Public (However this is optional, as xHarmony that comes with BepInEx does have AccessTools that does Reflection for you, to access said fields.)
- A in-game Unity Debugger, I honestly use Unity Explorer by Sinai-Dev Mono 5.X, which you use by adding to BepInEx/Plugins like any other mod. Something you can use to inspect and crawl through run-time elements in the game is great to track down what you might want to patch, or to see if something to coded is working as expected (did a custom component get added, was a value changed but just isn't showing visibly, etc.
Resources that you will need:
- Unity Unstripped Core Libs which you can find the right version that matches the game's UnityPlayer.dll version (file details) Here [Note if you already installed the BepInEx Bundle, you will already have the correct core libs in the folder "unstripped_corelibs"]
- .NET Framework 4.6.2 Developer Pack
Getting Started with writing your first mod for BloodWest (Using Visual Studio):
I will try my best to describe how to at least get a hello world mod working, which you can branch off with all sorts of patches of your own, but for now, lets get into setting up the project, and declaring the main class that will be our mod.
Now I'm more familiar with Visual Studio as an IDE, but if you have a preferred IDE that can compile C# code into .DLLs using .NET Framework that works too, since everything I will be talking about is not using a special VS feature.
So first things first, open Visual Studio and create a new project.
I choose the option "Create a new project" under "Get started"
Choose the project template "Class Library (.NET Framework)" you can search for it via the search bar (if you don't see it available) or use the filters of C# : Windows : Library
If you still can't see it you may have to run the installer for Visual Studio again, and choose Repair/Modify, and install the options for .NET Framework (might be in "Advanced")
Name your project as you see fit, as it's a tutorial we can with BloodWestHelloWorld, you can change the location to somewhere else that you would prefer the project to be created in, but I'm going to leave it as "C:\Users\<myuser>\source\repos" as that default location works for me, might not for you so change it as you please.
With the new project created we are given a basic class called Class1.cs we can name this whatever we want, but I generally like to rename this class+file something like Main.cs or Plugin.cs but you can name it whatever works for you, just as long as you can identify that this is what BepInEx will be loading.
With that renamed, we need the next important thing, which is adding our project references, which I will list in the format of <.dll name> | <path>
- 0Harmony.dll | C:\Program Files (x86)\Steam\steamapps\common\Blood West\BepInEx\core
- BepInEx.dll | C:\Program Files (x86)\Steam\steamapps\common\Blood West\BepInEx\core
- UnityEngine.dll | C:\Program Files (x86)\Steam\steamapps\common\Blood West\unstripped_corelibs
- UnityEngine.CoreModule.dll | C:\Program Files (x86)\Steam\steamapps\common\Blood West\unstripped_corelibs
Inside of our Plugin.cs (renamed Class1.cs) add these 3 usings to the top of the file:
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
And then make the Plugin class public, as well as extend the BaseUnityPlugin, and give it the annotation of [BepInPlugin(Plugin.GUID, Plugin.ModName, Plugin.Version)] as well as create some variables like so:
[BepInPlugin(Plugin.GUID, Plugin.ModName, Plugin.Version)]
public class Plugin : BaseUnityPlugin
{
public const string Version = "1.0.0.0";
public const string ModName = "BloodWestHelloWorld";
public const string GUID = "com.bloodwesthelloworld";
This will have our class be identifiable to the BepInEx chainloader, right now it won't do anything so lets get right to the rest of the code.
Next we will setup the ManualLogSource and a Harmony instance, as well as our mod's Awake method.
public static ManualLogSource Log; //Will Define in Awake
public readonly Harmony harmony = new Harmony(GUID); //readonly to avoid breaking the instance via human error
Next we will create the Awake method, this will call once the game is good and loaded, which will be when our mod will "wake up" as well, triggering this method. The mod works like a MonoBehaviour and the Awake is one of the execution steps Read Execution Order Here
Inside of the Awake we will setup the maunal logger, have it print to the console hello world. And altho we don't have any patches yet, we will tell our harmony instance to patchall
private void Awake()
{
Log = Logger;
//Log = new ManualLogSource(null); //This is if you want to turn your logging messages off for the whole mod
Log.LogMessage($"Hello World! This is my first mod {ModName}!");
harmony.PatchAll(); //If we had any Harmony Patches defined, this will execute them
}
Now we've gotten the code done, and we can compile this, take the .dll from the "C:\Users\<myuser>\source\repos\BloodWestHelloWorld\bin\Debug" location and drop it into the "C:\Program Files (x86)\Steam\steamapps\common\Blood West\BepInEx\plugins" location and fire up the game, If all is well we should see our message in the console window (which should open along side the game, if BepInEx is installed correctly, and the BepinEx.cfg as the window enabled). As I don't have screenshots I'll pull the relevant lines from the LogOutput.log file in the BepInEx folder
[Info : BepInEx] Loading [BloodWestHelloWorld 1.0.0.0]
[Message:BloodWestHelloWorld] Hello World! This is my first mod BloodWestHelloWorld!
So this is basically the bare minimum to get a BepInEx plugin working, as this one does nothing but just print some text to the log/console and patch nothing, we will have to change that. So the Next Steps will be Publicizing a game .dll and making a patch so that when we reload we don't consume any ammo, so infinite ammo (reloads)
So as mentioned above in the Tools section I will be using CabbageCrow's AssemblyPublicizer.exe I will do this by putting a copy of it inside the BloodWest managed folder "C:\Program Files (x86)\Steam\steamapps\common\Blood West\Blood West_Data\Managed" and for now, I will only publicize one assembly which would be "BloodWest.dll" to do this I will drag and drop this assembly onto AssemblyPublicizer.exe which if it worked correctly a new folder will be created called "publicized_assemblies" and inside it there will be a "BloodWest_publicized.dll"
Now with this assembly is publicized meaning everything is now public and easy to access in code, lets return to our project and add this as a reference
- BloodWest_publicized.dll | C:\Program Files (x86)\Steam\steamapps\common\Blood West\Blood West_Data\Managed\publicized_assemblies
Now I've done the legwork to look through the BloodWest.dll, track down the relevant class, and the method. And data-mined the Item Kinds, but this is to show you an example of a patch, which you can read up on Harmony Patches Here
The Patch code (you can add this under the Awake method, there is also ways to create other classes to store patches known as "patcher classes" which is also documented in the Harmony Docs)
//Cheat unlimited ammo
[HarmonyPatch(typeof(Game.Gameplay.State.GameplayStatePlayer),
nameof(Game.Gameplay.State.GameplayStatePlayer.RemoveSingleItemFromNonAmmoOwnedItems))]
private static class GameplayStatePlayer_RemoveSingleItemFromNonAmmoOwnedItems_Patch
{
public static bool Prefix(Game.Database.DatabaseItem kind, GameplayStateContainer ignoreContainer)
{
if (kind.name.StartsWith("Ammo") || kind.name.StartsWith("Bolt") || kind.name.StartsWith("Arrow"))
return false;//will skip original method
return true;//will execute original method
}
}
Now the project may build, or it will fail. Mainly if it fails it will be due to a project setting. To resolve this we go to Project > [projectname] Properties and go to the Build options/tab. In here we want to tick "Allow unsafe code" (Now if you didn't pub the assembly and reference BloodWest.dll directly, and use Harmony's AccessTools to use private methods/fields you don't need to do this step)
With this setting enabled, you can build the project again.
Do the same steps to copy from the projects bin/debug into the bepinex/plugins location (you can also automate this process, the easiest way is post-build commands)
Launch the game, see your hello world message again, and then start a new game, find a gun, and reload. And watch as you never consume ammo to reload. Happy shooting, happy modding.
Full Plugin.cs
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
namespace BloodWestHelloWorld
{
[BepInPlugin(Plugin.GUID, Plugin.ModName, Plugin.Version)]
public class Plugin : BaseUnityPlugin
{
public const string Version = "1.0.0.0";
public const string ModName = "BloodWestHelloWorld";
public const string GUID = "com.bloodwesthelloworld";
public static ManualLogSource Log; //Will Define in Awake
public readonly Harmony harmony = new Harmony(GUID); //readonly to avoid breaking the instance via human error
private void Awake()
{
Log = Logger;
//Log = new ManualLogSource(null); //This is if you want to turn your logging messages off for the whole mod
Log.LogMessage($"Hello World! This is my first mod {ModName}!");
harmony.PatchAll(); //If we had any Harmony Patches defined, this will execute them
}
//Cheat unlimited ammo
[HarmonyPatch(typeof(Game.Gameplay.State.GameplayStatePlayer), nameof(Game.Gameplay.State.GameplayStatePlayer.RemoveSingleItemFromNonAmmoOwnedItems))]
private static class GameplayStatePlayer_RemoveSingleItemFromNonAmmoOwnedItems_Patch
{
public static bool Prefix(Game.Database.DatabaseItem kind, GameplayStateContainer ignoreContainer)
{
if (kind.name.StartsWith("Ammo") || kind.name.StartsWith("Bolt") || kind.name.StartsWith("Arrow"))
return false;//will skip original method
return true;//will execute original method
}
}
}
}
1 comment
Hi. I've tried to follow this guide and the Hello World bit compiles. However, after doing the steps for the infinite ammo patch, it fails to compile with the following error:
Nevermind, I'm stupid. I created the project as a Class Library instead of Class Library (.NET Framework)error CS0246: The type or namespace name 'GameplayStateContainer' could not be found (are you missing a using directive or an assembly reference?)
I've got the four core references plus BloodWest.dll added as project references. Am I missing a step?