Modding:Migrate to SMAPI 4.0

From Stardew Valley Wiki
Jump to navigation Jump to search


This page is for modders. Players: see Modding:Mod compatibility instead.

This page explains how to update your C# mod code for compatibility with SMAPI 4.0.0. (Content packs aren't affected.) You can update mods now, there's no need to wait for the 4.0 release itself.


What's changing?

SMAPI compatibility over time. The SMAPI 2.0 release appears as a small bump in October 2017, and SMAPI 3.0 was released alongside Stardew Valley 1.4.

The content interception API (i.e. IAssetLoader and IAssetEditor) was introduced five years ago in SMAPI 2.0.0. Since then it's become one of the most important parts of SMAPI; for example, it's the basis for Content Patcher which is now the backbone for 39.7% of all mods. However, the API has remained essentially unchanged since its introduction and it doesn't account for all the use cases that apply today.

SMAPI 4.0.0 is the release that fixes that. This completely redesigns the content API:

  • The API is now fully discoverable through helper, just like any other API. That makes it much more intuitive for mod authors.
  • Load operations are no longer always exclusive, since that led to frequent mod conflicts. Instead you can now specify the priority for each load operation.
  • The API no longer hides locale handling — Data/Bundles and Data/ are not equivalent (though you can still apply locale-agnostic changes if needed).
  • Added content pack labels, which let you indicate that your mod is loading/editing an asset on behalf of a content pack. This is reflected in logged messages to simplify troubleshooting, and avoid every error being reported to the framework mod author.
  • Added edit priority, which lets you finetune compatibility with other mods or edits.

SMAPI 4.0.0 also adds compatibility with Stardew Valley 1.6 and drops all deprecated APIs.

Is this the modapocalypse?

Nope. Although this is a major change, significant efforts will be undertaken to minimize the impact:

  • the old content API will be supported for a long time with increasingly prominent warnings in the SMAPI console about its deprecation and removal;
  • pull requests will be submitted to update affected open-source mods;
  • unofficial updates will be created for mods which haven't updated officially by the time SMAPI 4.0.0 was released;
  • the changes will be actively communicated and documented to modders.

All of this means that the 4.0.0 release should have minimal impact on mod compatibility, despite the scope of the changes.

How to update your mod

You don't need to comb through your code manually. SMAPI can tell you if you're using a deprecated interface:

  1. Use the latest SMAPI for developers download. This will show deprecation messages in the console:
    Modding - updating deprecated SMAPI code - deprecation warnings.png
  2. When you look at the code, you'll see a deprecation warning with a hint of how to fix it:
    Modding - updating deprecated SMAPI code - deprecation intellisense.png
  3. You can refer to the following sections on how to replace specific interfaces.


Content interception API

The IAssetLoader and IAssetEditor interfaces no longer exist. Both have been replaced by the AssetRequested event, which is used like this:

public class ModEntry : Mod
    /// <inheritdoc />
    public override void Entry(IModHelper helper)
        this.Helper.Events.Content.AssetRequested += this.OnAssetRequested;

    /// <inheritdoc cref="IContentEvents.AssetRequested" />
    /// <param name="sender">The event sender.</param>
    /// <param name="e">The event arguments.</param>
    private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
        if (e.Name.IsEquivalentTo("Portraits/Abigail"))
            e.LoadFromModFile<Texture2D>("assets/portrait.png", AssetLoadPriority.Medium);

Migration tips:

  • Asset names are no longer locale-agnostic. For example, Data/Bundles and Data/ are not equivalent. If you want to apply changes regardless of the locale, check e.NameWithoutLocale instead of e.Name.
  • The old CanLoad/Load and CanEdit/Edit methods have been combined, so you only need to check any conditional logic once.
  • When loading an asset, you must now specify an AssetLoadPriority which decides what happens if two loads apply to the same asset. AssetLoadPriority.Exclusive matches the previous behavior, but may reduce mod compatibility. See the IntelliSense documentation for more info.

See the content events and content API docs for more info on how to use them.

Content loading API

The helper.Content API was confusing, since game content assets and mod files are handled differently. Some methods had an optional ContentSource parameter (which was easy to forget to specify), some only made sense for one or the other (like GetActualAssetKey), and the documentation tried to handle both by being more abstract. All assets it loaded were also non-cached, which could affect performance and prevented features like the new content events.

It's been split into two APIs to fix those issues:

field notes
helper.ModContent Loads assets from your mod's files. These aren't cached (similar to helper.Content), so they'll be re-read from the file each time you load them.
helper.GameContent Loads assets from the game's Content folder or content interception. Assets loaded through this are cached (which is needed for the new content events to work).

Here's how to migrate existing methods & properties:

old code migration
Use content events.
Use helper.GameContent.
helper.Content.GetActualAssetKey Use helper.ModContent.GetInternalAssetName, and remove the ContentSource parameter. This returns an IAssetName value; you can update your code to use that, or get the string value using its Name property.
helper.Content.GetPatchHelper Use helper.GameContent or helper.ModContent.
helper.Content.Load Use helper.GameContent or helper.ModContent, and remove the ContentSource parameter.

Migration notes:

  • When loading assets from helper.GameContent, don't add a .xnb file extension (e.g. use "Portraits/Abigail" instead of "Portraits/Abigail.xnb"). You're requesting an asset name, not a file path.
  • When loading XNB files from helper.ModContent, do add the .xnb file extension. It's no longer added automatically if needed.
helper.Content.NormalizeAssetName Use helper.GameContent.ParseAssetName instead. This returns an IAssetName value; you can update your code to use that, or get the string value using its Name property.

Other API changes

old code migration
Constants.ExecutionPath Use Constants.GamePath instead.
GameFramework.Xna XNA is no longer used on any platform; you can safely remove any XNA-specific logic.
helper.ConsoleCommands.Trigger No longer supported. You can use mod-provided APIs to integrate with other mods.
IAssetInfo.AssetName Use Name instead, which includes built-in utility methods to work with asset names.
IAssetInfo.AssetNameEquals(name) Use Name.IsEquivalentTo(name) instead.
IContentPack.LoadAsset Use ModContent.Load instead.
IContentPack.GetActualAssetKey Use ModContent.GetInternalAssetName, and remove the ContentSource parameter. This returns an IAssetName value; you can update your code to use that, or get the string value using its Name property.
PerScreen<T>(null) Passing null into the constructor is deprecated. You should call PerScreen<T>() to use the default value.

Nullable reference type annotations

SMAPI is now fully annotated for C# nullable reference types. This has no effect unless you enable them in your mod code too. If your mod does use them, you'll get helpful code analysis warnings from Visual Studio to avoid errors when null values are possible or prohibited. For example:

// warning: dereference of a possibly null reference
var api = this.Helper.ModRegistry.GetApi<IExampleApi>("SomeExample.ModId");

// warning: possible null reference argument for parameter 'message'
string? message = null;

Due to limitations in C# nullable reference annotations, three edge cases aren't fully covered. These are documented in the code IntelliSense too.

API edge cases
helper.Reflection The GetField, GetMethod, and GetProperty methods are marked as returning non-nullable values, since they throw an error if the target isn't found. That doesn't change if you explicitly set required: false; in that case make sure to null-check the result anyway.
helper.Translation Translations are marked non-nullable, since they fallback to the "missing translation: key" message. That doesn't change if you explicitly call translation.UsePlaceholder(false); in that case make sure to null-check the text anyway if needed.
PerScreen<T> This uses the nullability you set, like PerScreen<string> for a non-nullable string or PerScreen<string?> for a nullable one. However, calling the empty constructor with a non-nullable reference type will still create null values since that's the type default. For example:
var perScreen = new PerScreen<string>();
string value = perScreen.Value; // returns null despite being marked non-nullable

To avoid that, you can specify the default non-nullable value to use:

var perScreen = new PerScreen<string>(() => string.Empty);
string value = perScreen.Value; // returns empty string by default