Skip to content
OrianaVenture edited this page Feb 7, 2024 · 7 revisions

Best Practices

Table of Contents:

Introduction to BepInEx

BepInEx is the mod loader we use for making valheim mods. Other solutions exist, but as a community we highly recommend learning and using BepInEx to create your mods. This will make it easier for users to install and use your mod, since they will know what to expect. If you have never created a mod using BepInEx before this guide can help you get started. That guide also helps you set up your development environment for the first time. Once you have a basic project set up this page can help you with specific setup instructions and tips for Valheim modding.

Introduction to Harmony

Valheim utilizes HarmonyX for our patching solution. There are minor differences when using HarmonyX over Harmony 2. The main documentation can be found on this website and supplemental HarmonyX documentation is here. When designing your mods it is best to understand the differences so you can be sure your patches are working as intended. The documentation on the main site may be slightly inaccurate due to the changes in HarmonyX.

Referencing Assemblies

The game is no longer shipping stripped versions of the unity assemblies and these assemblies have now been removed from the bepinex pack. You may need to update your project references if creating mods before the 0.217.27 game patch. If there are assemblies missing that were once included in the pack you will now need to package these directly in your project (such as using ilrepack).

Another best practice, especially when working with other developers, is to dynamically assign your referenced paths. The Jotunn project stub, for example, provides a nice template to set this up for you. Another solution is to use a choose statement in your .csproj files to assign the game path. If you are setting up a project manually this is the general format to assign the game directory:

<Choose>
    <When Condition="($(OS) == 'Unix' OR $(OS) == 'OSX') AND $(GamePath) == ''">
      <PropertyGroup>
        <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.steam/steam/steamapps/common/Valheim</GamePath>
        <GamePath Condition="!Exists('$(GamePath)')">$(HOME)/Library/Application Support/Steam/steamapps/common/Valheim/Contents/MacOS</GamePath>
      </PropertyGroup>
    </When>
    <When Condition="($(OS) == 'Windows_NT') AND $(GamePath) == ''">
      <PropertyGroup>
        <GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 892970', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath>
        <_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32))</_SteamLibraryPath>
        <GamePath Condition="!Exists('$(GamePath)') AND '$(_SteamLibraryPath)' != ''">$(_SteamLibraryPath)\steamapps\common\Valheim</GamePath>
        <GamePath Condition="!Exists('$(GamePath)')">C:\Program Files\Steam\steamapps\common\Valheim</GamePath>
        <GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Valheim</GamePath>
      </PropertyGroup>
    </When>
</Choose>

If you have placed the BepInEx pack inside your game directory your references would look similar to:

<ItemGroup>
    <Reference Include="0Harmony, Version=2.9.0.0, Culture=neutral, PublicKeyToken=null">
        <HintPath>$(GamePath)\BepInEx\core\0Harmony.dll</HintPath>
    </Reference>
    <Reference Include="BepInEx, Version=5.4.22.0, Culture=neutral, PublicKeyToken=null">
        <HintPath>$(GamePath)\BepInEx\core\BepInEx.dll</HintPath>
    </Reference>
    <Reference Include="UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
        <HintPath>$(GamePath)\valheim_Data\Managed\UnityEngine.dll</HintPath>
    </Reference>
    ...etc
</ItemGroup>

Using Private Functions and Properties

In order to access private methods in the game you can do one of two options:

  1. Use Harmony AccessTools
  2. Publicize the game assemblies

Harmony

Harmony provides utilities to use private functions and access private properties. This method may look a bit more complicated and may be harder to understand as a beginner. If this code looks daunting to you then it would be recommended to use the publicize method. To access private methods with harmony the syntax will look similar to the following:

// __instance is of type Container
AccessTools.Method(typeof(Container), "Save").Invoke(__instance, new object[] { });

-or-

// __instance is of type InventoryGui
__instance.GetType().GetMethod("UpdateCraftingPanel", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(__instance, new object[] { false });

Publicize the Game Assemblies

This option allows using any game methods or properties as if they were public. This makes handling of private methods or properties much easier, and takes away some of the complexity so you can focus on making content. This is the option most developers choose in their setup for making mods.

First, allow unsafe code blocks in your project. To read more about what this means you can see the official Microsoft documentation. This is something you can typically toggle through your IDE, or manually add to your .csproj file:

<PropertyGroup>
    <AssemblyName>MyMod</AssemblyName>
    ...etc
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Next, download the CabbageCrow publicizer. To use the publicizer you simply drag and drop the assemblies one-by-one onto the exe and it will create a new folder called "publicized_assemblies" with the updated assemblies. You only need to publicize the assemblies you are referencing directly in your code. If using the example choose statement provided above, your references will now look similar to:

<ItemGroup>
    <Reference Include="assembly_valheim, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
        <HintPath>$(GamePath)\valheim_Data\Managed\publicized_assemblies\assembly_valheim_publicized.dll</HintPath>
    </Reference>
    ...etc
</ItemGroup>

You do not usually need to perform publicizing on Unity assemblies, just the valheim assemblies. These start with "assembly_" such as "assembly_valheim". Use your best judgment, if you need to access private methods for other assemblies then you must publicize them.

NOTE: Every time the game updates you will have to use the publicizer and update your files to ensure you are referencing the latest versions.

Troubleshooting:

In some cases the publicizer may fail due to file permission settings, commonly creating publicized assemblies of size 0. You can try moving the .exe into the same folder as the game assemblies you are trying to publicize, and try again. If this does not work copy your game assemblies to a local folder for your user, and try using the .exe there. Once you have the files move the publicized_assemblies folder into the original location of the game assemblies (if using the choose example above), or to your desired development setup location.

BepInEx/Harmony Best Practices

Don't Use UnpatchAll

When making patches in the game you have the ability to later unpatch your code. This can be useful when you want to disable your patches during runtime after PatchAll has been called. A common mistake people make is, when utilizing UnpatchAll, they use the wrong method and accidentally unpatch ALL mods. You will never want to unpatch all mods, as it is considered a bad practice and will cause other mods to shut down too soon and in worse case corrupt player and game saves. Never use UnpatchAll() with a null parameter. Only use the UnpatchAll("your.harmony.id") method that targets specific mods, or the other specific variations of this method.

It is NOT recommended to call UnpatchAll on game shutdown (or the plugin Destroy() function). There is no real benefit to doing so, and if done incorrectly can cause the above stated issues.

Logging

When logging with BepInEx plugins, especially with Valheim, you have many options for how you want to log. BepInEx offers a very robust Logging API. When using something like Debug.Log() or ZLog.Log(), your print statements are indistinguishable from actual game logs and will make it difficult for users and other developers to know where the message came from. Using the BepInEx Logging API will prefix your statements with the source (your mod's name) and prefixes the log level so you can specify which logs to show up through your BepInEx configuration.

The easiest way to create a logger is to set a ManualLogSource field on your main class that you can access from elsewhere in your mod. This allows you to access Main.logger anywhere else in your code, and it will allow you to access methods like LogInfo, LogDebug, LogWarning, LogError, etc.:

using BepInEx;
using BepInEx.Logging;

namespace MyBepInExPlugin
{
    [BepInPlugin(pluginGUID, pluginName, pluginVersion)]
    public class Main : BaseUnityPlugin
    {
        const string pluginGUID = "com.example.GUID";
        const string pluginName = "MyPlugin";
        const string pluginVersion = "1.0.0";
        public static ManualLogSource logger = BepInEx.Logging.Logger.CreateLogSource(pluginName);

        public void Awake()
        {
            Main.logger.LogInfo("Thank you for using my mod!");
        }
    }
}

When should you use logging?

  • LogInfo: If you have a general message for the user that is not related to an issue.
  • LogDebug: If you have logging statements used for your debugging purposes only. By default, the valheim BepInEx pack has debug logging hidden for users.
  • LogWarning: If you need to warn the user of something important or needing of attention, such as a message to update their configuration files after an update.
  • LogError: If you need to inform the user something went wrong that will affect the mod functionality, or that something will not work as expected.

Resources:

  • For more information on the ManualLogSource API refer to the official BepInEx documentation.
  • For the official BepInEx logging API writeup, refer here.
  • For the official BepInEx logging API documentation, refer here.

BepInEx Dependencies and Incompatibilities

If your mod relies on another, such as the Jotunn library, you will need to add a dependency tag to ensure it loads before your own:

[BepInPlugin("org.bepinex.plugins.MyMod", "My Mod", "1.0.0.0")]
[BepInDependency("org.bepinex.plugins.MyModDependency")]
public class ExamplePlugin : BaseUnityPlugin

BepInEx also has the ability to prevent your mod from loading if another mod is detected. Generally, unless using two mods together will cause game breaking issues, it is not recommended to add an incompatibility tag. This can be used, for example, if you have two versions of your own mod published where one has more features than the other. In cases where your mods add the same items to the game, you will want to ensure one of them (the smaller package in this example) does not load in case of user installation error:

[BepInPlugin("org.bepinex.plugins.MySmallMod", "My Small Mod", "1.0.0.0")]
[BepInIncompatibility("org.bepinex.plugins.MyBigMod")]
public class ExamplePlugin : BaseUnityPlugin

The documentation for Harmony basics covers more information on these topics.

BepInEx Configuration

BepInEx has a built in feature to provide configurations for your mod that the user can customize. It is recommend to use this feature over a custom solution since most mod users already understand how this feature works. You will also have more options for using community tools. Here is a link to the official documentation.

Many people use the official BepInEx configuration manager to change their mod configurations from in-game. It is important to consider this when designing your mod. Users also like the ability to make live changes to configuration files and see that data update live and saved when they shut down the game. Here is a basic example of a plugin using configurations with these extra features:

using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;

namespace MyBepInExPlugin
{
    [BepInPlugin(pluginGUID, pluginName, pluginVersion)]
    public class Main : BaseUnityPlugin
    {
        const string pluginGUID = "com.example.GUID";
        const string pluginName = "MyPlugin";
        const string pluginVersion = "1.0.0";
        public static ManualLogSource logger = BepInEx.Logging.Logger.CreateLogSource(pluginName);

        private static string ConfigFileName = pluginGUID + ".cfg";
        private static string ConfigFileFullPath = BepInEx.Paths.ConfigPath + Path.DirectorySeparatorChar + ConfigFileName;

        private static ConfigEntry<bool> ConfigurationExample = null!;

        // Getter function for reading this value throughout your mod
        public static bool GetConfigurationExample() => ConfigurationExample.Value;

        public void Awake()
        {
            Main.logger.LogInfo("Thank you for using my mod!");

            // Binds the configuration, the passed variable will always reflect the current value set
            ConfigurationExample = Config.Bind("General", "ExampleConfig", true, "This is my boolean config!");

            SetupWatcher();
        }

        private void OnDestroy()
        {
            Config.Save();
        }

        private void SetupWatcher()
        {
            FileSystemWatcher watcher = new(BepInEx.Paths.ConfigPath, ConfigFileName);
            watcher.Changed += ReadConfigValues;
            watcher.Created += ReadConfigValues;
            watcher.Renamed += ReadConfigValues;
            watcher.IncludeSubdirectories = true;
            watcher.SynchronizingObject = ThreadingHelper.SynchronizingObject;
            watcher.EnableRaisingEvents = true;
        }

        private void ReadConfigValues(object sender, FileSystemEventArgs e)
        {
            if (!File.Exists(ConfigFileFullPath)) return;
            try
            {
                Main.Logger.LogDebug("Attempting to reload configuration...");
                Config.Reload();
            }
            catch
            {
                Main.Logger.LogError($"There was an issue loading {ConfigFileName}");
            }
        }
    }
}

See more tips for configurations in Advanced Practices and Tools.

Configuration Syncing

There is no officially supported way to enforce BepInEx configurations in a multiplayer game. Usually users all have individual control over their own config files. There are two well known community supported tools you can use to sync your mod configurations and enforce one version for all clients. To learn more about how to set this up and how it works please see each project's documentation. The right solution for your project will often vary from person to person.

  • Jotunn: External library users install separately as a mod dependency.
    • Benefits: You do not have to update or support this external package.
    • Drawbacks: You do not have personal control over this code, and must work with the Jotunn team to add content.
  • ServeSync: Internal library you bundle inside your mod, requiring no external dependency installs.
    • Benefits: You can easily customize this code if needed for your solution.
    • Drawbacks: No automatic updates, you must recompile and publish mod updates to use newer versions of this package.
Clone this wiki locally