Custom Achievements/Charms/Items

Background

You can add custom achievements, charms and items without having a good understanding of the various aspects that run Hollow Knight and its mods. Therefore, this guide recommends and expects that you have already worked with the following topics:

  • Understanding the structure of a basic mod.

  • Adding embedded resources to a mod.

Note

These steps are made using Visual Studio as an IDE, if you’re using Rider, the details might not match up. If you are unsure about details, ask in the Discord.

Load embedded images

Embedded image resources can be loaded with:

Note

Embedded resources have the naming format {Projectname}.Resources.{Filename_With_Extension}.

private void loadResources()
{
    Assembly _asm = Assembly.GetExecutingAssembly();
    using (Stream s = _asm.GetManifestResourceStream("CharmHelperExample.Resources.Filename.png"))
    {
        if (s != null)
        {
            byte[] buffer = new byte[s.Length];
            s.Read(buffer, 0, buffer.Length);
            s.Dispose();

            //Create texture from bytes
            var tex = new Texture2D(2, 2);

            tex.LoadImage(buffer, true);

            // Create sprite from texture
            testSprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
        }
    }
}

Add a reference

  1. After downloading SFCore from the ModInstaller, open your project and right-click References and click on Add Reference

  2. Select Browse... and navigate to the Mods folder, where the mod has been installed, after which you can Add it.

  3. Click ok.

Add custom charms

  1. After adding the reference, you can follow the CharmHelper_Example code.

  2. Going through the example:

  3. Preparation of the save settings of the mod to hold the data for 4 charms. (Lines 13 - 20)

public class CHESaveSettings : ModSettings
{
    // insert default values here
    public List<bool> gotCharms = new List<bool>() { true, true, true, true };
    public List<bool> newCharms = new List<bool>() { false, false, false, false };
    public List<bool> equippedCharms = new List<bool>() { false, false, false, false };
    public List<int> charmCosts = new List<int>() { 1, 1, 1, 1 };
}
  1. Add the CharmHelper as a member of the main mod class (Lines 26 - 30)

public class CharmHelperExample : Mod<CHESaveSettings, CHEGlobalSettings>
{
    //private Sprite testSprite;
    public CharmHelper charmHelper { get; private set; }
}
  1. Initialize the CharmHelper with 4 custom charms and empty sprites, also initialize the callbacks needed. (Lines 54 - 66)

Note

This step is the one where you can add your custom sprites. Simply load those instead of the new Sprite().

public override void Initialize()
{
    //loadResources();
    charmHelper = new CharmHelper();
    charmHelper.customCharms = 4;
    charmHelper.customSprites = new Sprite[] { new Sprite(), new Sprite(), new Sprite(), new Sprite() };
    //charmHelper.customSprites = new Sprite[] { testSprite, testSprite, testSprite, testSprite };

    initCallbacks();
}
  1. Initialize the callbacks needed. (Lines 83 - 93)

private void initCallbacks()
{
    ModHooks.Instance.GetPlayerBoolHook += OnGetPlayerBoolHook;
    ModHooks.Instance.SetPlayerBoolHook += OnSetPlayerBoolHook;
    ModHooks.Instance.GetPlayerIntHook += OnGetPlayerIntHook;
    ModHooks.Instance.SetPlayerIntHook += OnSetPlayerIntHook;
    ModHooks.Instance.AfterSavegameLoadHook += initSaveSettings;
    ModHooks.Instance.ApplicationQuitHook += SaveCHEGlobalSettings;
    ModHooks.Instance.LanguageGetHook += OnLanguageGetHook;
}
  1. Form the callbacks for language. (Lines 101 - 124)

private string OnLanguageGetHook(string key, string sheet)
{
    if (key.StartsWith("CHARM_NAME_"))
    {
        int charmNum = int.Parse(key.Split('_')[2]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            return "CHARM NAME";
        }
    }
    if (key.StartsWith("CHARM_DESC_"))
    {
        int charmNum = int.Parse(key.Split('_')[2]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            return "CHARM DESC";
        }
    }
    return Language.Language.GetInternal(key, sheet);
}
  1. Form the callbacks for boolean checks. (Lines 126 - 197)

private bool OnGetPlayerBoolHook(string target)
{
    if (target.StartsWith("gotCharm_"))
    {
        int charmNum = int.Parse(target.Split('_')[1]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            return Settings.gotCharms[charmHelper.charmIDs.IndexOf(charmNum)];
        }
    }
    if (target.StartsWith("newCharm_"))
    {
        int charmNum = int.Parse(target.Split('_')[1]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            return Settings.newCharms[charmHelper.charmIDs.IndexOf(charmNum)];
        }
    }
    if (target.StartsWith("equippedCharm_"))
    {
        int charmNum = int.Parse(target.Split('_')[1]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            return Settings.equippedCharms[charmHelper.charmIDs.IndexOf(charmNum)];
        }
    }
    return PlayerData.instance.GetBoolInternal(target);
}
private void OnSetPlayerBoolHook(string target, bool val)
{
    if (target.StartsWith("gotCharm_"))
    {
        int charmNum = int.Parse(target.Split('_')[1]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            Settings.gotCharms[charmHelper.charmIDs.IndexOf(charmNum)] = val;
            return;
        }
    }
    if (target.StartsWith("newCharm_"))
    {
        int charmNum = int.Parse(target.Split('_')[1]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            Settings.newCharms[charmHelper.charmIDs.IndexOf(charmNum)] = val;
            return;
        }
    }
    if (target.StartsWith("equippedCharm_"))
    {
        int charmNum = int.Parse(target.Split('_')[1]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            Settings.equippedCharms[charmHelper.charmIDs.IndexOf(charmNum)] = val;
            return;
        }
    }
    PlayerData.instance.SetBoolInternal(target, val);
}
  1. Form the callbacks for integer checks. (Lines 199 - 228)

private int OnGetPlayerIntHook(string target)
{
    if (target.StartsWith("charmCost_"))
    {
        int charmNum = int.Parse(target.Split('_')[1]);
        if (charmHelper.charmIDs.Contains(charmNum))
        {
            return Settings.charmCosts[charmHelper.charmIDs.IndexOf(charmNum)];
        }
    }
    return PlayerData.instance.GetIntInternal(target);
}
private void OnSetPlayerIntHook(string target, int val)
{
    // We don't need other mods to adjust the cost of our charms, but it could be added if wanted
    PlayerData.instance.SetIntInternal(target, val);
}

Add custom achievements

  1. After adding the reference, you can follow the CharmHelper_Example code, but you can leave out a lot, as most things are handled by the helper.

  2. Initialize the AchievementHelper with 1 custom achievement and empty sprites.

Note

This step is the one where you can add your custom sprites. Simply load those instead of the new Sprite().

Note

For the Convo’s to work properly, you need the ModHooks.Instance.LanguageGetHook similar to the Helper above, but only listening to the custom convo keys.

public override void Initialize()
{
    //loadResources();

    AchievementHelper.Add("YourCustomAchievementKey", new Sprite(), "YourCustomLanguageConvo", "YourCustomLanguageConvo", false);
}
  1. Done! Now you can at some point in your mod have GameManager.instance.AwardAchievement("YourCustomAchievementKey"); to grant the player the achievement.

Add custom inventory items

  1. After adding the reference, you can follow the CharmHelper_Example code, but you can leave out a lot, as most things are handled by the helper.

  2. Initialize the ItemHelper with custom items and empty sprites.

Note

This step is the one where you can add your custom sprites. Simply load those instead of the new Sprite().

Note

For the Convo’s to work properly, you need the ModHooks.Instance.LanguageGetHook similar to the Helper above, but only listening to the custom convo keys.

Note

For the playerdataBool to work properly, you need the ModHooks.Instance.GetPlayerBoolHook & ModHooks.Instance.SetPlayerBoolHook similar to the CharmHelper, but only listening to the custom bool key.

Note

For the playerdataInt to work properly, you need the ModHooks.Instance.GetPlayerIntHook & ModHooks.Instance.SetPlayerIntHook similar to the CharmHelper, but only listening to the custom int key.

public override void Initialize()
{
    //loadResources();

    // Normal Items, like the Kings Brand, Crystal Heart, etc.
    ItemHelper.AddNormalItem("YourUniqueStateName", new Sprite(), "YourCustomPlayerDataBool", "YourCustomLanguageConvo", "YourCustomLanguageConvo");

    // Counted Items, like Simple Keys, Rancid Eggs, etc.
    ItemHelper.AddCountedItem("YourUniqueStateName", new Sprite(), "YourCustomPlayerDataInt", "YourCustomLanguageConvo", "YourCustomLanguageConvo");

    // 1 2 Both Items, like the Map, Quill and Map and Quill
    SFCore.ItemHelper.AddOneTwoBothItem("YourUniqueStateName",
        new Sprite(), new Sprite(), new Sprite(), // Sprites
        "YourCustomPlayerDataBool", "YourCustomPlayerDataBool", // PlayerData Bools
        "YourCustomLanguageConvo", "YourCustomLanguageConvo", "YourCustomLanguageConvo", // Name Convos
        "YourCustomLanguageConvo", "YourCustomLanguageConvo", "YourCustomLanguageConvo"); // Description Convos
}
  1. Done! You can now have custom Inventory Items.