Mod Features

Mod Loading

Daggerfall Unity Mod Builder as it appears inside the Unity Editor

Create

Inside the Unity Editor, go to Assets/Game/Mods and create a subfolder for your mod, then open Daggerfall Workshop > Mod Builder, choose a title 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

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.

Run

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.

These are the steps for running a mod inside the editor in virtual mode:

  1. When you create a new mod with the mod builder it asks you where to save the manifest file. Pick a subfolder of Assets/Game/Mods, for example Assets/Game/Mods/EXAMPLE/EXAMPLE.dfmod.json.
  2. Select all the assets that should be shipped with the mod inside the mod builder and save. You don’t need to build.
  3. Launch the game with the play button and EXAMPLE will be available as a virtual mod.

If you need to check if the mod is running in virtual mode you can use the following pattern:

#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

Scripts

C# scripts can be provided by a mod using one of the following methods:

  • Add C# files (.cs) as text assets to the list of assets bundled with the mod; they will be compiled at runtime with a portable mcs compiler. Please note that enums, string interpolation and tuple syntax are not supported.
  • Add C# files (.cs) as text assets to the list of assets bundled with the mod and enable the flag Precompiled (experimental); they will be compiled at build time in a single assembly using the same Mono compiler used for the core game.
  • If you want to compile an assembly directly from Visual Studio, you can add the result as a binary asset with extension .dll.bytes to the list of assets bundled with the mod. The assembly needs to reference UnityEngine.CoreModule (Unity) and Assembly-CSharp (Daggerfall Unity).

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/.

UI ControlC# TypeDescription
checkboxbooleanA flag that can be set or unset
multiplechoiceintIndex of selected choice
int sliderintAn integer number in a range
float sliderfloatA floating point number in a range
int tupleTuple<int, int>Two integer numbers
float tupleTuple<float, float>Two floating point numbers
textboxstringA raw string
color pickerColor32A color
Types available for mod settings.

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

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

Custom dave data can be stored in a serializable class, like shown in the following example.

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

A class that implements the IHasSaveData interface handles the process of storing and retrieving data. The instance of this class must be assigned to the mod object (mod.SaveDataInterface).

This is the type of the data class.

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

This is called when the mod is installed for the first time.

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

This method asks the data to store when a new save is created (can be null).

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

Retrieves stored data when a save is loaded.

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, while Mod.TemporaryCacheDirectory can be used to store temporary data (both of them are shared by all saves). Each mod is assigned a directory but it’s up to the mod itself to create the folder.

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

Localization

Localized text content can be provided by including csv-like tables with the following naming:

  • textdatabase.txt
  • [<identifier>]textdatabase.txt (i.e. [en]textdatabase.txt)
  • [<identifier>(<GUID>)]textdatabase.txt (i.e. [fr(7e0510a9-be7d-4fd4-9c89-ed2f2b2e5fb3)]textdatabase.txt))
  • mod_modname.txt inside StreamingAssets/Text

Localize 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.

Localize Additional Content

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.

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. 

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

You can also load all assets in the background and then 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

Custom scripts added by a mod can be instantiated at runtime.

GameObject go = new GameObject();
go.AddComponent<Example>();

Alternatively they can be marked with the ImportedComponentAttribute and added directly to a prefab. Daggerfall Unity relies on custom serialization to support these additional classes.

[ImportedComponent]
public class Example : MonoBehaviour
{
}

Loose Files

Loose files can be retrieved directly from script using the static methods defined inside the namespace 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 with FormulaHelper.RegisterOverride().

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

Mods Interaction

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;

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

The mod message system can also be used to handle events. Note that the receiver is setup from Awake (or the static entry point if you prefer) while the handler registration is performed from Start. This is consistent with the pattern of using Awake to initialize self and Start to access other objects, ensuring that you aren’t accessing something that doesn’t exist yet.

public event Action OnGameObjectSpawned;

void Awake()
{
    mod.MessageReceiver = (string message, object data, DFModMessageCallback callback) =>
    {
        switch (message)
        {
            case "RegisterOnGameObjectSpawned":
                OnGameObjectSpawned += data as Action<GameObject>;
                break;
        }
    };
}
void Start()
{
    ModManager.Instance.SendModMessage("Mod Title", "RegisterOnGameObjectSpawned", (Action<GameObject>)((go) =>
    {
        Debug.Log(go.name);
    }));
}

Dependencies

It is possible for a mod to declare other mods as dependencies that must also be installed, or define criteria to validate local load order. For example you can enforce that your mod is positioned below another one if is available or state compatibility only with a certain version.

To do so, open the mod manifest file (.dfmod.json) and add a new property named Dependencies:

"Files": [
    ],
"Dependencies": [
    {
        "Name": "example-mod",
        "IsOptional": false,
        "IsPeer": false,
        "Version": "1.0.0"
    }
]

These are the supported properties:

  • Name: Name of target mod.
  • IsOptional: If true, target mod doesn’t need to be available, but must validate these criteria if it is.
  • IsPeer: If true, target mod can be positioned anywhere in the load order, otherwise must be positioned above.
  • Version: If not null this string is the minimum accepted version with format X.Y.Z. Pre-release identifiers following an hyphen are ignored in target version so they must be omitted here. For example “1.0.0” is equal to “1.0.0-rc.1”.