Modding:Modder Guide/APIs/Content

From Stardew Valley Wiki
< Modding:Modder Guide‎ | APIs
Revision as of 22:52, 27 May 2018 by Pathoschild (talk | contribs) (move content from Modding:Modder Guide/APIs (only author is Pathoschild))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Creating SMAPI mods SMAPI mascot.png


Modding:Index

Content

Read assets

You can read custom images or maps to use from your mod folder or game content, with support for .xnb, .png, and .tbin files.

Example usage:

  • Read an image from your mod folder (from an assets subfolder in this example):
    // read an XNB file
    var texture = helper.Content.Load<Texture2D>(@"assets\texture.xnb", ContentSource.ModFolder);
    
    // read a PNG file
    var texture = helper.Content.Load<Texture2D>(@"assets\texture.png", ContentSource.ModFolder);
    
  • Read a map from your mod folder (will automatically fix tilesheet references):
    Map map = helper.Content.Load<Map>(@"assets\map.tbin", ContentSource.ModFolder);
    
  • Read a new tilesheet for a map from your mod folder:
    tilesheet.ImageSource = helper.Content.GetActualAssetKey(@"assets\tilesheet.png", ContentSource.ModFolder);
    
  • Read an asset from the game's content folder:
    var data = helper.Content.Load<Dictionary<int, string>>(@"Data\ObjectInformation.xnb", ContentSource.GameContent);
    

Notes:

  • Don't call content.Load<T> in draw code for performance reasons, since drawing happens ≈60 times per second. Instead, load your content ahead of time and reuse it.
  • When loading a mod file, it's automatically relative to your mod folder. SMAPI won't accept an absolute file path.
  • Don't worry about which path separators you use; SMAPI will normalise them automatically.

Edit assets

You can edit any texture or data loaded by the game (often called 'XNB data') 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. The Edit<T> method receives a helper for editing various data types.

Here are a few examples:

  • This mod adds a new item (see Modding:Object data). Note: it's better to use mod frameworks like Json Assets for custom items, to avoid dealing with save issues and ID collisions.
    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)
        {
            return asset.AssetNameEquals(@"Data\ObjectInformation");
        }
    
        /// <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)
        {
            int id = ...;
            asset
                .AsDictionary<int, string>()
                .Set(id, "Blood Rose/40/10/Basic -81/Blood Rose/Not the prettiest flower, but the leaves make a good salad.");
        }
    }
    
  • This mod lets crops grow in any season (example only, doesn't handle edge cases):
    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)
        {
            return asset.AssetNameEquals(@"Data\Crops");
        }
    
        /// <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)
        {
            asset
                .AsDictionary<int, string>()
                .Set((id, data) =>
                {
                    string[] fields = data.Split('/');
                    fields[1] = "spring summer fall winter";
                    return string.Join("/", fields);
                });
        }
    }
    
  • This code replaces part of a game image:
    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)
        {
            return asset.AssetNameEquals(@"Portraits\Abigail");
        }
    
        /// <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)
        {
            Texture2D customTexture = this.Helper.Content.Load<Texture2D>("custom-texture.png", ContentSource.ModFolder);
            asset
                .AsImage()
                .PatchImage(customTexture, targetArea: new Rectangle(300, 100, 200, 200));
        }
    

For more advanced scenarios, you can inject multiple IAssetEditor instances instead. When you inject a new editor, SMAPI will automatically reload all game assets so it can intercept them. (This is resource-intensive when done outside your Entry method, so avoid adding editors unnecessarily.)

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());
    }
}

Inject assets

You can inject new assets for the game to use (e.g. to add dialogue for a custom NPC). To edit existing assets, you should use asset editors instead. If multiple loaders match the same asset, SMAPI will show an error and use none of them.

In most cases, you can just implement IAssetLoader in your Mod class to do it. This adds two methods: CanLoad<T> returns whether the mod can load a particular asset, and Load<T> provides the asset data. 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) is a trick to cast anything to T if we know it's compatible
        {
            ["Introduction"] = "Hi there! My name is Jonathan."
        };
    }
}

For more advanced scenarios, you can inject multiple IAssetLoader instances instead. When you inject a new loader, SMAPI will automatically reload all game assets so it can intercept them. (This is resource-intensive when done outside your Entry method, so avoid adding loaders unnecessarily.)

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

Reload assets

Caution: reloading assets is fairly expensive, so use this API judiciously to avoid impacting game performance.

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 reload it automatically if it's a core asset that's kept in memory by the game. For example, this lets you change what clothes an NPC is wearing (by invalidating their cached sprites or portraits).

Typically you'll invalidate a specific asset key:

helper.Content.InvalidateCache(@"Data\ObjectInformation.xnb"); // path separators and capitalisation don't matter

You can also invalidate assets matching a lambda:

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