id | title | sidebar_label | slug | sidebar_position |
---|---|---|---|---|
unity-introduction |
Unity Introduction |
Unity Introduction |
/unity/unity-introduction/ |
0 |
Important
This is a new & improved version of the mod.io Unity plugin! You can find the old version (known as V2) on the "legacy-v2" branch. This version doesn't currently have support for all platforms, you can see in Platform Support when each platform is expected to be available.
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 can be enjoyed by players on PlayStation®4, PlayStation®5, Xbox, Switch, 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 2021.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.
Note
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.
To access console platforms and documentation, see Supporting Console Platforms. If a platform you require isn't available yet, please reach out to us.
Platform | Support |
---|---|
Windows | âś“ |
macOS | Q2 |
Linux | Q2 |
Meta Quest | âś“ |
Xbox One | Q2 |
Xbox Series X | Q2 |
PlayStation® 4 | Q2 |
PlayStation® 5 | Q2 |
Nintendo Switch | Q2 |
iOS | Q2 |
Android | Q2 |
mod.io guarantees full functionality and Long-Term Support of the plugin for the following Unity Versions:
Version | Support |
---|---|
2021.3 | âś“ |
2022.3 | âś“ |
Unity 6 | âś“ |
If you need assistance with first-party approval, or require a private, white-label UGC solution. Contact us!
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!
Warning
If you have a previous version of the plugin installed, it is highly recommended to delete it before updating to a later version.
- 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.
- 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.
- 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/Modio/ThirdParty
.
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
, API key
and API Path
for the following steps.
- Ensure you have installed the plugin using the installation instructions above.
- In Unity, select the mod.io config file by navigating to
Tools -> mod.io -> Edit Settings
. - In the Inspector, enter your
Game Id
,API key
andAPI Path
intoServer URL
.
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.
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 README in /Unity/UI
section for setup and usage instructions. However, we recommend following the guide below to better understand how the plugin works.
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.
Note
The mod.io plugin was built around Asynchronous programming to ensure a smooth experience for the user by never blocking the main thread. For more information on asynchronous concepts, check out the the docs here.
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:
- Create a new
Scene
. - In that scene, create an
Empty Game Object
(name it anything you'd like). - Add the
ModioExample
component to yourGameObject
. - Save the scene.
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 as an async method:
using Modio; // Add this to the top of your class
void Start()
{
InitPlugin();
}
async Task InitPlugin()
{
Error error = await ModioClient.Init();
if (error)
{
Debug.LogError($"Error initializing mod.io: {error}");
return;
}
Debug.Log("mod.io plugin initialized");
}
Important
All plugin configuration should be performed during Unity's Awake
step. Make sure you only ever bind services during Awake
, and only ever initialize the plugin during Start
or later.
It's worth noting the Error
class returned. All mod.io functions you call will provide this Error
class for detailed reporting on each function call. For convenience, Error
is implicitly converted to a boolean, allowing you to simply check if this conversion is true to know if an error occured.
Now, return to your scene in Unity, enter Play mode and you should see the logged success message.
Most of the API’s functionality requires player authentication. The plugin offers a large range of SSO (single-sign on) authentication options, including Steam, Oculus, Xbox, PlayStation 4/5, 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. To do so we need to bind the Email Authentication service so that it's chosen as the auth service for the plugin.
The ModioEmailAuthService
class is provided for convenience. It requires an async task or object implementing IEmailCodePrompter
. This is to tell a UI implementation when the email authentication process is ready to accept the code.
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:
With your UI created, let's add our authentication functionality:
using UnityEngine.UI; // Add these to the top of your class
using Modio.Users;
using Modio.Authentication;
[SerializeField] InputField authInput;
[SerializeField] Button authRequest;
[SerializeField] Button authSubmit;
// Services should be bound in the Awake event.
// Services bound in Start aren't guaranteed to bound in time for initialization.
void Awake()
{
// This enforces email auth to be used, a higher priority can be used if needed
ModioServices.Bind<IModioAuthService>()
.FromInstance(new ModioEmailAuthService(GetAuthCode));
}
// Start method...
async Task InitPlugin()
{
// Initialization ...
OnInit();
}
async Task OnInit()
{
if (User.Current.IsAuthenticated)
{
OnAuth();
return;
}
// You can assign these using the Inspector if you prefer
authRequest.clicked += async () => await Authenticate();
}
async Task Authenticate()
{
Error error = await ModioClient.AuthService.Authenticate(true, authInput.text);
if (error)
{
Debug.LogError($"Error authenticating with email: {error}");
return;
}
OnAuth();
}
// This will be called by the ModioEmailAuthService object we constructed earlier
async Task<string> GetAuthCode()
{
bool codeEntered = false;
authSubmit.onClick.AddListener(() => codeEntered = true);
while (!codeEntered)
await Task.Yield();
return authInput.text;
}
void OnAuth()
{
Debug.Log($"Authenticated user: {User.Current.Profile.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:
- Start Play mode in Unity
- Enter your email address in the input field and press the
authRequest
button - Retrieve the authorization code from your inbox
- Enter the authorization code into the input field and press the
authSubmit
button - 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 authentication service with the highest priority is the same as the one last used by the user to authenticate.
- At the beginning of
OnInit()
, we check to see if we are already authenticated, and if so move straight toOnAuth()
.
If you change the highest priority auth service to another one, then the user won't be automatically logged in. This is to help facilitate both a silent log in and multiple users on the same 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
Alternatively SSO methods with other platforms can be used to authenticate the user with mod.io. Check out our documentation to see a list of all platforms we support SSO with. For this example we're going to use Steam with the Facepunch Steamworks library.
Feel free to come back to this section later! Authentication is agnostic of the rest of this guide's behavior.
Click to expand
[!IMPORTANT]
Before we can implement Single Sign-On, we need to configure Steam SSO for your game on the mod.io website. Please read our documentation on how to do this before continuing with the implementation below.
To perform our Single Sign-On we're going to use Facepunch's Steamworks C# library to authenticate using a Steam account. Similarly to the Email authentication, we need to bind a Facepunch Auth Service:
using Modio.Platforms.Facepunch; // Add this to the top of your class
void Awake()
{
// Email binding...
// By passing in the DeveloperOverride priority with the + 10, this will take precedence over email auth
ModioServices.Bind<IModioAuthService>()
.FromInstance(new ModioFacepunchAuthService(), ModioServicePriority.DeveloperOverride + 10));
}
[!IMPORTANT]
This next section requires theSteamClient
to have been initialized before executing. This is out of scope for this guide, but you can find a convenient example of how to do this in/Unity/Examples/Steam/Facepunch/FacepunchExampl.cs
.
In order to authenticate a user with mod.io, they must agree to the mod.io Terms of Use. This differs from Email authentication as the Terms of Use is built into the email sign-up process, not requiring it in-game. This window requires links to the mod.io Terms of Use & the mod.io Privacy Policy to be valid.
[!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:
Using the above as a template, we'll want to modify the OnInt()
method to display the Terms of Use if the highest priority auth service is Facepunch:
async Task OnInit()
{
// IsAuthenticated check...
if (ModioClient.AuthService is ModioFacepunchAuthService)
{
tosContainer.SetActive(true);
termsLink.onClick.AddListener(() => Application.OpenURL("https://mod.io/terms"));
privacyLink.onClick.AddListener(() => Application.OpenURL("https://mod.io/privacy"));
acceptButton.onClick.AddListener(() => Authenticate());
denyButton.onClick.AddListener(() => tosContainer.SetActive(false));
return;
}
// Attach authRequest click listener...
}
Lastly, we need to add a compiler directive to your project settings in order for the Facepunch library to compile. In your Project Settings, under Player and the platform you're building for, add UNITY_FACEPUNCH
to the Scripting Define Symbols
:
And that should be it! Log into Steam, accept the Terms of Use and you should see your Steam account authenticated with mod.io! If you've initialized your Steam client with the correct AppId then the mod.io plugin will automatically detect the currently logged in user and authenticate using that user.
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 these to the top of your class
using Modio.Mods;
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 ModSearchFilters and GetMods
// properly later on.
(Error error, ModioPage<Mod> page) = await Mod.GetMods(new ModSearchFilter());
if (error)
{
Debug.LogError($"Error getting mods: {error}");
return;
}
if (page.Data.Length != 0)
{
Debug.Log($"{page.Data.Length} mods found. Not adding mods");
return;
}
// TODO: Keep reading the Getting Started guide
}
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 is a relatively simple two-step process:
- In order to upload a new mod, we need to prepare all the data with a Mod Builder. This builder stores and allows you to mutate all the data related to a mod, including its files.
- We then call
Publish()
on the builder to push all changes to the mod.io API.
Let's add a method that handles both steps:
async Task UploadMod(string name, string summary, Texture2D logo, string path)
{
Debug.Log($"Starting upload: {name}");
var builder = Mod.Create();
builder.SetName(name)
.SetSummary(summary)
.SetLogo(logo.GetRawTextureData())
.EditModfile()
.SetSourceDirectoryPath(path)
.FinishModfile();
(Error error, Mod mod) = await builder.Publish();
if (error)
{
Debug.LogError($"Error uploading mod {name}: {error}");
return;
}
else
{
Debug.Log($"Successfully created mod {mod.Name} with Id {mod.Id}");
}
}
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 uploading 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 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<Mod[]> GetAllMods()
{
(Error error, ModioPage<Mod> page) = await Mod.GetMods(new ModSearchFilter());
if (error)
{
Debug.LogError($"Error getting mods: {error}");
return Array.Empty<Mod>();
}
return page.Data;
}
Important
A Mod is a read-only mutable representation of the state of a mod. It will be updated by the mod.io plugin whenever new data for that mod is received. Each mod can be listened to for any changes so UI elements can be updated accordingly. In order to edit a mod, call Edit
on the mod instance you want to modify, which will provide a ModBuilder
class in edit mode.
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})"))}");
}
Note
We write all1 because while using the default Mod Search Filter settings will return all of your mods, this is only because you don't have many. This brings us to Mod Search Filters.
The maximum number of results returned can be set in the Mod 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 ModSearchFilter();
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 ModSearchFilter(0, 10); // Will return results 1-10
new ModSearchFilter(1, 10); // Will return results 11-20
new ModSearchFilter(2, 10); // Will return results 21-30
Note
Mod 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.
If you're looking for more granular filtering, check out the ModioAPI.Mods.GetModsFilter
class.
More specifically: downloading a Mod'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!
[SerializeField] Text randomName;
[SerializeField] Image randomLogo;
async void SetRandomMod()
{
Mod mod = allMods[UnityEngine.Random.Range(0, allMods.Length - 1)];
randomName.text = mod.name;
(Error error, Texture2D texture) = await mod.Logo.DownloadAsTexture2D(Mod.LogoResolution.X320_Y180);
if (error)
{
Debug.LogError($"Error downloading {mod.Name}'s logo: {error}");
return;
}
randomLogo.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.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, 320x180
. However, Mods have a number of sizes for each image as defined in Mod.LogoResolution
& Mod.GalleryResolution
. See the documentation (or use code completion in your IDE) to view available options.
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 when a User authenticates, their subscriptions will be automatically synced with the mod.io plugin. All mods retrieved from the mod.io API will be cached and updated with the most recent data, with the User's subscriptions updated and stored in User.Current.ModRepository
.
This makes getting Subscribed mods incredibly convenient as no requests need to be made:
static Mod[] GetSubscribedMods() => User.Current.ModRepository.GetSubscribed().ToArray();
Getting the user's subscribed mods first requires they be authenticated. This will be handled automatically when a User authenticates with mod.io. In order to know when everything has finished syncing, we'll want to wait until User.Current.IsUpdating
comes back as false:
async void OnAuth()
{
// ...
while (User.Current.IsUpdating)
await Task.Yield();
Mod[] subscribedMods = GetSubscribedMods();
Debug.Log($"Subscribed mods:\n{(subscribedMods.Length > 0 ? string.Join("\n", subscribedMods.Select(mod => $"{mod.Name} (id: {mod.Id})")) : "None")}");
}
Important
Sync()
will typically only be called automatically by the plugin on authentication. If you make any changes using the web interface, they won't be reflected in the mod.io plugin until this Sync()
runs. This method is available to you under User.Current.Sync()
. However, this is an expensive method and it's recommended it be called as little as possible.
Subscribing to mods is very simple. We call the async method mod.Subscribe()
. This method already contains checks for if you're already subscribed, short-cutting to the success result if so:
async Task SubscribeToMod(Mod mod)
{
var error = await mod.Subscribe();
if (error)
{
Debug.LogError($"Error subscribing to {mod.Name}: {error}");
return;
}
Debug.Log($"Subscribed to mod: {mod.Name}");
}
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 Sync()
needs to be run to synchronise the local state.
To test it out, in your OnAuth()
method, after we log all subscribed mods add the following line to subscribe to a random mod:
async void OnAuth()
{
// ...
await SubscribeToMod(allMods[UnityEngine.Random.Range(0, allMods.Length - 1)]);
}
Enter Play mode and you should see one of the generated mods appear in your "Subscribed mods" log!
Now, the moment we've all been waiting for. Downloading, installing, updating, and deleting mods are all handled automatically by the plugin. This behaviour can be disabled using your ModioSettings
we configured in Set Up.
In the following code we'll presume this has been disabled, so we can show how to activate & deactivate it at will. We'll also leverage ModInstallationManagement.MangementEvents
to see the download progress:
Mod currentDownload;
float downloadProgress;
float timeToProgressCheck = 1f;
void WakeUpModManagement()
{
void HandleModManagementEvent(
Mod mod,
Modfile modfile,
ModInstallationManagement.OperationType jobType,
ModInstallationManagement.OperationPhase jobPhase
){
Debug.Log($"{jobType} {jobPhase}: {mod.Name}");
switch (jobPhase)
{
case ModInstallationManagement.OperationPhase.Started
when jobType is not ModInstallationManagement.OperationType.Uninstall:
currentDownload = mod;
break;
case ModInstallationManagement.OperationPhase.Cancelled:
case ModInstallationManagement.OperationPhase.Failed:
currentDownload = null;
break;
case ModInstallationManagement.OperationPhase.Completed
when jobType is not ModInstallationManagement.OperationType.Uninstall:
Debug.Log($"Mod {mod.Name} installed at {mod.File.InstallLocation}");
currentDownload = null;
break;
case ModInstallationManagement.OperationPhase.Completed:
Debug.Log($"Mod {mod.Name} uninstalled");
break;
}
ModInstallationManagement.ManagementEvents += HandleModManagementEvent;
}
void Update()
{
if (currentDownload == null)
return;
timeToProgressCheck -= Time.deltaTime;
if (timeToProgressCheck > 0)
return;
Debug.Log($"Downloading {currentDownload.Name}: [{Mathf.RoundToInt(currentDownload.File.FileStateProgress * 100)}%]");
timeToProgressCheck += 1f;
}
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.
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: mod.File.InstallLocation
. Using this is as simple as expected, in the previous method lets move OperationPhase.Completed
to its own case and print the install location to the console:
case ModInstallationManagement.OperationPhase.Completed:
Debug.Log($"Mod {mod.Name} installed at {mod.File.InstallLocation}");
break;
In order to get a list of all installed mods on the file system, we simply call ModInstallationManagement.GetAllInstalledMods()
:
async void OnAuth()
{
// ...
var installedMods = ModInstallationManagement.GetAllInstalledMods();
Debug.Log($"Installed mods:\n{(installedMods.Count > 0 ? string.Join("\n", installedMods.Select(mod => $"{mod.Name} (id: {mod.Id})")) : "None")}");
}
We're currently logging each installed mod and the path to its files (mod.File.InstallLocation
). 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.
One last thing to note, this will provide mods installed by any user on the file system. This is to facilitate offline mod management so the user doesn't ever get stuck if they're offline. Use installedMod.IsSubscribed()
to determine if the mod is subscribed to by the current authenticated user.
And 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.
Note
You can also find the following class (along with an example scene) in Assets/Plugins/Modio/Unity/Example
.
Click to expand
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Modio;
using Modio.Authentication;
using Modio.Mods;
using Modio.Unity;
using Modio.Users;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Random = System.Random;
public class ModioUnityExample : MonoBehaviour
{
// 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();
[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
Mod[] allMods;
// Installing mods
Mod currentDownload;
float downloadProgress;
float timeToProgressCheck = 1f;
void Awake()
{
// This enforces email auth to be used, a higher priority can be used if needed
ModioServices.Bind<IModioAuthPlatform>()
.FromInstance(new ModioEmailAuthPlatform(GetAuthCode));
randomContainer.SetActive(false);
}
#region Initialization
void Start()
{
InitPlugin();
}
async Task InitPlugin()
{
Error error = await ModioClient.Init();
if (error)
{
Debug.LogError($"Error initializing mod.io: {error}");
return;
}
Debug.Log("mod.io plugin initialized");
OnInit();
}
void OnInit()
{
if (User.Current.IsAuthenticated)
{
OnAuth();
return;
}
// You can assign these using the Inspector if you prefer
authRequest.onClick.AddListener(() => Authenticate());
}
#endregion
#region Authentication
async Task Authenticate()
{
Error error = await ModioClient.AuthPlatform.Authenticate(true, authInput.text);
if (error)
{
Debug.LogError($"Error authenticating with email: {error}");
return;
}
OnAuth();
}
async Task<string> GetAuthCode()
{
bool codeEntered = false;
authSubmit.onClick.AddListener(() => codeEntered = true);
while (!codeEntered)
await Task.Yield();
return authInput.text;
}
async void OnAuth()
{
Debug.Log($"Authenticated user: {User.Current.Profile.Username}");
await AddModsIfNone();
allMods = await GetAllMods();
Debug.Log($"Available mods:\n{string.Join("\n", allMods.Select(mod => $"{mod.Name} (id: {mod.Id})"))}");
randomButton.onClick.AddListener(SetRandomMod);
randomContainer.SetActive(true);
SetRandomMod();
while (User.Current.IsUpdating)
await Task.Yield();
Mod[] subscribedMods = GetSubscribedMods();
Debug.Log($"Subscribed mods:\n{(subscribedMods.Length > 0 ? string.Join("\n", subscribedMods.Select(mod => $"{mod.Name} (id: {mod.Id})")) : "None")}");
await SubscribeToMod(allMods[UnityEngine.Random.Range(0, allMods.Length - 1)]);
WakeUpModManagement();
}
#endregion
#region Uploading Mods
async Task AddModsIfNone()
{
(Error error, ModioPage<Mod> page) = await Mod.GetMods(new ModSearchFilter());
if (error)
{
Debug.LogError($"Error getting mods: {error}");
return;
}
if (page.Data.Length != 0)
{
Debug.Log($"{page.Data.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); // Uncomment if you generated dummy mods
}
}
async Task UploadMod(string modName, string summary, Texture2D logo, string path)
{
Debug.Log($"Starting upload: {modName}");
var builder = Mod.Create();
builder.SetName(modName)
.SetSummary(summary)
.SetLogo(logo.GetRawTextureData())
.EditModfile()
.SetSourceDirectoryPath(path)
.FinishModfile();
(Error error, Mod mod) = await builder.Publish();
if (error)
{
Debug.LogError($"Error uploading mod {modName}: {error}");
return;
}
Debug.Log($"Successfully created mod {mod.Name} with Id {mod.Id}");
}
#endregion
#region Searching for Mods
async Task<Mod[]> GetAllMods()
{
(Error error, ModioPage<Mod> page) = await Mod.GetMods(new ModSearchFilter());
if (error)
{
Debug.LogError($"Error getting mods: {error}");
return Array.Empty<Mod>();
}
return page.Data;
}
#endregion
#region Downloading Images
async void SetRandomMod()
{
Mod mod = allMods[UnityEngine.Random.Range(0, allMods.Length - 1)];
randomName.text = mod.Name;
(Error error, Texture2D texture) = await mod.Logo.DownloadAsTexture2D(Mod.LogoResolution.X320_Y180);
if (error)
{
Debug.LogError($"Error downloading {mod.Name}'s logo: {error}");
return;
}
randomLogo.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
}
#endregion
#region Subscribing to Mods
static Mod[] GetSubscribedMods() => User.Current.ModRepository.GetSubscribed().ToArray();
async Task SubscribeToMod(Mod mod)
{
var error = await mod.Subscribe();
if (error)
{
Debug.LogError($"Error subscribing to {mod.Name}: {error}");
return;
}
Debug.Log($"Subscribed to mod: {mod.Name}");
}
#endregion
#region Installing Mods
void WakeUpModManagement()
{
void HandleModManagementEvent(
Mod mod,
Modfile modfile,
ModInstallationManagement.OperationType jobType,
ModInstallationManagement.OperationPhase jobPhase
){
Debug.Log($"{jobType} {jobPhase}: {mod.Name}");
switch (jobPhase)
{
case ModInstallationManagement.OperationPhase.Started
when jobType is not ModInstallationManagement.OperationType.Uninstall:
currentDownload = mod;
break;
case ModInstallationManagement.OperationPhase.Cancelled:
case ModInstallationManagement.OperationPhase.Failed:
currentDownload = null;
break;
case ModInstallationManagement.OperationPhase.Completed
when jobType is not ModInstallationManagement.OperationType.Uninstall:
Debug.Log($"Mod {mod.Name} installed at {mod.File.InstallLocation}");
currentDownload = null;
break;
case ModInstallationManagement.OperationPhase.Completed:
Debug.Log($"Mod {mod.Name} uninstalled");
break;
}
}
ModInstallationManagement.ManagementEvents += HandleModManagementEvent;
}
void Update()
{
if (currentDownload == null)
return;
timeToProgressCheck -= Time.deltaTime;
if (timeToProgressCheck > 0)
return;
Debug.Log($"Downloading {currentDownload.Name}: [{Mathf.RoundToInt(currentDownload.File.FileStateProgress * 100)}%]");
timeToProgressCheck += 1f;
}
#endregion
#region Generate Dummy Mods
async Task<DummyModData> GenerateDummyMod(
string dummyName,
string summary,
string backgroundColor,
string textColor,
int megabytes
)
{
Debug.Log($"Writing temporary mod file: {dummyName}");
string path = Path.Combine(Application.dataPath, $"../_temp_dummy_mods/{dummyName}");
Directory.CreateDirectory(path);
using (FileStream fs = File.OpenWrite(Path.Combine(path, $"{dummyName}.dummy")))
{
for (int i = 0; i < megabytes; i++)
{
RandomBytes.NextBytes(Megabyte);
await fs.WriteAsync(Megabyte, 0, Megabyte.Length);
}
}
return new DummyModData(
dummyName,
summary,
await GenerateLogo(dummyName.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;
}
}
#endregion
}