Creating Mods - Robocat999/AtlyssModLoader GitHub Wiki

This loader is deprecated! Please use BepInEx v5.4.23.2

See the README on the homepage for more information.


Initial Setup

Modding ATLYSS through the AtlyssModLoader will require some setup for the coding environment.

  1. A method of patching code. Harmony is the recommended library for handling this, and the mod loader is built with it in mind. Visual Studio 2022 can import Harmony using NuGet.
  2. The ability to compile to a .dll with Net Framework 4.x. Net Framework 4.6 is what AtlyssModLoader is released with. Visual Studio 2022 can be set to 4.6 in the project properties.
  3. An IDE which can pull in outside references. This lets you reference types from Unity and ATLYSS. Visual Studio 2022 can do this.

If you require a more detailed setup guide, check out the Visual Studio Setup Guide!

Baseline Mod Framework

Every mod should have at minimum four things:

  1. A public static class. Avoid using class names that appear in ATLYSS, as it uses the global namespace.
  2. A public static void Init() in the above class. This will be called by the loader on game startup.
  3. A namespace, ideally unique to the mod. Namespaces help prevent conflicts with other mods and with ATLYSS code.
  4. A way to patch the ATLYSS's code. Harmony is recommended, as the mod loader is built around supporting it.

All mods rotate around the Init(). When ATLYSS starts up, the loader will call the Init() of every mod in the Mods folder. Thus, all code you wish to run, typically a suite of patches, will need to be called from Init().

Example Mod

The following is an example of a functioning mod to help teach by example. This mod will create a unique item that will be dropped by all enemies. Note that this example is fairly crude, and not meant to represent the best way of doing things, just showcase some baseline necessities to help clear up obscure questions and errors.

using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;

/// Make sure to use a unique namespace to avoid conflicts!
namespace ExampleMod
{
    /// <summary>
    /// The entry class that will serve as the mod's driver.
    /// </summary>
    public static class ExampleModClass
    {

        /// <summary>
        /// The entry point of the mod.
        /// This function will be called on game startup by the mod loader.
        /// </summary>
        public static void Init()
        {
            // Set up a harmony instance 
            // Make sure to use a unique identifer for it
            // Otherwise you can conflict with other Harmony setups
            // atlyss.mod.creator.ModName is a good format to follow
            Harmony harmony = new Harmony("atlyss.mod.robocat999.ExampleMod");

            // Enable debugging if you desire debug logs for testing
            // Do NOT release a mod with Harmony.DEBUG = true, it will be bothersome to users
            Harmony.DEBUG = true;

            // Tell harmony to start patching
            harmony.PatchAll();
        }
    }

    /// <summary>
    /// This is a harmony patch. They must be identifed by the tag.
    /// </summary>
    [HarmonyPatch]
    class Test_Item_Patch
    {
        // These flags tell Harmony where exactly to patch
        // This will patch the GameManager function Cache_ScriptableAssets
        // It will apply a Postfix patch, which will fire *after* the orginal function completes
        [HarmonyPatch(typeof(GameManager), "Cache_ScriptableAssets")]
        [HarmonyPostfix]
        public static void AssetCacheAddition_Postfix(GameManager __instance)
        {
            // Harmony provides simple logging features
            // Note that these will functions will only produce logs if Harmony.DEBUG = true
            FileLog.Log("The test item postfix is running");

            // Create a new Item object
            ScriptableTradeItem testItem = ScriptableObject.CreateInstance<ScriptableTradeItem>();
            testItem._vendorCost = 2;
            testItem._itemName = "Example Mod Item";
            testItem._itemDescription = "Wow, what a drop!";
            testItem._itemIcon = Sprite.Create(Texture2D.blackTexture, new Rect(0.0f, 0.0f, Texture2D.blackTexture.width, Texture2D.blackTexture.height), new Vector2(0.5f, 0.5f), 100.0f);

            FileLog.Log("  Attempting GameManager cache insert");

            // Get access to the GameManager's private variables by using Traverse. See Harmony documentation for further explanation
            // Note that the ealier passed in __instance variable was used. This variable holds the instance of the class triggering our patch
            // Using the active __instance is very important if the class is not static 
            Traverse gameManager = Traverse.Create(__instance);
            Dictionary<string, ScriptableItem> itemCache = gameManager.Field("_cachedScriptableItems").GetValue<Dictionary<string, ScriptableItem>>();
            itemCache.Add(testItem._itemName, testItem);
            gameManager.Field("_cachedScriptableItems").SetValue(itemCache);

            FileLog.Log("The test item postfix is complete");
        }
    }

    /// <summary>
    /// This is another harmony patch. One way of organizing is to use a new class for each ATLYSS class you patch.
    /// </summary>
    [HarmonyPatch]
    class Drop_Item_Patch
    {
        // Note that this is a Prefix patch, and will run *before* the orginal function starts
        [HarmonyPatch(typeof(ItemDropEntity), "Client_DropItems")]
        [HarmonyPrefix]
        public static void Server_DropItem(ItemDropEntity __instance)
        {
            FileLog.Log("The Server_DropItem prefix is running");

            // Recreate the ealier item
            // This duplication is done for ease of understanding. In a real mod, avoid this kind of manual duplication
            ScriptableTradeItem testItem = ScriptableObject.CreateInstance<ScriptableTradeItem>();
            testItem._vendorCost = 2;
            testItem._itemName = "Example Mod Item";
            testItem._itemDescription = "Wow, what a drop!";
            testItem._itemIcon = Sprite.Create(Texture2D.blackTexture, new Rect(0.0f, 0.0f, Texture2D.blackTexture.width, Texture2D.blackTexture.height), new Vector2(0.5f, 0.5f), 100.0f);

            FileLog.Log("  Generated testItem");

            // Create a new ItemDrop object 
            ItemDrop testDrop = new ItemDrop();
            testDrop._item = testItem;
            testDrop._dropChance = 1f;
            testDrop._dropQuantity = 20;

            FileLog.Log("  Generated testDrop");

            // Access another private field and alter it with our new item
            Traverse itemDropEntity = Traverse.Create(__instance);
            ItemDrop[] itemDrops = itemDropEntity.Field("_itemDrops").GetValue<ItemDrop[]>();
            itemDrops[0] = testDrop;
            itemDropEntity.Field("_itemDrops").SetValue(itemDrops);

            FileLog.Log("The Server_DropItem prefix is complete");
        }
    }
}
⚠️ **GitHub.com Fallback** ⚠️