Modding:Modder Guide/APIs

From Stardew Valley Wiki
Jump to navigation Jump to search

Creating SMAPI mods SMAPI mascot.png


Modding:Index

SMAPI provides a number of APIs for mods to use. Click a section on the right or below for more details.

Basic APIs

page summary
Manifest A file needed for every mod or content pack which describes the mod, lists dependencies, enables update checks, etc.
Events Respond when something happens in the game (e.g. when a save is loaded), and often include details about what happened.
Mod configuration Let players edit a config.json file to configure your mod.
Content Load images/maps/data, and edit or replace the game's images/maps/data.
Logging Write messages to the SMAPI console and log.
Reflection Access fields, properties, or methods which are normally inaccessible.
Multiplayer Provides methods for supporting multiplayer.
Translation Translate your mod text into any game language.
Utilities Use constants, contextual information, date logic, and semantic versions.

Advanced APIs

page summary
Content packs Let other modders provide files for your mod to read, which players can install like any other mod.
Console commands Add custom commands to the SMAPI console.
Mod integrations Get information about loaded mods, and integrate with mods using mod-provided APIs.

Mod APIs

Configuration

You can let users configure your mod through a config.json file. SMAPI will automatically create the file and take care of reading, normalising, and updating it.

Basic configuration
Here's the simplest way to use config.json:
  1. Create your model. This is just a C# class with properties for the config options you want, and it can contain almost anything from a few boolean fields to a complex object graph (You should try to keep it simple for your users, though).
    You can set defaults directly:
    class ModConfig
    {
       public bool ExampleBoolean { get; set; } = true;
       public float ExampleFloat { get; set; } = 0.5f;
    }
    

    ...or with a constructor:

    class ModConfig
    {
       public bool ExampleBoolean { get; set; }
       public float ExampleFloat { get; set; }
    
       public ModConfig()
       {
          this.ExampleBoolean = true;
          this.ExampleFloat = 0.5f;
       }
    }
    
  2. In your ModEntry::Entry method, add this line to read the config options:
    ModConfig config = helper.ReadConfig<ModConfig>();
    

    Values are then accessible through the value config. Example:

    public class ModEntry : Mod
    {
      public override void Entry(IModHelper helper)
      {
        ModConfig config = helper.ReadConfig<ModConfig>();
        float exampleF = config.ExampleFloat;
      }
    }
    
That's it! When the player launches the game, SMAPI will create the config.json file automatically if it doesn't exist yet, using the default config options you provided in your model. If you need to edit and save the config, you can use helper.WriteConfig(config). You can access the helper in other methods using this.Helper.
Custom JSON files
Sometimes one config.json isn't enough, or you need to store data that's not meant to be edited by the user. This is pretty easy using the ModHelper:
  1. Create your model (just like the previous section).
  2. In your mod code, use this.Helper to read and write to a named file:
    // read file
    var model = this.Helper.ReadJsonFile<ModData>("data.json") ?? new ModData();
    
    // save file (if needed)
    this.Helper.WriteJsonFile("data.json", model);
    
    Note that ReadJsonFile will return null if the file doesn't exist. The above example will create a default instance if that happens; if you don't want to do that, just remove the ?? new ModData() part.
Per-save JSON files
You can create per-save files by using the save ID in the name. If you specify a folder path (relative to your mod folder), SMAPI will create the folders automatically if needed. For example, here's a typical per-save data file:
// read file
var model = this.Helper.ReadJsonFile<ModData>($"data/{Constants.SaveFolderName}.json") ?? new ModData();

// write file (if needed)
this.Helper.WriteJsonFile($"data/{Constants.SaveFolderName}.json", model);

Console commands

You can add commands to the SMAPI console (the terminal window that opens alongside the game), and invoke them by typing directly into the console. Note that most players aren't comfortable with a command-line interface, so you should prefer in-game interfaces for player-facing features.

Each console command must have:

  • a name which the player types to invoke the command.
  • a description shown when the player uses the help command. This should explain what the command does, how to use it, and what arguments it accepts. The example below shows the recommended convention.
  • the code to run when the command is called.

The code below creates a player_setmoney command (but don't forget to validate input, this is just an example).

public override void Entry(IModHelper helper)
{
   helper.ConsoleCommands.Add("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: the integer amount.", this.SetMoney);
}

/// <summary>Set the player's money when the 'player_setmoney' command is invoked.</summary>
/// <param name="command">The name of the command invoked.</param>
/// <param name="args">The arguments received by the command. Each word after the command name is a separate argument.</param>
private void SetMoney(string command, string[] args)
{
   Game1.player.money = int.Parse(args[0]);
   this.Monitor.Log($"OK, set your money to {args[0]}.");
}

Here's how the player would use it:

help player_setmoney
> player_setmoney: Sets the player's money.
> 
> Usage: player_setmoney <value>
> - value: the integer amount.

player_setmoney 5000
> OK, set your money to 5000.

Content

Read assets

You can read custom images or maps to use from your mod folder or game content, with support for .xnb, .png, and .tbin files.

Example usage:

  • Read an image from your mod folder (from an assets subfolder in this example):
    // read an XNB file
    var texture = helper.Content.Load<Texture2D>(@"assets\texture.xnb", ContentSource.ModFolder);
    
    // read a PNG file
    var texture = helper.Content.Load<Texture2D>(@"assets\texture.png", ContentSource.ModFolder);
    
  • Read a map from your mod folder (will automatically fix tilesheet references):
    Map map = helper.Content.Load<Map>(@"assets\map.tbin", ContentSource.ModFolder);
    
  • Read a new tilesheet for a map from your mod folder:
    tilesheet.ImageSource = helper.Content.GetActualAssetKey(@"assets\tilesheet.png", ContentSource.ModFolder);
    
  • Read an asset from the game's content folder:
    var data = helper.Content.Load<Dictionary<int, string>>(@"Data\ObjectInformation.xnb", ContentSource.GameContent);
    

Notes:

  • Don't call content.Load<T> in draw code for performance reasons, since drawing happens ≈60 times per second. Instead, load your content ahead of time and reuse it.
  • When loading a mod file, it's automatically relative to your mod folder. SMAPI won't accept an absolute file path.
  • Don't worry about which path separators you use; SMAPI will normalise them automatically.

Edit assets

You can edit any texture or data loaded by the game (often called 'XNB data') without changing the original files. You do this by implementing IAssetEditor in your Mod class, which adds two methods: CanEdit<T> returns whether the mod can edit a particular asset, and Edit<T> makes any changes needed. The Edit<T> method receives a helper for editing various data types.

Here are a few examples:

  • This mod adds a new item (see Modding:Object data). Note: it's better to use mod frameworks like Json Assets for custom items, to avoid dealing with save issues and ID collisions.
    public class ModEntry : Mod, IAssetEditor
    {
        /// <summary>Get whether this instance can edit the given asset.</summary>
        /// <param name="asset">Basic metadata about the asset being loaded.</param>
        public bool CanEdit<T>(IAssetInfo asset)
        {
            return asset.AssetNameEquals(@"Data\ObjectInformation");
        }
    
        /// <summary>Edit a matched asset.</summary>
        /// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param>
        public void Edit<T>(IAssetData asset)
        {
            int id = ...;
            asset
                .AsDictionary<int, string>()
                .Set(id, "Blood Rose/40/10/Basic -81/Blood Rose/Not the prettiest flower, but the leaves make a good salad.");
        }
    }
    
  • This mod lets crops grow in any season (example only, doesn't handle edge cases):
    public class ModEntry : Mod, IAssetEditor
    {
        /// <summary>Get whether this instance can edit the given asset.</summary>
        /// <param name="asset">Basic metadata about the asset being loaded.</param>
        public bool CanEdit<T>(IAssetInfo asset)
        {
            return asset.AssetNameEquals(@"Data\Crops");
        }
    
        /// <summary>Edit a matched asset.</summary>
        /// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param>
        public void Edit<T>(IAssetData asset)
        {
            asset
                .AsDictionary<int, string>()
                .Set((id, data) =>
                {
                    string[] fields = data.Split('/');
                    fields[1] = "spring summer fall winter";
                    return string.Join("/", fields);
                });
        }
    }
    
  • This code replaces part of a game image:
    public class ModEntry : Mod, IAssetEditor
    {
        /// <summary>Get whether this instance can edit the given asset.</summary>
        /// <param name="asset">Basic metadata about the asset being loaded.</param>
        public bool CanEdit<T>(IAssetInfo asset)
        {
            return asset.AssetNameEquals(@"Portraits\Abigail");
        }
    
        /// <summary>Edit a matched asset.</summary>
        /// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param>
        public void Edit<T>(IAssetData asset)
        {
            Texture2D customTexture = this.Helper.Content.Load<Texture2D>("custom-texture.png", ContentSource.ModFolder);
            asset
                .AsImage()
                .PatchImage(customTexture, targetArea: new Rectangle(300, 100, 200, 200));
        }
    

For more advanced scenarios, you can inject multiple IAssetEditor instances instead. When you inject a new editor, SMAPI will automatically reload all game assets so it can intercept them. (This is resource-intensive when done outside your Entry method, so avoid adding editors unnecessarily.)

public class ModEntry : Mod
{
    /// <summary>The mod entry point, called after the mod is first loaded.</summary>
    /// <param name="helper">Provides simplified APIs for writing mods.</param>
    public override void Entry(IModHelper helper)
    {
        helper.Content.AssetEditors.Add(new MyCropEditor());
    }
}

Inject assets

You can inject new assets for the game to use (e.g. to add dialogue for a custom NPC). To edit existing assets, you should use asset editors instead. If multiple loaders match the same asset, SMAPI will show an error and use none of them.

In most cases, you can just implement IAssetLoader in your Mod class to do it. This adds two methods: CanLoad<T> returns whether the mod can load a particular asset, and Load<T> provides the asset data. For example, this code adds a new dialogue file for a custom NPC.

public class ModEntry : Mod, IAssetLoader
{
    /// <summary>Get whether this instance can load the initial version of the given asset.</summary>
    /// <param name="asset">Basic metadata about the asset being loaded.</param>
    public bool CanLoad<T>(IAssetInfo asset)
    {
        return asset.AssetNameEquals(@"Characters\Dialogue\John");
    }

    /// <summary>Load a matched asset.</summary>
    /// <param name="asset">Basic metadata about the asset being loaded.</param>
    public T Load<T>(IAssetInfo asset)
    {
        return (T)(object)new Dictionary<string, string> // (T)(object) is a trick to cast anything to T if we know it's compatible
        {
            ["Introduction"] = "Hi there! My name is Jonathan."
        };
    }
}

For more advanced scenarios, you can inject multiple IAssetLoader instances instead. When you inject a new loader, SMAPI will automatically reload all game assets so it can intercept them. (This is resource-intensive when done outside your Entry method, so avoid adding loaders unnecessarily.)

public class ModEntry : Mod
{
    /// <summary>The mod entry point, called after the mod is first loaded.</summary>
    /// <param name="helper">Provides simplified APIs for writing mods.</param>
    public override void Entry(IModHelper helper)
    {
        helper.Content.AssetLoaders.Add(new MyDialogueLoader());
    }
}

Reload assets

Caution: reloading assets is fairly expensive, so use this API judiciously to avoid impacting game performance.

You can reload an asset by invalidating it from the cache. It will be reloaded next time the game requests it (and mods will have another chance to intercept it), and SMAPI will reload it automatically if it's a core asset that's kept in memory by the game. For example, this lets you change what clothes an NPC is wearing (by invalidating their cached sprites or portraits).

Typically you'll invalidate a specific asset key:

helper.Content.InvalidateCache(@"Data\ObjectInformation.xnb"); // path separators and capitalisation don't matter

You can also invalidate assets matching a lambda:

helper.Content.InvalidateCache(asset => asset.DataType == typeof(Texture2D) && asset.AssetNameEquals(@"Data\ObjectInformation.xnb"));

Content packs

A content pack is a sub-mod containing files your mod can read. These are installed just like a regular SMAPI mod, but don't do anything on their own. These must specify your mod in their manifest.json. See Modding:Content packs for more info about content packs.

SMAPI provides a method to get content packs loaded for your mod, and each content pack has an API you can use to read its files:

foreach(IContentPack contentPack in this.Helper.GetContentPacks())
{
    // read content pack manifest
    this.Monitor.Log($"Reading content pack: {contentPack.Manifest.Name} {contentPack.Manifest.Version}");

    // read a JSON file
    YourDataFile data = contentPack.ReadJsonFile<YourDataFile>("content.json");

    // load an asset or image
    Texture2D image = contentPack.LoadAsset<Texture2D>("image.png");
}

Logging

Your mod can write messages to the console window and log file using the monitor. For example, this code:

this.Monitor.Log("a trace message", LogLevel.Trace);
this.Monitor.Log("a debug message", LogLevel.Debug);
this.Monitor.Log("an info message", LogLevel.Info);
this.Monitor.Log("a warning message", LogLevel.Warn);
this.Monitor.Log("an error message", LogLevel.Error);

will log something like this:

[18:00:00 TRACE Mod Name] a trace message
[18:00:00 DEBUG Mod Name] a debug message
[18:00:00 INFO Mod Name] an info message
[18:00:00 WARN Mod Name] a warning message
[18:00:00 ERROR Mod Name] an error message

Note that LogLevel.Trace messages won't appear in the console window by default, they'll only be written to the log file. Trace messages are for troubleshooting details that are useful when someone sends you their error log, but which the player normally doesn't need to see. (You can see trace messages in the console if you install the "SMAPI for developers" version.)

Reflection

SMAPI provides an API for robustly accessing fields, properties, or methods you otherwise couldn't access, such as private fields. You can use it from helper.Reflection in your entry method, or this.Helper.Reflection elsewhere in your entry class.

Here are a few examples of what this lets you do:

// did you pet your pet today?
bool wasPet = this.Helper.Reflection.GetField<bool>(pet, "wasPetToday").GetValue();

// what is the spirit forecast today?
string forecast = this.Helper.Reflection
   .GetMethod(new TV(), "getFortuneForecast")
   .Invoke<string>();

// randomise the mines
if(Game1.currentLocation is MineShaft)
   this.Helper.Reflection.GetField<Random>(Game1.currentLocation, "mineRandom").SetValue(new Random());

This works with static or instance fields/methods, caches the reflection to improve performance, and will throw useful errors automatically when reflection fails.

If you need to do more, you can switch to C#'s underlying reflection API:

FieldInfo field = this.Helper.Reflection.GetField<string>().FieldInfo;
MethodInfo method = this.Helper.Reflection.GetMethod().MethodInfo;

Mod registry

Your mod can get information about loaded mods, or check if a particular mod is loaded. (All mods are loaded by the time your mod's Entry(…) method is called.)

// check if a mod is loaded
bool isLoaded = this.Helper.ModRegistry.IsLoaded("UniqueModID");

// get manifest info for a mod (name, description, version, etc.)
IManifest manifest = this.Helper.ModRegistry.Get("UniqueModID");

// get manifest info for all loaded mods
foreach(IManifest manifest in this.Helper.ModRegistry.GetAll()) {  }

Multiplayer

The multiplayer API provides methods to support modding in a multiplayer context:

// get a unique multiplayer ID (e.g. for animal IDs)
int uniqueID = this.Helper.Multiplayer.GetNewID();

// get the locations being sync'd from the main player
foreach (GameLocation location in this.Helper.Multiplayer.GetActiveLocations())

Translation

The translation API lets you translate your mod into the player's current language, accounting for locale fallback automatically (e.g. if a translation isn't available in pt-BR.json, SMAPI will check pt.json and default.json). Translations can be a simple string, or they can include tokens to inject values into the translation.

File structure
SMAPI reads translations from JSON files in a structure like this:
YourMod/
   i18n/
      default.json
      es.json
      pt.json
   manifest.json
   YourMod.dll

The default.json file should contain a flat key→value lookup with your default text. Each key is case-insensitive, and can contain alphanumeric, underscore, hyphen, and dot characters. Feel free to add JavaScript comments to organise or document your translations. For example:

{
    // example translations
    "item-type.label": "Item type",
    "item-type.fruit-tree": "{{fruitName}} tree",
}

You can then add translation files for each language you want to support, by copying the default.json file and translating its values. Each translation file should have one of these file names:

Language File name
Chinese zh.json
German de.json
Japanese ja.json
Portuguese pt.json
Russian ru.json
Spanish es.json
Reading translations
Once your i18n files are set up, you can read translations for the current locale:
// read a simple translation
string label = helper.Translation.Get("item-type.label");

// read a translation which uses tokens (accepts an anonymous object, dictionary, or model)
string text = helper.Translation.Get("item-type.fruit-tree", new { fruitName = "apple" });

The helper.Translate(…) method returns a fluent interface — you can keep calling methods on its return value to customise the translation. (See IntelliSense for a description of the available methods.) To get the text, simply assign it to a string:

// use fluent chain
string text = helper.Translate(key).Tokens(tokens).Tokens(moreTokens).Assert();

If your code has a lot of translation calls, you can make it less verbose by aliasing the translation helper:

var i18n = helper.Translation;

i18n.Get("item-type.fruit-tree", new { fruitName = i18n.Get("apple") });
Tips for translators
  • Save i18n files with UTF-8 encoding to avoid broken symbols in-game.
  • Type reload_i18n into the SMAPI console and hit enter to reload translations without exiting the game. (If a mod internally cached a translation, it may not be updated.)

Utilities

SMAPI provides some C# objects you can use to simplify your code.

Constants

The Constants class provides metadata about SMAPI and the game.

value meaning
Constants.ApiVersion The version of the running SMAPI instance.
Constants.MinimumGameVersion
Constants.MaximumGameVersion
The game versions supported by the running SMAPI instance.
Constants.ExecutionPath The absolute path to the Stardew Valley folder.
Constants.DataPath The absolute path to the game's data folder (which contains the save folder).
Constants.LogDir The absolute path to the folder containing the game and SMAPI logs.
Constants.SavesPath The absolute path to the save folder.
Constants.CurrentSavePath The absolute path to the current save folder, if a save is loaded.
Constants.SaveFolderName The name of the current save folder (like Name_012345789), if a save is loaded.

Context

The Context class provides information about the game state and player control:

value meaning
Context.IsWorldReady Whether the player has loaded a save and the world has finished initialising. Useful for ignoring events before the save is loaded.
Context.IsPlayerFree Whether Context.IsWorldReady and the player is free to act on the world (no menu is displayed, no cutscene is in progress, etc).
Context.CanPlayerMove Whether Context.IsPlayerFree and the player is free to move (e.g. not using a tool).
Context.IsMultiplayer
The following describes the upcoming SMAPI 2.6, and may change before release.
Whether Context.IsWorldReady, and the player loaded the save in multiplayer mode (regardless of whether any other players are connected).
Context.IsMainPlayer
The following describes the upcoming SMAPI 2.6, and may change before release.
Whether Context.IsWorldReady, and the player is the main player. This is always true in single-player, and true when hosting in multiplayer.

Dates

Use SDate for calculating in-game dates. You start by creating a date:

var date = SDate.Now(); // current date
var date = new SDate(28, "spring"); // date in the current year
var date = new SDate(28, "spring", 2); // date in the given year

Then you can calculate offsets from any date:

// add days
new SDate(28, "spring", 1).AddDays(370); // 06 fall in year 4

// subtract days
new SDate(01, "spring", 2).AddDays(-1); // 28 winter in year 1

...and compare dates:

var a = new SDate(01, "spring");
var b = new SDate(02, "spring");
if (a < b) // true
  ...

Note that SDate won't let you create invalid dates:

// ArgumentException: Invalid day '30', must be a value from 1 to 28.
new SDate(30, "spring");

// ArithmeticException: Adding -1 days to 01 spring Y1 would result in invalid date 28 winter Y0.
new SDate(01, "spring", 1).AddDays(-1);

Once created, dates have a few properties you can use:

property meaning
Day The day of month.
Season The normalised season name.
Year The year number.
DayOfWeek The day of week (like Monday).
DaysSinceStart [SMAPI 2.2+] The number of days since the first day, inclusively (i.e. 01 spring Y1 = 1).

Semantic versions

Use SemanticVersion to manipulate and compare versions per the Semantic Versioning 2.0 standard. Example usage:

// build version from parts
ISemanticVersion version = new SemanticVersion(5, 1, 0, "beta");

// build version from string
ISemanticVersion version = new SemanticVersion("5.1.0-beta");

// compare versions (also works with SemanticVersion instances instead of strings)
new SemanticVersion("5.2").IsOlderThan("5.10"); // true
new SemanticVersion("5.10").IsNewerThan("5.10-beta"); // true
new SemanticVersion("5.1").IsBetween("5.0", "5.2"); // true

Note that game versions before 1.2.0 and some mod versions are non-standard (e.g. Stardew Valley 1.11 comes before 1.2). All SMAPI versions are standard.

Mod-provided APIs

Mods can provide their own APIs to other mods, even without a dependency or assembly reference. This can be used to integrate mods, provide custom information, or provide new framework APIs beyond those offered by SMAPI itself.

Providing an API

To provide an API for other mods to use:

  1. Add a normal class to your mod project with the methods and properties you want to expose.
    public class YourModApi
    {
        public string ExampleProperty { get; set; } = "Example value";
    
        public bool GetThing(string example)
        {
           return true;
        }
    }
    
    (You can use a constructor to initialise the API if desired.)
  2. Override GetApi in your mod's entry class and return an instance of your API:
       public override object GetApi()
       {
          return new YourModApi();
       }
    

That's it! SMAPI will get one instance of your API and cache it.

Notes:

  • GetApi is always called after Entry, so it's safe to pass in your mod's initialised fields.
  • Be careful when changing your API! If you make a breaking change, other mods may need an update before they can access your API again.
  • You can optionally add a public interface to your API. If a mod directly references your mod DLL, they'll be able to use your interface instead of creating their own.
  • Known issue: SMAPI doesn't currently support non-public API classes.

Using an API

You can use a mod-provided API by mapping it to an interface:

  1. Create an interface with only the properties and methods you want to access. (If you directly reference the mod DLL and it provides a public API interface, you can use that instead.)
    internal interface ISomeModApi
    {
       bool GetThing(string example);
    }
    
  2. In your mod code (after Entry), you can get the API by specifying the interface you created in step 1, and the mod's unique ID:
    ISomeModApi api = helper.ModRegistry.GetApi<ISomeModApi>("other-mod-ID");
    if (api != null)
       bool result = api.GetThing("boop");
    

For a quick integration, you can also use reflection instead of creating an interface:

object api = helper.ModRegistry.GetApi("other-mod-id");
if (api != null)
   bool result = helper.Reflection.GetMethod(api, "GetThing").Invoke<bool>("boop");

Notes:

  • You can't call GetApi until all mods are initialised and their Entry methods called. You can use the GameEvents.FirstUpdateTick event if you need to access mod APIs early; this is guaranteed to happen after all mods are initialised.
  • You should always null-check APIs you consume. GetApi will return null if the API isn't available (e.g. because the mod isn't already installed or doesn't have one), or if an error occurs fetching the API.