Difference between revisions of "Modding:Migrate to SMAPI 4.0"
Pathoschild (talk | contribs) (→Changes: + Nullable reference type annotations) |
|||
(11 intermediate revisions by one other user not shown) | |||
Line 2: | Line 2: | ||
{{Modder compatibility header}} | {{Modder compatibility header}} | ||
− | |||
− | This page explains how to update your mod code for compatibility with SMAPI 4.0.0. | + | 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. |
==Overview== | ==Overview== | ||
Line 10: | Line 9: | ||
[[File:SMAPI compatibility.png|thumb|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.]] | [[File:SMAPI compatibility.png|thumb|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 [[Modding:Modder Guide/APIs/Content|content interception API]] (''i.e.'' <samp>IAssetLoader</samp> and <samp>IAssetEditor</samp>) was introduced five years ago in [https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes-archived.md#20 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 | + | The [[Modding:Modder Guide/APIs/Content|content interception API]] (''i.e.'' <samp>IAssetLoader</samp> and <samp>IAssetEditor</samp>) was introduced five years ago in [https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes-archived.md#20 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 41.1% 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: | SMAPI 4.0.0 is the release that fixes that. This completely redesigns the content API: | ||
Line 31: | Line 30: | ||
===How to update your mod=== | ===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 | + | You don't need to comb through your code manually. SMAPI can tell you if you're using a deprecated API: |
− | # | + | # SMAPI will show deprecation messages in the console window (the exact format changes depending on the deprecation level, but you can just search for your mod name):<br />[[File:Modding - updating deprecated SMAPI code - deprecation warnings.png]] |
− | # When you look at the code, you'll see | + | # When you look at the code in Visual Studio, you'll see build warnings with hints on how to fix them:<br />[[File:Modding - updating deprecated SMAPI code - deprecation intellisense.png]] |
− | # You can refer to the following sections on how to replace specific | + | # You can refer to the following sections on how to replace specific APIs. |
− | == | + | ==Breaking changes== |
===Content interception API=== | ===Content interception API=== | ||
The <samp>IAssetLoader</samp> and <samp>IAssetEditor</samp> interfaces no longer exist. Both have been replaced by the [[Modding:Modder Guide/APIs/Events#Content.AssetRequested|<samp>AssetRequested</samp> event]], which is used like this: | The <samp>IAssetLoader</samp> and <samp>IAssetEditor</samp> interfaces no longer exist. Both have been replaced by the [[Modding:Modder Guide/APIs/Events#Content.AssetRequested|<samp>AssetRequested</samp> event]], which is used like this: | ||
Line 61: | Line 60: | ||
} | } | ||
} | } | ||
+ | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 68: | Line 68: | ||
* When loading an asset, you must now specify an <samp>AssetLoadPriority</samp> which decides what happens if two loads apply to the same asset. <samp>AssetLoadPriority.Exclusive</samp> matches the previous behavior, but may reduce mod compatibility. See the IntelliSense documentation for more info. | * When loading an asset, you must now specify an <samp>AssetLoadPriority</samp> which decides what happens if two loads apply to the same asset. <samp>AssetLoadPriority.Exclusive</samp> matches the previous behavior, but may reduce mod compatibility. See the IntelliSense documentation for more info. | ||
− | See the [[Modding:Modder Guide/APIs/Events#Content|content | + | See the [[Modding:Modder Guide/APIs/Events#Content|content events]] and [[Modding:Modder Guide/APIs/Content|content API]] docs for more info on how to use them. |
===Content loading API=== | ===Content loading API=== | ||
Line 108: | Line 108: | ||
| <samp>helper.Content.Load</samp> | | <samp>helper.Content.Load</samp> | ||
| Use <samp>helper.GameContent</samp> or <samp>helper.ModContent</samp>, and remove the <samp>ContentSource</samp> parameter. | | Use <samp>helper.GameContent</samp> or <samp>helper.ModContent</samp>, and remove the <samp>ContentSource</samp> parameter. | ||
+ | |||
+ | Migration notes: | ||
+ | * When loading assets from <samp>helper.GameContent</samp>, don't add a <samp>.xnb</samp> file extension (''e.g.'' use <code>"Portraits/Abigail"</code> instead of <code>"Portraits/Abigail.xnb"</code>). You're requesting an [[Modding:Modder Guide/APIs/Content#What's an 'asset name'?|asset name]], not a file path. | ||
+ | * When loading XNB files from <samp>helper.ModContent</samp>, ''do'' add the <samp>.xnb</samp> file extension. It's no longer added automatically if needed. | ||
|- | |- | ||
| <samp>helper.Content.NormalizeAssetName</samp> | | <samp>helper.Content.NormalizeAssetName</samp> | ||
Line 139: | Line 143: | ||
| <samp>IContentPack.GetActualAssetKey</samp> | | <samp>IContentPack.GetActualAssetKey</samp> | ||
| Use <samp>ModContent.GetInternalAssetName</samp>, and remove the <samp>ContentSource</samp> parameter. This returns an <samp>IAssetName</samp> value; you can update your code to use that, or get the string value using its <samp>Name</samp> property. | | Use <samp>ModContent.GetInternalAssetName</samp>, and remove the <samp>ContentSource</samp> parameter. This returns an <samp>IAssetName</samp> value; you can update your code to use that, or get the string value using its <samp>Name</samp> property. | ||
+ | |- | ||
+ | | <samp>PerScreen<T>(null)</samp> | ||
+ | | Passing null into the constructor is deprecated. You should call <code>PerScreen<T>()</code> to use the default value. | ||
+ | |- | ||
+ | | <samp>SDate.Season</samp> | ||
+ | | <samp>SDate.Season</samp> is now the <samp>Season</samp> enum, to match the game. Use <samp>SDate.SeasonKey</samp> if you absolutely need the string form. | ||
|} | |} | ||
Line 150: | Line 160: | ||
// warning: possible null reference argument for parameter 'message' | // warning: possible null reference argument for parameter 'message' | ||
string? message = null; | string? message = null; | ||
− | this.Monitor.Log( | + | this.Monitor.Log(message); |
</syntaxhighlight> | </syntaxhighlight> | ||
− | Due to limitations in C# nullable reference annotations, | + | Due to limitations in C# nullable reference annotations, three edge cases aren't fully covered. These are documented in the code IntelliSense too. |
{| class="wikitable" | {| class="wikitable" | ||
|- | |- | ||
Line 160: | Line 170: | ||
|- | |- | ||
| <samp>helper.Reflection</samp> | | <samp>helper.Reflection</samp> | ||
− | | The methods | + | | The <samp>GetField</samp>, <samp>GetMethod</samp>, and <samp>GetProperty</samp> 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 <code>required: false</code>; in that case make sure to null-check the result anyway. |
|- | |- | ||
| <samp>helper.Translation</samp> | | <samp>helper.Translation</samp> | ||
Line 178: | Line 188: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
|} | |} | ||
+ | |||
+ | ===Removed dependencies=== | ||
+ | SMAPI 4.0.0 no longer uses these dependencies, so they won't be loaded automatically anymore. If you manually referenced one of them, either copy it into your mod's release folder or see the suggested migration below. | ||
+ | |||
+ | {| class="wikitable" | ||
+ | |- | ||
+ | ! dependency | ||
+ | ! suggested migration | ||
+ | |- | ||
+ | | <samp>System.Configuration.ConfigurationManager.dll</samp> | ||
+ | | Use the [[Modding:Modder Guide/APIs/Config|standard config API]] instead. | ||
+ | |- | ||
+ | | <samp>System.Runtime.Caching.dll</samp> | ||
+ | | Avoid <samp>MemoryCache</samp> or <samp>ObjectCache</samp> from this DLL, which can negatively impact performance for players. If you need cache expiry, consider the faster (but still heavy) [https://docs.microsoft.com/en-us/dotnet/core/extensions/caching <samp>Microsoft.Extensions.Caching.Memory</samp>] package instead. Otherwise consider using a plain <samp>Dictionary<TKey, TValue></samp> field instead. | ||
+ | |- | ||
+ | | <samp>System.Security.Permissions.dll</samp> | ||
+ | | This is usually only needed for <samp>System.Configuration.ConfigurationManager.dll</samp> or <samp>System.Runtime.Caching.dll</samp>, and can probably be removed. | ||
+ | |} | ||
+ | |||
+ | ==Other changes== | ||
+ | ===Raw texture data=== | ||
+ | Creating <samp>Texture2D</samp> instances is expensive and involves calls to the graphics card. When you don't need a full texture, you can now load it as <samp>IRawTextureData</samp> instead, and then pass that into SMAPI APIs that accept textures. | ||
+ | |||
+ | For example, you no longer need to create a <samp>Texture2D</samp> instance to apply an image overlay: | ||
+ | <syntaxhighlight lang="c#"> | ||
+ | private void OnAssetRequested(object? sender, AssetRequestedEventArgs e) | ||
+ | { | ||
+ | if (e.Name.IsEquivalentTo("Portraits/Abigail")) | ||
+ | { | ||
+ | e.Edit(asset => | ||
+ | { | ||
+ | IRawTextureData ribbon = this.Helper.ModContent.Load<IRawTextureData>("assets/ribbon.png"); | ||
+ | asset.AsImage().PatchImage(source: ribbon); | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
[[Category:Modding]] | [[Category:Modding]] |
Latest revision as of 09:53, 23 August 2023
This page is for mod authors. 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.
Overview
What's changing?
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 41.1% 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/Bundles.fr-FR 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 API:
- SMAPI will show deprecation messages in the console window (the exact format changes depending on the deprecation level, but you can just search for your mod name):
- When you look at the code in Visual Studio, you'll see build warnings with hints on how to fix them:
- You can refer to the following sections on how to replace specific APIs.
Breaking changes
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/Bundles.fr-FR 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 |
---|---|
helper.Content.AssetEditors helper.Content.AssetLoaders |
Use content events. |
helper.Content.CurrentLocale helper.Content.CurrentLocaleConstant helper.Content.InvalidateCache |
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:
|
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.
|
SDate.Season | SDate.Season is now the Season enum, to match the game. Use SDate.SeasonKey if you absolutely need the string form. |
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");
api.DoSomething();
// warning: possible null reference argument for parameter 'message'
string? message = null;
this.Monitor.Log(message);
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
|
Removed dependencies
SMAPI 4.0.0 no longer uses these dependencies, so they won't be loaded automatically anymore. If you manually referenced one of them, either copy it into your mod's release folder or see the suggested migration below.
dependency | suggested migration |
---|---|
System.Configuration.ConfigurationManager.dll | Use the standard config API instead. |
System.Runtime.Caching.dll | Avoid MemoryCache or ObjectCache from this DLL, which can negatively impact performance for players. If you need cache expiry, consider the faster (but still heavy) Microsoft.Extensions.Caching.Memory package instead. Otherwise consider using a plain Dictionary<TKey, TValue> field instead. |
System.Security.Permissions.dll | This is usually only needed for System.Configuration.ConfigurationManager.dll or System.Runtime.Caching.dll, and can probably be removed. |
Other changes
Raw texture data
Creating Texture2D instances is expensive and involves calls to the graphics card. When you don't need a full texture, you can now load it as IRawTextureData instead, and then pass that into SMAPI APIs that accept textures.
For example, you no longer need to create a Texture2D instance to apply an image overlay:
private void OnAssetRequested(object? sender, AssetRequestedEventArgs e)
{
if (e.Name.IsEquivalentTo("Portraits/Abigail"))
{
e.Edit(asset =>
{
IRawTextureData ribbon = this.Helper.ModContent.Load<IRawTextureData>("assets/ribbon.png");
asset.AsImage().PatchImage(source: ribbon);
});
}
}