Skip to content

Unity Engine Plugin for easily integrating mod.io into your game - the UGC management service for game developers

Notifications You must be signed in to change notification settings

modio/modio-unity

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

id title sidebar_label slug sidebar_position
unity-introduction
Unity Introduction
Unity Introduction
/unity/unity-introduction/
0

mod.io

mod.io Unity Plugin v2024.11

License Discord Master docs Unity 3D

Welcome to the mod.io Unity Engine plugin repository!

mod.io enables game developers of all sizes to integrate user-generated content directly into their games quickly and easily. This includes hosting, user profiles and subscriptions, moderation tools, file delivery, and more:

  • Completely cross-platform: content uploaded on PC can be enjoyed by players on Xbox, Switch, PlayStation, VR and mobile
  • One-click mod installs, synced to a single mod.io account across all platforms
  • Independent API: integrate mod.io into your game's launcher or your homepage using our Embedded UGC Hub. It can also be used by community fan sites and Discord bots
  • Founded by the ModDB.com team, with over two decades of experience in the UGC space
  • Constantly evolving - we continue to work alongside our partners to iterate and improve our plugin support

The mod.io Unity Engine plugin is the simplest and fastest way to integrate UGC into your Unity 2020.3+ game. It handles all of the common tasks, allowing game developers to quickly and easily implement a solution that enables players to access and discover user-generated content for their games.

A custom built ready-made UI for mod discovery is included, along with installation and collection management, and a full-featured C# interface which connects to the mod.io REST API.

Warning

The Browser UI is scheduled for deprecation, and may not receive updates. This is to be replaced with the Component UI / Template UI

Platform Support

To access console platforms and documentation, see Supporting Console Platforms.

Warning

To enable Mobile In-App Purchasing you need to define the MODIO_MOBILE_IAP in Project Settings > Player > Script Compilation > Scripting Define Symbols This will also require the Unity.Purchasing package to be installed.

Platform Support
Windows âś“
macOS âś“
Linux âś“
Xbox One âś“
Xbox Series X âś“
PlayStation 4 âś“
PlayStation 5 âś“
Nintendo Switch âś“
iOS âś“
Android âś“

Game Studios and Publishers

If you need assistance with first-party approval, or require a private, white-label UGC solution. Contact us!

Contributions Welcome

Our Unity plugin is public and open source. Game developers are welcome to utilize it as-is or fork it for their game's specific requirements.

Want to make changes to our plugin? Submit a pull request, and we'll review your recommended changes! Our goal at mod.io is an open modding API, and you're encouraged to view, fork and contribute to all of our codebases!

Installation

Warning

If you have a previous version of the plugin installed, it is highly recommended to delete it before updating to a later version.

  1. Install the Newtonsoft Json plugin using the Package Manager.
    • If your Unity Package Manager does not contain Newtonsoft Json, follow the instructions here to find the installation method for your Unity version.
  2. Download and install the plugin using one of the following methods:
    • Using the Unity Asset Store and Package Manager.
    • Download the .unitypackage directly from the Releases page.
    • Download an archive of the code using GitHub's download feature, and unpack it in your project's Assets/Plugins directory.
  3. Restart Unity, to ensure it recognises the new assembly definitions.

Note

If you receive errors due to conflicting libraries after installing the plugin, remove any duplicates from Assets/Plugins/mod.io/ThirdParty.

Setup

The first thing you'll need to do is create a game profile on mod.io (or our private test environment).

Important

You'll need your game ID and API key for the following steps.

  1. Ensure you have installed the plugin using the installation instructions above.
  2. In Unity, select the mod.io config file by navigating to Tools -> mod.io -> Edit Settings.
  3. In the Inspector under Server Settings, enter your game ID and API key.
  4. Use the Insert URL buttons to set the server URL depending on where you created your game profile earlier.

Warning

Deselect the config file before entering Play mode. A known Unity bug can cause the Editor to crash in Unity 2019-2021.

Your setup is now complete. The following sections will guide you through getting your mod.io integration up and running quickly.

If you have any questions or need some help join our Discord server.

Quick Start

The mod.io Unity Engine plugin comes with a prebuilt UI, a drop-in, instant solution for browsing and installing your game's mods.

If you want to skip implementing your own UI, head to the Browser UI section for setup and usage instructions. However, we recommend following the guide below to better understand how the plugin works.

Warning

The Browser UI is scheduled for deprecation, and may not receive updates. This is to be replaced with the Component UI / Template UI

Getting Started

In the following section, we will walk through implementing some of the most common functions of the mod.io Unity Engine plugin. We recommend reading this step-by-step guide to ensure you understand how everything works, but you can find the resulting class for reference here.

Video Tutorial

If you prefer a video tutorial, click the image below to view it on YouTube:

mod.io

Initial Setup

First, let's create a new MonoBehaviour called ModIOExample.cs that will contain all of our example functionality:

using UnityEngine;

public class ModIOExample : MonoBehaviour
{
    // TODO: Keep reading the Getting Started guide
}

Once you've created the above class:

  1. Create a new Scene.
  2. In that scene, create an Empty Game Object (name it anything you'd like).
  3. Add the ModIOExample component to your GameObject.
  4. Save the scene.

Initialization

Important

The plugin relies on the config file that is configured during the setup instructions above. Please ensure you have completed all of those steps before proceeding.

Before the plugin can be used, it needs to be initialized for the current player. This usually only needs to happen once, so let's implement Unity's Start method in our ModIOExample.cs file:

using ModIO; // Add this to the top of your class

void Start()
{
    Result result = ModIOUnity.InitializeForUser("default");
    if (!result.Succeeded())
        return;

    Debug.Log("ModIO plugin initialized!");
}

The value passed into InitializeForUser is important. We've used "default" here, however, if your game allows for multiple players on the same installation, you should instead use a unique identifier per player. This allows the plugin to cache authentication and mod-subscriptions on a per-player basis.

Now, return to your scene in Unity, enter Play mode and you should see the logged success message.

Authentication

Note

This guide uses ModIOUnityAsync wherever possible. However, you can find callback equivalents to every method in ModIOUnity if you prefer.

Most of the API’s functionality requires player authentication. The plugin offers a large range of SSO (single-sign on) authentication options, including Steam, Xbox, Google, PlayStation, and more. We strongly recommend using these options as they provide a frictionless user experience and don't require multiple steps.

For now, let's start with a simple email authentication to allow us full access.

Note

While creating the UI layout referenced below is outside the scope of this guide, there are great Unity UI tutorials available. You can, however, use the image below as a guide for the elements required to achieve the same functionality:

Auth Form Example

With your UI created, let's add our authentication functionality:

using UnityEngine.UI; // Add this to the top of your class

[SerializeField] InputField authInput;
[SerializeField] Button authRequest;
[SerializeField] Button authSubmit;
   
void Start()
{
    // Initialization ...

    OnInit();
}

async void OnInit()
{
    Result result = await ModIOUnityAsync.IsAuthenticated();
    if (result.Succeeded())
    {
        OnAuth();

        return;
    }
    
    // You can assign these using the Inspector if you prefer
    authRequest.onClick.AddListener(RequestAuthCode);
    authSubmit.onClick.AddListener(SubmitAuthCode);
}
   
async void RequestAuthCode()
{
    Result result = await ModIOUnityAsync.RequestAuthenticationEmail(authInput.text);
    if (!result.Succeeded())
    {
        Debug.LogError($"RequestAuthenticationEmail failed: {result.message}");

        return;
    }

    Debug.Log($"Authentication email sent to: {authInput.text}");

    authInput.text = string.Empty;
}

async void SubmitAuthCode()
{
    Result result = await ModIOUnityAsync.SubmitEmailSecurityCode(authInput.text);
    if (!result.Succeeded())
    {
        Debug.LogError($"SubmitEmailSecurityCode failed: {result.message}");

        return;
    }

    OnAuth();
}
   
async void OnAuth()
{
    ResultAnd<UserProfile> result = await ModIOUnityAsync.GetCurrentUser();
    if (!result.result.Succeeded())
    {
        Debug.LogError($"GetCurrentUser failed: {result.result.message}");
    }

    Debug.Log($"Authenticated user: {result.value.username}");
}

Important

Don't forget to assign the fields in the Inspector!

If you've implemented the above correctly, you should now be able to:

  1. Start Play mode in Unity
  2. Enter your email address in the input field and press the authRequest button
  3. Retrieve the authorization code from your inbox
  4. Enter the authorization code into the input field and press the authSubmit button
  5. See the logged authentication message

Note

If there is no mod.io account associated with the provided email address, one will automatically be created.

There is something worth highlighting: if you restart Play mode, you'll see the logged authentication message again almost immediately. This is the result of two separate factors:

  • The value passed to InitializeForUser during the initialization section needs to have been used when a user successfully authenticated.
  • At the beginning of OnInit(), we check to see if we are already authenticated, and if so move straight to OnAuth().

If you change the initialization value (currently "default"), you will no longer receive the authenticated log. This functionality can enable support for multiple players; separately tracking authentication states and mod-subscriptions. However, as mentioned previously, most games can pass a constant value if they only ever expect one player on the device.

Note

If your email provider supports it, you can use plus-addressing to test multiple users with a single email address:

john.smith+test1@gmail.com
john.smith+test2@gmail.com
john.smith+test3@gmail.com

Adding Mods

Note

Among a range of other functionality, players can use the mod.io website for creating, modifying, and removing mods for your game.

In this section, we're going to add mods using the plugin and API. Feel free to skip this section if you'd prefer to use the web interface.

Before we can interact with your game's mods via the API, we're going to need to create some test mods. We’ll start by adding some functionality that checks to see if your game has any mods. If it doesn't then we'll upload some using the API:

using System.Threading.Tasks; // Add this to the top of your class

async void OnAuth()
{
    // Authenticated ...
    
    await AddModsIfNone();
}

async Task AddModsIfNone()
{
    // This section ensures we only upload our mods once. Don't worry too much
    // about the specifics for now, we will introduce SearchFilters and GetMods 
    // properly later on.
    ResultAnd<ModPage> resultAnd = await ModIOUnityAsync.GetMods(new SearchFilter());
    if (!resultAnd.result.Succeeded())
    {
        Debug.LogError($"GetMods failed: {resultAnd.result.message}");

        return;
    }

    if (resultAnd.value.modProfiles.Length != 0)
    {
        Debug.Log($"{resultAnd.value.modProfiles.Length} mods found. Not adding mods");

        return;
    }

    // TODO: Keep reading the Getting Started guide
}

Generating Dummy Mods

Note

This section is going to generate some dummy mods for use throughout the rest of this guide. If you already have mods or test files ready to upload, you can skip to the uploading mods section.

Click to expand
Let's generate a few dummy mods for you to use for testing. At a minimum, a mod requires the following:
  • A name
  • A summary
  • A logo (image file with a minimum resolution of 512x288)
  • At least one file

We'll use a third-party API to generate a logo for each of your mods, and we'll create a temporary folder and dummy file in each in your Unity project's directory:

[!NOTE]
Don't worry if you don't understand the code below. Its only job is to generate our dummy mods, and it doesn't have any relation to the plugin! If you'd prefer to create your own dummy mods, skip to the uploading mods section!

[!WARNING]
The following code is going to generate a handful of 10-100 MB files, the size of which will give us enough time to show download progress later on. Ensure you have some free space available in your project directory.

using System.IO; // Add these to the top of your class
using UnityEngine.Networking;

// Reusing a single byte-array is a small memory-conscious
// optimization for when we are generating our dummy files.
static readonly byte[] Megabyte = new byte[1024 * 1024];
static readonly Random RandomBytes = new Random();

async Task AddModsIfNone()
{
    // Return if any mods exist ...
    
    DummyModData[] mods =
    {
        await GenerateDummyMod("Cool Weapon", "A really cool weapon.", "24466B", "FDA576", 10),
        await GenerateDummyMod("Funny Sound Pack", "You'll laugh a lot using this.", "B85675", "633E63", 50),
        await GenerateDummyMod("Klingon Language Pack", "tlhIngan Hol Dajatlh'a'?", "93681C", "FFEAD0", 1),
        await GenerateDummyMod("Ten New Missions", "Ported from the sequel to the prequel!", "FDA576", "D45B7A", 99),
    };
}

async Task<DummyModData> GenerateDummyMod(string name, string summary, string backgroundColor, string textColor, int megabytes)
{
    Debug.Log($"Writing temporary mod file: {name}");

    string path = Path.Combine(Application.dataPath, $"../_temp_dummy_mods/{name}");
    Directory.CreateDirectory(path);

    using (FileStream fs = File.OpenWrite(Path.Combine(path, $"{name}.dummy")))
    {
        for (int i = 0; i < megabytes; i++)
        {
            RandomBytes.NextBytes(Megabyte);
            await fs.WriteAsync(Megabyte, 0, Megabyte.Length);
        }
    }

    return new DummyModData(
        name,
        summary,
        await GenerateLogo(name.Replace(' ', '+'), backgroundColor, textColor),
        path
    );
}

// Uses a third-party API to generate a logo for each
// mod, adding some variety when we display them later
async Task<Texture2D> GenerateLogo(string text, string backgroundColor, string textColor)
{
    UnityWebRequest request = UnityWebRequestTexture.GetTexture($"https://placehold.co/512x288/{backgroundColor}/{textColor}.png?text={text}");
    request.SendWebRequest();

    while (!request.isDone)
        await Task.Yield();

    if (request.result != UnityWebRequest.Result.Success)
    {
        Debug.LogError($"GenerateLogo failed: {request.error}");

        return null;
    }

    return DownloadHandlerTexture.GetContent(request);
}

readonly struct DummyModData
{
    public readonly string name;
    public readonly string summary;
    public readonly Texture2D logo;
    public readonly string path;

    public DummyModData(string name, string summary, Texture2D logo, string path)
    {
        this.name = name;
        this.summary = summary;
        this.logo = logo;
        this.path = path;
    }
}

Uploading Mods

Uploading mods is a two-step process:

  1. We need to create a Mod Profile. This is essentially the mod's page or listing. It contains all of a mod's metadata, including its files.
    • Creating a Mod Profile requires an API call (CreateModProfile), which will return a mod id.
  2. We can upload our files to the associated Mod Profile with our newly created mod id.

Let's add a method that handles both steps:

Note

The following code takes advantage of ModIOUnity.GetCurrentUploadHandle, which can be used for obtaining the current upload progress. This isn't required, and you can use await ModIOUnityAsync.UploadModFile(details) instead if you prefer.

async Task UploadMod(string name, string summary, Texture2D logo, string path)
{
    Debug.Log($"Starting upload: {name}");

    ModProfileDetails details = new ModProfileDetails
    {
        name = name,
        summary = summary,
        logo = logo,
    };

    ResultAnd<ModId> resultCreate = await ModIOUnityAsync.CreateModProfile(ModIOUnity.GenerateCreationToken(), details);
    if (!resultCreate.result.Succeeded())
    {
        Debug.LogError($"CreateModProfile failed: {resultCreate.result.message}");

        return;
    }

    ModfileDetails modFile = new ModfileDetails
    {
        modId = resultCreate.value,
        directory = path,
    };

    float progress = 0f;

    Task<Result> taskUpload = ModIOUnityAsync.UploadModfile(modFile);
    while (!taskUpload.IsCompleted)
    {
        ProgressHandle progressHandle = ModIOUnity.GetCurrentUploadHandle();

        if (!Mathf.Approximately(progressHandle.Progress, progress))
        {
            progress = progressHandle.Progress;
            Debug.Log($"Uploading: {name} ({Mathf.RoundToInt(progress * 100)}%)");
        }

        await Task.Delay(1000);
    }

    if (!taskUpload.Result.Succeeded())
    {
        Debug.LogError($"UploadModfile failed: {taskUpload.Result.message}");

        return;
    }

    Debug.Log($"Finished upload: {name}");
}

All that's left now is to feed some mods to our brand new UploadMod method. After we test to see if any mods exist in AddModsIfNone, we will iterate our list of mods and upload them:

Important

If you didn't generate dummy mods in the previous section, modify the below to suit your mod files.

async Task AddModsIfNone()
{
    // Return if any mods exist ...
    
    DummyModData[] mods =
    {
        // ...
    };
    
    foreach (DummyModData mod in mods)
    {
        await UploadMod(mod.name, mod.summary, mod.logo, mod.path);
        // Directory.Delete(mod.path, true); // Uncomment if you generated dummy mods
    }
}

Note

When uploading a mod, the plugin expects a directory (for each mod) that it will compress before uploading. You do not need to zip your files before uploading.

That’s it! Enter Play mode now and, after authentication, the Unity console should come to life with the upload progress of your mods. If you're eager, you can view the mods as soon as they're uploaded by going to your game's mod.io page and using the web interface.

Searching for Mods

Searching for mods is a simple task — we've actually seen it already in the adding mods section. To get a list of all1 available mods you can use the GetMods method:

using System; // Add this to the top of your class

async Task<ModProfile[]> GetAllMods()
{
    ResultAnd<ModPage> resultAnd = await ModIOUnityAsync.GetMods(new SearchFilter());
    if (!resultAnd.result.Succeeded())
    {
        Debug.LogError($"GetMods failed: {resultAnd.result.message}");

        return Array.Empty<ModProfile>();
    }

    return resultAnd.value.modProfiles;
}

Important

A Mod Profile is a read-only snapshot of the state of a mod. It is not a unique or dynamic class. Compare ModProfile.id if you want to determine whether two Mod Profiles represent the same mod.

If we add the above to our example class, and then head back up to our OnAuth method we can quickly log a list of all1 of our available mods:

using System.Linq; // Add this to the top of your class

ModProfile[] allMods;

async void OnAuth()
{
    // ...
    
    allMods = await GetAllMods();
    Debug.Log($"Available mods:\n{string.Join("\n", allMods.Select(mod => $"{mod.name} (id: {mod.id.id})"))}");
}

We write all1 because while using the default Search Filter settings will return all of your mods, this is only because you don't have many. This brings us to Search Filters.

Search Filters

The maximum number of results returned can be set in the Search Filter using its SetPageSize() method. However, the default value of 100 is also the limit. In order to return later results, you can use the Search Filter's SetPageIndex() method.

This is fairly simple in practice and is explained best with the following snippet:

var searchFilter = new SearchFilter();
searchFilter.SetPageSize(10);
searchFilter.SetPageIndex(0); // Will return results 1-10
searchFilter.SetPageIndex(1); // Will return results 11-20
searchFilter.SetPageIndex(2); // Will return results 21-30

// You can also set pageIndex and pageSize using the constructor
new SearchFilter(0, 10); // Will return results 1-10
new SearchFilter(1, 10); // Will return results 11-20
new SearchFilter(2, 10); // Will return results 21-30

Note

Search Filters have a number of options for filtering and ordering your results. See the documentation (or use code completion in your IDE) for its available options.

Downloading Images

More specifically: downloading a Mod Profile's images. We'll cover subscribing to and installing mods soon.

A common feature when listing mods is to display an image along with its name and summary. Metadata images such as logos, screenshots, and avatars don't require subscribing to a mod to view them, and can be downloaded separately from a mod's in-game files.

As we know, all mods have a logo. So let's write a short method that selects a random mod, downloads its logo and displays it alongside its name:

Note

Below is a screenshot of the UI we're using to utilize the method. You can use this as a guide for your own or display the result however you'd like!

Auth Form Example

[SerializeField] Text randomName;
[SerializeField] Image randomLogo;

async void SetRandomMod()
{
    ModProfile modProfile = allMods[UnityEngine.Random.Range(0, allMods.Length)];

    randomName.text = modProfile.name;

    ResultAnd<Texture2D> resultAnd = await ModIOUnityAsync.DownloadTexture(modProfile.logoImage_320x180);
    if (!resultAnd.result.Succeeded())
    {
        Debug.LogError($"DownloadTexture failed: {resultAnd.result.message}");

        return;
    }

    Texture2D logo = resultAnd.value;
    randomLogo.sprite = Sprite.Create(logo, new Rect(0, 0, logo.width, logo.height), Vector2.zero);
}

Warning

The code above relies on allMods, which is set in the first searching for mods section. Ensure that allMods has been set before running this method.

This method is downloading the smallest version of the logo, logoImage_320x180. However, Mod Profiles have a number of sizes for each image. See the documentation (or use code completion in your IDE) to view available options.

Getting Subscribed Mods

We're going to cover mod subscriptions in what will seem like a backward way. First, we'll learn how to get a list of our subscribed mods, then we'll learn how to subscribe to a mod.

The reason we do it this way is because subscribed mods are cached locally, so we avoid redundant API calls if we're already subscribed.

With that in mind, the rest is quite straightforward:

static ModProfile[] GetSubscribedMods()
{
    SubscribedMod[] subscribed = ModIOUnity.GetSubscribedMods(out Result result);
    if (!result.Succeeded())
    {
        Debug.LogError($"GetSubscribedMods failed: {result.message}");

        return Array.Empty<ModProfile>();
    }

    return subscribed.Select(mod => mod.modProfile).ToArray();
}

Getting the user's subscribed mods first requires a call to FetchUpdates(). This method synchronises the cached local state with mod.io (subscriptions, ratings, etc.). This is an expensive method and usually only needs to be called on authentication, as local subscription changes are reflected automatically. However if, for example, you change subscriptions using the web interface, they won't be reflected in your game until FetchUpdates() has been called.

We'll make use of the above from our OnAuth() method, after we log all available mods. First, we call FetchUpdates() to synchronize our local state, and then we can get an array of our subscribed mods:

async void OnAuth()
{
    // ...
    
    Result resultUpdate = await ModIOUnityAsync.FetchUpdates(); // Synchronize our local state
    if (!resultUpdate.Succeeded())
    {
        Debug.LogError($"FetchUpdates failed: {resultUpdate.message}");
    }

    ModProfile[] subscribedMods = GetSubscribedMods();
    Debug.Log($"Subscribed mods:\n{(subscribedMods.Length > 0 ? string.Join("\n", subscribedMods.Select(mod => $"{mod.name} (id: {mod.id.id})")) : "None")}");
}

Important

A Mod Profile is a read-only snapshot of the state of a mod. It is not a unique or dynamic class. Compare ModProfile.id if you want to determine whether two Mod Profiles represent the same mod.

For now, you should see "Subscribed mods: None" in the log if you enter Play mode. But we're going to rectify that very soon.

Subscribing to Mods

Note

The web interface at your game's mod.io page can also be used to subscribe to mods. However, you'll need to exit and enter Play mode to see the changes, as FetchUpdates() needs to be run to synchronise the local state.

Subscribing to mods is very simple. The following code adds a check to see if we're already subscribed and then logs the name of the mod once the subscription is successful. The only actual requirement is the call to SubscribeToMod():

async Task SubscribeToMod(ModId modId, ModProfile[] subscribed)
{
    if (subscribed.Any(mod => mod.id == modId))
        return;

    Result result = await ModIOUnityAsync.SubscribeToMod(modId);
    if (!result.Succeeded())
    {
        Debug.LogError($"SubscribeToMod failed: {result.message}");

        return;
    }

    Debug.Log($"Subscribed to mod: {allMods.First(mod => mod.id == modId).name}");
}

Note

Inefficient code is used here to reduce the sample's complexity. Translating subscriptions to a List<ModId> and keeping a Dictionary<ModId, ModProfile> would be more efficient solutions for these look-ups.

To test it out:

  1. Enter Play mode, and in the "Available mods" log, locate the "Ten New Missions" id.
  2. In your OnAuth() method, after we log all subscribed mods add the following line (replace YOUR_MOD_ID with the id from step 1):
    async void OnAuth()
    {
        // ...
        
        await SubscribeToMod(new ModId(YOUR_MOD_ID), subscribedMods);
    }
  3. Restart Play mode and you should see "Ten New Missions" in your "Subscribed mods" log!

Installing Mods

Now, the moment we've all been waiting for. Downloading, installing, updating, and deleting mods are all handled automatically by the plugin. However, it requires both an authenticated user and to opt-in to the behaviour. The latter is done via a simple EnableModManagement() call. This method requires a delegate argument that exposes us to the various events that mod management emits.

In the following code we're going to enable mod management and also log the download progress when a mod is being downloaded:

string downloadName = "";
float downloadProgress;

void EnableModManagement()
{
    void HandleModManagementEvent(ModManagementEventType eventType, ModId modId, Result eventResult)
    {
        switch (eventType)
        {
            case ModManagementEventType.DownloadStarted:
                downloadName = allMods.First(mod => mod.id == modId).name;
                Debug.Log($"Downloading {downloadName}");
                break;
            case ModManagementEventType.Downloaded:
                Debug.Log($"Downloaded {downloadName}");
                downloadName = string.Empty;
                break;
            case ModManagementEventType.DownloadFailed:
                Debug.Log($"Download failed {downloadName}");
                downloadName = string.Empty;
                break;
        }
    }

    ModIOUnity.EnableModManagement(HandleModManagementEvent);
}

void Update()
{
    if (downloadName.Length == 0)
        return;

    ProgressHandle progress = ModIOUnity.GetCurrentModManagementOperation();

    if (Mathf.Approximately(progress.Progress, downloadProgress))
        return;

    downloadProgress = progress.Progress;
    Debug.Log($"Downloading {downloadName} ({Mathf.RoundToInt(downloadProgress * 100)}%)");
}

In a real implementation, you'll likely track the modId's download and install progress separately to display in your UI. But, this should give you an idea of what's possible with the mod management feature.

Note

There are a number of mod management events available. See the documentation (or use code completion in your IDE) for a complete list.

Using Mods

We’re nearing the end now. You've initialized. You've authenticated. You've uploaded. You've searched. You've subscribed. You've installed. It's all led to this single question:

"How do I find installed mods?"

The answer is very straight forward: GetInstalledModsForUser(). Using this is as simple as expected:

void LogInstalledModsForUser()
{
    UserInstalledMod[] installedMods = ModIOUnity.GetInstalledModsForUser(out Result result);
    if (!result.Succeeded())
    {
        Debug.LogError($"GetInstalledModsForUser failed: {result.message}");

        return;
    }

    Debug.Log($"Installed mods:\n{(installedMods.Length > 0 ? string.Join("\n", installedMods.Select(mod => $"{mod.modProfile.name} ({mod.directory})")) : "None")}");
}

We're currently logging each installed mod and the path to its files (UserInstalledMod.directory). However, you are only limited by how you want to utilize user-generated content. A mod's installation directory is exactly the same as when we uploaded it: uncompressed and ready for action.

That’s it, we’re done. The time has come to build a bridge to your creator community using mod.io.

Please join us on our Discord server if you have any questions or need some help.

Complete Class

Note

You can also find the following class (along with an example scene) in Assets/Plugins/mod.io/Example.

Click to expand
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

namespace ModIO
{
    public class ModIOExample : MonoBehaviour
    {
        // Generating Dummy Mods
        static readonly byte[] Megabyte = new byte[1024 * 1024];
        static readonly System.Random RandomBytes = new System.Random();

        [Header("Authentication")]
        [SerializeField] GameObject authContainer;
        [SerializeField] InputField authInput;
        [SerializeField] Button authRequest;
        [SerializeField] Button authSubmit;

        // Downloading Images
        [Header("Random Mod")]
        [SerializeField] GameObject randomContainer;
        [SerializeField] Text randomName;
        [SerializeField] Image randomLogo;
        [SerializeField] Button randomButton;

        // Searching for Mods
        ModProfile[] allMods;

        // Installing Mods
        string downloadName = "";
        float downloadProgress;

        void Awake()
        {
            randomContainer.SetActive(false);
        }

        #region Initialization

        void Start()
        {
            Result result = ModIOUnity.InitializeForUser("default");
            if (!result.Succeeded())
                return;

            Debug.Log("ModIO plugin initialized!");

            OnInit();
        }

        async void OnInit()
        {
            Result result = await ModIOUnityAsync.IsAuthenticated();
            if (result.Succeeded())
            {
                OnAuth();

                return;
            }

            authRequest.onClick.AddListener(RequestAuthCode);
            authSubmit.onClick.AddListener(SubmitAuthCode);
        }

        #endregion

        #region Authentication

        async void RequestAuthCode()
        {
            Result result = await ModIOUnityAsync.RequestAuthenticationEmail(authInput.text);
            if (!result.Succeeded())
            {
                Debug.LogError($"RequestAuthenticationEmail failed: {result.message}");

                return;
            }

            Debug.Log($"Authentication email sent to: {authInput.text}");

            authInput.text = string.Empty;
        }

        async void SubmitAuthCode()
        {
            Result result = await ModIOUnityAsync.SubmitEmailSecurityCode(authInput.text);
            if (!result.Succeeded())
            {
                Debug.LogError($"SubmitEmailSecurityCode failed: {result.message}");

                return;
            }

            OnAuth();
        }

        #endregion

        async void OnAuth()
        {
            ResultAnd<UserProfile> result = await ModIOUnityAsync.GetCurrentUser();
            if (!result.result.Succeeded())
            {
                Debug.LogError($"GetCurrentUser failed: {result.result.message}");
            }

            Debug.Log($"Authenticated user: {result.value.username}");

            authContainer.SetActive(false);

            await AddModsIfNone();

            allMods = await GetAllMods();
            Debug.Log($"Available mods:\n{string.Join("\n", allMods.Select(mod => $"{mod.name} (id: {mod.id.id})"))}");

            randomButton.onClick.AddListener(SetRandomMod);
            randomContainer.SetActive(true);
            SetRandomMod();

            ModProfile[] subscribedMods = await GetSubscribedMods();
            Debug.Log($"Subscribed mods:\n{(subscribedMods.Length > 0 ? string.Join("\n", subscribedMods.Select(mod => $"{mod.name} (id: {mod.id.id})")) : "None")}");

            if (allMods.Length > 0)
            {
                int index = Array.FindIndex(allMods, mod => mod.name == "Ten New Missions");
                if (index != -1)
                    await SubscribeToMod(allMods[index].id, subscribedMods);
                else
                    Debug.Log("Couldn't find Ten New Missions mod, not subscribing");
            } else
                Debug.Log("No mods found, not subscribing");

            EnableModManagement();

            LogInstalledModsForUser();
        }

        #region Adding Mods

        async Task AddModsIfNone()
        {
            ResultAnd<ModPage> resultAnd = await ModIOUnityAsync.GetMods(new SearchFilter());
            if (!resultAnd.result.Succeeded())
            {
                Debug.LogError($"GetMods failed: {resultAnd.result.message}");

                return;
            }

            if (resultAnd.value.modProfiles.Length != 0)
            {
                Debug.Log($"{resultAnd.value.modProfiles.Length} mods found. Not adding mods");

                return;
            }

            DummyModData[] mods =
            {
                await GenerateDummyMod("Cool Weapon", "A really cool weapon.", "24466B", "FDA576", 10),
                await GenerateDummyMod("Funny Sound Pack", "You'll laugh a lot using this.", "B85675", "633E63", 50),
                await GenerateDummyMod("Klingon Language Pack", "tlhIngan Hol Dajatlh'a'?", "93681C", "FFEAD0", 1),
                await GenerateDummyMod("Ten New Missions", "Ported from the sequel to the prequel!", "FDA576", "D45B7A", 99),
            };

            foreach (DummyModData mod in mods)
            {
                await UploadMod(mod.name, mod.summary, mod.logo, mod.path);
                Directory.Delete(mod.path, true);
            }
        }

        #endregion

        #region Uploading Mods

        static async Task UploadMod(string name, string summary, Texture2D logo, string path)
        {
            Debug.Log($"Starting upload: {name}");

            ModProfileDetails details = new ModProfileDetails
            {
                name = name,
                summary = summary,
                logo = logo,
            };

            ResultAnd<ModId> resultCreate = await ModIOUnityAsync.CreateModProfile(ModIOUnity.GenerateCreationToken(), details);
            if (!resultCreate.result.Succeeded())
            {
                Debug.LogError($"CreateModProfile failed: {resultCreate.result.message}");

                return;
            }

            ModfileDetails modFile = new ModfileDetails
            {
                modId = resultCreate.value,
                directory = path,
            };

            float progress = 0f;

            Task<Result> taskUpload = ModIOUnityAsync.UploadModfile(modFile);
            while (!taskUpload.IsCompleted)
            {
                ProgressHandle progressHandle = ModIOUnity.GetCurrentUploadHandle();

                if (!Mathf.Approximately(progressHandle.Progress, progress))
                {
                    progress = progressHandle.Progress;
                    Debug.Log($"Uploading: {name} ({Mathf.RoundToInt(progress * 100)}%)");
                }

                await Task.Delay(1000);
            }

            if (!taskUpload.Result.Succeeded())
            {
                Debug.LogError($"UploadModfile failed: {taskUpload.Result.message}");

                return;
            }

            Debug.Log($"Finished upload: {name}");
        }

        #endregion

        #region Searching for Mods

        async Task<ModProfile[]> GetAllMods()
        {
            ResultAnd<ModPage> resultAnd = await ModIOUnityAsync.GetMods(new SearchFilter());
            if (!resultAnd.result.Succeeded())
            {
                Debug.LogError($"GetMods failed: {resultAnd.result.message}");

                return Array.Empty<ModProfile>();
            }

            return resultAnd.value.modProfiles;
        }

        #endregion

        #region Downloading Images

        async void SetRandomMod()
        {
            ModProfile modProfile = allMods[UnityEngine.Random.Range(0, allMods.Length)];

            randomName.text = modProfile.name;

            ResultAnd<Texture2D> resultAnd = await ModIOUnityAsync.DownloadTexture(modProfile.logoImage_320x180);
            if (!resultAnd.result.Succeeded())
            {
                Debug.LogError($"DownloadTexture failed: {resultAnd.result.message}");

                return;
            }

            Texture2D logo = resultAnd.value;
            randomLogo.sprite = Sprite.Create(logo, new Rect(0, 0, logo.width, logo.height), Vector2.zero);
        }

        #endregion

        #region Getting Subscribed Mods

        static async Task<ModProfile[]> GetSubscribedMods()
        {
            Result resultUpdate = await ModIOUnityAsync.FetchUpdates();
            if (!resultUpdate.Succeeded())
            {
                Debug.LogError($"FetchUpdates failed: {resultUpdate.message}");

                return Array.Empty<ModProfile>();
            }

            SubscribedMod[] subscribed = ModIOUnity.GetSubscribedMods(out Result resultSubscribed);
            if (!resultSubscribed.Succeeded())
            {
                Debug.LogError($"GetSubscribedMods failed: {resultSubscribed.message}");

                return Array.Empty<ModProfile>();
            }

            return subscribed.Select(mod => mod.modProfile).ToArray();
        }

        #endregion

        #region Subscribing to Mods

        async Task SubscribeToMod(ModId modId, ModProfile[] subscribed)
        {
            if (subscribed.Any(mod => mod.id == modId))
                return;

            Result result = await ModIOUnityAsync.SubscribeToMod(modId);
            if (!result.Succeeded())
            {
                Debug.LogError($"SubscribeToMod failed: {result.message}");

                return;
            }

            Debug.Log($"Subscribed to mod: {allMods.First(mod => mod.id == modId).name}");
        }

        #endregion

        #region Installing Mods

        void EnableModManagement()
        {
            void HandleModManagementEvent(ModManagementEventType eventType, ModId modId, Result eventResult)
            {
                switch (eventType)
                {
                    case ModManagementEventType.DownloadStarted:
                        downloadName = allMods.First(mod => mod.id == modId).name;
                        Debug.Log($"Downloading {downloadName}");
                        break;
                    case ModManagementEventType.Downloaded:
                        Debug.Log($"Downloaded {downloadName}");
                        downloadName = string.Empty;
                        break;
                    case ModManagementEventType.DownloadFailed:
                        Debug.Log($"Download failed {downloadName}");
                        downloadName = string.Empty;
                        break;
                }
            }

            ModIOUnity.EnableModManagement(HandleModManagementEvent);
        }

        void Update()
        {
            if (downloadName.Length == 0)
                return;

            ProgressHandle progress = ModIOUnity.GetCurrentModManagementOperation();

            if (Mathf.Approximately(progress.Progress, downloadProgress))
                return;

            downloadProgress = progress.Progress;
            Debug.Log($"Downloading {downloadName} ({Mathf.RoundToInt(downloadProgress * 100)}%)");
        }

        #endregion

        #region Using Mods

        static void LogInstalledModsForUser()
        {
            UserInstalledMod[] installedMods = ModIOUnity.GetInstalledModsForUser(out Result result);
            if (!result.Succeeded())
            {
                Debug.LogError($"GetInstalledModsForUser failed: {result.message}");

                return;
            }

            Debug.Log($"Installed mods:\n{(installedMods.Length > 0 ? string.Join("\n", installedMods.Select(mod => $"{mod.modProfile.name} ({mod.directory})")) : "None")}");
        }

        #endregion

        #region Generate Dummy Mods

        static async Task<DummyModData> GenerateDummyMod(string name, string summary, string backgroundColor, string textColor, int megabytes)
        {
            Debug.Log($"Writing temporary mod file: {name}");

            string path = Path.Combine(Application.dataPath, $"../_temp_dummy_mods/{name}");
            Directory.CreateDirectory(path);

            using (FileStream fs = File.OpenWrite(Path.Combine(path, $"{name}.dummy")))
            {
                for (int i = 0; i < megabytes; i++)
                {
                    RandomBytes.NextBytes(Megabyte);
                    await fs.WriteAsync(Megabyte, 0, Megabyte.Length);
                }
            }

            return new DummyModData(
                name,
                summary,
                await GenerateLogo(name.Replace(' ', '+'), backgroundColor, textColor),
                path
            );
        }

        static async Task<Texture2D> GenerateLogo(string text, string backgroundColor, string textColor)
        {
            UnityWebRequest request = UnityWebRequestTexture.GetTexture($"https://placehold.co/512x288/{backgroundColor}/{textColor}.png?text={text}");
            request.SendWebRequest();

            while (!request.isDone)
                await Task.Yield();

            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"GenerateLogo failed: {request.error}");

                return null;
            }

            return DownloadHandlerTexture.GetContent(request);
        }

        readonly struct DummyModData
        {
            public readonly string name;
            public readonly string summary;
            public readonly Texture2D logo;
            public readonly string path;

            public DummyModData(string name, string summary, Texture2D logo, string path)
            {
                this.name = name;
                this.summary = summary;
                this.logo = logo;
                this.path = path;
            }
        }

        #endregion
    }
}

Browser UI

Warning

The Browser UI is scheduled for deprecation, and may not receive updates. This is to be replaced with the Component UI / Template UI

Important

The Browser UI relies on the config file that is configured during the setup instructions above. Ensure you have completed all of those steps before proceeding.

The Browser UI is incredibly simple to set up, and completely avoids the complexity that can come with building a full-featured mod browser:

  1. Drag the Browser UI prefab at Assets/Plugins/mod.io/UI/Examples/ModIOBrowser into your scene.
  2. Call its ModIOBrowser.Browser.Open() method to show the browser in your scene.

If you want a fuller understanding of the plugin and its features, we recommend following the getting started guide above.

Browser UI screenshot Browser UI screenshot

Terms of Use Localization and RTL Languages

When a user authenticates, the Browser UI will localize mod.io's terms of use based on the language code set in your config file (Tools > mod.io > Edit Settings, or Settings.server.languageCode).

To avoid the plugin conflicting with an existing solution, in the case of right-to-left languages you will need to apply your current implementation for mixed RTL and LTR text to the terms text-elements in the browser.

Component UI

The Component UI is an experimental module that allows for a more easily integrated and customised Mod Browsing UI. This can be accessed via the modio-ui.unitypackage provided in the plugin in the experimental folder. A Readme is provided in the package.

Important

This will be replacing the functionality provided by the Browser UI, once the Browser UI is deprecated.

Marketplace

The mod.io SDK supports full monetization features, allowing you to sell a per-game virtual currency to your players that they can use to purchase mods, with a share of the revenue split between creators and your studio. Every platform requires specific setup for monetization features to work, with regards to the virtual currency configuration and API calls.

The following documentation walks you through the setup process and gives example usages. The mod.io monetization features are enabled as part of the onboarding process on your game profile. Once that is setup, there is nothing further you need to do for initialization in the SDK.

Enable Marketplace in the Plugin

The first thing you will need to do is enable the marketplace in the mod.io portal for your game under Admin->Monetization->Settings->Enable Marketplace.

Get User Wallet Balance

Returns the current user's virtual currency credits balance.

Note

This function creates a wallet for the user the first time it is called so this must be called before any sync entitlements calls.

async void GetUserWalletBalanceExample()
{
    var response = await ModIOUnityAsync.GetUserWalletBalance();
    if (response.result.Succeeded())
        Debug.Log($"User has a balance of {response.value.balance} credits.");
    else
        Debug.Log("failed to get balance");
}

Purchase Item

Purchases a mod using virtual currency credits.

async void PurchaseItemExample()
{
    ModId modId = new ModId(1234); // Mod to purchase
    int displayedAmount = 12; // Price displayed to the player (must match mod price)
    string idempotent = $"aUniqueKey"; // Unique key used to prevent duplicate purchases
    
    var response = await ModIOUnityAsync.PurchaseItem(modId, displayedAmount, idempotent);
    if (response.result.Succeeded())
        Debug.Log("Completed Purchase");
    else
        Debug.Log("failed to complete purchase");
}

Get User Purchases

Returns the current user's purchased mods.

async void GetUserPurchases()
{
    ModProfile[] purchased = ModIOUnity.GetPurchasedMods(out Result result);
    if (result.Succeeded())
        foreach (ModProfile mod in purchased)
            Debug.Log($"User owns mod with id: {mod.id}");
    else
        Debug.Log("Failed to get purchases");
}

Syncing Purchases with Steam

Note

Setup virtual currency pack SKUs from your game's mod.io website dashboard by navigating to Admin -> Monetization -> Manage SKUs.

Note

The GetUserWalletBalanceExample function creates a wallet for the user the first time it is called so this must be called before any sync entitlements calls.

Once you have setup SKUs for your users to purchase virtual currency packs through Steam, you can sync these purchases with the mod.io server using the SyncEntitlments() method.

After a user purchases a virtual currency pack on Steam, calling SyncEntitlements() will consume the purchased item, and add those credits to the user's wallet. Below is a very simple example of how to use the method:

Warning

It is highly recommended that you call SyncEntitlements() after any successful external purchase.

async void SyncEntitlements()
{
    Result result = await ModIOUnityAsync.SyncEntitlements();
    if (response.result.Succeeded())
        Debug.Log("Entitlements are synced");
    else
        Debug.Log("Failed to sync entitlements");
}

Note

SyncEntitlements() is automatically run during ModIOUnity.FetchUpdates().

Note

SyncEntitlements() can also be used for consuming purchases on console platforms.

Service to Service API

To facilitate HTTP requests to mod.io's Service To Service (S2S) API's, your backend server must first authenticate and generate credentials that your backend service will use. Credentials required by S2S API's are separate from mod.io's public API endpoints and cannot be used interchangeably.

Requesting a User Delegation Token

Some service-to-service endpoints require user context to be able to make requests on behalf of a user, such as creating a transaction. To facilitate this, mod.io hosts a public endpoint which can be called by an authenticated user with their bearer token which returns what we call a User Delegation Token. This token should then be sent to your secure backend server from your game client, where you can then use it for specific endpoints in conjunction with a valid service token.

Note

User must me authenticated to request a User Delegation Token.

async void Example()
{
     ResultAnd<UserDelegationToken> response = await ModIOUnityImplementation.RequestUserDelegationToken();

    if (response.result.Succeeded())
    {
        Debug.Log("successful.");
        //TODO: Send response.value.token to server
    }
    else
    {
        Debug.Log("failed.");
    }
 }

Temp Mod Sets

Temp Mod sets allow users to download mods that they are not subscribed to. This can be helpful in multiplayer situations, when a player might join a game that requires specific mods to be downloaded. The intended flow in this situation would be to Create a temp mod set when a player joins the game and Delete the temp mod set when the game is over. The following documentation walks you through the setup process and gives example usages.

Creating a temp mod set starts to download mods in the set in a temporary location unassociated with their subscribed mods.

Note

Mods that the user is subscribed to will be not be re-downloaded and will remain in the installed mods location for that user.

ModId[] modIds;
void Example()
{
    Result result = await ModIOUnityAsync.CreateTempModSet(modIds);
    if (result.Succeeded())
    {
        Debug.Log("Successful");
    }
    else
    {
        Debug.Log("Failed");
    }
}

Destroying a temp mod set removes the temporary installed mods in that set and allows for uninstallation of temporary mods.

void Example()
{
    Result result = await ModIOUnityAsync.ModIOUnity.DeleteTempModSet();
    if (result.Succeeded())
    {
        Debug.Log("Successful");
    }
    else
    {
        Debug.Log("Failed");
    }
}

Adds mods to an existing Temp mod set and downloads/installs them if needed.

Note

Mods that the user is subscribed to will be not be re-downloaded and will remain in the installed mods location for that user.

ModId[] modIds;
void Example()
{
    Result result = await ModIOUnityAsync.AddModToTempModSet(modIds);
    if (result.Succeeded())
    {
        Debug.Log("Successful");
    }
    else
    {
        Debug.Log("Failed");
    }
}

Removes mods from an existing Temporary Mod Set. This removes them from the list in the set but does not uninstall them, that is done when the set is destroyed.

ModId[] modIds;
void Example()
{
    Result result = await ModIOUnityAsync.RemoveModsFromTempModSet(modIds);
    if (result.Succeeded())
    {
        Debug.Log("Successful");
    }
    else
    {
        Debug.Log("Failed");
    }
}

Gets an array of temp mods that are installed on the current device.

Note

These will not be subscribed by the current user. If you wish to get all the current user's installed mods use ModIOUnity.GetSubscribedMods() and check the SubscribedMod.status equals SubscribedModStatus.Installed.

void Example()
{
    InstalledMod[] mods = await ModIOUnityAsync.GetTempSystemInstalledMods(out Result result);
    if (result.Succeeded())
    {
        Debug.Log("found " + mods.Length.ToString() + " temp mods installed");
    }
    else
    {
        Debug.Log("failed to get temp installed mods");
    }
}

Analytics

The mod.io SDK supports analytic features, allowing you to view your players' mod activity.

The following documentation walks you through the setup process and gives example usages.

When you want to start tracking analytic data you can call StartAnalyticsSession(). This function requires a unique sessionId, an array of modIds being used in the session, and the autoStartHeartbeat boolean that represents automation of the analytics heartbeat.

Note

You will need to cache your session id as it will be needed later.

Note

It is recommended that you allow automation of the heartbeat function by passing in true.

string[] modIds;
string sessionId;

void Example()
{
    var r = await ModIOUnityAsync.StartAnalyticsSession(sessionId, modIds, true);
    if (r.result.Succeeded())
    {
        //Store the returned session id to end the session later.
        sessionId = r.value;
        Debug.Log("Successfully sent start analytics session request");
    }
    else
    {
        Debug.Log("Failed to send start analytics session request");
    }
}

If you decide that you want more control over when the heartbeat function is called, you can manually call SendAnalyticsHeartbeat(). This function tells our backend that the session is still active. The only parameter required is the sessionId that you used to start the analytics session.

Note

Each call to SendAnalyticsHeartbeat() should be at least 5 minutes apart.

string sessionId;
 
async void Example()
{
    Result r = await ModIOUnityAsync.SendAnalyticsHeartbeat(sessionId);

    if (r.result.Succeeded())
    {
        Debug.Log("Successfully sent analytics heartbeat request");
    }
    else
    {
        Debug.Log("Failed to send analytics heartbeat request");
    }
}

When you are ready to end the session, you can call EndAnalyticsSession() by passing in the sessionId that you used to start the analytics session.

string sessionId;
async void Example()
{
    Result result = await ModIOUnityAsync.EndAnalyticsSession(sessionId);

    if (result.Succeeded())
    {
        Debug.Log("Successfully sent end analytics request");
    }
    else
    {
        Debug.Log("Failed to send end analytics request");
    }
}

About

Unity Engine Plugin for easily integrating mod.io into your game - the UGC management service for game developers

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages