Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of AnnoyingSpeaker v1.0.0
plugins/AnnoyingSpeaker.dll
Decompiled 3 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using AnnoyingSpeaker.Patches; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.Events; [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("AnnoyingSpeaker")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("AnnoyingSpeaker")] [assembly: AssemblyTitle("AnnoyingSpeaker")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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; } } } namespace AnnoyingSpeaker { internal sealed class AdOverlayController : MonoBehaviour { private const float TickSeconds = 0.5f; internal static AdOverlayController Instance { get; private set; } private static bool InGame { get { if ((Object)(object)StartOfRound.Instance != (Object)null && (Object)(object)GameNetworkManager.Instance != (Object)null) { return (Object)(object)GameNetworkManager.Instance.localPlayerController != (Object)null; } return false; } } private static bool ShouldRun { get { if (PluginConfig.AdsEnabled.Value && InGame) { return SpeakerState.IsHost; } return false; } } private void Awake() { Instance = this; } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void OnEnable() { ((MonoBehaviour)this).StartCoroutine(AdLoop()); } private IEnumerator AdLoop() { while (true) { PluginConfig.AdInterval(out var min, out var max); float remaining = Random.Range(min, max); if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo($"[Ads] next advert in {remaining:F0}s (of {min:F0}-{max:F0}s)."); } while (remaining > 0f) { yield return (object)new WaitForSecondsRealtime(0.5f); if (ShouldRun) { remaining -= 0.5f; } } try { ShowAd(); } catch (Exception arg) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[Ads] advert failed: {arg}"); } } } } private static void ShowAd() { if (!PluginConfig.AdsEnabled.Value || !SpeakerState.IsHost) { return; } HUDManager instance = HUDManager.Instance; if ((Object)(object)instance == (Object)null) { return; } Terminal val = Object.FindObjectOfType<Terminal>(); if ((Object)(object)val == (Object)null || val.buyableItemsList == null || val.itemSalesPercentages == null) { if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo("[Ads] no terminal yet — advert skipped."); } return; } instance.ChooseAdItem(); Plugin.LogInfo("[Ads] advert displayed."); if (PluginConfig.BypassVanillaAdLimits.Value) { TimeOfDay instance2 = TimeOfDay.Instance; if ((Object)(object)instance2 != (Object)null) { instance2.hasShownAdThisQuota = false; } } } } internal sealed class AnnoyingSpeakerController : MonoBehaviour { private const float TickSeconds = 0.25f; private AudioSource _source; private Coroutine _playback; private Coroutine _pendingNoise; private bool _noiseStarted; private static float _lastNoiseTime = float.NegativeInfinity; private float _onDuration; private float _nextSustainedPulse; internal static AnnoyingSpeakerController Instance { get; private set; } internal static float SecondsSinceNoise => Time.realtimeSinceStartup - _lastNoiseTime; internal static bool NoiseIsFresh => SecondsSinceNoise < PluginConfig.InterestFadeSeconds.Value; private static bool InPlayablePhase { get { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return false; } if (instance.shipIsLeaving) { return false; } if (PluginConfig.PlayInOrbit.Value) { return true; } if (instance.shipHasLanded) { return !instance.inShipPhase; } return false; } } private static bool ShouldRun { get { if (PluginConfig.Enabled.Value && SpeakerState.IsOn) { return InPlayablePhase; } return false; } } internal bool IsBroadcasting => _playback != null; private void Awake() { Instance = this; SpeakerState.OnStateChanged += HandleStateChanged; } private void OnDestroy() { SpeakerState.OnStateChanged -= HandleStateChanged; if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void OnEnable() { ((MonoBehaviour)this).StartCoroutine(BroadcastLoop()); } private void HandleStateChanged(bool on) { if (!on) { _onDuration = 0f; _nextSustainedPulse = 0f; CancelPendingNoise(); StopPlayback(); } } private IEnumerator BroadcastLoop() { while (true) { PluginConfig.SpeakerInterval(out var min, out var max); float remaining = Random.Range(min, max); if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo($"[Speaker] next broadcast in {remaining:F1}s (of {min:F0}-{max:F0}s)."); } while (remaining > 0f) { yield return (object)new WaitForSecondsRealtime(0.25f); SpeakerToggle.TryHook(); if (!ShouldRun) { _onDuration = 0f; continue; } remaining -= 0.25f; _onDuration += 0.25f; TickSustainedNoise(); } if (!SpeakerState.IsHost || !ShouldRun) { continue; } try { FireBroadcast(); } catch (Exception arg) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[Speaker] broadcast failed: {arg}"); } } } } private void TickSustainedNoise() { if (!SpeakerState.IsHost || !PluginConfig.AttractDogs.Value || _onDuration < PluginConfig.SustainedNoiseAfterSeconds.Value || _onDuration < _nextSustainedPulse) { return; } _nextSustainedPulse = _onDuration + PluginConfig.SustainedNoiseInterval.Value; if (PluginConfig.SustainedNoiseSkipIfDogNear.Value && DogNearShip()) { if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo("[Speaker] sustained pulse skipped — a dog is already at the ship."); } return; } EmitNoise(); if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo($"[Speaker] sustained noise pulse at the ship ({_onDuration:F0}s on)."); } } private void FireBroadcast() { AudioBankManager.Build(); if (!AudioBankManager.IsReady) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[Speaker] no clips resolved — nothing to broadcast."); } return; } List<SpeakerCue> list = new List<SpeakerCue>(); float value = PluginConfig.Volume.Value; string category; if (Random.value < PluginConfig.ScreamChance.Value && TryPick(5, list)) { category = "scream"; value = PluginConfig.ScreamVolume.Value; } else if (HostileInside() && Random.value < PluginConfig.EmergencyChance.Value && TryBuildWrapped(0, list)) { category = "emergency"; } else if (HasWeather() && Random.value < PluginConfig.WeatherChance.Value && TryBuildWrapped(1, list)) { category = "weather"; } else { list.Clear(); if (!TryBuildAdvert(list)) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)"[Speaker] advert banks are empty — broadcast skipped."); } return; } category = "advert"; } SpeakerState.BroadcastSequence(category, list, value); if (PluginConfig.AttractDogs.Value) { CancelPendingNoise(); _noiseStarted = false; _pendingNoise = ((MonoBehaviour)this).StartCoroutine(BroadcastNoiseRoutine()); } } private static bool TryPick(int bank, List<SpeakerCue> into) { int num = AudioBankManager.RandomIndex(bank); if (num < 0) { return false; } into.Add(new SpeakerCue(bank, num)); return true; } private static bool TryBuildWrapped(int bank, List<SpeakerCue> into) { into.Clear(); TryPick(4, into); if (!TryPick(bank, into)) { into.Clear(); return false; } TryPick(4, into); return true; } private static bool TryBuildAdvert(List<SpeakerCue> into) { int num = Mathf.Max(1, PluginConfig.AdClipsMin.Value); int num2 = Mathf.Max(num, PluginConfig.AdClipsMax.Value); int num3 = Random.Range(num, num2 + 1); if (!TryPick(2, into)) { for (int i = 0; i < num3; i++) { TryPick(3, into); } return into.Count > 0; } int num4 = Mathf.Max(0, num3 - ((num3 < 3) ? 1 : 2)); for (int j = 0; j < num4; j++) { TryPick(3, into); } if (num3 >= 3) { TryPick(2, into); } return into.Count > 0; } private static bool HostileInside() { List<EnemyAI> list = RoundManager.Instance?.SpawnedEnemies; if (list == null) { return false; } foreach (EnemyAI item in list) { if (!((Object)(object)item == (Object)null) && !item.isEnemyDead && (item is FlowermanAI || item is NutcrackerEnemyAI)) { return true; } } return false; } private static bool HasWeather() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Invalid comparison between Unknown and I4 SelectableLevel val = StartOfRound.Instance?.currentLevel; if ((Object)(object)val != (Object)null) { return (int)val.currentWeather != -1; } return false; } private IEnumerator BroadcastNoiseRoutine() { float delay = PluginConfig.NoiseDelaySeconds.Value; if (delay > 0f) { yield return (object)new WaitForSeconds(delay); } if (!ShouldRun || !PluginConfig.AttractDogs.Value) { _pendingNoise = null; yield break; } _noiseStarted = true; EmitNoise(); if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo($"[Speaker] noise reached the creatures ({delay:F1}s in); " + "it will keep calling them until the clip ends."); } float repeat = Mathf.Max(0.25f, PluginConfig.NoiseRepeatSeconds.Value); while (true) { yield return (object)new WaitForSeconds(repeat); if (!IsBroadcasting || !ShouldRun || !PluginConfig.AttractDogs.Value) { break; } EmitNoise(); } if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo("[Speaker] the broadcast went quiet; the dogs stop being called."); } _pendingNoise = null; } private void CancelPendingNoise() { if (_pendingNoise != null) { ((MonoBehaviour)this).StopCoroutine(_pendingNoise); _pendingNoise = null; } } internal void SkipCurrent(bool cancelNoise) { bool num = _playback != null; bool flag = _pendingNoise != null; bool noiseStarted = _noiseStarted; StopPlayback(); if (cancelNoise) { CancelPendingNoise(); } if (num || flag) { Plugin.LogInfo("[Speaker] broadcast skipped" + ((!(cancelNoise && flag)) ? "." : (noiseStarted ? " - it stops calling the dogs." : " - the dogs never heard it."))); } } internal void PlaySequenceLocal(string category, IList<SpeakerCue> cues, float volume) { if (cues == null || cues.Count == 0) { return; } AudioBankManager.Build(); List<AudioClip> list = (from c in cues select AudioBankManager.Resolve(c.Bank, c.Index) into c where (Object)(object)c != (Object)null select c).ToList(); if (list.Count == 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Speaker] '" + category + "' resolved to no clips on this client.")); } return; } Plugin.LogInfo("[Speaker] " + category + ": " + string.Join(" -> ", list.Select((AudioClip c) => ((Object)c).name)) + " " + $"(vol {volume:F2})"); StopPlayback(); _playback = ((MonoBehaviour)this).StartCoroutine(PlayRoutine(list, volume)); SpeakerToggle.Repaint(); } private void StopPlayback() { bool num = _playback != null; if (_playback != null) { ((MonoBehaviour)this).StopCoroutine(_playback); _playback = null; } if ((Object)(object)_source != (Object)null) { _source.Stop(); } if (num) { SpeakerToggle.Repaint(); } } private IEnumerator PlayRoutine(List<AudioClip> clips, float volume) { AudioSource src = GetSource(); if ((Object)(object)src == (Object)null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[Speaker] the ship speaker was not found — playback skipped."); } yield break; } foreach (AudioClip clip in clips) { if (PluginConfig.Enabled.Value && SpeakerState.IsOn) { src.PlayOneShot(clip, volume); yield return (object)new WaitForSeconds(clip.length); continue; } yield break; } _playback = null; SpeakerToggle.Repaint(); } private AudioSource GetSource() { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown //IL_00dc: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_source != (Object)null) { return _source; } AudioSource val = StartOfRound.Instance?.speakerAudioSource; if ((Object)(object)val == (Object)null) { return null; } Transform val2 = ((Component)val).transform.Find("AnnoyingSpeaker_Source"); if ((Object)(object)val2 != (Object)null) { _source = ((Component)val2).GetComponent<AudioSource>(); if ((Object)(object)_source != (Object)null) { return _source; } } GameObject val3 = new GameObject("AnnoyingSpeaker_Source"); val3.transform.SetParent(((Component)val).transform, false); _source = val3.AddComponent<AudioSource>(); _source.playOnAwake = false; _source.spatialBlend = val.spatialBlend; _source.minDistance = val.minDistance; _source.maxDistance = val.maxDistance; _source.rolloffMode = val.rolloffMode; _source.spread = val.spread; _source.dopplerLevel = val.dopplerLevel; _source.outputAudioMixerGroup = val.outputAudioMixerGroup; Plugin.LogInfo("[Speaker] playback source attached to the ship speaker."); return _source; } private static bool DogNearShip() { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) AudioSource val = StartOfRound.Instance?.speakerAudioSource; List<EnemyAI> list = RoundManager.Instance?.SpawnedEnemies; if ((Object)(object)val == (Object)null || list == null) { return false; } float value = PluginConfig.NoiseRange.Value; foreach (EnemyAI item in list) { if (!((Object)(object)item == (Object)null) && !item.isEnemyDead && item is MouthDogAI && Vector3.Distance(((Component)item).transform.position, ((Component)val).transform.position) <= value) { return true; } } return false; } internal static void EmitNoise() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) RoundManager instance = RoundManager.Instance; AudioSource val = StartOfRound.Instance?.speakerAudioSource; if ((Object)(object)instance == (Object)null || (Object)(object)val == (Object)null) { return; } try { instance.PlayAudibleNoise(((Component)val).transform.position, PluginConfig.NoiseRange.Value, PluginConfig.NoiseLoudness.Value, 0, false, 0); _lastNoiseTime = Time.realtimeSinceStartup; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Speaker] noise emit failed: " + ex.Message)); } } } } internal static class AudioBankManager { internal static readonly string[] BankNames = new string[6] { "emergency", "weather", "ad_jingle", "ad_item", "static", "scream" }; internal const int BankEmergency = 0; internal const int BankWeather = 1; internal const int BankAdJingle = 2; internal const int BankAdItem = 3; internal const int BankStatic = 4; internal const int BankScream = 5; private static readonly Dictionary<string, List<AudioClip>> Banks = new Dictionary<string, List<AudioClip>>(); private static bool _built; private const int TournamentSize = 4; private static readonly Dictionary<string, Type> TypeCache = new Dictionary<string, Type>(); private static readonly Dictionary<Type, Object> InstanceCache = new Dictionary<Type, Object>(); internal static bool IsReady { get { if (_built) { return Banks.Values.Any((List<AudioClip> b) => b.Count > 0); } return false; } } internal static void Invalidate() { Banks.Clear(); _built = false; } internal static void Build() { if (_built) { return; } Banks.Clear(); AddBank("emergency", PluginConfig.BankEmergency.Value); AddBank("weather", PluginConfig.BankWeather.Value); AddBank("ad_jingle", PluginConfig.BankAdJingle.Value); AddBank("ad_item", PluginConfig.BankAdItem.Value); AddBank("static", PluginConfig.BankStatic.Value); AddBank("scream", PluginConfig.BankScream.Value); _built = true; string[] bankNames = BankNames; foreach (string text in bankNames) { int count = Get(text).Count; if (count == 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Banks] '" + text + "' resolved to 0 clips — check its Type.field tokens in the config.")); } continue; } List<AudioClip> source = Get(text); Plugin.LogInfo($"[Banks] '{text}': {count} clip(s), " + $"{source.Min((AudioClip c) => c.length):F2}-{source.Max((AudioClip c) => c.length):F2}s."); } if (!PluginConfig.VerboseLogging.Value) { return; } bankNames = BankNames; foreach (string text2 in bankNames) { Plugin.LogInfo("[Banks] " + text2 + " = " + string.Join(", ", from c in Get(text2) select ((Object)c).name)); } } internal static List<AudioClip> Get(string bank) { if (!Banks.TryGetValue(bank, out var value)) { return new List<AudioClip>(); } return value; } internal static List<AudioClip> Get(int bankIndex) { if (bankIndex < 0 || bankIndex >= BankNames.Length) { return new List<AudioClip>(); } return Get(BankNames[bankIndex]); } internal static int RandomIndex(int bankIndex) { List<AudioClip> list = Get(bankIndex); if (list.Count == 0) { return -1; } if (list.Count == 1 || !PluginConfig.PreferLongestClips.Value) { return Random.Range(0, list.Count); } int num = Random.Range(0, list.Count); for (int i = 1; i < 4; i++) { int num2 = Random.Range(0, list.Count); if (list[num2].length > list[num].length) { num = num2; } } return num; } internal static AudioClip Resolve(int bankIndex, int clipIndex) { List<AudioClip> list = Get(bankIndex); if (clipIndex < 0 || clipIndex >= list.Count) { return null; } return list[clipIndex]; } private static void AddBank(string bankName, string tokens) { List<AudioClip> list = new List<AudioClip>(); HashSet<AudioClip> hashSet = new HashSet<AudioClip>(); string[] array = (tokens ?? string.Empty).Split(','); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } try { foreach (AudioClip item in ResolveToken(text)) { if ((Object)(object)item != (Object)null && hashSet.Add(item)) { list.Add(item); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Banks] token '" + text + "' failed: " + ex.Message)); } } } float minLength = PluginConfig.MinClipSeconds.Value; if (minLength > 0f) { List<AudioClip> list2 = list.Where((AudioClip c) => c.length >= minLength).ToList(); if (list2.Count > 0) { if (list2.Count < list.Count && PluginConfig.VerboseLogging.Value) { Plugin.LogInfo($"[Banks] '{bankName}': dropped {list.Count - list2.Count} " + $"clip(s) shorter than {minLength:F2}s."); } list = list2; } else { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[Banks] '" + bankName + "': every clip is shorter than MinClipSeconds " + $"({minLength:F2}s) — keeping them all rather than emptying the bank.")); } } } list.Sort((AudioClip a, AudioClip b) => string.CompareOrdinal(((Object)a).name, ((Object)b).name)); Banks[bankName] = list; } private static IEnumerable<AudioClip> ResolveToken(string token) { int num = token.LastIndexOf('.'); if (num <= 0 || num == token.Length - 1) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Banks] token '" + token + "' is not in Type.field form — skipped.")); } yield break; } string text = token.Substring(0, num); string text2 = token.Substring(num + 1); Type type = FindGameType(text); if (type == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[Banks] unknown type '" + text + "' — skipped.")); } yield break; } FieldInfo fieldInfo = AccessField(type, text2); if (fieldInfo == null) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("[Banks] '" + text + "' has no AudioClip field '" + text2 + "' — skipped.")); } yield break; } object obj = FindInstance(type); if (obj == null) { if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo("[Banks] no live instance of '" + text + "' yet — '" + token + "' skipped this round."); } yield break; } object value = fieldInfo.GetValue(obj); AudioClip val = (AudioClip)((value is AudioClip) ? value : null); if (val != null) { yield return val; } else if (value is AudioClip[] array) { AudioClip[] array2 = array; for (int i = 0; i < array2.Length; i++) { yield return array2[i]; } } } private static Type FindGameType(string simpleName) { if (TypeCache.TryGetValue(simpleName, out var value)) { return value; } Type type = typeof(StartOfRound).Assembly.GetTypes().FirstOrDefault((Type t) => string.Equals(t.Name, simpleName, StringComparison.Ordinal)); if (type != null) { TypeCache[simpleName] = type; return type; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetTypes().FirstOrDefault((Type t) => string.Equals(t.Name, simpleName, StringComparison.Ordinal)); } catch (ReflectionTypeLoadException) { continue; } catch (Exception) { continue; } if (type != null) { break; } } TypeCache[simpleName] = type; return type; } private static FieldInfo AccessField(Type type, string name) { FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (field == null) { return null; } if (!(field.FieldType == typeof(AudioClip)) && !(field.FieldType == typeof(AudioClip[]))) { return null; } return field; } private static Object FindInstance(Type type) { if (InstanceCache.TryGetValue(type, out var value) && value != (Object)null) { return value; } Object val = null; PropertyInfo property = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public); if (property != null && typeof(Object).IsAssignableFrom(property.PropertyType)) { object? value2 = property.GetValue(null); val = (Object)((value2 is Object) ? value2 : null); } if (val == (Object)null) { FieldInfo field = type.GetField("Instance", BindingFlags.Static | BindingFlags.Public); if (field != null && typeof(Object).IsAssignableFrom(field.FieldType)) { object? value3 = field.GetValue(null); val = (Object)((value3 is Object) ? value3 : null); } } if (val == (Object)null) { Object[] array = Resources.FindObjectsOfTypeAll(type); if (array != null && array.Length != 0) { val = array.OrderBy<Object, string>((Object o) => o.name, StringComparer.Ordinal).First(); } } if (val != (Object)null) { InstanceCache[type] = val; } return val; } } [BepInPlugin("Solon.AnnoyingSpeaker", "AnnoyingSpeaker", "1.0.0")] public sealed class Plugin : BaseUnityPlugin { public const string GUID = "Solon.AnnoyingSpeaker"; public const string NAME = "AnnoyingSpeaker"; public const string VERSION = "1.0.0"; private Harmony _harmony; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; PluginConfig.Bind(((BaseUnityPlugin)this).Config); GameObject val = new GameObject("AnnoyingSpeaker_Manager") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)val); val.AddComponent<AnnoyingSpeakerController>(); val.AddComponent<AdOverlayController>(); _harmony = new Harmony("Solon.AnnoyingSpeaker"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); ApplyPatches(); LogInfo("AnnoyingSpeaker v1.0.0 loaded. The Company would like a word."); } private void ApplyPatches() { TryPatch(typeof(Terminal), new string[1] { "ParsePlayerSentence" }, null, M(typeof(TerminalPatch), "ParsePlayerSentence_Prefix")); TryPatch(typeof(MouthDogAI), new string[1] { "Update" }, null, null, M(typeof(DogHearingPatch), "Update_Postfix")); TryPatch(typeof(TimeOfDay), new string[1] { "MeetsRequirementsToShowAd" }, null, M(typeof(AdGatePatches), "MeetsRequirementsToShowAd_Prefix")); TryPatch(typeof(StartOfRound), new string[1] { "Start" }, null, null, M(typeof(RoundPatches), "StartOfRound_Start_Postfix")); } internal static void LogInfo(string message) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)message); } } private static MethodInfo M(Type type, string name) { return AccessTools.Method(type, name, (Type[])null, (Type[])null); } private void TryPatch(Type targetType, string[] methodNames, Type[] argTypes, MethodInfo prefix = null, MethodInfo postfix = null) { //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) try { MethodInfo methodInfo = null; foreach (string text in methodNames) { methodInfo = AccessTools.Method(targetType, text, argTypes, (Type[])null); if (methodInfo != null) { break; } } if (methodInfo == null) { Log.LogWarning((object)("[Patch] No target found on " + targetType.Name + " (tried: " + string.Join(", ", methodNames) + "). Skipped.")); } else { _harmony.Patch((MethodBase)methodInfo, (!(prefix != null)) ? ((HarmonyMethod)null) : new HarmonyMethod(prefix), (!(postfix != null)) ? ((HarmonyMethod)null) : new HarmonyMethod(postfix), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); LogInfo("[Patch] Hooked " + targetType.Name + "." + methodInfo.Name + "."); } } catch (Exception arg) { Log.LogError((object)$"[Patch] Failed to patch {targetType.Name}: {arg}"); } } } internal static class PluginConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> StartsOn; internal static ConfigEntry<float> IntervalMinSeconds; internal static ConfigEntry<float> IntervalMaxSeconds; internal static ConfigEntry<float> Volume; internal static ConfigEntry<bool> PlayInOrbit; internal static ConfigEntry<float> ScreamChance; internal static ConfigEntry<float> EmergencyChance; internal static ConfigEntry<float> WeatherChance; internal static ConfigEntry<int> AdClipsMin; internal static ConfigEntry<int> AdClipsMax; internal static ConfigEntry<float> ScreamVolume; internal static ConfigEntry<float> MinClipSeconds; internal static ConfigEntry<bool> PreferLongestClips; internal static ConfigEntry<string> BankAdJingle; internal static ConfigEntry<string> BankAdItem; internal static ConfigEntry<string> BankStatic; internal static ConfigEntry<string> BankEmergency; internal static ConfigEntry<string> BankWeather; internal static ConfigEntry<string> BankScream; internal static ConfigEntry<bool> AdsEnabled; internal static ConfigEntry<float> AdIntervalMinSeconds; internal static ConfigEntry<float> AdIntervalMaxSeconds; internal static ConfigEntry<bool> BypassVanillaAdLimits; internal static ConfigEntry<bool> AttractDogs; internal static ConfigEntry<float> DogHearingMultiplier; internal static ConfigEntry<float> NoiseRange; internal static ConfigEntry<float> NoiseLoudness; internal static ConfigEntry<float> NoiseDelaySeconds; internal static ConfigEntry<float> NoiseRepeatSeconds; internal static ConfigEntry<bool> SkipCancelsNoise; internal static ConfigEntry<float> SustainedNoiseAfterSeconds; internal static ConfigEntry<float> SustainedNoiseInterval; internal static ConfigEntry<bool> SustainedNoiseSkipIfDogNear; internal static ConfigEntry<bool> FadeInterest; internal static ConfigEntry<float> InterestFadeSeconds; internal static ConfigEntry<float> InterestFadeTickSeconds; internal static ConfigEntry<bool> VerboseLogging; internal static void Bind(ConfigFile cfg) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Expected O, but got Unknown //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Expected O, but got Unknown //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Expected O, but got Unknown //IL_0192: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Expected O, but got Unknown //IL_01be: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Expected O, but got Unknown //IL_01ea: Unknown result type (might be due to invalid IL or missing references) //IL_01f4: Expected O, but got Unknown //IL_0222: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Expected O, but got Unknown //IL_025a: Unknown result type (might be due to invalid IL or missing references) //IL_0264: Expected O, but got Unknown //IL_0382: Unknown result type (might be due to invalid IL or missing references) //IL_038c: Expected O, but got Unknown //IL_03ba: Unknown result type (might be due to invalid IL or missing references) //IL_03c4: Expected O, but got Unknown //IL_0428: Unknown result type (might be due to invalid IL or missing references) //IL_0432: Expected O, but got Unknown //IL_0460: Unknown result type (might be due to invalid IL or missing references) //IL_046a: Expected O, but got Unknown //IL_0498: Unknown result type (might be due to invalid IL or missing references) //IL_04a2: Expected O, but got Unknown //IL_04d0: Unknown result type (might be due to invalid IL or missing references) //IL_04da: Expected O, but got Unknown //IL_0508: Unknown result type (might be due to invalid IL or missing references) //IL_0512: Expected O, but got Unknown //IL_055b: Unknown result type (might be due to invalid IL or missing references) //IL_0565: Expected O, but got Unknown //IL_0593: Unknown result type (might be due to invalid IL or missing references) //IL_059d: Expected O, but got Unknown //IL_0601: Unknown result type (might be due to invalid IL or missing references) //IL_060b: Expected O, but got Unknown //IL_0639: Unknown result type (might be due to invalid IL or missing references) //IL_0643: Expected O, but got Unknown Enabled = cfg.Bind<bool>("1. Speaker", "Enabled", true, "Master switch for the ship speaker. Off = the speaker never plays anything and the terminal 'speaker' command is disabled. The HUD advertisement system (section 4) is INDEPENDENT and keeps working even with this off."); StartsOn = cfg.Bind<bool>("1. Speaker", "StartsOn", true, "Whether the speaker begins each session switched ON. Off = the crew has to turn it on themselves with the toggle on the ship or 'speaker on' in the terminal. (Host setting.)"); IntervalMinSeconds = cfg.Bind<float>("1. Speaker", "IntervalMinSeconds", 45f, new ConfigDescription("Shortest wait (seconds) between two commercials. The actual wait is random between min and max. Lower = far more annoying. TIP: set both to ~10 while testing.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 600f), Array.Empty<object>())); IntervalMaxSeconds = cfg.Bind<float>("1. Speaker", "IntervalMaxSeconds", 240f, new ConfigDescription("Longest wait (seconds) between two commercials. Clamped to at least IntervalMinSeconds.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 900f), Array.Empty<object>())); Volume = cfg.Bind<float>("1. Speaker", "Volume", 1f, new ConfigDescription("Volume multiplier for normal commercials (the jumpscare has its own setting below).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 2f), Array.Empty<object>())); PlayInOrbit = cfg.Bind<bool>("1. Speaker", "PlayInOrbit", false, "On = the speaker also talks while you are in orbit between moons, shopping in the terminal. Off (default) = it only runs during a landed round, so the noise always ties back to the real risk of drawing Eyeless Dogs to the ship."); ScreamChance = cfg.Bind<float>("2. Categories", "ScreamChance", 0.05f, new ConfigDescription("Chance (0..1) that a broadcast is replaced by a single loud JUMPSCARE clip instead of a commercial. Rolled first and independently, so it can replace any other category. 0 = never.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); EmergencyChance = cfg.Bind<float>("2. Categories", "EmergencyChance", 0.5f, new ConfigDescription("Chance (0..1) of an EMERGENCY broadcast (alarms + static) when a Bracken or a Nutcracker is currently alive inside the complex. If the roll fails the speaker falls through to weather/advert instead, so it is never silent.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); WeatherChance = cfg.Bind<float>("2. Categories", "WeatherChance", 0.3f, new ConfigDescription("Chance (0..1) of a WEATHER broadcast when the current moon actually has weather (rain, storm, fog, flood, eclipse). Falls through to an advert if the roll fails.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); AdClipsMin = cfg.Bind<int>("2. Categories", "AdClipsMin", 2, new ConfigDescription("Fewest clips stitched together into one fake commercial (jingle, product, jingle).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 8), Array.Empty<object>())); AdClipsMax = cfg.Bind<int>("2. Categories", "AdClipsMax", 4, new ConfigDescription("Most clips stitched together into one fake commercial. Longer = more absurd.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 8), Array.Empty<object>())); ScreamVolume = cfg.Bind<float>("2. Categories", "ScreamVolume", 1f, new ConfigDescription("Volume multiplier for the jumpscare clip. Deliberately separate from the normal volume so you can make it disproportionately loud.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 3f), Array.Empty<object>())); MinClipSeconds = cfg.Bind<float>("3. Audio Banks", "MinClipSeconds", 1f, new ConfigDescription("Throw away any clip SHORTER than this (seconds) when a bank is built. The game is full of 0.1s ticks and blips that are useless as a broadcast; this keeps the banks to sounds long enough to actually be annoying. If a bank would end up empty the filter is ignored for that bank, so it can never silence the mod. 0 = keep everything.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>())); PreferLongestClips = cfg.Bind<bool>("3. Audio Banks", "PreferLongestClips", true, "On = strongly favour the LONGEST clips in a bank. Four candidates are sampled and the longest wins, so meaty sounds come up far more often than short ones while the short ones stay possible. Off = every clip in the bank is equally likely."); BankAdJingle = cfg.Bind<string>("3. Audio Banks", "ad_jingle", "HUDManager.advertMusic,HUDManager.advertMusic2,Terminal.enterTerminalSFX,Terminal.leaveTerminalSFX", "JINGLE bank: the musical sting that opens and closes a fake commercial. Defaults to the game's own advertisement music. Format: comma-separated Type.field tokens."); BankAdItem = cfg.Bind<string>("3. Audio Banks", "ad_item", "Terminal.syncedAudios,Terminal.codeBroadcastSFX,BoomboxItem.musicAudios,WalkieTalkie.switchWalkieTalkiePowerOn,WalkieTalkie.switchWalkieTalkiePowerOff", "PRODUCT bank: the merchandise noises in the middle of a commercial. Defaults to the terminal's purchase-confirmation sounds (the ones you associate with spending credits), boombox music and walkie-talkie clicks."); BankStatic = cfg.Bind<string>("3. Audio Banks", "static", "WalkieTalkie.startTransmissionSFX,WalkieTalkie.stopTransmissionSFX,WalkieTalkie.talkingOnWalkieTalkieNotHeldSFX", "STATIC bank: radio interference used as the transition in and out of an emergency bulletin, so it sounds like a real broadcast cut-in."); BankEmergency = cfg.Bind<string>("3. Audio Banks", "emergency", "StartOfRound.alarmSFX,StartOfRound.firedVoiceSFX,StartOfRound.zeroDaysLeftAlertSFX,HUDManager.shipAlarmHornSFX,HUDManager.warningSFX,HUDManager.globalNotificationSFX", "EMERGENCY bank: alarms and company announcements, played out of context when a Bracken or Nutcracker is alive inside."); BankWeather = cfg.Bind<string>("3. Audio Banks", "weather", "HUDManager.radiationWarningAudio,HUDManager.meteorShowerWarningAudio,StartOfRound.shipCreakSFX", "WEATHER bank: the warning tones used for a weather bulletin."); BankScream = cfg.Bind<string>("3. Audio Banks", "scream", "MouthDogAI.screamSFX,StartOfRound.playerCrushDeath,StartOfRound.playerFallDeath,WalkieTalkie.playerDieOnWalkieTalkieSFX", "JUMPSCARE bank: an existing loud, frightening game sound. Note MouthDogAI is resolved from the loaded enemy PREFABS, so it works even with no dog on the map."); AdsEnabled = cfg.Bind<bool>("4. HUD Ads", "AdsEnabled", true, "On = the company's on-screen advertisement banner (the one with the rotating 3D product, tied to the real store discounts) is shown far more often than vanilla. Fully independent of the speaker, so this keeps working even with section 1 disabled."); AdIntervalMinSeconds = cfg.Bind<float>("4. HUD Ads", "AdIntervalMinSeconds", 90f, new ConfigDescription("Shortest wait (seconds) between two on-screen adverts.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 900f), Array.Empty<object>())); AdIntervalMaxSeconds = cfg.Bind<float>("4. HUD Ads", "AdIntervalMaxSeconds", 300f, new ConfigDescription("Longest wait (seconds) between two on-screen adverts. Clamped to at least the minimum.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 1800f), Array.Empty<object>())); BypassVanillaAdLimits = cfg.Bind<bool>("4. HUD Ads", "BypassVanillaAdLimits", true, "Vanilla only ever shows the advert ONCE PER QUOTA, and only if more than one player is alive and you have already met a quota at least once, so in singleplayer you never see it at all. On = drop those restrictions so adverts appear in solo play and from day one. Off = keep vanilla's gates and merely speed up its timer."); AttractDogs = cfg.Bind<bool>("5. Dogs", "AttractDogs", true, "THE GAMEPLAY COST OF THE SPEAKER. On = every broadcast emits a real audible noise at the ship through the game's own noise system, which Eyeless Dogs hear and investigate. Off = the speaker is pure comedy with no danger attached."); DogHearingMultiplier = cfg.Bind<float>("5. Dogs", "DogHearingMultiplier", 1.5f, new ConfigDescription("While the speaker is ON, multiply how ACCURATELY Eyeless Dogs pinpoint a noise (the game's 'noiseApproximation'). 1.5 = they guess the ship's position from a 1.5x tighter radius, so they home in instead of wandering. 1 = vanilla accuracy.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 5f), Array.Empty<object>())); NoiseRange = cfg.Bind<float>("5. Dogs", "NoiseRange", 45f, new ConfigDescription("How far (metres) a broadcast's noise carries to creatures that listen for sound. Higher = dogs notice the ship from much further away.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 150f), Array.Empty<object>())); NoiseLoudness = cfg.Bind<float>("5. Dogs", "NoiseLoudness", 0.8f, new ConfigDescription("How loud (0..1) the broadcast reads to creatures. Higher = a stronger reaction.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 1f), Array.Empty<object>())); NoiseDelaySeconds = cfg.Bind<float>("5. Dogs", "NoiseDelaySeconds", 5f, new ConfigDescription("Seconds between a broadcast STARTING and the noise reaching the creatures. This is your window: sprint to the speaker and hit [E] to skip the advert before the dogs ever hear it. 0 = they react instantly and the skip cannot save you.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 60f), Array.Empty<object>())); NoiseRepeatSeconds = cfg.Bind<float>("5. Dogs", "NoiseRepeatSeconds", 1.5f, new ConfigDescription("Once the delay above has passed, the broadcast keeps making noise every this many seconds FOR AS LONG AS IT IS STILL PLAYING — a blaring speaker should hold a dog's attention the whole time, not for one instant. The noise stops the moment the music does. Lower = a denser stream of noise (and slightly more CPU).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 15f), Array.Empty<object>())); SkipCancelsNoise = cfg.Bind<bool>("5. Dogs", "SkipCancelsNoise", true, "On = skipping an advert cuts its noise dead — silence it inside NoiseDelaySeconds and the dogs never hear anything at all, silence it later and the noise stops there and then. Off = skipping always costs you one burst of noise anyway, and only spares your ears."); SustainedNoiseAfterSeconds = cfg.Bind<float>("5. Dogs", "SustainedNoiseAfterSeconds", 120f, new ConfigDescription("Leave the speaker on this long (seconds) without a break and it starts pulsing noise at the ship CONTINUOUSLY, between commercials, so dogs come for the ship even while the crew stays perfectly silent. Resets whenever the speaker is switched off.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(15f, 900f), Array.Empty<object>())); SustainedNoiseInterval = cfg.Bind<float>("5. Dogs", "SustainedNoiseInterval", 45f, new ConfigDescription("Seconds between those sustained noise pulses once they have started. Keep this ABOVE about 40s: a dog needs roughly that long to lose interest on its own, so a shorter pulse re-enrages it before it can ever leave and it will camp the ship forever.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 300f), Array.Empty<object>())); SustainedNoiseSkipIfDogNear = cfg.Bind<bool>("5. Dogs", "SustainedNoiseSkipIfDogNear", true, "On = a sustained pulse is skipped while a dog is already at the ship. There is no point shouting for something that has arrived, and re-triggering it at point-blank range is what pins dogs to the ship permanently. Off = pulse regardless."); FadeInterest = cfg.Bind<bool>("5. Dogs", "FadeInterest", true, "On = once the speaker has been quiet for InterestFadeSeconds, dogs that are loitering at the ship lose interest FASTER than vanilla would allow, and wander off. This only speeds up the game's own suspicion countdown; a dog actively hunting a player is never touched."); InterestFadeSeconds = cfg.Bind<float>("5. Dogs", "InterestFadeSeconds", 30f, new ConfigDescription("How long (seconds) after the last broadcast noise a dog stays legitimately interested. Until this passes nothing is faded and its hearing stays sharpened; afterwards the interest starts draining away.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 300f), Array.Empty<object>())); InterestFadeTickSeconds = cfg.Bind<float>("5. Dogs", "InterestFadeTickSeconds", 2f, new ConfigDescription("Seconds between each extra point of suspicion drained during the fade. Lower = dogs give up on the ship faster. Vanilla drains one point every 3-4s on its own, so 2 roughly doubles the speed.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 30f), Array.Empty<object>())); VerboseLogging = cfg.Bind<bool>("6. Debug", "VerboseLogging", false, "Log extra detail (bank contents, network messages, per-clip playback) to the BepInEx console. The one-line 'what category played and which clips' entry is always logged."); } internal static void SpeakerInterval(out float min, out float max) { min = IntervalMinSeconds.Value; max = Mathf.Max(min, IntervalMaxSeconds.Value); } internal static void AdInterval(out float min, out float max) { min = AdIntervalMinSeconds.Value; max = Mathf.Max(min, AdIntervalMaxSeconds.Value); } } internal readonly struct SpeakerCue { internal readonly int Bank; internal readonly int Index; internal SpeakerCue(int bank, int index) { Bank = bank; Index = index; } } internal static class SpeakerState { [CompilerGenerated] private static class <>O { public static HandleNamedMessageDelegate <0>__OnHostReceivedRequest; public static HandleNamedMessageDelegate <1>__OnClientReceivedState; public static HandleNamedMessageDelegate <2>__OnClientReceivedPlay; public static HandleNamedMessageDelegate <3>__OnClientReceivedSkip; } private const string MsgRequest = "AnnoyingSpeaker_Req"; private const string MsgState = "AnnoyingSpeaker_State"; private const string MsgPlay = "AnnoyingSpeaker_Play"; private const string MsgSkip = "AnnoyingSpeaker_Skip"; private const byte OpQueryState = 0; private const byte OpSetState = 1; private const byte OpSkip = 2; private static readonly string[] Categories = new string[4] { "advert", "emergency", "weather", "scream" }; internal static bool IsOn { get; private set; } internal static bool IsHost { get { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null) { return singleton.IsServer; } return false; } } internal static event Action<bool> OnStateChanged; private static byte CategoryId(string name) { int num = Array.IndexOf(Categories, name); return (byte)((num >= 0) ? ((uint)num) : 0u); } private static string CategoryName(byte id) { if (id >= Categories.Length) { return "advert"; } return Categories[id]; } internal unsafe static void OnLocalConnect() { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Expected O, but got Unknown //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Expected O, but got Unknown //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) SafeUnregister(); NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { return; } CustomMessagingManager customMessagingManager = singleton.CustomMessagingManager; if (customMessagingManager == null) { return; } if (singleton.IsServer) { object obj = <>O.<0>__OnHostReceivedRequest; if (obj == null) { HandleNamedMessageDelegate val = OnHostReceivedRequest; <>O.<0>__OnHostReceivedRequest = val; obj = (object)val; } customMessagingManager.RegisterNamedMessageHandler("AnnoyingSpeaker_Req", (HandleNamedMessageDelegate)obj); SetLocal(PluginConfig.StartsOn.Value); Plugin.LogInfo("[Net] Host: speaker starts " + (IsOn ? "ON" : "OFF") + "."); return; } object obj2 = <>O.<1>__OnClientReceivedState; if (obj2 == null) { HandleNamedMessageDelegate val2 = OnClientReceivedState; <>O.<1>__OnClientReceivedState = val2; obj2 = (object)val2; } customMessagingManager.RegisterNamedMessageHandler("AnnoyingSpeaker_State", (HandleNamedMessageDelegate)obj2); object obj3 = <>O.<2>__OnClientReceivedPlay; if (obj3 == null) { HandleNamedMessageDelegate val3 = OnClientReceivedPlay; <>O.<2>__OnClientReceivedPlay = val3; obj3 = (object)val3; } customMessagingManager.RegisterNamedMessageHandler("AnnoyingSpeaker_Play", (HandleNamedMessageDelegate)obj3); object obj4 = <>O.<3>__OnClientReceivedSkip; if (obj4 == null) { HandleNamedMessageDelegate val4 = OnClientReceivedSkip; <>O.<3>__OnClientReceivedSkip = val4; obj4 = (object)val4; } customMessagingManager.RegisterNamedMessageHandler("AnnoyingSpeaker_Skip", (HandleNamedMessageDelegate)obj4); FastBufferWriter val5 = default(FastBufferWriter); ((FastBufferWriter)(ref val5))..ctor(2, (Allocator)2, -1); try { byte b = 0; ((FastBufferWriter)(ref val5)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); b = 0; ((FastBufferWriter)(ref val5)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); customMessagingManager.SendNamedMessage("AnnoyingSpeaker_Req", 0uL, val5, (NetworkDelivery)3); Plugin.LogInfo("[Net] Client: asked the host for the speaker state."); } finally { ((IDisposable)(*(FastBufferWriter*)(&val5))/*cast due to .constrained prefix*/).Dispose(); } } private static void SafeUnregister() { CustomMessagingManager val = (((Object)(object)NetworkManager.Singleton != (Object)null) ? NetworkManager.Singleton.CustomMessagingManager : null); if (val == null) { return; } try { val.UnregisterNamedMessageHandler("AnnoyingSpeaker_Req"); } catch { } try { val.UnregisterNamedMessageHandler("AnnoyingSpeaker_State"); } catch { } try { val.UnregisterNamedMessageHandler("AnnoyingSpeaker_Play"); } catch { } try { val.UnregisterNamedMessageHandler("AnnoyingSpeaker_Skip"); } catch { } } internal unsafe static void RequestSet(bool on) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) if (IsHost) { if (IsOn != on) { SetLocal(on); BroadcastState(); } return; } CustomMessagingManager val = (((Object)(object)NetworkManager.Singleton != (Object)null) ? NetworkManager.Singleton.CustomMessagingManager : null); if (val == null) { return; } try { FastBufferWriter val2 = default(FastBufferWriter); ((FastBufferWriter)(ref val2))..ctor(2, (Allocator)2, -1); try { byte b = 1; ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); b = (on ? ((byte)1) : ((byte)0)); ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); val.SendNamedMessage("AnnoyingSpeaker_Req", 0uL, val2, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val2))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Net] set request failed: " + ex.Message)); } } } internal unsafe static void RequestSkip() { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) if (IsHost) { DoSkip(); return; } CustomMessagingManager val = (((Object)(object)NetworkManager.Singleton != (Object)null) ? NetworkManager.Singleton.CustomMessagingManager : null); if (val == null) { return; } try { FastBufferWriter val2 = default(FastBufferWriter); ((FastBufferWriter)(ref val2))..ctor(2, (Allocator)2, -1); try { byte b = 2; ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); b = 0; ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); val.SendNamedMessage("AnnoyingSpeaker_Req", 0uL, val2, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val2))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Net] skip request failed: " + ex.Message)); } } } private unsafe static void DoSkip() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) CustomMessagingManager val = (((Object)(object)NetworkManager.Singleton != (Object)null) ? NetworkManager.Singleton.CustomMessagingManager : null); if (val != null) { try { FastBufferWriter val2 = default(FastBufferWriter); ((FastBufferWriter)(ref val2))..ctor(1, (Allocator)2, -1); try { byte b = 0; ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); val.SendNamedMessageToAll("AnnoyingSpeaker_Skip", val2, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val2))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Net] skip broadcast failed: " + ex.Message)); } } } AnnoyingSpeakerController.Instance?.SkipCurrent(PluginConfig.SkipCancelsNoise.Value); } private static void SetLocal(bool on) { if (IsOn == on) { return; } IsOn = on; try { SpeakerState.OnStateChanged?.Invoke(on); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Net] state listener threw: " + ex.Message)); } } } private unsafe static void BroadcastState() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) CustomMessagingManager val = (((Object)(object)NetworkManager.Singleton != (Object)null) ? NetworkManager.Singleton.CustomMessagingManager : null); if (val == null) { return; } try { FastBufferWriter val2 = default(FastBufferWriter); ((FastBufferWriter)(ref val2))..ctor(1, (Allocator)2, -1); try { byte b = (IsOn ? ((byte)1) : ((byte)0)); ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); val.SendNamedMessageToAll("AnnoyingSpeaker_State", val2, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val2))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Net] state broadcast failed: " + ex.Message)); } } } internal unsafe static void BroadcastSequence(string category, IList<SpeakerCue> cues, float volume) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) if (cues == null || cues.Count == 0) { return; } CustomMessagingManager val = (((Object)(object)NetworkManager.Singleton != (Object)null) ? NetworkManager.Singleton.CustomMessagingManager : null); if (val != null) { try { int num = 6 + cues.Count * 2; FastBufferWriter val2 = default(FastBufferWriter); ((FastBufferWriter)(ref val2))..ctor(num, (Allocator)2, -1); try { ((FastBufferWriter)(ref val2)).WriteValueSafe<float>(ref volume, default(ForPrimitives)); byte b = CategoryId(category); ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); b = (byte)cues.Count; ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); foreach (SpeakerCue cue in cues) { b = (byte)cue.Bank; ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); b = (byte)cue.Index; ((FastBufferWriter)(ref val2)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); } val.SendNamedMessageToAll("AnnoyingSpeaker_Play", val2, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val2))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Net] sequence broadcast failed: " + ex.Message)); } } } AnnoyingSpeakerController.Instance?.PlaySequenceLocal(category, cues, volume); } private unsafe static void OnHostReceivedRequest(ulong clientId, FastBufferReader reader) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) try { if (!IsHost || !((FastBufferReader)(ref reader)).TryBeginRead(2)) { return; } byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); byte b2 = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b2, default(ForPrimitives)); switch (b) { case 2: DoSkip(); Plugin.LogInfo($"[Net] Client {clientId} skipped the broadcast."); return; case 1: { bool flag = b2 != 0; if (IsOn != flag) { SetLocal(flag); BroadcastState(); Plugin.LogInfo(string.Format("[Net] Client {0} switched the speaker {1}.", clientId, flag ? "ON" : "OFF")); } return; } } CustomMessagingManager customMessagingManager = NetworkManager.Singleton.CustomMessagingManager; FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(1, (Allocator)2, -1); try { byte b3 = (IsOn ? ((byte)1) : ((byte)0)); ((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b3, default(ForPrimitives)); customMessagingManager.SendNamedMessage("AnnoyingSpeaker_State", clientId, val, (NetworkDelivery)3); if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo($"[Net] Host: sent state to client {clientId}."); } } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception arg) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[Net] request handler: {arg}"); } } } private static void OnClientReceivedState(ulong _, FastBufferReader reader) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) try { if (((FastBufferReader)(ref reader)).TryBeginRead(1)) { byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); SetLocal(b != 0); if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo("[Net] Client: speaker is now " + (IsOn ? "ON" : "OFF") + "."); } } } catch (Exception arg) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[Net] state handler: {arg}"); } } } private static void OnClientReceivedSkip(ulong _, FastBufferReader reader) { AnnoyingSpeakerController.Instance?.SkipCurrent(cancelNoise: false); } private static void OnClientReceivedPlay(ulong _, FastBufferReader reader) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) try { if (!((FastBufferReader)(ref reader)).TryBeginRead(6)) { return; } float volume = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe<float>(ref volume, default(ForPrimitives)); byte id = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref id, default(ForPrimitives)); byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); if (b != 0 && ((FastBufferReader)(ref reader)).TryBeginRead(b * 2)) { List<SpeakerCue> list = new List<SpeakerCue>(b); byte bank = default(byte); byte index = default(byte); for (int i = 0; i < b; i++) { ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref bank, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref index, default(ForPrimitives)); list.Add(new SpeakerCue(bank, index)); } AnnoyingSpeakerController.Instance?.PlaySequenceLocal(CategoryName(id), list, volume); } } catch (Exception arg) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[Net] play handler: {arg}"); } } } } [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] internal static class SpeakerConnectPatch { [HarmonyPostfix] private static void Postfix() { try { SpeakerState.OnLocalConnect(); } catch (Exception arg) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)$"[Net] connect hook: {arg}"); } } } } internal static class SpeakerToggle { private static InteractTrigger _trigger; private static Renderer _indicator; private static bool _subscribed; private static float _nextAttempt; private static int _attemptsLeft = 12; private const int MaxAttempts = 12; private const float RetrySeconds = 5f; internal static void Reset() { _trigger = null; _indicator = null; _nextAttempt = 0f; _attemptsLeft = 12; } internal static void TryHook() { if ((Object)(object)_trigger != (Object)null || _attemptsLeft <= 0 || Time.realtimeSinceStartup < _nextAttempt) { return; } _nextAttempt = Time.realtimeSinceStartup + 5f; _attemptsLeft--; if (!_subscribed) { SpeakerState.OnStateChanged += delegate { Repaint(); }; _subscribed = true; } try { _trigger = FindSpeakerTrigger(); if (!((Object)(object)_trigger == (Object)null)) { ((UnityEvent<PlayerControllerB>)(object)_trigger.onInteract).RemoveListener((UnityAction<PlayerControllerB>)OnInteracted); ((UnityEvent<PlayerControllerB>)(object)_trigger.onInteract).AddListener((UnityAction<PlayerControllerB>)OnInteracted); _trigger.triggerOnce = false; _trigger.interactable = true; if (_trigger.cooldownTime > 0.6f) { _trigger.cooldownTime = 0.6f; } _indicator = ((Component)_trigger).GetComponent<Renderer>(); Plugin.LogInfo("[Toggle] hooked the ship speaker trigger at '" + HierarchyPath(((Component)_trigger).transform) + "'" + (((Object)(object)_indicator != (Object)null) ? " (with indicator)" : " (no indicator mesh)")); Repaint(); } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Toggle] could not hook the speaker trigger: " + ex.Message)); } } } private static void OnInteracted(PlayerControllerB player) { if (PluginConfig.Enabled.Value) { SpeakerState.RequestSkip(); } } internal static void Repaint() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_trigger == (Object)null) { return; } try { bool flag = (Object)(object)AnnoyingSpeakerController.Instance != (Object)null && AnnoyingSpeakerController.Instance.IsBroadcasting; _trigger.hoverTip = (flag ? "Speaker: ON" : "Speaker: OFF"); if (!((Object)(object)_indicator == (Object)null)) { Material material = _indicator.material; Color val = (flag ? Color.green : Color.red); if (material.HasProperty("_EmissiveColor")) { material.SetColor("_EmissiveColor", val * 2f); } else if (material.HasProperty("_EmissionColor")) { material.SetColor("_EmissionColor", val * 2f); } } } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Toggle] repaint failed: " + ex.Message)); } _indicator = null; } } private static InteractTrigger FindSpeakerTrigger() { //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) InteractTrigger[] array = Object.FindObjectsOfType<InteractTrigger>(true); if (array == null || array.Length == 0) { return null; } InteractTrigger[] array2 = array; foreach (InteractTrigger val in array2) { if ((Object)(object)val == (Object)null || val.onInteract == null) { continue; } int persistentEventCount = ((UnityEventBase)val.onInteract).GetPersistentEventCount(); for (int j = 0; j < persistentEventCount; j++) { if (((UnityEventBase)val.onInteract).GetPersistentTarget(j) is StartOfRound && string.Equals(((UnityEventBase)val.onInteract).GetPersistentMethodName(j), "DisableShipSpeaker", StringComparison.Ordinal)) { return val; } } } AudioSource speaker = StartOfRound.Instance?.speakerAudioSource; if ((Object)(object)speaker == (Object)null) { return null; } InteractTrigger val2 = array.Where((InteractTrigger t) => (Object)(object)t != (Object)null).OrderBy(delegate(InteractTrigger t) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) Vector3 val3 = ((Component)t).transform.position - ((Component)speaker).transform.position; return ((Vector3)(ref val3)).sqrMagnitude; }).FirstOrDefault(); if ((Object)(object)val2 == (Object)null) { return null; } float num = Vector3.Distance(((Component)val2).transform.position, ((Component)speaker).transform.position); if (num > 5f) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)($"[Toggle] no speaker trigger found (nearest is {num:F1}m away) — " + "use the terminal command instead.")); } return null; } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)$"[Toggle] exact match failed; falling back to the nearest trigger ({num:F1}m)."); } return val2; } private static string HierarchyPath(Transform t) { string text = ((Object)t).name; while ((Object)(object)t.parent != (Object)null) { t = t.parent; text = ((Object)t).name + "/" + text; } return text; } } } namespace AnnoyingSpeaker.Patches { internal static class AdGatePatches { internal static bool MeetsRequirementsToShowAd_Prefix(ref bool __result) { if (!PluginConfig.AdsEnabled.Value || !PluginConfig.BypassVanillaAdLimits.Value) { return true; } __result = true; return false; } } internal static class DogHearingPatch { private static readonly Dictionary<int, float> Baseline = new Dictionary<int, float>(); private static readonly Dictionary<int, float> NextFadeTick = new Dictionary<int, float>(); internal static void Reset() { Baseline.Clear(); NextFadeTick.Clear(); } internal static void Update_Postfix(MouthDogAI __instance) { if ((Object)(object)__instance == (Object)null || ((EnemyAI)__instance).isEnemyDead) { return; } int instanceID = ((Object)__instance).GetInstanceID(); if (!Baseline.TryGetValue(instanceID, out var value)) { value = __instance.noiseApproximation; Baseline[instanceID] = value; } int num; float num2; if (PluginConfig.Enabled.Value && PluginConfig.AttractDogs.Value && SpeakerState.IsOn) { num = (AnnoyingSpeakerController.NoiseIsFresh ? 1 : 0); if (num != 0) { num2 = value * PluginConfig.DogHearingMultiplier.Value; goto IL_0077; } } else { num = 0; } num2 = value; goto IL_0077; IL_0077: float num3 = num2; if (!Mathf.Approximately(__instance.noiseApproximation, num3)) { __instance.noiseApproximation = num3; } if (num != 0) { NextFadeTick.Remove(instanceID); } else { FadeInterest(__instance, instanceID); } } private static void FadeInterest(MouthDogAI dog, int id) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) if (!PluginConfig.FadeInterest.Value || !((NetworkBehaviour)dog).IsOwner) { return; } int currentBehaviourStateIndex = ((EnemyAI)dog).currentBehaviourStateIndex; if ((currentBehaviourStateIndex != 1 && currentBehaviourStateIndex != 2) || (Object)(object)((EnemyAI)dog).targetPlayer != (Object)null) { return; } AudioSource val = StartOfRound.Instance?.speakerAudioSource; if ((Object)(object)val == (Object)null || Vector3.Distance(((Component)dog).transform.position, ((Component)val).transform.position) > PluginConfig.NoiseRange.Value) { return; } float time = Time.time; if (!NextFadeTick.TryGetValue(id, out var value) || !(time < value)) { NextFadeTick[id] = time + PluginConfig.InterestFadeTickSeconds.Value; if (dog.suspicionLevel > 0) { dog.suspicionLevel--; } if (PluginConfig.VerboseLogging.Value) { Plugin.LogInfo("[Dogs] '" + ((Object)((Component)dog).gameObject).name + "' losing interest in the ship " + $"(suspicion {dog.suspicionLevel}, state {currentBehaviourStateIndex})."); } } } } internal static class RoundPatches { internal static void StartOfRound_Start_Postfix() { SpeakerToggle.Reset(); DogHearingPatch.Reset(); AudioBankManager.Invalidate(); Plugin.LogInfo("[Round] ship loaded — caches cleared."); } } internal static class TerminalPatch { internal static bool ParsePlayerSentence_Prefix(Terminal __instance, ref TerminalNode __result) { try { if (!PluginConfig.Enabled.Value || (Object)(object)__instance == (Object)null || (Object)(object)__instance.screenText == (Object)null) { return true; } if ((Object)(object)__instance.currentNode != (Object)null && __instance.currentNode.overrideOptions) { return true; } string[] array = ExtractWords(__instance.screenText.text, __instance.textAdded); if (array.Length == 0 || !string.Equals(array[0], "speaker", StringComparison.Ordinal)) { return true; } string text = HandleCommand(array); if (text == null) { return true; } __result = MakeNode(text); return false; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[Terminal] command parse failed: " + ex.Message)); } return true; } } private static string HandleCommand(string[] words) { if (words.Length == 1) { if (!SpeakerState.IsOn) { return "<color=red>Speaker is INACTIVE.</color> Type 'speaker on' to enable it."; } return "<color=green>Speaker is ACTIVE.</color> Type 'speaker off' to silence it."; } string text = words[1]; if (!(text == "on")) { if (text == "off") { SpeakerState.RequestSet(on: false); return "<color=red>Speaker deactivated.</color>"; } return "Usage: speaker [on|off]"; } SpeakerState.RequestSet(on: true); return "<color=green>Speaker activated. Stay quiet...</color>"; } private static string[] ExtractWords(string fullText, int textAdded) { if (string.IsNullOrEmpty(fullText) || textAdded <= 0 || textAdded > fullText.Length) { return Array.Empty<string>(); } string text = fullText.Substring(fullText.Length - textAdded); StringBuilder stringBuilder = new StringBuilder(text.Length); string text2 = text; foreach (char c in text2) { if (!char.IsPunctuation(c)) { stringBuilder.Append(c); } } return stringBuilder.ToString().ToLower().Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); } private static TerminalNode MakeNode(string message) { TerminalNode obj = ScriptableObject.CreateInstance<TerminalNode>(); obj.displayText = "\n" + message + "\n\n"; obj.clearPreviousText = true; obj.maxCharactersToType = message.Length + 8; return obj; } } }