Difference between revisions of "Modding:Modder Guide/APIs/Content"

From Stardew Valley Wiki
Jump to navigation Jump to search
(→‎Compare asset names: update for SMAPI 3.7 release)
m (Replace deprecated <source> tags with <syntaxhighlight> tags)
Line 26: Line 26:
 
===Read mod assets===
 
===Read mod assets===
 
You can read custom assets from your mod folder by specifying its path (relative to your mod folder) and type. For example:
 
You can read custom assets from your mod folder by specifying its path (relative to your mod folder) and type. For example:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
// read an image file
 
// read an image file
 
Texture2D texture = helper.Content.Load<Texture2D>("assets/texture.png", ContentSource.ModFolder);
 
Texture2D texture = helper.Content.Load<Texture2D>("assets/texture.png", ContentSource.ModFolder);
Line 35: Line 35:
 
// read a data file
 
// read a data file
 
IDictionary<string, string> data = helper.Content.Load<Dictionary<string, string>>("assets/data.json", ContentSource.ModFolder);
 
IDictionary<string, string> data = helper.Content.Load<Dictionary<string, string>>("assets/data.json", ContentSource.ModFolder);
</source>
+
</syntaxhighlight>
  
 
The supported file types are...
 
The supported file types are...
Line 69: Line 69:
 
===Get actual mod asset keys===
 
===Get actual mod asset keys===
 
When you load an asset from your mod folder, SMAPI stores it with an asset name that uniquely identifies it. If you need to pass the asset name to game code, you can retrieve the actual asset name:
 
When you load an asset from your mod folder, SMAPI stores it with an asset name that uniquely identifies it. If you need to pass the asset name to game code, you can retrieve the actual asset name:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
tilesheet.ImageSource = helper.Content.GetActualAssetKey("assets/tilesheet.png", ContentSource.ModFolder);
 
tilesheet.ImageSource = helper.Content.GetActualAssetKey("assets/tilesheet.png", ContentSource.ModFolder);
</source>
+
</syntaxhighlight>
  
 
===Read content assets===
 
===Read content assets===
 
You can also read assets from the game folder:
 
You can also read assets from the game folder:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
Texture2D portraits = helper.Content.Load<Texture2D>("Portraits/Abigail", ContentSource.GameContent);
 
Texture2D portraits = helper.Content.Load<Texture2D>("Portraits/Abigail", ContentSource.GameContent);
</source>
+
</syntaxhighlight>
  
 
Note that this requires the asset name, ''not'' a filename. You can get the asset name by taking the path relative to the <tt>Content</tt> folder, and removing the language code and <tt>.xnb</tt> extension. See [[#What's an 'asset'?]].
 
Note that this requires the asset name, ''not'' a filename. You can get the asset name by taking the path relative to the <tt>Content</tt> folder, and removing the language code and <tt>.xnb</tt> extension. See [[#What's an 'asset'?]].
Line 89: Line 89:
 
For example, here's a mod which replaces Abigail's portraits with a custom version from its mod folder:
 
For example, here's a mod which replaces Abigail's portraits with a custom version from its mod folder:
  
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
public class ModEntry : Mod, IAssetLoader
 
public class ModEntry : Mod, IAssetLoader
 
{
 
{
Line 116: Line 116:
 
     }
 
     }
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
===Replace a map file===
 
===Replace a map file===
Line 135: Line 135:
 
You can load the custom map like this:
 
You can load the custom map like this:
  
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
public class ModEntry : Mod, IAssetLoader
 
public class ModEntry : Mod, IAssetLoader
 
{
 
{
Line 152: Line 152:
 
     }
 
     }
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
That's it! SMAPI will detect a reference to <tt>spring_customTilesheet.png</tt>, find the file relative to the map file, and load it too. When the season changes in-game, SMAPI will automatically switch it to <tt>summer_customTilesheet.png</tt>, etc. The other tilesheet references will be left untouched (since there's no local file), and use the Content files.
 
That's it! SMAPI will detect a reference to <tt>spring_customTilesheet.png</tt>, find the file relative to the map file, and load it too. When the season changes in-game, SMAPI will automatically switch it to <tt>summer_customTilesheet.png</tt>, etc. The other tilesheet references will be left untouched (since there's no local file), and use the Content files.
Line 158: Line 158:
 
===Add a new asset===
 
===Add a new asset===
 
Providing a new asset is exactly like replacing an existing one (see previous section). For example, this code adds a new dialogue file for a custom NPC:
 
Providing a new asset is exactly like replacing an existing one (see previous section). For example, this code adds a new dialogue file for a custom NPC:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
public class ModEntry : Mod, IAssetLoader
 
public class ModEntry : Mod, IAssetLoader
 
{
 
{
Line 178: Line 178:
 
     }
 
     }
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
===Disadvantages===
 
===Disadvantages===
Line 188: Line 188:
  
 
For example, here's a mod which doubles the sale price of all items:
 
For example, here's a mod which doubles the sale price of all items:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
public class ModEntry : Mod, IAssetEditor
 
public class ModEntry : Mod, IAssetEditor
 
{
 
{
Line 219: Line 219:
 
     }
 
     }
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
The <tt>IAssetData asset</tt> argument for your <tt>Edit</tt> method has some helpers to make editing data easier, documented below. (See IntelliSense for the <tt>asset</tt> argument for more info.)
 
The <tt>IAssetData asset</tt> argument for your <tt>Edit</tt> method has some helpers to make editing data easier, documented below. (See IntelliSense for the <tt>asset</tt> argument for more info.)
Line 234: Line 234:
 
===Edit a dictionary===
 
===Edit a dictionary===
 
A ''dictionary'' is a key/value data structure, represented like this in JSON exports:
 
A ''dictionary'' is a key/value data structure, represented like this in JSON exports:
<source lang="json">
+
<syntaxhighlight lang="json">
 
{
 
{
 
   "key A": "value A",
 
   "key A": "value A",
Line 240: Line 240:
 
   ...
 
   ...
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
You can get a dictionary helper using <tt>asset.AsDictionary<TKey, string>()</tt>, where <tt>TKey</tt> is replaced with the key type (usually <tt>int</tt> or <tt>string</tt>).
 
You can get a dictionary helper using <tt>asset.AsDictionary<TKey, string>()</tt>, where <tt>TKey</tt> is replaced with the key type (usually <tt>int</tt> or <tt>string</tt>).
Line 246: Line 246:
 
; Data
 
; Data
 
: A reference to the loaded data. For example, here's how to add or replace a specific entry to the above example:
 
: A reference to the loaded data. For example, here's how to add or replace a specific entry to the above example:
: <source lang="C#">
+
: <syntaxhighlight lang="C#">
 
public void Edit<T>(IAssetData asset)
 
public void Edit<T>(IAssetData asset)
 
{
 
{
Line 252: Line 252:
 
   editor.Data["Key C"] = "Value C";
 
   editor.Data["Key C"] = "Value C";
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
===Edit an image===
 
===Edit an image===
Line 262: Line 262:
 
; PatchImage
 
; PatchImage
 
: Edit or replace part of the image. This is basically a copy & paste operation, so the source texture is applied over the loaded texture. For example:
 
: Edit or replace part of the image. This is basically a copy & paste operation, so the source texture is applied over the loaded texture. For example:
: <source lang="C#">
+
: <syntaxhighlight lang="C#">
 
public void Edit<T>(IAssetData asset)
 
public void Edit<T>(IAssetData asset)
 
{
 
{
Line 270: Line 270:
 
   editor.PatchImage(sourceImage, targetArea: new Rectangle(300, 100, 200, 200));
 
   editor.PatchImage(sourceImage, targetArea: new Rectangle(300, 100, 200, 200));
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
: Available method arguments:
 
: Available method arguments:
Line 295: Line 295:
 
; ExtendImage
 
; ExtendImage
 
: Extend the image if needed to fit the given size. Note that '''this is an expensive operation''', creates a new texture instance, and extending a spritesheet horizontally may cause game errors or bugs. For example:
 
: Extend the image if needed to fit the given size. Note that '''this is an expensive operation''', creates a new texture instance, and extending a spritesheet horizontally may cause game errors or bugs. For example:
: <source lang="C#">
+
: <syntaxhighlight lang="C#">
 
public void Edit<T>(IAssetData asset)
 
public void Edit<T>(IAssetData asset)
 
{
 
{
Line 303: Line 303:
 
   editor.ExtendImage(minWidth: editor.Data.Width, minHeight: 1000);
 
   editor.ExtendImage(minWidth: editor.Data.Width, minHeight: 1000);
 
}
 
}
</source>
+
</syntaxhighlight>
 
: Available method arguments:
 
: Available method arguments:
 
: {| class="wikitable"
 
: {| class="wikitable"
Line 325: Line 325:
 
; PatchMap
 
; PatchMap
 
: Edit or replace part of the map. This is basically a copy & paste operation, so the source map is applied over the loaded map. For example:
 
: Edit or replace part of the map. This is basically a copy & paste operation, so the source map is applied over the loaded map. For example:
: <source lang="C#">
+
: <syntaxhighlight lang="C#">
 
public void Edit<T>(IAssetData asset)
 
public void Edit<T>(IAssetData asset)
 
{
 
{
Line 333: Line 333:
 
   editor.PatchMap(sourceMap, targetArea: new Rectangle(30, 10, 20, 20));
 
   editor.PatchMap(sourceMap, targetArea: new Rectangle(30, 10, 20, 20));
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
: Available method arguments:
 
: Available method arguments:
Line 358: Line 358:
  
 
If you need to check manually, you should normalize the asset names using [[Modding:Modder Guide/APIs/Utilities#File paths|<tt>PathUtilities</tt>]] and compare case-insensitively. For example:
 
If you need to check manually, you should normalize the asset names using [[Modding:Modder Guide/APIs/Utilities#File paths|<tt>PathUtilities</tt>]] and compare case-insensitively. For example:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
string dialoguePrefix = PathUtilities.NormalizePath(@"Characters\Dialogue\");
 
string dialoguePrefix = PathUtilities.NormalizePath(@"Characters\Dialogue\");
 
bool isDialogue = asset.AssetName.StartsWith(dialoguePrefix, StringComparison.OrdinalIgnoreCase);
 
bool isDialogue = asset.AssetName.StartsWith(dialoguePrefix, StringComparison.OrdinalIgnoreCase);
</source>
+
</syntaxhighlight>
  
 
===Cache invalidation===
 
===Cache invalidation===
Line 369: Line 369:
  
 
Typically you'll invalidate a specific asset key:
 
Typically you'll invalidate a specific asset key:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
helper.Content.InvalidateCache("Data/ObjectInformation");
 
helper.Content.InvalidateCache("Data/ObjectInformation");
</source>
+
</syntaxhighlight>
  
 
You can also invalidate assets matching a lambda:
 
You can also invalidate assets matching a lambda:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
helper.Content.InvalidateCache(asset => asset.DataType == typeof(Texture2D) && asset.AssetNameEquals("Data/ObjectInformation"));
 
helper.Content.InvalidateCache(asset => asset.DataType == typeof(Texture2D) && asset.AssetNameEquals("Data/ObjectInformation"));
</source>
+
</syntaxhighlight>
  
 
===Create separate asset editors/loaders===
 
===Create separate asset editors/loaders===
 
All the examples above say to implement <tt>IAssetEditor</tt> or <tt>IAssetLoader</tt> directly on your mod class. That's fine in the vast majority of cases, but you can also provide separate instances instead:
 
All the examples above say to implement <tt>IAssetEditor</tt> or <tt>IAssetLoader</tt> directly on your mod class. That's fine in the vast majority of cases, but you can also provide separate instances instead:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
public class ModEntry : Mod
 
public class ModEntry : Mod
 
{
 
{
Line 391: Line 391:
 
     }
 
     }
 
}
 
}
</source>
+
</syntaxhighlight>
  
 
When you add or remove an asset editor/loader, SMAPI will call their <tt>CanEdit</tt> and <tt>CanLoad</tt> methods for all loaded assets and reload matched assets. This is an expensive process when done outside your <tt>Entry</tt> method, so avoid adding editors/loaders unnecessarily.
 
When you add or remove an asset editor/loader, SMAPI will call their <tt>CanEdit</tt> and <tt>CanLoad</tt> methods for all loaded assets and reload matched assets. This is an expensive process when done outside your <tt>Entry</tt> method, so avoid adding editors/loaders unnecessarily.
Line 399: Line 399:
  
 
You can get a patch helper for arbitrary data. For example, this loads two map files and merges them:
 
You can get a patch helper for arbitrary data. For example, this loads two map files and merges them:
<source lang="c#">
+
<syntaxhighlight lang="c#">
 
Map farm = this.Helper.Content.Load<Map>("assets/farm.tmx", ContentSource.ModFolder);
 
Map farm = this.Helper.Content.Load<Map>("assets/farm.tmx", ContentSource.ModFolder);
 
Map islands = this.Helper.Content.Load<Map>("assets/islands.tmx", ContentSource.ModFolder);
 
Map islands = this.Helper.Content.Load<Map>("assets/islands.tmx", ContentSource.ModFolder);
Line 407: Line 407:
 
   .AsMap()
 
   .AsMap()
 
   .PatchMap(source: islands, targetArea: new Rectangle(0, 26, 56, 49));
 
   .PatchMap(source: islands, targetArea: new Rectangle(0, 26, 56, 49));
</source>
+
</syntaxhighlight>
  
 
See [[#Edit a game asset|''edit a game asset'']] for a description of the available patch helpers.
 
See [[#Edit a game asset|''edit a game asset'']] for a description of the available patch helpers.

Revision as of 18:20, 19 February 2021

Creating SMAPI mods SMAPI mascot.png


Modding:Index

The content API lets you read custom assets, or read/edit/replace game assets.

Intro

What's an 'asset'?

An asset is one image, map, or data structure provided to the game. The game stores its default assets in its Content folder, though mods can have custom assets too. For example, all of Abigail's portraits are stored in one asset inside Content\Portraits\Abigail.xnb. If you unpack that file, you'll see it contains an image file:

Modding - creating an XNB mod - example portraits.png

See Editing XNB files for more info about asset files.

What's an 'asset name'?

An asset name identifies an asset. For a Content file, this is the file path relative to Content without the .xnb extension or language. For example, Content\Maps\Desert.xnb and Content\Maps\Desert.ja-JA.xnb both have asset name Maps\Desert.

What does the content API do?

SMAPI handles content loading for the game. This lets you...

  • read data, images, or maps from your mod folder (with support for .json, .png, .tbin, .tmx, and .xnb files);
  • read assets from the game's Content folder;
  • make changes to game assets (without changing the actual files);
  • provide new assets to the game.

The rest of this page explains each one in more detail.

Read assets

Read mod assets

You can read custom assets from your mod folder by specifying its path (relative to your mod folder) and type. For example:

// read an image file
Texture2D texture = helper.Content.Load<Texture2D>("assets/texture.png", ContentSource.ModFolder);

// read a map file
Map map = helper.Content.Load<Map>("assets/map.tmx", ContentSource.ModFolder);

// read a data file
IDictionary<string, string> data = helper.Content.Load<Dictionary<string, string>>("assets/data.json", ContentSource.ModFolder);

The supported file types are...

file extension in-game type notes
.xnb any A packed file, like those in the game's Content folder. Not recommended since it's harder to edit and maintain.
.json any A data file, typically used to store Dictionary<int, string> or Dictionary<string, string> data.
.png Texture2D An image file. You can use this to load textures, spritesheets, tilesheets, etc.
.tbin or .tmx xTile.Map A map file, which can be used to create or modify an in-game location. SMAPI will automatically match tilesheets to image files in the same folder as the map if they exist; otherwise the game will check the Content folders for them.

Some usage notes:

  • The normal convention is to have them in an assets subfolder, though that's not required.
  • Don't worry about which path separators you use; SMAPI will normalise them automatically.
  • To avoid performance issues, don't call content.Load<T> repeatedly in draw code. Instead, load your asset once and reuse it.

Get actual mod asset keys

When you load an asset from your mod folder, SMAPI stores it with an asset name that uniquely identifies it. If you need to pass the asset name to game code, you can retrieve the actual asset name:

tilesheet.ImageSource = helper.Content.GetActualAssetKey("assets/tilesheet.png", ContentSource.ModFolder);

Read content assets

You can also read assets from the game folder:

Texture2D portraits = helper.Content.Load<Texture2D>("Portraits/Abigail", ContentSource.GameContent);

Note that this requires the asset name, not a filename. You can get the asset name by taking the path relative to the Content folder, and removing the language code and .xnb extension. See #What's an 'asset'?.

Replace a game asset

Basics

You can replace an asset entirely by providing the asset to SMAPI yourself. The original file won't be read at all and won't be changed.

You can do this by implementing IAssetLoader in your Mod class. This adds two methods: CanLoad<T> returns whether the mod can provide a particular asset, and Load<T> provides the asset data. SMAPI will call your CanLoad<T> every time an asset is loaded (which may happen multiple times per asset), then call Load<T> if it returns true. (If multiple mods return true from CanLoad<T> for the same asset, SMAPI will display an error and not call Load<T> for any of them.)

For example, here's a mod which replaces Abigail's portraits with a custom version from its mod folder:

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)
    {
        if (asset.AssetNameEquals("Portraits/Abigail"))
        {
            return true;
        }

        return false;
    }

    /// <summary>Load a matched asset.</summary>
    /// <param name="asset">Basic metadata about the asset being loaded.</param>
    public T Load<T>(IAssetInfo asset)
    {
        if (asset.AssetNameEquals("Portraits/Abigail"))
        {
            return this.Helper.Content.Load<T>("assets/abigail-portaits.png", ContentSource.ModFolder);
        }

        throw new InvalidOperationException($"Unexpected asset '{asset.AssetName}'.");
    }
}

Replace a map file

You can use IAssetLoader to load custom maps too. When you load a map file, and an unpacked tilesheet is present in the mod folder (relative to the map file), SMAPI will automatically link the map to that file and handle loading it too. If the tilesheet filename starts with a season and underscore, the game will apply its normal seasonal logic to it too.

For example, let's say you have a mod with this structure:

ExampleMapMod.dll
manifest.json
assets/
   Farm.tmx
   fall_customTilesheet.png
   spring_customTilesheet.png
   summer_customTilesheet.png
   winter_customTilesheet.png

You can load the custom map like this:

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("Maps/Farm");
    }

    /// <summary>Load a matched asset.</summary>
    /// <param name="asset">Basic metadata about the asset being loaded.</param>
    public T Load<T>(IAssetInfo asset)
    {
        return this.Helper.Content.Load<T>("assets/Farm.tmx");
    }
}

That's it! SMAPI will detect a reference to spring_customTilesheet.png, find the file relative to the map file, and load it too. When the season changes in-game, SMAPI will automatically switch it to summer_customTilesheet.png, etc. The other tilesheet references will be left untouched (since there's no local file), and use the Content files.

Add a new asset

Providing a new asset is exactly like replacing an existing one (see previous section). 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) converts a known type to the generic 'T' placeholder
        {
            ["Introduction"] = "Hi there! My name is Jonathan."
        };
    }
}

Disadvantages

Logically there's only one initial version of an asset. If multiple mods want to provide the same asset, SMAPI will show an error and reject all of them. In some cases this is still the best approach, but if you're only changing part of the asset see edit a game assets below instead.

Edit a game asset

Basics

You can edit any game asset after it's loaded (but before it's provided to the game), 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. SMAPI will call your CanEdit<T> every time an asset is loaded (which may happen multiple times per asset), then call Edit<T> if it returns true.

For example, here's a mod which doubles the sale price of all items:

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)
    {
        if (asset.AssetNameEquals("Data/ObjectInformation"))
        {
            return true;
        }

        return false;
    }

    /// <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)
    {
        if (asset.AssetNameEquals("Data/ObjectInformation"))
        {
            IDictionary<int, string> data = asset.AsDictionary<int, string>().Data;
            foreach (int itemID in data.Keys)
            {
                string[] fields = data[itemID].Split('/');
                fields[1] = (int.Parse(fields[1]) * 2).ToString();
                data[itemID] = string.Join("/", fields);
            }
        }
    }
}

The IAssetData asset argument for your Edit method has some helpers to make editing data easier, documented below. (See IntelliSense for the asset argument for more info.)

Edit any file

These fields/methods are available directly on the asset received by your Edit method for any asset type, and also available through the helpers listed below.

Data
A reference to the loaded asset data.
ReplaceWith
Replace the entire asset with a new version. You shouldn't do that in most cases though; see replace a game asset instead, or use one of the helpers below.

Edit a dictionary

A dictionary is a key/value data structure, represented like this in JSON exports:

{
   "key A": "value A",
   "key B": "value B",
   ...
}

You can get a dictionary helper using asset.AsDictionary<TKey, string>(), where TKey is replaced with the key type (usually int or string).

Data
A reference to the loaded data. For example, here's how to add or replace a specific entry to the above example:
public void Edit<T>(IAssetData asset)
{
   var editor = asset.AsDictionary<string, string>();
   editor.Data["Key C"] = "Value C";
}

Edit an image

When editing an image file, you can get a helper using asset.AsImage().

Data
A reference to the loaded image. You can directly edit each pixel in the image through this field, though that's rarely needed.
PatchImage
Edit or replace part of the image. This is basically a copy & paste operation, so the source texture is applied over the loaded texture. For example:
public void Edit<T>(IAssetData asset)
{
   var editor = asset.AsImage();
   
   Texture2D sourceImage = this.Helper.Content.Load<Texture2D>("custom-texture.png", ContentSource.ModFolder);
   editor.PatchImage(sourceImage, targetArea: new Rectangle(300, 100, 200, 200));
}
Available method arguments:
argument usage
source The source image to copy & paste onto the loaded image.
sourceArea (optional) The pixel area within the source image to copy (or omit to use the entire source image). This must fit within the target image.
targetArea (optional) The pixel area within the loaded image to replace (or omit to replace starting from the top-left corner up to the full source size).
patchMode (optional) How the image should be patched. The possible values...
  • PatchMode.Replace (default): erase the original content within the area before pasting in the new content;
  • PatchMode.Overlay: draw the new content over the original content, so the original content shows through any fully transparent pixels.
ExtendImage
Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and extending a spritesheet horizontally may cause game errors or bugs. For example:
public void Edit<T>(IAssetData asset)
{
   var editor = asset.AsImage();

   // make sure the image is at least 1000px high
   editor.ExtendImage(minWidth: editor.Data.Width, minHeight: 1000);
}
Available method arguments:
argument usage
minWidth The minimum desired width. If the image width is less than this value, it'll be extended on the right up to that size.
minHeight The minimum desired height. If the image height is less than this value, it'll be extended from the bottom up to that size.

Edit a map

When editing a map file, you can get a helper using asset.AsMap().

Data
A reference to the loaded map. You can directly edit the map or tiles through this field.
PatchMap
Edit or replace part of the map. This is basically a copy & paste operation, so the source map is applied over the loaded map. For example:
public void Edit<T>(IAssetData asset)
{
   var editor = asset.AsMap();
   
   Map sourceMap = this.Helper.Content.Load<Map>("custom-map.tmx", ContentSource.ModFolder);
   editor.PatchMap(sourceMap, targetArea: new Rectangle(30, 10, 20, 20));
}
Available method arguments:
argument usage
source The source map to copy & paste onto the loaded map.
sourceArea (optional) The tile area within the source map to copy (or omit to use the entire source map). This must fit within the target map.
targetArea (optional) The tile area within the loaded map to replace (or omit to replace starting from the top-left corner up to the full source size).

Advanced

Compare asset names

You can't compare asset names directly because they depend on the current OS (e.g. Characters/Abigail versus Characters\Abigail) and they're case-insensitive (e.g. Characters/ABIGAIL).

To check for an exact match when you have an IAssetData instance (e.g. from the content API), just call asset.AssetNameEquals("Characters/Abigail") and SMAPI will normalize the asset names internally.

If you need to check manually, you should normalize the asset names using PathUtilities and compare case-insensitively. For example:

string dialoguePrefix = PathUtilities.NormalizePath(@"Characters\Dialogue\");
bool isDialogue = asset.AssetName.StartsWith(dialoguePrefix, StringComparison.OrdinalIgnoreCase);

Cache invalidation

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 automatically update references to the asset in many cases. For example, this lets you change what clothes an NPC is wearing (by invalidating their cached sprites or portraits).

Reloading assets is fairly expensive, so use this API judiciously to avoid impacting game performance. Definitely don't do this every update tick.

Typically you'll invalidate a specific asset key:

helper.Content.InvalidateCache("Data/ObjectInformation");

You can also invalidate assets matching a lambda:

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

Create separate asset editors/loaders

All the examples above say to implement IAssetEditor or IAssetLoader directly on your mod class. That's fine in the vast majority of cases, but you can also provide separate instances instead:

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());
        helper.Content.AssetLoaders.Add(new MyDialogueLoader());
    }
}

When you add or remove an asset editor/loader, SMAPI will call their CanEdit and CanLoad methods for all loaded assets and reload matched assets. This is an expensive process when done outside your Entry method, so avoid adding editors/loaders unnecessarily.

Patch helper for custom assets

A patch helper provides utility methods for editing a given asset (e.g. to merge maps or resize an image).

You can get a patch helper for arbitrary data. For example, this loads two map files and merges them:

Map farm = this.Helper.Content.Load<Map>("assets/farm.tmx", ContentSource.ModFolder);
Map islands = this.Helper.Content.Load<Map>("assets/islands.tmx", ContentSource.ModFolder);

this.Helper.Content
   .GetPatchHelper(farm)
   .AsMap()
   .PatchMap(source: islands, targetArea: new Rectangle(0, 26, 56, 49));

See edit a game asset for a description of the available patch helpers.