using System; using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using MonoMod.RuntimeDetour; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using loaforcsSoundAPI.Core; using loaforcsSoundAPI.Core.Data; using loaforcsSoundAPI.Core.JSON; using loaforcsSoundAPI.Core.Networking; using loaforcsSoundAPI.Core.Patches; using loaforcsSoundAPI.Core.Patches.Harmony; using loaforcsSoundAPI.Core.Patches.Native; using loaforcsSoundAPI.Core.Util; using loaforcsSoundAPI.Core.Util.Extensions; using loaforcsSoundAPI.Reporting; using loaforcsSoundAPI.Reporting.Data; using loaforcsSoundAPI.SoundPacks; using loaforcsSoundAPI.SoundPacks.Data; using loaforcsSoundAPI.SoundPacks.Data.Conditions; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("me.loaforc.soundapi")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("2.0.11.0")] [assembly: AssemblyInformationalVersion("2.0.11+d2a4b5d20178b48907ed339ab8ee696943361aaf")] [assembly: AssemblyProduct("loaforcsSoundAPI")] [assembly: AssemblyTitle("me.loaforc.soundapi")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("2.0.11.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } internal static class IsExternalInit { } } namespace loaforcsSoundAPI { [Flags] public enum AudioSourceCopyFlags { DontCopyPlayOnAwake = 1 } [BepInPlugin("me.loaforc.soundapi", "loaforcsSoundAPI", "2.0.11")] internal class loaforcsSoundAPI : BaseUnityPlugin { internal static ManualLogSource Logger { get; private set; } private void Awake() { Logger = Logger.CreateLogSource("me.loaforc.soundapi"); ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; Logger.LogInfo((object)"Setting up config"); Debuggers.Bind(((BaseUnityPlugin)this).Config); SoundReportHandler.Bind(((BaseUnityPlugin)this).Config); PatchConfig.Bind(((BaseUnityPlugin)this).Config); PackLoadingConfig.Bind(((BaseUnityPlugin)this).Config); Logger.LogInfo((object)"Running patches"); Harmony harmony = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "me.loaforc.soundapi"); if (PatchConfig.PreferredBackend == PatchConfig.Backend.HarmonyX) { Logger.LogInfo((object)"Native backend is manually disabled."); HarmonyBackend.Init(harmony); } else if (NativeBackend.TryInit()) { Logger.LogInfo((object)("Native backend is supported on " + Application.unityVersion + "!")); } else { Logger.LogWarning((object)"Native backend failed, falling back to default harmony backend!"); HarmonyBackend.Init(harmony); } Logger.LogInfo((object)"Registering data"); SoundAPI.RegisterAll(Assembly.GetExecutingAssembly()); SoundAPIAudioManager.SpawnManager(); SoundReplacementHandler.Register(); ((BaseUnityPlugin)this).Config.Save(); Logger.LogInfo((object)"me.loaforc.soundapi by loaforc has loaded :3"); } internal static ConfigFile GenerateConfigFile(string name, BepInPlugin metadata) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown return new ConfigFile(Utility.CombinePaths(new string[2] { Paths.ConfigPath, name + ".cfg" }), false, metadata); } } public static class SoundAPI { public const string PLUGIN_GUID = "me.loaforc.soundapi"; internal static NetworkAdapter CurrentNetworkAdapter { get; private set; } public static async Task LoadAudioFileAsync(string fullPath) { if (!File.Exists(fullPath)) { throw new FileNotFoundException("'" + fullPath + "' not found."); } if (!SoundPackLoadPipeline.audioExtensions.ContainsKey(Path.GetExtension(fullPath))) { throw new NotImplementedException("Audio file extension: '" + Path.GetExtension(fullPath) + "' is not implemented."); } UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(fullPath, SoundPackLoadPipeline.audioExtensions[Path.GetExtension(fullPath)]); await (AsyncOperation)(object)request.SendWebRequest(); AudioClip content = DownloadHandlerAudioClip.GetContent(request); request.Dispose(); return content; } public static void RegisterAll(Assembly assembly) { foreach (Type loadableType in assembly.GetLoadableTypes()) { if (loadableType.IsNested) { continue; } foreach (SoundAPIConditionAttribute conditionAttribute in loadableType.GetCustomAttributes()) { if (!typeof(Condition).IsAssignableFrom(loadableType)) { loaforcsSoundAPI.Logger.LogError((object)("Condition: '" + loadableType.FullName + "' has been marked with [SoundAPICondition] but does not extend Condition!")); continue; } ConstructorInfo info = loadableType.GetConstructor(Array.Empty()); if (info == null) { loaforcsSoundAPI.Logger.LogError((object)("Condition: '" + loadableType.FullName + "' has no valid constructor! It must have a constructor with no parameters! If you need extra parameters do not mark it with [SoundAPICondition] and register it manually.")); continue; } RegisterCondition(conditionAttribute.ID, delegate { if (conditionAttribute.IsDeprecated) { if (conditionAttribute.DeprecationReason == null) { loaforcsSoundAPI.Logger.LogWarning((object)("Condition: '" + conditionAttribute.ID + "' is deprecated and may be removed in future.")); } else { loaforcsSoundAPI.Logger.LogWarning((object)("Condition: '" + conditionAttribute.ID + "' is deprecated. " + conditionAttribute.DeprecationReason)); } } return (Condition)info.Invoke(Array.Empty()); }); } } } public static void RegisterCondition(string id, Func factory) { SoundPackDataHandler.Register(id, factory); } public static void RegisterNetworkAdapter(NetworkAdapter adapter) { CurrentNetworkAdapter = adapter; loaforcsSoundAPI.Logger.LogInfo((object)("Registered network adapter: '" + CurrentNetworkAdapter.Name + "'")); CurrentNetworkAdapter.OnRegister(); } public static void RegisterSoundPack(SoundPack pack) { if (SoundPackDataHandler.LoadedPacks.Contains(pack)) { throw new InvalidOperationException("Already registered sound-pack: '" + pack.Name + "'!"); } SoundPackDataHandler.AddLoadedPack(pack); foreach (SoundReplacementCollection replacementCollection in pack.ReplacementCollections) { foreach (SoundReplacementGroup replacement in replacementCollection.Replacements) { SoundPackDataHandler.AddReplacement(replacement); } } } public static AudioSource CopyAudioSource(AudioSource source, GameObject target, AudioSourceCopyFlags flags = (AudioSourceCopyFlags)0) { //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) AudioSource val = target.AddComponent(); val.clip = source.clip; val.loop = source.loop; val.mute = source.mute; val.pitch = source.pitch; val.outputAudioMixerGroup = source.outputAudioMixerGroup; val.priority = source.priority; val.spatialize = source.spatialize; val.spread = source.spread; val.volume = source.volume; val.bypassEffects = source.bypassEffects; val.dopplerLevel = source.dopplerLevel; val.maxDistance = source.maxDistance; val.minDistance = source.minDistance; val.panStereo = source.panStereo; val.rolloffMode = source.rolloffMode; val.spatialBlend = source.spatialBlend; val.bypassReverbZones = source.bypassReverbZones; val.ignoreListenerPause = source.ignoreListenerPause; val.ignoreListenerVolume = source.ignoreListenerVolume; if ((flags & AudioSourceCopyFlags.DontCopyPlayOnAwake) == 0) { val.playOnAwake = source.playOnAwake; } val.reverbZoneMix = source.reverbZoneMix; val.spatializePostEffects = source.spatializePostEffects; val.velocityUpdateMode = source.velocityUpdateMode; return val; } } internal static class MyPluginInfo { public const string PLUGIN_GUID = "me.loaforc.soundapi"; public const string PLUGIN_NAME = "loaforcsSoundAPI"; public const string PLUGIN_VERSION = "2.0.11"; } } namespace loaforcsSoundAPI.SoundPacks { public readonly struct AudioSourcePlayEvent { public bool IsOneShot { get; } public AudioSource Source { get; } public AudioSourceAdditionalData Data { get; } public IContext Context { get; } public AudioClip Clip { get; } public AudioSourcePlayEvent(AudioSource source, AudioClip clip, bool isOneShot) { IsOneShot = isOneShot; Source = source; Clip = clip; Data = AudioSourceAdditionalData.GetOrCreate(source); Context = Data.CurrentContext ?? DefaultConditionContext.DEFAULT; } } internal class LoadSoundOperation { public readonly UnityWebRequest WebRequest = webRequest.webRequest; public readonly SoundInstance Sound; public bool IsReady => WebRequest.isDone; public bool IsDone { get; set; } public LoadSoundOperation(SoundInstance soundInstance, UnityWebRequestAsyncOperation webRequest) { Sound = soundInstance; base..ctor(); } } internal static class PackLoadingConfig { internal static bool MetadataSpoofing { get; private set; } internal static void Bind(ConfigFile file) { MetadataSpoofing = file.Bind("PackLoading", "MetadataSpoofing", true, "Should SoundAPI use a fake BepInPlugin attribute when generating configs? This can fix some issues with mod managers, notably with Gale displaying the config file name, instead of the sound-pack name.").Value; } } internal static class SoundPackDataHandler { private static List _loadedPacks = new List(); internal static Dictionary> SoundReplacements = new Dictionary>(); internal static Dictionary> conditionFactories = new Dictionary>(); internal static List allLoadedClips = new List(); internal static IReadOnlyList LoadedPacks => _loadedPacks.AsReadOnly(); internal static void Register(string id, Func factory) { conditionFactories[id] = factory; } public static Condition CreateCondition(string id) { if (conditionFactories.TryGetValue(id, out var value)) { return value(); } return new InvalidCondition(id); } internal static void AddLoadedPack(SoundPack pack) { _loadedPacks.Add(pack); } internal static void AddReplacement(SoundReplacementGroup group) { foreach (string match in group.Matches) { string key = match.Split(":").Last(); if (!SoundReplacements.TryGetValue(key, out var value)) { value = new List(); } if (!value.Contains(group)) { value.Add(group); SoundReplacements[key] = value; } } } } internal static class SoundPackLoadPipeline { private class SkippedResults { public int Collections; public int Groups; public int Sounds; } private static volatile int _activeThreads; private static Dictionary> mappings; internal static Dictionary audioExtensions; internal static event Action OnFinishedPipeline; internal static async void StartPipeline() { Stopwatch completeLoadingTimer = Stopwatch.StartNew(); Stopwatch timer = Stopwatch.StartNew(); List list = FindAndLoadPacks(); loaforcsSoundAPI.Logger.LogInfo((object)$"(Step 1) Loading Sound-pack definitions took {timer.ElapsedMilliseconds}ms"); if (list.Count == 0) { loaforcsSoundAPI.Logger.LogWarning((object)"No sound-packs were found to load! This can be ignorable if you're doing testing or using SoundAPI for another purpose, but if you expected sound-packs to load you may have set it up incorrectly."); } timer.Restart(); List webRequestOperations = new List(); foreach (SoundPack item2 in list) { string path = Path.Combine(item2.PackFolder, "soundapi_mappings.json"); if (!File.Exists(path)) { continue; } Dictionary> dictionary = JSONDataLoader.LoadFromFile>>(path); foreach (KeyValuePair> item3 in dictionary) { if (mappings.ContainsKey(item3.Key)) { mappings[item3.Key].AddRange(item3.Value); } else { mappings[item3.Key] = item3.Value; } } } loaforcsSoundAPI.Logger.LogInfo((object)$"(Step 2) Loading Sound-pack mappings ('{mappings.Count}') took {timer.ElapsedMilliseconds}ms"); timer.Restart(); SkippedResults skippedStats = new SkippedResults(); foreach (SoundPack item4 in list) { foreach (SoundReplacementCollection item5 in LoadSoundReplacementCollections(item4, ref skippedStats)) { foreach (SoundReplacementGroup replacement in item5.Replacements) { SoundPackDataHandler.AddReplacement(replacement); foreach (SoundInstance sound in replacement.Sounds) { if (sound.Condition is ConstantCondition constantCondition && !constantCondition.Value) { Debuggers.SoundReplacementLoader?.Log("skipping a sound in '" + LogFormats.FormatFilePath(item5.FilePath) + "' because sound is marked as constant and has a value of false."); skippedStats.Sounds++; } else { webRequestOperations.Add(StartWebRequestOperation(item4, sound, audioExtensions[Path.GetExtension(sound.Sound)])); } } } } } int amountOfOperations = webRequestOperations.Count; loaforcsSoundAPI.Logger.LogInfo((object)$"(Step 3) Skipped {skippedStats.Collections} collection(s), {skippedStats.Groups} replacement(s), {skippedStats.Sounds} sound(s)"); loaforcsSoundAPI.Logger.LogInfo((object)$"(Step 3) Loading sound replacement collections took {timer.ElapsedMilliseconds}ms"); if (SoundReportHandler.CurrentReport != null) { SoundReportHandler.CurrentReport.AudioClipsLoaded = amountOfOperations; } loaforcsSoundAPI.Logger.LogInfo((object)$"(Step 4) Started loading {amountOfOperations} audio file(s)"); loaforcsSoundAPI.Logger.LogInfo((object)"Waiting for splash screens to complete to continue..."); completeLoadingTimer.Stop(); await Task.Delay(1); loaforcsSoundAPI.Logger.LogInfo((object)"Splash screens done! Continuing pipeline"); loaforcsSoundAPI.Logger.LogWarning((object)"The game will freeze for a moment!"); timer.Restart(); completeLoadingTimer.Start(); bool flag = false; bool threadsShouldExit = false; ConcurrentQueue queuedOperations = new ConcurrentQueue(); ConcurrentBag threadPoolExceptions = new ConcurrentBag(); for (int i = 0; i < 16; i++) { new Thread((ThreadStart)delegate { while (queuedOperations.Count == 0 && !threadsShouldExit) { Thread.Yield(); } Interlocked.Increment(ref _activeThreads); Debuggers.SoundReplacementLoader?.Log($"active threads at {_activeThreads}"); LoadSoundOperation result; while (queuedOperations.TryDequeue(out result)) { try { AudioClip content = DownloadHandlerAudioClip.GetContent(result.WebRequest); result.Sound.Clip = content; result.WebRequest.Dispose(); Debuggers.SoundReplacementLoader?.Log("clip generated"); result.IsDone = true; } catch (Exception item) { threadPoolExceptions.Add(item); } } Interlocked.Decrement(ref _activeThreads); }).Start(); } while (webRequestOperations.Count > 0) { foreach (LoadSoundOperation item6 in from operation in webRequestOperations.ToList() where operation.IsReady select operation) { queuedOperations.Enqueue(item6); webRequestOperations.Remove(item6); } if (!flag && webRequestOperations.Count < amountOfOperations / 2) { flag = true; loaforcsSoundAPI.Logger.LogInfo((object)"(Step 5) Queued half of the needed operations!"); } Thread.Yield(); } loaforcsSoundAPI.Logger.LogInfo((object)"(Step 5) All file reads are done, waiting for the audio clips conversions."); threadsShouldExit = true; while (_activeThreads > 0 || webRequestOperations.Any((LoadSoundOperation operation) => !operation.IsDone)) { Thread.Yield(); } loaforcsSoundAPI.Logger.LogInfo((object)$"(Step 6) Took {timer.ElapsedMilliseconds}ms to finish loading audio clips from files"); if (threadPoolExceptions.Count != 0) { loaforcsSoundAPI.Logger.LogError((object)$"(Step 6) {threadPoolExceptions.Count} internal error(s) happened while loading:"); foreach (Exception item7 in threadPoolExceptions) { loaforcsSoundAPI.Logger.LogError((object)item7.ToString()); } } SoundPackLoadPipeline.OnFinishedPipeline(); mappings = null; loaforcsSoundAPI.Logger.LogDebug((object)$"Active Threads that are left over: {_activeThreads}"); loaforcsSoundAPI.Logger.LogInfo((object)$"Entire load process took an effective {completeLoadingTimer.ElapsedMilliseconds}ms"); } private static List FindAndLoadPacks(string entryPoint = "sound_pack.json") { //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Expected O, but got Unknown Dictionary dictionary = new Dictionary(); string[] files = Directory.GetFiles(Paths.PluginPath, entryPoint, SearchOption.AllDirectories); foreach (string text in files) { Debuggers.SoundReplacementLoader?.Log("found entry point: '" + text + "'!"); SoundPack soundPack = JSONDataLoader.LoadFromFile(text); if (soundPack == null) { continue; } soundPack.PackFolder = Path.GetDirectoryName(text); if (dictionary.TryGetValue(soundPack.Name, out var value)) { IValidatable.LogAndCheckValidationResult("loading '" + text + "'", new List(1) { new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "A sound-pack with name '" + soundPack.Name + "' was already loaded from '" + LogFormats.FormatFilePath(Path.Combine(value.PackFolder, "sound_pack.json")) + "'. Skipping loading the duplicate!!") }, soundPack.Logger); continue; } Debuggers.SoundReplacementLoader?.Log("json loaded, validating"); List results = soundPack.Validate(); if (IValidatable.LogAndCheckValidationResult("loading '" + text + "'", results, soundPack.Logger)) { ConfigFile val = loaforcsSoundAPI.GenerateConfigFile(metadata: (BepInPlugin)((!PackLoadingConfig.MetadataSpoofing) ? ((object)MetadataHelper.GetMetadata(typeof(loaforcsSoundAPI))) : ((object)new BepInPlugin(soundPack.GUID, soundPack.Name, soundPack.Version ?? "1.0.0"))), name: soundPack.GUID); val.SaveOnConfigSet = false; soundPack.Bind(val); if (val.Count > 0) { val.Save(); } dictionary[soundPack.Name] = soundPack; SoundPackDataHandler.AddLoadedPack(soundPack); Debuggers.SoundReplacementLoader?.Log("pack folder: " + soundPack.PackFolder); } } Debuggers.SoundReplacementLoader?.Log($"loaded '{dictionary.Count}' packs."); return dictionary.Values.ToList(); } private static List LoadSoundReplacementCollections(SoundPack pack, ref SkippedResults skippedStats) { List list = new List(); if (!Directory.Exists(Path.Combine(pack.PackFolder, "replacers"))) { return list; } Debuggers.SoundReplacementLoader?.Log("start loading '" + pack.Name + "'!"); string[] files = Directory.GetFiles(Path.Combine(pack.PackFolder, "replacers"), "*.json", SearchOption.AllDirectories); foreach (string text in files) { Debuggers.SoundReplacementLoader?.Log("found replacer: '" + text + "'!"); SoundReplacementCollection soundReplacementCollection = JSONDataLoader.LoadFromFile(text); if (soundReplacementCollection == null) { continue; } soundReplacementCollection.Pack = pack; if (soundReplacementCollection.Condition is ConstantCondition constantCondition && !constantCondition.Value) { Debuggers.SoundReplacementLoader?.Log("skipping '" + LogFormats.FormatFilePath(soundReplacementCollection.FilePath) + "' because collection is marked as constant and has a value of false."); skippedStats.Collections++; } else { if (!IValidatable.LogAndCheckValidationResult("loading '" + LogFormats.FormatFilePath(text) + "'", soundReplacementCollection.Validate(), pack.Logger)) { continue; } List list2 = new List(); foreach (SoundReplacementGroup replacement in soundReplacementCollection.Replacements) { replacement.Parent = soundReplacementCollection; if (replacement.Condition is ConstantCondition constantCondition2 && !constantCondition2.Value) { Debuggers.SoundReplacementLoader?.Log("skipping a replacement in '" + LogFormats.FormatFilePath(soundReplacementCollection.FilePath) + "' because group is marked as constant and has a value of false."); skippedStats.Groups++; continue; } List list3 = replacement.Validate(); foreach (string item in replacement.Matches.ToList()) { if (item.StartsWith("#")) { replacement.Matches.Remove(item); Dictionary> dictionary = mappings; string text2 = item; if (dictionary.TryGetValue(text2.Substring(1, text2.Length - 1), out var value)) { replacement.Matches.AddRange(value); } else { list3.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Mapping: '" + item + "' has not been found. If it's part of a soft dependency, make sure to use a 'mod_installed' condition with 'constant' enabled.")); } } } if (list3.Count != 0) { list2.AddRange(list3); continue; } foreach (SoundInstance sound in replacement.Sounds) { sound.Parent = replacement; list3.AddRange(sound.Validate()); } if (list3.Count != 0) { list2.AddRange(list3); continue; } List collection = replacement.Matches.Select((string match) => (match.Split(":").Length != 2) ? match : ("*:" + match)).ToList(); replacement.Matches.Clear(); replacement.Matches.AddRange(collection); } if (IValidatable.LogAndCheckValidationResult("loading '" + LogFormats.FormatFilePath(text) + "'", list2, pack.Logger)) { list.Add(soundReplacementCollection); } } } return list; } private static LoadSoundOperation StartWebRequestOperation(SoundPack pack, SoundInstance sound, AudioType type) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) string text = Path.Combine(pack.PackFolder, "sounds", sound.Sound); UnityWebRequest audioClip = UnityWebRequestMultimedia.GetAudioClip(text, type); return new LoadSoundOperation(sound, audioClip.SendWebRequest()); } static SoundPackLoadPipeline() { SoundPackLoadPipeline.OnFinishedPipeline = delegate { }; mappings = new Dictionary>(); audioExtensions = new Dictionary { { ".ogg", (AudioType)14 }, { ".wav", (AudioType)20 }, { ".mp3", (AudioType)13 } }; } } internal readonly struct ReplacementResult { public AudioClip ReplacedClip { get; } public SoundReplacementGroup ReplacedWith { get; } public bool IsUpdateEveryFrame { get; } public ReplacementResult(AudioClip replacement, SoundReplacementGroup group) { ReplacedClip = replacement; ReplacedWith = group; IsUpdateEveryFrame = group.Parent.UpdateEveryFrame; } } internal static class SoundReplacementHandler { private const int TOKEN_PARENT_NAME = 0; private const int TOKEN_OBJECT_NAME = 1; private const int TOKEN_CLIP_NAME = 2; private static readonly string[] _suffixesToRemove = new string[1] { "(Clone)" }; private static readonly Dictionary _cachedObjectNames = new Dictionary(); private static readonly StringBuilder _builder = new StringBuilder(); private static string[] _workingName = Array.Empty(); internal static void Register() { SceneManager.sceneLoaded += delegate { _cachedObjectNames.Clear(); }; } private static bool ShouldBeReplaced(in AudioSourcePlayEvent @event) { if (!Object.op_Implicit((Object)(object)((Component)@event.Source).gameObject)) { return false; } if (@event.Data.ReplacedWith != null && @event.Data.ReplacedWith.Parent.UpdateEveryFrame) { return false; } return !@event.Data.DisableReplacing; } internal static bool TryReplaceAudio(in AudioSourcePlayEvent @event, [NotNullWhen(true)] out ReplacementResult? result) { result = null; if (!ShouldBeReplaced(in @event)) { return false; } string[] name = ArrayPool.Shared.Rent(3); if (!TryProcessName(ref name, @event.Source, @event.Clip) || !TryGetReplacementClip(name, out var group, out var clip, @event.Context ?? DefaultConditionContext.DEFAULT)) { ArrayPool.Shared.Return(name); return false; } ArrayPool.Shared.Return(name); ((Object)clip).name = group.Pack.Name + " " + ((Object)@event.Clip).name; result = new ReplacementResult(clip, group); @event.Data.ReplacedWith = group; if (result.Value.IsUpdateEveryFrame) { Debuggers.UpdateEveryFrame?.Log($"swapped to a clip that uses update_every_frame !!! isOneShot = {@event.IsOneShot}"); } return true; } [Obsolete] internal static bool TryReplaceAudio(AudioSource source, AudioClip clip, out AudioClip replacement) { AudioSourcePlayEvent @event = new AudioSourcePlayEvent(source, clip, isOneShot: false); if (TryReplaceAudio(in @event, out var result)) { replacement = result.Value.ReplacedClip; return true; } replacement = null; return false; } private static string TrimObjectName(GameObject gameObject) { if (_cachedObjectNames.ContainsKey(((object)gameObject).GetHashCode())) { return _cachedObjectNames[((object)gameObject).GetHashCode()]; } _builder.Clear(); _builder.Append(((Object)gameObject).name); string[] suffixesToRemove = _suffixesToRemove; foreach (string oldValue in suffixesToRemove) { _builder.Replace(oldValue, string.Empty); } for (int j = 0; j < _builder.Length; j++) { if (_builder[j] == '(') { int num = j; for (j++; j < _builder.Length && char.IsDigit(_builder[j]); j++) { } if (j < _builder.Length && _builder[j] == ')') { _builder.Remove(num, j - num + 1); j = num - 1; } } } int num2 = _builder.Length; while (num2 > 0 && _builder[num2 - 1] == ' ') { num2--; } _builder.Remove(num2, _builder.Length - num2); string text = _builder.ToString(); _cachedObjectNames[((object)gameObject).GetHashCode()] = text; return text; } private static bool TryProcessName(ref string[] name, AudioSource source, AudioClip clip) { if ((Object)(object)clip == (Object)null) { return false; } if ((Object)(object)((Component)source).transform.parent == (Object)null) { name[0] = "*"; } else { name[0] = TrimObjectName(((Component)((Component)source).transform.parent).gameObject); } name[1] = TrimObjectName(((Component)source).gameObject); name[2] = ((Object)clip).name; if (SoundReportHandler.CurrentReport != null) { string caller; try { caller = new StackTrace(fNeedFileInfo: true).GetFrame(5).GetMethod().DeclaringType.Name; } catch { caller = "unknown caller"; } SoundReport.PlayedSound playedSound = new SoundReport.PlayedSound(name[0] + ":" + name[1] + ":" + name[2], caller, source.playOnAwake); if (!SoundReportHandler.CurrentReport.PlayedSounds.Any(playedSound.Equals)) { SoundReportHandler.CurrentReport.PlayedSounds.Add(playedSound); } } Debuggers.MatchStrings?.Log(name[0] + ":" + name[1] + ":" + name[2]); return true; } private static bool TryGetReplacementClip(string[] name, out SoundReplacementGroup group, out AudioClip clip, IContext context) { group = null; clip = null; if (name == null) { return false; } Debuggers.SoundReplacementHandler?.Log("beginning replacement attempt for " + name[2]); if (!SoundPackDataHandler.SoundReplacements.TryGetValue(name[2], out var value)) { return false; } Debuggers.SoundReplacementHandler?.Log("sound dictionary hit"); value = value.Where((SoundReplacementGroup it) => it.Parent.Evaluate(context) && it.Evaluate(context) && CheckGroupMatches(it, name)).ToList(); if (value.Count == 0) { return false; } Debuggers.SoundReplacementHandler?.Log("sound group that matches"); group = value[Random.Range(0, value.Count)]; List list = group.Sounds.Where((SoundInstance it) => it.Evaluate(context)).ToList(); if (list.Count == 0) { return false; } Debuggers.SoundReplacementHandler?.Log("has valid sounds"); int totalWeight = 0; list.ForEach(delegate(SoundInstance replacement) { totalWeight += replacement.Weight; }); int num = Random.Range(0, totalWeight + 1); SoundInstance soundInstance = null; foreach (SoundInstance item in list) { soundInstance = item; num -= soundInstance.Weight; if (num <= 0) { break; } } clip = soundInstance.Clip; Debuggers.SoundReplacementHandler?.Log("done, dumping stack trace!"); Debuggers.SoundReplacementHandler?.Log(string.Join(", ", group.Matches)); Debuggers.SoundReplacementHandler?.Log(((Object)clip).name); Debuggers.SoundReplacementHandler?.Log(new StackTrace(fNeedFileInfo: true).ToString().Trim()); return true; } private static bool CheckGroupMatches(SoundReplacementGroup group, string[] a) { foreach (string match in group.Matches) { if (MatchStrings(a, match)) { return true; } } return false; } private static bool MatchStrings(string[] a, string b) { string[] array = b.Split(":"); if (array[0] != "*" && array[0] != a[0]) { return false; } if (array[1] != "*" && array[1] != a[1]) { return false; } return a[2] == array[2]; } } } namespace loaforcsSoundAPI.SoundPacks.Data { public interface IPackData { SoundPack Pack { get; internal set; } } public class SoundInstance : Conditional { [field: NonSerialized] public SoundReplacementGroup Parent { get; internal set; } public string Sound { get; private set; } public int Weight { get; private set; } [field: NonSerialized] public AudioClip Clip { get; internal set; } public override SoundPack Pack { get { return Parent.Pack; } set { if (Parent.Pack != null) { throw new InvalidOperationException("Pack has already been set."); } Parent.Pack = value; } } [JsonConstructor] internal SoundInstance() { } public SoundInstance(SoundReplacementGroup parent, int weight, AudioClip clip) { Parent = parent; Weight = weight; Clip = clip; parent.AddSoundReplacement(this); } public override List Validate() { List list = base.Validate(); if (!File.Exists(Path.Combine(Pack.PackFolder, "sounds", Sound))) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Sound '" + Sound + "' couldn't be found or doesn't exist!")); } else if (!SoundPackLoadPipeline.audioExtensions.ContainsKey(Path.GetExtension(Sound))) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Audio type: '" + Path.GetExtension(Sound) + "' is not supported!")); } return list; } } public class SoundPack : IValidatable { [NonSerialized] private readonly Dictionary _configData = new Dictionary(); private ManualLogSource _logger; [JsonProperty] private Dictionary config { get; set; } public string Name { get; private set; } public string GUID => "soundpack." + Name; [CanBeNull] public string Version { get; private set; } public string PackFolder { get; internal set; } [field: NonSerialized] public List ReplacementCollections { get; private set; } = new List(); public ManualLogSource Logger { get { if (_logger == null) { _logger = Logger.CreateLogSource(GUID); } return _logger; } } [JsonConstructor] internal SoundPack() { } internal void Bind(ConfigFile file) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Invalid comparison between Unknown and I4 //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Invalid comparison between Unknown and I4 if (config == null || config.Count == 0) { return; } loaforcsSoundAPI.Logger.LogDebug((object)"handling config"); JToken val2 = default(JToken); foreach (KeyValuePair item in config) { string[] array = item.Key.Split(":"); string text = array[0]; string text2 = array[1]; JToken val = item.Value["default"]; string text3 = (item.Value.TryGetValue("description", ref val2) ? ((object)val2).ToString() : "no description defined!"); JTokenType type = val.Type; if ((int)type != 8) { if ((int)type != 9) { throw new NotImplementedException("WHAT"); } _configData[item.Key] = file.Bind(text, text2, (bool)val, text3).Value; } else { _configData[item.Key] = file.Bind(text, text2, (string)val, text3).Value; } } } public SoundPack(string name, string packFolder) { Name = name; PackFolder = packFolder; } internal bool TryGetConfigValue(string id, out object returnValue) { returnValue = null; if (!_configData.TryGetValue(id, out var value)) { return false; } returnValue = value; return true; } public List Validate() { //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Invalid comparison between Unknown and I4 //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Invalid comparison between Unknown and I4 //IL_0154: Unknown result type (might be due to invalid IL or missing references) List list = new List(); if (string.IsNullOrEmpty(Name)) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "'name' can not be missing or empty!")); return list; } string name = Name; foreach (char c in name) { if (!char.IsLetter(c) && c != '_') { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, $"'name' can not contain special character '{c}'!")); } } if (string.IsNullOrEmpty(Version) && PackLoadingConfig.MetadataSpoofing) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.WARN, "'version' should not be empty, defaulting to '1.0.0' (needed by MetadataSpoofing config)")); } if (config == null) { return list; } JToken val = default(JToken); foreach (KeyValuePair item in config) { string[] array = item.Key.Split(":"); if (array.Length != 2) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "'" + item.Key + "' is not a valid key for config! It must be 'section:name' with exactly one colon!")); } if (!item.Value.TryGetValue("default", ref val)) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "'" + item.Key + "' does not have a 'default' value! This is needed to get what type the config is!")); } else if ((int)val.Type != 9 && (int)val.Type != 8) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, $"'{item.Key}' is of unsupported type: '{val.Type}'! Only supported types are strings/text or booleans!")); } if (!item.Value.ContainsKey("description")) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.WARN, "'" + item.Key + "' does not have a description.")); } } return list; } } public class SoundReplacementCollection : Conditional, IFilePathAware, IPackData { [field: NonSerialized] public override SoundPack Pack { get; set; } public bool UpdateEveryFrame { get; private set; } public bool Synced { get; private set; } public List Replacements { get; private set; } = new List(); public string FilePath { get; set; } [JsonConstructor] internal SoundReplacementCollection() { } public SoundReplacementCollection(SoundPack pack) { Pack = pack; pack.ReplacementCollections.Add(this); } internal void AddSoundReplacementGroup(SoundReplacementGroup group) { Replacements.Add(group); } } public class SoundReplacementGroup : Conditional { [field: NonSerialized] public SoundReplacementCollection Parent { get; internal set; } public List Matches { get; private set; } public List Sounds { get; private set; } = new List(); public override SoundPack Pack { get { return Parent.Pack; } set { if (Parent.Pack != null) { throw new InvalidOperationException("Pack has already been set."); } Parent.Pack = value; } } [JsonConstructor] internal SoundReplacementGroup() { } public SoundReplacementGroup(SoundReplacementCollection parent, List matches) { Parent = parent; Matches = matches; if (SoundPackDataHandler.LoadedPacks.Contains(parent.Pack)) { throw new InvalidOperationException("SoundPack has already been registered, trying to add a new SoundReplacementGroup does not work!"); } parent.AddSoundReplacementGroup(this); } internal void AddSoundReplacement(SoundInstance sound) { Sounds.Add(sound); } public override List Validate() { List list = base.Validate(); foreach (string match in Matches) { if (string.IsNullOrEmpty(match)) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Match string can not be empty!")); continue; } string[] array = match.Split(":"); if (array.Length == 1) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "'" + match + "' is not valid! If you mean to match to all Audio clips with this name you must explicitly do '*:" + match + "'.")); } if (array.Length > 3) { list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.WARN, $"'{match}' has more than 3 parts! SoundAPI will handle this as '{match[0]}:{match[1]}:{match[2]}', discarding the rest!")); } } return list; } } } namespace loaforcsSoundAPI.SoundPacks.Data.Conditions { public abstract class Condition : IValidatable { [field: NonSerialized] public Conditional Parent { get; internal set; } protected SoundPack Pack => Parent.Pack; public bool? Constant { get; private set; } protected internal virtual void OnRegistered() { } public abstract bool Evaluate(IContext context); public virtual List Validate() { return new List(); } protected bool EvaluateRangeOperator(int number, string condition) { return EvaluateRangeOperator((double)number, condition); } protected bool EvaluateRangeOperator(float number, string condition) { return EvaluateRangeOperator((double)number, condition); } protected bool EvaluateRangeOperator(double value, string condition) { string[] array = condition.Split(".."); if (array.Length == 1) { if (double.TryParse(array[0], out var result)) { return value == result; } return false; } if (array.Length == 2) { double result2; if (array[0] == "") { result2 = double.MinValue; } else if (!double.TryParse(array[0], out result2)) { return false; } double result3; if (array[1] == "") { result3 = double.MaxValue; } else if (!double.TryParse(array[1], out result3)) { return false; } if (value >= result2) { return value <= result3; } return false; } return false; } protected bool ValidateRangeOperator(string condition, out IValidatable.ValidationResult result) { result = null; if (string.IsNullOrEmpty(condition)) { result = new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Range operator can not be missing or empty!"); return false; } string[] array = condition.Split(".."); int num = array.Length; if (num <= 2) { switch (num) { case 1: { if (!double.TryParse(array[0], out var _)) { result = new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Failed to parse: '" + array[0] + "' as a number!"); } break; } case 2: { double num2; if (array[0] == "") { num2 = double.MinValue; } else if (!double.TryParse(array[0], out num2)) { result = new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Failed to parse: '" + array[0] + "' as a number!"); } double num3; if (array[1] == "") { num3 = double.MaxValue; } else if (!double.TryParse(array[1], out num3)) { result = new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Failed to parse: '" + array[1] + "' as a number!"); } break; } } } else { result = new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Range operator: '" + condition + "' uses .. more than once!"); } return result == null; } protected static void LogDebug(string name, object message) { Debuggers.ConditionsInfo?.Log($"({name}) {message}"); } } internal sealed class InvalidCondition : Condition { [CompilerGenerated] private string P; public InvalidCondition(string type) { P = type; base..ctor(); } public override bool Evaluate(IContext context) { return false; } public override List Validate() { if (string.IsNullOrEmpty(P)) { return new List(1) { new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Condition must have a type!") }; } return new List(1) { new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "'" + P + "' is not a valid condition type!") }; } } internal sealed class ConstantCondition : Condition { public static ConstantCondition TRUE = new ConstantCondition(constant: true); public static ConstantCondition FALSE = new ConstantCondition(constant: false); public bool Value { get; private set; } private ConstantCondition(bool constant) { Value = constant; } public override bool Evaluate(IContext context) { return Value; } } public abstract class Condition : Condition where TContext : IContext { public override bool Evaluate(IContext context) { if (!(context is TContext context2)) { return EvaluateFallback(context); } return EvaluateWithContext(context2); } protected abstract bool EvaluateWithContext(TContext context); protected virtual bool EvaluateFallback(IContext context) { return false; } } public abstract class Conditional : IValidatable, IPackData { public Condition Condition { get; set; } public abstract SoundPack Pack { get; set; } public bool Evaluate(IContext context) { if (Condition == null) { return true; } return Condition.Evaluate(context); } public virtual List Validate() { if (Condition == null) { return new List(); } Condition.Parent = this; Condition.OnRegistered(); return Condition.Validate(); } } public interface IContext { } internal class DefaultConditionContext : IContext { internal static readonly DefaultConditionContext DEFAULT = new DefaultConditionContext(); private DefaultConditionContext() { } } [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] [MeansImplicitUse] public class SoundAPIConditionAttribute : Attribute { public string ID { get; private set; } public bool IsDeprecated { get; private set; } public string DeprecationReason { get; private set; } public SoundAPIConditionAttribute(string id, bool deprecated = false, string deprecationReason = null) { ID = id; IsDeprecated = deprecated; DeprecationReason = deprecationReason; base..ctor(); } } } namespace loaforcsSoundAPI.SoundPacks.Conditions { public abstract class LogicGateCondition : Condition { public abstract Condition[] Conditions { get; protected set; } protected abstract string ValidateWarnMessage { get; } protected internal override void OnRegistered() { Condition[] conditions = Conditions; foreach (Condition condition in conditions) { condition.Parent = base.Parent; condition.OnRegistered(); } } public override List Validate() { if (Conditions.Length == 0) { return new List(1) { new IValidatable.ValidationResult(IValidatable.ResultType.WARN, ValidateWarnMessage) }; } return new List(); } protected static bool And(Condition[] conditions, IContext context) { foreach (Condition condition in conditions) { if (condition is InvalidCondition) { return false; } if (!condition.Evaluate(context)) { return false; } } return true; } protected static bool Or(Condition[] conditions, IContext context) { foreach (Condition condition in conditions) { if (condition is InvalidCondition) { return false; } if (condition.Evaluate(context)) { return true; } } return false; } } [SoundAPICondition("and", false, null)] internal class AndCondition : LogicGateCondition { public override Condition[] Conditions { get; protected set; } protected override string ValidateWarnMessage => "'and' condition has no conditions and will always return true!"; public override bool Evaluate(IContext context) { return LogicGateCondition.And(Conditions, context); } } [SoundAPICondition("nand", false, null)] internal class NandCondition : LogicGateCondition { public override Condition[] Conditions { get; protected set; } protected override string ValidateWarnMessage => "'nand' condition has no conditions and will always return false!"; public override bool Evaluate(IContext context) { return !LogicGateCondition.And(Conditions, context); } } [SoundAPICondition("config", false, null)] internal class ConfigCondition : Condition { public string Config { get; private set; } public object Value { get; private set; } public override bool Evaluate(IContext context) { if (!base.Pack.TryGetConfigValue(Config, out var returnValue)) { return false; } if (Value == null) { if (returnValue is bool) { return (bool)returnValue; } if (returnValue is string value) { return string.IsNullOrEmpty(value); } return false; } if (returnValue is bool flag) { return flag == (bool)Value; } if (returnValue is string text) { return text == (string)Value; } return false; } public override List Validate() { if (!base.Pack.TryGetConfigValue(Config, out var returnValue)) { List list = new List(1); list.Add(new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Config '" + Config + "' does not exist on SoundPack '" + base.Pack.Name + "'")); return list; } if (Value != null && returnValue.GetType() != Value.GetType()) { return new List(1) { new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, $"Config '{Config}' has a type of: '{returnValue.GetType()}' but the Value type is '{Value.GetType()}'!") }; } return new List(); } } [SoundAPICondition("counter", false, null)] public class CounterCondition : Condition { private int _count; public string Value { get; private set; } public int? ResetsAt { get; private set; } public override bool Evaluate(IContext context) { Condition.LogDebug("counter", $"counting: {_count} -> {_count + 1}"); _count++; bool flag = EvaluateRangeOperator(_count, Value); Condition.LogDebug("counter", $"is {_count} in range ({Value})? {flag}"); if (_count >= ResetsAt) { _count = 0; Condition.LogDebug("counter", "reset count to 0."); } return flag; } public override List Validate() { if (!ValidateRangeOperator(Value, out var result)) { return new List(1) { result }; } return new List(); } } [SoundAPICondition("mod_installed", false, null)] internal class ModInstalledCondition : Condition { public string Value { get; private set; } public override bool Evaluate(IContext context) { return Chainloader.PluginInfos.ContainsKey(Value); } public override List Validate() { if (string.IsNullOrEmpty(Value)) { return new List(1) { new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "Value on 'mod_installed' must be there and must not be empty.") }; } return new List(); } } [SoundAPICondition("not", false, null)] internal class NotCondition : Condition { public Condition Condition { get; private set; } protected internal override void OnRegistered() { if (Condition != null) { Condition.Parent = base.Parent; Condition.OnRegistered(); } } public override bool Evaluate(IContext context) { if (Condition is InvalidCondition) { return false; } return !Condition.Evaluate(context); } public override List Validate() { if (Condition == null) { return new List(1) { new IValidatable.ValidationResult(IValidatable.ResultType.FAIL, "'not' condition has no valid condition to invert!") }; } return new List(); } } [SoundAPICondition("or", false, null)] internal class OrCondition : LogicGateCondition { public override Condition[] Conditions { get; protected set; } protected override string ValidateWarnMessage => "'or' condition has no conditions and will always return false!"; public override bool Evaluate(IContext context) { return LogicGateCondition.Or(Conditions, context); } } [SoundAPICondition("nor", false, null)] internal class NorCondition : LogicGateCondition { public override Condition[] Conditions { get; protected set; } protected override string ValidateWarnMessage => "'nor' condition has no conditions and will always return true!"; public override bool Evaluate(IContext context) { return !LogicGateCondition.Or(Conditions, context); } } } namespace loaforcsSoundAPI.Reporting { public static class SoundReportHandler { private const string _datetimeFormat = "dd_MM_yyyy-HH_mm"; private static Action _reportSections = delegate { }; public static SoundReport CurrentReport { get; private set; } public static void AddReportSection(string header, Action callback) { _reportSections = (Action)Delegate.Combine(_reportSections, (Action)delegate(StreamWriter stream, SoundReport report) { stream.WriteLine("## " + header); callback(stream, report); stream.WriteLine(""); stream.WriteLine(""); }); } internal static void Register() { Directory.CreateDirectory(GetFolder()); CurrentReport = new SoundReport(); loaforcsSoundAPI.Logger.LogWarning((object)"SoundAPI is generating a report!"); loaforcsSoundAPI.Logger.LogInfo((object)("The report will be located at '" + LogFormats.FormatFilePath(Path.Combine(GetFolder(), GetFileName(CurrentReport, ".md"))))); Application.quitting += delegate { WriteReportToFile(CurrentReport); }; SoundPackLoadPipeline.OnFinishedPipeline += delegate { foreach (SoundPack loadedPack in SoundPackDataHandler.LoadedPacks) { CurrentReport.SoundPackNames.Add(loadedPack.Name); } }; AddReportSection("General Information", delegate(StreamWriter stream, SoundReport report) { stream.WriteLine("Game name: `" + Application.productName + "` `v" + Application.version + "` by `" + Application.companyName + "`
"); stream.WriteLine("Unity version: `" + Application.unityVersion + "
"); stream.WriteLine($"Native Backend supported: `{NativeBackend.TryGetSettings(out var _)}`, enabled: `{NativeBackend.Enabled}`
"); stream.WriteLine("SoundAPI version: `2.0.11`

"); stream.WriteLine($"Audio-clips loaded: `{report.AudioClipsLoaded}`
"); stream.WriteLine($"Match strings registered: `{SoundPackDataHandler.SoundReplacements.Values.Sum((List it) => it.Count)}`
"); WriteList("Loaded sound-packs", stream, report.SoundPackNames); }); AddReportSection("Dynamic Data", delegate(StreamWriter stream, SoundReport _) { if (SoundAPI.CurrentNetworkAdapter != null) { stream.WriteLine("Network Adapter: `" + SoundAPI.CurrentNetworkAdapter.Name + "`

"); } WriteList("Registered Conditions", stream, SoundPackDataHandler.conditionFactories.Keys.ToList()); }); AddReportSection("All Played Sounds", delegate(StreamWriter stream, SoundReport report) { WriteList(null, stream, report.PlayedSounds.Select((SoundReport.PlayedSound it) => it.FormatForReport()).ToList()); }); AddReportSection("Native Backend", delegate { }); } internal static void Bind(ConfigFile file) { if (file.Bind("Developer", "GenerateReports", false, "While true SoundAPI will generate a json and markdown file per session that records information SoundAPI and related mods find.").Value) { Register(); } } private static string GetFileName(SoundReport report, string extension) { return "generated_report-" + report.StartedAt.ToString("dd_MM_yyyy-HH_mm") + extension; } private static string GetFolder() { return Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "reports"); } private static void WriteReportToFile(SoundReport report) { using StreamWriter streamWriter = new StreamWriter(Path.Combine(GetFolder(), GetFileName(report, ".md"))); streamWriter.WriteLine("# Generated Report"); streamWriter.WriteLine($"At {report.StartedAt} :3"); streamWriter.WriteLine(""); _reportSections(streamWriter, report); streamWriter.Flush(); streamWriter.Close(); using StreamWriter streamWriter2 = new StreamWriter(Path.Combine(GetFolder(), GetFileName(report, ".json"))); streamWriter2.WriteLine(JsonConvert.SerializeObject((object)report, (Formatting)1)); } public static void WriteList(string header, StreamWriter stream, ICollection list) { if (!string.IsNullOrEmpty(header)) { stream.WriteLine($"### {header} (`{list.Count}`)"); } stream.WriteLine(string.Join("
\n", list.Select((string it) => "- " + it))); } public static void WriteEnum(string header, StreamWriter stream) where T : Enum { WriteList(header, stream, (from it in Enum.GetValues(typeof(T)).OfType() select it.ToString().ToLowerInvariant()).ToList()); } } } namespace loaforcsSoundAPI.Reporting.Data { public class SoundReport { public class PlayedSound { public string MatchString { get; private set; } public string Caller { get; private set; } public bool IsPlayOnAwake { get; private set; } public PlayedSound(string matchString, string caller, bool isPlayOnAwake) { MatchString = matchString; Caller = caller; IsPlayOnAwake = isPlayOnAwake; base..ctor(); } public override bool Equals(object obj) { if (!(obj is PlayedSound other)) { return false; } return Equals(other); } protected bool Equals(PlayedSound other) { if (MatchString == other.MatchString && Caller == other.Caller) { return IsPlayOnAwake == other.IsPlayOnAwake; } return false; } public override int GetHashCode() { return HashCode.Combine(MatchString, Caller, IsPlayOnAwake); } public string FormatForReport() { return $"Match String: {MatchString}, Caller: {Caller}, IsPlayOnAwake: {IsPlayOnAwake}"; } } public DateTime StartedAt { get; private set; } = DateTime.Now; public List PlayedSounds { get; private set; } = new List(); public List SoundPackNames { get; private set; } = new List(); public int AudioClipsLoaded { get; set; } } } namespace loaforcsSoundAPI.Core { public class AudioSourceAdditionalData { private SoundReplacementGroup _replacedWith; public AudioSource Source { get; private set; } public AudioClip OriginalClip { get; internal set; } public AudioClip RealClip { get { using (new SpoofBypassContext()) { if (Debuggers.AudioSourceAdditionalData != null) { string text = "null"; if (Object.op_Implicit((Object)(object)Source.clip)) { text = ((Object)Source.clip).name; } string text2 = "null"; if (Object.op_Implicit((Object)(object)OriginalClip)) { text2 = ((Object)OriginalClip).name; } Debuggers.AudioSourceAdditionalData.Log("(" + ((Object)Source).name + ") Getting real clip: " + text + " (original clip: " + text2 + ")"); } return Source.clip; } } set { using (new SpoofBypassContext()) { if (Debuggers.AudioSourceAdditionalData != null) { string text = "null"; if (Object.op_Implicit((Object)(object)OriginalClip)) { text = ((Object)OriginalClip).name; } Debuggers.AudioSourceAdditionalData?.Log("(" + ((Object)Source).name + ") Setting real clip: " + ((Object)value).name + " (original clip: " + text + ")"); } Source.clip = value; } } } internal SoundReplacementGroup ReplacedWith { get { return _replacedWith; } set { _replacedWith = value; if (RequiresUpdateFunction()) { if (!SoundAPIAudioManager.liveAudioSourceData.Contains(this)) { SoundAPIAudioManager.liveAudioSourceData.Add(this); } } else if (SoundAPIAudioManager.liveAudioSourceData.Contains(this)) { SoundAPIAudioManager.liveAudioSourceData.Remove(this); } } } public bool DisableReplacing { get; set; } public IContext CurrentContext { get; set; } internal AudioSourceAdditionalData(AudioSource source) { Source = source; } internal void Update() { if (RequiresUpdateFunction() && AudioSourceIsPlaying()) { Debuggers.UpdateEveryFrame?.Log("success: updating every frame for " + ((Object)Source).name); IContext context = CurrentContext ?? DefaultConditionContext.DEFAULT; SoundInstance soundInstance = ReplacedWith.Sounds.FirstOrDefault((SoundInstance x) => x.Evaluate(context)); if (soundInstance != null && !((Object)(object)soundInstance.Clip == (Object)(object)Source.clip)) { Debuggers.UpdateEveryFrame?.Log("new clip found, swapping!!"); float time = Source.time; Source.clip = soundInstance.Clip; Source.Play(); Source.time = time; Debuggers.UpdateEveryFrame?.Log("new clip found, swapped"); } } } private bool RequiresUpdateFunction() { if (ReplacedWith != null) { return ReplacedWith.Parent.UpdateEveryFrame; } return false; } private bool AudioSourceIsPlaying() { if (Object.op_Implicit((Object)(object)Source) && ((Behaviour)Source).enabled) { return Source.isPlaying; } return false; } public static AudioSourceAdditionalData GetOrCreate(AudioSource source) { if (SoundAPIAudioManager.audioSourceData.TryGetValue(source, out var value)) { return value; } value = new AudioSourceAdditionalData(source); value.OriginalClip = value.RealClip; SoundAPIAudioManager.audioSourceData[source] = value; Debuggers.AudioSourceAdditionalData?.Log($"created {((Object)((Component)source).gameObject).name} = {((Object)source).m_CachedPtr.ToInt64()}"); return value; } internal static bool TryGet(AudioSource source, out AudioSourceAdditionalData data) { return SoundAPIAudioManager.audioSourceData.TryGetValue(source, out data); } } internal static class Debuggers { internal static DebugLogSource AudioSourceAdditionalData; internal static DebugLogSource SoundReplacementLoader; internal static DebugLogSource SoundReplacementHandler; internal static DebugLogSource MatchStrings; internal static DebugLogSource ConditionsInfo; internal static DebugLogSource UpdateEveryFrame; internal static DebugLogSource AudioClipSpoofing; internal static DebugLogSource NativeBackend; internal static void Bind(ConfigFile file) { FieldInfo[] fields = typeof(Debuggers).GetFields(BindingFlags.Static | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (file.Bind("InternalDebugging", fieldInfo.Name, false, "Enable/Disable this DebugLogSource. Should only be true if you know what you are doing or have been asked to.").Value) { fieldInfo.SetValue(null, new DebugLogSource(fieldInfo.Name)); loaforcsSoundAPI.Logger.LogDebug((object)("created a DebugLogSource for " + fieldInfo.Name + "!")); } else { fieldInfo.SetValue(null, null); loaforcsSoundAPI.Logger.LogDebug((object)("no DebugLogSource for " + fieldInfo.Name + ".")); } } } } internal class DebugLogSource { [CompilerGenerated] private string P; public DebugLogSource(string title) { <title>P = title; base..ctor(); } internal void Log(object message) { loaforcsSoundAPI.Logger.LogDebug((object)$"[Debug-{<title>P}] {message}"); } } internal class SoundAPIAudioManager : MonoBehaviour { internal static readonly Dictionary<AudioSource, AudioSourceAdditionalData> audioSourceData = new Dictionary<AudioSource, AudioSourceAdditionalData>(); internal static readonly List<AudioSourceAdditionalData> liveAudioSourceData = new List<AudioSourceAdditionalData>(); private static SoundAPIAudioManager Instance; private void Awake() { SceneManager.sceneLoaded += delegate { if (!Object.op_Implicit((Object)(object)Instance)) { SpawnManager(); } }; } internal static void SpawnManager() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown loaforcsSoundAPI.Logger.LogInfo((object)"Starting AudioManager."); GameObject val = new GameObject("SoundAPI_AudioManager"); Object.DontDestroyOnLoad((Object)(object)val); Instance = val.AddComponent<SoundAPIAudioManager>(); } private void Update() { Debuggers.UpdateEveryFrame?.Log("sanity check: soundapi audio manager is running!"); foreach (AudioSourceAdditionalData liveAudioSourceDatum in liveAudioSourceData) { liveAudioSourceDatum.Update(); } } private void OnDisable() { loaforcsSoundAPI.Logger.LogDebug((object)"manager disabled"); } private void OnDestroy() { loaforcsSoundAPI.Logger.LogDebug((object)"manager destroyed"); } internal static void RunCleanup() { loaforcsSoundAPI.Logger.LogDebug((object)"cleaning up old audio source entries"); AudioSourceAdditionalData[] array = audioSourceData.Values.ToArray(); foreach (AudioSourceAdditionalData audioSourceAdditionalData in array) { if (!Object.op_Implicit((Object)(object)audioSourceAdditionalData.Source)) { Remove(audioSourceAdditionalData); } } } internal static void Remove(AudioSourceAdditionalData data) { if (liveAudioSourceData.Contains(data)) { liveAudioSourceData.Remove(data); } audioSourceData.Remove(data.Source); } } internal class SpoofBypassContext : IDisposable { public SpoofBypassContext() { AudioSourcePatch.bypassSpoofing = true; } public void Dispose() { AudioSourcePatch.bypassSpoofing = false; } } } namespace loaforcsSoundAPI.Core.Util { public class AdaptiveConfigEntry { public AdaptiveBool State { get; private set; } public bool DefaultValue { get; private set; } public bool? OverrideValue { get; set; } public bool Value => State switch { AdaptiveBool.Enabled => true, AdaptiveBool.Disabled => false, _ => OverrideValue ?? DefaultValue, }; public AdaptiveConfigEntry(AdaptiveBool state, bool defaultValue) { State = state; DefaultValue = defaultValue; } } public enum AdaptiveBool { Automatic, Enabled, Disabled } internal static class LogFormats { internal static string FormatFilePath(string path) { return $"plugins{Path.DirectorySeparatorChar}{Path.Combine(Path.GetRelativePath(Paths.PluginPath, path))}"; } } } namespace loaforcsSoundAPI.Core.Util.Extensions { internal static class AssemblyExtensions { internal static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) { if (assembly == null) { throw new ArgumentNullException("assembly"); } try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where((Type t) => t != null); } } } public static class AsyncOperationExtensions { public static TaskAwaiter GetAwaiter(this AsyncOperation asyncOp) { TaskCompletionSource<AsyncOperation> tcs = new TaskCompletionSource<AsyncOperation>(); asyncOp.completed += delegate(AsyncOperation operation) { tcs.SetResult(operation); }; return ((Task)tcs.Task).GetAwaiter(); } } public static class AudioSourceExtensions { public static void PlayThenDestroy(this AudioSource source) { source.Play(); Object.Destroy((Object)(object)((Component)source).gameObject, source.clip.length); } public static void PlayWithoutReplacement(this AudioSource source) { AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(source); orCreate.DisableReplacing = true; source.Play(); orCreate.DisableReplacing = false; } } public static class ConfigFileExtensions { public static AdaptiveConfigEntry BindAdaptive(this ConfigFile file, string section, string key, bool defaultValue, string description) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown AdaptiveBool value = file.Bind<AdaptiveBool>(section, key, AdaptiveBool.Automatic, new ConfigDescription($"{description}\nAutomatic default: {defaultValue}", (AcceptableValueBase)null, Array.Empty<object>())).Value; return new AdaptiveConfigEntry(value, defaultValue); } } public static class ListExtensions { public static void AddUnique<T>(this List<T> list, T item) { if (!list.Contains(item)) { list.Add(item); } } } } namespace loaforcsSoundAPI.Core.Patches { [HarmonyPatch(typeof(AudioSource))] internal static class AudioSourcePatch { internal static bool bypassSpoofing; [HarmonyPrefix] [HarmonyPatch("PlayOneShot", new Type[] { typeof(AudioClip), typeof(float) })] private static bool PlayOneShot(AudioSource __instance, ref AudioClip clip) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown if (!Object.op_Implicit((Object)(object)clip)) { return true; } AudioSourcePlayEvent @event = new AudioSourcePlayEvent(__instance, clip, isOneShot: true); if (SoundReplacementHandler.TryReplaceAudio(in @event, out var result)) { if (result.Value.IsUpdateEveryFrame) { if (PatchConfig.UEFOneShotWorkaround) { GameObject val = new GameObject("UEFOneShotFix - " + ((Object)clip).name); val.transform.SetParent(((Component)__instance).transform, false); AudioSource val2 = SoundAPI.CopyAudioSource(__instance, val, AudioSourceCopyFlags.DontCopyPlayOnAwake); AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(val2); val2.clip = result.Value.ReplacedClip; orCreate.ReplacedWith = result.Value.ReplacedWith; @event.Data.ReplacedWith = null; val2.PlayThenDestroy(); return false; } loaforcsSoundAPI.Logger.LogWarning((object)"Replacing a PlayOneShot with a UpdateEveryFrame replacer, things are likely to break. Enable `Experiments -> UEFOneShotFix` to make this warning disappear (and help test it ig)"); } clip = result.Value.ReplacedClip; } return true; } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPriority(0)] [HarmonyPrefix] private static void UpdateOriginalClip(AudioSource __instance, AudioClip value, bool __runOriginal) { if (__runOriginal && !bypassSpoofing) { AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(__instance); orCreate.OriginalClip = value; Debuggers.AudioClipSpoofing?.Log("(" + ((Object)((Component)__instance).gameObject).name + ") updating original clip to: " + ((Object)value).name); } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPrefix] private static bool PreventClipRestartingWithSpoofed(AudioSource __instance, AudioClip value) { if (!PatchConfig.AudioClipSpoofing || bypassSpoofing) { return true; } AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(__instance); if ((Object)(object)orCreate.OriginalClip == (Object)(object)value) { Debuggers.AudioClipSpoofing?.Log("prevented clip from restarting"); } return (Object)(object)orCreate.OriginalClip != (Object)(object)value; } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPostfix] private static void SpoofAudioSourceClip(AudioSource __instance, ref AudioClip __result) { if (PatchConfig.AudioClipSpoofing && !bypassSpoofing) { AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(__instance); __result = orCreate.OriginalClip; Debuggers.AudioClipSpoofing?.Log($"({((Object)((Component)__instance).gameObject).name}) spoofing result to {orCreate.OriginalClip}"); } } } [HarmonyPatch(typeof(GameObject))] internal static class GameObjectPatch { [HarmonyPostfix] [HarmonyPatch("AddComponent", new Type[] { typeof(Type) })] internal static void NewAudioSource(GameObject __instance, ref Component __result) { Component obj = __result; AudioSource val = (AudioSource)(object)((obj is AudioSource) ? obj : null); if (val != null) { AudioSourceAdditionalData.GetOrCreate(val); } } } [HarmonyPatch(typeof(Logger))] internal static class LoggerPatch { [HarmonyPrefix] [HarmonyPatch("LogMessage")] private static void ReenableAndSaveConfigs(object data) { if (data is string text && text == "Chainloader startup complete") { loaforcsSoundAPI.Logger.LogInfo((object)"Starting Sound-pack loading pipeline"); SoundPackLoadPipeline.StartPipeline(); } } } internal static class PatchConfig { public enum Backend { HarmonyX, NativeBackend } private const string ExperimentsHeader = "Experiments"; internal static bool AudioClipSpoofing { get; private set; } internal static bool UEFOneShotWorkaround { get; private set; } internal static Backend PreferredBackend { get; private set; } internal static void Bind(ConfigFile file) { AudioClipSpoofing = file.Bind<bool>("Advanced", "AudioClipSpoofing", true, "Should SoundAPI spoof the return value of AudioSource.clip? This improves compatibility in most edge cases.").Value; PreferredBackend = file.Bind<Backend>("Advanced", "PreferredBackend", Backend.NativeBackend, "What backend should SoundAPI try to use? You should only use this option if you know what it means. Note: NativeBackend needs to be supported on a per unity version basis, if the current version is not supported it will fallback to the old HarmonyX backend automatically.").Value; UEFOneShotWorkaround = file.Bind<bool>("Experiments", "UEFOneShotWorkaround", false, "update_every_frame by default does not work on `.PlayOneshot()`. This experiment works around the issue by instead playing the clip on a duplicate AudioSource, allowing UEF to work as intended.").Value; } } } namespace loaforcsSoundAPI.Core.Patches.Native { internal static class AudioSourceNativePatch { [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void PlayDelegate(IntPtr self, IntPtr delay); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void RemoveFromManagerDelegate(IntPtr self); private static PlayDelegate _origPlay; private static RemoveFromManagerDelegate _origRemoveFromManager; internal static void Init(NativeOffsets offsets) { _origPlay = NativeBackend.PatchNative<PlayDelegate>(offsets.AudioSource_Play, Play); if (offsets.AudioSource_RemoveFromManager.HasValue) { _origRemoveFromManager = NativeBackend.PatchNative<RemoveFromManagerDelegate>(offsets.AudioSource_RemoveFromManager.Value, PatchedRemoveFromManager); return; } loaforcsSoundAPI.Logger.LogWarning((object)"No RemoveFromManager offset for this unity version, falling back."); SceneManager.sceneLoaded += delegate { SoundAPIAudioManager.RunCleanup(); }; } private static void PatchedRemoveFromManager(IntPtr self) { AudioSource scriptingWrapper = NativeBackend.GetScriptingWrapper<AudioSource>(self); if (AudioSourceAdditionalData.TryGet(scriptingWrapper, out var data)) { Debuggers.NativeBackend?.Log("AudioSource::RemoveFromManager() cleaned up an audio source"); SoundAPIAudioManager.Remove(data); } _origRemoveFromManager(self); } private static void Play(IntPtr self, IntPtr delay) { AudioSource scriptingWrapper = NativeBackend.GetScriptingWrapper<AudioSource>(self); Debuggers.NativeBackend?.Log($"native detour source = {scriptingWrapper} (gameobject: {((Object)((Component)scriptingWrapper).gameObject).name})"); AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(scriptingWrapper); if (SoundReplacementHandler.TryReplaceAudio(orCreate.Source, orCreate.OriginalClip, out var replacement)) { if (!Object.op_Implicit((Object)(object)replacement)) { return; } orCreate.RealClip = replacement; } Debuggers.NativeBackend?.Log($"AudioSource::Play() with native detour. IntPtr self = {self}"); _origPlay(self, delay); } } internal static class NativeBackend { internal static readonly IntPtr BaseAddress = GetUnityPlayerModule().BaseAddress; private static NativeBackendSettings[] _allSettings = new NativeBackendSettings[3] { new NativeBackendSettings("2022.3.9f1", new NativeOffsets(12188608)), new NativeBackendSettings("2022.3.62f2", new NativeOffsets(12373984, 12387760)), new NativeBackendSettings("2022.3.60f1", new NativeOffsets(12369632)) }; [UsedImplicitly] private static List<NativeDetour> _allDetours = new List<NativeDetour>(); internal static bool Enabled { get; private set; } internal static RuntimePlatform Platform => Application.platform; internal static bool TryGetSettings(out NativeBackendSettings settings) { settings = _allSettings.FirstOrDefault((NativeBackendSettings it) => it.CurrentVersionMatches); return settings != null; } internal static IEnumerable<string> SupportedUnityVersions() { return _allSettings.Select((NativeBackendSettings it) => it.UnityVersion); } private static void Init(NativeBackendSettings settings) { Enabled = true; AudioSourceNativePatch.Init(settings.WindowsReleaseOffsets); } internal static bool TryInit() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Invalid comparison between Unknown and I4 if (!TryGetSettings(out var settings)) { loaforcsSoundAPI.Logger.LogWarning((object)("Native backend is not yet supported for unity version: " + Application.unityVersion)); return false; } if ((int)Platform != 2) { loaforcsSoundAPI.Logger.LogWarning((object)"Native backend is not yet supported on other platforms than Windows. (Proton, etc. may be a workaround?)"); return false; } Init(settings); return true; } internal static T PatchNative<T>(int offset, T patch) where T : Delegate { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown IntPtr intPtr = BaseAddress + offset; IntPtr functionPointerForDelegate = Marshal.GetFunctionPointerForDelegate(patch); NativeDetour val = new NativeDetour(intPtr, functionPointerForDelegate); _allDetours.Add(val); return val.GenerateTrampoline<T>(); } private static ProcessModule GetUnityPlayerModule() { ProcessModuleCollection modules = Process.GetCurrentProcess().Modules; for (int i = 0; i < modules.Count; i++) { ProcessModule processModule = modules[i]; if (processModule.ModuleName.Contains("UnityPlayer")) { return processModule; } } return null; } internal unsafe static int GetInstanceID(IntPtr obj) { if (Object.OffsetOfInstanceIDInCPlusPlusObject == -1) { Object.OffsetOfInstanceIDInCPlusPlusObject = Object.GetOffsetOfInstanceIDInCPlusPlusObject(); } return *(int*)(void*)(obj + Object.OffsetOfInstanceIDInCPlusPlusObject); } internal unsafe static IntPtr GetGCHandle(IntPtr obj) { int num = 24; return *(IntPtr*)(void*)(obj + num); } internal static T GetScriptingWrapper<T>(IntPtr self) where T : Object { if (GetGCHandle(self) != IntPtr.Zero) { IntPtr gCHandle = GetGCHandle(self); return (T)GCHandle.FromIntPtr(gCHandle).Target; } Debuggers.NativeBackend?.Log("gc handle was zero"); int instanceID = GetInstanceID(self); Debuggers.NativeBackend?.Log($"instance id = {instanceID}"); Object val = Resources.InstanceIDToObject(instanceID); Debuggers.NativeBackend?.Log($"obj = {val}"); return (T)(object)val; } } public record NativeBackendSettings(string UnityVersion, NativeOffsets WindowsReleaseOffsets) { public bool CurrentVersionMatches => Application.unityVersion == UnityVersion; } public record NativeOffsets(int AudioSource_Play, int? AudioSource_RemoveFromManager = null); } namespace loaforcsSoundAPI.Core.Patches.Harmony { internal static class HarmonyBackend { internal static void Init(Harmony harmony) { harmony.PatchAll(typeof(HarmonyBackend)); UnityObjectPatch.Init(harmony); SceneManager.sceneLoaded += delegate(Scene scene, LoadSceneMode _) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) SoundAPIAudioManager.RunCleanup(); AudioSource[] array = Object.FindObjectsOfType<AudioSource>(true); foreach (AudioSource val in array) { if (!(((Component)val).gameObject.scene != scene)) { CheckAudioSource(val); } } }; } internal static void CheckAudioSource(AudioSource source) { AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(source); if (source.playOnAwake && ((Behaviour)source).isActiveAndEnabled && Object.op_Implicit((Object)(object)orCreate.RealClip)) { source.Play(); } } [HarmonyPrefix] [HarmonyPatch(typeof(AudioSource), "Play", new Type[] { })] [HarmonyPatch(typeof(AudioSource), "Play", new Type[] { typeof(ulong) })] [HarmonyPatch(typeof(AudioSource), "Play", new Type[] { typeof(double) })] public static bool Play(AudioSource __instance) { Debuggers.SoundReplacementHandler?.Log("HarmonyX Backend: AudioSource.Play patch"); AudioSourceAdditionalData orCreate = AudioSourceAdditionalData.GetOrCreate(__instance); if (SoundReplacementHandler.TryReplaceAudio(__instance, orCreate.OriginalClip, out var replacement)) { if ((Object)(object)replacement == (Object)null) { return false; } orCreate.RealClip = replacement; } return true; } } internal static class UnityObjectPatch { private static void InstantiatePatch(Object __result) { Debuggers.AudioSourceAdditionalData?.Log("aghuobr: " + __result.name); GameObject val = (GameObject)(object)((__result is GameObject) ? __result : null); if (val != null) { CheckInstantiationRecursively(val); } } internal static void Init(Harmony harmony) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown HarmonyMethod val = new HarmonyMethod(typeof(UnityObjectPatch).GetMethod("InstantiatePatch", BindingFlags.Static | BindingFlags.NonPublic)); MethodInfo[] methods = typeof(Object).GetMethods(); foreach (MethodInfo methodInfo in methods) { if (!(methodInfo.Name != "Instantiate")) { Debuggers.AudioSourceAdditionalData?.Log($"patching {methodInfo}"); if (methodInfo.IsGenericMethod) { harmony.Patch((MethodBase)methodInfo.MakeGenericMethod(typeof(Object)), (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } } } private static void CheckInstantiationRecursively(GameObject gameObject) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown AudioSource[] components = gameObject.GetComponents<AudioSource>(); foreach (AudioSource source in components) { HarmonyBackend.CheckAudioSource(source); } foreach (Transform item in gameObject.transform) { Transform val = item; CheckInstantiationRecursively(((Component)val).gameObject); } } } } namespace loaforcsSoundAPI.Core.Networking { public abstract class NetworkAdapter { public abstract string Name { get; } public abstract void OnRegister(); } } namespace loaforcsSoundAPI.Core.JSON { public static class JSONDataLoader { private class MatchesJSONConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(List<string>); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 JToken val = JToken.Load(reader); if ((int)val.Type == 2) { return val.ToObject<List<string>>(); } return new List<string> { ((object)val).ToString() }; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } } private class IncludePrivatePropertiesContractResolver : DefaultContractResolver { internal IncludePrivatePropertiesContractResolver() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown ((DefaultContractResolver)this).NamingStrategy = (NamingStrategy)new SnakeCaseNamingStrategy(); } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) JsonProperty val = ((DefaultContractResolver)this).CreateProperty(member, memberSerialization); if (!val.Writable && member is PropertyInfo propertyInfo) { val.Writable = propertyInfo.GetSetMethod(nonPublic: true) != null; } return val; } } private class ConditionConverter : JsonConverter<Condition> { public override Condition ReadJson(JsonReader reader, Type objectType, Condition existingValue, bool hasExistingValue, JsonSerializer serializer) { JObject val = JObject.Load(reader); string text = ((object)val["type"])?.ToString(); if (string.IsNullOrEmpty(text)) { return new InvalidCondition(null); } Condition condition = SoundPackDataHandler.CreateCondition(text); if (condition == null) { return null; } serializer.Populate(((JToken)val).CreateReader(), (object)condition); if (condition.Constant.GetValueOrDefault()) { if (!condition.Evaluate(DefaultConditionContext.DEFAULT)) { return ConstantCondition.FALSE; } return ConstantCondition.TRUE; } return condition; } public override void WriteJson(JsonWriter writer, Condition value, JsonSerializer serializer) { throw new NotImplementedException("no."); } } private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings { ContractResolver = (IContractResolver)(object)new IncludePrivatePropertiesContractResolver(), Converters = new List<JsonConverter>(2) { (JsonConverter)(object)new MatchesJSONConverter(), (JsonConverter)(object)new ConditionConverter() } }; public static T LoadFromFile<T>(string path) { //IL_0061: Expected O, but got Unknown string text = File.ReadAllText(path); try { T val = JsonConvert.DeserializeObject<T>(text, _settings); if ((object)val is IFilePathAware filePathAware) { filePathAware.FilePath = path; } if ((object)val is Conditional conditional && conditional.Condition != null) { conditional.Condition.Parent = conditional; conditional.Condition.OnRegistered(); } return val; } catch (JsonReaderException val2) { JsonReaderException val3 = val2; loaforcsSoundAPI.Logger.LogError((object)$"Failed to read json file: 'plugins{Path.DirectorySeparatorChar}{Path.GetRelativePath(Paths.PluginPath, path)}'"); loaforcsSoundAPI.Logger.LogError((object)((Exception)(object)val3).Message); string[] array = text.Split("\n"); int num = int.MaxValue; for (int i = Mathf.Max(0, val3.LineNumber - 3); i < Mathf.Min(array.Length, val3.LineNumber + 3); i++) { int num2 = array[i].TakeWhile(char.IsWhiteSpace).Count(); num = Mathf.Min(num, num2); } for (int j = Mathf.Max(0, val3.LineNumber - 3); j < Mathf.Min(array.Length, val3.LineNumber + 3); j++) { string text2 = $"{(j + 1).ToString(),-5}| "; string text3 = array[j]; int num3 = Mathf.Min(array[j].Length, num); string text4 = text2 + text3.Substring(num3, text3.Length - num3).TrimEnd(); if (j + 1 == val3.LineNumber) { text4 += " // <- HERE"; } loaforcsSoundAPI.Logger.LogError((object)text4); } } return default(T); } } } namespace loaforcsSoundAPI.Core.Data { public interface IFilePathAware { string FilePath { get; internal set; } } public interface IValidatable { public enum ResultType { WARN, FAIL } public class ValidationResult { public ResultType Status { get; private set; } public string Reason { get; private set; } public ValidationResult(ResultType resultType, string reason = null) { Status = resultType; Reason = reason ?? string.Empty; base..ctor(); } } private static readonly StringBuilder _stringBuilder = new StringBuilder(); List<ValidationResult> Validate(); internal static bool LogAndCheckValidationResult(string context, List<ValidationResult> results, ManualLogSource logger) { if (results.Count == 0) { return true; } int num = 0; int num2 = 0; foreach (ValidationResult result in results) { switch (result.Status) { case ResultType.WARN: num++; break; case ResultType.FAIL: num2++; break; default: throw new ArgumentOutOfRangeException(); } } _stringBuilder.Clear(); if (num2 != 0) { _stringBuilder.Append(num2); _stringBuilder.Append(" fail(s)"); } if (num != 0) { if (num2 != 0) { _stringBuilder.Append(" and "); } _stringBuilder.Append(num); _stringBuilder.Append(" warning(s)"); } _stringBuilder.Append(" while "); _stringBuilder.Append(context); _stringBuilder.Append(": "); if (num2 != 0) { logger.LogError((object)_stringBuilder); } else { logger.LogWarning((object)_stringBuilder); } foreach (ValidationResult result2 in results) { switch (result2.Status) { case ResultType.WARN: logger.LogWarning((object)("WARN: " + result2.Reason)); break; case ResultType.FAIL: logger.LogError((object)("FAIL: " + result2.Reason)); break; default: throw new ArgumentOutOfRangeException(); } } return num2 == 0; } } }