Skip to content

Loading mods

Indraneel Mahendrakumar edited this page Jul 27, 2022 · 3 revisions

Modot loads mods from mod directories at runtime (see Defining mods on how to define a mod directory).

Mod loading is handled by the static ModLoader class, in the Godot.Modding namespace (which is the namespace used for all mod-related classes). ModLoader provides two primary ways to load mods - together and individually.

Loading multiple mods together

The recommended way to load mods is to load all of them together. This enables Modot to load each mod's metadata, filter out invalid or cyclically dependent metadata, and sort the metadata according to the best possible load order before actually loading the mods.

ModLoader.LoadMods(IEnumerable<string>) loads multiple mods together. It takes in mod directory paths as an IEnumerable<string>, loads the mods, and returns an IEnumerable<Mod> (the return value does not have to be enumerated; it is not a lazy operation).

Take for example a user://Mods directory with multiple mods under it:

user://
    Mods
        Mod 1
            Mod.xml
            ...
        Mod 2
            Mod.xml
            ...
        Mod 3

These mods could be loaded like so:

using System.Linq;

using Godot;
using Godot.Modding;

string[] modDirectories = new[] {"user://Mods/Mod 1", "user://Mods/Mod 2", "user://Mods/Mod 3"};
IEnumerable<string> modDirectoriesFullPaths = modDirectories.Select(ProjectSettings.GlobalizePath); // convert Godot paths to native OS paths
ModLoader.LoadMods(modDirectoriesFullPaths);

However, if there are an arbitrary or unknown number of mod directories inside user://Mods, then the approach would be slightly different:

using System.IO;

using Godot;
using Godot.Modding;

string userModsDirectory = ProjectSettings.GlobalizePath("user://Mods");
string[] modDirectories = System.IO.Directory.GetDirectories(userModsDirectory);
ModLoader.LoadMods(modDirectories);

Using this approach, all mod directories under user://Mods will be loaded together.
Note that here a Select() operation need not be performed as the C# filesystem API returns the directory paths as native OS paths.

Loading mods individually

Modot also provides a way to load a mod individually, which bypasses all restrictions related to dependencies, incompatibilities, and load order, and simply loads the mod. This is not recommended as it may lead to elusive errors when other mods are loaded.

Nevertheless, it does have its uses, such as ensuring that a particular mod is always loaded, or is always loaded in a specific position regardless of load order.

ModLoader.LoadMod(string) loads a mod individually. It takes in the mod's directory path as a string, loads the mod, and returns it as a Mod.

Take for example a user://Mods directory with a single mod under it:

user://
    Mods
        Mod 1
            Mod.xml
            ...

This could be loaded like so:

using Godot;
using Godot.Modding;

string modDirectory = ProjectSettings.GlobalizePath("user://Mods/Mod 1");
ModLoader.LoadMod(modDirectory);

Loading mods from the Godot project's directory

As seen in the above examples, Godot filesystem paths (paths beginning with user:// and res://) have to be converted to native OS filesystem paths before they can be used with ModLoader. This is because assemblies and XML documents require a native OS path in order to be loaded, and do not recognise Godot paths.

In most cases, this can be fixed by converting the paths using ProjectSettings.GlobalizePath(string). However, this method only works with user:// paths on export. res:// paths may not actually have a native OS equivalent, as the project could be running on a "virtualized" filesystem or be compressed into a single .pck file (to reduce project size).

This poses a problem, since it means that mods cannot be loaded from res:// file paths. Thankfully, Modot offers a workaround for this. Mod directories in res:// (or any of its subdirectories) can simply be copied over to a directory in user:// before loading them. This can be accomplished using the Directory.CopyContents(string, string, bool) extension method found in the Godot.Utility.Extensions namespace.

CopyContents() allows all files inside a directory to be copied to another location - with the option to copy recursively as well.
For example, a mod in res://Mods/Mod 1 can be loaded like so:

using System.IO;

using Godot;
using Godot.Modding;
using Godot.Modding.Utility.Extensions;

using Godot.Directory directory = new(); // Godot's Directory class
directory.CopyContents("res://Mods/Mod 1", "user://Mods/Mod 1", true); // "true" specifies to perform recursive copy - copying all files inside subdirectories and so on

string userModsDirectory = ProjectSettings.GlobalizePath("user://Mods");
string[] modDirectories = System.IO.Directory.GetDirectories(userModsDirectory);

ModLoader.LoadMods(modDirectories);

This is especially useful as it allows developers to design their entire game as a mod (or multiple mods), bundle it all in res://, and then load it like a regular mod at runtime by transferring the data to user://.

Accessing loaded mods

Modot keeps track of the mods that have been loaded. They can be accessed using the ModLoader.LoadedMods property, which is an IReadOnlyDictionary<string, Mod> where the keys are the mod's IDs.

Note that a mod's assemblies and XML data can be accessed this way, and the XML data could potentially be mutated by other mods. It is also not recommended to enumerate a mod's assemblies like this, as it will run the LINQ Select() query each time, wasting processor cycles and resources (though the assemblies themselves will only be loaded once as Mono prevents loading duplicate assemblies).

Mod loading issues

Simply because a mod directory is present in user:// or res:// does not necessarily mean it has been loaded. There are various issues that could potentially prevent a mod from loading - duplicate IDs, invalid metadata, incompatibilities, and incomplete dependencies, to name a few.

In such cases, the mod will not be present inside ModLoader.LoadedMods, and its assemblies, XML data, and resource packs will not be loaded.

Modot uses GDLogger as a logging framework, and if there is an error during mod loading, the exact error message will be printed to the log file (the default log file path is user://Log.txt).