Mod Features

This page provides an insight into the modding system of Daggerfall Unity and documentation for its patterns and features.

Settings


Mod settings can be created or changed with the UI settings editor. Open it from the the tools menu and select a folder; for example Addons/ModName/.

Settings consist of a list of options grouped inside sections. Each section has a name, an optional description and a advanced flag. Each option has a name, an optional description, a type, a default value and a number of other values depending on type. The type determines how the option is proposed to the player with a in-game UI. For example a boolean is a simple checkbox while a number is drawn as a slider in the requested range.

The end result is saved in a file named settings.json inside the current folder. This file needs to be bundled inside the mod.

Settings can be retrived in game from the mod instance:

var settings = mod.GetSettings();
int number = settings.GetValue<int>("section", "key");

This is a list of accepted types:

Presets

A preset is a group of values for a given number of settings, which can be provided by the mod developer or created by an user and shared among the community.
Presets are identified by a title and a description.

  • Presets created from the mod settings editor are saved in a file named presets.json inside the current folder. This file needs to be bundled inside the mod.
  • Presets created from the in-game UI are saved in a file named modFileName_presets.json, where modFileName is the name excluding the extension of modFileName.dfmod, inside the folder StreamingAssets/Mods.
  • Presets can also be shared as files named modFileName_presets_name.json, where name is an unique identifier, and placed in the same folder. Such file contains a list of one or more presets, each one with a title and description.

Local settings

Local mod settings are saved alongside mod files in the folder StreamingAssets/Mods with the name modFileName.json, where modFileName is the name excluding the extension of modFileName.dfmod.

Versioning

Mod settings can provide a version number which is used to validate presets and local settings. Outdated presets can still be used but a warning is shown in the preset selection list, while local settings are deleted and replaced with a fresh, updated, copy. Mod settings version is different than mod version because a mod can benefits of updates that don’t require wiping local settings.

Save data


The IHasModSaveData interface allows mods to store and retrieve save data.

First of all a mod must define a class to hold the data that needs to be saved. SaveLoadManager sends and requests the mod an instance of this class, which is automatically serialized in a file specific to the mod. This is modFileName.txt inside a save folder, where modFileName is the name excluding the extension of modFileName.dfmod. In the worst case this file can be deleted but it may be not a bad idea to make use of the versioning system for a smoother upgrade. See FullSerializer docs for details on versioning.

Deleting this file don’t affect changes that the mod could have caused in the core game! Always refer to mod developer for instructions on how to uninstall a mod.

[FullSerializer.fsObject("v1")]
public class MyModSaveData
{
    public string Text;
    public List<int> Items;
}

When the mod is started we have to link the interface to the Mod instance given by InitParams. We can do this in Init, Start or another method used to retrieve the singleton.

mod.SaveDataInterface = instance;

SaveDataType

This is the type of the data class, used for deserialization.

public Type SaveDataType
{
    get { return typeof(MyModSaveData); }
}

NewSaveData()

This is called when the mod is installed for the first time or the serialized file is missing for any reason. In most cases this is just a new instance of the save data class, but it’s also a chance to set initial values different than type defaults.

public object NewSaveData()
{
    return new MyModSaveData
    {
        Text = "Default text",
        Items = new List<int>();
    };
}

GetSaveData()

This method asks the data to serialize when a new save is created. If the return value is null the file is not created.

public object GetSaveData()
{
    return new MyModSaveData
    {
        Text = text,
        Items = items;
    };
}

RestoreSaveData()

Gives the mod the data deserialized (or result of NewSaveData()) to be applied in the running instance. Be sure to set all values to avoid leftovers from the previously running save.

Loading a save game must result in recreating the same situation as when the save was made and must not be affected by the game that was previosuly running. Make sure to reinitialize all mod functionalities based on the game instance.

public void RestoreSaveData(object saveData)
{
    var myModSaveData = (MyModSaveData)saveData;
    text = myModSaveData.Text;
    items = myModSaveData.Items;
}

Localization


A mod called modname is automatically linked to a file named mod_modname.txt in StreamingAssets/Text and/or a file name textdatabase.txt inside the mod bundle. The table in StreamingAssets takes precedence.

Settings

The file settings.json bundled with a mod provides informations used by the mod manager to draw controls in the mod settings window and store values to a local file. This includes setting names and descriptions/tooltips. These text string are seked automatically in the language table using the following patterns:

Mod.Description
Settings.SectionName.Name
Settings.SectionName.Description
Settings.SectionName.KeyName.Name
Settings.SectionName.KeyName.Description
Presets.PresetTitle.Title
Presets.PresetTitle.Description

If a key is missing, the default value from settings.json is used. This means there is no need to provide an english table. No support is required from individual mods, this is all managed by the mod manager.

The mod settings editor can automatically export an english table that can be given to translators for localization.

Mod Strings

The Mod instance includes a method called Localize() which can be used to seek additional localized strings from the text table. The key can be a string or a succession of strings wich are concatenated.

string Localize(string key) // key, text
string Localize(params string[] keyParts) // a.b.c, text
schema: *key, $text

Fountain.Drink, "Press key to drink at the fountain."
string text = mod.Localize("Fountain.Drink").

If a key is not found in the table from the Text folder, is seeked in the fallback table inside the mod. This means that no additional files other than the dfmod are required for a standard english version. Mod developers only need to export strings to a text file and build it as an asset.

Asset Loading


The Modding System provides a ready-to-use framework to store assets and load them at run time, which is internally based on AssetBundles. A mod bundle can contains any kind of asset that derives from UnityEngine.Object, including textures, meshes, sounds, shaders etc. 

Asset-Injection retrieves assets from all mods automatically to replace classic assets without any interaction with the mod. See Import Assets for details.

Assets bundled with a mod can be retrieved from the AssetBundle with the Mod instance.

// Load and get a reference to an asset
Material mat = mod.GetAsset("assetName");

// Load and clone an asset
GameObject go = mod.GetAsset("assetName", true);

For performance reasons one may want to load all assets in the background and dispose the AssetBundle.

// Load assets from AssetBundle at startup
IEnumerator loadAssets = mod.LoadAllAssetsFromBundleAsync(true);
ModManager.Instance.StartCoroutine(loadAssets);

// Get a reference to individual assets when needed
Texture tex = mod.GetAsset("assetName");

Components

A serialized GameObject must not have any script wich is not defined in the Unity Engine or Daggerfall Unity APIs. Custom scripts that derives from UnityEngine.Component can be instantiated at run time.

GameObject go = new GameObject();
Foo foo = go.AddComponent<Foo>();

Loose Files

The main purpose of loose files is to easily provide contributions to  Asset-Injection, but they can also be useful if an asset file is to be provided, edited or replaced by user, which is otherwise impossible if the asset is stored in the mod bundle.

Loose files are organized in folders for different asset types; mods that want to benefit of this feature should place assets within subfolders to avoid name collision with other mods. Valid examples are StreamingAssets/Textures/ModName or StreamingAssets/Textures/AuthorName/ModName.

// Import a texture from StreamingAssets/Textures/relPath.png
using DaggerfallWorkshop.Utility.AssetInjection;
bool TextureReplacement.TryImportTextureFromLooseFiles(string relPath, bool mipMaps, bool encodeAsNormalMap, out Texture2D tex);

Mods Interaction


A good modding framework ought to allow mods to find each other to implement compatibility patches when needed and without user interaction, provide additional extensibility as well as actual frameworks on which other mods can be based. Be advised that a solid understanding of C# patterns is suggested.

The ModManager singleton allows to retreve a Mod instance from its title.

Mod mod = ModManager.Instance.GetMod("modTitle");
bool modFound = mod != null;
bool modStarted = mod != null && mod.IsReady;

If a prerequisite is not found do not let a NullReferenceException be thrown, but rather take appropriate actions such as printing to log “modX was terminated because it requires modY.” Also consider using GetModFromGUID(string modGUID) to avoid breaking if the title is changed.

Message Receiver

The message receiver allows to exchange messages and data with other mods. The receiving mod must have a delegate of type DFModMessageReceiver assigned to Mod.MessageReceiver. A reply can be sent with the callback parameter.

void ModManager.Instance.SendModMessage(string modTitle, string message, object data = null, DFModMessageCallback callback = null);
void DFModMessageReceiver(string message, object data, DFModMessageCallback callBack);
void DFModMessageCallback(string message, object data);

A simple example:

mod.MessageReceiver = (string message, object data, DFModMessageCallback callBack) =>
{
    if (message == "numberRequest" && callBack != null)
        callBack("numberReply", 0);
};
ModManager.Instance.SendModMessage("modTitle", "numberRequest", null, (string message, object data) =>
{
    int number = (int)data;
});

Types

Scripts from other mods can be used with reflection. In the example below a class type is retrieved from a mod assembly and a new object is instantiated. Then a method is invoked, taking care of using a correct function signature.

// Type safe code
Example example = new Example();
int result = example.MethodName("arg0", "arg1");
// Find the type
Mod mod = ModManager.Instance.GetMod("ModTitle");
Type type = mod.GetCompiledType("type");

// Make a new class instance
object instance = Activator.CreateInstance(type);

// Call a method on the class
MethodInfo method = type.GetMethod("MethodName", BindingFlags.Public | BindingFlags.Instance);
int result = (int)method.Invoke(instance, new object[] { "arg0", "arg1" });

If we want to access a specific instance, the procedure is the same but we need to retrieve a reference to the object. This can be requested via Message Receiver or, if the aim is to edit an instantiated gameobject, it can be found inside the scene.

// Get a MonoBehaviour instance from a gameobject
Component component = go.GetComponent("type");

// Read the value of a field
FieldInfo field = component.GetType().GetField("FieldName", BindingFlags.Public | BindingFlags.Instance);
float value = (float)field.GetValue(component);

This pattern can be used for all members of a type, but it’s important for performance to store all results of reflection that need to be accessed again. For more informations on reflection see C# docs Reflection and Dynamically Loading and Using Types.

Events

If a mod relies on a framework provided by another mod, it is not unlikely that events are needed. The following example shows how to subscribe to an instance event in a class that follows the singleton pattern. Subscription to a static event is the same except that a null value is passed in place of the instance.

// Type safe code
Example example = Example.Instance;
example.EventName += HandlerName;
// Get instance from static property
PropertyInfo propertyInfo = type.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static);
object instance = propertyInfo.GetValue(null, null);

// Subscribe to instance event
EventInfo eventInfo = type.GetEvent("EventName", BindingFlags.Public | BindingFlags.Instance);
Delegate handler = Delegate.CreateDelegate(eventInfo.EventHandlerType, this.GetType(), "HandlerName");
eventInfo.GetAddMethod().Invoke(instance, new object[] { handler });