summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ModHelpers
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework/ModHelpers')
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs236
-rw-r--r--src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs99
2 files changed, 131 insertions, 204 deletions
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 4f5bd2f0..be9594ee 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Exceptions;
@@ -74,12 +73,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ContentManager = contentManager;
this.ModFolderPath = modFolderPath;
this.ModName = modName;
- this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath);
+ this.ModFolderPathFromContent = this.ContentManager.GetRelativePath(modFolderPath);
this.Monitor = monitor;
}
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
- /// <typeparam name="T">The expected data type. The main supported types are <see cref="Texture2D"/> and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
+ /// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
/// <param name="source">Where to search for a matching content asset.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
@@ -88,9 +87,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}.");
- this.AssertValidAssetKeyFormat(key);
try
{
+ this.AssertValidAssetKeyFormat(key);
switch (source)
{
case ContentSource.GameContent:
@@ -103,60 +102,32 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw GetContentError($"there's no matching file at path '{file.FullName}'.");
// get asset path
- string assetPath = this.GetModAssetPath(key, file.FullName);
+ string assetName = this.GetModAssetPath(key, file.FullName);
// try cache
- if (this.ContentManager.IsLoaded(assetPath))
- return this.ContentManager.Load<T>(assetPath);
+ if (this.ContentManager.IsLoaded(assetName))
+ return this.ContentManager.Load<T>(assetName);
- // load content
- switch (file.Extension.ToLower())
+ // fix map tilesheets
+ if (file.Extension.ToLower() == ".tbin")
{
- // XNB file
- case ".xnb":
- {
- T asset = this.ContentManager.Load<T>(assetPath);
- if (asset is Map)
- this.FixLocalMapTilesheets(asset as Map, key);
- return asset;
- }
-
- // unpacked map
- case ".tbin":
- {
- // validate
- if (typeof(T) != typeof(Map))
- throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
-
- // fetch & cache
- FormatManager formatManager = FormatManager.Instance;
- Map map = formatManager.LoadMap(file.FullName);
- this.FixLocalMapTilesheets(map, key);
-
- // inject map
- this.ContentManager.Inject(assetPath, map);
- return (T)(object)map;
- }
-
- // unpacked image
- case ".png":
- // validate
- if (typeof(T) != typeof(Texture2D))
- throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
-
- // fetch & cache
- using (FileStream stream = File.OpenRead(file.FullName))
- {
- Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
- texture = this.PremultiplyTransparency(texture);
- this.ContentManager.Inject(assetPath, texture);
- return (T)(object)texture;
- }
-
- default:
- throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'.");
+ // validate
+ if (typeof(T) != typeof(Map))
+ throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
+
+ // fetch & cache
+ FormatManager formatManager = FormatManager.Instance;
+ Map map = formatManager.LoadMap(file.FullName);
+ this.FixCustomTilesheetPaths(map, key);
+
+ // inject map
+ this.ContentManager.Inject(assetName, map, this.ContentManager);
+ return (T)(object)map;
}
+ // load through content manager
+ return this.ContentManager.Load<T>(assetName);
+
default:
throw GetContentError($"unknown content source '{source}'.");
}
@@ -193,9 +164,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <returns>Returns whether the given asset key was cached.</returns>
public bool InvalidateCache(string key)
{
- this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace);
string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent);
- return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase));
+ this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace);
+ return this.ContentManager.InvalidateCache(asset => asset.AssetNameEquals(actualKey));
}
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
@@ -207,28 +178,50 @@ namespace StardewModdingAPI.Framework.ModHelpers
return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type));
}
+ /// <summary>Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
+ /// <param name="predicate">A predicate matching the assets to invalidate.</param>
+ /// <returns>Returns whether any cache entries were invalidated.</returns>
+ public bool InvalidateCache(Func<IAssetInfo, bool> predicate)
+ {
+ this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace);
+ return this.ContentManager.InvalidateCache(predicate);
+ }
+
/*********
** Private methods
*********/
- /// <summary>Fix the tilesheets for a map loaded from the mod folder.</summary>
+ /// <summary>Assert that the given key has a valid format.</summary>
+ /// <param name="key">The asset key to check.</param>
+ /// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
+ [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
+ private void AssertValidAssetKeyFormat(string key)
+ {
+ this.ContentManager.AssertValidAssetKeyFormat(key);
+ if (Path.IsPathRooted(key))
+ throw new ArgumentException("The asset key must not be an absolute path.");
+ }
+
+ /// <summary>Fix custom map tilesheet paths so they can be found by the content manager.</summary>
/// <param name="map">The map whose tilesheets to fix.</param>
/// <param name="mapKey">The map asset key within the mod folder.</param>
- /// <exception cref="ContentLoadException">The map tilesheets could not be loaded.</exception>
+ /// <exception cref="ContentLoadException">A map tilesheet couldn't be resolved.</exception>
/// <remarks>
- /// The game's logic for tilesheets in <see cref="Game1.setGraphicsForSeason"/> is a bit specialised. It boils down to this:
- /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded as-is relative to the <c>Content</c> folder.
+ /// The game's logic for tilesheets in <see cref="Game1.setGraphicsForSeason"/> is a bit specialised. It boils
+ /// down to this:
+ /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded
+ /// as-is relative to the <c>Content</c> folder.
/// * Else it's loaded from <c>Content\Maps</c> with a seasonal prefix.
///
/// That logic doesn't work well in our case, mainly because we have no location metadata at this point.
/// Instead we use a more heuristic approach: check relative to the map file first, then relative to
- /// <c>Content\Maps</c>, then <c>Content</c>. If the image source filename contains a seasonal prefix, we try
- /// for a seasonal variation and then an exact match.
+ /// <c>Content\Maps</c>, then <c>Content</c>. If the image source filename contains a seasonal prefix, try for a
+ /// seasonal variation and then an exact match.
///
/// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference.
/// </remarks>
- private void FixLocalMapTilesheets(Map map, string mapKey)
+ private void FixCustomTilesheetPaths(Map map, string mapKey)
{
- // check map info
+ // get map info
if (!map.TileSheets.Any())
return;
mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators
@@ -239,7 +232,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
string imageSource = tilesheet.ImageSource;
- // validate
+ // validate tilesheet path
if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains(".."))
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../).");
@@ -264,8 +257,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
try
{
string key =
- this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource)
- ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource);
+ this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource)
+ ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
if (key != null)
{
tilesheet.ImageSource = key;
@@ -282,33 +275,22 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
}
- /// <summary>Load a tilesheet image source if the file exists.</summary>
- /// <param name="relativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
+ /// <summary>Get the actual asset name for a tilesheet.</summary>
+ /// <param name="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
/// <param name="imageSource">The tilesheet image source to load.</param>
- /// <returns>Returns the loaded asset key (if it was loaded successfully).</returns>
- /// <remarks>See remarks on <see cref="FixLocalMapTilesheets"/>.</remarks>
- private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource)
+ /// <returns>Returns the asset name.</returns>
+ /// <remarks>See remarks on <see cref="FixCustomTilesheetPaths"/>.</remarks>
+ private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource)
{
if (imageSource == null)
return null;
// check relative to map file
{
- string localKey = Path.Combine(relativeMapFolder, imageSource);
+ string localKey = Path.Combine(modRelativeMapFolder, imageSource);
FileInfo localFile = this.GetModFile(localKey);
if (localFile.Exists)
- {
- try
- {
- this.Load<Texture2D>(localKey);
- }
- catch (Exception ex)
- {
- throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex);
- }
-
return this.GetActualAssetKey(localKey);
- }
}
// check relative to content folder
@@ -327,7 +309,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
catch
{
// ignore file-not-found errors
- // TODO: while it's useful to suppress a asset-not-found error here to avoid
+ // TODO: while it's useful to suppress an asset-not-found error here to avoid
// confusion, this is a pretty naive approach. Even if the file doesn't exist,
// the file may have been loaded through an IAssetLoader which failed. So even
// if the content file doesn't exist, that doesn't mean the error here is a
@@ -343,18 +325,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
return null;
}
- /// <summary>Assert that the given key has a valid format.</summary>
- /// <param name="key">The asset key to check.</param>
- /// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception>
- [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "Parameter is only used for assertion checks by design.")]
- private void AssertValidAssetKeyFormat(string key)
- {
- if (string.IsNullOrWhiteSpace(key))
- throw new ArgumentException("The asset key or local path is empty.");
- if (key.Intersect(Path.GetInvalidPathChars()).Any())
- throw new ArgumentException("The asset key or local path contains invalid characters.");
- }
-
/// <summary>Get a file from the mod folder.</summary>
/// <param name="path">The asset path relative to the mod folder.</param>
private FileInfo GetModFile(string path)
@@ -400,81 +370,5 @@ namespace StardewModdingAPI.Framework.ModHelpers
return absolutePath;
#endif
}
-
- /// <summary>Get a directory path relative to a given root.</summary>
- /// <param name="rootPath">The root path from which the path should be relative.</param>
- /// <param name="targetPath">The target file path.</param>
- private string GetRelativePath(string rootPath, string targetPath)
- {
- // convert to URIs
- Uri from = new Uri(rootPath + "/");
- Uri to = new Uri(targetPath + "/");
- if (from.Scheme != to.Scheme)
- throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'.");
-
- // get relative path
- return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
- .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
- }
-
- /// <summary>Premultiply a texture's alpha values to avoid transparency issues in the game. This is only possible if the game isn't currently drawing.</summary>
- /// <param name="texture">The texture to premultiply.</param>
- /// <returns>Returns a premultiplied texture.</returns>
- /// <remarks>Based on <a href="https://gist.github.com/Layoric/6255384">code by Layoric</a>.</remarks>
- private Texture2D PremultiplyTransparency(Texture2D texture)
- {
- // validate
- if (Context.IsInDrawLoop)
- throw new NotSupportedException("Can't load a PNG file while the game is drawing to the screen. Make sure you load content outside the draw loop.");
-
- // process texture
- SpriteBatch spriteBatch = Game1.spriteBatch;
- GraphicsDevice gpu = Game1.graphics.GraphicsDevice;
- using (RenderTarget2D renderTarget = new RenderTarget2D(Game1.graphics.GraphicsDevice, texture.Width, texture.Height))
- {
- // create blank render target to premultiply
- gpu.SetRenderTarget(renderTarget);
- gpu.Clear(Color.Black);
-
- // multiply each color by the source alpha, and write just the color values into the final texture
- spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState
- {
- ColorDestinationBlend = Blend.Zero,
- ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue,
- AlphaDestinationBlend = Blend.Zero,
- AlphaSourceBlend = Blend.SourceAlpha,
- ColorSourceBlend = Blend.SourceAlpha
- });
- spriteBatch.Draw(texture, texture.Bounds, Color.White);
- spriteBatch.End();
-
- // copy the alpha values from the source texture into the final one without multiplying them
- spriteBatch.Begin(SpriteSortMode.Immediate, new BlendState
- {
- ColorWriteChannels = ColorWriteChannels.Alpha,
- AlphaDestinationBlend = Blend.Zero,
- ColorDestinationBlend = Blend.Zero,
- AlphaSourceBlend = Blend.One,
- ColorSourceBlend = Blend.One
- });
- spriteBatch.Draw(texture, texture.Bounds, Color.White);
- spriteBatch.End();
-
- // release GPU
- gpu.SetRenderTarget(null);
-
- // extract premultiplied data
- Color[] data = new Color[texture.Width * texture.Height];
- renderTarget.GetData(data);
-
- // unset texture from GPU to regain control
- gpu.Textures[0] = null;
-
- // update texture with premultiplied data
- texture.SetData(data);
- }
-
- return texture;
- }
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
index 8d435416..8788b142 100644
--- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs
@@ -1,4 +1,5 @@
using System;
+using System.Reflection;
using StardewModdingAPI.Framework.Reflection;
namespace StardewModdingAPI.Framework.ModHelpers
@@ -42,8 +43,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <returns>Returns the field wrapper, or <c>null</c> if the field doesn't exist and <paramref name="required"/> is <c>false</c>.</returns>
public IPrivateField<TValue> GetPrivateField<TValue>(object obj, string name, bool required = true)
{
- this.AssertAccessAllowed(obj);
- return this.Reflector.GetPrivateField<TValue>(obj, name, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateField<TValue>(obj, name, required)
+ );
}
/// <summary>Get a private static field.</summary>
@@ -53,8 +55,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateField<TValue> GetPrivateField<TValue>(Type type, string name, bool required = true)
{
- this.AssertAccessAllowed(type);
- return this.Reflector.GetPrivateField<TValue>(type, name, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateField<TValue>(type, name, required)
+ );
}
/****
@@ -67,8 +70,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(object obj, string name, bool required = true)
{
- this.AssertAccessAllowed(obj);
- return this.Reflector.GetPrivateProperty<TValue>(obj, name, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateProperty<TValue>(obj, name, required)
+ );
}
/// <summary>Get a private static property.</summary>
@@ -78,8 +82,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="required">Whether to throw an exception if the private property is not found.</param>
public IPrivateProperty<TValue> GetPrivateProperty<TValue>(Type type, string name, bool required = true)
{
- this.AssertAccessAllowed(type);
- return this.Reflector.GetPrivateProperty<TValue>(type, name, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateProperty<TValue>(type, name, required)
+ );
}
/****
@@ -98,7 +103,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// </remarks>
public TValue GetPrivateValue<TValue>(object obj, string name, bool required = true)
{
- this.AssertAccessAllowed(obj);
IPrivateField<TValue> field = this.GetPrivateField<TValue>(obj, name, required);
return field != null
? field.GetValue()
@@ -117,7 +121,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// </remarks>
public TValue GetPrivateValue<TValue>(Type type, string name, bool required = true)
{
- this.AssertAccessAllowed(type);
IPrivateField<TValue> field = this.GetPrivateField<TValue>(type, name, required);
return field != null
? field.GetValue()
@@ -133,8 +136,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true)
{
- this.AssertAccessAllowed(obj);
- return this.Reflector.GetPrivateMethod(obj, name, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateMethod(obj, name, required)
+ );
}
/// <summary>Get a private static method.</summary>
@@ -143,8 +147,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true)
{
- this.AssertAccessAllowed(type);
- return this.Reflector.GetPrivateMethod(type, name, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateMethod(type, name, required)
+ );
}
/****
@@ -157,8 +162,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true)
{
- this.AssertAccessAllowed(obj);
- return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required)
+ );
}
/// <summary>Get a private static method.</summary>
@@ -168,33 +174,60 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="required">Whether to throw an exception if the private field is not found.</param>
public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true)
{
- this.AssertAccessAllowed(type);
- return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required);
+ return this.AssertAccessAllowed(
+ this.Reflector.GetPrivateMethod(type, name, argumentTypes, required)
+ );
}
/*********
** Private methods
*********/
- /// <summary>Assert that mods can use the reflection helper to access the given type.</summary>
- /// <param name="type">The type being accessed.</param>
- private void AssertAccessAllowed(Type type)
+ /// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
+ /// <typeparam name="T">The field value type.</typeparam>
+ /// <param name="field">The field being accessed.</param>
+ /// <returns>Returns the same field instance for convenience.</returns>
+ private IPrivateField<T> AssertAccessAllowed<T>(IPrivateField<T> field)
{
- // validate type namespace
- if (type.Namespace != null)
- {
- string rootSmapiNamespace = typeof(Program).Namespace;
- if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + "."))
- throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning.");
- }
+ this.AssertAccessAllowed(field?.FieldInfo);
+ return field;
}
- /// <summary>Assert that mods can use the reflection helper to access the given type.</summary>
- /// <param name="obj">The object being accessed.</param>
- private void AssertAccessAllowed(object obj)
+ /// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
+ /// <typeparam name="T">The property value type.</typeparam>
+ /// <param name="property">The property being accessed.</param>
+ /// <returns>Returns the same property instance for convenience.</returns>
+ private IPrivateProperty<T> AssertAccessAllowed<T>(IPrivateProperty<T> property)
{
- if (obj != null)
- this.AssertAccessAllowed(obj.GetType());
+ this.AssertAccessAllowed(property?.PropertyInfo);
+ return property;
+ }
+
+ /// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
+ /// <param name="method">The method being accessed.</param>
+ /// <returns>Returns the same method instance for convenience.</returns>
+ private IPrivateMethod AssertAccessAllowed(IPrivateMethod method)
+ {
+ this.AssertAccessAllowed(method?.MethodInfo);
+ return method;
+ }
+
+ /// <summary>Assert that mods can use the reflection helper to access the given member.</summary>
+ /// <param name="member">The member being accessed.</param>
+ private void AssertAccessAllowed(MemberInfo member)
+ {
+ if (member == null)
+ return;
+
+ // get type which defines the member
+ Type declaringType = member.DeclaringType;
+ if (declaringType == null)
+ throw new InvalidOperationException($"Can't validate access to {member.MemberType} {member.Name} because it has no declaring type."); // should never happen
+
+ // validate access
+ string rootNamespace = typeof(Program).Namespace;
+ if (declaringType.Namespace == rootNamespace || declaringType.Namespace?.StartsWith(rootNamespace + ".") == true)
+ throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning. (Detected access to {declaringType.FullName}.{member.Name}.)");
}
}
}