Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of YoutubeOnTV v0.3.0
BepInEx/plugins/YoutubeOnTV/YoutubeOnTV.dll
Decompiled 5 days agousing 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.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using LethalNetworkAPI; using LethalNetworkAPI.Utils; using Microsoft.CodeAnalysis; using Unity.Netcode; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Video; using YoutubeDLSharp; using YoutubeDLSharp.Options; using YoutubeOnTV.Patches; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("YoutubeOnTV")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("YoutubeOnTV")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("637d9e92-bc74-4646-8436-67954c0a9f24")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [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 YoutubeOnTV { public class NetworkHandler : MonoBehaviour { private const string Prefix = "YoutubeOnTV2_"; private LNetworkMessage<string> addVideoMessage; private LNetworkEvent skipVideoEvent; private LNetworkEvent clearQueueEvent; private LNetworkMessage<string> removeFromQueueMessage; private LNetworkMessage<string> loadVideoMessage; private LNetworkMessage<VideoPlayData> playVideoMessage; private LNetworkMessage<float> syncPlaybackMessage; private LNetworkEvent playFallbackEvent; private LNetworkEvent requestTVStateEvent; private LNetworkMessage<TVStateData> syncTVStateMessage; public static NetworkHandler Instance { get; private set; } private static bool IsHost => LNetworkUtils.IsHostOrServer; private static ulong[] OtherClients => LNetworkUtils.AllConnectedClients.Where((ulong id) => id != NetworkManager.Singleton.LocalClientId).ToArray(); private void Awake() { if ((Object)(object)Instance != (Object)null) { Object.Destroy((Object)(object)this); return; } Instance = this; addVideoMessage = LNetworkMessage<string>.Connect("YoutubeOnTV2_AddVideo", (Action<string, ulong>)delegate(string entry, ulong _) { HostAddVideo(entry); }, (Action<string>)OnClientReceivedAddVideo, (Action<string, ulong>)null); skipVideoEvent = LNetworkEvent.Connect("YoutubeOnTV2_Skip", (Action<ulong>)delegate { HostSkip(); }, (Action)OnClientReceivedSkip, (Action<ulong>)null); clearQueueEvent = LNetworkEvent.Connect("YoutubeOnTV2_Clear", (Action<ulong>)delegate { HostClear(); }, (Action)OnClientReceivedClear, (Action<ulong>)null); removeFromQueueMessage = LNetworkMessage<string>.Connect("YoutubeOnTV2_RemoveFromQueue", (Action<string, ulong>)null, (Action<string>)OnClientReceivedRemoveFromQueue, (Action<string, ulong>)null); loadVideoMessage = LNetworkMessage<string>.Connect("YoutubeOnTV2_LoadVideo", (Action<string, ulong>)null, (Action<string>)delegate(string entry) { VideoManager.Instance?.OnHostLoadingVideo(entry); }, (Action<string, ulong>)null); playVideoMessage = LNetworkMessage<VideoPlayData>.Connect("YoutubeOnTV2_PlayVideo", (Action<VideoPlayData, ulong>)null, (Action<VideoPlayData>)delegate(VideoPlayData data) { VideoManager.Instance?.OnHostPlayingVideo(data.input, data.videoUrl, data.startTime); }, (Action<VideoPlayData, ulong>)null); syncPlaybackMessage = LNetworkMessage<float>.Connect("YoutubeOnTV2_SyncPlayback", (Action<float, ulong>)null, (Action<float>)delegate(float time) { VideoManager.Instance?.OnHostPlaybackTime(time); }, (Action<float, ulong>)null); playFallbackEvent = LNetworkEvent.Connect("YoutubeOnTV2_PlayFallback", (Action<ulong>)null, (Action)delegate { VideoManager.Instance?.OnHostPlayingFallback(); }, (Action<ulong>)null); requestTVStateEvent = LNetworkEvent.Connect("YoutubeOnTV2_RequestTVState", (Action<ulong>)OnServerReceivedRequestTVState, (Action)null, (Action<ulong>)null); syncTVStateMessage = LNetworkMessage<TVStateData>.Connect("YoutubeOnTV2_SyncTVState", (Action<TVStateData, ulong>)null, (Action<TVStateData>)delegate(TVStateData state) { VideoManager.Instance?.ApplyTVStateFromNetwork(state); }, (Action<TVStateData, ulong>)null); YoutubeOnTVBase.Log.LogInfo((object)"Network messages registered"); } public void RequestAddVideo(string entry) { if (IsHost) { HostAddVideo(entry); } else { addVideoMessage.SendServer(entry); } } public void RequestSkipVideo() { if (IsHost) { HostSkip(); } else { skipVideoEvent.InvokeServer(); } } public void RequestClearQueue() { if (IsHost) { HostClear(); } else { clearQueueEvent.InvokeServer(); } } public void RequestTVState() { if (!IsHost) { YoutubeOnTVBase.Log.LogInfo((object)"Requesting TV state from host"); requestTVStateEvent.InvokeServer(); } } private void HostAddVideo(string entry) { entry = VideoInput.Normalize(entry); if (entry != null) { YoutubeOnTVBase.Log.LogInfo((object)("[Host] Adding to queue: " + entry)); VideoQueue.Add(entry); addVideoMessage.SendClients(entry, OtherClients); } } private void HostSkip() { YoutubeOnTVBase.Log.LogInfo((object)"[Host] Skip"); VideoManager.Instance?.OnSkipRequested(); skipVideoEvent.InvokeClients(OtherClients); } private void HostClear() { YoutubeOnTVBase.Log.LogInfo((object)"[Host] Clear queue"); VideoManager.Instance?.OnClearRequested(); clearQueueEvent.InvokeClients(OtherClients); } private void OnServerReceivedRequestTVState(ulong clientId) { if (!((Object)(object)VideoManager.Instance == (Object)null)) { YoutubeOnTVBase.Log.LogInfo((object)$"[Host] Sending TV state to client {clientId}"); syncTVStateMessage.SendClient(VideoManager.Instance.GetCurrentTVState(), clientId); } } public void BroadcastRemoveFromQueue(string entry) { if (IsHost) { removeFromQueueMessage.SendClients(entry, OtherClients); } } public void BroadcastLoadVideo(string entry) { if (IsHost) { loadVideoMessage.SendClients(entry, OtherClients); } } public void BroadcastPlayVideo(string entry, string videoUrl, float startTime) { if (IsHost) { YoutubeOnTVBase.Log.LogInfo((object)$"Broadcasting play: {videoUrl} ({entry}) at {startTime:0.0}s"); playVideoMessage.SendClients(new VideoPlayData { input = entry, videoUrl = videoUrl, startTime = startTime }, OtherClients); } } public void BroadcastPlaybackTime(float time) { if (IsHost) { syncPlaybackMessage.SendClients(time, OtherClients); } } public void BroadcastPlayFallback() { if (IsHost) { playFallbackEvent.InvokeClients(OtherClients); } } private void OnClientReceivedAddVideo(string entry) { YoutubeOnTVBase.Log.LogInfo((object)("[Client] Adding to queue: " + entry)); VideoQueue.Add(entry); } private void OnClientReceivedRemoveFromQueue(string entry) { VideoQueue.RemoveFirstMatch(entry); } private void OnClientReceivedSkip() { VideoManager.Instance?.OnSkipRequested(); } private void OnClientReceivedClear() { VideoManager.Instance?.OnClearRequested(); } } [Serializable] public struct VideoPlayData { public string input; public string videoUrl; public float startTime; } [Serializable] public struct TVStateData { public bool isTVOn; public int mode; public string input; public string videoUrl; public float playbackTime; public string[] queue; } [BepInPlugin("com.roandegraaf.youtubeontv", "YoutubeOnTV", "0.3.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class YoutubeOnTVBase : BaseUnityPlugin { public const string Guid = "com.roandegraaf.youtubeontv"; private readonly Harmony harmony = new Harmony("com.roandegraaf.youtubeontv"); public static YoutubeOnTVBase Instance; internal static ManualLogSource Log; public static ConfigEntry<int> MaxVideoMinutes; public static ConfigEntry<int> MaxCacheMegabytes; public static ConfigEntry<bool> PrefetchNextVideo; private void Awake() { Instance = this; Log = ((BaseUnityPlugin)this).Logger; MaxVideoMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("Videos", "MaxVideoMinutes", 60, "Longest video that can be queued, in minutes. Videos are downloaded in full before they play, so very long ones take a while and use disk space."); MaxCacheMegabytes = ((BaseUnityPlugin)this).Config.Bind<int>("Videos", "MaxCacheMegabytes", 1024, "Size limit of the downloaded-video cache in the plugin folder. The least recently played videos are removed first."); PrefetchNextVideo = ((BaseUnityPlugin)this).Config.Bind<bool>("Videos", "PrefetchNextVideo", true, "Download the next queued video while the current one plays, so it starts without waiting."); string location = Assembly.GetExecutingAssembly().Location; Log.LogInfo((object)$"YoutubeOnTV {((BaseUnityPlugin)this).Info.Metadata.Version} loaded (build {File.GetLastWriteTimeUtc(location):yyyy-MM-dd HH:mm:ss} UTC)"); harmony.PatchAll(typeof(TVScriptPatch)); harmony.PatchAll(typeof(TerminalPatch)); harmony.PatchAll(typeof(SessionPatch)); SceneManager.sceneLoaded += OnSceneLoaded; } private void Start() { if (Chainloader.PluginInfos.ContainsKey("rattenbonkers.TVLoader")) { Log.LogWarning((object)"TVLoader is installed. Both mods take over the ship TV, so disable one of them; with TVLoader videos present they will fight over the screen."); } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)VideoManager.Instance != (Object)null)) { GameObject val = new GameObject("YoutubeOnTVManagers"); Object.DontDestroyOnLoad((Object)val); val.AddComponent<VideoManager>(); val.AddComponent<NetworkHandler>(); Log.LogInfo((object)"Managers created"); } } internal static TerminalNode HandleTerminalCommand(string typed) { string text = (typed ?? "").Trim(); string text2 = text.ToLowerInvariant(); string argument = ""; string command; if (text2 == "tv") { command = ""; } else if (text2.StartsWith("tv ")) { string text3 = text.Substring(3).Trim(); int num = text3.IndexOf(' '); command = ((num < 0) ? text3 : text3.Substring(0, num)).ToLowerInvariant(); argument = ((num < 0) ? "" : text3.Substring(num + 1).Trim()); } else { switch (text2) { case "skip tv": case "clear tv": case "queue tv": break; default: return null; } command = text2.Substring(0, text2.Length - 3); } Log.LogInfo((object)("Terminal command: " + text)); return Page(RunCommand(command, argument)); } private static string RunCommand(string command, string argument) { NetworkHandler instance = NetworkHandler.Instance; switch (command) { case "add": { string text = VideoInput.Normalize(argument); if (text == null) { return "Usage: tv add [url, video id or search]\n\n tv add dQw4w9WgXcQ\n tv add https://youtube.com/watch?v=...\n tv add never gonna give you up\n"; } instance?.RequestAddVideo(text); return "Added to queue: " + VideoInput.Describe(text) + "\n\nVideos download before they play, so give it a few seconds.\n"; } case "skip": instance?.RequestSkipVideo(); return "Skipped the current video.\n"; case "clear": { int num = VideoQueue.Count(); instance?.RequestClearQueue(); return string.Format("Cleared the queue ({0} video{1}) and stopped the current one.\n", num, (num == 1) ? "" : "s"); } case "queue": return QueueText(); default: return "TV CONTROLS\n===========\n\n tv add [url/id/search] Add a video to the queue\n tv queue Show what is playing and queued\n tv skip Skip the current video\n tv clear Empty the queue\n"; } } private static string QueueText() { StringBuilder stringBuilder = new StringBuilder(); VideoManager instance = VideoManager.Instance; if (instance != null && instance.CurrentInput != null) { string text = (instance.IsLoadingVideo ? " (downloading)" : ""); stringBuilder.Append("Now playing" + text + ": " + VideoInput.Describe(instance.CurrentVideoUrl ?? instance.CurrentInput) + "\n\n"); } else { stringBuilder.Append("Nothing from the queue is playing.\n\n"); } if (VideoQueue.IsEmpty()) { stringBuilder.Append("Queue is empty.\n"); } else { stringBuilder.Append($"Videos in queue: {VideoQueue.Count()}\n"); int num = 1; foreach (string item in VideoQueue.Entries.Take(10)) { stringBuilder.Append($"{num++}. {VideoInput.Describe(item)}\n"); } if (VideoQueue.Count() > 10) { stringBuilder.Append($"...and {VideoQueue.Count() - 10} more\n"); } } return stringBuilder.ToString(); } private static TerminalNode Page(string text) { TerminalNode obj = ScriptableObject.CreateInstance<TerminalNode>(); obj.displayText = "\n" + text + "\n"; obj.clearPreviousText = true; obj.maxCharactersToType = 500; return obj; } } public class TVController : MonoBehaviour { public VideoPlayer videoPlayer; private VideoPlayer vanillaVideoPlayer; private RenderTexture renderTexture; private AudioSource tvAudioSource; private TVScript tvScript; private ManualLogSource logger; private bool isPreparing; private double pendingSeek = -1.0; private float prepareStartedAt; private int prepareAttempts; private const float PREPARE_TIMEOUT = 6f; private const int MAX_PREPARE_ATTEMPTS = 3; public static TVController Instance { get; private set; } public string LoadedPath { get; private set; } public AudioSource AudioSource => tvAudioSource; public double Time => videoPlayer.time; private void Awake() { //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Expected O, but got Unknown //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Expected O, but got Unknown //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Expected O, but got Unknown //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0194: Expected O, but got Unknown Instance = this; logger = YoutubeOnTVBase.Log; tvScript = ((Component)this).gameObject.GetComponent<TVScript>(); if ((Object)(object)tvScript == (Object)null) { logger.LogError((object)"TVScript not found on this GameObject!"); return; } tvAudioSource = tvScript.tvSFX; vanillaVideoPlayer = tvScript.video; if ((Object)(object)vanillaVideoPlayer != (Object)null) { renderTexture = vanillaVideoPlayer.targetTexture; vanillaVideoPlayer.Stop(); ((Behaviour)vanillaVideoPlayer).enabled = false; } else { logger.LogError((object)"Vanilla VideoPlayer not found!"); } videoPlayer = ((Component)this).gameObject.AddComponent<VideoPlayer>(); videoPlayer.playOnAwake = false; videoPlayer.isLooping = false; videoPlayer.source = (VideoSource)1; videoPlayer.skipOnDrop = true; videoPlayer.controlledAudioTrackCount = 1; videoPlayer.audioOutputMode = (VideoAudioOutputMode)1; videoPlayer.SetTargetAudioSource((ushort)0, tvAudioSource); videoPlayer.targetTexture = renderTexture; tvScript.video = videoPlayer; videoPlayer.prepareCompleted += new EventHandler(OnVideoPrepared); videoPlayer.loopPointReached += new EventHandler(OnVideoEnd); videoPlayer.errorReceived += new ErrorEventHandler(OnVideoError); videoPlayer.seekCompleted += (EventHandler)delegate(VideoPlayer vp) { logger.LogInfo((object)$"Seek completed at {vp.time:0.0}s"); }; ManualLogSource obj = logger; RenderTexture obj2 = renderTexture; object arg = ((obj2 != null) ? new int?(((Texture)obj2).width) : ((int?)null)); RenderTexture obj3 = renderTexture; obj.LogInfo((object)$"TVController attached (screen {arg}x{((obj3 != null) ? new int?(((Texture)obj3).height) : ((int?)null))})"); } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } public void PlayFile(string filePath, double startAt = 0.0) { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { logger.LogError((object)("Cannot play video: file not found at " + filePath)); OnVideoError(videoPlayer, "file not found: " + filePath); return; } logger.LogInfo((object)("Playing " + Path.GetFileName(filePath) + ((startAt > 0.0) ? $" from {startAt:0.0}s" : ""))); videoPlayer.Stop(); LoadedPath = filePath; pendingSeek = ((startAt > 0.0) ? startAt : (-1.0)); videoPlayer.url = "file://" + filePath; prepareAttempts = 0; StartPrepare(); } private void StartPrepare() { isPreparing = true; prepareAttempts++; prepareStartedAt = Time.realtimeSinceStartup; videoPlayer.Prepare(); } private void Update() { if (isPreparing && !(Time.realtimeSinceStartup - prepareStartedAt < 6f)) { if (prepareAttempts < 3) { logger.LogWarning((object)$"Video did not finish preparing in {6f}s, retrying ({prepareAttempts + 1}/{3})"); string url = videoPlayer.url; videoPlayer.Stop(); videoPlayer.url = url; StartPrepare(); } else { OnVideoError(videoPlayer, $"the video did not finish preparing after {3} attempts"); } } } private void OnVideoPrepared(VideoPlayer vp) { if (isPreparing) { isPreparing = false; if (vp.audioTrackCount == 0) { logger.LogWarning((object)"Video has no audio track"); } logger.LogInfo((object)$"Prepared: length={vp.length:0.0}s frames={vp.frameCount} canSetTime={vp.canSetTime} audioTracks={vp.audioTrackCount}"); if (pendingSeek > 0.0 && pendingSeek < vp.length - 1.0) { vp.time = pendingSeek; } pendingSeek = -1.0; vp.Play(); } } public void Seek(double time) { logger.LogInfo((object)$"Seeking from {videoPlayer.time:0.0}s to {time:0.0}s"); if (isPreparing) { pendingSeek = time; } else { videoPlayer.time = time; } } public void Stop() { isPreparing = false; pendingSeek = -1.0; LoadedPath = null; videoPlayer.Stop(); } public void Pause() { if (videoPlayer.isPlaying) { videoPlayer.Pause(); } } public void Resume() { if (IsPaused()) { videoPlayer.Play(); } } public bool IsPaused() { if (LoadedPath != null && !isPreparing && !videoPlayer.isPlaying) { return videoPlayer.isPrepared; } return false; } public bool IsPlaying() { return videoPlayer.isPlaying; } public bool IsBusy() { if (!isPreparing && !videoPlayer.isPlaying) { return IsPaused(); } return true; } public TVScript GetTVScript() { return tvScript; } private void OnVideoEnd(VideoPlayer vp) { logger.LogInfo((object)"Video playback completed"); LoadedPath = null; VideoManager.Instance?.OnVideoFinished(); } private void OnVideoError(VideoPlayer vp, string message) { isPreparing = false; LoadedPath = null; logger.LogError((object)("Video playback error: " + message)); try { VideoManager.Instance?.OnVideoPlaybackError(message); } catch (Exception arg) { logger.LogError((object)$"Error handling playback failure: {arg}"); } } } public class DownloadResult { public bool Success; public string VideoId; public string FilePath; public string Error; public string CanonicalUrl { get { if (VideoId != null) { return VideoInput.WatchUrl(VideoId); } return null; } } } public class VideoDownloader : MonoBehaviour { private enum ToolState { Pending, Ready, Failed } private class Request { public string Key; public readonly List<Action<DownloadResult>> Callbacks = new List<Action<DownloadResult>>(); public readonly CancellationTokenSource Cancellation = new CancellationTokenSource(); public volatile string VideoIdSeen; } private static VideoDownloader _instance; private const string FormatSelector = "bv*[vcodec^=avc1][height<=480]+ba[acodec^=mp4a]/18/bv*[vcodec^=avc1][height<=720]+ba[acodec^=mp4a]/b[vcodec^=avc1][acodec^=mp4a]/b[ext=mp4]"; private static readonly TimeSpan YtDlpUpdateInterval = TimeSpan.FromHours(24.0); private static readonly TimeSpan DownloadTimeout = TimeSpan.FromMinutes(15.0); private ManualLogSource _logger; private string _toolsDir; private string _cacheDir; private string _ytDlpPath; private string _ffmpegPath; private ToolState _toolState; private string _toolError; private readonly Dictionary<string, Request> _inFlight = new Dictionary<string, Request>(); private readonly Dictionary<string, DownloadResult> _completed = new Dictionary<string, DownloadResult>(); public static VideoDownloader Instance { get { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("YoutubeOnTV.VideoDownloader"); Object.DontDestroyOnLoad((Object)val); _instance = val.AddComponent<VideoDownloader>(); } return _instance; } } public string CacheDirectory => _cacheDir; private void Awake() { _logger = YoutubeOnTVBase.Log; _toolsDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); _cacheDir = Path.Combine(_toolsDir, "cache"); _ytDlpPath = Path.Combine(_toolsDir, "yt-dlp.exe"); _ffmpegPath = Path.Combine(_toolsDir, "ffmpeg.exe"); Directory.CreateDirectory(_cacheDir); CleanCache(removeLeftovers: true); ((MonoBehaviour)this).StartCoroutine(EnsureTools()); } private IEnumerator EnsureTools() { _toolState = ToolState.Pending; bool ytDlpExists = File.Exists(_ytDlpPath); bool flag = ytDlpExists && DateTime.UtcNow - File.GetLastWriteTimeUtc(_ytDlpPath) > YtDlpUpdateInterval; if (!ytDlpExists || flag) { _logger.LogInfo((object)(ytDlpExists ? "Updating yt-dlp.exe..." : "Downloading yt-dlp.exe...")); Task task = Utils.DownloadYtDlp(_toolsDir); while (!task.IsCompleted) { yield return null; } if (task.IsFaulted) { string text = task.Exception?.GetBaseException().Message; if (!ytDlpExists) { Fail("Could not download yt-dlp.exe: " + text); yield break; } _logger.LogWarning((object)("Could not update yt-dlp.exe, keeping the existing one: " + text)); } else { _logger.LogInfo((object)"yt-dlp.exe is up to date."); } } if (!File.Exists(_ffmpegPath)) { _logger.LogInfo((object)"Downloading ffmpeg.exe (one-time, about 55 MB)..."); Task task = Utils.DownloadFFmpeg(_toolsDir); while (!task.IsCompleted) { yield return null; } if (task.IsFaulted || !File.Exists(_ffmpegPath)) { Fail("Could not download ffmpeg.exe: " + (task.Exception?.GetBaseException().Message ?? "file missing after download")); yield break; } _logger.LogInfo((object)"ffmpeg.exe downloaded."); } _toolState = ToolState.Ready; _logger.LogInfo((object)"Video download tools are ready."); } private void Fail(string error) { _toolError = error; _toolState = ToolState.Failed; _logger.LogError((object)error); } public void Get(string input, Action<DownloadResult> onDone) { if (TryGetCompleted(input, out var result)) { onDone?.Invoke(result); return; } if (_inFlight.TryGetValue(input, out var value)) { if (onDone != null) { value.Callbacks.Add(onDone); } return; } Request request = new Request { Key = input }; if (onDone != null) { request.Callbacks.Add(onDone); } _inFlight[input] = request; ((MonoBehaviour)this).StartCoroutine(Guarded(request)); } public void Prefetch(string input) { Get(input, null); } public bool IsReadyOrInFlight(string input) { DownloadResult result; if (!_inFlight.ContainsKey(input)) { return TryGetCompleted(input, out result); } return true; } public void Cancel(string input) { if (input != null && _inFlight.TryGetValue(input, out var value)) { _logger.LogInfo((object)("Cancelling download: " + input)); value.Cancellation.Cancel(); } } public void CancelAll() { foreach (Request item in _inFlight.Values.Distinct().ToList()) { item.Cancellation.Cancel(); } } private IEnumerator Guarded(Request request) { IEnumerator run = Run(request); while (true) { object current; try { if (!run.MoveNext()) { break; } current = run.Current; } catch (Exception ex) { _logger.LogError((object)$"Download of {request.Key} failed unexpectedly: {ex}"); if (_inFlight.ContainsValue(request)) { Finish(request, new DownloadResult { Error = "Unexpected error: " + ex.Message }); } break; } yield return current; } } private IEnumerator Run(Request request) { if (_toolState == ToolState.Failed) { yield return EnsureTools(); } while (_toolState == ToolState.Pending) { yield return null; } if (_toolState == ToolState.Failed) { Finish(request, new DownloadResult { Error = _toolError }); yield break; } _logger.LogInfo((object)("Downloading: " + request.Key)); float startedAt = Time.realtimeSinceStartup; List<string> stdout = new List<string>(); StringBuilder stderr = new StringBuilder(); YoutubeDLProcess val = new YoutubeDLProcess(_ytDlpPath); val.OutputReceived += delegate(object o, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { lock (stdout) { stdout.Add(e.Data.Trim()); } if (request.VideoIdSeen == null && VideoInput.ExtractVideoId(e.Data.Trim()) == e.Data.Trim()) { request.VideoIdSeen = e.Data.Trim(); } } }; val.ErrorReceived += delegate(object o, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { lock (stderr) { stderr.AppendLine(e.Data); } } }; OptionSet val2 = new OptionSet(); val2.Format = "bv*[vcodec^=avc1][height<=480]+ba[acodec^=mp4a]/18/bv*[vcodec^=avc1][height<=720]+ba[acodec^=mp4a]/b[vcodec^=avc1][acodec^=mp4a]/b[ext=mp4]"; val2.MergeOutputFormat = (DownloadMergeFormat)1; val2.FfmpegLocation = _ffmpegPath; val2.Output = Path.Combine(_cacheDir, "%(id)s.%(ext)s"); val2.NoPlaylist = true; val2.NoSimulate = true; val2.NoProgress = true; val2.NoMtime = true; val2.Print = new MultiValue<string>(new string[2] { "id", "after_move:filepath" }); val2.MatchFilters = MultiValue<string>.op_Implicit($"!is_live & duration <=? {YoutubeOnTVBase.MaxVideoMinutes.Value * 60}"); OptionSet val3 = val2; request.Cancellation.CancelAfter(DownloadTimeout); Task<int> task; try { task = val.RunAsync(new string[1] { request.Key }, val3, request.Cancellation.Token, (IProgress<DownloadProgress>)null); } catch (Exception ex) { Finish(request, new DownloadResult { Error = "Could not start yt-dlp: " + ex.Message }); yield break; } bool aliased = false; while (!task.IsCompleted) { if (!aliased && request.VideoIdSeen != null) { aliased = true; string key = VideoInput.WatchUrl(request.VideoIdSeen); if (!_inFlight.ContainsKey(key)) { _inFlight[key] = request; } } yield return null; } string text; lock (stderr) { text = stderr.ToString().Trim(); } List<string> source; lock (stdout) { source = new List<string>(stdout); } if (task.IsCanceled || request.Cancellation.IsCancellationRequested) { double num = Time.realtimeSinceStartup - startedAt; TimeSpan downloadTimeout = DownloadTimeout; bool flag = num >= downloadTimeout.TotalSeconds - 1.0; Finish(request, new DownloadResult { Error = (flag ? "Download timed out" : "Download cancelled") }); yield break; } if (task.IsFaulted) { Finish(request, new DownloadResult { Error = "yt-dlp failed to run: " + task.Exception?.GetBaseException().Message }); yield break; } string text2 = source.FirstOrDefault((string l) => VideoInput.ExtractVideoId(l) == l); string text3 = source.LastOrDefault((string l) => l.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase)); if (task.Result == 0 && text3 != null && File.Exists(text3)) { Touch(text3); if (!string.IsNullOrEmpty(text)) { _logger.LogInfo((object)("yt-dlp warnings: " + text)); } _logger.LogInfo((object)$"Downloaded {text2} in {Time.realtimeSinceStartup - startedAt:0.0}s: {text3}"); DownloadResult downloadResult = new DownloadResult { Success = true, VideoId = text2, FilePath = text3 }; _completed[request.Key] = downloadResult; if (text2 != null) { _completed[downloadResult.CanonicalUrl] = downloadResult; } Finish(request, downloadResult); CleanCache(removeLeftovers: false); } else { string text4 = ((task.Result != 0) ? $"yt-dlp exited with code {task.Result}" : "No playable file (live stream, longer than the configured limit, or no H.264/AAC format)"); _logger.LogError((object)(text4 + ": " + request.Key)); if (!string.IsNullOrEmpty(text)) { _logger.LogError((object)("yt-dlp output: " + text)); } Finish(request, new DownloadResult { VideoId = text2, Error = text4 }); } } private void Finish(Request request, DownloadResult result) { foreach (string item in (from kv in _inFlight where kv.Value == request select kv.Key).ToList()) { _inFlight.Remove(item); } if (!result.Success) { _logger.LogWarning((object)("Download failed for " + request.Key + ": " + result.Error)); } foreach (Action<DownloadResult> callback in request.Callbacks) { try { callback(result); } catch (Exception arg) { _logger.LogError((object)$"Download callback threw: {arg}"); } } } private bool TryGetCompleted(string input, out DownloadResult result) { if (_completed.TryGetValue(input, out result) && File.Exists(result.FilePath)) { return true; } string text = (input.StartsWith("https://www.youtube.com/watch?v=") ? VideoInput.ExtractVideoId(input) : null); if (text != null) { string text2 = Path.Combine(_cacheDir, text + ".mp4"); if (File.Exists(text2)) { Touch(text2); result = new DownloadResult { Success = true, VideoId = text, FilePath = text2 }; _completed[input] = result; return true; } } result = null; return false; } private static void Touch(string path) { try { File.SetLastWriteTimeUtc(path, DateTime.UtcNow); } catch (IOException) { } catch (UnauthorizedAccessException) { } } private void CleanCache(bool removeLeftovers) { try { FileInfo[] files = new DirectoryInfo(_cacheDir).GetFiles(); if (removeLeftovers) { foreach (FileInfo item in files.Where((FileInfo fileInfo) => !fileInfo.Name.EndsWith(".mp4") || fileInfo.Name.Count((char c) => c == '.') > 1)) { TryDelete(item); } files = new DirectoryInfo(_cacheDir).GetFiles("*.mp4"); } long num = (long)YoutubeOnTVBase.MaxCacheMegabytes.Value * 1024L * 1024; long num2 = files.Sum((FileInfo fileInfo) => fileInfo.Length); TVController instance = TVController.Instance; object obj; if (instance == null) { obj = null; } else { VideoPlayer videoPlayer = instance.videoPlayer; obj = ((videoPlayer != null) ? videoPlayer.url : null); } if (obj == null) { obj = ""; } string text = (string)obj; foreach (FileInfo f in files.OrderBy((FileInfo fileInfo) => fileInfo.LastWriteTimeUtc)) { if (num2 <= num) { break; } if (text.EndsWith(f.Name) || _completed.Values.Any((DownloadResult r) => r.FilePath == f.FullName && IsInUse(r))) { continue; } num2 -= f.Length; TryDelete(f); foreach (string item2 in (from kv in _completed where kv.Value.FilePath == f.FullName select kv.Key).ToList()) { _completed.Remove(item2); } } } catch (Exception ex) { _logger.LogWarning((object)("Cache cleanup failed: " + ex.Message)); } } private static bool IsInUse(DownloadResult result) { return VideoQueue.Entries.Any((string e) => e == result.CanonicalUrl); } private void TryDelete(FileInfo file) { try { file.Delete(); } catch (Exception ex) { _logger.LogWarning((object)("Could not delete " + file.Name + ": " + ex.Message)); } } } public static class VideoInput { public const string WatchUrlPrefix = "https://www.youtube.com/watch?v="; public const string SearchPrefix = "ytsearch:"; private static readonly Regex VideoIdPattern = new Regex("^[a-zA-Z0-9_-]{11}$"); private static readonly Regex[] UrlPatterns = new Regex[3] { new Regex("youtube\\.com/.*[?&]v=([a-zA-Z0-9_-]{11})(?![a-zA-Z0-9_-])"), new Regex("youtu\\.be/([a-zA-Z0-9_-]{11})(?![a-zA-Z0-9_-])"), new Regex("youtube\\.com/(?:embed|v|shorts|live)/([a-zA-Z0-9_-]{11})(?![a-zA-Z0-9_-])") }; public static string Normalize(string input) { input = (input ?? "").Replace("\"", "").Trim(); if (input.Length == 0) { return null; } string text = ExtractVideoId(input); if (text != null) { return "https://www.youtube.com/watch?v=" + text; } if (input.StartsWith("http://") || input.StartsWith("https://") || input.StartsWith("ytsearch:")) { return input; } return "ytsearch:" + input; } public static string ExtractVideoId(string input) { if (string.IsNullOrEmpty(input)) { return null; } if (VideoIdPattern.IsMatch(input) && !IsLowercaseWord(input)) { return input; } Regex[] urlPatterns = UrlPatterns; for (int i = 0; i < urlPatterns.Length; i++) { Match match = urlPatterns[i].Match(input); if (match.Success) { return match.Groups[1].Value; } } return null; } private static bool IsLowercaseWord(string s) { foreach (char c in s) { if (c < 'a' || c > 'z') { return false; } } return true; } public static string WatchUrl(string videoId) { return "https://www.youtube.com/watch?v=" + videoId; } public static string Describe(string entry) { if (string.IsNullOrEmpty(entry)) { return ""; } if (entry.StartsWith("ytsearch:")) { return "search: " + entry.Substring("ytsearch:".Length); } if (entry.StartsWith("https://www.youtube.com/watch?v=")) { return "youtu.be/" + entry.Substring("https://www.youtube.com/watch?v=".Length); } return entry; } } public class VideoManager : MonoBehaviour { private enum Mode { Idle, Fallback, Video } private ManualLogSource logger; private Mode mode; private bool fallbackBroken; private int currentRetries; private float retryAtTime; private float lastSyncTime; private float hostTime; private float hostTimeReceivedAt; private string failedVideoUrl; private string loadedVideoUrl; private bool? pendingTvOn; private const int MAX_ATTEMPTS = 2; private const float RETRY_DELAY = 3f; private const float SYNC_INTERVAL = 2f; private const float SYNC_TOLERANCE = 1.5f; public static VideoManager Instance { get; private set; } public string CurrentInput { get; private set; } public string CurrentVideoUrl { get; private set; } public bool IsLoadingVideo { get; private set; } public bool IsPlayingFallback => mode == Mode.Fallback; private static bool IsHost => LNetworkUtils.IsHostOrServer; private static string FallbackVideoPath => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "fallback.mp4"); public float HostPositionEstimate => EstimatedHostTime(); private void Awake() { if ((Object)(object)Instance != (Object)null) { Object.Destroy((Object)(object)this); return; } Instance = this; logger = YoutubeOnTVBase.Log; _ = VideoDownloader.Instance; } public void ResetSession() { logger.LogInfo((object)"Resetting TV state for a new session"); VideoDownloader.Instance.CancelAll(); VideoQueue.Clear(); TVController.Instance?.Stop(); mode = Mode.Idle; fallbackBroken = false; IsLoadingVideo = false; CurrentInput = null; CurrentVideoUrl = null; currentRetries = 0; retryAtTime = 0f; failedVideoUrl = null; loadedVideoUrl = null; pendingTvOn = null; } private void Update() { TVController instance = TVController.Instance; if ((Object)(object)instance == (Object)null) { return; } if (pendingTvOn.HasValue) { SetTVPower(pendingTvOn.Value); pendingTvOn = null; } if (!IsTVOn()) { instance.Pause(); return; } if (IsHost) { AdvanceHostPlayback(instance); SyncPlaybackToClients(instance); } else { FollowHost(instance); } PrefetchNext(); } private void AdvanceHostPlayback(TVController tv) { if (IsLoadingVideo) { return; } if (mode == Mode.Fallback && !VideoQueue.IsEmpty()) { logger.LogInfo((object)"Switching from fallback to queue video"); tv.Stop(); mode = Mode.Idle; } if (tv.IsBusy()) { return; } if (CurrentInput != null) { if (retryAtTime > 0f) { if (Time.time >= retryAtTime) { retryAtTime = 0f; LoadCurrentVideo(); } return; } ClearCurrentVideo(); } if (!VideoQueue.IsEmpty()) { PlayNextFromQueue(); } else if (mode != Mode.Fallback && !fallbackBroken) { PlayFallback(broadcast: true); } } private void PlayNextFromQueue() { CurrentInput = VideoQueue.Dequeue(); CurrentVideoUrl = null; currentRetries = 0; retryAtTime = 0f; mode = Mode.Video; NetworkHandler.Instance?.BroadcastRemoveFromQueue(CurrentInput); NetworkHandler.Instance?.BroadcastLoadVideo(CurrentInput); LoadCurrentVideo(); } private void LoadCurrentVideo() { string input = CurrentInput; logger.LogInfo((object)("Loading video: " + input)); IsLoadingVideo = true; VideoDownloader.Instance.Get(input, delegate(DownloadResult result) { if (!(CurrentInput != input)) { IsLoadingVideo = false; if (!result.Success) { OnCurrentVideoFailed(result.Error); } else { CurrentVideoUrl = result.CanonicalUrl ?? input; if ((Object)(object)TVController.Instance == (Object)null) { logger.LogError((object)"TVController not found!"); } else { TVController.Instance.PlayFile(result.FilePath); lastSyncTime = Time.time; NetworkHandler.Instance?.BroadcastPlayVideo(input, CurrentVideoUrl, 0f); } } } }); } private void OnCurrentVideoFailed(string reason) { logger.LogError((object)("Video failed: " + CurrentInput + " (" + reason + ")")); TVController.Instance?.Stop(); currentRetries++; if (currentRetries < 2) { logger.LogInfo((object)$"Retrying in {3f}s (attempt {currentRetries + 1}/{2})"); retryAtTime = Time.time + 3f; return; } logger.LogError((object)$"Giving up on {CurrentInput} after {currentRetries} attempts"); ShowTip("Video Error", "Could not play " + VideoInput.Describe(CurrentInput) + ". " + reason); ClearCurrentVideo(); mode = Mode.Idle; } private void ClearCurrentVideo() { CurrentInput = null; CurrentVideoUrl = null; currentRetries = 0; retryAtTime = 0f; } private void SyncPlaybackToClients(TVController tv) { if (mode == Mode.Video && tv.IsPlaying() && !(Time.time - lastSyncTime < 2f)) { lastSyncTime = Time.time; NetworkHandler.Instance?.BroadcastPlaybackTime((float)tv.Time); } } public void OnSkipRequested() { string currentInput = CurrentInput; if (currentInput != null && !VideoQueue.Entries.Contains(currentInput)) { VideoDownloader.Instance.Cancel(currentInput); } ClearCurrentVideo(); IsLoadingVideo = false; mode = Mode.Idle; TVController.Instance?.Stop(); } public void OnClearRequested() { VideoQueue.Clear(); VideoDownloader.Instance.CancelAll(); OnSkipRequested(); } private void FollowHost(TVController tv) { if (mode == Mode.Fallback) { if (!tv.IsBusy() && !fallbackBroken) { StartFallbackPlayback(); } } else { if (mode != Mode.Video || CurrentVideoUrl == null || IsLoadingVideo || CurrentVideoUrl == failedVideoUrl) { return; } string wanted = CurrentVideoUrl; if (loadedVideoUrl == wanted && tv.LoadedPath != null) { return; } IsLoadingVideo = true; VideoDownloader.Instance.Get(wanted, delegate(DownloadResult result) { if (!(CurrentVideoUrl != wanted)) { IsLoadingVideo = false; if (!result.Success) { failedVideoUrl = wanted; ShowTip("Video Error", "Could not download the host's video. " + result.Error); } else { loadedVideoUrl = wanted; TVController.Instance?.PlayFile(result.FilePath, EstimatedHostTime()); } } }); } } private float EstimatedHostTime() { return hostTime + (Time.realtimeSinceStartup - hostTimeReceivedAt); } public void OnHostLoadingVideo(string input) { if (!IsHost) { VideoDownloader.Instance.Prefetch(input); } } public void OnHostPlayingVideo(string input, string videoUrl, float startTime) { if (!IsHost) { logger.LogInfo((object)("Host is playing " + videoUrl + " (" + input + ")")); CurrentInput = input; CurrentVideoUrl = videoUrl; IsLoadingVideo = false; mode = Mode.Video; hostTime = startTime; hostTimeReceivedAt = Time.realtimeSinceStartup; } } public void OnHostPlaybackTime(float time) { if (!IsHost) { hostTime = time; hostTimeReceivedAt = Time.realtimeSinceStartup; TVController instance = TVController.Instance; if (!((Object)(object)instance == (Object)null) && instance.IsPlaying() && mode == Mode.Video && Mathf.Abs((float)instance.Time - time) > 1.5f) { logger.LogInfo((object)$"Resyncing to host: {instance.Time:0.0}s -> {time:0.0}s"); instance.Seek(time); } } } public void OnHostPlayingFallback() { if (!IsHost) { mode = Mode.Fallback; CurrentInput = null; CurrentVideoUrl = null; IsLoadingVideo = false; TVController.Instance?.Stop(); } } private void PrefetchNext() { if (!IsLoadingVideo && YoutubeOnTVBase.PrefetchNextVideo.Value) { string text = VideoQueue.Peek(); if (text != null && !VideoDownloader.Instance.IsReadyOrInFlight(text)) { logger.LogInfo((object)("Prefetching next video: " + text)); VideoDownloader.Instance.Prefetch(text); } } } private void PlayFallback(bool broadcast) { StartFallbackPlayback(); if (broadcast) { NetworkHandler.Instance?.BroadcastPlayFallback(); } } private void StartFallbackPlayback() { if (!((Object)(object)TVController.Instance == (Object)null)) { mode = Mode.Fallback; loadedVideoUrl = null; TVController.Instance.PlayFile(FallbackVideoPath); } } public void OnVideoFinished() { if (mode == Mode.Fallback) { StartFallbackPlayback(); return; } if (IsHost) { ClearCurrentVideo(); } mode = Mode.Idle; } public void OnVideoPlaybackError(string message) { if (mode == Mode.Fallback) { logger.LogError((object)"The fallback video cannot be played; the TV stays dark until a video is queued"); fallbackBroken = true; mode = Mode.Idle; } else if (!IsHost) { failedVideoUrl = CurrentVideoUrl; } else if (CurrentInput != null) { OnCurrentVideoFailed("playback failed: " + message); } } public void OnTVPoweredOn() { TVController.Instance?.Resume(); } private bool IsTVOn() { TVScript val = TVController.Instance?.GetTVScript(); if ((Object)(object)val != (Object)null) { return val.tvOn; } return false; } private void SetTVPower(bool on) { TVScript val = TVController.Instance?.GetTVScript(); if ((Object)(object)val != (Object)null && val.tvOn != on) { val.TurnTVOnOff(on); } } private static void ShowTip(string header, string body) { HUDManager instance = HUDManager.Instance; if (instance != null) { instance.DisplayTip(header, body, true, false, "LC_Tip1"); } } public TVStateData GetCurrentTVState() { TVController instance = TVController.Instance; return new TVStateData { isTVOn = IsTVOn(), mode = (int)mode, input = CurrentInput, videoUrl = CurrentVideoUrl, playbackTime = (((Object)(object)instance != (Object)null && mode == Mode.Video && CurrentVideoUrl != null) ? ((float)instance.Time) : 0f), queue = VideoQueue.Entries.ToArray() }; } public void ApplyTVStateFromNetwork(TVStateData state) { if (IsHost) { return; } ManualLogSource obj = logger; object[] obj2 = new object[5] { state.isTVOn, (Mode)state.mode, state.videoUrl, state.playbackTime, null }; string[] queue = state.queue; obj2[4] = ((queue != null) ? queue.Length : 0); obj.LogInfo((object)string.Format("Applying host TV state: on={0} mode={1} video={2} t={3:0.0} queue={4}", obj2)); VideoQueue.ReplaceWith(state.queue); pendingTvOn = state.isTVOn; switch ((Mode)state.mode) { case Mode.Fallback: OnHostPlayingFallback(); return; case Mode.Video: if (state.videoUrl != null) { OnHostPlayingVideo(state.input, state.videoUrl, state.playbackTime); return; } break; } mode = Mode.Idle; CurrentInput = state.input; CurrentVideoUrl = null; if (state.input != null) { VideoDownloader.Instance.Prefetch(state.input); } } } public static class VideoQueue { private static readonly List<string> _entries = new List<string>(); public static IReadOnlyList<string> Entries => _entries; public static void Add(string entry) { if (!string.IsNullOrEmpty(entry)) { _entries.Add(entry); YoutubeOnTVBase.Log.LogInfo((object)$"[VideoQueue] Added '{entry}'. Queue size: {_entries.Count}"); } } public static void Clear() { _entries.Clear(); } public static void ReplaceWith(IEnumerable<string> entries) { _entries.Clear(); if (entries != null) { _entries.AddRange(entries); } } public static bool IsEmpty() { return _entries.Count == 0; } public static int Count() { return _entries.Count; } public static string Peek() { if (_entries.Count != 0) { return _entries[0]; } return null; } public static string Dequeue() { if (_entries.Count == 0) { return null; } string text = _entries[0]; _entries.RemoveAt(0); YoutubeOnTVBase.Log.LogInfo((object)$"[VideoQueue] Dequeued '{text}'. Remaining: {_entries.Count}"); return text; } public static void RemoveFirstMatch(string entry) { int num = _entries.IndexOf(entry); if (num >= 0) { _entries.RemoveAt(num); } } } public static class PluginInfo { public const string PLUGIN_GUID = "YoutubeOnTV"; public const string PLUGIN_NAME = "YoutubeOnTV"; public const string PLUGIN_VERSION = "1.0.0"; } } namespace YoutubeOnTV.Patches { internal static class SessionPatch { [HarmonyPatch(typeof(StartOfRound), "Awake")] [HarmonyPostfix] private static void OnRoundLoaded() { VideoManager.Instance?.ResetSession(); } [HarmonyPatch(typeof(GameNetworkManager), "Disconnect")] [HarmonyPostfix] private static void OnDisconnected() { VideoManager.Instance?.ResetSession(); } [HarmonyPatch(typeof(PlayerControllerB), "ConnectClientToPlayerObject")] [HarmonyPostfix] private static void OnLocalPlayerConnected() { NetworkHandler.Instance?.RequestTVState(); } } [HarmonyPatch(typeof(Terminal))] internal class TerminalPatch { [HarmonyPatch("ParsePlayerSentence")] [HarmonyPrefix] private static bool ParsePlayerSentencePrefix(Terminal __instance, ref TerminalNode __result) { string text = __instance.screenText.text; int textAdded = __instance.textAdded; if (textAdded <= 0 || textAdded > text.Length) { return true; } TerminalNode val = YoutubeOnTVBase.HandleTerminalCommand(text.Substring(text.Length - textAdded)); if ((Object)(object)val == (Object)null) { return true; } __result = val; return false; } [HarmonyPatch("TextChanged")] [HarmonyPrefix] private static bool TextChangedPatch(Terminal __instance, string newText, ref int ___textAdded, ref string ___currentText, ref bool ___modifyingText) { if ((Object)(object)__instance.currentNode == (Object)null) { return false; } if (___modifyingText) { ___modifyingText = false; return false; } ___textAdded += newText.Length - ___currentText.Length; if (___textAdded < 0) { __instance.screenText.text = ___currentText; ___textAdded = 0; return false; } int num = 500; if (___textAdded > num) { __instance.screenText.text = ___currentText; ___textAdded = num; return false; } ___currentText = newText; return false; } } [HarmonyPatch(typeof(TVScript))] internal class TVScriptPatch { [HarmonyPatch("Update")] [HarmonyPrefix] private static bool UpdatePatch(TVScript __instance) { if ((Object)(object)((Component)__instance).gameObject.GetComponent<TVController>() == (Object)null) { ((Component)__instance).gameObject.AddComponent<TVController>(); Debug.Log((object)"Attached TVController to TVScript object."); } return false; } [HarmonyPatch("TVFinishedClip")] [HarmonyPrefix] private static bool TVFinishedClipPatch() { return false; } [HarmonyPatch("TurnTVOnOff")] [HarmonyPrefix] private static bool TurnTVOnOffPatch(TVScript __instance, bool on) { __instance.tvOn = on; if (on) { Debug.Log((object)"TV turned ON - triggering mod video playback"); __instance.tvSFX.PlayOneShot(__instance.switchTVOn); WalkieTalkie.TransmitOneShotAudio(__instance.tvSFX, __instance.switchTVOn, 1f); if ((Object)(object)VideoManager.Instance != (Object)null) { VideoManager.Instance.OnTVPoweredOn(); } } else { Debug.Log((object)"TV turned OFF"); __instance.tvSFX.PlayOneShot(__instance.switchTVOff); WalkieTalkie.TransmitOneShotAudio(__instance.tvSFX, __instance.switchTVOff, 1f); } typeof(TVScript).GetMethod("SetTVScreenMaterial", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(__instance, new object[1] { on }); return false; } } }