{"id":44160425,"url":"https://github.com/modio/modio-unity","last_synced_at":"2026-04-17T02:00:43.041Z","repository":{"id":44725700,"uuid":"473799680","full_name":"modio/modio-unity","owner":"modio","description":"Unity Engine Plugin for easily integrating mod.io into your game - the UGC management service for game developers","archived":false,"fork":false,"pushed_at":"2026-02-09T05:12:14.000Z","size":504596,"stargazers_count":51,"open_issues_count":5,"forks_count":8,"subscribers_count":9,"default_branch":"main","last_synced_at":"2026-02-09T11:38:46.372Z","etag":null,"topics":["csharp","modding","modio","unity","unity3d"],"latest_commit_sha":null,"homepage":"https://mod.io","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/modio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-03-24T23:02:14.000Z","updated_at":"2026-02-09T05:06:48.000Z","dependencies_parsed_at":"2024-07-31T09:05:01.808Z","dependency_job_id":"729af089-6892-484d-8ce6-c0807e901c79","html_url":"https://github.com/modio/modio-unity","commit_stats":null,"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"purl":"pkg:github/modio/modio-unity","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modio%2Fmodio-unity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modio%2Fmodio-unity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modio%2Fmodio-unity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modio%2Fmodio-unity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/modio","download_url":"https://codeload.github.com/modio/modio-unity/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/modio%2Fmodio-unity/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31911846,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-16T18:22:33.417Z","status":"online","status_checked_at":"2026-04-17T02:00:06.879Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","modding","modio","unity","unity3d"],"created_at":"2026-02-09T07:29:58.061Z","updated_at":"2026-04-17T02:00:43.034Z","avatar_url":"https://github.com/modio.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\nid: unity-introduction\ntitle: Unity Introduction\nslug: /unity/unity-introduction\n---\n\n\u003ca href=\"https://mod.io\"\u003e\u003cimg src=\"https://mod.io/images/branding/modio-logo-bluewhite.svg\" alt=\"mod.io\" width=\"360\" align=\"right\"/\u003e\u003c/a\u003e\n# mod.io Unity Plugin v2026.4\n[![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/modio/modio-unity/blob/master/Documentation/LICENSE.md)\n[![Discord](https://img.shields.io/discord/389039439487434752.svg?label=Discord\u0026logo=discord\u0026color=7289DA\u0026labelColor=2C2F33)](https://discord.mod.io)\n[![Master docs](https://img.shields.io/badge/docs-master-green.svg)](https://docs.mod.io/unity/)\n[![Unity 3D](https://img.shields.io/badge/Unity-2021.3+-lightgrey.svg)](https://unity3d.com)\n\n\u003e [!IMPORTANT]  \n\u003e This is a new \u0026amp; improved version of the mod.io Unity plugin! You can find the old version (known as V2) on the [legacy-v2](https://github.com/modio/modio-unity/tree/legacy/v2) branch. This version doesn't currently have support for all platforms, you can see in [Platform Support](#platform-support) when each platform is expected to be available.\n\nWelcome to the mod.io Unity Engine plugin [repository](https://github.com/modio/modio-unity)!\n\nmod.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*: \n\n- Completely cross-platform: content uploaded can be enjoyed by players on PlayStation®4, PlayStation®5, Xbox, Switch, VR and mobile\n- One-click mod installs, synced to a single mod.io account across all platforms\n- 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\n- Founded by the [ModDB.com](https://www.moddb.com) team, with over two decades of experience in the UGC space\n- Constantly evolving - we continue to work alongside our partners to iterate and improve our plugin support\n\nThe 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.\n\n\u003e [!NOTE]  \n\u003e A custom-built [ready-made UI](#quick-start) for mod discovery is included, along with installation and collection management, and a full-featured [C# interface](#getting-started) which connects to the [mod.io REST API](https://docs.mod.io).\n\u003e\n\u003e![Auth Form Example](Unity/Examples/Images/template_ui_example.png)\n\n## Platform Support\nTo access console platforms and documentation, see [Supporting Console Platforms](https://docs.mod.io/platforms/console). If a platform you require isn't available yet, please reach out to us.\n\n| Platform          | Support |\n|-------------------|:-------:|\n| Windows           |    ✓    |\n| macOS             |    ✓    |\n| Linux             |    ✓    |\n| Meta Quest        |    ✓    |\n| Xbox One          |    ✓    |\n| Xbox Series X     |    ✓    |\n| PlayStation® 4    |    ✓    |\n| PlayStation® 5    |    ✓    |\n| Nintendo Switch   |    ✓    |\n| iOS               |    ✓    |\n| Android           |    ✓    |\n\n## Unity Version Support\nmod.io guarantees full functionality and Long-Term Support of the plugin for the following Unity Versions:\n\n| Version | Support |\n|---------|:-------:|\n| 2021.3  |    ✓    |\n| 2022.3  |    ✓    |\n| Unity 6 |    ✓    |\n\n## Game Studios and Publishers\nIf you need assistance with first-party approval, or require a private, white-label UGC solution. [Contact us](mailto:developers@mod.io)!\n\n## Contributions Welcome\nOur 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.  \n\nWant to make changes to our plugin? Submit a pull request, and we'll review your recommended changes! Our goal at [mod.io](https://mod.io) is an [open modding API](https://docs.mod.io), and you're encouraged to view, fork and contribute to [all of our codebases](https://github.com/modio)!\n\n## Installation\n\n\u003e [!WARNING]  \n\u003e If you have a previous version of the plugin installed, it is _highly_ recommended to delete it before updating to a later version.\n\n1. Install the *Newtonsoft Json* plugin using the Package Manager.\n   - If your Unity Package Manager does not contain Newtonsoft Json, follow the instructions [here](https://github.com/applejag/Newtonsoft.Json-for-Unity/wiki/Install-official-via-UPM#installing-the-package-via-upm-window) to find the installation method for your Unity version.\n2. Download and install the plugin using one of the following methods:\n   - Using the [Unity Asset Store](https://assetstore.unity.com/packages/tools/integration/mod-browser-manager-by-mod-io-138866) and Package Manager.\n   - Download the `.unitypackage` directly from the [Releases page](https://github.com/modio/modio-unity/releases).\n   - Download an archive of the code using GitHub's download feature, and unpack it in your project's `Assets/Plugins` directory.\n3. Restart Unity, to ensure it recognises the new assembly definitions.\n\n\u003e [!NOTE]  \n\u003e If you receive errors due to conflicting libraries after installing the plugin, remove any duplicates from `Assets/Plugins/Modio/ThirdParty`.\n\n## Setup\n\nThe first thing you'll need to do is [create a game profile](https://mod.io/g/add) on mod.io (or our [private test environment](https://test.mod.io/g/add)).\n\n\u003e [!IMPORTANT]  \n\u003e You'll need your `Game Id`, `API key` and `API Path` for the following steps.\n\n1. Ensure you have installed the plugin using the [installation instructions](#installation) above.\n2. In Unity, select the mod.io *config file* by navigating to `Tools -\u003e mod.io -\u003e Edit Settings`.\n3. In the Inspector, enter your `Game Id`, `API key` and `API Path` into `Server URL`.\n\nYour setup is now complete. The following sections will guide you through getting your mod.io integration up and running quickly.\n\nIf you have any questions or need some help join our [Discord](https://discord.mod.io) server.\n\n## Quick Start\n\nThe mod.io Unity Engine plugin comes with a prebuilt UI, a drop-in, instant solution for browsing and installing your game's mods.\n\nIf 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.\n\n## Getting Started\n\nIn 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](#complete-class).\n\n\u003e [!NOTE]  \n\u003e 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](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/).\n\n### Initial Setup\n\nFirst, let's create a new `MonoBehaviour` called `ModioExample.cs` that will contain all of our example functionality:\n\n```csharp\nusing UnityEngine;\n\npublic class ModioExample : MonoBehaviour\n{\n    // TODO: Keep reading the Getting Started guide\n}\n```\n\nOnce you've created the above class:\n\n1. Create a new `Scene`.\n2. In that scene, create an `Empty Game Object` (name it anything you'd like).\n3. Add the `ModioExample` component to your `GameObject`.\n4. Save the scene.\n\n### Initialization\n\n\u003e [!IMPORTANT]  \n\u003e The plugin relies on the *config file* that is configured during the [setup instructions](#setup) above. Please ensure you have completed all of those steps before proceeding.\n\nBefore 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:\n\n```csharp\nusing Modio; // Add this to the top of your class\n\nvoid Start()\n{\n    InitPlugin();\n}\n\nasync Task InitPlugin()\n{\n    Error error = await ModioClient.Init();\n    \n    if (error)\n    {\n        Debug.LogError($\"Error initializing mod.io: {error}\");\n        return;\n    }\n\n    Debug.Log(\"mod.io plugin initialized\");\n}\n```\n\n\u003e [!IMPORTANT]  \n\u003e 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.\n\nIt'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.\n\nNow, return to your scene in Unity, enter Play mode and you should see the logged success message.\n\n## Authentication\n\nMost 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.\n\nFor 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.\n\nThe `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.\n\n\u003e [!NOTE]  \n\u003e 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:\n\u003e\n\u003e ![Auth Form Example](Unity/Examples/Images/auth_form_example.png)\n\nWith your UI created, let's add our authentication functionality: \n\n```csharp\nusing UnityEngine.UI; // Add these to the top of your class\nusing Modio.Users;\nusing Modio.Authentication;\n\n[SerializeField] InputField authInput;\n[SerializeField] Button authRequest;\n[SerializeField] Button authSubmit;\n\n// Services should be bound in the Awake event. \n// Services bound in Start aren't guaranteed to bound in time for initialization.\nvoid Awake()\n{\n    // This enforces email auth to be used, a higher priority can be used if needed\n    ModioServices.Bind\u003cIModioAuthService\u003e()\n                      .FromInstance(new ModioEmailAuthService(GetAuthCode));\n}\n\n// Start method...\n\nasync Task InitPlugin()\n{\n    // Initialization ...\n\n    OnInit();\n}\n\nasync Task OnInit()\n{\n    if (User.Current.IsAuthenticated)\n    {\n        OnAuth();\n        return;\n    }\n    \n    // You can assign these using the Inspector if you prefer\n    authRequest.clicked += async () =\u003e await Authenticate();\n}\n   \nasync Task Authenticate()\n{\n    Error error = await ModioClient.AuthService.Authenticate(true, authInput.text);\n    \n    if (error)\n    {\n        Debug.LogError($\"Error authenticating with email: {error}\");\n        return;\n    }\n    \n    OnAuth();\n}\n\n// This will be called by the ModioEmailAuthService object we constructed earlier\nasync Task\u003cstring\u003e GetAuthCode()\n{\n    bool codeEntered = false;\n    \n    authSubmit.onClick.AddListener(() =\u003e codeEntered = true);\n    \n    while (!codeEntered)\n        await Task.Yield();\n    \n    return authInput.text;\n}\n   \nvoid OnAuth()\n{\n    Debug.Log($\"Authenticated user: {User.Current.Profile.Username}\");\n}\n```\n\n\u003e [!IMPORTANT]  \n\u003e Don't forget to assign the fields in the Inspector!\n\nIf you've implemented the above correctly, you should now be able to:\n\n1. Start Play mode in Unity\n2. Enter your email address in the input field and press the `authRequest` button\n3. Retrieve the authorization code from your inbox\n4. Enter the authorization code into the input field and press the `authSubmit` button\n5. See the logged authentication message\n\n\u003e [!NOTE]  \n\u003e If there is no mod.io account associated with the provided email address, one will automatically be created.\n\nThere 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:\n\n- The authentication service with the highest priority is the same as the one last used by the user to authenticate.\n- At the beginning of `OnInit()`, we check to see if we are already authenticated, and if so move straight to `OnAuth()`.\n\nIf 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.\n\n\u003e [!NOTE]  \n\u003e If your email provider supports it, you can use plus-addressing to test multiple users with a single email address:\n\u003e ```\n\u003e john.smith+test1@gmail.com\n\u003e john.smith+test2@gmail.com\n\u003e john.smith+test3@gmail.com\n\u003e ```\n\n### Steam Single Sign-On\n\nAlternatively SSO methods with other platforms can be used to authenticate the user with mod.io. Check out our [documentation](https://docs.mod.io/restapi/introduction#authentication) 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](https://wiki.facepunch.com/steamworks/).\n\nFeel free to come back to this section later! Authentication is agnostic of the rest of this guide's behavior.\n\n\u003cdetails\u003e\n\n\u003csummary\u003e\u003ci\u003eClick to expand\u003c/i\u003e\u003c/summary\u003e\n\n\u003e [!IMPORTANT]   \n\u003e 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](https://docs.mod.io/platforms/steam/authentication) on how to do this before continuing with the implementation below.\n\u003e\n\nTo 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:\n\n```csharp\nusing Modio.Platforms.Facepunch; // Add this to the top of your class\n\nvoid Awake()\n{\n    // Email binding...\n        \n    // By passing in the DeveloperOverride priority with the + 10, this will take precedence over email auth\n    ModioServices.Bind\u003cIModioAuthService\u003e()\n                 .FromInstance(new ModioFacepunchAuthService(), ModioServicePriority.DeveloperOverride + 10));    \n}\n```\n\n\u003e [!IMPORTANT]  \n\u003e This next section requires the `SteamClient` 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`.\n\n### Terms of Use\n\nIn 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 \u0026amp; the mod.io Privacy Policy to be valid.\n\n\u003e [!NOTE]  \n\u003e 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:\n\u003e\n\u003e ![Auth Form Example](Unity/Examples/Images/terms_of_use_example.png)\n\nUsing 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:\n\n```csharp\nasync Task OnInit()\n{\n    // IsAuthenticated check...\n    \n    if (ModioClient.AuthService is ModioFacepunchAuthService)\n    {\n        tosContainer.SetActive(true);\n        \n        termsLink.onClick.AddListener(() =\u003e Application.OpenURL(\"https://mod.io/terms\"));\n        privacyLink.onClick.AddListener(() =\u003e Application.OpenURL(\"https://mod.io/privacy\"));\n        \n        acceptButton.onClick.AddListener(() =\u003e Authenticate());\n        denyButton.onClick.AddListener(() =\u003e tosContainer.SetActive(false));\n        \n        return;\n    }\n    \n    // Attach authRequest click listener...\n}\n```\n\nLastly, 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`:\n\n![Unity Scripting Symbols](Unity/Examples/Images/precompile_directive.png)\n\nAnd 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.\n\u003c/details\u003e\n\n## Adding Mods\n\n\u003e [!NOTE]  \n\u003e Among a range of other functionality, players can use the mod.io website for creating, modifying, and removing mods for your game.\n\u003e\n\u003e 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.\n\nBefore 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:\n\n```csharp\nusing System.Threading.Tasks; // Add these to the top of your class\nusing Modio.Mods;\n\nvoid OnAuth()\n{\n    // Authenticated ...\n    \n    await AddModsIfNone();\n}\n\nasync Task AddModsIfNone()\n{\n    // This section ensures we only upload our mods once. Don't worry too much\n    // about the specifics for now, we will introduce ModSearchFilters and GetMods \n    // properly later on.\n    (Error error, ModioPage\u003cMod\u003e page) = await Mod.GetMods(new ModSearchFilter());\n    if (error)\n    {\n        Debug.LogError($\"Error getting mods: {error}\");\n        return;\n    }\n\n    if (page.Data.Length != 0)\n    {\n        Debug.Log($\"{page.Data.Length} mods found. Not adding mods\");\n        return;\n    }\n\n    // TODO: Keep reading the Getting Started guide\n}\n```\n\n### Generating Dummy Mods\n\n\u003e [!NOTE]  \n\u003e 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](#uploading-mods) section.\n\n\u003cdetails\u003e\n\n\u003csummary\u003e\u003ci\u003eClick to expand\u003c/i\u003e\u003c/summary\u003e\n\u003cbr\u003e\nLet's generate a few dummy mods for you to use for testing. At a minimum, a mod requires the following:\n\n- A name\n- A summary\n- A logo (image file with a minimum resolution of 512x288)\n- At least one file\n\nWe'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:\n\n\u003e [!NOTE]  \n\u003e 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](#uploading-mods) section!\n\n\u003e [!WARNING]  \n\u003e 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.\n\n```csharp\nusing System.IO; // Add these to the top of your class\nusing UnityEngine.Networking;\n\n// Reusing a single byte-array is a small memory-conscious\n// optimization for when we are generating our dummy files.\nstatic readonly byte[] Megabyte = new byte[1024 * 1024];\nstatic readonly Random RandomBytes = new Random();\n\nasync Task AddModsIfNone()\n{\n    // Return if any mods exist ...\n    \n    DummyModData[] mods =\n    {\n        await GenerateDummyMod(\"Cool Weapon\", \"A really cool weapon.\", \"24466B\", \"FDA576\", 10),\n        await GenerateDummyMod(\"Funny Sound Pack\", \"You'll laugh a lot using this.\", \"B85675\", \"633E63\", 50),\n        await GenerateDummyMod(\"Klingon Language Pack\", \"tlhIngan Hol Dajatlh'a'?\", \"93681C\", \"FFEAD0\", 1),\n        await GenerateDummyMod(\"Ten New Missions\", \"Ported from the sequel to the prequel!\", \"FDA576\", \"D45B7A\", 99),\n    };\n}\n\nasync Task\u003cDummyModData\u003e GenerateDummyMod(string name, string summary, string backgroundColor, string textColor, int megabytes)\n{\n    Debug.Log($\"Writing temporary mod file: {name}\");\n\n    string path = Path.Combine(Application.dataPath, $\"../_temp_dummy_mods/{name}\");\n    Directory.CreateDirectory(path);\n\n    using (FileStream fs = File.OpenWrite(Path.Combine(path, $\"{name}.dummy\")))\n    {\n        for (int i = 0; i \u003c megabytes; i++)\n        {\n            RandomBytes.NextBytes(Megabyte);\n            await fs.WriteAsync(Megabyte, 0, Megabyte.Length);\n        }\n    }\n\n    return new DummyModData(\n        name,\n        summary,\n        await GenerateLogo(name.Replace(' ', '+'), backgroundColor, textColor),\n        path\n    );\n}\n\n// Uses a third-party API to generate a logo for each\n// mod, adding some variety when we display them later\nasync Task\u003cTexture2D\u003e GenerateLogo(string text, string backgroundColor, string textColor)\n{\n    UnityWebRequest request = UnityWebRequestTexture.GetTexture($\"https://placehold.co/512x288/{backgroundColor}/{textColor}.png?text={text}\");\n    request.SendWebRequest();\n\n    while (!request.isDone)\n        await Task.Yield();\n\n    if (request.result != UnityWebRequest.Result.Success)\n    {\n        Debug.LogError($\"GenerateLogo failed: {request.error}\");\n\n        return null;\n    }\n\n    return DownloadHandlerTexture.GetContent(request);\n}\n\nreadonly struct DummyModData\n{\n    public readonly string name;\n    public readonly string summary;\n    public readonly Texture2D logo;\n    public readonly string path;\n\n    public DummyModData(string name, string summary, Texture2D logo, string path)\n    {\n        this.name = name;\n        this.summary = summary;\n        this.logo = logo;\n        this.path = path;\n    }\n}\n```\n\n\u003c/details\u003e\n\n### Uploading Mods\n\nUploading mods is a relatively simple two-step process:\n\n1. 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.\n2. We then call `Publish()` on the builder to push all changes to the mod.io API.\n\nLet's add a method that handles both steps:\n\n```csharp\nasync Task UploadMod(string name, string summary, Texture2D logo, string path)\n{\n    Debug.Log($\"Starting upload: {name}\");\n    \n    var builder = Mod.Create();\n    \n    builder.SetName(name)\n           .SetSummary(summary)\n           .SetLogo(logo.GetRawTextureData())\n               .EditModfile()\n               .SetSourceDirectoryPath(path)\n               .FinishModfile();\n    \n    (Error error, Mod mod) = await builder.Publish();\n    \n    if (error)\n    {\n        Debug.LogError($\"Error uploading mod {name}: {error}\");\n        return;\n    }\n    else\n    {\n        Debug.Log($\"Successfully created mod {mod.Name} with Id {mod.Id}\");\n    }\n}\n```\n\nAll 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:\n\n\u003e [!IMPORTANT]  \n\u003e If you didn't generate dummy mods in the previous section, modify the below to suit your mod files.\n\n```csharp\nasync Task AddModsIfNone()\n{\n    // Return if any mods exist ...\n    \n    DummyModData[] mods =\n    {\n        // ...\n    };\n    \n    foreach (DummyModData mod in mods)\n    {\n        await UploadMod(mod.name, mod.summary, mod.logo, mod.path);\n        // Directory.Delete(mod.path, true); // Uncomment if you generated dummy mods\n    }\n}\n```\n\n\u003e [!NOTE]  \n\u003e When uploading a mod, the plugin expects a directory (for each mod) that it will compress before uploading. You do \u003cb\u003e\u003cu\u003enot\u003c/u\u003e\u003c/b\u003e need to zip your files before uploading.\n\nThat’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.\n\n## Searching for Mods\n\nSearching for mods is a simple task — we've actually seen it already in the [adding mods](#adding-mods) section. To get a list of all\u003csup\u003e1\u003c/sup\u003e available mods you can use the `GetMods` method:\n\n```csharp\nusing System; // Add this to the top of your class\n\nasync Task\u003cMod[]\u003e GetAllMods()\n{\n    (Error error, ModioPage\u003cMod\u003e page) = await Mod.GetMods(new ModSearchFilter());\n    if (error)\n    {\n        Debug.LogError($\"Error getting mods: {error}\");\n        return Array.Empty\u003cMod\u003e();\n    }\n\n    return page.Data;\n}\n```\n\n\u003e [!IMPORTANT]  \n\u003e 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.\n\nIf we add the above to our example class, and then head back up to our `OnAuth` method we can quickly log a list of all\u003csup\u003e1\u003c/sup\u003e of our available mods:\n\n```csharp\nusing System.Linq; // Add this to the top of your class\n\nModProfile[] allMods;\n\nasync void OnAuth()\n{\n    // ...\n    \n    allMods = await GetAllMods();\n    Debug.Log($\"Available mods:\\n{string.Join(\"\\n\", allMods.Select(mod =\u003e $\"{mod.Name} (id: {mod.Id})\"))}\");\n}\n```\n\n\u003e [!NOTE]\n\u003e We write *all\u003csup\u003e1\u003c/sup\u003e* 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*.\n\n### Mod Search Filters\n\nThe 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.\n\nThis is fairly simple in practice and is explained best with the following snippet:\n\n```csharp\nvar searchFilter = new ModSearchFilter();\nsearchFilter.SetPageSize(10);\nsearchFilter.SetPageIndex(0); // Will return results 1-10\nsearchFilter.SetPageIndex(1); // Will return results 11-20\nsearchFilter.SetPageIndex(2); // Will return results 21-30\n\n// You can also set pageIndex and pageSize using the constructor\nnew ModSearchFilter(0, 10); // Will return results 1-10\nnew ModSearchFilter(1, 10); // Will return results 11-20\nnew ModSearchFilter(2, 10); // Will return results 21-30\n```\n\n\u003e [!NOTE]  \n\u003e Mod Search Filters have a number of options for filtering and ordering your results. See the [documentation](https://docs.mod.io/unity/) (or use code completion in your IDE) for its available options.\n\u003e If you're looking for more granular filtering, check out the `ModioAPI.Mods.GetModsFilter` class.\n\n### Downloading Images\n\nMore specifically: downloading a *Mod's* images. We'll cover subscribing to and installing mods soon.\n\nA 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.\n\nAs we know, [all mods have a logo](#adding-mods). So let's write a short method that selects a random mod, downloads its logo and displays it alongside its name:\n\n\u003e [!NOTE]  \n\u003e 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!\n\u003e\n\u003e ![Auth Form Example](Unity/Examples/Images/random_mod_example.png)\n\n```csharp\n[SerializeField] Text randomName;\n[SerializeField] Image randomLogo;\n\nasync void SetRandomMod()\n{\n    Mod mod = allMods[UnityEngine.Random.Range(0, allMods.Length - 1)];\n\n    randomName.text = mod.name;\n    \n    (Error error, Texture2D texture) = await mod.Logo.DownloadAsTexture2D(Mod.LogoResolution.X320_Y180);\n\n    if (error)\n    {\n        Debug.LogError($\"Error downloading {mod.Name}'s logo: {error}\");\n        return;\n    }\n    \n    randomLogo.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);\n}\n```\n\n\u003e [!WARNING]  \n\u003e The code above relies on `allMods`, which is set in the first [searching for mods](#searching-for-mods) section. **Ensure that `allMods` has been set before running this method.**\n\nThis 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` \u0026amp; `Mod.GalleryResolution`. See the [documentation](https://docs.mod.io/unity/cs-ref) (or use code completion in your IDE) to view available options. \n\n## Getting Subscribed Mods\n\nWe'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.\n\nThe 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`.\n\nThis makes getting Subscribed mods incredibly convenient as no requests need to be made:\n\n```csharp\nstatic Mod[] GetSubscribedMods() =\u003e User.Current.ModRepository.GetSubscribed().ToArray();\n```\n\nGetting 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:\n\n```csharp\nasync void OnAuth()\n{\n    // ...\n    \n    while (User.Current.IsUpdating)\n        await Task.Yield();\n\n    Mod[] subscribedMods = GetSubscribedMods();\n    Debug.Log($\"Subscribed mods:\\n{(subscribedMods.Length \u003e 0 ? string.Join(\"\\n\", subscribedMods.Select(mod =\u003e $\"{mod.Name} (id: {mod.Id})\")) : \"None\")}\");\n}\n```\n\n\u003e [!IMPORTANT]  \n\u003e `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.\n\n## Subscribing to Mods\n\nSubscribing 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:\n\n```csharp\nasync Task SubscribeToMod(Mod mod)\n{\n    var error = await mod.Subscribe();\n\n    if (error)\n    {\n        Debug.LogError($\"Error subscribing to {mod.Name}: {error}\");\n        return;\n    }\n\n    Debug.Log($\"Subscribed to mod: {mod.Name}\");\n}\n```\n\n\u003e [!NOTE]\n\u003e 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.\n\nTo test it out, in your `OnAuth()` method, after we log all subscribed mods add the following line to subscribe to a random mod:\n\n```csharp\nasync void OnAuth()\n{\n    // ...\n       \n    await SubscribeToMod(allMods[UnityEngine.Random.Range(0, allMods.Length - 1)]);\n}\n```\n\nEnter Play mode and you should see one of the generated mods appear in your \"Subscribed mods\" log!\n\n## Installing Mods\n\nNow, 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](#setup).\n\nIn the following code we'll presume this has been disabled, so we can show how to activate \u0026amp; deactivate it at will. We'll also leverage `ModInstallationManagement.MangementEvents` to see the download progress:\n\n```csharp\nMod currentDownload;\nfloat downloadProgress;\nfloat timeToProgressCheck = 1f;\n\nvoid WakeUpModManagement()\n{\n    void HandleModManagementEvent(\n        Mod mod, \n        Modfile modfile, \n        ModInstallationManagement.OperationType jobType, \n        ModInstallationManagement.OperationPhase jobPhase\n    ){\n        Debug.Log($\"{jobType} {jobPhase}: {mod.Name}\");\n\n        switch (jobPhase)\n        {\n            case ModInstallationManagement.OperationPhase.Started\n                when jobType is not ModInstallationManagement.OperationType.Uninstall:\n                currentDownload = mod;\n                break;\n\n            case ModInstallationManagement.OperationPhase.Cancelled:\n            case ModInstallationManagement.OperationPhase.Failed:\n                currentDownload = null;\n                break;\n            \n            case ModInstallationManagement.OperationPhase.Completed\n                when jobType is not ModInstallationManagement.OperationType.Uninstall:\n                Debug.Log($\"Mod {mod.Name} installed at {mod.File.InstallLocation}\");\n                currentDownload = null;\n                break;\n            \n            case ModInstallationManagement.OperationPhase.Completed:\n                Debug.Log($\"Mod {mod.Name} uninstalled\");\n                break;\n    }\n    \n    ModInstallationManagement.ManagementEvents += HandleModManagementEvent;\n}\n\nvoid Update()\n{\n    if (currentDownload == null)\n        return;\n\n    timeToProgressCheck -= Time.deltaTime;\n\n    if (timeToProgressCheck \u003e 0)\n        return;\n\n    Debug.Log($\"Downloading {currentDownload.Name}: [{Mathf.RoundToInt(currentDownload.File.FileStateProgress * 100)}%]\");\n    timeToProgressCheck += 1f;\n}\n```\n\nIn 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.\n\n\u003e [!NOTE]  \n\u003e There are a number of mod management events available. See the [documentation](https://docs.mod.io/unity/cs-ref) (or use code completion in your IDE) for a complete list.\n\n## Using Mods\n\nWe’re nearing the end now. You've [initialized](#initialization). You've [authenticated](#authentication). You've [uploaded](#adding-mods). You've [searched](#searching-for-mods). You've [subscribed](#subscribing-to-mods). You've [installed](#installing-mods). It's all led to this single question:\n\n*\"How do I find installed mods?\"*\n\nThe 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:\n\n```csharp\ncase ModInstallationManagement.OperationPhase.Completed:\n    Debug.Log($\"Mod {mod.Name} installed at {mod.File.InstallLocation}\");\n    break;\n```\n\nIn order to get a list of all installed mods on the file system, we simply call `ModInstallationManagement.GetAllInstalledMods()`:\n\n```csharp\nasync void OnAuth()\n{\n    // ...\n       \n    var installedMods = ModInstallationManagement.GetAllInstalledMods();\n    Debug.Log($\"Installed mods:\\n{(installedMods.Count \u003e 0 ? string.Join(\"\\n\", installedMods.Select(mod =\u003e $\"{mod.Name} (id: {mod.Id})\")) : \"None\")}\");\n}\n```\n\nWe'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.\n\nOne 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.\n\nAnd that’s it, we’re done! The time has come to build a bridge to your creator community using mod.io.\n\nPlease join us on our [Discord server](https://discord.mod.io) if you have any questions or need some help.\n\n## Complete Class\n\n\u003e [!NOTE]  \n\u003e You can also find the following class (along with an example scene) in `Assets/Plugins/Modio/Unity/Example`. \n\n\u003cdetails\u003e\n\n\u003csummary\u003e\u003ci\u003eClick to expand\u003c/i\u003e\u003c/summary\u003e\n\n```csharp\nusing System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Modio;\nusing Modio.Authentication;\nusing Modio.Mods;\nusing Modio.Unity;\nusing Modio.Users;\nusing UnityEngine;\nusing UnityEngine.Networking;\nusing UnityEngine.UI;\nusing Random = System.Random;\n\npublic class ModioUnityExample : MonoBehaviour\n{\n    // Reusing a single byte-array is a small memory-conscious\n    // optimization for when we are generating our dummy files.\n    static readonly byte[] Megabyte = new byte[1024 * 1024];\n    static readonly Random RandomBytes = new Random();\n\n    [Header(\"Authentication\")]\n    [SerializeField] GameObject authContainer;\n    [SerializeField] InputField authInput;\n    [SerializeField] Button authRequest;\n    [SerializeField] Button authSubmit;\n\n    // Downloading images\n    [Header(\"Random Mod\")]\n    [SerializeField] GameObject randomContainer;\n    [SerializeField] Text randomName;\n    [SerializeField] Image randomLogo;\n    [SerializeField] Button randomButton;\n    \n    // Searching for mods\n    Mod[] allMods;\n\n    // Installing mods\n    Mod currentDownload;\n    float downloadProgress;\n    float timeToProgressCheck = 1f;\n\n    void Awake()\n    {\n        // This enforces email auth to be used, a higher priority can be used if needed\n        ModioServices.Bind\u003cIModioAuthPlatform\u003e()\n                          .FromInstance(new ModioEmailAuthPlatform(GetAuthCode));\n        \n        randomContainer.SetActive(false);\n    }\n\n#region Initialization\n\n    void Start()\n    {\n        InitPlugin();\n    }\n\n    async Task InitPlugin()\n    {\n        Error error = await ModioClient.Init();\n\n        if (error)\n        {\n            Debug.LogError($\"Error initializing mod.io: {error}\");\n            return;\n        }\n\n        Debug.Log(\"mod.io plugin initialized\");\n        OnInit();\n    }\n\n    void OnInit()\n    {\n        if (User.Current.IsAuthenticated)\n        {\n            OnAuth();\n            return;\n        }\n\n        // You can assign these using the Inspector if you prefer\n        authRequest.onClick.AddListener(() =\u003e Authenticate());\n    }\n\n#endregion\n\n#region Authentication\n\n    async Task Authenticate()\n    {\n        Error error = await ModioClient.AuthPlatform.Authenticate(true, authInput.text);\n\n        if (error)\n        {\n            Debug.LogError($\"Error authenticating with email: {error}\");\n            return;\n        }\n\n        OnAuth();\n    }\n\n    async Task\u003cstring\u003e GetAuthCode()\n    {\n        bool codeEntered = false;\n\n        authSubmit.onClick.AddListener(() =\u003e codeEntered = true);\n        \n        while (!codeEntered)\n            await Task.Yield();\n\n        return authInput.text;\n    }\n\n    async void OnAuth()\n    {\n        Debug.Log($\"Authenticated user: {User.Current.Profile.Username}\");\n\n        await AddModsIfNone();\n        \n        allMods = await GetAllMods();\n        Debug.Log($\"Available mods:\\n{string.Join(\"\\n\", allMods.Select(mod =\u003e $\"{mod.Name} (id: {mod.Id})\"))}\");\n        \n        randomButton.onClick.AddListener(SetRandomMod);\n        randomContainer.SetActive(true);\n        SetRandomMod();\n        \n        while (User.Current.IsUpdating)\n            await Task.Yield();\n\n        Mod[] subscribedMods = GetSubscribedMods();\n        Debug.Log($\"Subscribed mods:\\n{(subscribedMods.Length \u003e 0 ? string.Join(\"\\n\", subscribedMods.Select(mod =\u003e $\"{mod.Name} (id: {mod.Id})\")) : \"None\")}\");\n\n        await SubscribeToMod(allMods[UnityEngine.Random.Range(0, allMods.Length - 1)]);\n        \n        WakeUpModManagement();\n    }\n\n#endregion\n\n#region Uploading Mods\n\n    async Task AddModsIfNone()\n    {\n        (Error error, ModioPage\u003cMod\u003e page) = await Mod.GetMods(new ModSearchFilter());\n        if (error)\n        {\n            Debug.LogError($\"Error getting mods: {error}\");\n            return;\n        }\n\n        if (page.Data.Length != 0)\n        {\n            Debug.Log($\"{page.Data.Length} mods found. Not adding mods\");\n            return;\n        }\n\n        DummyModData[] mods =\n        {\n            await GenerateDummyMod(\"Cool Weapon\", \"A really cool weapon.\", \"24466B\", \"FDA576\", 10),\n            await GenerateDummyMod( \"Funny Sound Pack\", \"You'll laugh a lot using this.\", \"B85675\", \"633E63\", 50),\n            await GenerateDummyMod(\"Klingon Language Pack\", \"tlhIngan Hol Dajatlh'a'?\", \"93681C\", \"FFEAD0\", 1),\n            await GenerateDummyMod( \"Ten New Missions\", \"Ported from the sequel to the prequel!\", \"FDA576\", \"D45B7A\", 99),\n        };\n        \n        foreach (DummyModData mod in mods)\n        {\n            await UploadMod(mod.name, mod.summary, mod.logo, mod.path);\n            // Directory.Delete(mod.path, true); // Uncomment if you generated dummy mods\n        }\n    }\n\n    async Task UploadMod(string modName, string summary, Texture2D logo, string path)\n    {\n        Debug.Log($\"Starting upload: {modName}\");\n    \n        var builder = Mod.Create();\n    \n        builder.SetName(modName)\n               .SetSummary(summary)\n               .SetLogo(logo.GetRawTextureData())\n               .EditModfile()\n               .SetSourceDirectoryPath(path)\n               .FinishModfile();\n    \n        (Error error, Mod mod) = await builder.Publish();\n    \n        if (error)\n        {\n            Debug.LogError($\"Error uploading mod {modName}: {error}\");\n            return;\n        }\n\n        Debug.Log($\"Successfully created mod {mod.Name} with Id {mod.Id}\");\n    }\n\n#endregion\n\n#region Searching for Mods\n\n    async Task\u003cMod[]\u003e GetAllMods()\n    {\n        (Error error, ModioPage\u003cMod\u003e page) = await Mod.GetMods(new ModSearchFilter());\n        if (error)\n        {\n            Debug.LogError($\"Error getting mods: {error}\");\n            return Array.Empty\u003cMod\u003e();\n        }\n\n        return page.Data;\n    }\n\n#endregion\n\n#region Downloading Images\n\n    async void SetRandomMod()\n    {\n        Mod mod = allMods[UnityEngine.Random.Range(0, allMods.Length - 1)];\n        \n        randomName.text = mod.Name;\n\n        (Error error, Texture2D texture) = await mod.Logo.DownloadAsTexture2D(Mod.LogoResolution.X320_Y180);\n\n        if (error)\n        {\n            Debug.LogError($\"Error downloading {mod.Name}'s logo: {error}\");\n            return;\n        }\n        \n        randomLogo.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);\n    }\n\n#endregion\n\n#region Subscribing to Mods\n\n    static Mod[] GetSubscribedMods() =\u003e User.Current.ModRepository.GetSubscribed().ToArray();\n\n    async Task SubscribeToMod(Mod mod)\n    {\n        var error = await mod.Subscribe();\n\n        if (error)\n        {\n            Debug.LogError($\"Error subscribing to {mod.Name}: {error}\");\n            return;\n        }\n        \n        Debug.Log($\"Subscribed to mod: {mod.Name}\");\n    }\n\n#endregion\n\n#region Installing Mods\n\n    void WakeUpModManagement()\n    {\n        void HandleModManagementEvent(\n            Mod mod, \n            Modfile modfile, \n            ModInstallationManagement.OperationType jobType, \n            ModInstallationManagement.OperationPhase jobPhase\n        ){\n            Debug.Log($\"{jobType} {jobPhase}: {mod.Name}\");\n\n            switch (jobPhase)\n            {\n                case ModInstallationManagement.OperationPhase.Started\n                    when jobType is not ModInstallationManagement.OperationType.Uninstall:\n                    currentDownload = mod;\n                    break;\n\n                case ModInstallationManagement.OperationPhase.Cancelled:\n                case ModInstallationManagement.OperationPhase.Failed:\n                    currentDownload = null;\n                    break;\n                \n                case ModInstallationManagement.OperationPhase.Completed\n                    when jobType is not ModInstallationManagement.OperationType.Uninstall:\n                    Debug.Log($\"Mod {mod.Name} installed at {mod.File.InstallLocation}\");\n                    currentDownload = null;\n                    break;\n                \n                case ModInstallationManagement.OperationPhase.Completed:\n                    Debug.Log($\"Mod {mod.Name} uninstalled\");\n                    break;\n            }\n        }\n        \n        ModInstallationManagement.ManagementEvents += HandleModManagementEvent;\n    }\n\n    void Update()\n    {\n        if (currentDownload == null)\n            return;\n\n        timeToProgressCheck -= Time.deltaTime;\n\n        if (timeToProgressCheck \u003e 0)\n            return;\n\n        Debug.Log($\"Downloading {currentDownload.Name}: [{Mathf.RoundToInt(currentDownload.File.FileStateProgress * 100)}%]\");\n        timeToProgressCheck += 1f;\n    }\n\n#endregion\n\n#region Generate Dummy Mods\n\n    async Task\u003cDummyModData\u003e GenerateDummyMod(\n        string dummyName,\n        string summary,\n        string backgroundColor,\n        string textColor,\n        int megabytes\n    )\n    {\n        Debug.Log($\"Writing temporary mod file: {dummyName}\");\n\n        string path = Path.Combine(Application.dataPath, $\"../_temp_dummy_mods/{dummyName}\");\n        Directory.CreateDirectory(path);\n\n        using (FileStream fs = File.OpenWrite(Path.Combine(path, $\"{dummyName}.dummy\")))\n        {\n            for (int i = 0; i \u003c megabytes; i++)\n            {\n                RandomBytes.NextBytes(Megabyte);\n                await fs.WriteAsync(Megabyte, 0, Megabyte.Length);\n            }\n        }\n\n        return new DummyModData(\n            dummyName,\n            summary,\n            await GenerateLogo(dummyName.Replace(' ', '+'), backgroundColor, textColor),\n            path\n        );\n    }\n\n    // Uses a third-party API to generate a logo for each\n    // mod, adding some variety when we display them later\n    async Task\u003cTexture2D\u003e GenerateLogo(string text, string backgroundColor, string textColor)\n    {\n        UnityWebRequest request = UnityWebRequestTexture.GetTexture(\n            $\"https://placehold.co/512x288/{backgroundColor}/{textColor}.png?text={text}\"\n        );\n\n        request.SendWebRequest();\n\n        while (!request.isDone)\n            await Task.Yield();\n\n        if (request.result != UnityWebRequest.Result.Success)\n        {\n            Debug.LogError($\"GenerateLogo failed: {request.error}\");\n\n            return null;\n        }\n\n        return DownloadHandlerTexture.GetContent(request);\n    }\n\n    readonly struct DummyModData\n    {\n        public readonly string name;\n        public readonly string summary;\n        public readonly Texture2D logo;\n        public readonly string path;\n\n        public DummyModData(string name, string summary, Texture2D logo, string path)\n        {\n            this.name = name;\n            this.summary = summary;\n            this.logo = logo;\n            this.path = path;\n        }\n    }\n\n#endregion\n}\n\n```\n\n\u003c/details\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmodio%2Fmodio-unity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmodio%2Fmodio-unity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmodio%2Fmodio-unity/lists"}