Difference between revisions of "Modding:Migrate to SMAPI 4.0"

From Stardew Valley Wiki
Jump to navigation Jump to search
(create initial page)
 
 
(19 intermediate revisions by one other user not shown)
Line 2: Line 2:
  
 
{{Modder compatibility header}}
 
{{Modder compatibility header}}
{{SMAPI upcoming|4.0.0}}
 
  
This page explains how to update your mod code for compatibility with SMAPI 4.0.0. See also [[Modding:Migrate to Stardew Valley 1.6|''Migrate to Stardew Valley 1.6'']].
+
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 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.
+
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 22: Line 21:
  
 
===Is this the modapocalypse?===
 
===Is this the modapocalypse?===
Nope. Although this is a major change, significant effort were undertaken to minimize the impact:
+
Nope. Although this is a major change, significant efforts will be undertaken to minimize the impact:
* the old content API was supported for a long time with increasingly prominent warnings in the SMAPI console about its deprecation and removal;
+
* 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 were submitted to update affected open-source mods;
+
* pull requests will be submitted to update affected open-source mods;
* unofficial updates were created for mods which haven't updated officially by the time SMAPI 4.0.0 was released;
+
* unofficial updates will be created for mods which haven't updated officially by the time SMAPI 4.0.0 was released;
* the changes were actively communicated and documented to modders.
+
* 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.
 
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===
 
===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:
+
You don't need to comb through your code manually. SMAPI can tell you if you're using a deprecated API:
  
# Use the latest [https://smapi.io/ SMAPI for developers] download. This will show deprecation messages in the console:<br />[[File:Modding - updating deprecated SMAPI code - deprecation warnings.png]]
+
# 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 a deprecation warning with a hint of how to fix it:<br />[[File:Modding - updating deprecated SMAPI code - deprecation intellisense.png]]
+
# 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 interfaces.
+
# You can refer to the following sections on how to replace specific APIs.
  
==Changes==
+
==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 54: Line 53:
 
     /// <param name="sender">The event sender.</param>
 
     /// <param name="sender">The event sender.</param>
 
     /// <param name="e">The event arguments.</param>
 
     /// <param name="e">The event arguments.</param>
     private void OnAssetRequested(object? sender, AssetRequestedEventArgs e)
+
     private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
 
     {
 
     {
 
         if (e.Name.IsEquivalentTo("Portraits/Abigail"))
 
         if (e.Name.IsEquivalentTo("Portraits/Abigail"))
Line 61: Line 60:
 
         }
 
         }
 
     }
 
     }
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
Migration tips:
 
Migration tips:
* The old <samp>CanLoad</samp>/<samp>CanEdit</samp> and <samp>CanEdit</samp>/<samp>Edit</samp> methods have been combined, so you only need to check any conditional logic once.
+
* '''Asset names are no longer locale-agnostic.''' For example, <samp>Data/Bundles</samp> and <samp>Data/Bundles.fr-FR</samp> are ''not'' equivalent. If you want to apply changes regardless of the locale, check <samp>e.NameWithoutLocale</samp> instead of <samp>e.Name</samp>.
* 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. See the IntelliSense documentation for more info.
+
* The old <samp>CanLoad</samp>/<samp>Load</samp> and <samp>CanEdit</samp>/<samp>Edit</samp> methods have been combined, so you only need to check any conditional logic once.
 +
* 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 event docs]] for more info on how to use them.
+
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.
  
===Custom command triggers no longer allowed===
+
===Content loading API===
You can no longer manually trigger a console command using <samp>helper.ConsoleCommands.Trigger</samp>. You should use [[Modding:Modder Guide/APIs/Integrations#Mod-provided APIs|mod-provided APIs]] to integrate with other mods.
+
The <samp>helper.Content</samp> API was confusing, since game content assets and mod files are handled differently. Some methods had an optional <samp>ContentSource</samp> parameter (which was easy to forget to specify), some only made sense for one or the other (like <samp>GetActualAssetKey</samp>), 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 [[#Content interception API|new content events]].
 +
 
 +
It's been split into two APIs to fix those issues:
 +
 
 +
{| class="wikitable"
 +
|-
 +
! field
 +
! notes
 +
|-
 +
| <samp>helper.ModContent</samp>
 +
| Loads assets from your mod's files. These aren't cached (similar to <samp>helper.Content</samp>), so they'll be re-read from the file each time you load them.
 +
|-
 +
| <samp>helper.GameContent</samp>
 +
| Loads assets from the game's <samp>Content</samp> folder or [[#Content interception API|content interception]]. Assets loaded through this ''are'' cached (which is needed for the [[#Content interception API|new content events]] to work).
 +
|}
 +
 
 +
Here's how to migrate existing methods & properties:
 +
 
 +
{| class="wikitable"
 +
|-
 +
! old code
 +
! migration
 +
|-
 +
| <samp>helper.Content.AssetEditors</samp><br /><samp>helper.Content.AssetLoaders</samp>
 +
| Use [[Modding:Modder Guide/APIs/Events#Content|content events]].
 +
|-
 +
| <samp>helper.Content.CurrentLocale</samp><br /><samp>helper.Content.CurrentLocaleConstant</samp><br /><samp>helper.Content.InvalidateCache</samp>
 +
| Use <samp>helper.GameContent</samp>.
 +
|-
 +
| <samp>helper.Content.GetActualAssetKey</samp>
 +
| Use <samp>helper.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>helper.Content.GetPatchHelper</samp>
 +
| Use <samp>helper.GameContent</samp> or <samp>helper.ModContent</samp>.
 +
|-
 +
| <samp>helper.Content.Load</samp>
 +
| 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>
 +
| Use <samp>helper.GameContent.ParseAssetName</samp> instead. 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.
 +
|}
  
 
===Other API changes===
 
===Other API changes===
Line 83: Line 128:
 
| <samp>GameFramework.Xna</samp>
 
| <samp>GameFramework.Xna</samp>
 
| XNA is no longer used on any platform; you can safely remove any XNA-specific logic.
 
| XNA is no longer used on any platform; you can safely remove any XNA-specific logic.
 +
|-
 +
| <samp>helper.ConsoleCommands.Trigger</samp>
 +
| No longer supported. You can use [[Modding:Modder Guide/APIs/Integrations#Mod-provided APIs|mod-provided APIs]] to integrate with other mods.
 
|-
 
|-
 
| <samp>IAssetInfo.AssetName</samp>
 
| <samp>IAssetInfo.AssetName</samp>
| Use <samp>IAssetInfo.Name</samp> instead, which includes built-in utility methods to work with asset names.
+
| Use <samp>Name</samp> instead, which includes built-in utility methods to work with asset names.
 
|-
 
|-
 
| <samp>IAssetInfo.AssetNameEquals(name)</samp>
 
| <samp>IAssetInfo.AssetNameEquals(name)</samp>
| Use <samp>IAssetInfo.Name.IsEquivalentTo(name)</samp> instead.
+
| Use <samp>Name.IsEquivalentTo(name)</samp> instead.
 +
|-
 +
| <samp>IContentPack.LoadAsset</samp>
 +
| Use <samp>ModContent.Load</samp> instead.
 +
|-
 +
| <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.
 +
|-
 +
| <samp>PerScreen&lt;T&gt;(null)</samp>
 +
| Passing null into the constructor is deprecated. You should call <code>PerScreen&lt;T&gt;()</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.
 +
|}
 +
 
 +
===Nullable reference type annotations===
 +
SMAPI is now fully annotated for [https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references 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:
 +
<syntaxhighlight lang="c#">
 +
// 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);
 +
</syntaxhighlight>
 +
 
 +
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"
 +
|-
 +
! API
 +
! edge cases
 +
|-
 +
| <samp>helper.Reflection</samp>
 +
| 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>
 +
| Translations are marked non-nullable, since they fallback to the "''missing translation: key''" message. That doesn't change if you explicitly call <code>translation.UsePlaceholder(false)</code>; in that case make sure to null-check the text anyway if needed.
 +
|-
 +
| <samp>PerScreen&lt;T&gt;</samp>
 +
| This uses the nullability you set, like <code>PerScreen&lt;string&gt;</code> for a non-nullable string or <code>PerScreen&lt;string?&gt;</code> 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:
 +
<syntaxhighlight lang="c#">
 +
var perScreen = new PerScreen<string>();
 +
string value = perScreen.Value; // returns null despite being marked non-nullable
 +
</syntaxhighlight>
 +
 
 +
To avoid that, you can specify the default non-nullable value to use:
 +
<syntaxhighlight lang="c#">
 +
var perScreen = new PerScreen<string>(() => string.Empty);
 +
string value = perScreen.Value; // returns empty string by default
 +
</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&lt;TKey, TValue&gt;</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

Index

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?

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 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:

  1. 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):
    Modding - updating deprecated SMAPI code - deprecation warnings.png
  2. When you look at the code in Visual Studio, you'll see build warnings with hints on how to fix them:
    Modding - updating deprecated SMAPI code - deprecation intellisense.png
  3. 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:

  • 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.
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);
        });
    }
}