Decompiled source of LethalTV v1.0.0

LethalTV.dll

Decompiled 4 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using GameNetcodeStuff;
using HarmonyLib;
using LethalCompanyInputUtils.Api;
using Microsoft.CodeAnalysis;
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.Video;

[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("LethalTV")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("LethalTV")]
[assembly: AssemblyTitle("LethalTV")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace LethalTV
{
	[BepInPlugin("brant.LethalTV", "LethalTV", "1.0.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		public const string Guid = "brant.LethalTV";

		public const string Name = "LethalTV";

		public const string Version = "1.0.0";

		internal static ManualLogSource Log;

		internal static PluginConfig Cfg;

		internal static TVRemoteInputs Inputs;

		private void Awake()
		{
			//IL_002a: 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_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Cfg = new PluginConfig(((BaseUnityPlugin)this).Config);
			Inputs = new TVRemoteInputs();
			new Harmony("brant.LethalTV").PatchAll(typeof(TVScriptPatches));
			GameObject val = new GameObject("LethalTV")
			{
				hideFlags = (HideFlags)61
			};
			Object.DontDestroyOnLoad((Object)val);
			val.AddComponent<TVSyncManager>();
			Log.LogInfo((object)("LethalTV 1.0.0 loaded. Host video folder: " + VideoLibrary.Folder));
		}
	}
	internal sealed class PluginConfig
	{
		public readonly ConfigEntry<string> VideoFolder;

		public readonly ConfigEntry<bool> Shuffle;

		public readonly ConfigEntry<bool> AllowClientControl;

		public readonly ConfigEntry<int> MaxUploadKBps;

		public readonly ConfigEntry<int> TransferWindowKB;

		public readonly ConfigEntry<float> Volume;

		public readonly ConfigEntry<bool> Muted;

		public readonly ConfigEntry<float> VolumeStep;

		public readonly ConfigEntry<float> RemoteRange;

		public readonly ConfigEntry<bool> ShowHud;

		public readonly ConfigEntry<bool> DownloadAllVideos;

		public readonly ConfigEntry<int> MaxCacheMB;

		public readonly ConfigEntry<int> MaxVideoMB;

		public PluginConfig(ConfigFile cfg)
		{
			//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d2: Expected O, but got Unknown
			//IL_011d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0127: Expected O, but got Unknown
			VideoFolder = cfg.Bind<string>("Host", "VideoFolder", "", "Folder the host's videos are loaded from. Leave empty to use <your Videos folder>\\LethalTV. Best compatibility: H.264 video + AAC audio in .mp4. Also tried: .webm .mov .m4v .wmv .avi .mpg .mpeg .ogv");
			Shuffle = cfg.Bind<bool>("Host", "Shuffle", false, "Shuffle the playlist order each time you host. Otherwise videos play in file name order.");
			AllowClientControl = cfg.Bind<bool>("Host", "AllowClientControl", true, "Let other players change videos with the TV remote. Anyone can still turn the TV on/off, like vanilla.");
			MaxUploadKBps = cfg.Bind<int>("Host", "MaxUploadKBps", 8192, "Upper limit on how fast the host sends videos to players who don't have them yet (KB/s, shared by everyone). 0 = no limit.");
			TransferWindowKB = cfg.Bind<int>("Host", "TransferWindowKB", 256, "How much unacknowledged video data may be in flight per player over Steam. Higher can be faster on high-ping connections; too high can cause resends.");
			Volume = cfg.Bind<float>("Player", "Volume", 1f, new ConfigDescription("Your personal TV volume (only affects what you hear).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			Muted = cfg.Bind<bool>("Player", "Muted", false, "Your personal TV mute.");
			VolumeStep = cfg.Bind<float>("Player", "VolumeStep", 0.1f, new ConfigDescription("How much one volume button press changes the volume.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.01f, 0.5f), Array.Empty<object>()));
			RemoteRange = cfg.Bind<float>("Player", "RemoteRange", 20f, "How close to the TV (in meters) you need to be to use the remote when you're not inside the ship.");
			ShowHud = cfg.Bind<bool>("Player", "ShowHud", true, "Show the on-screen TV banner when you use the remote or the video changes.");
			DownloadAllVideos = cfg.Bind<bool>("Player", "DownloadAllVideos", true, "Download every one of the host's videos in the background. If false, only the current and next video are downloaded.");
			MaxCacheMB = cfg.Bind<int>("Player", "MaxCacheMB", 8192, "Downloaded videos are kept so you don't redownload them next time. Oldest unused ones are deleted past this size.");
			MaxVideoMB = cfg.Bind<int>("Player", "MaxVideoMB", 4096, "Refuse to download any single video larger than this.");
		}
	}
	internal sealed class TVController : MonoBehaviour
	{
		private static readonly FieldRef<TVScript, bool> WasTvOnLastFrame = AccessTools.FieldRefAccess<TVScript, bool>("wasTvOnLastFrame");

		private static readonly FieldRef<TVScript, int> VanillaClip = AccessTools.FieldRefAccess<TVScript, int>("currentClip");

		private float baseVolume;

		private VideoAudioOutputMode origAudioMode;

		private VideoAspectRatio origAspect;

		private bool origLooping;

		private bool customMode;

		private bool screenOn;

		private string loadedId;

		private int loadedSeq = -1;

		private bool preparing;

		private float nextDriftCheck;

		private string failedId;

		private InteractTrigger trigger;

		private string baseHoverTip;

		private float nextTipUpdate;

		public static TVController Current { get; private set; }

		public TVScript Script { get; private set; }

		public static void Attach(TVScript tv)
		{
			TVController tVController = ((Component)tv).GetComponent<TVController>();
			if ((Object)(object)tVController == (Object)null)
			{
				tVController = ((Component)tv).gameObject.AddComponent<TVController>();
			}
			tVController.Init(tv);
		}

		private void Init(TVScript tv)
		{
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Expected O, but got Unknown
			Current = this;
			if (!((Object)(object)Script != (Object)null))
			{
				Script = tv;
				baseVolume = tv.tvSFX.volume;
				origAudioMode = tv.video.audioOutputMode;
				origAspect = tv.video.aspectRatio;
				origLooping = tv.video.isLooping;
				tv.video.errorReceived += new ErrorEventHandler(OnVideoError);
				FindTrigger();
			}
		}

		private void OnEnable()
		{
			if ((Object)(object)Script != (Object)null)
			{
				Current = this;
			}
		}

		private void OnDisable()
		{
			if ((Object)(object)Current == (Object)(object)this)
			{
				Current = null;
			}
		}

		private void OnDestroy()
		{
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Expected O, but got Unknown
			if ((Object)(object)Script != (Object)null && (Object)(object)Script.video != (Object)null)
			{
				Script.video.errorReceived -= new ErrorEventHandler(OnVideoError);
			}
		}

		private void Update()
		{
			if (!((Object)(object)Script == (Object)null))
			{
				PluginConfig cfg = Plugin.Cfg;
				Script.tvSFX.volume = baseVolume * (cfg.Muted.Value ? 0f : cfg.Volume.Value);
				TVSyncManager instance = TVSyncManager.Instance;
				if ((Object)(object)instance != (Object)null && instance.CustomActive)
				{
					CustomUpdate(instance);
				}
				else if (customMode)
				{
					ExitCustomMode();
				}
				UpdateHoverTip(instance);
			}
		}

		private void CustomUpdate(TVSyncManager mgr)
		{
			if (!customMode)
			{
				EnterCustomMode();
			}
			PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null);
			VideoEntry currentEntry = mgr.CurrentEntry;
			bool flag = currentEntry != null && currentEntry.Path != null && currentEntry.Id != failedId;
			bool num = Script.tvOn && flag && (Object)(object)val != (Object)null && !val.isInsideFactory;
			VideoPlayer video = Script.video;
			if (!num)
			{
				if (loadedId != null || screenOn)
				{
					video.Stop();
					loadedId = null;
					preparing = false;
					SetScreen(on: false);
				}
			}
			else if (loadedId != currentEntry.Id)
			{
				loadedId = currentEntry.Id;
				preparing = true;
				SetScreen(on: false);
				video.Stop();
				video.url = VideoLibrary.ToUrl(currentEntry.Path);
				video.Prepare();
			}
			else if (preparing)
			{
				if (video.isPrepared)
				{
					preparing = false;
					SyncTo(mgr, currentEntry, force: true);
					SetScreen(on: true);
				}
			}
			else if (loadedSeq != mgr.Seq)
			{
				SyncTo(mgr, currentEntry, force: true);
			}
			else if (Time.unscaledTime >= nextDriftCheck)
			{
				SyncTo(mgr, currentEntry, force: false);
			}
		}

		private void SyncTo(TVSyncManager mgr, VideoEntry entry, bool force)
		{
			nextDriftCheck = Time.unscaledTime + 2f;
			loadedSeq = mgr.Seq;
			VideoPlayer video = Script.video;
			double elapsed = mgr.Elapsed;
			double num = ((video.length > 0.0) ? video.length : entry.Duration);
			if (!(elapsed >= num - 0.25))
			{
				if (force || Math.Abs(video.time - elapsed) > 1.0)
				{
					video.time = elapsed;
				}
				if (!video.isPlaying)
				{
					video.Play();
				}
			}
		}

		private void EnterCustomMode()
		{
			customMode = true;
			VideoPlayer video = Script.video;
			Script.tvSFX.Stop();
			video.Stop();
			video.source = (VideoSource)1;
			video.isLooping = false;
			video.playOnAwake = false;
			video.waitForFirstFrame = true;
			video.skipOnDrop = true;
			video.aspectRatio = (VideoAspectRatio)3;
			video.audioOutputMode = (VideoAudioOutputMode)1;
			video.controlledAudioTrackCount = 1;
			video.EnableAudioTrack((ushort)0, true);
			video.SetTargetAudioSource((ushort)0, Script.tvSFX);
			WasTvOnLastFrame.Invoke(Script) = false;
			loadedId = null;
			preparing = false;
			failedId = null;
			SetScreen(on: false);
		}

		private void ExitCustomMode()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			customMode = false;
			VideoPlayer video = Script.video;
			video.Stop();
			video.source = (VideoSource)0;
			video.audioOutputMode = origAudioMode;
			video.aspectRatio = origAspect;
			video.isLooping = origLooping;
			loadedId = null;
			preparing = false;
			SetScreen(on: false);
			WasTvOnLastFrame.Invoke(Script) = false;
			int num = VanillaClip.Invoke(Script);
			if (Script.tvOn && num >= 0 && num < Script.tvAudioClips.Length)
			{
				Script.tvSFX.clip = Script.tvAudioClips[num];
				Script.tvSFX.Play();
			}
		}

		private void SetScreen(bool on)
		{
			screenOn = on;
			Material[] sharedMaterials = ((Renderer)Script.tvMesh).sharedMaterials;
			if (sharedMaterials.Length > 1)
			{
				sharedMaterials[1] = (on ? Script.tvOnMaterial : Script.tvOffMaterial);
				((Renderer)Script.tvMesh).sharedMaterials = sharedMaterials;
			}
			if ((Object)(object)Script.tvLight != (Object)null)
			{
				((Behaviour)Script.tvLight).enabled = on;
			}
		}

		private void OnVideoError(VideoPlayer source, string message)
		{
			if (customMode)
			{
				Plugin.Log.LogError((object)("TV couldn't play video: " + message));
				failedId = loadedId;
			}
		}

		private void FindTrigger()
		{
			NetworkObject componentInParent = ((Component)Script).GetComponentInParent<NetworkObject>();
			InteractTrigger[] componentsInChildren = ((Component)(((Object)(object)componentInParent != (Object)null) ? ((Component)componentInParent).transform : ((Component)Script).transform.root)).GetComponentsInChildren<InteractTrigger>(true);
			foreach (InteractTrigger val in componentsInChildren)
			{
				for (int j = 0; j < ((UnityEventBase)val.onInteract).GetPersistentEventCount(); j++)
				{
					if (((UnityEventBase)val.onInteract).GetPersistentTarget(j) == (Object)(object)Script)
					{
						trigger = val;
						baseHoverTip = val.hoverTip;
						return;
					}
				}
			}
		}

		private void UpdateHoverTip(TVSyncManager mgr)
		{
			if (!((Object)(object)trigger == (Object)null) && !(Time.unscaledTime < nextTipUpdate))
			{
				nextTipUpdate = Time.unscaledTime + 0.25f;
				string text = (((Object)(object)mgr != (Object)null) ? mgr.TvStatusLine() : null);
				string text2 = ((text == null) ? baseHoverTip : (baseHoverTip + "\n" + text));
				if (trigger.hoverTip != text2)
				{
					trigger.hoverTip = text2;
				}
			}
		}
	}
	public class TVRemoteInputs : LcInputActions
	{
		[InputAction("<Keyboard>/backslash", Name = "TV Remote: Power")]
		public InputAction Power { get; set; }

		[InputAction("<Keyboard>/rightBracket", Name = "TV Remote: Next Video")]
		public InputAction Next { get; set; }

		[InputAction("<Keyboard>/leftBracket", Name = "TV Remote: Previous Video")]
		public InputAction Previous { get; set; }

		[InputAction("<Keyboard>/equals", Name = "TV Remote: Volume Up")]
		public InputAction VolumeUp { get; set; }

		[InputAction("<Keyboard>/minus", Name = "TV Remote: Volume Down")]
		public InputAction VolumeDown { get; set; }

		[InputAction("<Keyboard>/quote", Name = "TV Remote: Mute")]
		public InputAction Mute { get; set; }
	}
	[HarmonyPatch(typeof(TVScript))]
	internal static class TVScriptPatches
	{
		private static bool CustomActive
		{
			get
			{
				if ((Object)(object)TVSyncManager.Instance != (Object)null)
				{
					return TVSyncManager.Instance.CustomActive;
				}
				return false;
			}
		}

		[HarmonyPatch("OnEnable")]
		[HarmonyPostfix]
		private static void OnEnablePostfix(TVScript __instance)
		{
			TVController.Attach(__instance);
		}

		[HarmonyPatch("Update")]
		[HarmonyPrefix]
		private static bool UpdatePrefix()
		{
			return !CustomActive;
		}

		[HarmonyPatch("TVFinishedClip")]
		[HarmonyPrefix]
		private static bool TVFinishedClipPrefix()
		{
			return !CustomActive;
		}

		[HarmonyPatch("TurnTVOnOff")]
		[HarmonyPrefix]
		private static bool TurnTVOnOffPrefix(TVScript __instance, bool on)
		{
			if (!CustomActive)
			{
				return true;
			}
			__instance.tvOn = on;
			AudioClip val = (on ? __instance.switchTVOn : __instance.switchTVOff);
			__instance.tvSFX.PlayOneShot(val);
			WalkieTalkie.TransmitOneShotAudio(__instance.tvSFX, val, 1f);
			return false;
		}
	}
	internal enum RemoteCommand : byte
	{
		Next = 1,
		Previous
	}
	internal sealed class TVSyncManager : MonoBehaviour
	{
		private delegate void Writer(ref FastBufferWriter w);

		private sealed class Upload
		{
			public VideoEntry Entry;

			public FileStream Stream;

			public long Sent;

			public long Acked;

			public float LastProgress;
		}

		private sealed class Download
		{
			public VideoEntry Entry;

			public FileStream Stream;

			public long Received;

			public float LastData;

			public float LastRequest;
		}

		private const string MsgHello = "LethalTV.Hello";

		private const string MsgManifest = "LethalTV.Manifest";

		private const string MsgState = "LethalTV.State";

		private const string MsgCommand = "LethalTV.Command";

		private const string MsgRequestFile = "LethalTV.RequestFile";

		private const string MsgChunk = "LethalTV.Chunk";

		private const string MsgAck = "LethalTV.Ack";

		private const NetworkDelivery Delivery = (NetworkDelivery)4;

		private const int MaxMessageSize = 1048576;

		private const int MaxPlaylist = 2000;

		private NetworkManager net;

		private CustomMessagingManager messaging;

		private bool isServer;

		private bool manifestReceived;

		private bool clientControlAllowed = true;

		public readonly List<VideoEntry> Playlist = new List<VideoEntry>();

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

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

		private int chunkSize = 32768;

		private int windowBytes = 262144;

		private byte[] sendBuffer = new byte[32768];

		private float uploadAllowance;

		private Download download;

		private Task<string> verifyTask;

		private VideoEntry verifyEntry;

		private string verifyPath;

		private readonly Dictionary<string, int> verifyFailures = new Dictionary<string, int>();

		private byte[] recvBuffer = new byte[65536];

		private float hudUntil;

		private string hudNote;

		private GUIStyle hudStyle;

		private GUIStyle hudShadowStyle;

		public static TVSyncManager Instance { get; private set; }

		public int CurrentIndex { get; private set; }

		public double ClipStart { get; private set; }

		public int Seq { get; private set; }

		public bool InSession => messaging != null;

		public bool CustomActive
		{
			get
			{
				if (InSession && Playlist.Count > 0)
				{
					if (!isServer)
					{
						return manifestReceived;
					}
					return true;
				}
				return false;
			}
		}

		public VideoEntry CurrentEntry
		{
			get
			{
				if (!CustomActive || CurrentIndex < 0 || CurrentIndex >= Playlist.Count)
				{
					return null;
				}
				return Playlist[CurrentIndex];
			}
		}

		public 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)net != (Object)null))
				{
					return 0.0;
				}
				NetworkTime serverTime = net.ServerTime;
				return ((NetworkTime)(ref serverTime)).Time;
			}
		}

		public double Elapsed => Math.Max(0.0, Now - ClipStart);

		private static PlayerControllerB LocalPlayer
		{
			get
			{
				if (!((Object)(object)GameNetworkManager.Instance != (Object)null))
				{
					return null;
				}
				return GameNetworkManager.Instance.localPlayerController;
			}
		}

		private void Awake()
		{
			Instance = this;
			VideoLibrary.Changed += OnLibraryChanged;
			((MonoBehaviour)this).StartCoroutine(VideoLibrary.Scan());
		}

		private void OnDestroy()
		{
			VideoLibrary.Changed -= OnLibraryChanged;
			EndSession();
		}

		private void Update()
		{
			TrackSession();
			if (InSession)
			{
				try
				{
					if (isServer)
					{
						AdvanceTimeline();
						PumpUploads();
					}
					else
					{
						PumpDownloads();
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)ex);
				}
			}
			HandleRemote();
		}

		private void TrackSession()
		{
			NetworkManager singleton = NetworkManager.Singleton;
			if (!((Object)(object)singleton != (Object)null) || !singleton.IsListening || singleton.ShutdownInProgress || (!singleton.IsServer && !singleton.IsConnectedClient) || singleton.CustomMessagingManager == null)
			{
				if (InSession)
				{
					EndSession();
				}
			}
			else if (messaging != singleton.CustomMessagingManager)
			{
				if (InSession)
				{
					EndSession();
				}
				BeginSession(singleton);
			}
		}

		private void BeginSession(NetworkManager nm)
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Expected O, but got Unknown
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Expected O, but got Unknown
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Expected O, but got Unknown
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008f: Expected O, but got Unknown
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Expected O, but got Unknown
			//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c7: Expected O, but got Unknown
			//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e3: Expected O, but got Unknown
			net = nm;
			isServer = nm.IsServer;
			messaging = nm.CustomMessagingManager;
			messaging.RegisterNamedMessageHandler("LethalTV.Hello", new HandleNamedMessageDelegate(OnHello));
			messaging.RegisterNamedMessageHandler("LethalTV.Manifest", new HandleNamedMessageDelegate(OnManifest));
			messaging.RegisterNamedMessageHandler("LethalTV.State", new HandleNamedMessageDelegate(OnState));
			messaging.RegisterNamedMessageHandler("LethalTV.Command", new HandleNamedMessageDelegate(OnCommand));
			messaging.RegisterNamedMessageHandler("LethalTV.RequestFile", new HandleNamedMessageDelegate(OnRequestFile));
			messaging.RegisterNamedMessageHandler("LethalTV.Chunk", new HandleNamedMessageDelegate(OnChunk));
			messaging.RegisterNamedMessageHandler("LethalTV.Ack", new HandleNamedMessageDelegate(OnAck));
			ConfigureTransfer(nm);
			TuneSteamNetworking(nm);
			Playlist.Clear();
			manifestReceived = false;
			CurrentIndex = 0;
			ClipStart = Now;
			Seq = 0;
			if (isServer)
			{
				nm.OnClientDisconnectCallback += OnClientDisconnected;
				clientControlAllowed = Plugin.Cfg.AllowClientControl.Value;
				RebuildPlaylist(broadcast: false);
				((MonoBehaviour)this).StartCoroutine(VideoLibrary.Scan());
				Plugin.Log.LogInfo((object)$"Hosting with {Playlist.Count} custom video(s).");
			}
			else
			{
				SendToServer("LethalTV.Hello", 8, delegate
				{
				});
			}
		}

		private void EndSession()
		{
			if (messaging != null)
			{
				try
				{
					string[] array = new string[7] { "LethalTV.Hello", "LethalTV.Manifest", "LethalTV.State", "LethalTV.Command", "LethalTV.RequestFile", "LethalTV.Chunk", "LethalTV.Ack" };
					foreach (string text in array)
					{
						messaging.UnregisterNamedMessageHandler(text);
					}
				}
				catch (Exception)
				{
				}
			}
			if ((Object)(object)net != (Object)null && isServer)
			{
				try
				{
					net.OnClientDisconnectCallback -= OnClientDisconnected;
				}
				catch (Exception)
				{
				}
			}
			foreach (Upload value in uploads.Values)
			{
				value.Stream.Dispose();
			}
			uploads.Clear();
			lastCommandTime.Clear();
			CloseDownload(sendStop: false);
			verifyTask = null;
			verifyEntry = null;
			Playlist.Clear();
			manifestReceived = false;
			messaging = null;
			net = null;
		}

		private void OnClientDisconnected(ulong clientId)
		{
			CloseUpload(clientId);
			lastCommandTime.Remove(clientId);
		}

		private void ConfigureTransfer(NetworkManager nm)
		{
			NetworkTransport val = nm.NetworkConfig?.NetworkTransport;
			if ((Object)(object)val != (Object)null && ((object)val).GetType().Name == "UnityTransport")
			{
				int num = 6144;
				try
				{
					PropertyInfo property = ((object)val).GetType().GetProperty("MaxPayloadSize");
					if (property != null)
					{
						num = (int)property.GetValue(val);
					}
				}
				catch (Exception)
				{
				}
				chunkSize = Mathf.Clamp(num - 512, 1024, 16384);
				windowBytes = chunkSize * 4;
			}
			else
			{
				chunkSize = 32768;
				windowBytes = Math.Max(chunkSize, Plugin.Cfg.TransferWindowKB.Value * 1024);
			}
			sendBuffer = new byte[chunkSize];
		}

		private void TuneSteamNetworking(NetworkManager nm)
		{
			try
			{
				if (((object)nm.NetworkConfig?.NetworkTransport)?.GetType().Name != "FacepunchTransport")
				{
					return;
				}
				Type type = Type.GetType("Steamworks.SteamNetworkingUtils, Facepunch.Steamworks.Win64");
				Type configEnum = type?.Assembly.GetType("Steamworks.NetConfig");
				MethodInfo get = type?.GetMethod("GetConfigInt", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				MethodInfo set = type?.GetMethod("SetConfigInt", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				if (!(configEnum == null) && !(get == null) && !(set == null))
				{
					int value = Plugin.Cfg.MaxUploadKBps.Value;
					int num = ((value > 0) ? (Math.Max(value, 1024) * 1024) : 0);
					int num2 = Get("SendRateMax");
					if (num2 != 0 && (num == 0 || num2 < num))
					{
						Set("SendRateMax", num);
					}
					int num3 = Math.Max(windowBytes * 4, 2097152);
					if (Get("SendBufferSize") < num3)
					{
						Set("SendBufferSize", num3);
					}
					Plugin.Log.LogInfo((object)string.Format("Steam networking: SendRateMax={0} SendBufferSize={1}", Get("SendRateMax"), Get("SendBufferSize")));
				}
				int Get(string key)
				{
					return (int)get.Invoke(null, new object[1] { Enum.Parse(configEnum, key) });
				}
				void Set(string key, int num4)
				{
					set.Invoke(null, new object[2]
					{
						Enum.Parse(configEnum, key),
						num4
					});
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Couldn't tune Steam networking (video transfers may be slower): " + ex.Message));
			}
		}

		private void Send(string name, ulong clientId, int initialSize, Writer write)
		{
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			FastBufferWriter w = default(FastBufferWriter);
			((FastBufferWriter)(ref w))..ctor(initialSize, (Allocator)2, 1048576);
			try
			{
				write(ref w);
				messaging.SendNamedMessage(name, clientId, w, (NetworkDelivery)4);
			}
			finally
			{
				((FastBufferWriter)(ref w)).Dispose();
			}
		}

		private void SendToServer(string name, int initialSize, Writer write)
		{
			Send(name, 0uL, initialSize, write);
		}

		private void Broadcast(string name, int initialSize, Writer write)
		{
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			List<ulong> list = net.ConnectedClientsIds.Where((ulong id) => id != net.LocalClientId).ToList();
			if (list.Count == 0)
			{
				return;
			}
			FastBufferWriter w = default(FastBufferWriter);
			((FastBufferWriter)(ref w))..ctor(initialSize, (Allocator)2, 1048576);
			try
			{
				write(ref w);
				messaging.SendNamedMessage(name, (IReadOnlyList<ulong>)list, w, (NetworkDelivery)4);
			}
			finally
			{
				((FastBufferWriter)(ref w)).Dispose();
			}
		}

		private void OnLibraryChanged()
		{
			if (InSession && isServer)
			{
				RebuildPlaylist(broadcast: true);
			}
		}

		private void RebuildPlaylist(bool broadcast)
		{
			string currentId = ((CurrentIndex >= 0 && CurrentIndex < Playlist.Count) ? Playlist[CurrentIndex].Id : null);
			List<VideoEntry> list = VideoLibrary.Ready.ToList();
			if (Plugin.Cfg.Shuffle.Value)
			{
				Random random = new Random();
				for (int num = list.Count - 1; num > 0; num--)
				{
					int num2 = random.Next(num + 1);
					List<VideoEntry> list2 = list;
					int index = num;
					int index2 = num2;
					VideoEntry value = list[num2];
					VideoEntry value2 = list[num];
					list2[index] = value;
					list[index2] = value2;
				}
			}
			Playlist.Clear();
			Playlist.AddRange(list);
			int num3 = ((currentId != null) ? Playlist.FindIndex((VideoEntry e) => e.Id == currentId) : (-1));
			if (num3 >= 0)
			{
				CurrentIndex = num3;
			}
			else
			{
				CurrentIndex = 0;
				ClipStart = Now;
			}
			Seq++;
			if (broadcast)
			{
				Broadcast("LethalTV.Manifest", 1024, WriteManifest);
				Broadcast("LethalTV.State", 64, WriteState);
				Plugin.Log.LogInfo((object)$"Playlist updated: {Playlist.Count} video(s).");
			}
		}

		private void AdvanceTimeline()
		{
			if (Playlist.Count == 0)
			{
				return;
			}
			if (CurrentIndex >= Playlist.Count)
			{
				CurrentIndex = 0;
			}
			bool flag = false;
			for (int i = 0; i <= Playlist.Count; i++)
			{
				if (!(Now - ClipStart >= Playlist[CurrentIndex].Duration))
				{
					break;
				}
				ClipStart += Playlist[CurrentIndex].Duration;
				CurrentIndex = (CurrentIndex + 1) % Playlist.Count;
				flag = true;
			}
			if (Now - ClipStart >= Playlist[CurrentIndex].Duration)
			{
				ClipStart = Now;
			}
			if (flag)
			{
				Seq++;
				Broadcast("LethalTV.State", 64, WriteState);
				NotifyChannelChanged();
			}
		}

		private void ApplyCommand(RemoteCommand cmd)
		{
			if (Playlist.Count != 0)
			{
				int count = Playlist.Count;
				switch (cmd)
				{
				default:
					return;
				case RemoteCommand.Next:
					CurrentIndex = (CurrentIndex + 1) % count;
					break;
				case RemoteCommand.Previous:
					CurrentIndex = (CurrentIndex - 1 + count) % count;
					break;
				}
				ClipStart = Now;
				Seq++;
				Broadcast("LethalTV.State", 64, WriteState);
				NotifyChannelChanged();
			}
		}

		private void WriteManifest(ref FastBufferWriter w)
		{
			//IL_0009: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: 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)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_008d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			((FastBufferWriter)(ref w)).WriteValueSafe<bool>(ref clientControlAllowed, default(ForPrimitives));
			int count = Playlist.Count;
			((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref count, default(ForPrimitives));
			foreach (VideoEntry item in Playlist)
			{
				((FastBufferWriter)(ref w)).WriteValueSafe(item.Id, true);
				((FastBufferWriter)(ref w)).WriteValueSafe(item.Name, false);
				((FastBufferWriter)(ref w)).WriteValueSafe(item.Ext, true);
				((FastBufferWriter)(ref w)).WriteValueSafe<long>(ref item.Size, default(ForPrimitives));
				((FastBufferWriter)(ref w)).WriteValueSafe<double>(ref item.Duration, default(ForPrimitives));
			}
		}

		private void WriteState(ref FastBufferWriter w)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: 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_0042: Unknown result type (might be due to invalid IL or missing references)
			int currentIndex = CurrentIndex;
			((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref currentIndex, default(ForPrimitives));
			double clipStart = ClipStart;
			((FastBufferWriter)(ref w)).WriteValueSafe<double>(ref clipStart, default(ForPrimitives));
			currentIndex = Seq;
			((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref currentIndex, default(ForPrimitives));
		}

		private void OnHello(ulong sender, FastBufferReader reader)
		{
			if (isServer)
			{
				Send("LethalTV.Manifest", sender, 1024, WriteManifest);
				Send("LethalTV.State", sender, 64, WriteState);
			}
		}

		private void OnCommand(ulong sender, FastBufferReader reader)
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			if (isServer)
			{
				byte cmd = default(byte);
				((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref cmd, default(ForPrimitives));
				if (clientControlAllowed && (!lastCommandTime.TryGetValue(sender, out var value) || !(Time.unscaledTime - value < 0.2f)))
				{
					lastCommandTime[sender] = Time.unscaledTime;
					ApplyCommand((RemoteCommand)cmd);
				}
			}
		}

		private void OnRequestFile(ulong sender, FastBufferReader reader)
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			if (!isServer)
			{
				return;
			}
			string id = default(string);
			((FastBufferReader)(ref reader)).ReadValueSafe(ref id, true);
			long val = default(long);
			((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref val, default(ForPrimitives));
			CloseUpload(sender);
			if (string.IsNullOrEmpty(id))
			{
				return;
			}
			VideoEntry videoEntry = Playlist.Find((VideoEntry e) => e.Id == id);
			if (videoEntry == null || videoEntry.Path == null)
			{
				return;
			}
			try
			{
				FileStream stream = new FileStream(videoEntry.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 65536);
				val = Math.Max(0L, Math.Min(val, videoEntry.Size));
				uploads[sender] = new Upload
				{
					Entry = videoEntry,
					Stream = stream,
					Sent = val,
					Acked = val,
					LastProgress = Time.unscaledTime
				};
				Plugin.Log.LogInfo((object)$"Sending '{videoEntry.Name}' to client {sender} from {val / 1024} KB");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Couldn't open '" + videoEntry.Path + "' to send: " + ex.Message));
			}
		}

		private void OnAck(ulong sender, FastBufferReader reader)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			if (!isServer)
			{
				return;
			}
			string text = default(string);
			((FastBufferReader)(ref reader)).ReadValueSafe(ref text, true);
			long num = default(long);
			((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref num, default(ForPrimitives));
			if (uploads.TryGetValue(sender, out var value) && !(value.Entry.Id != text))
			{
				if (num > value.Acked && num <= value.Sent)
				{
					value.Acked = num;
					value.LastProgress = Time.unscaledTime;
				}
				if (value.Acked >= value.Entry.Size)
				{
					Plugin.Log.LogInfo((object)$"Client {sender} finished receiving '{value.Entry.Name}'");
					CloseUpload(sender);
				}
			}
		}

		private void CloseUpload(ulong clientId)
		{
			if (uploads.TryGetValue(clientId, out var value))
			{
				value.Stream.Dispose();
				uploads.Remove(clientId);
			}
		}

		private void PumpUploads()
		{
			if (uploads.Count == 0)
			{
				uploadAllowance = 0f;
				return;
			}
			int value = Plugin.Cfg.MaxUploadKBps.Value;
			if (value > 0)
			{
				float num = (float)value * 1024f;
				uploadAllowance = Mathf.Min(uploadAllowance + num * Time.unscaledDeltaTime, num * 0.25f + (float)chunkSize);
			}
			else
			{
				uploadAllowance = float.MaxValue;
			}
			foreach (KeyValuePair<ulong, Upload> item in uploads.ToList())
			{
				Upload value2 = item.Value;
				if (value2.Sent > value2.Acked && Time.unscaledTime - value2.LastProgress > 10f)
				{
					Plugin.Log.LogWarning((object)$"Transfer of '{value2.Entry.Name}' to client {item.Key} stalled, resending from {value2.Acked / 1024} KB");
					value2.Sent = value2.Acked;
					value2.LastProgress = Time.unscaledTime;
				}
				while (value2.Sent < value2.Entry.Size && value2.Sent - value2.Acked < windowBytes && uploadAllowance > 0f)
				{
					int count = (int)Math.Min(chunkSize, value2.Entry.Size - value2.Sent);
					int read;
					try
					{
						value2.Stream.Position = value2.Sent;
						read = ReadFully(value2.Stream, sendBuffer, count);
					}
					catch (Exception ex)
					{
						Plugin.Log.LogError((object)("Error reading '" + value2.Entry.Path + "': " + ex.Message));
						read = 0;
					}
					if (read <= 0)
					{
						CloseUpload(item.Key);
						break;
					}
					string id = value2.Entry.Id;
					long offset = value2.Sent;
					Send("LethalTV.Chunk", item.Key, read + 256, delegate(ref FastBufferWriter w)
					{
						//IL_0016: Unknown result type (might be due to invalid IL or missing references)
						//IL_001c: Unknown result type (might be due to invalid IL or missing references)
						//IL_002b: Unknown result type (might be due to invalid IL or missing references)
						//IL_0031: Unknown result type (might be due to invalid IL or missing references)
						((FastBufferWriter)(ref w)).WriteValueSafe(id, true);
						((FastBufferWriter)(ref w)).WriteValueSafe<long>(ref offset, default(ForPrimitives));
						((FastBufferWriter)(ref w)).WriteValueSafe<int>(ref read, default(ForPrimitives));
						((FastBufferWriter)(ref w)).WriteBytesSafe(sendBuffer, read, 0);
					});
					value2.Sent += read;
					uploadAllowance -= read;
				}
			}
		}

		private static int ReadFully(Stream stream, byte[] buffer, int count)
		{
			int i;
			int num;
			for (i = 0; i < count; i += num)
			{
				num = stream.Read(buffer, i, count - i);
				if (num <= 0)
				{
					break;
				}
			}
			return i;
		}

		private void OnManifest(ulong sender, FastBufferReader reader)
		{
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: 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_00bd: Unknown result type (might be due to invalid IL or missing references)
			if (isServer)
			{
				return;
			}
			try
			{
				bool flag = default(bool);
				((FastBufferReader)(ref reader)).ReadValueSafe<bool>(ref flag, default(ForPrimitives));
				int num = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives));
				if (num < 0 || num > 2000)
				{
					throw new InvalidDataException($"bad playlist size {num}");
				}
				long num2 = (long)Plugin.Cfg.MaxVideoMB.Value * 1024L * 1024;
				List<VideoEntry> list = new List<VideoEntry>(num);
				string id = default(string);
				string text = default(string);
				string ext = default(string);
				long num3 = default(long);
				double num4 = default(double);
				for (int i = 0; i < num; i++)
				{
					((FastBufferReader)(ref reader)).ReadValueSafe(ref id, true);
					((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false);
					((FastBufferReader)(ref reader)).ReadValueSafe(ref ext, true);
					((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref num3, default(ForPrimitives));
					((FastBufferReader)(ref reader)).ReadValueSafe<double>(ref num4, default(ForPrimitives));
					VideoEntry videoEntry = new VideoEntry
					{
						Id = id,
						Name = (text ?? "video"),
						Ext = ext,
						Size = num3,
						Duration = num4
					};
					if (!VideoCache.IsValidId(id) || !VideoLibrary.IsSupportedExtension(ext) || num3 <= 0 || !(num4 > 0.0))
					{
						videoEntry.Invalid = "Invalid video";
					}
					else if (num3 > num2)
					{
						videoEntry.Invalid = "Video too large (see MaxVideoMB)";
					}
					else
					{
						videoEntry.Path = VideoCache.FindComplete(videoEntry);
					}
					list.Add(videoEntry);
				}
				clientControlAllowed = flag;
				Playlist.Clear();
				Playlist.AddRange(list);
				manifestReceived = true;
				if (download != null)
				{
					VideoEntry videoEntry2 = Playlist.Find((VideoEntry e) => e.Id == download.Entry.Id && e.Invalid == null);
					if (videoEntry2 == null)
					{
						CloseDownload(sendStop: true);
					}
					else
					{
						download.Entry = videoEntry2;
					}
				}
				int num5 = Playlist.Count((VideoEntry e) => e.Path != null);
				Plugin.Log.LogInfo((object)$"Host playlist: {Playlist.Count} video(s), {num5} already cached.");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Bad playlist from host: " + ex.Message));
			}
		}

		private void OnState(ulong sender, FastBufferReader reader)
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			if (!isServer)
			{
				int num = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives));
				double clipStart = default(double);
				((FastBufferReader)(ref reader)).ReadValueSafe<double>(ref clipStart, default(ForPrimitives));
				int num2 = default(int);
				((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num2, default(ForPrimitives));
				bool num3 = num != CurrentIndex || num2 != Seq;
				CurrentIndex = num;
				ClipStart = clipStart;
				Seq = num2;
				if (num3 && CustomActive)
				{
					NotifyChannelChanged();
				}
			}
		}

		private void OnChunk(ulong sender, FastBufferReader reader)
		{
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Unknown result type (might be due to invalid IL or missing references)
			if (isServer)
			{
				return;
			}
			string id = default(string);
			((FastBufferReader)(ref reader)).ReadValueSafe(ref id, true);
			long num = default(long);
			((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref num, default(ForPrimitives));
			int num2 = default(int);
			((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num2, default(ForPrimitives));
			if (num2 <= 0 || num2 > 1048576)
			{
				return;
			}
			if (recvBuffer.Length < num2)
			{
				recvBuffer = new byte[num2];
			}
			((FastBufferReader)(ref reader)).ReadBytesSafe(ref recvBuffer, num2, 0);
			Download download = this.download;
			if (download == null || download.Entry.Id != id || num < download.Received)
			{
				return;
			}
			if (num > download.Received)
			{
				if (Time.unscaledTime - download.LastRequest > 2f)
				{
					RequestFrom(download);
				}
				return;
			}
			if (num + num2 > download.Entry.Size)
			{
				Plugin.Log.LogError((object)("Host sent too much data for '" + download.Entry.Name + "', discarding download."));
				FailDownload(download);
				return;
			}
			download.Stream.Write(recvBuffer, 0, num2);
			download.Received += num2;
			download.LastData = Time.unscaledTime;
			long received = download.Received;
			SendToServer("LethalTV.Ack", 96, delegate(ref FastBufferWriter w)
			{
				//IL_0016: Unknown result type (might be due to invalid IL or missing references)
				//IL_001c: Unknown result type (might be due to invalid IL or missing references)
				((FastBufferWriter)(ref w)).WriteValueSafe(id, true);
				((FastBufferWriter)(ref w)).WriteValueSafe<long>(ref received, default(ForPrimitives));
			});
			if (download.Received >= download.Entry.Size)
			{
				CompleteDownload();
			}
		}

		private void PumpDownloads()
		{
			if (!manifestReceived)
			{
				return;
			}
			if (verifyTask != null)
			{
				if (!verifyTask.IsCompleted)
				{
					return;
				}
				FinishVerify();
			}
			VideoEntry videoEntry = PickDownload();
			if (this.download != null && this.download.Entry != videoEntry)
			{
				CloseDownload(videoEntry == null);
			}
			if (this.download == null && videoEntry != null)
			{
				StartDownload(videoEntry);
			}
			Download download = this.download;
			if (download != null && Time.unscaledTime - download.LastData > 15f && Time.unscaledTime - download.LastRequest > 15f)
			{
				Plugin.Log.LogWarning((object)("Download of '" + download.Entry.Name + "' stalled, asking host to resume."));
				RequestFrom(download);
			}
		}

		private VideoEntry PickDownload()
		{
			int count = Playlist.Count;
			if (count == 0)
			{
				return null;
			}
			int num = ((CurrentIndex >= 0 && CurrentIndex < count) ? CurrentIndex : 0);
			int num2 = (Plugin.Cfg.DownloadAllVideos.Value ? count : Math.Min(2, count));
			for (int i = 0; i < num2; i++)
			{
				VideoEntry videoEntry = Playlist[(num + i) % count];
				if (videoEntry.Path == null && videoEntry.Invalid == null && (!verifyFailures.TryGetValue(videoEntry.Id, out var value) || value < 2))
				{
					return videoEntry;
				}
			}
			return null;
		}

		private void StartDownload(VideoEntry e)
		{
			try
			{
				Directory.CreateDirectory(VideoCache.Dir);
				FileStream fileStream = new FileStream(VideoCache.PartPath(e), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
				if (fileStream.Length > e.Size)
				{
					fileStream.SetLength(0L);
				}
				fileStream.Position = fileStream.Length;
				download = new Download
				{
					Entry = e,
					Stream = fileStream,
					Received = fileStream.Length,
					LastData = Time.unscaledTime
				};
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Couldn't write to video cache: " + ex.Message));
				verifyFailures[e.Id] = 99;
				return;
			}
			if (download.Received >= e.Size)
			{
				CompleteDownload();
				return;
			}
			Plugin.Log.LogInfo((object)((download.Received > 0) ? $"Resuming download of '{e.Name}' at {download.Received * 100 / e.Size}%" : $"Downloading '{e.Name}' ({(double)e.Size / 1048576.0:0.0} MB) from host"));
			RequestFrom(download);
		}

		private void RequestFrom(Download d)
		{
			d.LastRequest = Time.unscaledTime;
			d.LastData = Time.unscaledTime;
			string id = d.Entry.Id;
			long offset = d.Received;
			SendToServer("LethalTV.RequestFile", 96, delegate(ref FastBufferWriter w)
			{
				//IL_0016: Unknown result type (might be due to invalid IL or missing references)
				//IL_001c: Unknown result type (might be due to invalid IL or missing references)
				((FastBufferWriter)(ref w)).WriteValueSafe(id, true);
				((FastBufferWriter)(ref w)).WriteValueSafe<long>(ref offset, default(ForPrimitives));
			});
		}

		private void CloseDownload(bool sendStop)
		{
			if (download == null)
			{
				return;
			}
			try
			{
				download.Stream.Dispose();
			}
			catch (Exception)
			{
			}
			download = null;
			if (sendStop && InSession)
			{
				SendToServer("LethalTV.RequestFile", 96, delegate(ref FastBufferWriter w)
				{
					//IL_0014: Unknown result type (might be due to invalid IL or missing references)
					//IL_001a: Unknown result type (might be due to invalid IL or missing references)
					((FastBufferWriter)(ref w)).WriteValueSafe("", true);
					long num = 0L;
					((FastBufferWriter)(ref w)).WriteValueSafe<long>(ref num, default(ForPrimitives));
				});
			}
		}

		private void FailDownload(Download d)
		{
			CloseDownload(sendStop: true);
			try
			{
				File.Delete(VideoCache.PartPath(d.Entry));
			}
			catch (Exception)
			{
			}
			verifyFailures.TryGetValue(d.Entry.Id, out var value);
			verifyFailures[d.Entry.Id] = value + 1;
		}

		private void CompleteDownload()
		{
			Download download = this.download;
			this.download = null;
			download.Stream.Flush();
			download.Stream.Dispose();
			verifyEntry = download.Entry;
			verifyPath = VideoCache.PartPath(download.Entry);
			string path = verifyPath;
			verifyTask = Task.Run(() => VideoLibrary.HashFile(path));
		}

		private void FinishVerify()
		{
			VideoEntry videoEntry = verifyEntry;
			string text = ((verifyTask.Status == TaskStatus.RanToCompletion) ? verifyTask.Result : null);
			verifyTask = null;
			verifyEntry = null;
			try
			{
				if (text == videoEntry.Id)
				{
					string text2 = VideoCache.CompletePath(videoEntry);
					if (File.Exists(text2))
					{
						File.Delete(text2);
					}
					File.Move(verifyPath, text2);
					foreach (VideoEntry item in Playlist)
					{
						if (item.Id == videoEntry.Id)
						{
							item.Path = text2;
						}
					}
					Plugin.Log.LogInfo((object)("Downloaded '" + videoEntry.Name + "'"));
					VideoCache.Prune(Playlist.Select((VideoEntry p) => p.Id), (long)Plugin.Cfg.MaxCacheMB.Value * 1024L * 1024);
				}
				else
				{
					File.Delete(verifyPath);
					verifyFailures.TryGetValue(videoEntry.Id, out var value);
					verifyFailures[videoEntry.Id] = value + 1;
					Plugin.Log.LogWarning((object)("Download of '" + videoEntry.Name + "' was corrupted, retrying."));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Couldn't finish download of '" + videoEntry.Name + "': " + ex.Message));
				verifyFailures[videoEntry.Id] = 99;
			}
		}

		private static bool InRemoteRange(PlayerControllerB player, TVController tv)
		{
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null || (Object)(object)tv == (Object)null || !((Behaviour)tv).isActiveAndEnabled)
			{
				return false;
			}
			if (!player.isPlayerDead && !player.isInHangarShipRoom)
			{
				return Vector3.Distance(((Component)player).transform.position, ((Component)tv).transform.position) <= Plugin.Cfg.RemoteRange.Value;
			}
			return true;
		}

		private void HandleRemote()
		{
			TVRemoteInputs inputs = Plugin.Inputs;
			PlayerControllerB localPlayer = LocalPlayer;
			if (inputs == null || (Object)(object)localPlayer == (Object)null || localPlayer.isTypingChat || localPlayer.inTerminalMenu || ((Object)(object)localPlayer.quickMenuManager != (Object)null && localPlayer.quickMenuManager.isMenuOpen))
			{
				return;
			}
			bool flag = inputs.Power.WasPressedThisFrame();
			bool flag2 = inputs.Next.WasPressedThisFrame();
			bool flag3 = inputs.Previous.WasPressedThisFrame();
			bool flag4 = inputs.VolumeUp.WasPressedThisFrame();
			bool flag5 = inputs.VolumeDown.WasPressedThisFrame();
			bool flag6 = inputs.Mute.WasPressedThisFrame();
			if (!(flag || flag2 || flag3 || flag4 || flag5 || flag6))
			{
				return;
			}
			TVController current = TVController.Current;
			if (!InRemoteRange(localPlayer, current))
			{
				return;
			}
			PluginConfig cfg = Plugin.Cfg;
			if (flag4 || flag5)
			{
				float num = cfg.Volume.Value + (flag4 ? cfg.VolumeStep.Value : (0f - cfg.VolumeStep.Value));
				cfg.Volume.Value = Mathf.Clamp01(Mathf.Round(num * 100f) / 100f);
				cfg.Muted.Value = false;
				ShowHud();
			}
			if (flag6)
			{
				cfg.Muted.Value = !cfg.Muted.Value;
				ShowHud();
			}
			if (flag && !localPlayer.isPlayerDead)
			{
				current.Script.SwitchTVLocalClient();
				ShowHud(current.Script.tvOn ? "TV off" : "TV on");
			}
			if ((flag2 || flag3) && !localPlayer.isPlayerDead)
			{
				if (!CustomActive)
				{
					ShowHud("The host has no custom videos");
				}
				else if (!isServer && !clientControlAllowed)
				{
					ShowHud("The host has disabled changing videos");
				}
				else
				{
					SendCommand(flag2 ? RemoteCommand.Next : RemoteCommand.Previous);
				}
			}
		}

		private void SendCommand(RemoteCommand cmd)
		{
			if (isServer)
			{
				ApplyCommand(cmd);
				return;
			}
			SendToServer("LethalTV.Command", 8, delegate(ref FastBufferWriter w)
			{
				//IL_000c: Unknown result type (might be due to invalid IL or missing references)
				//IL_0012: Unknown result type (might be due to invalid IL or missing references)
				byte b = (byte)cmd;
				((FastBufferWriter)(ref w)).WriteValueSafe<byte>(ref b, default(ForPrimitives));
			});
		}

		private void NotifyChannelChanged()
		{
			TVController current = TVController.Current;
			if ((Object)(object)current != (Object)null && current.Script.tvOn && InRemoteRange(LocalPlayer, current))
			{
				ShowHud();
			}
		}

		private void ShowHud(string note = null)
		{
			hudUntil = Time.unscaledTime + 3f;
			hudNote = note;
		}

		public string TvStatusLine()
		{
			VideoEntry currentEntry = CurrentEntry;
			if (currentEntry == null)
			{
				return null;
			}
			if (currentEntry.Invalid != null)
			{
				return currentEntry.Invalid;
			}
			if (currentEntry.Path != null)
			{
				return "Playing: " + Shorten(currentEntry.Name, 32);
			}
			if (download != null && download.Entry.Id == currentEntry.Id)
			{
				return $"Downloading video... {download.Received * 100 / Math.Max(1L, currentEntry.Size)}%";
			}
			if (verifyEntry != null && verifyEntry.Id == currentEntry.Id)
			{
				return "Finishing download...";
			}
			return "Waiting for video...";
		}

		private static string Shorten(string s, int max)
		{
			if (s.Length > max)
			{
				return s.Substring(0, max - 3) + "...";
			}
			return s;
		}

		private static string Bind(InputAction action)
		{
			try
			{
				string bindingDisplayString = InputActionRebindingExtensions.GetBindingDisplayString(action, (DisplayStringOptions)0, (string)null);
				return string.IsNullOrEmpty(bindingDisplayString) ? "?" : bindingDisplayString;
			}
			catch (Exception)
			{
				return "?";
			}
		}

		private void OnGUI()
		{
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Expected O, but got Unknown
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Expected O, but got Unknown
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f2: Unknown result type (might be due to invalid IL or missing references)
			//IL_0304: Unknown result type (might be due to invalid IL or missing references)
			if (Plugin.Cfg.ShowHud.Value && !(Time.unscaledTime > hudUntil) && !((Object)(object)LocalPlayer == (Object)null))
			{
				if (hudStyle == null)
				{
					hudStyle = new GUIStyle(GUI.skin.label)
					{
						alignment = (TextAnchor)1,
						fontStyle = (FontStyle)1,
						wordWrap = false
					};
					hudStyle.normal.textColor = new Color(1f, 0.55f, 0.15f);
					hudShadowStyle = new GUIStyle(hudStyle);
					hudShadowStyle.normal.textColor = new Color(0f, 0f, 0f, 0.85f);
				}
				int num = Mathf.Max(12, Mathf.RoundToInt((float)Screen.height / 1080f * 22f));
				hudStyle.fontSize = num;
				hudShadowStyle.fontSize = num;
				PluginConfig cfg = Plugin.Cfg;
				List<string> list = new List<string>();
				if (hudNote != null)
				{
					list.Add(hudNote);
				}
				VideoEntry currentEntry = CurrentEntry;
				if (currentEntry != null)
				{
					list.Add($"CH {CurrentIndex + 1}/{Playlist.Count}   {Shorten(currentEntry.Name, 40)}");
				}
				string text = TvStatusLine();
				if (text != null && currentEntry != null && currentEntry.Path == null)
				{
					list.Add(text);
				}
				int num2 = Mathf.RoundToInt(cfg.Volume.Value * 100f);
				int num3 = Mathf.RoundToInt(cfg.Volume.Value * 10f);
				list.Add(cfg.Muted.Value ? "VOLUME  MUTED" : $"VOLUME  [{new string('|', num3)}{new string('.', 10 - num3)}]  {num2}%");
				TVRemoteInputs inputs = Plugin.Inputs;
				list.Add(Bind(inputs.Power) + " power   " + Bind(inputs.Previous) + " / " + Bind(inputs.Next) + " video   " + Bind(inputs.VolumeDown) + " / " + Bind(inputs.VolumeUp) + " volume   " + Bind(inputs.Mute) + " mute");
				string text2 = string.Join("\n", list);
				Rect val = default(Rect);
				((Rect)(ref val))..ctor(0f, (float)Screen.height * 0.7f, (float)Screen.width, (float)num * 1.6f * (float)list.Count);
				GUI.Label(new Rect(((Rect)(ref val)).x + 2f, ((Rect)(ref val)).y + 2f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), text2, hudShadowStyle);
				GUI.Label(val, text2, hudStyle);
			}
		}
	}
	internal sealed class VideoEntry
	{
		public string Id;

		public string Name;

		public string Ext;

		public long Size;

		public double Duration;

		public string Path;

		public string Invalid;
	}
	internal static class VideoLibrary
	{
		public static readonly string[] Extensions = new string[12]
		{
			".mp4", ".m4v", ".mov", ".webm", ".wmv", ".avi", ".mpg", ".mpeg", ".ogv", ".asf",
			".dv", ".vp8"
		};

		private static readonly Dictionary<string, VideoEntry> known = new Dictionary<string, VideoEntry>(StringComparer.OrdinalIgnoreCase);

		public static List<VideoEntry> Ready { get; private set; } = new List<VideoEntry>();

		public static bool Scanning { get; private set; }

		public static string Folder
		{
			get
			{
				string text = Environment.ExpandEnvironmentVariables(Plugin.Cfg.VideoFolder.Value ?? "").Trim().Trim('"');
				if (!string.IsNullOrEmpty(text))
				{
					return text;
				}
				string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos);
				if (!string.IsNullOrEmpty(folderPath))
				{
					return Path.Combine(folderPath, "LethalTV");
				}
				return Path.Combine(Paths.BepInExRootPath, "LethalTV", "Videos");
			}
		}

		public static event Action Changed;

		public static bool IsSupportedExtension(string ext)
		{
			if (ext != null)
			{
				return Extensions.Contains(ext.ToLowerInvariant());
			}
			return false;
		}

		public static IEnumerator Scan()
		{
			if (Scanning)
			{
				yield break;
			}
			Scanning = true;
			try
			{
				string folder = Folder;
				string[] array;
				try
				{
					Directory.CreateDirectory(folder);
					array = (from f in Directory.GetFiles(folder)
						where IsSupportedExtension(Path.GetExtension(f))
						select f).OrderBy<string, string>((string f) => f, StringComparer.OrdinalIgnoreCase).ToArray();
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)("Couldn't read video folder '" + folder + "': " + ex.Message));
					array = Array.Empty<string>();
				}
				List<VideoEntry> result = new List<VideoEntry>();
				HashSet<string> seen = new HashSet<string>();
				string[] array2 = array;
				foreach (string fileName in array2)
				{
					FileInfo info = new FileInfo(fileName);
					string key = $"{info.FullName}|{info.Length}|{info.LastWriteTimeUtc.Ticks}";
					if (!known.TryGetValue(key, out var value))
					{
						Task<string> hashTask = Task.Run(() => HashFile(info.FullName));
						while (!hashTask.IsCompleted)
						{
							yield return null;
						}
						if (hashTask.IsFaulted)
						{
							Plugin.Log.LogWarning((object)("Couldn't read '" + info.Name + "': " + hashTask.Exception?.GetBaseException().Message));
							continue;
						}
						double duration = 0.0;
						yield return ProbeDuration(info.FullName, delegate(double d)
						{
							duration = d;
						});
						value = new VideoEntry
						{
							Id = hashTask.Result,
							Name = Path.GetFileNameWithoutExtension(info.Name),
							Ext = info.Extension.ToLowerInvariant(),
							Size = info.Length,
							Duration = duration,
							Path = info.FullName
						};
						known[key] = value;
						if (info.FullName.IndexOfAny(new char[2] { '#', '%' }) >= 0)
						{
							Plugin.Log.LogWarning((object)("'" + info.Name + "': '#' or '%' in a file or folder name can stop videos from playing. Rename it if it shows up blank."));
						}
						if (duration > 0.0)
						{
							Plugin.Log.LogInfo((object)$"Loaded '{info.Name}' ({duration:0.0}s, {(double)info.Length / 1048576.0:0.0} MB)");
						}
						else
						{
							Plugin.Log.LogWarning((object)("Skipping '" + info.Name + "': the game can't play it. Re-encode it as H.264/AAC .mp4."));
						}
					}
					if (value.Duration > 0.0 && seen.Add(value.Id))
					{
						result.Add(value);
					}
				}
				bool num2 = !result.Select((VideoEntry e) => e.Id).SequenceEqual(Ready.Select((VideoEntry e) => e.Id));
				Ready = result;
				Plugin.Log.LogInfo((object)$"{result.Count} video(s) ready in '{folder}'");
				if (num2)
				{
					VideoLibrary.Changed?.Invoke();
				}
			}
			finally
			{
				Scanning = false;
			}
		}

		private static IEnumerator ProbeDuration(string path, Action<double> done)
		{
			GameObject go = new GameObject("LethalTV Probe")
			{
				hideFlags = (HideFlags)61
			};
			Object.DontDestroyOnLoad((Object)(object)go);
			VideoPlayer player = go.AddComponent<VideoPlayer>();
			player.playOnAwake = false;
			player.renderMode = (VideoRenderMode)4;
			player.audioOutputMode = (VideoAudioOutputMode)0;
			player.source = (VideoSource)1;
			player.url = ToUrl(path);
			bool failed = false;
			player.errorReceived += (ErrorEventHandler)delegate(VideoPlayer _, string message)
			{
				failed = true;
				Plugin.Log.LogWarning((object)("Video error for '" + Path.GetFileName(path) + "': " + message));
			};
			player.Prepare();
			float waited = 0f;
			while (!player.isPrepared && !failed && waited < 20f)
			{
				waited += Time.unscaledDeltaTime;
				yield return null;
			}
			double num = 0.0;
			if (player.isPrepared)
			{
				num = player.length;
				if (num <= 0.0 && player.frameRate > 0f)
				{
					num = (float)player.frameCount / player.frameRate;
				}
			}
			Object.Destroy((Object)(object)go);
			done(num);
		}

		public static string ToUrl(string path)
		{
			return "file://" + path.Replace('\\', '/');
		}

		public static string HashFile(string path)
		{
			using SHA256 sHA = SHA256.Create();
			using FileStream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1048576);
			byte[] array = sHA.ComputeHash(inputStream);
			StringBuilder stringBuilder = new StringBuilder(array.Length * 2);
			byte[] array2 = array;
			foreach (byte b in array2)
			{
				stringBuilder.Append(b.ToString("x2"));
			}
			return stringBuilder.ToString();
		}
	}
	internal static class VideoCache
	{
		public static string Dir => Path.Combine(Paths.BepInExRootPath, "LethalTV", "Cache");

		public static string CompletePath(VideoEntry e)
		{
			return Path.Combine(Dir, e.Id + e.Ext);
		}

		public static string PartPath(VideoEntry e)
		{
			return Path.Combine(Dir, e.Id + ".part");
		}

		public static bool IsValidId(string id)
		{
			if (id == null || id.Length != 64)
			{
				return false;
			}
			foreach (char c in id)
			{
				if ((c < '0' || c > '9') && (c < 'a' || c > 'f'))
				{
					return false;
				}
			}
			return true;
		}

		public static string FindComplete(VideoEntry e)
		{
			try
			{
				FileInfo fileInfo = new FileInfo(CompletePath(e));
				if (!fileInfo.Exists || fileInfo.Length != e.Size)
				{
					return null;
				}
				fileInfo.LastWriteTimeUtc = DateTime.UtcNow;
				return fileInfo.FullName;
			}
			catch (Exception)
			{
				return null;
			}
		}

		public static void Prune(IEnumerable<string> keepIds, long maxBytes)
		{
			try
			{
				DirectoryInfo directoryInfo = new DirectoryInfo(Dir);
				if (!directoryInfo.Exists)
				{
					return;
				}
				HashSet<string> hashSet = new HashSet<string>(keepIds);
				List<FileInfo> list = (from f in directoryInfo.GetFiles()
					orderby f.LastWriteTimeUtc
					select f).ToList();
				long num = list.Sum((FileInfo f) => f.Length);
				foreach (FileInfo item in list)
				{
					if (num > maxBytes)
					{
						if (!hashSet.Contains(Path.GetFileNameWithoutExtension(item.Name)))
						{
							long length = item.Length;
							item.Delete();
							num -= length;
							Plugin.Log.LogInfo((object)("Pruned cached video " + item.Name));
						}
						continue;
					}
					break;
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Cache prune failed: " + ex.Message));
			}
		}
	}
}