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 NpcTalking { get; private set; } = Array.Empty(); public static IReadOnlyList Eating { get; private set; } = Array.Empty(); public static IReadOnlyList Drinking { get; private set; } = Array.Empty(); public static IReadOnlyList Burps { get; private set; } = Array.Empty(); 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 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(); } List list = new List(); bool flag = default(bool); foreach (string item in Directory.GetFiles(directory, "*.wav", SearchOption.TopDirectoryOnly).OrderBy((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 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 val = ((BaseUnityPlugin)this).Config.Bind("Audio", "Gain", 1.5f, new ConfigDescription("Extra gain applied after loudness normalization. Restart the game after changing it.", (AcceptableValueBase)(object)new AcceptableValueRange(0.25f, 4f), Array.Empty())); 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 ?? "") + ".")); } _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 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; } } }