using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace StardewModdingAPI.Framework
{
/// Encapsulates access and changes to content being read from a data file.
internal class ContentEventHelper : EventArgs, IContentEventHelper
{
/*********
** Properties
*********/
/// Normalises an asset key to match the cache key.
private readonly Func GetNormalisedPath;
/*********
** Accessors
*********/
/// The content's locale code, if the content is localised.
public string Locale { get; }
/// The normalised asset name being read. The format may change between platforms; see to compare with a known path.
public string AssetName { get; }
/// The content data being read.
public object Data { get; private set; }
/*********
** Public methods
*********/
/// Construct an instance.
/// The content's locale code, if the content is localised.
/// The normalised asset name being read.
/// The content data being read.
/// Normalises an asset key to match the cache key.
public ContentEventHelper(string locale, string assetName, object data, Func getNormalisedPath)
{
this.Locale = locale;
this.AssetName = assetName;
this.Data = data;
this.GetNormalisedPath = getNormalisedPath;
}
/// Get whether the asset name being loaded matches a given name after normalisation.
/// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation').
public bool IsAssetName(string path)
{
path = this.GetNormalisedPath(path);
return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase);
}
/// Get the data as a given type.
/// The expected data type.
/// The data can't be converted to .
public TData GetData()
{
if (!(this.Data is TData))
throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}.");
return (TData)this.Data;
}
/// Add or replace an entry in the dictionary data.
/// The entry key type.
/// The entry value type.
/// The entry key.
/// The entry value.
/// The content being read isn't a dictionary.
public void SetDictionaryEntry(TKey key, TValue value)
{
IDictionary data = this.GetData>();
data[key] = value;
}
/// Add or replace an entry in the dictionary data.
/// The entry key type.
/// The entry value type.
/// The entry key.
/// A callback which accepts the current value and returns the new value.
/// The content being read isn't a dictionary.
public void SetDictionaryEntry(TKey key, Func value)
{
IDictionary data = this.GetData>();
data[key] = value(data[key]);
}
/// Overwrite part of the image.
/// The image to patch into the content.
/// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture.
/// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.
/// Indicates how an image should be patched.
/// One of the arguments is null.
/// The is outside the bounds of the spritesheet.
/// The content being read isn't an image.
public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
{
// get texture
Texture2D target = this.GetData();
// get areas
sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height);
targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height));
// validate
if (source == null)
throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture.");
if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height)
throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture.");
if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height)
throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture.");
if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height)
throw new InvalidOperationException("The source and target areas must be the same size.");
// get source data
int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height;
Color[] sourceData = new Color[pixelCount];
source.GetData(0, sourceArea, sourceData, 0, pixelCount);
// merge data in overlay mode
if (patchMode == PatchMode.Overlay)
{
Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height];
target.GetData(0, targetArea, newData, 0, newData.Length);
for (int i = 0; i < sourceData.Length; i++)
{
Color pixel = sourceData[i];
if (pixel.A != 0) // not transparent
newData[i] = pixel;
}
sourceData = newData;
}
// patch target texture
target.SetData(0, targetArea, sourceData, 0, pixelCount);
}
/// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game.
/// The new content value.
/// The is null.
/// The 's type is not compatible with the loaded asset's type.
public void ReplaceWith(object value)
{
if (value == null)
throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value.");
if (!this.Data.GetType().IsInstanceOfType(value))
throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.Data.GetType())} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors.");
this.Data = value;
}
/*********
** Private methods
*********/
/// Get a human-readable type name.
/// The type to name.
private string GetFriendlyTypeName(Type type)
{
// dictionary
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
Type[] genericArgs = type.GetGenericArguments();
return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>";
}
// texture
if (type == typeof(Texture2D))
return type.Name;
// native type
if (type == typeof(int))
return "int";
if (type == typeof(string))
return "string";
// default
return type.FullName;
}
}
}