using System;
using System.Reflection;
using StardewModdingAPI.Framework.Reflection;
namespace StardewModdingAPI.Framework.ModHelpers
{
/// Provides helper methods for accessing private game code.
/// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimize performance without unnecessary memory usage).
internal class ReflectionHelper : BaseHelper, IReflectionHelper
{
/*********
** Fields
*********/
/// The underlying reflection helper.
private readonly Reflector Reflector;
/// The mod name for error messages.
private readonly string ModName;
/*********
** Public methods
*********/
/// Construct an instance.
/// The mod using this instance.
/// The mod name for error messages.
/// The underlying reflection helper.
public ReflectionHelper(IModMetadata mod, string modName, Reflector reflector)
: base(mod)
{
this.ModName = modName;
this.Reflector = reflector;
}
///
public IReflectedField GetField(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetField(obj, name, required)
)!;
}
///
public IReflectedField GetField(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetField(type, name, required)
)!;
}
///
public IReflectedProperty GetProperty(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetProperty(obj, name, required)
)!;
}
///
public IReflectedProperty GetProperty(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetProperty(type, name, required)
)!;
}
///
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetMethod(obj, name, required)
)!;
}
///
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
{
return this.AssertAccessAllowed(
this.Reflector.GetMethod(type, name, required)
)!;
}
/*********
** Private methods
*********/
/// Assert that mods can use the reflection helper to access the given member.
/// The field value type.
/// The field being accessed.
/// Returns the same field instance for convenience.
private IReflectedField? AssertAccessAllowed(IReflectedField? field)
{
this.AssertAccessAllowed(field?.FieldInfo);
return field;
}
/// Assert that mods can use the reflection helper to access the given member.
/// The property value type.
/// The property being accessed.
/// Returns the same property instance for convenience.
private IReflectedProperty? AssertAccessAllowed(IReflectedProperty? property)
{
this.AssertAccessAllowed(property?.PropertyInfo.GetMethod?.GetBaseDefinition());
this.AssertAccessAllowed(property?.PropertyInfo.SetMethod?.GetBaseDefinition());
return property;
}
/// Assert that mods can use the reflection helper to access the given member.
/// The method being accessed.
/// Returns the same method instance for convenience.
private IReflectedMethod? AssertAccessAllowed(IReflectedMethod? method)
{
this.AssertAccessAllowed(method?.MethodInfo.GetBaseDefinition());
return method;
}
/// Assert that mods can use the reflection helper to access the given member.
/// The member being accessed.
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}.)");
}
}
}