Mod Features

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

Mod Loading


Create a mod

Inside the Unity Editor, go to Assets/Game/Mods and create a subfolder for your mod. A ReadMe.txt file in this folder suggest how to organize files inside it. Then open Daggerfall Workshop > Mod Builder, fill mod informations (title, author, version etc.) and save the manifest file inside the folder from previous point (i.e. Assets/Game/Mods/Example/Example.dfmod.json).

If the mod contains scripts, a static intro point is needed to instantiate one of them and effectively start the mod.

namespace ExampleMod
{
    public class Example : MonoBehaviour
    {
        static Mod mod;

        [Invoke(StateManager.StateTypes.Start, 0)]
        public static void Init(InitParams initParams)
        {
            mod = initParams.Mod;
            var go = new GameObject(mod.Title);
            go.AddComponent<Example>();
        }

        void Awake()
        {
            var settings = mod.GetSettings();
            // ---

            mod.IsReady = true;
        }
    }
}

Build a mod

Open Daggerfall Workshop > Mod Builder, ensure that all the required assets are listed and build the mod. Three files with .dfmod extension are created, one for each OS. Additional meta files are also generated but are not required for mod operation.

The .dfmod file can be placed inside StreamingAssets/Mods, or any subdirectory, both in game build and Unity Editor.

Debug a mod

Mod manifest files automatically generate a virtual mod inside the Unity Editor, meaning that all assets listed are assumed to be part of the mod even without a physical .dfmod file. A mod inside StreamingAssets/Mods overrides its virtual version.

Operations such as loading all assets from AssetBundle are ignored when running a virtual mod beacuse assets are immediately available. In most cases everything should work just fine, but conditional execution is possible if you have particular requirements.

#if UNITY_EDITOR
    if (mod.IsVirtual)
        Debug.LogFormat("{0} is running in virtual mode.", mod.Title);
    else
        Debug.LogFormat("{0} is running in editor mode.", mod.Title);
#else
    Debug.LogFormat("{0} is running in release mode.", mod.Title);
#endif

Please note that virtual mods are experimental and some discrepancies are possible. Always test a release mod before publishing it.

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:

It is possible to benefit of live changes to mod settings by setting the callback Mod.LoadSettingsCallback.

private void Awake()
{
    mod.LoadSettingsCallback = LoadSettings;
    mod.LoadSettings();
    mod.IsReady = true;
}

private void LoadSettings(ModSettings settings, ModSettingsChange change)
{
    if (change.HasChanged("Foo"))
    {
        var number = settings.GetValue<int>("Foo", "Number");
        var color = settings.GetValue<Color32>("Foo", "Color");
    }

    if (change.HasChanged("Bar", "Text"))
    {
        var text = settings.GetValue<string>("Bar", "Text");
    }
}

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 players and shared with 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 to PersistentDataPath/Mods/GameData/GUID/modpresets.json (EditorData when testing with the Unity Editor). Previously they were stored  in a file named modFileName_presets.json, where modFileName is the name excluding the extension of modFileName.dfmod, inside the folder StreamingAssets/Mods. Existing presets are moved automatically to new position.
  • Presets can also be created by players and shared; the same functionality allows mod developers to provide presets for other mods by placing json files inside StreamingAssets/Presets/TargetModFileName or %ModRoot%/Assets/Presets/TargetModFileName.  Read Share Presets section on the main page for deails.
  • Legacy support is currently also provided for files named modFileName_presets_name.json, where name is an unique identifier, and placed in the same folder of second point. Such file contains a list of one or more presets, each one with a title and description. This functionality is deprecated and will be eventually removed.

Local settings

Local mod settings are stored inside PersistentDataPath/Mods/GameData/GUID/modsettings.json (EditorData when testing with the Unity Editor). Previously, they were 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. Existing settings are moved automatically to new position.

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;
}

Custom Data


Mod.PersistentDataDirectory can be used to store custom data shader by all saves. Each mod is assigned a directory but it’s up to the mod itself to create the folder, load and store data as well as deleting it when not needed anymore. The whole folder is also deleted if mod is uninstalled.

Directory.CreateDirectory(mod.PersistentDataDirectory);
string filePath = Path.Combine(mod.PersistentDataDirectory, "PersistentData.txt");
File.WriteAllText(filePath, "content");

Mod.TemporaryCacheDirectory can be used to store temporary data shared by all saves. Each mod is assigned a directory but it’s up to the mod itself to create the folder, load and store data. The content of this folder can be deleted at any time by the OS.

Directory.CreateDirectory(mod.TemporaryCacheDirectory);
string filePath = Path.Combine(mod.TemporaryCacheDirectory, "Cache.txt");
File.WriteAllText(filePath, "content");

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>();

Starting from version 0.6, a MonoBehaviour class imported from a mod can be marked with the ImportedComponentAttribute to automatize this process. During mod building, imported component instances are serialized to a separate file and stripped from the prefab. When this prefab is requested at run time, imported components are recreated and deserialized, with the types retrieved from mod assemblies. Asset references are also serialized with their name, then seeked in the mod assetbundle.

[ImportedComponent]
public class Foo : MonoBehaviour
{
}

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 png file from StreamingAssets/Textures to override the bundled texture asset 
using DaggerfallWorkshop.Utility.AssetInjection;

Texture2D tex;
if (!TextureReplacement.TryImportTextureFromLooseFiles(Path.Combine("ModName", "Example"), mipMaps, encodeAsNormalMap, readOnly, out tex))
    tex = mod.GetAsset<Texture2D>("Example");

Game Resources


Actions

New quest actions, implementing IQuestAction, can be registered with QuestMachine.RegisterAction, when requested by OnRegisterCustomActions event.

Effects

New effects, inheriting from BaseEntityEffect, can be registered with EntityEffectBroker.RegisterEffectTemplate; It is also possible to override classic effects.

Formulas

Formulas can be overridden from FormulaHelper, registering a delegate to the appropriate dictionary.

formula_2de_2i.Add("CalculateHitPointsPerLevelUp", (de1, de2, a, b) => {
    var player = de1 as PlayerEntity;
    return 0; // add implementation here
});

Item Templates

Items data can be overridden providing a file named ItemTemplates.json. The property index is mandatory because is the identifier for the item; everything else can be omitted not to alter classic value.

[
    {
        "index": 151,
        "name": "Casual Pants",
        "baseWeight": 0.5,
        "hitPoints": 150,
        "capacityOrTarget": 0,
        "basePrice": 5,
        "enchantmentPoints": 40,
        "rarity": 1,
        "variants": 4,
        "drawOrderOrEffect": 10,
        "isBluntWeapon": false,
        "isLiquid": false,
        "isOneHanded": false,
        "isIngredient": false,
        "worldTextureArchive": 204,
        "worldTextureRecord": 0,
        "playerTextureArchive": 239,
        "playerTextureRecord": 16
    }
]

World and Locations

Refer to the following tutorials:

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 });