Decompiled source of BlackBoxAudioLog v0.1.0

BepInEx/plugins/BlackBoxAudioLog/BlackBoxAudioLog.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using Dissonance;
using Dissonance.Audio.Capture;
using GameNetcodeStuff;
using HarmonyLib;
using LethalLib.Modules;
using Microsoft.CodeAnalysis;
using NAudio.Wave;
using Newtonsoft.Json;
using Unity.Collections;
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("BlackBoxAudioLog")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+56153eb1e2dca9088104bb612534bcb5ea72f93b")]
[assembly: AssemblyProduct("BlackBoxAudioLog")]
[assembly: AssemblyTitle("BlackBoxAudioLog")]
[assembly: AssemblyVersion("0.1.0.0")]
[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 BlackBoxAudioLog
{
	public sealed class Recorder : IMicrophoneSubscriber
	{
		public const int Rate = 12000;

		public const int MaxSeconds = 30;

		private readonly object gate = new object();

		private readonly short[] ring;

		private int head;

		private int count;

		private double phase;

		public volatile bool Enabled;

		public volatile bool Speaking;

		public Recorder(int seconds)
		{
			ring = new short[Math.Clamp(seconds, 5, 30) * 12000];
		}

		public void ReceiveMicrophoneData(ArraySegment<float> buffer, WaveFormat format)
		{
			if (!Enabled || buffer.Array == null || format.Channels < 1 || format.SampleRate < 12000)
			{
				return;
			}
			lock (gate)
			{
				for (int i = 0; i + format.Channels <= buffer.Count; i += format.Channels)
				{
					phase += 12000.0;
					if (phase < (double)format.SampleRate)
					{
						continue;
					}
					phase -= format.SampleRate;
					float num = 0f;
					if (Speaking)
					{
						for (int j = 0; j < format.Channels; j++)
						{
							num += buffer.Array[buffer.Offset + i + j] / (float)format.Channels;
						}
					}
					ring[head] = (short)(Math.Clamp(num, -1f, 1f) * 32767f);
					head = (head + 1) % ring.Length;
					count = Math.Min(count + 1, ring.Length);
				}
			}
		}

		public byte[] Snapshot()
		{
			lock (gate)
			{
				byte[] array = new byte[count * 2];
				for (int i = 0; i < count; i++)
				{
					short num = ring[(head - count + ring.Length + i) % ring.Length];
					array[i * 2] = (byte)num;
					array[i * 2 + 1] = (byte)(num >> 8);
				}
				return array;
			}
		}

		public void Reset()
		{
			lock (gate)
			{
				Array.Clear(ring, 0, ring.Length);
				head = (count = 0);
				phase = 0.0;
			}
		}
	}
	public static class TapeAudio
	{
		public static float[] Render(byte[] bytes)
		{
			int num = 12000;
			int num2 = 3000;
			float[] array = new float[num + bytes.Length / 2 + num2];
			Random random = new Random(8128);
			for (int i = 0; i < num; i++)
			{
				array[i] = (float)(random.NextDouble() * 2.0 - 1.0) * ((i < 120) ? 0.4f : 0.045f);
			}
			float num3 = 0f;
			float num4 = 0f;
			for (int j = 0; j < bytes.Length / 2; j++)
			{
				float num5 = (float)(short)(bytes[j * 2] | (bytes[j * 2 + 1] << 8)) / 32768f;
				num4 += 0.09f * (num5 - num4);
				num3 += 0.55f * (num5 - num4 - num3);
				array[num + j] = Math.Clamp(num3 * 1.7f, -0.8f, 0.8f) + (float)(random.NextDouble() * 2.0 - 1.0) * 0.006f;
			}
			for (int k = 0; k < num2; k++)
			{
				array[array.Length - num2 + k] = (float)(random.NextDouble() * 2.0 - 1.0) * 0.22f * (1f - (float)k / (float)num2);
			}
			return array;
		}
	}
	public sealed class ChunkAssembly
	{
		public const int ChunkSize = 4096;

		public const int MaxBytes = 720000;

		private readonly byte[][] parts;

		private int received;

		public int Length { get; }

		public ChunkAssembly(int length)
		{
			if (length < 0 || length > 720000 || length % 2 != 0)
			{
				throw new ArgumentOutOfRangeException("length");
			}
			Length = length;
			parts = new byte[(length + 4096 - 1) / 4096][];
		}

		public bool Add(int index, byte[] bytes)
		{
			if (index < 0 || index >= parts.Length || bytes.Length != Math.Min(4096, Length - index * 4096))
			{
				return false;
			}
			if (parts[index] == null)
			{
				parts[index] = bytes;
				received++;
			}
			return true;
		}

		public byte[]? Complete()
		{
			if (received != parts.Length)
			{
				return null;
			}
			byte[] array = new byte[Length];
			for (int i = 0; i < parts.Length; i++)
			{
				Buffer.BlockCopy(parts[i], 0, array, i * 4096, parts[i].Length);
			}
			return array;
		}
	}
	public sealed class Cassette : MonoBehaviour
	{
		public AudioSource Speaker;

		public double LastStart = -1.0;

		public AudioClip? Clip;

		private void Awake()
		{
			Speaker = ((Component)this).gameObject.AddComponent<AudioSource>();
			Speaker.playOnAwake = false;
			Speaker.spatialBlend = 1f;
			Speaker.minDistance = 1.5f;
			Speaker.maxDistance = 18f;
			Speaker.rolloffMode = (AudioRolloffMode)1;
			Speaker.dopplerLevel = 0f;
		}

		public void Apply(TapeState state, byte[]? audio, double time)
		{
			((Component)this).GetComponent<GrabbableObject>().SetScrapValue(state.Value);
			ScanNodeProperties componentInChildren = ((Component)this).GetComponentInChildren<ScanNodeProperties>();
			componentInChildren.headerText = "Black box: " + state.Name;
			componentInChildren.subText = ((audio == null) ? "Recovering recording..." : "Final transmission");
			if (state.Start <= 0.0)
			{
				Speaker.Stop();
				LastStart = -1.0;
			}
			else if (audio != null && LastStart != state.Start)
			{
				LastStart = state.Start;
				if (!Object.op_Implicit((Object)(object)Clip))
				{
					float[] array = TapeAudio.Render(audio);
					Clip = AudioClip.Create("Black box recording", array.Length, 1, 12000, false);
					Clip.SetData(array, 0);
				}
				Speaker.clip = Clip;
				Speaker.volume = Plugin.Instance.Volume.Value;
				double num = Math.Max(0.0, time - state.Start);
				if (num < (double)Clip.length)
				{
					Speaker.time = (float)num;
					Speaker.Play();
					Plugin.Instance.Log("Spatial playback started for tape " + state.Id + " at " + num.ToString("F2") + "s; clip=" + Clip.length + "s.");
				}
			}
		}

		private void OnDestroy()
		{
			if (Object.op_Implicit((Object)(object)Clip))
			{
				Object.Destroy((Object)(object)Clip);
			}
		}
	}
	public static class Content
	{
		public static Item Tape;

		private static Material? shell;

		private static Material? label;

		private static Material? reel;

		public static void Create()
		{
			//IL_011d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0122: Unknown result type (might be due to invalid IL or missing references)
			//IL_013b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0219: Unknown result type (might be due to invalid IL or missing references)
			//IL_0237: Unknown result type (might be due to invalid IL or missing references)
			//IL_0251: Unknown result type (might be due to invalid IL or missing references)
			//IL_0265: Unknown result type (might be due to invalid IL or missing references)
			//IL_028e: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_031a: Unknown result type (might be due to invalid IL or missing references)
			//IL_031f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0331: Unknown result type (might be due to invalid IL or missing references)
			//IL_0339: Unknown result type (might be due to invalid IL or missing references)
			//IL_0340: Unknown result type (might be due to invalid IL or missing references)
			//IL_037e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0384: Expected O, but got Unknown
			//IL_0436: Unknown result type (might be due to invalid IL or missing references)
			//IL_0445: Unknown result type (might be due to invalid IL or missing references)
			//IL_03ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_03f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_03ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_03d6: Unknown result type (might be due to invalid IL or missing references)
			if (Object.op_Implicit((Object)(object)Tape))
			{
				return;
			}
			Tape = ScriptableObject.CreateInstance<Item>();
			((Object)Tape).name = "BlackBoxAudioLogCassette";
			Tape.itemName = "Black box tape";
			Tape.isScrap = true;
			Tape.minValue = 30;
			Tape.maxValue = 51;
			Tape.weight = 1.02f;
			Tape.requiresBattery = false;
			Tape.canBeGrabbedBeforeGameStart = true;
			Tape.grabAnim = "HoldLung";
			Tape.useAnim = (Tape.pocketAnim = (Tape.throwAnim = ""));
			Tape.toolTips = new string[1] { "Play / stop tape : [LMB]" };
			Tape.syncUseFunction = false;
			Tape.meshVariants = Array.Empty<Mesh>();
			Tape.materialVariants = Array.Empty<Material>();
			Tape.clinkAudios = Array.Empty<AudioClip>();
			Tape.verticalOffset = 0.04f;
			Tape.restingRotation = Vector3.zero;
			Tape.positionOffset = new Vector3(0f, 0.02f, 0.04f);
			GameObject val = NetworkPrefabs.CreateNetworkPrefab(((Object)Tape).name);
			Object.DontDestroyOnLoad((Object)(object)((Component)val.transform.root).gameObject);
			((Object)val).hideFlags = (HideFlags)0;
			val.tag = "PhysicsProp";
			val.layer = 6;
			PhysicsProp val2 = val.AddComponent<PhysicsProp>();
			((GrabbableObject)val2).itemProperties = Tape;
			((GrabbableObject)val2).grabbable = true;
			((GrabbableObject)val2).grabbableToEnemies = false;
			((GrabbableObject)val2).propBody = val.AddComponent<Rigidbody>();
			((GrabbableObject)val2).propBody.isKinematic = true;
			BoxCollider val3 = val.AddComponent<BoxCollider>();
			val3.size = new Vector3(0.28f, 0.065f, 0.18f);
			((GrabbableObject)val2).propColliders = (Collider[])(object)new Collider[1] { (Collider)val3 };
			shell = Material(new Color(0.075f, 0.085f, 0.075f));
			label = Material(new Color(0.7f, 0.44f, 0.12f));
			reel = Material(new Color(0.25f, 0.27f, 0.24f));
			Part(val.transform, "Recorder shell", Vector3.zero, new Vector3(0.28f, 0.06f, 0.18f), shell);
			Part(val.transform, "Company label", new Vector3(0f, 0.031f, 0.046f), new Vector3(0.24f, 0.004f, 0.05f), label);
			for (int i = -1; i <= 1; i += 2)
			{
				Part(val.transform, "Tape reel", new Vector3((float)i * 0.065f, 0.034f, -0.025f), new Vector3(0.055f, 0.008f, 0.055f), reel);
			}
			val.AddComponent<Cassette>();
			((GrabbableObject)val2).mainObjectRenderer = val.GetComponentInChildren<MeshRenderer>();
			GameObject val4 = new GameObject("ScanNode");
			val4.transform.SetParent(val.transform, false);
			val4.layer = 22;
			val4.AddComponent<BoxCollider>().size = val3.size;
			ScanNodeProperties obj = val4.AddComponent<ScanNodeProperties>();
			obj.headerText = "Black box tape";
			obj.subText = "Final transmission";
			obj.nodeType = 2;
			obj.maxRange = 13;
			obj.minRange = 1;
			Texture2D val5 = new Texture2D(32, 32);
			Color[] array = (Color[])(object)new Color[1024];
			for (int j = 0; j < 32; j++)
			{
				for (int k = 0; k < 32; k++)
				{
					array[j * 32 + k] = (Color)((j > 6 && j < 26 && k > 2 && k < 30) ? ((j > 19) ? new Color(0.8f, 0.5f, 0.12f) : new Color(0.2f, 0.22f, 0.2f)) : Color.clear);
				}
			}
			val5.SetPixels(array);
			val5.Apply();
			Tape.itemIcon = Sprite.Create(val5, new Rect(0f, 0f, 32f, 32f), new Vector2(0.5f, 0.5f));
			Tape.spawnPrefab = val;
			Items.RegisterItem(Tape);
			Plugin.Instance.Log("Cassette prefab registered.");
		}

		private static Material Material(Color c)
		{
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Expected O, but got Unknown
			return new Material(Shader.Find("HDRP/Lit") ?? Shader.Find("Standard"))
			{
				color = c
			};
		}

		private static void Part(Transform parent, string name, Vector3 pos, Vector3 scale, Material material)
		{
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			GameObject obj = GameObject.CreatePrimitive((PrimitiveType)3);
			((Object)obj).name = name;
			obj.transform.SetParent(parent, false);
			obj.transform.localPosition = pos;
			obj.transform.localScale = scale;
			obj.layer = 6;
			Object.Destroy((Object)(object)obj.GetComponent<Collider>());
			obj.GetComponent<Renderer>().sharedMaterial = material;
		}

		public static void Cleanup()
		{
			if (Object.op_Implicit((Object)(object)Tape))
			{
				Object.Destroy((Object)(object)Tape.spawnPrefab);
				if (Object.op_Implicit((Object)(object)Tape.itemIcon))
				{
					Object.Destroy((Object)(object)Tape.itemIcon.texture);
					Object.Destroy((Object)(object)Tape.itemIcon);
				}
				Object.Destroy((Object)(object)Tape);
				Object.Destroy((Object)(object)shell);
				Object.Destroy((Object)(object)label);
				Object.Destroy((Object)(object)reel);
			}
		}
	}
	[BepInPlugin("blackbox.audiolog", "Black Box Audio Log", "0.1.0")]
	[BepInDependency("evaisa.lethallib", "1.2.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Id = "blackbox.audiolog";

		public static Plugin Instance;

		public ConfigEntry<bool> Record;

		public ConfigEntry<bool> AttractEnemies;

		public ConfigEntry<int> Seconds;

		public ConfigEntry<int> MinValue;

		public ConfigEntry<int> MaxValue;

		public ConfigEntry<int> TapeLimit;

		public ConfigEntry<float> Volume;

		public Session Session;

		private Harmony patches;

		private void Awake()
		{
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Expected O, but got Unknown
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a5: Expected O, but got Unknown
			//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dc: Expected O, but got Unknown
			//IL_0106: Unknown result type (might be due to invalid IL or missing references)
			//IL_0110: Expected O, but got Unknown
			//IL_0165: Unknown result type (might be due to invalid IL or missing references)
			//IL_016f: Expected O, but got Unknown
			//IL_0186: Unknown result type (might be due to invalid IL or missing references)
			//IL_0190: Expected O, but got Unknown
			Instance = this;
			((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
			Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject);
			Record = ((BaseUnityPlugin)this).Config.Bind<bool>("Recording", "Enabled", true, "Record local transmitted microphone audio in memory and share final moments with the lobby on death. False produces static-only tapes. Never writes audio to disk.");
			Seconds = ((BaseUnityPlugin)this).Config.Bind<int>("Recording", "Seconds", 20, new ConfigDescription("Rolling buffer duration; restart required.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(5, 30), Array.Empty<object>()));
			MinValue = ((BaseUnityPlugin)this).Config.Bind<int>("Host", "MinimumValue", 30, new ConfigDescription("Minimum tape scrap value.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 1000), Array.Empty<object>()));
			MaxValue = ((BaseUnityPlugin)this).Config.Bind<int>("Host", "MaximumValue", 50, new ConfigDescription("Maximum tape scrap value.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 1000), Array.Empty<object>()));
			TapeLimit = ((BaseUnityPlugin)this).Config.Bind<int>("Host", "MaximumTapes", 16, new ConfigDescription("Maximum live tapes; additional deaths do not create tapes until space is available.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 32), Array.Empty<object>()));
			AttractEnemies = ((BaseUnityPlugin)this).Config.Bind<bool>("Host", "AttractEnemies", true, "Playback emits audible noise for hearing-sensitive enemies.");
			Volume = ((BaseUnityPlugin)this).Config.Bind<float>("Playback", "Volume", 0.8f, new ConfigDescription("Local playback volume.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			Session = new Session(this);
			patches = new Harmony("blackbox.audiolog");
			patches.PatchAll();
			Log("Initialized. Recording=" + Record.Value + "; microphone audio remains in session memory.");
		}

		private void Start()
		{
			Content.Create();
		}

		private void Update()
		{
			Session.Tick();
		}

		public void Log(string message)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)message);
		}

		public void Error(Exception e)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)e);
		}

		private void OnDestroy()
		{
			Session?.Dispose();
			Harmony obj = patches;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			Content.Cleanup();
		}
	}
	[HarmonyPatch(typeof(GameNetworkManager), "Start")]
	internal static class InitializePatch
	{
		private static void Prefix()
		{
			Content.Create();
		}
	}
	[HarmonyPatch(typeof(GrabbableObject), "ItemActivate")]
	internal static class ActivatePatch
	{
		private static void Postfix(GrabbableObject __instance, bool buttonDown)
		{
			if (buttonDown && ((NetworkBehaviour)__instance).IsOwner && Object.op_Implicit((Object)(object)((Component)__instance).GetComponent<Cassette>()))
			{
				Plugin.Instance.Session.RequestPlay(((NetworkBehaviour)__instance).NetworkObjectId);
			}
		}
	}
	public sealed class TapeState
	{
		public ulong Id;

		public ulong Owner;

		public string Name = "Unknown";

		public int Value;

		public int Length = -1;

		public double Start;

		public double Created;
	}
	public sealed class Message
	{
		public string Kind = "";

		public ulong Id;

		public int Index;

		public int Length;

		public byte[]? Data;

		public TapeState[]? Tapes;
	}
	public sealed class Session : IDisposable
	{
		private const string Channel = "blackbox/audio/1";

		private readonly Plugin plugin;

		public readonly Recorder Recorder;

		private NetworkManager? manager;

		private DissonanceComms? comms;

		private readonly Dictionary<ulong, TapeState> tapes = new Dictionary<ulong, TapeState>();

		private readonly Dictionary<ulong, byte[]> audio = new Dictionary<ulong, byte[]>();

		private readonly Dictionary<ulong, ChunkAssembly> partial = new Dictionary<ulong, ChunkAssembly>();

		private readonly Dictionary<ulong, bool> dead = new Dictionary<ulong, bool>();

		private readonly Dictionary<ulong, Vector3> lastPosition = new Dictionary<ulong, Vector3>();

		private readonly Dictionary<ulong, ulong> deathTapes = new Dictionary<ulong, ulong>();

		private readonly Dictionary<ulong, double> requests = new Dictionary<ulong, double>();

		private readonly Dictionary<(ulong, string, ulong), double> rate = new Dictionary<(ulong, string, ulong), double>();

		private readonly Queue<(ulong, Message)> outgoing = new Queue<(ulong, Message)>();

		private readonly HashSet<(ulong, ulong)> transfers = new HashSet<(ulong, ulong)>();

		private byte[]? pending;

		private bool localDead;

		private double nextState;

		private double nextNoise;

		private float sendBudget;

		private float nextVoiceCheck;

		private readonly Collider[] noiseHits = (Collider[])(object)new Collider[128];

		private bool Host
		{
			get
			{
				if ((Object)(object)manager != (Object)null)
				{
					return manager.IsServer;
				}
				return false;
			}
		}

		private double Now
		{
			get
			{
				//IL_001e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0023: Unknown result type (might be due to invalid IL or missing references)
				if (!((Object)(object)manager != (Object)null))
				{
					return 0.0;
				}
				NetworkTime serverTime = manager.ServerTime;
				return ((NetworkTime)(ref serverTime)).Time;
			}
		}

		public Session(Plugin p)
		{
			plugin = p;
			Recorder = new Recorder(p.Seconds.Value);
		}

		public void Tick()
		{
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Expected O, but got Unknown
			NetworkManager val = NetworkManager.Singleton;
			if (Object.op_Implicit((Object)(object)val) && (!val.IsListening || val.CustomMessagingManager == null))
			{
				val = null;
			}
			if ((Object)(object)val != (Object)(object)manager)
			{
				Dispose();
				manager = val;
				if (Object.op_Implicit((Object)(object)manager))
				{
					manager.CustomMessagingManager.RegisterNamedMessageHandler("blackbox/audio/1", new HandleNamedMessageDelegate(Receive));
					plugin.Log("Session transport registered.");
				}
			}
			if (!Object.op_Implicit((Object)(object)manager) || !Object.op_Implicit((Object)(object)StartOfRound.Instance))
			{
				return;
			}
			PlayerControllerB local = GameNetworkManager.Instance?.localPlayerController;
			DissonanceComms val2 = comms;
			if (Time.unscaledTime >= nextVoiceCheck)
			{
				nextVoiceCheck = Time.unscaledTime + 1f;
				val2 = Object.FindObjectOfType<DissonanceComms>();
			}
			if ((Object)(object)val2 != (Object)(object)comms)
			{
				if (Object.op_Implicit((Object)(object)comms))
				{
					comms.UnsubscribeFromRecordedAudio((IMicrophoneSubscriber)(object)Recorder);
				}
				Recorder.Reset();
				comms = val2;
				if (Object.op_Implicit((Object)(object)comms))
				{
					comms.SubscribeToRecordedAudio((IMicrophoneSubscriber)(object)Recorder);
					plugin.Log("Subscribed to microphone recording stream.");
				}
			}
			bool flag = Object.op_Implicit((Object)(object)local) && local.isPlayerDead;
			if (flag && !localDead)
			{
				pending = (plugin.Record.Value ? Recorder.Snapshot() : Array.Empty<byte>());
				Recorder.Reset();
			}
			if (!flag && localDead)
			{
				pending = null;
				Recorder.Reset();
			}
			localDead = flag;
			Recorder.Enabled = plugin.Record.Value && Object.op_Implicit((Object)(object)local) && local.isPlayerControlled && !flag;
			Recorder.Speaking = Recorder.Enabled && Object.op_Implicit((Object)(object)comms) && !comms.IsMuted && ((Channels<RoomChannel, string>)(object)comms.RoomChannels).Count + ((Channels<PlayerChannel, string>)(object)comms.PlayerChannels).Count > 0;
			if (!plugin.Record.Value)
			{
				Recorder.Reset();
			}
			if (Host)
			{
				HostTick();
			}
			if (pending != null && Object.op_Implicit((Object)(object)local))
			{
				TapeState tapeState = tapes.Values.FirstOrDefault((TapeState t) => t.Owner == local.actualClientId && t.Length < 0 && Now - t.Created < 15.0);
				if (tapeState != null)
				{
					if (Host)
					{
						FinishAudio(tapeState.Id, pending);
					}
					else
					{
						QueueAudio(0uL, "upload", tapeState.Id, pending);
					}
					pending = null;
				}
			}
			foreach (TapeState value4 in tapes.Values)
			{
				if (manager.SpawnManager.SpawnedObjects.TryGetValue(value4.Id, out var value))
				{
					Cassette component = ((Component)value).GetComponent<Cassette>();
					if (Object.op_Implicit((Object)(object)component))
					{
						component.Apply(value4, audio.TryGetValue(value4.Id, out byte[] value2) ? value2 : null, Now);
					}
				}
				if (!Host && value4.Length >= 0 && !audio.ContainsKey(value4.Id) && (!requests.TryGetValue(value4.Id, out var value3) || Now - value3 > 20.0))
				{
					requests[value4.Id] = Now;
					Send(0uL, new Message
					{
						Kind = "fetch",
						Id = value4.Id
					});
				}
			}
			sendBudget = Math.Min(sendBudget + Time.unscaledDeltaTime * 24f, 4f);
			while (sendBudget >= 1f && outgoing.Count > 0)
			{
				(ulong, Message) tuple = outgoing.Dequeue();
				sendBudget -= 1f;
				if (manager.ConnectedClientsIds.Contains(tuple.Item1) || !Host)
				{
					Send(tuple.Item1, tuple.Item2);
				}
				if (tuple.Item2.Length == 0 || (tuple.Item2.Index + 1) * 4096 >= tuple.Item2.Length)
				{
					transfers.Remove((tuple.Item1, tuple.Item2.Id));
				}
			}
		}

		private void HostTick()
		{
			//IL_0230: Unknown result type (might be due to invalid IL or missing references)
			//IL_0582: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Unknown result type (might be due to invalid IL or missing references)
			//IL_0619: Unknown result type (might be due to invalid IL or missing references)
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: 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)
			PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts;
			foreach (PlayerControllerB val in allPlayerScripts)
			{
				if (Object.op_Implicit((Object)(object)val) && (val.isPlayerControlled || val.isPlayerDead))
				{
					ulong actualClientId = val.actualClientId;
					bool value;
					bool flag = dead.TryGetValue(actualClientId, out value) && value;
					if (val.isPlayerDead && !flag && tapes.Count < plugin.TapeLimit.Value)
					{
						Vector3 value2;
						Vector3 val2 = (lastPosition.TryGetValue(actualClientId, out value2) ? value2 : ((Component)val).transform.position);
						GameObject obj = Object.Instantiate<GameObject>(Content.Tape.spawnPrefab, val2 + Vector3.up * 0.1f, Quaternion.identity);
						obj.GetComponent<NetworkObject>().Spawn(false);
						GrabbableObject component = obj.GetComponent<GrabbableObject>();
						int num = Random.Range(plugin.MinValue.Value, Math.Max(plugin.MinValue.Value, plugin.MaxValue.Value) + 1);
						component.SetScrapValue(num);
						component.isInFactory = val.isInsideFactory;
						TapeState tapeState = new TapeState
						{
							Id = ((NetworkBehaviour)component).NetworkObjectId,
							Owner = actualClientId,
							Name = ((val.playerUsername.Length > 32) ? val.playerUsername.Substring(0, 32) : val.playerUsername),
							Value = num,
							Created = Now
						};
						tapes[tapeState.Id] = tapeState;
						deathTapes[actualClientId] = tapeState.Id;
						plugin.Log("Spawned death tape " + tapeState.Id + " for " + tapeState.Name + " ($" + num + ").");
						nextState = 0.0;
					}
					if (!val.isPlayerDead)
					{
						lastPosition[actualClientId] = ((Component)val).transform.position;
						deathTapes.Remove(actualClientId);
					}
					dead[actualClientId] = val.isPlayerDead;
				}
			}
			Cassette[] array = Object.FindObjectsOfType<Cassette>();
			for (int i = 0; i < array.Length; i++)
			{
				GrabbableObject component2 = ((Component)array[i]).GetComponent<GrabbableObject>();
				if (((NetworkBehaviour)component2).IsSpawned && !tapes.ContainsKey(((NetworkBehaviour)component2).NetworkObjectId) && tapes.Count < plugin.TapeLimit.Value)
				{
					tapes[((NetworkBehaviour)component2).NetworkObjectId] = new TapeState
					{
						Id = ((NetworkBehaviour)component2).NetworkObjectId,
						Value = component2.scrapValue,
						Name = "Recovered",
						Created = Now,
						Length = 0
					};
					audio[((NetworkBehaviour)component2).NetworkObjectId] = Array.Empty<byte>();
				}
			}
			TapeState[] array2 = tapes.Values.ToArray();
			foreach (TapeState tapeState2 in array2)
			{
				if (!manager.SpawnManager.SpawnedObjects.ContainsKey(tapeState2.Id))
				{
					tapes.Remove(tapeState2.Id);
					audio.Remove(tapeState2.Id);
					partial.Remove(tapeState2.Id);
					requests.Remove(tapeState2.Id);
					continue;
				}
				if (tapeState2.Length < 0 && Now - tapeState2.Created > 15.0)
				{
					FinishAudio(tapeState2.Id, Array.Empty<byte>());
				}
				if (tapeState2.Start > 0.0 && tapeState2.Length >= 0 && Now - tapeState2.Start > 1.25 + (double)tapeState2.Length / 24000.0)
				{
					tapeState2.Start = 0.0;
				}
			}
			if (Now >= nextState)
			{
				nextState = Now + 2.0;
				Broadcast(new Message
				{
					Kind = "state",
					Tapes = tapes.Values.ToArray()
				});
			}
			if (!(Now >= nextNoise))
			{
				return;
			}
			nextNoise = Now + 1.0;
			if (!plugin.AttractEnemies.Value || !Object.op_Implicit((Object)(object)RoundManager.Instance))
			{
				return;
			}
			INoiseListener val3 = default(INoiseListener);
			foreach (TapeState item in tapes.Values.Where((TapeState t) => t.Start > 0.0))
			{
				if (!manager.SpawnManager.SpawnedObjects.TryGetValue(item.Id, out var value3))
				{
					continue;
				}
				bool flag2 = ((Component)value3).GetComponent<GrabbableObject>().isInShipRoom && StartOfRound.Instance.hangarDoorsClosed;
				int num2 = Physics.OverlapSphereNonAlloc(((Component)value3).transform.position, (float)(flag2 ? 9 : 18), noiseHits, 8912896);
				HashSet<INoiseListener> hashSet = new HashSet<INoiseListener>();
				for (int num3 = 0; num3 < num2; num3++)
				{
					try
					{
						if (Object.op_Implicit((Object)(object)noiseHits[num3]) && ((Component)((Component)noiseHits[num3]).transform).TryGetComponent<INoiseListener>(ref val3) && hashSet.Add(val3))
						{
							EnemyAI component3 = ((Component)noiseHits[num3]).GetComponent<EnemyAI>();
							if (!flag2 || (Object.op_Implicit((Object)(object)component3) && component3.isInsidePlayerShip))
							{
								val3.DetectNoise(((Component)value3).transform.position, 0.65f, 0, 0);
							}
						}
					}
					catch (Exception e)
					{
						plugin.Error(e);
					}
					finally
					{
						noiseHits[num3] = null;
					}
				}
			}
		}

		public void RequestPlay(ulong id)
		{
			if (Object.op_Implicit((Object)(object)manager))
			{
				if (Host)
				{
					Play(manager.LocalClientId, id);
					return;
				}
				Send(0uL, new Message
				{
					Kind = "play",
					Id = id
				});
			}
		}

		private void Play(ulong sender, ulong id)
		{
			if (tapes.TryGetValue(id, out TapeState value) && value.Length >= 0 && Allowed(sender, "play", id, 0.5) && manager.SpawnManager.SpawnedObjects.TryGetValue(id, out var value2))
			{
				GrabbableObject component = ((Component)value2).GetComponent<GrabbableObject>();
				if (Object.op_Implicit((Object)(object)component.playerHeldBy) && component.playerHeldBy.actualClientId == sender && !component.playerHeldBy.isPlayerDead)
				{
					value.Start = ((value.Start > 0.0) ? 0.0 : Now);
					Broadcast(new Message
					{
						Kind = "state",
						Tapes = tapes.Values.ToArray()
					});
					plugin.Log("Tape " + id + ((value.Start > 0.0) ? " playing." : " stopped."));
				}
			}
		}

		private bool Allowed(ulong sender, string kind, ulong id, double interval)
		{
			(ulong, string, ulong) key = (sender, kind, id);
			if (rate.TryGetValue(key, out var value) && Now - value < interval)
			{
				return false;
			}
			rate[key] = Now;
			return true;
		}

		private void Receive(ulong sender, FastBufferReader reader)
		{
			if (((FastBufferReader)(ref reader)).Length > 16000)
			{
				return;
			}
			try
			{
				string text = default(string);
				((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false);
				Message message = JsonConvert.DeserializeObject<Message>(text);
				if (message == null)
				{
					return;
				}
				if (Host)
				{
					byte[] value;
					ulong value2;
					TapeState value3;
					if (message.Kind == "play")
					{
						Play(sender, message.Id);
					}
					else if (message.Kind == "fetch" && audio.TryGetValue(message.Id, out value) && Allowed(sender, "fetch", message.Id, 15.0) && outgoing.Count < 4096)
					{
						QueueAudio(sender, "audio", message.Id, value);
					}
					else if (message.Kind == "upload" && deathTapes.TryGetValue(sender, out value2) && value2 == message.Id && tapes.TryGetValue(message.Id, out value3) && value3.Length < 0 && Now - value3.Created < 15.0)
					{
						ReceiveAudio(message);
					}
				}
				else
				{
					if (sender != 0L)
					{
						return;
					}
					TapeState value4;
					if (message.Kind == "state" && message.Tapes != null && message.Tapes.Length <= 32)
					{
						HashSet<ulong> keep = new HashSet<ulong>(message.Tapes.Select((TapeState t) => t.Id));
						ulong[] array = tapes.Keys.Where((ulong id) => !keep.Contains(id)).ToArray();
						foreach (ulong key in array)
						{
							tapes.Remove(key);
							audio.Remove(key);
							partial.Remove(key);
							requests.Remove(key);
						}
						TapeState[] array2 = message.Tapes;
						foreach (TapeState tapeState in array2)
						{
							if (tapeState.Length >= -1 && tapeState.Length <= 720000)
							{
								tapes[tapeState.Id] = tapeState;
							}
						}
					}
					else if (message.Kind == "audio" && tapes.TryGetValue(message.Id, out value4) && value4.Length == message.Length)
					{
						ReceiveAudio(message);
					}
				}
			}
			catch (Exception e)
			{
				plugin.Error(e);
			}
		}

		private void ReceiveAudio(Message m)
		{
			if (audio.ContainsKey(m.Id) || m.Length < 0 || m.Length > 720000 || m.Length % 2 != 0)
			{
				return;
			}
			if (m.Length == 0)
			{
				FinishAudio(m.Id, Array.Empty<byte>());
			}
			else
			{
				if (m.Data == null || m.Data.Length > 4096)
				{
					return;
				}
				if (!partial.TryGetValue(m.Id, out ChunkAssembly value))
				{
					value = (partial[m.Id] = new ChunkAssembly(m.Length));
				}
				if (value.Length == m.Length && value.Add(m.Index, m.Data))
				{
					byte[] array = value.Complete();
					if (array != null)
					{
						FinishAudio(m.Id, array);
					}
				}
			}
		}

		private void FinishAudio(ulong id, byte[] bytes)
		{
			audio[id] = bytes;
			partial.Remove(id);
			if (tapes.TryGetValue(id, out TapeState value))
			{
				value.Length = bytes.Length;
			}
			if (Host)
			{
				nextState = 0.0;
			}
			plugin.Log("Tape " + id + " audio ready: " + bytes.Length + " bytes.");
		}

		private void QueueAudio(ulong target, string kind, ulong id, byte[] bytes)
		{
			if (!transfers.Add((target, id)))
			{
				return;
			}
			if (bytes.Length == 0)
			{
				outgoing.Enqueue((target, new Message
				{
					Kind = kind,
					Id = id,
					Length = 0
				}));
				return;
			}
			int num = 0;
			int num2 = 0;
			while (num < bytes.Length)
			{
				byte[] array = new byte[Math.Min(4096, bytes.Length - num)];
				Buffer.BlockCopy(bytes, num, array, 0, array.Length);
				outgoing.Enqueue((target, new Message
				{
					Kind = kind,
					Id = id,
					Length = bytes.Length,
					Index = num2,
					Data = array
				}));
				num += 4096;
				num2++;
			}
		}

		private void Broadcast(Message m)
		{
			foreach (ulong connectedClientsId in manager.ConnectedClientsIds)
			{
				if (connectedClientsId != manager.LocalClientId)
				{
					Send(connectedClientsId, m);
				}
			}
		}

		private unsafe void Send(ulong target, Message m)
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			if (!Object.op_Implicit((Object)(object)manager) || !manager.IsListening)
			{
				return;
			}
			string text = JsonConvert.SerializeObject((object)m);
			FastBufferWriter val = default(FastBufferWriter);
			((FastBufferWriter)(ref val))..ctor(text.Length * 4 + 16, (Allocator)2, -1);
			try
			{
				((FastBufferWriter)(ref val)).WriteValueSafe(text, false);
				manager.CustomMessagingManager.SendNamedMessage("blackbox/audio/1", target, val, (NetworkDelivery)4);
			}
			finally
			{
				((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose();
			}
		}

		public void Dispose()
		{
			Recorder.Enabled = false;
			if (Object.op_Implicit((Object)(object)comms))
			{
				comms.UnsubscribeFromRecordedAudio((IMicrophoneSubscriber)(object)Recorder);
			}
			if (Object.op_Implicit((Object)(object)manager) && manager.CustomMessagingManager != null)
			{
				manager.CustomMessagingManager.UnregisterNamedMessageHandler("blackbox/audio/1");
			}
			Cassette[] array = Object.FindObjectsOfType<Cassette>();
			foreach (Cassette cassette in array)
			{
				if (Object.op_Implicit((Object)(object)cassette.Speaker))
				{
					cassette.Speaker.Stop();
				}
				if (Object.op_Implicit((Object)(object)cassette.Clip))
				{
					Object.Destroy((Object)(object)cassette.Clip);
				}
				cassette.LastStart = -1.0;
			}
			manager = null;
			comms = null;
			Recorder.Reset();
			tapes.Clear();
			audio.Clear();
			partial.Clear();
			dead.Clear();
			lastPosition.Clear();
			deathTapes.Clear();
			requests.Clear();
			rate.Clear();
			outgoing.Clear();
			transfers.Clear();
			pending = null;
			localDead = false;
			nextState = (nextNoise = 0.0);
			sendBudget = 0f;
		}
	}
}