Skip to content

Latest commit

 

History

History
336 lines (269 loc) · 11.7 KB

extensibility.md

File metadata and controls

336 lines (269 loc) · 11.7 KB

README

This doc helps SMAPI mod authors extend Content Patcher with custom functionality.

To use Content Patcher conditions in your mod, see the conditions API. See the main README for other info.

Contents

Introduction

Content Patcher has a mod-provided API you can use from your own SMAPI mod to add custom tokens. Custom tokens are always prefixed with the ID of the mod that created them, like your-mod-id/SomeTokenName.

There are two parts of the API you can use:

  • The basic API is strongly recommended for most mods. This lets you create custom tokens with minimal knowledge of how Content Patcher works internally; Content Patcher will automatically handle the gritty details for you, and your tokens are highly compatible with future versions of SMAPI.

  • The advanced API gives you much more control over how the token works. The disadvantages are that your token code will be more complex, you need a grasp of how Content Patcher works internally, and your token may break in future versions of Content Patcher when its internal implementation changes. Using the advanced API is strongly discouraged unless you can't use the basic API.

Note that you can use both at once from your mod code.

Access the API

To access the API:

  1. Add Content Patcher as a dependency in your mod's manifest.json:

    "Dependencies": [
       { "UniqueID": "Pathoschild.ContentPatcher", "IsRequired": false }
    ]
  2. Copy IContentPatcherAPI into your mod code, and delete any methods you won't need for future compatibility.

  3. Hook into SMAPI's GameLoop.GameLaunched event and get a copy of the API:

    var api = this.Helper.ModRegistry.GetApi<IContentPatcherAPI>("Pathoschild.ContentPatcher");
  4. Use the API to extend Content Patcher (see below).

Basic API

Concepts

The basic API handles most of the design considerations for you. There's just two things to keep in mind:

Scope

Content Patcher will call your code to get the values each time it updates tokens. That can happen before a save is loaded, while it's still loading, and after it's loaded. You can return null or an empty list if your token isn't ready yet. See the example under add a token which handles all three cases.

Value order

The order you return values affects features like valueAt. You should use the order which makes most sense for your token, since content pack authors can't change it. For most tokens, alphanumeric order is fine (e.g. .OrderBy(p => p, StringComparer.OrdinalIgnoreCase)).

Add a token

You can add a simple token by calling RegisterToken from SMAPI's GameLaunched event (see Access the API above). For example, this creates a {{your-mod-id/PlayerName}} token for the current player's name:

api.RegisterToken(this.ModManifest, "PlayerName", () =>
{
    // save is loaded
    if (Context.IsWorldReady)
        return new[] { Game1.player.Name };

    // or save is currently loading
    if (SaveGame.loaded?.player != null)
        return new[] { SaveGame.loaded.player.Name };

    // no save loaded (e.g. on the title screen)
    return null;
});

RegisterToken in this case has three arguments:

argument type purpose
mod IManifest The manifest of the mod defining the token. You can just pass in this.ModManifest from your entry class.
name string The token name. This only needs to be unique for your mod; Content Patcher will prefix it with your mod ID automatically, so PlayerName in the above example will become your-mod-id/PlayerName.
getValue Func<IEnumerable<string>> A function which returns the current token value. If this returns a null or empty list, the token is considered unavailable in the current context and any patches or dynamic tokens using it are disabled.

That's it! Now any content pack which lists your mod as a dependency can use the token in its fields:

{
   "Format": "2.0.0",
   "Changes": [
      {
         "Action": "EditData",
         "Target": "Characters/Dialogue/Abigail",
         "Entries": {
            "Mon": "Oh hey {{your-mod-id/PlayerName}}! Taking a break from work?"
         }
      }
   ]
}

Advanced API

Caveats

The basic API section above is strongly recommended for most tokens, since Content Patcher will handle details like context updates and change tracking for you, it's easier to troubleshoot, and it's guaranteed not to break without a major-version update.

If you really need it, the advanced API gives you full control (almost equivalent to a token in the Content Patcher core). However:

  • This is experimental. There's no guarantee that future versions will be backwards compatible, or that you'll get any warning before it changes.
  • This is low-level. You must account for the token design considerations documented below, unlike the basic API above which handles them for you.

Concepts

When registering a token through the advanced API, here are some design considerations to avoid problems.

Scope and value order

See Basic API: concepts above.

Context updates

Token values are a cached view of the game state, updated at specific points (e.g. on day start). The combination of all tokens is called the 'context'; a 'context update' is when Content Patcher refreshes all tokens, rebuilds caches, rechecks patch conditions, reloads assets if needed, etc.

Tokens must not change value outside of the UpdateContext method. Doing so may have severe and undocumented effects, from graphical glitches to outright game crashes.

That doesn't preclude tokens that calculate their value dynamically (e.g. FileExists), so long as this calculation does not change. If a token may change dynamically between context updates (e.g. Random), it must implement caching to ensure it does not.

Bounded values

A token is bounded if its values are guaranteed to match a set of known values; otherwise it's unrestricted.

This affects two things:

  • Where the token can be used. For example, a token not guaranteed to return integer values can't be used in a number field, even if it currently returns a number.
  • Validation when the token is used as part of a When condition. For example, this will show a warning since it's guaranteed to always be false:
    "When": {
       "Season": "totally not a valid season"
    }

Note that boundedness is per-input. For example, your token might be bounded if it receives input arguments, but unrestricted without one:

"When": {
   "Relationship": "Abigail:Married", // unrestricted: may return any value (e.g. for custom NPCs)
   "Relationship:Abigail": "Married"  // bounded: returns predefined values like 'married' or 'dating'
}

When registering a token, a token is bounded if you implement HasBoundedValues or HasBoundedRangeValues. Implementing TryValidateValues lets you add custom validation, but does not make the token bounded since Content Patcher can't get a list of possible values.

Immutable values

A token is immutable if its value for a given input will never change for the entire lifetime of the current game instance (from game launch to full exit). Most tokens are mutable, meaning their value may change.

Immutability enables several optimizations. For example, since Content Patcher doesn't need to update their value, it also doesn't need to update dependent tokens/patches (and their dependents, etc).

Input arguments

See input arguments in the tokens guide for more info.

Due to limitations in SMAPI's API proxying, your mod will receive a normalised input string identical to the format shown in the tokens guide instead of a parsed object. Any tokens in the input will be replaced by their value. Note that if no input arguments were given, the token will receive null.

Add a token

To register a custom token using the advanced API:

  1. Create a token class with any combination of the methods listed in this file. Note that the methods in your class must exactly match the names, return values, and arguments. If Content Patcher files a non-matching or unrecognized public method, it'll show an error and reject the token.

    For example, let's say we want a token which returns the initials for the given name (like {{Initials:John Smith}}JS), or the player's name if called with no input. Here's a token class to do that:

    /// <summary>A token which returns the player's initials, or the initials of the input name.</summary>
    internal class InitialsToken
    {
        /*********
        ** Fields
        *********/
        /// <summary>The player name as of the last context update.</summary>
        private string PlayerName;
    
    
        /*********
        ** Public methods
        *********/
        /****
        ** Metadata
        ****/
        /// <summary>Get whether the token allows input arguments (e.g. an NPC name for a relationship token).</summary>
        public bool AllowsInput()
        {
            return true;
        }
    
        /// <summary>Whether the token may return multiple values for the given input.</summary>
        /// <param name="input">The input arguments, if applicable.</param>
        public bool CanHaveMultipleValues(string input = null)
        {
            return false;
        }
    
        /****
        ** State
        ****/
        /// <summary>Update the values when the context changes.</summary>
        /// <returns>Returns whether the value changed, which may trigger patch updates.</returns>
        public bool UpdateContext()
        {
            string oldName = this.PlayerName;
            this.PlayerName = Game1.player?.Name ?? SaveGame.loaded?.player?.Name; // tokens may update while the save is still being loaded
            return this.PlayerName != oldName;
        }
    
        /// <summary>Get whether the token is available for use.</summary>
        public bool IsReady()
        {
            return this.PlayerName != null;
        }
    
        /// <summary>Get the current values.</summary>
        /// <param name="input">The input arguments, if applicable.</param>
        public IEnumerable<string> GetValues(string input)
        {
            // get name
            string name = input ?? this.PlayerName;
            if (string.IsNullOrWhiteSpace(name))
                yield break;
    
            // get initials
            yield return string.Join("", name.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(p => p[0]));
        }
    }
  2. Next let's register it with Content Patcher in the GameLaunched event (see Access the API above):

    api.RegisterToken(this.ModManifest, "Initials", new InitialsToken());
  3. That's it! Now any content pack which lists your mod as a dependency can use the token in its fields:

    {
       "Format": "2.0.0",
       "Changes": [
          {
             "Action": "EditData",
             "Target": "Characters/Dialogue/Abigail",
             "Entries": {
                "Mon": "Oh hey {{your-mod-id/Initials}}! Taking a break from work?"
             }
          }
       ]
    }

    See also