Changes

Jump to navigation Jump to search
expand & rewrite
Line 1: Line 1:  
{{../../header}}
 
{{../../header}}
   −
===Content===
+
The content API lets you read custom assets, or read/edit/replace game assets.
====Read assets====
  −
You can read custom images or maps to use from your mod folder or game content, with support for <tt>.xnb</tt>, <tt>.png</tt>, and <tt>.tbin</tt> files.
     −
Example usage:
+
==Intro==
<ul>
+
===What's an 'asset'?===
<li>Read an image from your mod folder (from an <tt>assets</tt> subfolder in this example):
+
An ''asset'' is one image, map, or data structure provided to the game. The game stores its default assets in its <tt>Content</tt> folder, though mods can have custom assets too. For example, all of Abigail's portraits are stored in one asset inside <tt>Content\Portraits\Abigail.xnb</tt>. If you [[Modding:Editing XNB files|unpack that file]], you'll see it contains an image file:
 +
 
 +
[[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 <tt>Content</tt> file, this is the file path relative to <tt>Content</tt> ''without'' the <tt>.xnb</tt> extension or language. For example, <tt>Content\Maps\Desert.xnb</tt> and <tt>Content\Maps\Desert.ja-JA.xnb</tt> both have asset name <tt>Maps\Desert</tt>.
 +
 
 +
===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 <tt>.xnb</tt>, <tt>.png</tt>, and <tt>.tbin</tt> files);
 +
* read assets from the game's <tt>Content</tt> 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 <tt>.png</tt> images, <tt>.tbin</tt> maps, or packed <tt>.xnb</tt> asset files. The normal convention is to have custom asset files in an <tt>assets</tt> subfolder, though that's not required. To read a file, you specify the type and path relative to your mod folder. For example:
 
<source lang="c#">
 
<source lang="c#">
 +
// 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
 
// read an XNB file
var texture = helper.Content.Load<Texture2D>(@"assets\texture.xnb", ContentSource.ModFolder);
+
IDictionary<string, string> data = helper.Content.Load<Dictionary<string, string>>("assets/data.xnb", ContentSource.ModFolder);
 +
</source>
   −
// read a PNG file
+
Some usage notes:
var texture = helper.Content.Load<Texture2D>(@"assets\texture.png", ContentSource.ModFolder);
+
* Don't worry about which path separators you use; SMAPI will normalise them automatically.
</source></li>
+
* When you load a <tt>.tbin</tt> map file, SMAPI will automatically fix any tilesheet references. For example, let's say your map references a file named <tt>tilesheet.png</tt>; SMAPI will automatically reference a <tt>tilesheet.png</tt> file in the same folder if it exists, otherwise it will use the game's default <tt>Content</tt> folders.
 +
* To avoid performance issues, don't call <tt>content.Load<T></tt> repeatedly in draw code. Instead, load your asset once and reuse it.
   −
<li>Read a map from your mod folder (will automatically fix tilesheet references):
+
===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:
 
<source lang="c#">
 
<source lang="c#">
Map map = helper.Content.Load<Map>(@"assets\map.tbin", ContentSource.ModFolder);
+
tilesheet.ImageSource = helper.Content.GetActualAssetKey("assets/tilesheet.png", ContentSource.ModFolder);
</source></li>
+
</source>
   −
<li>Read a new tilesheet for a map from your mod folder:
+
===Read content assets===
 +
You can also read assets from the game folder:
 
<source lang="c#">
 
<source lang="c#">
tilesheet.ImageSource = helper.Content.GetActualAssetKey(@"assets\tilesheet.png", ContentSource.ModFolder);
+
Texture2D portraits = helper.Content.Load<Texture2D>("Portraits/Abigail", ContentSource.GameContent);
</source></li>
+
</source>
 
  −
<li>Read an asset from the game's content folder:
  −
<source lang="c#">
  −
var data = helper.Content.Load<Dictionary<int, string>>(@"Data\ObjectInformation.xnb", ContentSource.GameContent);
  −
</source></li>
  −
</ul>
     −
Notes:
+
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'?]].
* Don't call <tt>content.Load<T></tt> 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====
+
==Edit the game's 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 <tt>IAssetEditor</tt> in your <tt>Mod</tt> class, which adds two methods: <tt>CanEdit&lt;T&gt;</tt> returns whether the mod can edit a particular asset, and <tt>Edit&lt;T&gt;</tt> makes any changes needed. The <tt>Edit&lt;T&gt;</tt> method receives a helper for editing various data types.
+
===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 <tt>IAssetEditor</tt> in your <tt>Mod</tt> class, which adds two methods: <tt>CanEdit&lt;T&gt;</tt> returns whether the mod can edit a particular asset, and <tt>Edit&lt;T&gt;</tt> makes any changes needed.
   −
Here are a few examples:
+
For example, here's a mod which makes crops grow in any season and changes the winter dirt texture.
<ul>
+
<source lang="c#">
<li>This mod adds a new item (see [[Modding:Object data]]). '''Note:''' it's better to use mod frameworks like [https://www.nexusmods.com/stardewvalley/mods/1720/ Json Assets] for custom items, to avoid dealing with save issues and ID collisions.
  −
<source lang="C#">
   
public class ModEntry : Mod, IAssetEditor
 
public class ModEntry : Mod, IAssetEditor
 
{
 
{
Line 50: Line 68:  
     public bool CanEdit<T>(IAssetInfo asset)
 
     public bool CanEdit<T>(IAssetInfo asset)
 
     {
 
     {
         return asset.AssetNameEquals(@"Data\ObjectInformation");
+
         return
 +
            asset.AssetNameEquals("Data/Crops") // change crop seasons
 +
            || asset.AssetNameEquals("TerrainFeatures/hoeDirtSnow"); // change dirt texture
 
     }
 
     }
   Line 57: Line 77:  
     public void Edit<T>(IAssetData asset)
 
     public void Edit<T>(IAssetData asset)
 
     {
 
     {
         int id = ...;
+
         // change crop seasons
         asset
+
        if (asset.AssetNameEquals("Data/Crops"))
            .AsDictionary<int, string>()
+
         {
            .Set(id, "Blood Rose/40/10/Basic -81/Blood Rose/Not the prettiest flower, but the leaves make a good salad.");
+
            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));
 
     }
 
     }
 
}
 
}
 
</source>
 
</source>
</li>
+
 
<li>This mod lets crops grow in any season (example only, doesn't handle edge cases):
+
The <tt>IAssetData asset</tt> argument your <tt>Edit</tt> method receives has some helpers to make editing data easier.
<source lang="c#">
+
 
public class ModEntry : Mod, IAssetEditor
+
<ol>
 +
<li>The above example uses <tt>asset.AsDictionary<int, string>().Set(...)</tt> to make changes to each entry in the dictionary.</li>
 +
<li>The above example also uses <tt>asset.ReplaceWith</tt> to replace the entire asset with a new version. (Usually you shouldn't do that though; see ''Replace an asset entirely'' below.</li>
 +
<li>You can retrieve the underlying data to change it:
 +
<source lang="C#">
 +
public void Edit<T>(IAssetData asset)
 +
{
 +
  IDictionary<int, string> crops = asset.AsDictionary<int, string>().Data;
 +
  crops[999] = "... some new crop string ...";
 +
}
 +
</source></li>
 +
<li>You can paste a custom image into a texture being loaded (e.g. to replace one sprite):
 +
<source lang="C#">
 +
public void Edit<T>(IAssetData asset)
 
{
 
{
     /// <summary>Get whether this instance can edit the given asset.</summary>
+
     Texture2D customTexture = this.Helper.Content.Load<Texture2D>("custom-texture.png", ContentSource.ModFolder);
    /// <param name="asset">Basic metadata about the asset being loaded.</param>
+
    asset
    public bool CanEdit<T>(IAssetInfo asset)
+
        .AsImage()
    {
+
        .PatchImage(customTexture, targetArea: new Rectangle(300, 100, 200, 200));
        return asset.AssetNameEquals(@"Data\Crops");
+
}
    }
+
</source></li>
 +
</ol>
 +
 
 +
See IntelliSense for the <tt>asset</tt> 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 <tt>IAssetLoader</tt> in your <tt>Mod</tt> class. This adds two methods: <tt>CanLoad&lt;T&gt;</tt> returns whether the mod can provide a particular asset, and <tt>Load&lt;T&gt;</tt> provides the asset data. For example, here's a mod which replaces Abigail's portraits with a custom version from its mod folder:
   −
    /// <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);
  −
            });
  −
    }
  −
}
  −
</source>
  −
</li>
  −
<li>
  −
This code replaces part of a game image:
   
<source lang="c#">
 
<source lang="c#">
public class ModEntry : Mod, IAssetEditor
+
public class ModEntry : Mod, IAssetLoader
 
{
 
{
     /// <summary>Get whether this instance can edit the given asset.</summary>
+
     /// <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>
 
     /// <param name="asset">Basic metadata about the asset being loaded.</param>
     public bool CanEdit<T>(IAssetInfo asset)
+
     public bool CanLoad<T>(IAssetInfo asset)
 
     {
 
     {
         return asset.AssetNameEquals(@"Portraits\Abigail");
+
         return asset.AssetNameEquals("Portraits/Abigail");
 
     }
 
     }
   −
     /// <summary>Edit a matched asset.</summary>
+
     /// <summary>Load a matched asset.</summary>
     /// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param>
+
     /// <param name="asset">Basic metadata about the asset being loaded.</param>
     public void Edit<T>(IAssetData asset)
+
     public T Load<T>(IAssetInfo asset)
    {
  −
        Texture2D customTexture = this.Helper.Content.Load<Texture2D>("custom-texture.png", ContentSource.ModFolder);
  −
        asset
  −
            .AsImage()
  −
            .PatchImage(customTexture, targetArea: new Rectangle(300, 100, 200, 200));
  −
    }
  −
</source>
  −
</li>
  −
</ul>
  −
 
  −
For more advanced scenarios, you can inject multiple <tt>IAssetEditor</tt> 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 <tt>Entry</tt> method, so avoid adding editors unnecessarily.)
  −
<source lang="c#">
  −
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());
+
         return this.Helper.Content.Load<T>("assets/abigail-portaits.png", ContentSource.ModFolder);
 
     }
 
     }
 
}
 
}
 
</source>
 
</source>
   −
====Inject assets====
+
===Add a new asset===
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 [[#Edit assets|asset editors]] instead.''' If multiple loaders match the same asset, SMAPI will show an error and use none of them.
+
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:
 
  −
In most cases, you can just implement <tt>IAssetLoader</tt> in your <tt>Mod</tt> class to do it. This adds two methods: <tt>CanLoad&lt;T&gt;</tt> returns whether the mod can load a particular asset, and <tt>Load&lt;T&gt;</tt> provides the asset data. For example, this code adds a new dialogue file for a custom NPC.
   
<source lang="c#">
 
<source lang="c#">
 
public class ModEntry : Mod, IAssetLoader
 
public class ModEntry : Mod, IAssetLoader
Line 141: Line 157:  
     public bool CanLoad<T>(IAssetInfo asset)
 
     public bool CanLoad<T>(IAssetInfo asset)
 
     {
 
     {
         return asset.AssetNameEquals(@"Characters\Dialogue\John");
+
         return asset.AssetNameEquals("Characters/Dialogue/John");
 
     }
 
     }
   Line 148: Line 164:  
     public T Load<T>(IAssetInfo asset)
 
     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
+
         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."
 
             ["Introduction"] = "Hi there! My name is Jonathan."
Line 156: Line 172:  
</source>
 
</source>
   −
For more advanced scenarios, you can inject multiple <tt>IAssetLoader</tt> 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 <tt>Entry</tt> method, so avoid adding loaders unnecessarily.)
+
==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:
 +
<source lang="c#">
 +
helper.Content.InvalidateCache("Data/ObjectInformation");
 +
</source>
 +
 
 +
You can also invalidate assets matching a lambda:
 +
<source lang="c#">
 +
helper.Content.InvalidateCache(asset => asset.DataType == typeof(Texture2D) && asset.AssetNameEquals(@"Data\ObjectInformation.xnb"));
 +
</source>
 +
 
 +
===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:
 
<source lang="c#">
 
<source lang="c#">
 
public class ModEntry : Mod
 
public class ModEntry : Mod
Line 164: Line 197:  
     public override void Entry(IModHelper helper)
 
     public override void Entry(IModHelper helper)
 
     {
 
     {
 +
        helper.Content.AssetEditors.Add(new MyCropEditor());
 
         helper.Content.AssetLoaders.Add(new MyDialogueLoader());
 
         helper.Content.AssetLoaders.Add(new MyDialogueLoader());
 
     }
 
     }
Line 169: Line 203:  
</source>
 
</source>
   −
====Reload assets====
+
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.
'''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:
  −
<source lang="c#">
  −
helper.Content.InvalidateCache(@"Data\ObjectInformation.xnb"); // path separators and capitalisation don't matter
  −
</source>
  −
 
  −
You can also invalidate assets matching a lambda:
  −
<source lang="c#">
  −
helper.Content.InvalidateCache(asset => asset.DataType == typeof(Texture2D) && asset.AssetNameEquals(@"Data\ObjectInformation.xnb"));
  −
</source>
 
translators
8,437

edits

Navigation menu