Skip to content
Headline edited this page Jun 6, 2021 · 1 revision

This submodule allows you to add custom achievements and unlockables to your mod.

Important types and functions

UnlockableAPI.AddUnlockable

public static void AddUnlockable<TUnlockable>( bool serverTracked );
    where TUnlockable : BaseAchievement, IModdedUnlockableDataProvider, new()

This method adds the achievement described by TUnlockable. In nearly all cases, TUnlockable should be a type inheriting ModdedUnlockable. This method works off the interfaces solely for flexibility.

serverTracked indicates if the achievement unlock conditions are tracked by the server or the client.

Throws when

  • The submodule is not loaded
  • AchievementManager or UnlockableCatalog have already initialized (they initialize after plugin Awake() and OnEnable() but before Start())
  • The supplied reward identifier is already in use by another mod or the base game.
  • any implementation code in TUnlockable that is called here throws.

Usage Example

// Usings and standard attributes are omitted for simplicity
[R2APISubmoduleDependency( nameof(UnlockableAPI) )]
public class MyExampleMod : BaseUnityPlugin
{
    // Add the unlockable in Awake (or OnEnable) or it will be too late 
    protected void Awake()
    {
        // This call adds the unlockable type
        UnlockableAPI.AddUnlockable<ExampleUnlockable>( true );
    }
}

// The type that is used to implement the functionality of the achievement and provide the relevant metadata
// VanillaSpriteProvider can be swapped for anything that implements IAchievementSpriteProvider.
// There is an additional provider for use with ResourcesAPI called CustomSpriteProvider.
public class ExampleUnlockable : ModdedUnlockable<VanillaSpriteProvider>
{
    // All of the strings defined here must be unique in order to interop with both the base game and other mods.
    // For that reason, you should always use something of the format "Author_Mod_Achievement_..."
    // Note that changing these values is a breaking change for your mod. All users will have to redo the challenge if the id changes.
    // Also, note that in most cases it is a good idea for you to add a config option to reset unlockable progress for users. 
    // Not only does this make testing much easier, but it also allows users to gain some potential replayability.


    // The name used to identify the achievement.
    public override String AchievementIdentifier { get; } = "MYNAME_MYEXAMPLEMOD_EXAMPLEUNLOCKABLE_ACHIEVEMENT_ID";

    // The key used to identify things that are unlocked by this.
    public override String UnlockableIdentifier { get; } = "MYNAME_MYEXAMPLEMOD_EXAMPLEUNLOCKABLE_REWARD_ID";

    // The key of a prereq for unlocking this. Use "" for none.
    public override String PrerequisiteUnlockableIdentifier { get; } = "MYNAME_MYEXAMPLEMOD_EXAMPLEUNLOCKABLE_PREREQ_ID";

    // Language token for the achievement name.
    public override String AchievementNameToken { get; } = "MYNAME_MYEXAMPLEMOD_EXAMPLEUNLOCKABLE_ACHIEVEMENT_NAME";

    // Language token for the achievement description.
    public override String AchievementDescToken { get; } = "MYNAME_MYEXAMPLEMOD_EXAMPLEUNLOCKABLE_ACHIEVEMENT_DESC";

    // Language token for the unlockable name.
    public override String UnlockableNameToken { get; } = "MYNAME_MYEXAMPLEMOD_EXAMPLEUNLOCKABLE_UNLOCKABLE_NAME";

    // The sprite provider.
    protected override VanillaSpriteProvider SpriteProvider { get; } = new VanillaSpriteProvider( "VANILLA PATH" );


    // All of the functionality and behaviour of your achievement is implemented through overriding base methods

    // A function we are registering to an event.
    public void CheckIfDeathIsABeetle( DamageReport report )
    {
        // Null checks, because good people don't break the game by throwing exceptions in their hooks/events.
        if( report is null ) return;
        if( report.victimBody is null ) return;

        // Not a very good way to check, but works for demonstration. The proper approach would involve getting the bodyindex and checking that.
        if( report.victiomBody.name.Contains( "BeetleBody" ) )
        {
            // Grants the achievement.
            base.Grant();
        }
    }

    // This is called in order to set up the checks for the achievement.
    public override void OnInstall()
    {
        // In general, always should call the base, unless you know what you are doing.
        base.OnInstall();

        // Register our function to onCharacterDeathGlobal to check if the thing that died was a beetle.
        // You are not limited to vanilla events. You can even apply hooks here if you need to. 
        GlobalEventManager.onCharacterDeathGlobal += this.CheckIfDeathIsABeetle;
    }

    // This is called when the achievement is granted in order to clean up the checks.
    // You should be removing events/hooks that you register in OnInstall here.
    public override void OnUninstall()
    {
        base.OnUninstall();
        GlobalEventManager.onCharacterDeathGlobal -= this.CheckIfDeathISABeetle;
    }
}