Modding:Modder Guide/APIs/Content

From Stardew Valley Wiki
< Modding:Modder Guide‎ | APIs
Revision as of 17:56, 8 June 2018 by Pathoschild (talk | contribs) (→‎Edit an asset after it's loaded: clarify how CanEdit / Edit are called)
Jump to navigation Jump to search

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 Modding: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 images or maps from your mod folder (with support for .xnb, .png, and .tbin 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. You can load .png images, .tbin maps, or packed .xnb asset files. The normal convention is to have custom asset files in an assets subfolder, though that's not required. To read a file, you specify the type and path relative to your mod folder. For example:

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

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

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

Some usage notes:

  • Don't worry about which path separators you use; SMAPI will normalise them automatically.
  • When you load a .tbin map file, SMAPI will automatically fix any tilesheet references. For example, let's say your map references a file named tilesheet.png; SMAPI will automatically reference a tilesheet.png file in the same folder if it exists, otherwise it will use the game's default Content folders.
  • 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'?.

Edit the game's assets

Edit an asset after it's loaded

You can edit any of the game's assets after it's loaded from the file (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 makes crops grow in any season and changes the winter dirt texture.

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)
    {
        // change crop seasons
        if (asset.AssetNameEquals("Data/Crops"))
            return true;

        // change dirt texture
        if (asset.AssetNameEquals("TerrainFeatures/hoeDirtSnow"))
            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)
    {
        // change crop seasons
        if (asset.AssetNameEquals("Data/Crops"))
        {
            asset
                .AsDictionary<int, string>()
                .Set((id, data) =>
                {
                    string[] fields = data.Split('/');
                    fields[1] = "spring summer fall winter";
                    return string.Join("/", fields);
                });
        }

        // change dirt texture
        else if (asset.AssetNameEquals("TerrainFeatures/hoeDirtSnow"))
            asset.ReplaceWith(this.Helper.Content.Load<Texture2D>("TerrainFeatures/hoeDirt", ContentSource.GameContent));
    }
}

The IAssetData asset argument your Edit method receives has some helpers to make editing data easier.

  1. The above example uses asset.AsDictionary<int, string>().Set(...) to make changes to each entry in the dictionary.
  2. The above example also uses asset.ReplaceWith to replace the entire asset with a new version. (Usually you shouldn't do that though; see Replace an asset entirely below.
  3. You can retrieve the underlying data to change it:
    public void Edit<T>(IAssetData asset)
    {
       IDictionary<int, string> crops = asset.AsDictionary<int, string>().Data;
       crops[999] = "... some new crop string ...";
    }
    
  4. You can paste a custom image into a texture being loaded (e.g. to replace one sprite):
    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));
    }
    

See IntelliSense for the asset argument for more info.

Replace an asset entirely

Sometimes you may need to replace an asset entirely (without changing the original file), not just edit it after it's loaded. In other words, you're providing the asset to SMAPI yourself so the original file won't be read at all. Note that two mods can't both provide the same asset.

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

    /// <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/abigail-portaits.png", ContentSource.ModFolder);
    }
}

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."
        };
    }
}

Advanced

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.xnb"));

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.