Changes

Jump to navigation Jump to search
no edit summary
Line 43: Line 43:  
// read an image file
 
// read an image file
 
Texture2D texture = helper.ModContent.Load<Texture2D>("assets/texture.png");
 
Texture2D texture = helper.ModContent.Load<Texture2D>("assets/texture.png");
 +
// alternatively
 +
IRawTextureData texture = helper.ModContent.Load<IRawTextureData>("assets/texture.png");
    
// read a map file
 
// read a map file
Line 68: Line 70:  
|-
 
|-
 
| <samp>.png</samp>
 
| <samp>.png</samp>
| <samp>[https://docs.microsoft.com/en-us/previous-versions/windows/xna/bb199316%28v%3dxnagamestudio.41%29 Texture2D]</samp>
+
| <samp>[https://docs.monogame.net/api/Microsoft.Xna.Framework.Graphics.Texture2D.html Texture2D]</samp>
 
| An image file. You can use this to load textures, spritesheets, tilesheets, etc.
 
| An image file. You can use this to load textures, spritesheets, tilesheets, etc.
 
|-
 
|-
Line 113: Line 115:     
<syntaxhighlight lang="c#">
 
<syntaxhighlight lang="c#">
public class ModEntry : Mod
+
internal sealed class ModEntry : Mod
 
{
 
{
 
     /// <inheritdoc/>
 
     /// <inheritdoc/>
Line 155: Line 157:     
<syntaxhighlight lang="c#">
 
<syntaxhighlight lang="c#">
public class ModEntry : Mod
+
internal sealed class ModEntry : Mod
 
{
 
{
 
     /// <inheritdoc/>
 
     /// <inheritdoc/>
Line 181: Line 183:  
Providing a new asset is exactly like replacing an existing one (see previous sections). 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 sections). For example, this code adds a new dialogue file for a custom NPC:
 
<syntaxhighlight lang="c#">
 
<syntaxhighlight lang="c#">
public class ModEntry : Mod
+
internal sealed class ModEntry : Mod
 
{
 
{
 
     /// <inheritdoc/>
 
     /// <inheritdoc/>
Line 225: Line 227:  
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:
 
<syntaxhighlight lang="c#">
 
<syntaxhighlight lang="c#">
public class ModEntry : Mod
+
internal sealed class ModEntry : Mod
 
{
 
{
 
     /// <inheritdoc/>
 
     /// <inheritdoc/>
Line 238: Line 240:  
     private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
 
     private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
 
     {
 
     {
         if (e.NameWithoutLocale.IsEquivalentTo("Data/ObjectInformation"))
+
         if (e.NameWithoutLocale.IsEquivalentTo("Data/Objects"))
 
         {
 
         {
 
             e.Edit(asset =>
 
             e.Edit(asset =>
 
             {
 
             {
                 var data = asset.AsDictionary<int, string>().Data;
+
                 var data = asset.AsDictionary<string, ObjectData>().Data;
   −
                 foreach (int itemID in data.Keys.ToArray())
+
                 foreach ((string itemID, ObjectData itemData) in data)
 
                 {
 
                 {
                     string[] fields = data[itemID].Split('/');
+
                     itemData.Price *= 2;
                    fields[1] = (int.Parse(fields[1]) * 2).ToString();
  −
                    data[itemID] = string.Join("/", fields);
   
                 }
 
                 }
 
             });
 
             });
Line 255: Line 255:  
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  −
Note that you'll run into errors if you try to edit the collection you're iterating over. The <samp>ToArray()</samp> avoids that by iterating over a copy of the keys instead.
      
The <samp>IAssetData asset</samp> argument from the <samp>Edit</samp> method has some helpers to make editing data easier, documented below. (See IntelliSense for more info.)
 
The <samp>IAssetData asset</samp> argument from the <samp>Edit</samp> method has some helpers to make editing data easier, documented below. (See IntelliSense for more info.)
Line 284: Line 282:  
:: 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:
 
:: <syntaxhighlight lang="C#">
 
:: <syntaxhighlight lang="C#">
public void Edit<T>(IAssetData asset)
+
 
{
+
    /// <inheritdoc cref="IContentEvents.AssetRequested"/>
  var editor = asset.AsDictionary<string, string>();
+
    /// <param name="sender">The event sender.</param>
  editor.Data["Key C"] = "Value C";
+
    /// <param name="e">The event data.</param>
}
+
    private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
 +
    {
 +
        if (e.NameWithoutLocale.IsEquivalentTo("Location/Of/The/Asset"))
 +
        {
 +
            e.Edit(asset =>
 +
            {
 +
                var editor = asset.AsDictionary<string, string>();
 +
                editor.Data["Key C"] = "Value C";
 +
            });
 +
        }
 +
    }
 
</syntaxhighlight>
 
</syntaxhighlight>
   Line 300: Line 308:  
:: 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:
 
:: <syntaxhighlight lang="C#">
 
:: <syntaxhighlight lang="C#">
public void Edit<T>(IAssetData asset)
+
 
{
+
    /// <inheritdoc cref="IContentEvents.AssetRequested"/>
  var editor = asset.AsImage();
+
    /// <param name="sender">The event sender.</param>
 
+
    /// <param name="e">The event data.</param>
  Texture2D sourceImage = this.Helper.ModContent.Load<Texture2D>("custom-texture.png");
+
    private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
  editor.PatchImage(sourceImage, targetArea: new Rectangle(300, 100, 200, 200));
+
    {
}
+
        if (e.NameWithoutLocale.IsEquivalentTo("Location/Of/The/Asset"))
 +
        {
 +
            e.Edit(asset =>
 +
            {
 +
                  var editor = asset.AsImage();
 +
                  IRawTextureData sourceImage = this.Helper.ModContent.Load<IRawTextureData>("custom-texture.png");
 +
                  editor.PatchImage(sourceImage, targetArea: new Rectangle(300, 100, 200, 200));
 +
            });
 +
        }
 +
    }
 
</syntaxhighlight>
 
</syntaxhighlight>
   Line 316: Line 333:  
|-
 
|-
 
| <samp>source</samp>
 
| <samp>source</samp>
| The source image to copy & paste onto the loaded image.
+
| The source image to copy & paste onto the loaded image. May be a <samp>Texture2D</samp> or a <samp>IRawTextureData</samp>
 
|-
 
|-
 
| <samp>sourceArea</samp>
 
| <samp>sourceArea</samp>
Line 327: Line 344:  
| ''(optional)'' How the image should be patched. The possible values...
 
| ''(optional)'' How the image should be patched. The possible values...
 
* <samp>PatchMode.Replace</samp> (default): erase the original content within the area before pasting in the new content;
 
* <samp>PatchMode.Replace</samp> (default): erase the original content within the area before pasting in the new content;
* <samp>PatchMode.Overlay</samp>: draw the new content over the original content, so the original content shows through any ''fully'' transparent pixels.
+
* <samp>PatchMode.Overlay</samp>: draw the new content over the original content, so the original content shows through transparent or semi-transparent pixels.
 
|}
 
|}
    
:; 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 '''resizing the image is an expensive operation''', creates a new texture instance, and extending a spritesheet horizontally may cause game errors or bugs. For example:
 
:: <syntaxhighlight lang="C#">
 
:: <syntaxhighlight lang="C#">
public void Edit<T>(IAssetData asset)
+
 
 +
e.Edit(asset =>
 
{
 
{
 
   var editor = asset.AsImage();
 
   var editor = asset.AsImage();
Line 339: Line 357:  
   // make sure the image is at least 1000px high
 
   // make sure the image is at least 1000px high
 
   editor.ExtendImage(minWidth: editor.Data.Width, minHeight: 1000);
 
   editor.ExtendImage(minWidth: editor.Data.Width, minHeight: 1000);
}
+
});
 
</syntaxhighlight>
 
</syntaxhighlight>
 
:: Available method arguments:
 
:: Available method arguments:
Line 363: Line 381:  
:: 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:
 
:: <syntaxhighlight lang="C#">
 
:: <syntaxhighlight lang="C#">
public void Edit<T>(IAssetData asset)
+
e.Edit(asset =>
 
{
 
{
 
   var editor = asset.AsMap();
 
   var editor = asset.AsMap();
Line 369: Line 387:  
   Map sourceMap = this.Helper.ModContent.Load<Map>("custom-map.tmx");
 
   Map sourceMap = this.Helper.ModContent.Load<Map>("custom-map.tmx");
 
   editor.PatchMap(sourceMap, targetArea: new Rectangle(30, 10, 20, 20));
 
   editor.PatchMap(sourceMap, targetArea: new Rectangle(30, 10, 20, 20));
}
+
});
 
</syntaxhighlight>
 
</syntaxhighlight>
   Line 403: Line 421:  
:: Extend the map if needed to fit the given size. Note that '''this is an expensive operation''' and resizes the map in-place. For example:
 
:: Extend the map if needed to fit the given size. Note that '''this is an expensive operation''' and resizes the map in-place. For example:
 
:: <syntaxhighlight lang="C#">
 
:: <syntaxhighlight lang="C#">
public void Edit<T>(IAssetData asset)
+
 
 +
e.Edit(asset =>
 
{
 
{
 
   var editor = asset.AsMap();
 
   var editor = asset.AsMap();
    
   // make sure the map is at least 256 tiles high
 
   // make sure the map is at least 256 tiles high
   editor.ExtendImage(minHeight: 256);
+
   editor.ExtendMap(minHeight: 256);
}
+
});
 
</syntaxhighlight>
 
</syntaxhighlight>
 
:: Available method arguments:
 
:: Available method arguments:
Line 425: Line 444:     
==Advanced==
 
==Advanced==
 +
===Use <samp>IRawTextureData</samp>===
 +
When loading image data, creating a <samp>Texture2D</samp> instance or calling its <samp>GetData</samp>/<samp>SetData</samp> methods is very expensive and involves GPU calls. You can avoid that by loading images as SMAPI's <samp>IRawTextureData</samp> instead, which returns the data directly with no GPU calls. You can then pass it directly to other SMAPI APIs like [[Modding:Modder Guide/APIs/Content#Edit an image|<samp>PatchImage</samp>]].
 +
 +
For example, this mod applies an image overlay to Abigail's portrait without loading the overlay as a <samp>Texture2D</samp> instance:
 +
<syntaxhighlight lang="c#">
 +
public class ModEntry : Mod
 +
{
 +
    public override void Entry(IModHelper helper)
 +
    {
 +
        helper.Events.Content.AssetRequested += this.OnAssetRequested;
 +
    }
 +
 +
    private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
 +
    {
 +
        if (e.NameWithoutLocale.IsEquivalentTo("Portraits/Abigail"))
 +
        {
 +
            e.Edit(asset =>
 +
            {
 +
                var editor = asset.AsImage();
 +
 
 +
                IRawTextureData overlay = this.Helper.ModContent.Load<IRawTextureData>("assets/overlay.png");
 +
                editor.PatchImage(overlay);
 +
            });
 +
        }
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
You can also edit the <samp>IRawTextureData</samp> data directly before passing it to other methods. For example, this converts the texture to grayscale:
 +
<syntaxhighlight lang="c#">
 +
IRawTextureData image = this.Helper.ModContent.Load<IRawTextureData>("assets/image.png");
 +
 +
int pixelCount = image.Width * image.Height;
 +
for (int i = 0; i < pixelCount; i++)
 +
{
 +
    Color color = image.Data[i];
 +
    if (color.A == 0)
 +
        continue; // ignore transparent color
 +
 +
    int grayscale = (int)((color.R * 0.3) + (color.G * 0.59) + (color.B * 0.11)); // https://stackoverflow.com/a/596282/262123
 +
    image.Data[i] = new Color(grayscale, grayscale, grayscale, color.A);
 +
}
 +
</syntaxhighlight>
 +
 +
(Note: while SMAPI's implementation of IRawTextureData is exactly as long as it needs to be, this may not be the case for implementations of IRawTextureData from mods.)
 +
 
===Compare asset names===
 
===Compare asset names===
 
You can't use normal string comparison with asset names. For example, <samp>Characters/Abigail</samp> and <samp>CHARACTERS\ABIGAIL</samp> are the same asset name, but comparing them with C#'s <code>==</code> operator will return false.
 
You can't use normal string comparison with asset names. For example, <samp>Characters/Abigail</samp> and <samp>CHARACTERS\ABIGAIL</samp> are the same asset name, but comparing them with C#'s <code>==</code> operator will return false.
Line 448: Line 513:  
===Cache invalidation===
 
===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).
 
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).
 +
 +
Please be aware that in some cases a localized version of the asset will be cached and simply invalidating the default asset will not work for any language other than english.
    
Reloading assets is fairly expensive, so use this API judiciously to avoid impacting game performance. Definitely don't do this every update tick.
 
Reloading assets is fairly expensive, so use this API judiciously to avoid impacting game performance. Definitely don't do this every update tick.
Line 476: Line 543:     
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.
 +
 +
===Let other mods edit your internal assets===
 +
Other mods can't edit your internal mod files (including data or texture files), but they ''can'' edit custom assets you provide through the content pipeline. This technique consists of three steps:
 +
# Define a custom asset based on the internal file using the [[Modding:Modder Guide/APIs/Events#Content.AssetRequested|<samp>AssetRequested</samp> event]].
 +
# Detect when it's loaded/changed using the [[Modding:Modder Guide/APIs/Events#Content.AssetReady|<samp>AssetReady</samp> event]].
 +
# Load it through the content pipeline when you need it.
 +
 +
For example, this mod just loads a data asset (a dictionary of model entries):
 +
 +
<syntaxhighlight lang="c#">
 +
public class ModEntry : Mod
 +
{
 +
    /// <summary>The loaded data.</summary>
 +
    private Dictionary<string, ExampleModel> Data;
 +
 +
    /// <inheritdoc/>
 +
    public override void Entry(IModHelper helper)
 +
    {
 +
        helper.Events.Content.AssetRequested += this.OnAssetRequested;
 +
        helper.Events.Content.AssetReady += this.OnAssetReady;
 +
        helper.Events.GameLoop.GameLaunched += this.OnGameLaunched;
 +
    }
 +
 +
    private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
 +
    {
 +
        //
 +
        // 1. define the custom asset based on the internal file
 +
        //
 +
        if (e.Name.IsEquivalentTo("Mods/Your.ModId/Data"))
 +
        {
 +
            e.LoadFromModFile<Dictionary<string, ExampleModel>>("assets/default-data.json", AssetLoadPriority.Medium);
 +
        }
 +
    }
 +
 +
    private void OnAssetReady(object sender, AssetReadyEventArgs e)
 +
    {
 +
        //
 +
        // 2. update the data when it's reloaded
 +
        //
 +
        if (e.Name.IsEquivalentTo("Mods/Your.ModId/Data"))
 +
        {
 +
            this.Data = Game1.content.Load<Dictionary<string, ExampleModel>>("Mods/Your.ModId/Data");
 +
        }
 +
    }
 +
 +
    private void OnGameLaunched(object sender, GameLaunchedEventArgs e)
 +
    {
 +
        //
 +
        // 3. load the data
 +
        //    (This doesn't need to be in OnGameLaunched, you can load it later depending on your mod logic.)
 +
        //
 +
        this.Data = Game1.content.Load<Dictionary<string, ExampleModel>>("Mods/Your.ModId/Data");
 +
    }
 +
}
 +
</syntaxhighlight>
 +
 +
This works for any asset type (e.g. maps or textures), and you can do this even without an internal file (e.g. using <code>e.LoadFrom(() => new Dictionary<string, ExampleModel>(), AssetLoadPriority.Medium)</code>).
    
[[ru:Модификации:Моддер гайд/APIs/Контент]]
 
[[ru:Модификации:Моддер гайд/APIs/Контент]]
2

edits

Navigation menu