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 BetterVC v1.0.0
plugins/bobabulkerENTERPRISE.BetterVC.dll
Decompiled 2 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Dissonance; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using Unity.Netcode; using UnityEngine; [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("bobabulkerENTERPRISE.BetterVC")] [assembly: AssemblyConfiguration("Thunderstore")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+1325548ae24a331e2854162f633f3dff7a91fc61")] [assembly: AssemblyProduct("BetterVC")] [assembly: AssemblyTitle("bobabulkerENTERPRISE.BetterVC")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 BetterVC { public enum HudVisibilityMode { Always, Never, WhenHavingWalkie, WhenHoldingWalkie } [BepInPlugin("bobabulkerENTERPRISE.BetterVC", "BetterVC", "1.0.0")] public class Plugin : BaseUnityPlugin { internal static ConfigEntry<float> MixLowerDistance; internal static ConfigEntry<float> MixUpperDistance; internal static ConfigEntry<bool> DebugLogging; internal static ConfigEntry<float> NearbySoundsVolume; internal static ConfigEntry<float> IdleChatterVolume; internal static ConfigEntry<float> RadioEffectStrength; internal static ConfigEntry<float> WalkieVolume; internal static ConfigEntry<HudVisibilityMode> HudVisibility; internal static ConfigEntry<float> HudScale; private Harmony? harmony; internal static bool RunnerLost; internal static ConfigFile? ConfigRef; private static FileSystemWatcher? configWatcher; private static volatile bool configChanged; private static DateTime configReloadAt; private static DateTime ignoreUntil; private static bool compatChecked; private static int errorCount; internal static Plugin Instance { get; private set; } internal static ManualLogSource Logger { get; private set; } private void Awake() { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Expected O, but got Unknown //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Expected O, but got Unknown //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Expected O, but got Unknown //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Expected O, but got Unknown //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Expected O, but got Unknown //IL_0264: Unknown result type (might be due to invalid IL or missing references) //IL_026e: Expected O, but got Unknown Instance = this; Logger = ((BaseUnityPlugin)this).Logger; MixLowerDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Routing", "MixLowerDistance", 20f, new ConfigDescription("Players closer than this (meters) are heard in person.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 200f), Array.Empty<object>())); MixUpperDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Routing", "MixUpperDistance", 25f, new ConfigDescription("Players farther than this (meters) are heard fully through the walkie. In between, the two blend.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 300f), Array.Empty<object>())); WalkieVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "WalkieVolume", 0.75f, new ConfigDescription("Volume of the walkie voice and of your own walkie's beeps. 1 = normal voice volume.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>())); NearbySoundsVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "NearbySoundsVolume", 0.5f, new ConfigDescription("Volume of nearby sounds that transmitting walkies repeat (footsteps, doors, music). 0 = off.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); IdleChatterVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "IdleChatterVolume", 0.5f, new ConfigDescription("Volume of the faint chatter from powered-on walkies that are not yours. 0 = off.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); RadioEffectStrength = ((BaseUnityPlugin)this).Config.Bind<float>("Walkie Audio", "RadioEffectStrength", 0.7f, new ConfigDescription("How radio-like a walkie voice sounds. 0 = crisp, like in person. 1 = the game's full walkie sound.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); DebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugLogging", false, "Write detailed audio and routing diagnostics to the log. Turn on when reporting a bug."); ConfigRef = ((BaseUnityPlugin)this).Config; MixLowerDistance.SettingChanged += OnMixChanged; MixUpperDistance.SettingChanged += OnMixChanged; HudVisibility = ((BaseUnityPlugin)this).Config.Bind<HudVisibilityMode>("HUD", "Visibility", HudVisibilityMode.Always, "When the list of talking players is shown."); HudScale = ((BaseUnityPlugin)this).Config.Bind<float>("HUD", "HudScale", 1f, "Size of the list of talking players. 1 = default."); ((BaseUnityPlugin)this).Config.SettingChanged += OnAnySettingChanged; harmony = new Harmony("bobabulkerENTERPRISE.BetterVC"); harmony.PatchAll(); StartConfigWatcher(); Runner.Create(); Logger.LogInfo((object)"bobabulkerENTERPRISE.BetterVC v1.0.0 has loaded!"); } private static void OnAnySettingChanged(object? sender, SettingChangedEventArgs e) { ConfigEntryBase changedSetting = e.ChangedSetting; if (changedSetting != null) { Logger.LogInfo((object)$"[config] {changedSetting.Definition.Section} / {changedSetting.Definition.Key} is now {changedSetting.BoxedValue}, applied immediately"); } VoiceRouter.MarkDirty(); } private static void OnMixChanged(object? sender, EventArgs e) { Logger.LogInfo((object)$"[config] mix distances now: in person below {MixLowerDistance.Value:F1}m, distortion ramps 0-100% up to {VoiceRouter.MixUpper:F1}m, full walkie beyond"); } private void StartConfigWatcher() { try { string configFilePath = ((BaseUnityPlugin)this).Config.ConfigFilePath; configWatcher = new FileSystemWatcher(Path.GetDirectoryName(configFilePath), Path.GetFileName(configFilePath)) { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite) }; FileSystemEventHandler handler = delegate { if (!(DateTime.UtcNow < ignoreUntil)) { configReloadAt = DateTime.UtcNow.AddMilliseconds(300.0); configChanged = true; } }; configWatcher.Changed += handler; configWatcher.Created += handler; configWatcher.Renamed += delegate(object s, RenamedEventArgs e) { handler(s, e); }; configWatcher.EnableRaisingEvents = true; } catch (Exception ex) { Logger.LogWarning((object)("[config] could not watch the config file for edits (" + ex.Message + "); restart the game to apply edits")); } } private static void ReloadConfigIfChanged() { if (!configChanged || ConfigRef == null || DateTime.UtcNow < configReloadAt) { return; } configChanged = false; try { ConfigRef.Reload(); ignoreUntil = DateTime.UtcNow.AddSeconds(1.0); Logger.LogInfo((object)"[config] config file changed on disk, reloaded"); VoiceRouter.MarkDirty(); } catch (Exception ex) { Logger.LogWarning((object)("[config] could not reload the config file: " + ex.Message)); } } internal static void Debug(string message) { if (DebugLogging != null && DebugLogging.Value) { Logger.LogInfo((object)message); } } private void Update() { FrameDriver.Tick("Plugin.Update"); } private void OnGUI() { FrameDriver.Draw("Plugin.OnGUI"); } private void OnDisable() { Debug("[driver] the plugin component was DISABLED (harmless: the other drivers keep the mod running)"); } private void OnDestroy() { Debug("[driver] the plugin component was DESTROYED (harmless: the other drivers keep the mod running)"); } internal static void PerFrame(float dt) { if (RunnerLost) { RunnerLost = false; Runner.Create(); } try { if (!compatChecked) { compatChecked = true; Compat.ReportOverlaps("bobabulkerENTERPRISE.BetterVC"); } ReloadConfigIfChanged(); VoiceRouter.ResetIfNewLobby(); VoiceRouter.TickIfDirty(); VoiceRouter.UpdateBlend(dt); VoiceHud.Sample(dt); VoiceRouter.LogSnapshot(dt); } catch (Exception arg) { if (errorCount++ < 5) { Logger.LogError((object)$"Per-frame update failed ({errorCount}): {arg}"); } } } } internal static class AutoTransmit { private static readonly FieldInfo? HolderField = AccessTools.Field(typeof(WalkieTalkie), "previousPlayerHeldBy"); private static bool on; private static bool sending; private static int lastPlayers = -1; private static bool warnedOwner; private static bool warnedField; private static bool warnedAnnounce; private const float AnnounceInterval = 10f; private const float BurstInterval = 3f; private const float BurstDuration = 30f; private static float nextAnnounce; private static float burstUntil; private static bool everTransmitted; internal static bool Wanted => on; internal static bool Sending => sending; internal static void Reset() { on = false; sending = false; lastPlayers = -1; nextAnnounce = 0f; burstUntil = 0f; everTransmitted = false; warnedOwner = false; warnedAnnounce = false; } internal static void Tick() { GameNetworkManager instance = GameNetworkManager.Instance; StartOfRound instance2 = StartOfRound.Instance; PlayerControllerB val = (((Object)(object)instance != (Object)null) ? instance.localPlayerController : null); if ((Object)(object)instance2 == (Object)null || (Object)(object)val == (Object)null) { return; } if (HolderField == null) { if (!warnedField) { warnedField = true; Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] WalkieTalkie.previousPlayerHeldBy was not found (game changed?). Automatic transmit is off; the walkie must be used by hand.")); } return; } int num = 0; PlayerControllerB[] allPlayerScripts = instance2.allPlayerScripts; foreach (PlayerControllerB val2 in allPlayerScripts) { if ((Object)(object)val2 != (Object)null && val2.isPlayerControlled) { num++; } } string where; WalkieTalkie val3 = (val.isPlayerDead ? null : VoiceRouter.FindPoweredWalkie(val, out where)); float unscaledTime = Time.unscaledTime; bool flag = num > 1; bool flag2 = lastPlayers >= 0 && num != lastPlayers; if (flag2) { burstUntil = unscaledTime + 30f; nextAnnounce = 0f; } float num2 = ((unscaledTime < burstUntil) ? 3f : 10f); if ((Object)(object)val3 != (Object)null) { val.holdingWalkieTalkie = true; if (!on || !val.speakingToWalkieTalkie) { Send(speaking: true, val3, val, on); nextAnnounce = unscaledTime + num2; } else if (flag && unscaledTime >= nextAnnounce) { Announce(speaking: true, val3, val); nextAnnounce = unscaledTime + num2; if (flag2) { Plugin.Debug($"{Clock.Stamp()} [auto] player count changed ({lastPlayers} -> {num}); transmit state re-sent (and will repeat for {30f:F0}s) so new players see it"); } } } else { if (!val.isPlayerDead && val.holdingWalkieTalkie) { val.holdingWalkieTalkie = false; } if (on) { Send(speaking: false, FindCarrier(val), val, resend: false); nextAnnounce = unscaledTime + num2; } else if (flag && everTransmitted && unscaledTime >= nextAnnounce) { Announce(speaking: false, FindCarrier(val), val); nextAnnounce = unscaledTime + num2; } } lastPlayers = num; } private static void Announce(bool speaking, WalkieTalkie? carrier, PlayerControllerB local) { if ((Object)(object)carrier == (Object)null || !((NetworkBehaviour)carrier).IsOwner) { return; } try { int num = (int)local.playerClientId; if (speaking) { carrier.SetPlayerSpeakingOnWalkieTalkieServerRpc(num); } else { carrier.UnsetPlayerSpeakingOnWalkieTalkieServerRpc(num); } } catch (Exception ex) { if (!warnedAnnounce) { warnedAnnounce = true; Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] could not re-send the transmit state (" + ex.Message + "); other clients that missed the first message will not catch up")); } } } private static WalkieTalkie? FindCarrier(PlayerControllerB local) { List<WalkieTalkie> allWalkieTalkies = WalkieTalkie.allWalkieTalkies; if (allWalkieTalkies == null) { return null; } WalkieTalkie val = null; for (int i = 0; i < allWalkieTalkies.Count; i++) { WalkieTalkie val2 = allWalkieTalkies[i]; if (!((Object)(object)val2 == (Object)null) && ((NetworkBehaviour)val2).IsOwner) { if ((Object)(object)((GrabbableObject)val2).playerHeldBy == (Object)(object)local) { return val2; } if (val == null) { val = val2; } } } return val; } private static void Send(bool speaking, WalkieTalkie? carrier, PlayerControllerB local, bool resend) { if ((Object)(object)carrier == (Object)null || !((NetworkBehaviour)carrier).IsOwner) { if (speaking && !warnedOwner) { warnedOwner = true; Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] this client does not own its walkie, so it cannot start transmitting yet (will retry)")); } if (!speaking) { local.speakingToWalkieTalkie = false; on = false; Plugin.Logger.LogWarning((object)(Clock.Stamp() + " [auto] no walkie to send the stop message through; cleared the flag locally only")); } return; } warnedOwner = false; try { HolderField.SetValue(carrier, local); if (resend || (speaking && local.speakingToWalkieTalkie)) { local.speakingToWalkieTalkie = !speaking; } sending = true; carrier.SetLocalClientSpeaking(speaking); } finally { sending = false; } if (on != speaking) { Plugin.Debug(Clock.Stamp() + " [auto] " + (speaking ? "now transmitting (powered walkie carried)" : "stopped transmitting (no powered walkie)")); } on = speaking; if (speaking) { everTransmitted = true; } } } internal static class Clock { private static bool? lastWasNetwork; internal static string Stamp() { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) NetworkManager singleton = NetworkManager.Singleton; bool flag = (Object)(object)singleton != (Object)null && singleton.IsListening; if (lastWasNetwork != flag) { lastWasNetwork = flag; Plugin.Debug(flag ? "[clock] log lines are now stamped [S...] with the network's server time (comparable across players' logs)" : "[clock] log lines are stamped [L...] with this machine's local time (not comparable across players' logs)"); } if (!flag) { return $"[L{Time.realtimeSinceStartupAsDouble:F2}]"; } NetworkTime serverTime = singleton.ServerTime; return $"[S{((NetworkTime)(ref serverTime)).Time:F2}]"; } } internal static class Compat { internal static void ReportOverlaps(string ownHarmonyId) { try { List<string> list = new List<string>(); foreach (MethodBase item in Harmony.GetAllPatchedMethods().ToList()) { Patches patchInfo = Harmony.GetPatchInfo(item); if (patchInfo == null) { continue; } HashSet<string> hashSet = new HashSet<string>(patchInfo.Owners); if (hashSet.Remove(ownHarmonyId) && hashSet.Count != 0) { list.Add(item.DeclaringType?.Name + "." + item.Name + ": also patched by " + string.Join(", ", hashSet.OrderBy((string o) => o))); } } if (list.Count == 0) { Plugin.Debug("[compat] no other plugin patches a game method that this mod patches"); return; } Plugin.Logger.LogInfo((object)$"[compat] {list.Count} game method(s) are patched by this mod and by other plugins. This is normal and usually fine; if voices sound wrong, include these lines in a bug report:"); foreach (string item2 in list.OrderBy((string l) => l)) { Plugin.Logger.LogInfo((object)("[compat] " + item2)); } } catch (Exception ex) { Plugin.Debug("[compat] could not list overlapping patches: " + ex.Message); } } } internal static class FrameDriver { private static readonly HashSet<string> SeenUpdate = new HashSet<string>(); private static readonly HashSet<string> SeenGui = new HashSet<string>(); private static int lastUpdateFrame = -1; private static int lastRepaintFrame = -1; private static bool guiFailed; internal static void Tick(string source) { if (SeenUpdate.Add(source)) { Plugin.Debug("[driver] update driver '" + source + "' is running"); } int frameCount = Time.frameCount; if (frameCount != lastUpdateFrame) { lastUpdateFrame = frameCount; Plugin.PerFrame(Time.unscaledDeltaTime); } } internal static void Draw(string source) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 Event current = Event.current; if (current == null || (int)current.type != 7) { return; } if (SeenGui.Add(source)) { Plugin.Debug("[driver] OnGUI driver '" + source + "' is running"); } int frameCount = Time.frameCount; if (frameCount == lastRepaintFrame) { return; } lastRepaintFrame = frameCount; if (guiFailed) { return; } try { VoiceHud.Draw(); } catch (Exception arg) { guiFailed = true; Plugin.Logger.LogError((object)$"HUD drawing failed, disabling it: {arg}"); } } } internal sealed class Runner : MonoBehaviour { private static Runner? instance; private static bool quitting; internal static void Create() { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown if ((Object)(object)instance != (Object)null) { return; } if (!quitting) { Application.quitting += delegate { quitting = true; }; } GameObject val = new GameObject("BetterVC.Runner"); ((Object)val).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)val); instance = val.AddComponent<Runner>(); } private void Update() { FrameDriver.Tick("Runner.Update"); } private void OnGUI() { FrameDriver.Draw("Runner.OnGUI"); } private void OnDestroy() { instance = null; if (!quitting) { Plugin.Logger.LogWarning((object)"[driver] the Runner object was destroyed by something (it will be recreated)"); Plugin.RunnerLost = true; } } } internal enum VoiceBand { InPerson, Mixed, Walkie } internal static class VoiceHud { private sealed class Entry { public string Name = ""; public VoiceBand Band; public float Y = float.NaN; public float Presence; public int LastFrame; } private const float FadeOutSeconds = 0.4f; private const float SlideRate = 12f; private const float MinAmplitude = 0.002f; private static readonly Dictionary<ulong, Entry> Entries = new Dictionary<ulong, Entry>(); private static readonly List<ulong> SortedIds = new List<ulong>(); private static GUIStyle? nameStyle; private static Font? gameFont; private static bool fontSearched; private static float debugTimer; private static bool Allowed(PlayerControllerB listener) { switch (Plugin.HudVisibility.Value) { case HudVisibilityMode.Never: return false; case HudVisibilityMode.WhenHavingWalkie: return VoiceRouter.HasActiveWalkie(listener); case HudVisibilityMode.WhenHoldingWalkie: { GrabbableObject currentlyHeldObjectServer = listener.currentlyHeldObjectServer; WalkieTalkie val = (WalkieTalkie)(object)((currentlyHeldObjectServer is WalkieTalkie) ? currentlyHeldObjectServer : null); if (val != null) { return (Object)(object)val != (Object)null; } return false; } default: return true; } } internal static VoiceBand? BandFor(VoiceMode mode, float distance) { switch (mode) { case VoiceMode.Spatial: return VoiceBand.InPerson; case VoiceMode.Blend: return VoiceBand.Mixed; case VoiceMode.Walkie: return VoiceBand.Walkie; default: if (!(distance <= Plugin.MixLowerDistance.Value)) { return null; } return VoiceBand.InPerson; } } internal static void Reset() { Entries.Clear(); debugTimer = 0f; } internal static void Sample(float dt) { //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; PlayerControllerB val = VoiceRouter.CurrentListener(); if ((Object)(object)instance == (Object)null || (Object)(object)val == (Object)null || !Allowed(val)) { Entries.Clear(); return; } debugTimer += dt; bool flag = Plugin.DebugLogging.Value && debugTimer >= 1f; if (flag) { debugTimer = 0f; } PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val2 in allPlayerScripts) { if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2 == (Object)(object)val) && val2.isPlayerControlled && !val2.isPlayerDead) { VoicePlayerState voicePlayerState = val2.voicePlayerState; float distance = Vector3.Distance(((Component)val).transform.position, ((Component)val2).transform.position); VoiceBand? voiceBand = BandFor(VoiceRouter.ModeFor(val2), distance); bool flag2 = voiceBand.HasValue && voicePlayerState != null && voicePlayerState.IsSpeaking && voicePlayerState.Amplitude > 0.002f; float num = (flag2 ? voicePlayerState.Amplitude : 0f); string text = (string.IsNullOrEmpty(val2.playerUsername) ? $"Player {val2.playerClientId}" : val2.playerUsername); Feed(val2.playerClientId, text, flag2, voiceBand.GetValueOrDefault(), dt); if (flag && flag2) { Plugin.Debug($"{Clock.Stamp()} [hud] {text}: {voiceBand}, raw amplitude={num:F4}"); } } } List<ulong> list = null; foreach (KeyValuePair<ulong, Entry> entry in Entries) { Entry value = entry.Value; if (value.LastFrame != Time.frameCount) { value.Presence -= dt / 0.4f; } if (value.Presence <= 0f) { (list ?? (list = new List<ulong>())).Add(entry.Key); } } if (list == null) { return; } foreach (ulong item in list) { Entries.Remove(item); } } internal static void Feed(ulong id, string name, bool speaking, VoiceBand band, float dt) { if (!Entries.TryGetValue(id, out Entry value)) { if (!speaking) { return; } value = new Entry(); Entries[id] = value; } value.LastFrame = Time.frameCount; value.Name = name; if (speaking) { value.Band = band; } if (speaking) { value.Presence = 1f; } else { value.Presence -= dt / 0.4f; } } internal static void Draw() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Invalid comparison between Unknown and I4 //IL_01fd: Unknown result type (might be due to invalid IL or missing references) //IL_0164: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Unknown result type (might be due to invalid IL or missing references) if ((int)Event.current.type != 7) { return; } float num = (float)Screen.height / 1080f * Plugin.HudScale.Value; if (Entries.Count != 0) { EnsureNameStyle(); float num2 = 34f * num; float num3 = 24f * num; SortedIds.Clear(); SortedIds.AddRange(Entries.Keys); SortedIds.Sort(delegate(ulong a, ulong b) { int num8 = Entries[a].Band.CompareTo(Entries[b].Band); return (num8 == 0) ? a.CompareTo(b) : num8; }); float num4 = ((float)Screen.height - (float)SortedIds.Count * num2) / 2f; float num5 = 1f - Mathf.Exp(-12f * Time.unscaledDeltaTime); nameStyle.fontSize = Mathf.Max(10, Mathf.RoundToInt(24f * num)); Rect val = default(Rect); for (int num6 = 0; num6 < SortedIds.Count; num6++) { Entry entry = Entries[SortedIds[num6]]; float num7 = num4 + (float)num6 * num2; entry.Y = (float.IsNaN(entry.Y) ? num7 : Mathf.Lerp(entry.Y, num7, num5)); ((Rect)(ref val))..ctor(num3, entry.Y, 520f * num, num2); GUI.color = new Color(0f, 0f, 0f, 0.85f * entry.Presence); GUI.Label(new Rect(((Rect)(ref val)).x + num * 1.5f, ((Rect)(ref val)).y + num * 1.5f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), entry.Name, nameStyle); Color color = NameColor(entry.Band); color.a = entry.Presence; GUI.color = color; GUI.Label(val, entry.Name, nameStyle); } GUI.color = Color.white; } } private static Color NameColor(VoiceBand band) { //IL_0019: 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_0034: Unknown result type (might be due to invalid IL or missing references) return (Color)(band switch { VoiceBand.Mixed => new Color(1f, 0.9f, 0.2f), VoiceBand.Walkie => new Color(0.3f, 1f, 0.4f), _ => Color.white, }); } private static Font? FindGameFont() { try { TMP_FontAsset[] array = Resources.FindObjectsOfTypeAll<TMP_FontAsset>(); foreach (TMP_FontAsset val in array) { if ((Object)(object)val != (Object)null && ((Object)val).name.IndexOf("3270", StringComparison.OrdinalIgnoreCase) >= 0 && (Object)(object)val.sourceFontFile != (Object)null) { Plugin.Debug("[hud] using the game's font '" + ((Object)val.sourceFontFile).name + "' (from '" + ((Object)val).name + "')"); return val.sourceFontFile; } } Font[] array2 = Resources.FindObjectsOfTypeAll<Font>(); foreach (Font val2 in array2) { if ((Object)(object)val2 != (Object)null && ((Object)val2).name.IndexOf("3270", StringComparison.OrdinalIgnoreCase) >= 0) { Plugin.Debug("[hud] using the game's font '" + ((Object)val2).name + "'"); return val2; } } Plugin.Logger.LogWarning((object)"[hud] the game's 3270 font was not found as a usable Font; using a monospace system font instead"); } catch (Exception ex) { Plugin.Logger.LogWarning((object)("[hud] font lookup failed (" + ex.Message + "); using a monospace system font instead")); } return Font.CreateDynamicFontFromOSFont(new string[4] { "Consolas", "Lucida Console", "DejaVu Sans Mono", "Courier New" }, 24); } private static void EnsureNameStyle() { //IL_0029: 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_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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_004f: Expected O, but got Unknown if (nameStyle == null) { if (!fontSearched) { fontSearched = true; gameFont = FindGameFont(); } nameStyle = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)3, fontStyle = (FontStyle)0, wordWrap = false, clipping = (TextClipping)0 }; if ((Object)(object)gameFont != (Object)null) { nameStyle.font = gameFont; } } } } internal enum VoiceMode { Vanilla, Spatial, Blend, Walkie } internal static class VoiceRouter { private sealed class Link { public float Radio; public VoiceMode Mode; public VoiceMode Applied; public float OcclusionCutoff = 22000f; public float LastBlend = -1f; public float AppliedStrength = -1f; public int FlipFrame = -100; } private struct Fight { public int Count; public float WindowStart; public float PausedUntil; } private const float RadioLowPass = 4000f; private const float RadioQ = 3f; private const float OpenLowPass = 22000f; private static readonly Dictionary<ulong, Link> Links = new Dictionary<ulong, Link>(); private static readonly Dictionary<ulong, float> HighPassDefaults = new Dictionary<ulong, float>(); private static readonly Dictionary<ulong, float> SpreadDefaults = new Dictionary<ulong, float>(); private const float SpreadDoneAtRadio = 0.75f; private static int lastPostfixFrame = -1; private static bool warnedBlendOverwritten; private static string lastCensus = ""; private const float MaxRadioRate = 1.5f; private static bool dirty = true; private static bool inTick; private static StartOfRound? knownRound; private static bool hadRound; private static readonly Dictionary<ulong, string> SeenWalkie = new Dictionary<ulong, string>(); private static float snapshotTimer; private static int driftCount; private const int FightLimit = 6; private const float FightWindow = 4f; private const float FightPause = 30f; private static readonly Dictionary<ulong, Fight> Fights = new Dictionary<ulong, Fight>(); internal static float WalkieVolume => Plugin.WalkieVolume.Value; internal static float RadioEffectStrength => Mathf.Clamp01(Plugin.RadioEffectStrength.Value); internal static float MixUpper => Mathf.Max(Plugin.MixLowerDistance.Value + 0.5f, Plugin.MixUpperDistance.Value); private static float SpreadFor(ulong id, float radio) { float value; return Mathf.Lerp(SpreadDefaults.TryGetValue(id, out value) ? value : 0f, 180f, Ease(Mathf.Clamp01(radio / 0.75f))); } internal static VoiceMode ModeFor(PlayerControllerB player) { if (!Links.TryGetValue(player.playerClientId, out Link value)) { return VoiceMode.Vanilla; } return value.Mode; } internal static float RadioAmountFor(PlayerControllerB player) { if (!Links.TryGetValue(player.playerClientId, out Link value)) { return 0f; } return value.Radio; } internal static bool HasActiveWalkie(PlayerControllerB? player) { return FindActiveWalkie(player) != null; } internal static string? FindActiveWalkie(PlayerControllerB? player) { FindPoweredWalkie(player, out string where); return where; } internal static WalkieTalkie? FindPoweredWalkie(PlayerControllerB? player, out string? where) { where = null; if ((Object)(object)player == (Object)null) { return null; } GrabbableObject itemOnlySlot = player.ItemOnlySlot; WalkieTalkie val = (WalkieTalkie)(object)((itemOnlySlot is WalkieTalkie) ? itemOnlySlot : null); if (val != null && (Object)(object)val != (Object)null && ((GrabbableObject)val).isBeingUsed) { where = "utility slot"; return val; } GrabbableObject[] itemSlots = player.ItemSlots; if (itemSlots != null) { for (int i = 0; i < itemSlots.Length; i++) { GrabbableObject obj = itemSlots[i]; WalkieTalkie val2 = (WalkieTalkie)(object)((obj is WalkieTalkie) ? obj : null); if (val2 != null && (Object)(object)val2 != (Object)null && ((GrabbableObject)val2).isBeingUsed) { where = $"slot {i + 1}"; return val2; } } } List<WalkieTalkie> allWalkieTalkies = WalkieTalkie.allWalkieTalkies; if (allWalkieTalkies != null) { for (int j = 0; j < allWalkieTalkies.Count; j++) { WalkieTalkie val3 = allWalkieTalkies[j]; if ((Object)(object)val3 != (Object)null && ((GrabbableObject)val3).isBeingUsed && (Object)(object)((GrabbableObject)val3).playerHeldBy == (Object)(object)player && !((GrabbableObject)val3).isHeldByEnemy) { where = "carried (extra slot)"; return val3; } } } return null; } private static string SlotOf(PlayerControllerB player, WalkieTalkie walkie) { if ((Object)(object)player.ItemOnlySlot == (Object)(object)walkie) { return "utility slot"; } GrabbableObject[] itemSlots = player.ItemSlots; if (itemSlots != null) { for (int i = 0; i < itemSlots.Length; i++) { if ((Object)(object)itemSlots[i] == (Object)(object)walkie) { return $"slot {i + 1}/{itemSlots.Length}"; } } } return "NOT in ItemSlots (extra-slot mod?)"; } private static void LogCensus() { List<WalkieTalkie> allWalkieTalkies = WalkieTalkie.allWalkieTalkies; if (allWalkieTalkies == null) { return; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < allWalkieTalkies.Count; i++) { WalkieTalkie val = allWalkieTalkies[i]; if (!((Object)(object)val == (Object)null) && !((Object)(object)((GrabbableObject)val).playerHeldBy == (Object)null)) { PlayerControllerB playerHeldBy = ((GrabbableObject)val).playerHeldBy; if (stringBuilder.Length > 0) { stringBuilder.Append("; "); } stringBuilder.Append(string.Format("{0}#{1} {2} {3} @ {4}, transmit flag={5}", playerHeldBy.playerUsername, playerHeldBy.playerClientId, ((GrabbableObject)val).isBeingUsed ? "ON" : "off", ((GrabbableObject)val).isPocketed ? "pocketed" : "in hand", SlotOf(playerHeldBy, val), playerHeldBy.speakingToWalkieTalkie)); } } string text = ((stringBuilder.Length == 0) ? "nobody is carrying a walkie" : stringBuilder.ToString()); if (!(text == lastCensus)) { lastCensus = text; Plugin.Debug(Clock.Stamp() + " [census] walkies as seen by this client: " + text); } } internal static float TargetRadio(float distance) { float value = Plugin.MixLowerDistance.Value; return Mathf.InverseLerp(value, MixUpper, distance); } internal static VoiceMode Classify(float radio) { if (radio <= 0f) { return VoiceMode.Spatial; } if (radio >= 1f) { return VoiceMode.Walkie; } return VoiceMode.Blend; } internal static float SmoothRadio(float current, float target, float dt) { float num = Mathf.Lerp(current, target, 1f - Mathf.Exp(-6f * dt)); float num2 = 1.5f * dt; num = current + Mathf.Clamp(num - current, 0f - num2, num2); if (!(Mathf.Abs(num - target) < 0.004f)) { return num; } return target; } private static float Ease(float t) { t = Mathf.Clamp01(t); return t * t * (3f - 2f * t); } internal static float LogLerp(float from, float to, float t) { if (from <= 0f || to <= 0f) { return Mathf.Lerp(from, to, t); } return from * Mathf.Pow(to / from, Mathf.Clamp01(t)); } internal static float LogLerpUnclamped(float from, float to, float t) { if (from <= 0f || to <= 0f) { return Mathf.LerpUnclamped(from, to, t); } return from * Mathf.Pow(to / from, t); } private static float BaseVolume(bool localDead) { if (!localDead) { return 1f; } return 0.8f; } private static PlayerControllerB? GetListener(PlayerControllerB local) { if (!local.isPlayerDead || !((Object)(object)local.spectatedPlayerScript != (Object)null)) { return local; } return local.spectatedPlayerScript; } internal static PlayerControllerB? CurrentListener() { PlayerControllerB local = GetLocal(); if (!((Object)(object)local == (Object)null)) { return GetListener(local); } return null; } private static PlayerControllerB? GetLocal() { GameNetworkManager instance = GameNetworkManager.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return instance.localPlayerController; } private static bool IsRoutable(PlayerControllerB p, PlayerControllerB local, PlayerControllerB listener) { if ((Object)(object)p == (Object)(object)local || (Object)(object)p == (Object)(object)listener) { return false; } if (!p.isPlayerControlled && !p.isPlayerDead) { return false; } if (p.isPlayerDead) { return false; } if (p.voicePlayerState == null || (Object)(object)p.currentVoiceChatAudioSource == (Object)null || (Object)(object)p.currentVoiceChatIngameSettings == (Object)null) { return false; } if (p.currentVoiceChatIngameSettings._playerState != p.voicePlayerState) { StartOfRound.Instance.RefreshPlayerVoicePlaybackObjects(); if (p.voicePlayerState == null || p.currentVoiceChatIngameSettings._playerState != p.voicePlayerState) { if (Plugin.DebugLogging.Value) { Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [voice] {p.playerUsername} (#{p.playerClientId}): voice state does not match the audio source even after a refresh; leaving them to the base game"); } return false; } Plugin.Debug($"{Clock.Stamp()} [voice] {p.playerUsername} (#{p.playerClientId}): stale voice state replaced after a rejoin"); } return true; } internal static void ResetIfNewLobby() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { if (hadRound) { hadRound = false; knownRound = null; ResetState("lobby ended"); } } else if (instance != knownRound) { bool flag = hadRound; knownRound = instance; hadRound = true; if (flag) { ResetState("a different lobby started"); } } } private static void ResetState(string why) { Links.Clear(); HighPassDefaults.Clear(); SpreadDefaults.Clear(); SeenWalkie.Clear(); lastCensus = ""; lastPostfixFrame = -1; snapshotTimer = 0f; driftCount = 0; Fights.Clear(); dirty = true; inTick = false; AutoTransmit.Reset(); VoiceHud.Reset(); Plugin.Debug(Clock.Stamp() + " [lobby] " + why + ": routing, transmit and HUD state cleared"); } internal static void MarkDirty() { if (!inTick) { dirty = true; } } internal static void TickIfDirty() { if (dirty) { dirty = false; Tick(); } } private static void Tick() { if ((Object)(object)StartOfRound.Instance == (Object)null) { dirty = true; return; } inTick = true; try { LogCensus(); AutoTransmit.Tick(); if (Refresh()) { StartOfRound.Instance.UpdatePlayerVoiceEffects(); } } finally { inTick = false; } } internal static bool Refresh() { //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; PlayerControllerB local = GetLocal(); if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null) { return false; } PlayerControllerB listener = GetListener(local); string text = FindActiveWalkie(listener); bool flag = text != null; bool result = false; LogWalkieSeen(listener, listener, text, "you"); PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null)) { ulong playerClientId = val.playerClientId; string text2 = (((Object)(object)val == (Object)(object)listener || (!val.isPlayerControlled && !val.isPlayerDead)) ? null : FindActiveWalkie(val)); if ((Object)(object)val != (Object)(object)listener) { LogWalkieSeen(listener, val, text2, null); } bool flag2 = (Object)(object)val != (Object)(object)listener && (val.speakingToWalkieTalkie || text2 != null); bool flag3 = IsRoutable(val, local, listener) && flag && flag2; bool flag4 = Links.ContainsKey(playerClientId); if (flag3 && !flag4) { float num = Vector3.Distance(((Component)listener).transform.position, ((Component)val).transform.position); float radio = TargetRadio(num); Links[playerClientId] = new Link { Radio = radio, Mode = Classify(radio) }; result = true; Plugin.Debug(string.Format("{0} [route] {1} (#{2}): LINKED, starting as {3} (dist={4:F1}m; yours: {5}, theirs: {6}, transmit flag: {7})", Clock.Stamp(), val.playerUsername, playerClientId, Links[playerClientId].Mode, num, text, text2 ?? "none seen", val.speakingToWalkieTalkie)); } else if (!flag3 && flag4) { RestoreVanilla(val); Links.Remove(playerClientId); result = true; Plugin.Debug(string.Format("{0} [route] {1} (#{2}): UNLINKED (yours: {3}, theirs: {4}, transmit flag: {5})", Clock.Stamp(), val.playerUsername, playerClientId, text ?? "no powered walkie", text2 ?? "no powered walkie", val.speakingToWalkieTalkie)); } } } return result; } private static void LogWalkieSeen(PlayerControllerB listener, PlayerControllerB p, string? where, string? label) { string text = where ?? "no powered walkie"; if (!SeenWalkie.TryGetValue(p.playerClientId, out string value) || !(value == text)) { SeenWalkie[p.playerClientId] = text; string text2 = label ?? $"{p.playerUsername} (#{p.playerClientId})"; Plugin.Debug(Clock.Stamp() + " [walkie] as seen by this client, " + text2 + ": " + text); } } internal static void LogSnapshot(float dt) { //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.DebugLogging.Value) { return; } snapshotTimer += dt; if (snapshotTimer < 3f) { return; } snapshotTimer = 0f; StartOfRound instance = StartOfRound.Instance; PlayerControllerB local = GetLocal(); if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null) { return; } PlayerControllerB listener = GetListener(local); PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)local) && (val.isPlayerControlled || val.isPlayerDead)) { VoicePlayerState voicePlayerState = val.voicePlayerState; AudioSource currentVoiceChatAudioSource = val.currentVoiceChatAudioSource; float num = float.NaN; float[] array = (((Object)(object)SoundManager.Instance != (Object)null) ? SoundManager.Instance.playerVoiceVolumes : null); if (array != null && val.playerClientId < (ulong)array.Length) { num = array[val.playerClientId]; } Plugin.Debug($"{Clock.Stamp()} [voice] {val.playerUsername} (#{val.playerClientId}) dead={val.isPlayerDead} mode={ModeFor(val)} dist={Vector3.Distance(((Component)listener).transform.position, ((Component)val).transform.position):F1}m " + string.Format("| dissonance: state={0} speaking={1} amp={2:F4} vol={3:F2} ", (voicePlayerState == null) ? "MISSING" : "ok", (voicePlayerState != null) ? new bool?(voicePlayerState.IsSpeaking) : ((bool?)null), (voicePlayerState != null) ? voicePlayerState.Amplitude : 0f, (voicePlayerState != null) ? voicePlayerState.Volume : 0f) + "| source: " + (((Object)(object)currentVoiceChatAudioSource == (Object)null) ? "MISSING" : $"playing={currentVoiceChatAudioSource.isPlaying} mute={currentVoiceChatAudioSource.mute} vol={currentVoiceChatAudioSource.volume:F2} blend={currentVoiceChatAudioSource.spatialBlend:F2} mixerVol={num:F2}")); } } } private static void RestoreVanilla(PlayerControllerB p) { if (!((Object)(object)p.currentVoiceChatAudioSource == (Object)null)) { AudioHighPassFilter component = ((Component)p.currentVoiceChatAudioSource).GetComponent<AudioHighPassFilter>(); if ((Object)(object)component != (Object)null && HighPassDefaults.TryGetValue(p.playerClientId, out var value)) { component.cutoffFrequency = value; } if (SpreadDefaults.TryGetValue(p.playerClientId, out var value2)) { p.currentVoiceChatAudioSource.spread = value2; } if (Plugin.DebugLogging.Value) { Plugin.Debug($"{Clock.Stamp()} [fx] {p.playerUsername} (#{p.playerClientId}): handed back to the base game"); } } } internal static void UpdateBlend(float dt) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; PlayerControllerB local = GetLocal(); if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null || Links.Count == 0) { return; } PlayerControllerB listener = GetListener(local); if ((Object)(object)listener == (Object)null) { return; } bool isPlayerDead = local.isPlayerDead; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if ((Object)(object)val == (Object)null || !Links.TryGetValue(val.playerClientId, out Link value) || (Object)(object)val.currentVoiceChatAudioSource == (Object)null || (Object)(object)val.currentVoiceChatIngameSettings == (Object)null || val.voicePlayerState == null) { continue; } float num = Vector3.Distance(((Component)listener).transform.position, ((Component)val).transform.position); value.Radio = SmoothRadio(value.Radio, TargetRadio(num), dt); VoiceMode voiceMode = Classify(value.Radio); if (voiceMode != value.Mode) { if (Plugin.DebugLogging.Value) { Plugin.Debug($"{Clock.Stamp()} [route] {val.playerUsername} (#{val.playerClientId}): {value.Mode} -> {voiceMode} (dist={num:F1}m, radio={value.Radio * 100f:F0}%)"); } value.Mode = voiceMode; } if (value.Applied == VoiceMode.Spatial && voiceMode == VoiceMode.Spatial) { AudioLowPassFilter component = ((Component)val.currentVoiceChatAudioSource).GetComponent<AudioLowPassFilter>(); if ((Object)(object)component != (Object)null) { value.OcclusionCutoff = component.cutoffFrequency; } } if (voiceMode == VoiceMode.Blend || voiceMode != value.Applied) { Apply(val, value, isPlayerDead); } else if (voiceMode == VoiceMode.Walkie) { VerifyWalkie(val, value, isPlayerDead); } } } internal static void ApplyAll() { lastPostfixFrame = Time.frameCount; StartOfRound instance = StartOfRound.Instance; PlayerControllerB local = GetLocal(); if ((Object)(object)instance == (Object)null || (Object)(object)local == (Object)null) { return; } bool isPlayerDead = local.isPlayerDead; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null) && Links.TryGetValue(val.playerClientId, out Link value) && !((Object)(object)val.currentVoiceChatAudioSource == (Object)null) && !((Object)(object)val.currentVoiceChatIngameSettings == (Object)null) && val.voicePlayerState != null) { Apply(val, value, isPlayerDead); } } } private static void RadioTargets(Link link, float highPassCutoff, float radio, out float highPassHz, out float lowPassHz, out float resonanceQ) { radio = Ease(radio); float num = radio * RadioEffectStrength; float t = Mathf.Pow(num, 0.6f); float num2 = num * num * num; highPassHz = Mathf.Clamp(LogLerp(10f, Mathf.Max(10f, highPassCutoff), t), 10f, 5000f); float num3 = LogLerp(link.OcclusionCutoff, 22000f, radio); lowPassHz = Mathf.Clamp(LogLerp(num3, 4000f, num), 300f, 22000f); resonanceQ = Mathf.Clamp(Mathf.Lerp(1f, 3f, num2), 1f, 10f); } private static void ApplyRadioFilters(AudioLowPassFilter lowPass, AudioHighPassFilter highPass, OccludeAudio occlude, Link link, float highPassCutoff, float radio) { RadioTargets(link, highPassCutoff, radio, out var highPassHz, out var lowPassHz, out var resonanceQ); ((Behaviour)lowPass).enabled = true; ((Behaviour)highPass).enabled = true; highPass.cutoffFrequency = highPassHz; occlude.overridingLowPass = true; occlude.lowPassOverride = lowPassHz; lowPass.lowpassResonanceQ = resonanceQ; } private static void VerifyWalkie(PlayerControllerB p, Link link, bool localDead) { AudioSource currentVoiceChatAudioSource = p.currentVoiceChatAudioSource; AudioLowPassFilter component = ((Component)currentVoiceChatAudioSource).GetComponent<AudioLowPassFilter>(); AudioHighPassFilter component2 = ((Component)currentVoiceChatAudioSource).GetComponent<AudioHighPassFilter>(); OccludeAudio component3 = ((Component)currentVoiceChatAudioSource).GetComponent<OccludeAudio>(); if ((Object)(object)component == (Object)null || (Object)(object)component2 == (Object)null || (Object)(object)component3 == (Object)null || !HighPassDefaults.TryGetValue(p.playerClientId, out var value)) { return; } if (Mathf.Abs(link.AppliedStrength - RadioEffectStrength) > 0.0001f) { Apply(p, link, localDead); return; } Fights.TryGetValue(p.playerClientId, out var value2); float unscaledTime = Time.unscaledTime; if (unscaledTime < value2.PausedUntil) { return; } RadioTargets(link, value, 1f, out var highPassHz, out var lowPassHz, out var resonanceQ); string text = null; if (!((Behaviour)component).enabled) { text = "low-pass filter was switched off"; } else if (!((Behaviour)component2).enabled) { text = "high-pass filter was switched off"; } else if (!component3.overridingLowPass) { text = "low-pass override was released"; } else if (Mathf.Abs(component3.lowPassOverride - lowPassHz) > 1f) { text = $"low-pass target is {component3.lowPassOverride:F0} Hz, expected {lowPassHz:F0}"; } else if (Mathf.Abs(component.lowpassResonanceQ - resonanceQ) > 0.01f) { text = $"resonance is {component.lowpassResonanceQ:F2}, expected {resonanceQ:F2}"; } else if (Mathf.Abs(component2.cutoffFrequency - highPassHz) > 1f) { text = $"high-pass is {component2.cutoffFrequency:F0} Hz, expected {highPassHz:F0}"; } else if (currentVoiceChatAudioSource.spatialBlend > 0.01f || !p.currentVoiceChatIngameSettings.set2D) { text = "voice was switched back to in-person 3D"; } if (text == null) { return; } if (unscaledTime - value2.WindowStart > 4f) { value2.WindowStart = unscaledTime; value2.Count = 0; } value2.Count++; if (value2.Count > 6) { value2.Count = 0; value2.PausedUntil = unscaledTime + 30f; Fights[p.playerClientId] = value2; Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [compat] another plugin keeps changing {p.playerUsername}'s voice settings ({text}). Not re-applying the radio sound for {30f:F0} s to avoid a tug of war."); return; } Fights[p.playerClientId] = value2; driftCount++; if (driftCount <= 20 || driftCount % 100 == 0) { Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [fx] {p.playerUsername} (#{p.playerClientId}): radio filters had drifted ({text}); re-applied (#{driftCount})"); } Apply(p, link, localDead); } private static float VoiceVolume(bool localDead, float radio) { return BaseVolume(localDead) * Mathf.Lerp(1f, Mathf.Max(0f, WalkieVolume), radio); } private static void Apply(PlayerControllerB p, Link link, bool localDead) { AudioSource currentVoiceChatAudioSource = p.currentVoiceChatAudioSource; AudioLowPassFilter component = ((Component)currentVoiceChatAudioSource).GetComponent<AudioLowPassFilter>(); AudioHighPassFilter component2 = ((Component)currentVoiceChatAudioSource).GetComponent<AudioHighPassFilter>(); OccludeAudio component3 = ((Component)currentVoiceChatAudioSource).GetComponent<OccludeAudio>(); if (!HighPassDefaults.TryGetValue(p.playerClientId, out var value)) { value = component2.cutoffFrequency; HighPassDefaults[p.playerClientId] = value; } if (!SpreadDefaults.ContainsKey(p.playerClientId)) { SpreadDefaults[p.playerClientId] = currentVoiceChatAudioSource.spread; } if (link.Mode == VoiceMode.Blend && link.LastBlend >= 0f && lastPostfixFrame != Time.frameCount && Time.frameCount - link.FlipFrame > 3 && Mathf.Abs(currentVoiceChatAudioSource.spatialBlend - link.LastBlend) > 0.05f && !warnedBlendOverwritten) { warnedBlendOverwritten = true; Plugin.Logger.LogWarning((object)$"{Clock.Stamp()} [blend] spatialBlend of {p.playerUsername} was changed by something else (expected {link.LastBlend:F2}, found {currentVoiceChatAudioSource.spatialBlend:F2}). Another mod or setting is overriding the in-person/radio mix."); } float num = Mathf.Clamp01(link.Radio); link.AppliedStrength = RadioEffectStrength; bool flag = link.Mode == VoiceMode.Walkie; if (p.currentVoiceChatIngameSettings.set2D != flag) { link.FlipFrame = Time.frameCount; } currentVoiceChatAudioSource.bypassListenerEffects = false; currentVoiceChatAudioSource.bypassEffects = false; currentVoiceChatAudioSource.outputAudioMixerGroup = SoundManager.Instance.playerVoiceMixers[p.playerClientId]; p.voicePlayerState.Volume = VoiceVolume(localDead, num); if (link.Mode == VoiceMode.Spatial) { ((Behaviour)component).enabled = true; ((Behaviour)component2).enabled = false; component2.cutoffFrequency = value; component3.overridingLowPass = p.voiceMuffledByEnemy; component.lowpassResonanceQ = 1f; p.currentVoiceChatIngameSettings.set2D = false; currentVoiceChatAudioSource.spatialBlend = 1f; currentVoiceChatAudioSource.panStereo = 0f; currentVoiceChatAudioSource.spread = SpreadFor(p.playerClientId, 0f); } else if (link.Mode == VoiceMode.Walkie) { ApplyRadioFilters(component, component2, component3, link, value, 1f); p.currentVoiceChatIngameSettings.set2D = true; currentVoiceChatAudioSource.spatialBlend = 0f; currentVoiceChatAudioSource.panStereo = 0f; currentVoiceChatAudioSource.spread = SpreadFor(p.playerClientId, 1f); } else { ApplyRadioFilters(component, component2, component3, link, value, num); p.currentVoiceChatIngameSettings.set2D = false; currentVoiceChatAudioSource.spatialBlend = 1f - num; currentVoiceChatAudioSource.panStereo = 0f; currentVoiceChatAudioSource.spread = SpreadFor(p.playerClientId, num); } if (Plugin.DebugLogging.Value && link.Applied != link.Mode) { Plugin.Debug($"{Clock.Stamp()} [fx] {p.playerUsername} (#{p.playerClientId}): applied {link.Mode} | " + $"low-pass on={((Behaviour)component).enabled} override={component3.overridingLowPass}/{component3.lowPassOverride:F0}Hz Q={component.lowpassResonanceQ:F1} | " + $"high-pass on={((Behaviour)component2).enabled} {component2.cutoffFrequency:F0}Hz | blend={currentVoiceChatAudioSource.spatialBlend:F2} set2D={p.currentVoiceChatIngameSettings.set2D} pan={currentVoiceChatAudioSource.panStereo:F2} vol={p.voicePlayerState.Volume:F2}"); } link.LastBlend = currentVoiceChatAudioSource.spatialBlend; link.Applied = link.Mode; } internal static bool IsVoiceOfWalkiePlayer(AudioSource source) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)source == (Object)null || (Object)(object)instance == (Object)null) { return false; } PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if ((Object)(object)val != (Object)null && (Object)(object)val.currentVoiceChatAudioSource == (Object)(object)source && HasActiveWalkie(val)) { return true; } } return false; } } public static class MyPluginInfo { public const string PLUGIN_GUID = "bobabulkerENTERPRISE.BetterVC"; public const string PLUGIN_NAME = "BetterVC"; public const string PLUGIN_VERSION = "1.0.0"; } } namespace BetterVC.Patches { [HarmonyPatch(typeof(PlayerControllerB), "Update")] internal static class FrameDriverPatch { [HarmonyPostfix] private static void Postfix() { FrameDriver.Tick("PlayerControllerB.Update"); } } [HarmonyPatch(typeof(WalkieTalkie), "SwitchWalkieTalkieOn")] internal static class WalkiePowerPatch { [HarmonyPostfix] private static void Postfix() { VoiceRouter.MarkDirty(); } } [HarmonyPatch(typeof(WalkieTalkie), "EquipItem")] internal static class WalkieEquipPatch { [HarmonyPostfix] private static void Postfix() { VoiceRouter.MarkDirty(); } } [HarmonyPatch(typeof(WalkieTalkie), "PocketItem")] internal static class WalkiePocketPatch { [HarmonyPostfix] private static void Postfix() { VoiceRouter.MarkDirty(); } } [HarmonyPatch(typeof(WalkieTalkie), "DiscardItem")] internal static class WalkieDiscardPatch { [HarmonyPostfix] private static void Postfix() { VoiceRouter.MarkDirty(); } } [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] internal static class PlayerJoinPatch { [HarmonyPostfix] private static void Postfix() { VoiceRouter.MarkDirty(); } } [HarmonyPatch(typeof(StartOfRound), "UpdatePlayerVoiceEffects")] internal static class VoiceEffectsPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix() { try { VoiceRouter.ResetIfNewLobby(); VoiceRouter.Refresh(); VoiceRouter.ApplyAll(); VoiceRouter.MarkDirty(); } catch (Exception arg) { Plugin.Logger.LogError((object)$"Applying voice routing failed: {arg}"); } } } [HarmonyPatch(typeof(WalkieTalkie), "GetAllAudioSourcesToReplay")] internal static class WalkieReplayPatch { [HarmonyPostfix] private static void Postfix(List<AudioSource> ___audioSourcesToReplay) { if (___audioSourcesToReplay == null || ___audioSourcesToReplay.Count == 0) { return; } try { if (Plugin.NearbySoundsVolume.Value <= 0.001f) { ___audioSourcesToReplay.Clear(); return; } int num = ___audioSourcesToReplay.RemoveAll(VoiceRouter.IsVoiceOfWalkiePlayer); if (num > 0 && Plugin.DebugLogging.Value) { Plugin.Debug($"{Clock.Stamp()} [overlap] kept {num} voice source(s) out of the walkie re-broadcast"); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"Replay filter failed: {arg}"); } } } [HarmonyPatch(typeof(WalkieTalkie), "SetLocalClientSpeaking")] internal static class WalkieSpeakPatch { [HarmonyPrefix] private static bool Prefix(bool speaking) { if (!speaking && AutoTransmit.Wanted) { return AutoTransmit.Sending; } return true; } } [HarmonyPatch(typeof(WalkieTalkie), "TransmitOneShotAudio")] internal static class WalkieOneShotPatch { [HarmonyPrefix] private static bool Prefix() { return Plugin.NearbySoundsVolume.Value > 0.001f; } } internal static class WalkieChatter { private static readonly HashSet<WalkieTalkie> Muted = new HashSet<WalkieTalkie>(); internal static bool Allowed() { if (Plugin.IdleChatterVolume.Value <= 0.001f) { return false; } PlayerControllerB player = VoiceRouter.CurrentListener(); return !VoiceRouter.HasActiveWalkie(player); } private static bool IsListenersOwn(WalkieTalkie walkie) { return WalkieSpeaker.IsListenersOwn(walkie); } internal static void Enforce(WalkieTalkie walkie, Dictionary<AudioSource, AudioSource>? receiving) { if ((Object)(object)walkie == (Object)null || (Object)(object)walkie.target == (Object)null) { return; } if (((GrabbableObject)walkie).isBeingUsed && !IsListenersOwn(walkie) && !Allowed()) { if (Muted.Add(walkie) && Plugin.DebugLogging.Value) { Plugin.Debug(Clock.Stamp() + " [chatter] silenced a walkie's replay noise (held by " + (((Object)(object)((GrabbableObject)walkie).playerHeldBy != (Object)null) ? ((GrabbableObject)walkie).playerHeldBy.playerUsername : "nobody") + ")"); } walkie.target.mute = true; if (receiving == null) { return; } { foreach (AudioSource value in receiving.Values) { if ((Object)(object)value != (Object)null) { value.mute = true; } } return; } } if (!Muted.Remove(walkie)) { return; } walkie.target.mute = false; if (receiving == null) { return; } foreach (AudioSource value2 in receiving.Values) { if ((Object)(object)value2 != (Object)null) { value2.mute = false; } } } internal static void Forget(WalkieTalkie walkie) { Muted.Remove(walkie); } } internal static class WalkieSpeaker { private sealed class Saved { public float Blend; public float Pan; public float Volume; public float BaseVolume; public float LastSetVolume = -1f; } private static readonly Dictionary<AudioSource, Saved> Touched = new Dictionary<AudioSource, Saved>(); internal static bool IsListenersOwn(WalkieTalkie walkie) { PlayerControllerB val = VoiceRouter.CurrentListener(); if ((Object)(object)val != (Object)null) { return (Object)(object)((GrabbableObject)walkie).playerHeldBy == (Object)(object)val; } return false; } internal static void Enforce(WalkieTalkie walkie, Dictionary<AudioSource, AudioSource>? receiving) { if ((Object)(object)walkie == (Object)null) { return; } bool isBeingUsed = ((GrabbableObject)walkie).isBeingUsed; bool flag = isBeingUsed && IsListenersOwn(walkie); float volumeFactor = Mathf.Clamp01(Plugin.NearbySoundsVolume.Value); Apply(walkie.target, isBeingUsed, flag, volumeFactor); Apply(walkie.thisAudio, flag, flag, Mathf.Max(0f, Plugin.WalkieVolume.Value)); if (receiving != null) { foreach (AudioSource value in receiving.Values) { Apply(value, isBeingUsed, flag, volumeFactor); } } if (Touched.Count > 64) { Prune(); } } private static void Apply(AudioSource? source, bool active, bool center, float volumeFactor) { if ((Object)(object)source == (Object)null) { return; } Saved value2; if (active) { if (!Touched.TryGetValue(source, out Saved value) || value == null) { value = new Saved { Blend = source.spatialBlend, Pan = source.panStereo, Volume = source.volume, BaseVolume = source.volume }; Touched[source] = value; } if (center) { source.spatialBlend = 0f; source.panStereo = 0f; } else { source.spatialBlend = value.Blend; source.panStereo = value.Pan; } if (Mathf.Abs(source.volume - value.LastSetVolume) > 0.0001f) { value.BaseVolume = source.volume; } float lastSetVolume = (source.volume = Mathf.Clamp01(value.BaseVolume * volumeFactor)); value.LastSetVolume = lastSetVolume; } else if (Touched.TryGetValue(source, out value2)) { source.spatialBlend = value2.Blend; source.panStereo = value2.Pan; source.volume = value2.BaseVolume; Touched.Remove(source); } } private static void Prune() { List<AudioSource> list = new List<AudioSource>(); foreach (AudioSource key in Touched.Keys) { if ((Object)(object)key == (Object)null) { list.Add(key); } } foreach (AudioSource item in list) { Touched.Remove(item); } } } [HarmonyPatch(typeof(WalkieTalkie), "Update")] internal static class WalkieChatterUpdatePatch { [HarmonyPostfix] private static void Postfix(WalkieTalkie __instance, Dictionary<AudioSource, AudioSource> ___audioSourcesReceiving) { try { WalkieChatter.Enforce(__instance, ___audioSourcesReceiving); WalkieSpeaker.Enforce(__instance, ___audioSourcesReceiving); } catch (Exception arg) { Plugin.Logger.LogError((object)$"Chatter gate failed: {arg}"); } } } [HarmonyPatch(typeof(WalkieTalkie), "SetWallAudioVolume")] internal static class WalkieIdleChatterPatch { private const float MaxHearingDistance = 12f; [HarmonyPrefix] private static bool Prefix(WalkieTalkie __instance) { AudioSource wallAudio = __instance.wallAudio; if ((Object)(object)wallAudio == (Object)null) { return true; } if (!WalkieChatter.Allowed() || !((GrabbableObject)__instance).isBeingUsed) { if (wallAudio.isPlaying) { wallAudio.Stop(); } wallAudio.volume = 0f; return false; } AudioClip[] wallAudios = __instance.wallAudios; if (!wallAudio.isPlaying) { if ((Object)(object)wallAudio.clip == (Object)null && wallAudios != null && wallAudios.Length != 0) { wallAudio.clip = wallAudios[0]; } if ((Object)(object)wallAudio.clip == (Object)null) { return false; } wallAudio.loop = true; wallAudio.spatialBlend = 1f; wallAudio.rolloffMode = (AudioRolloffMode)1; wallAudio.minDistance = 1f; wallAudio.maxDistance = 12f; wallAudio.pitch = Random.Range(0.92f, 1.08f); wallAudio.Play(); } wallAudio.volume = Mathf.Clamp01(Plugin.IdleChatterVolume.Value); return false; } } internal static class TransmitSfx { private static readonly Dictionary<int, bool> LastAnnounced = new Dictionary<int, bool>(); private static object? lastRound; internal static bool IsRepeat(int playerId, bool speaking) { object instance = StartOfRound.Instance; if (instance != lastRound) { LastAnnounced.Clear(); lastRound = instance; } bool value; bool result = LastAnnounced.TryGetValue(playerId, out value) && value == speaking; LastAnnounced[playerId] = speaking; return result; } } [HarmonyPatch(typeof(WalkieTalkie), "SendWalkieTalkieStartTransmissionSFX")] internal static class WalkieStartSfxPatch { [HarmonyPrefix] private static bool Prefix(int playerId) { return !TransmitSfx.IsRepeat(playerId, speaking: true); } } [HarmonyPatch(typeof(WalkieTalkie), "SendWalkieTalkieEndTransmissionSFX")] internal static class WalkieEndSfxPatch { [HarmonyPrefix] private static bool Prefix(int playerId) { return !TransmitSfx.IsRepeat(playerId, speaking: false); } } }