using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("od.e")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Replaces How to Fish NPC dialogue and eating sounds with matching Minecraft audio.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("MinecraftSoundReplacement")]
[assembly: AssemblyTitle("MinecraftSoundReplacement")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
}
namespace HowToFish.MinecraftSoundReplacement
{
internal static class AudioAssets
{
private readonly struct WaveFormat
{
public ushort Encoding { get; }
public ushort Channels { get; }
public uint SampleRate { get; }
public ushort BlockAlign { get; }
public ushort BitsPerSample { get; }
public WaveFormat(ushort encoding, ushort channels, uint sampleRate, ushort blockAlign, ushort bitsPerSample)
{
Encoding = encoding;
Channels = channels;
SampleRate = sampleRate;
BlockAlign = blockAlign;
BitsPerSample = bitsPerSample;
}
}
private readonly struct WaveData
{
public int SampleRate { get; }
public int Channels { get; }
public float[] Samples { get; }
public WaveData(int sampleRate, int channels, float[] samples)
{
SampleRate = sampleRate;
Channels = channels;
Samples = samples;
}
}
private const BindingFlags PublicInstance = BindingFlags.Instance | BindingFlags.Public;
private const BindingFlags PublicStatic = BindingFlags.Static | BindingFlags.Public;
private static int _nextNpcTalking = -1;
private static int _nextEating = -1;
private static int _nextDrinking = -1;
private static int _nextBurp = -1;
public static IReadOnlyList<LoadedAudioClip> NpcTalking { get; private set; } = Array.Empty<LoadedAudioClip>();
public static IReadOnlyList<LoadedAudioClip> Eating { get; private set; } = Array.Empty<LoadedAudioClip>();
public static IReadOnlyList<LoadedAudioClip> Drinking { get; private set; } = Array.Empty<LoadedAudioClip>();
public static IReadOnlyList<LoadedAudioClip> Burps { get; private set; } = Array.Empty<LoadedAudioClip>();
public static void Load(string pluginDirectory, float gain, ManualLogSource log)
{
Type obj = AccessTools.TypeByName("UnityEngine.AudioClip") ?? throw new TypeLoadException("UnityEngine.AudioClip was not found.");
MethodInfo create = obj.GetMethods(BindingFlags.Static | BindingFlags.Public).Single((MethodInfo method) => method.Name == "Create" && method.GetParameters().Length == 5 && method.GetParameters()[0].ParameterType == typeof(string));
MethodInfo setData = obj.GetMethods(BindingFlags.Instance | BindingFlags.Public).Single((MethodInfo method) => method.Name == "SetData" && method.GetParameters().Length == 2 && method.GetParameters()[0].ParameterType == typeof(float[]));
string path = Path.Combine(pluginDirectory, "audio");
NpcTalking = LoadCategory(Path.Combine(path, "talking"), "NPC dialogue", gain, create, setData, log);
Eating = LoadCategory(Path.Combine(path, "eating"), "eating", gain, create, setData, log);
Drinking = LoadCategory(Path.Combine(path, "drinking"), "drinking", gain, create, setData, log);
Burps = LoadCategory(Path.Combine(path, "burp"), "burp", gain, create, setData, log);
}
public static LoadedAudioClip? NextNpcTalking()
{
return Next(NpcTalking, ref _nextNpcTalking);
}
public static LoadedAudioClip? NextEating()
{
return Next(Eating, ref _nextEating);
}
public static LoadedAudioClip? NextDrinking()
{
return Next(Drinking, ref _nextDrinking);
}
public static LoadedAudioClip? NextBurp()
{
return Next(Burps, ref _nextBurp);
}
private static IReadOnlyList<LoadedAudioClip> LoadCategory(string directory, string displayName, float gain, MethodInfo create, MethodInfo setData, ManualLogSource log)
{
if (!Directory.Exists(directory))
{
log.LogWarning((object)("No " + displayName + " audio directory found at '" + directory + "'; original sounds remain active."));
return Array.Empty<LoadedAudioClip>();
}
List<LoadedAudioClip> list = new List<LoadedAudioClip>();
bool flag = default(bool);
foreach (string item in Directory.GetFiles(directory, "*.wav", SearchOption.TopDirectoryOnly).OrderBy<string, string>((string path) => path, StringComparer.OrdinalIgnoreCase))
{
try
{
WaveData waveData = ReadWave(item);
NormalizeAndBoost(waveData.Samples, gain);
int num = list.Count + 1;
string text = $"MinecraftSoundReplacement_{displayName}_{num}";
object obj = create.Invoke(null, new object[5]
{
text,
waveData.Samples.Length / waveData.Channels,
waveData.Channels,
waveData.SampleRate,
false
}) ?? throw new InvalidOperationException("Unity returned a null AudioClip.");
object obj2 = setData.Invoke(obj, new object[2] { waveData.Samples, 0 });
int num2;
if (obj2 is bool)
{
flag = (bool)obj2;
num2 = 1;
}
else
{
num2 = 0;
}
if (((uint)num2 & (flag ? 1u : 0u)) == 0)
{
throw new InvalidOperationException("Unity AudioClip.SetData rejected the decoded samples.");
}
list.Add(new LoadedAudioClip(obj, num));
}
catch (Exception exception)
{
log.LogError((object)("Could not load " + displayName + " clip '" + item + "': " + Unwrap(exception).Message));
}
}
if (list.Count == 0)
{
log.LogWarning((object)("No valid PCM WAV files were found for " + displayName + "; original sounds remain active."));
}
return list;
}
private static LoadedAudioClip? Next(IReadOnlyList<LoadedAudioClip> values, ref int counter)
{
if (values.Count == 0)
{
return null;
}
int num = Interlocked.Increment(ref counter) & 0x7FFFFFFF;
return values[num % values.Count];
}
private static WaveData ReadWave(string path)
{
using FileStream fileStream = File.OpenRead(path);
using BinaryReader binaryReader = new BinaryReader(fileStream);
if (ReadFourCc(binaryReader) != "RIFF")
{
throw new InvalidDataException("Expected a RIFF header.");
}
binaryReader.ReadUInt32();
if (ReadFourCc(binaryReader) != "WAVE")
{
throw new InvalidDataException("Expected WAVE data.");
}
WaveFormat? waveFormat = null;
byte[] array = null;
while (fileStream.Position + 8 <= fileStream.Length)
{
string text = ReadFourCc(binaryReader);
uint num = binaryReader.ReadUInt32();
long num2 = checked(fileStream.Position + num);
if (num2 > fileStream.Length)
{
throw new InvalidDataException("Chunk '" + text + "' extends past the end of the file.");
}
if (text == "fmt ")
{
if (num < 16)
{
throw new InvalidDataException("The WAV format chunk is too short.");
}
ushort encoding = binaryReader.ReadUInt16();
ushort channels = binaryReader.ReadUInt16();
uint sampleRate = binaryReader.ReadUInt32();
binaryReader.ReadUInt32();
ushort blockAlign = binaryReader.ReadUInt16();
ushort bitsPerSample = binaryReader.ReadUInt16();
waveFormat = new WaveFormat(encoding, channels, sampleRate, blockAlign, bitsPerSample);
}
else if (text == "data")
{
if (num > int.MaxValue)
{
throw new InvalidDataException("The WAV data chunk is too large.");
}
array = binaryReader.ReadBytes((int)num);
if (array.Length != (int)num)
{
throw new EndOfStreamException("The WAV data chunk ended unexpectedly.");
}
}
fileStream.Position = num2 + (num & 1);
}
if (!waveFormat.HasValue || array == null)
{
throw new InvalidDataException("The WAV file must contain both 'fmt ' and 'data' chunks.");
}
return Decode(waveFormat.Value, array);
}
private static WaveData Decode(WaveFormat format, byte[] bytes)
{
if (format.Channels == 0 || format.Channels > 8 || format.SampleRate == 0 || format.SampleRate > 384000)
{
throw new InvalidDataException("The channel count or sample rate is invalid.");
}
int num = (format.BitsPerSample + 7) / 8;
int num2 = checked(format.Channels * num);
if (num == 0 || format.BlockAlign != num2 || bytes.Length % format.BlockAlign != 0)
{
throw new InvalidDataException("The WAV block alignment is invalid.");
}
float[] array = new float[bytes.Length / num];
if (format.Encoding == 1)
{
DecodePcm(format.BitsPerSample, bytes, array);
}
else
{
if (format.Encoding != 3 || format.BitsPerSample != 32)
{
throw new InvalidDataException($"Unsupported WAV encoding {format.Encoding} at {format.BitsPerSample} bits. " + "Use uncompressed PCM (8/16/24/32-bit) or 32-bit IEEE float WAV.");
}
for (int i = 0; i < array.Length; i++)
{
float num3 = BitConverter.ToSingle(bytes, i * 4);
array[i] = (float.IsNaN(num3) ? 0f : Math.Max(-1f, Math.Min(1f, num3)));
}
}
return new WaveData((int)format.SampleRate, format.Channels, array);
}
private static void NormalizeAndBoost(float[] samples, float userGain)
{
if (samples.Length == 0)
{
return;
}
double num = 0.0;
foreach (float num2 in samples)
{
num += (double)(num2 * num2);
}
double num3 = Math.Sqrt(num / (double)samples.Length);
if (!(num3 < 1E-06))
{
double num4 = Math.Min(12.0, 0.18 / num3) * Math.Max(0.25, Math.Min(4.0, userGain));
for (int j = 0; j < samples.Length; j++)
{
samples[j] = (float)(0.98 * Math.Tanh((double)samples[j] * num4 / 0.98));
}
}
}
private static void DecodePcm(ushort bitsPerSample, byte[] bytes, float[] samples)
{
switch (bitsPerSample)
{
case 8:
{
for (int l = 0; l < samples.Length; l++)
{
samples[l] = (float)(bytes[l] - 128) / 128f;
}
break;
}
case 16:
{
for (int j = 0; j < samples.Length; j++)
{
samples[j] = (float)BitConverter.ToInt16(bytes, j * 2) / 32768f;
}
break;
}
case 24:
{
for (int k = 0; k < samples.Length; k++)
{
int num = k * 3;
int num2 = bytes[num] | (bytes[num + 1] << 8) | (bytes[num + 2] << 16);
if ((num2 & 0x800000) != 0)
{
num2 |= -16777216;
}
samples[k] = (float)num2 / 8388608f;
}
break;
}
case 32:
{
for (int i = 0; i < samples.Length; i++)
{
samples[i] = (float)BitConverter.ToInt32(bytes, i * 4) / 2.1474836E+09f;
}
break;
}
default:
throw new InvalidDataException($"Unsupported PCM bit depth {bitsPerSample}. Use 8, 16, 24, or 32-bit PCM WAV.");
}
}
private static string ReadFourCc(BinaryReader reader)
{
return new string(reader.ReadChars(4));
}
private static Exception Unwrap(Exception exception)
{
while (exception is TargetInvocationException && exception.InnerException != null)
{
exception = exception.InnerException;
}
return exception;
}
}
internal sealed class LoadedAudioClip
{
public object UnityClip { get; }
public int CategoryIndex { get; }
public LoadedAudioClip(object unityClip, int categoryIndex)
{
UnityClip = unityClip;
CategoryIndex = categoryIndex;
}
}
[BepInPlugin("od.e.howtofish.minecraftsoundreplacement", "Minecraft Sound Replacement", "1.0.0")]
public sealed class MinecraftSoundReplacementPlugin : BaseUnityPlugin
{
public const string PluginGuid = "od.e.howtofish.minecraftsoundreplacement";
public const string PluginName = "Minecraft Sound Replacement";
public const string PluginVersion = "1.0.0";
private const string FurModelReplaceGuid = "dev.codex.howtofish.furmodelreplace";
private static readonly Guid SupportedGameMvid = new Guid("5bfce772-ee82-40f5-9e5b-e1cd1212f3dd");
private const string SupportedUnityVersion = "6000.4.4f1";
private const BindingFlags AllMembers = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
[ThreadStatic]
private static int _npcEatingDepth;
private static ManualLogSource _log = null;
private static FieldInfo _npcMouthSource = null;
private static FieldInfo _npcMouthVolume = null;
private static FieldInfo _playerEatSource = null;
private static FieldInfo _playerEatClip = null;
private static FieldInfo _playerDrinkClip = null;
private static FieldInfo _playerEatVolume = null;
private static FieldInfo _playerDrinkVolume = null;
private static FieldInfo _playerIsEating = null;
private static FieldInfo _audioManagerClips = null;
private static PropertyInfo _audioSourceClip = null;
private static PropertyInfo _audioSourceIsPlaying = null;
private Harmony? _harmony;
private void Awake()
{
//IL_0039: Unknown result type (might be due to invalid IL or missing references)
//IL_0043: Expected O, but got Unknown
//IL_0218: Unknown result type (might be due to invalid IL or missing references)
//IL_0222: Expected O, but got Unknown
_log = ((BaseUnityPlugin)this).Logger;
try
{
ConfigEntry<float> val = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "Gain", 1.5f, new ConfigDescription("Extra gain applied after loudness normalization. Restart the game after changing it.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 4f), Array.Empty<object>()));
AudioAssets.Load(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Paths.PluginPath, val.Value, ((BaseUnityPlugin)this).Logger);
Type type = RequireType("NPC");
Type type2 = RequireType("PlayerEating");
Type type3 = RequireType("AudioManager");
Type type4 = RequireType("UnityEngine.AudioSource");
_npcMouthSource = RequireField(type, "_mouthSource");
_npcMouthVolume = RequireField(type, "_mouthVol");
_playerEatSource = RequireField(type2, "_eatSoundSource");
_playerEatClip = RequireField(type2, "_eatSound");
_playerDrinkClip = RequireField(type2, "_drinkSound");
_playerEatVolume = RequireField(type2, "_eatSoundVolume");
_playerDrinkVolume = RequireField(type2, "_drinkSoundVolume");
_playerIsEating = RequireField(type2, "_localIsEating");
_audioManagerClips = RequireField(type3, "_realClips");
_audioSourceClip = RequireProperty(type4, "clip");
_audioSourceIsPlaying = RequireProperty(type4, "isPlaying");
MethodInfo methodInfo = RequireMethod(type, "SetNpcText", 4);
Guid moduleVersionId = methodInfo.DeclaringType.Assembly.ManifestModule.ModuleVersionId;
if (moduleVersionId != SupportedGameMvid)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("This game assembly differs from Steam build 24911270 " + $"(expected MVID {SupportedGameMvid}, found {moduleVersionId})."));
}
string text = RequireType("UnityEngine.Application").GetProperty("unityVersion", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null, null)?.ToString();
if (!string.Equals(text, "6000.4.4f1", StringComparison.Ordinal))
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("This mod targets Unity 6000.4.4f1; the running game reports " + (text ?? "<unknown>") + "."));
}
_harmony = new Harmony("od.e.howtofish.minecraftsoundreplacement");
Patch(methodInfo, "ConfigureNpcTalking", null);
MethodInfo original = AccessTools.EnumeratorMoveNext((MethodBase)RequireMethod(type, "EatEffects", 0)) ?? throw new MissingMethodException("NPC", "EatEffects iterator MoveNext");
Patch(original, "BeforeNpcEating", "AfterNpcEating");
Patch(RequireMethod(type2, "OnStartClient", 0), null, "ConfigurePlayerEating");
Patch(RequireMethod(type2, "UpdateEating", 0), "PreparePlayerEatingSound", null);
Patch(RequireMethod(type3, "Start", 0), null, "RegisterAudioClips");
Patch(RequireMethod(type3, "PlayClipAt", 6), "ReplaceNpcEatingSound", null);
Patch(RequireMethod(type3, "PlayPlayerClip", 6), "ReplacePlayerSwallow", null);
((BaseUnityPlugin)this).Logger.LogInfo((object)($"Minecraft Sound Replacement is active with {AudioAssets.NpcTalking.Count} NPC dialogue, " + $"{AudioAssets.Eating.Count} eating, {AudioAssets.Drinking.Count} drinking, and " + $"{AudioAssets.Burps.Count} burp clip(s); normalized gain={val.Value:0.##}x."));
}
catch (Exception exception)
{
((BaseUnityPlugin)this).Logger.LogError((object)"Minecraft Sound Replacement could not install its patches.");
((BaseUnityPlugin)this).Logger.LogError((object)Unwrap(exception));
Harmony? harmony = _harmony;
if (harmony != null)
{
harmony.UnpatchSelf();
}
_harmony = null;
}
}
private void OnDestroy()
{
Harmony? harmony = _harmony;
if (harmony != null)
{
harmony.UnpatchSelf();
}
}
private void Patch(MethodInfo original, string? prefixName, string? postfixName)
{
_harmony.Patch((MethodBase)original, (prefixName == null) ? null : CreatePatch(prefixName), (postfixName == null) ? null : CreatePatch(postfixName), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
}
private static HarmonyMethod CreatePatch(string name)
{
//IL_0045: Unknown result type (might be due to invalid IL or missing references)
//IL_004b: Expected O, but got Unknown
return new HarmonyMethod(typeof(MinecraftSoundReplacementPlugin).GetMethod(name, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingMethodException(typeof(MinecraftSoundReplacementPlugin).FullName, name), 0, (string[])null, new string[1] { "dev.codex.howtofish.furmodelreplace" }, (bool?)null);
}
private static void ConfigureNpcTalking(object __instance)
{
LoadedAudioClip loadedAudioClip = AudioAssets.NextNpcTalking();
object value = _npcMouthSource.GetValue(__instance);
if (loadedAudioClip != null && value != null)
{
_audioSourceClip.SetValue(value, loadedAudioClip.UnityClip, null);
_npcMouthVolume.SetValue(__instance, 1f);
}
}
private static void BeforeNpcEating()
{
_npcEatingDepth++;
}
private static void AfterNpcEating()
{
if (_npcEatingDepth > 0)
{
_npcEatingDepth--;
}
}
private static void ConfigurePlayerEating(object __instance)
{
LoadedAudioClip loadedAudioClip = AudioAssets.NextEating();
LoadedAudioClip loadedAudioClip2 = AudioAssets.NextDrinking();
if (loadedAudioClip != null)
{
_playerEatClip.SetValue(__instance, loadedAudioClip.UnityClip);
_playerEatVolume.SetValue(__instance, 1f);
}
if (loadedAudioClip2 != null)
{
_playerDrinkClip.SetValue(__instance, loadedAudioClip2.UnityClip);
_playerDrinkVolume.SetValue(__instance, 1f);
}
}
private static void PreparePlayerEatingSound(object __instance)
{
if (AudioAssets.Eating.Count == 0)
{
return;
}
object value = _playerIsEating.GetValue(__instance);
if (!(value is bool) || !(bool)value)
{
return;
}
object value2 = _playerEatSource.GetValue(__instance);
if (value2 == null)
{
return;
}
value = _audioSourceIsPlaying.GetValue(value2, null);
bool flag = default(bool);
int num;
if (value is bool)
{
flag = (bool)value;
num = 1;
}
else
{
num = 0;
}
if (((uint)num & (flag ? 1u : 0u)) == 0)
{
LoadedAudioClip loadedAudioClip = AudioAssets.NextEating();
if (loadedAudioClip != null)
{
_playerEatClip.SetValue(__instance, loadedAudioClip.UnityClip);
_playerEatVolume.SetValue(__instance, 1f);
_playerDrinkVolume.SetValue(__instance, 1f);
}
}
}
private static void RegisterAudioClips()
{
if (!(_audioManagerClips.GetValue(null) is IDictionary dictionary))
{
_log.LogError((object)"Could not access AudioManager._realClips; NPC eating and final burp remain unchanged.");
return;
}
Register(dictionary, "MinecraftEat", AudioAssets.Eating);
Register(dictionary, "MinecraftDrink", AudioAssets.Drinking);
Register(dictionary, "MinecraftBurp", AudioAssets.Burps);
}
private static void ReplaceNpcEatingSound(ref string clip, ref float volume)
{
if (_npcEatingDepth > 0 && AudioAssets.Eating.Count != 0)
{
LoadedAudioClip loadedAudioClip = AudioAssets.NextEating();
if (loadedAudioClip != null)
{
clip = $"MinecraftEat{loadedAudioClip.CategoryIndex}";
volume = Math.Max(volume, 1f);
}
}
}
private static void ReplacePlayerSwallow(ref string clip, ref float volume)
{
if (AudioAssets.Burps.Count != 0 && (string.Equals(clip, "Swallow", StringComparison.Ordinal) || clip.StartsWith("FurPlayerEating", StringComparison.Ordinal)))
{
LoadedAudioClip loadedAudioClip = AudioAssets.NextBurp();
if (loadedAudioClip != null)
{
clip = $"MinecraftBurp{loadedAudioClip.CategoryIndex}";
volume = Math.Max(volume, 1f);
}
}
}
private static void Register(IDictionary dictionary, string prefix, IReadOnlyList<LoadedAudioClip> clips)
{
foreach (LoadedAudioClip clip in clips)
{
dictionary[$"{prefix}{clip.CategoryIndex}"] = clip.UnityClip;
}
}
private static Type RequireType(string typeName)
{
return AccessTools.TypeByName(typeName) ?? throw new TypeLoadException("Required runtime type '" + typeName + "' was not found.");
}
private static MethodInfo RequireMethod(Type type, string methodName, int parameterCount)
{
MethodInfo[] array = (from method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
where method.Name == methodName && method.GetParameters().Length == parameterCount
select method).ToArray();
if (array.Length != 1)
{
throw new MissingMethodException(type.FullName, $"{methodName} with {parameterCount} parameter(s); found {array.Length}");
}
return array[0];
}
private static FieldInfo RequireField(Type type, string fieldName)
{
return type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingFieldException(type.FullName, fieldName);
}
private static PropertyInfo RequireProperty(Type type, string propertyName)
{
return type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new MissingMemberException(type.FullName, propertyName);
}
private static Exception Unwrap(Exception exception)
{
while (exception is TargetInvocationException && exception.InnerException != null)
{
exception = exception.InnerException;
}
return exception;
}
}
}