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 LethalTubeTV v0.3.1
LethalTubeTV.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.SceneManagement; 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("LethalTubeTV")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.3.1.0")] [assembly: AssemblyInformationalVersion("0.3.1")] [assembly: AssemblyProduct("LethalTubeTV")] [assembly: AssemblyTitle("LethalTubeTV")] [assembly: AssemblyVersion("0.3.1.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 LethalTubeTV { internal sealed class MediaPipeline { private readonly struct ProcessResult { internal readonly int ExitCode; internal readonly string StdOut; internal readonly string StdErr; internal readonly long ElapsedMs; internal ProcessResult(int exitCode, string stdout, string stderr, long elapsedMs) { ExitCode = exitCode; StdOut = stdout; StdErr = stderr; ElapsedMs = elapsedMs; } } private readonly string _pluginDir; private readonly string _cacheRoot; private readonly ManualLogSource _log; private readonly ToolBootstrapper _toolBootstrapper; private readonly SemaphoreSlim _mediaGate = new SemaphoreSlim(1, 1); internal MediaPipeline(string pluginDir, ManualLogSource log) { _pluginDir = pluginDir; _log = log; _cacheRoot = Path.Combine(pluginDir, "cache"); _toolBootstrapper = new ToolBootstrapper(pluginDir, log); Directory.CreateDirectory(_cacheRoot); } internal ToolSetupResult EnsureTools(Action<string> progress) { return _toolBootstrapper.EnsureTools(progress); } internal ToolCheckResult CheckTools() { string text = ResolveTool("yt-dlp.exe"); string text2 = ResolveTool("ffmpeg.exe"); string text3 = ResolveTool("deno.exe"); if (text == null) { return ToolCheckResult.Fail("yt-dlp.exe가 아직 준비되지 않았습니다. 자동 설치가 끝날 때까지 잠시 기다려 주세요."); } if (text2 == null) { return ToolCheckResult.Fail("ffmpeg.exe가 아직 준비되지 않았습니다. 자동 설치가 끝날 때까지 잠시 기다려 주세요."); } if (text3 == null) { return ToolCheckResult.Fail("deno.exe가 아직 준비되지 않았습니다. 자동 설치가 끝날 때까지 잠시 기다려 주세요."); } return ToolCheckResult.Success(); } internal PipelineResult DownloadAndConvert(string url, int maxHeight, Action<string> progress) { return DownloadAndConvertInternal(url, maxHeight, "current", progress); } internal PipelineResult PreloadForQueue(string url, int maxHeight, int queueId, Action<string> progress) { return DownloadAndConvertInternal(url, maxHeight, "queue_" + queueId, progress); } internal void DeleteCachedFile(string path) { if (string.IsNullOrWhiteSpace(path)) { return; } try { string fullPath = Path.GetFullPath(path); string value = Path.GetFullPath(_cacheRoot) + Path.DirectorySeparatorChar; if (fullPath.StartsWith(value, StringComparison.OrdinalIgnoreCase)) { if (File.Exists(fullPath)) { File.Delete(fullPath); } string path2 = Path.ChangeExtension(fullPath, ".wav"); if (File.Exists(path2)) { File.Delete(path2); } } } catch (Exception ex) { _log.LogDebug((object)("Cached file cleanup warning: " + ex.Message)); } } private PipelineResult DownloadAndConvertInternal(string url, int maxHeight, string cacheKey, Action<string> progress) { _mediaGate.Wait(); try { string text = ResolveTool("yt-dlp.exe"); string text2 = ResolveTool("ffmpeg.exe"); string text3 = ResolveTool("deno.exe"); if (text == null || text2 == null || text3 == null) { return PipelineResult.Fail("필수 도구를 찾지 못했습니다."); } Directory.CreateDirectory(_cacheRoot); string text4 = SanitizeCacheKey(cacheKey); string text5 = Path.Combine(_cacheRoot, "work_" + text4); ResetWorkDirectory(text5); string text6 = Path.Combine(_cacheRoot, text4 + ".mp4"); string text7 = Path.Combine(_cacheRoot, text4 + ".wav"); string text8 = Path.Combine(text5, "title.txt"); int num = Math.Max(144, maxHeight); try { if (File.Exists(text6)) { File.Delete(text6); } } catch { } try { if (File.Exists(text7)) { File.Delete(text7); } } catch { } string text9 = "--no-playlist --no-simulate --no-color --encoding utf-8 --js-runtimes " + Q("deno:" + text3) + " --ffmpeg-location " + Q(text2) + " "; string empty = string.Empty; string text10 = null; bool flag = false; progress?.Invoke("1/3 YouTube 영상/오디오 받는 중..."); string value = Path.Combine(text5, "prepared.%(ext)s"); string value2 = $"bv[ext=mp4][vcodec^=avc1][height<={num}]+ba[ext=m4a]/" + $"b[ext=mp4][vcodec^=avc1][height<={num}]"; string text11 = text9 + "--print-to-file " + Q("before_dl:%(title)s") + " " + Q(text8) + " -f " + Q(value2) + " --merge-output-format mp4 -o " + Q(value) + " " + Q(url); _log.LogInfo((object)("yt-dlp fast-path args [" + text4 + "]: " + RedactUrl(text11))); ProcessResult processResult = RunProcess(text, text11, text5, 300000); string text12 = ((processResult.ExitCode == 0) ? FindMediaFile(text5, "prepared.*") : null); if (processResult.ExitCode == 0 && text12 != null) { empty = ReadUtf8TextFile(text8); if (!string.IsNullOrWhiteSpace(empty)) { _log.LogInfo((object)("YouTube title [" + text4 + "]: " + empty)); } try { if (File.Exists(text6)) { File.Delete(text6); } } catch { } File.Move(text12, text6); text10 = text6; flag = true; _log.LogInfo((object)($"Fast-path remux ready [{text4}] in {(double)processResult.ElapsedMs / 1000.0:0.00}s. " + "AVC/H.264 video was kept without re-encoding.")); } else { _log.LogWarning((object)($"Fast-path unavailable [{text4}] after {(double)processResult.ElapsedMs / 1000.0:0.00}s; " + "using compatibility fallback. " + ShortError(processResult.StdErr))); ResetWorkDirectory(text5); text8 = Path.Combine(text5, "title.txt"); string value3 = Path.Combine(text5, "video.%(ext)s"); string value4 = Path.Combine(text5, "audio.%(ext)s"); progress?.Invoke("1/4 호환 영상 스트림 받는 중..."); string value5 = $"bv*[height<={num}]/b[height<={num}]"; string text13 = text9 + "--print-to-file " + Q("before_dl:%(title)s") + " " + Q(text8) + " -f " + Q(value5) + " -o " + Q(value3) + " " + Q(url); _log.LogInfo((object)("yt-dlp video args [" + text4 + "]: " + RedactUrl(text13))); ProcessResult processResult2 = RunProcess(text, text13, text5, 240000); if (processResult2.ExitCode != 0) { _log.LogError((object)("yt-dlp video stderr [" + text4 + "]: " + processResult2.StdErr)); return PipelineResult.Fail("영상 다운로드 실패: " + ShortError(processResult2.StdErr)); } string text14 = FindMediaFile(text5, "video.*"); if (text14 == null) { return PipelineResult.Fail("영상 다운로드는 완료되었지만 video 파일을 찾지 못했습니다."); } empty = ReadUtf8TextFile(text8); if (!string.IsNullOrWhiteSpace(empty)) { _log.LogInfo((object)("YouTube title [" + text4 + "]: " + empty)); } progress?.Invoke("2/4 YouTube 오디오 스트림 받는 중..."); string text15 = text9 + "-f " + Q("ba/b") + " -o " + Q(value4) + " " + Q(url); _log.LogInfo((object)("yt-dlp audio args [" + text4 + "]: " + RedactUrl(text15))); ProcessResult processResult3 = RunProcess(text, text15, text5, 240000); if (processResult3.ExitCode != 0) { _log.LogError((object)("yt-dlp audio stderr [" + text4 + "]: " + processResult3.StdErr)); return PipelineResult.Fail("오디오 다운로드 실패: " + ShortError(processResult3.StdErr)); } string text16 = FindMediaFile(text5, "audio.*"); if (text16 == null) { return PipelineResult.Fail("오디오 다운로드는 완료되었지만 audio 파일을 찾지 못했습니다."); } progress?.Invoke("3/4 TV용 MP4 만드는 중..."); string text17 = "-y -hide_banner -loglevel warning -i " + Q(text14) + " -i " + Q(text16) + " -map 0:v:0 -map 1:a:0 -c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p -c:a aac -b:a 160k -ar 48000 -ac 2 -shortest -movflags +faststart " + Q(text6); _log.LogInfo((object)("ffmpeg fallback args [" + text4 + "]: " + text17)); ProcessResult processResult4 = RunProcess(text2, text17, text5, 360000); if (processResult4.ExitCode != 0 || !File.Exists(text6)) { _log.LogError((object)("ffmpeg stderr [" + text4 + "]: " + processResult4.StdErr)); return PipelineResult.Fail("ffmpeg 변환 실패: " + ShortError(processResult4.StdErr)); } _log.LogInfo((object)$"Compatibility transcode [{text4}] completed in {(double)processResult4.ElapsedMs / 1000.0:0.00}s."); text10 = text16; } progress?.Invoke(flag ? "2/3 붐박스/크루저용 오디오 만드는 중..." : "4/4 붐박스/크루저용 오디오 만드는 중..."); string text18 = "-y -hide_banner -loglevel warning -i " + Q(text10) + " -vn -c:a pcm_s16le -ar 44100 -ac 1 " + Q(text7); _log.LogInfo((object)("ffmpeg WAV args [" + text4 + "]: " + text18)); ProcessResult processResult5 = RunProcess(text2, text18, text5, 180000); if (processResult5.ExitCode != 0 || !File.Exists(text7)) { _log.LogError((object)("ffmpeg WAV stderr [" + text4 + "]: " + processResult5.StdErr)); return PipelineResult.Fail("붐박스/크루저용 오디오 변환 실패: " + ShortError(processResult5.StdErr)); } if (flag) { progress?.Invoke("3/3 재생 준비 완료!"); } long length = new FileInfo(text6).Length; long length2 = new FileInfo(text7).Length; _log.LogInfo((object)$"Created TV MP4 [{text4}]: {text6} ({(double)length / 1024.0 / 1024.0:0.00} MiB)"); _log.LogInfo((object)$"Created synchronized WAV [{text4}]: {text7} ({(double)length2 / 1024.0 / 1024.0:0.00} MiB)"); return PipelineResult.Success(text6, empty); } catch (Exception ex) { return PipelineResult.Fail("미디어 준비 오류: " + ex.Message); } finally { try { string text19 = SanitizeCacheKey(cacheKey); string path = Path.Combine(_cacheRoot, "work_" + text19); if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } } catch { } _mediaGate.Release(); } } internal TitleResult FetchTitle(string url) { string text = ResolveTool("yt-dlp.exe"); string text2 = ResolveTool("deno.exe"); if (text == null || text2 == null) { return TitleResult.Fail("yt-dlp 또는 Deno를 찾지 못했습니다."); } Directory.CreateDirectory(_cacheRoot); string text3 = Path.Combine(_cacheRoot, "queue_title_" + Guid.NewGuid().ToString("N") + ".txt"); try { string args = "--skip-download --no-playlist --no-color --encoding utf-8 --js-runtimes " + Q("deno:" + text2) + " --print-to-file " + Q("%(title)s") + " " + Q(text3) + " " + Q(url); ProcessResult processResult = RunProcess(text, args, _cacheRoot, 60000); if (processResult.ExitCode != 0) { return TitleResult.Fail(ShortError(processResult.StdErr)); } string text4 = ReadUtf8TextFile(text3); return string.IsNullOrWhiteSpace(text4) ? TitleResult.Fail("제목이 비어 있습니다.") : TitleResult.Success(text4); } catch (Exception ex) { return TitleResult.Fail(ex.Message); } finally { try { if (File.Exists(text3)) { File.Delete(text3); } } catch { } } } internal static bool IsSupportedYouTubeUrl(string raw, out string normalized, out string reason) { normalized = string.Empty; reason = string.Empty; if (string.IsNullOrWhiteSpace(raw)) { reason = "YouTube 링크를 입력해 주세요."; return false; } raw = raw.Trim(); if (!Uri.TryCreate(raw, UriKind.Absolute, out Uri result)) { reason = "링크 형식이 이상해."; return false; } if (result.Scheme != Uri.UriSchemeHttp && result.Scheme != Uri.UriSchemeHttps) { reason = "http/https 링크만 지원해."; return false; } string text = result.Host.ToLowerInvariant(); if (!(text == "youtu.be") && !(text == "youtube.com") && !text.EndsWith(".youtube.com")) { reason = "v0.1에서는 YouTube 링크만 지원해."; return false; } normalized = result.AbsoluteUri; return true; } private static void ResetWorkDirectory(string path) { try { if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } } catch { } Directory.CreateDirectory(path); } private static string SanitizeCacheKey(string key) { if (string.IsNullOrWhiteSpace(key)) { return "media"; } char[] invalid = Path.GetInvalidFileNameChars(); return new string(key.Select((char c) => (!invalid.Contains(c)) ? c : '_').ToArray()); } private static string FindMediaFile(string directory, string pattern) { return (from p in Directory.GetFiles(directory, pattern) where !p.EndsWith(".part", StringComparison.OrdinalIgnoreCase) where !p.EndsWith(".ytdl", StringComparison.OrdinalIgnoreCase) orderby new FileInfo(p).Length descending select p).FirstOrDefault(); } private static string ReadUtf8TextFile(string path) { try { if (!File.Exists(path)) { return string.Empty; } return File.ReadAllText(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)).Trim(); } catch { try { return File.ReadAllText(path, Encoding.UTF8).Trim(); } catch { return string.Empty; } } } private string ResolveTool(string exeName) { string text = Path.Combine(_pluginDir, "tools", exeName); if (File.Exists(text)) { return text; } string text2 = Path.Combine(_pluginDir, exeName); if (File.Exists(text2)) { return text2; } string[] array = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator); foreach (string text3 in array) { try { if (!string.IsNullOrWhiteSpace(text3)) { string text4 = Path.Combine(text3.Trim().Trim('"'), exeName); if (File.Exists(text4)) { return text4; } } } catch { } } try { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); if (!string.IsNullOrWhiteSpace(folderPath)) { string text5 = Path.Combine(folderPath, "Microsoft", "WinGet", "Links", exeName); if (File.Exists(text5)) { return text5; } string path = Path.Combine(folderPath, "Microsoft", "WinGet", "Packages"); if (Directory.Exists(path)) { string text6 = Directory.EnumerateFiles(path, exeName, SearchOption.AllDirectories).FirstOrDefault(); if (!string.IsNullOrEmpty(text6) && File.Exists(text6)) { return text6; } } } } catch (Exception ex) { _log.LogDebug((object)("WinGet tool lookup failed for " + exeName + ": " + ex.Message)); } return null; } private static ProcessResult RunProcess(string exe, string args, string workingDir, int timeoutMs) { ProcessStartInfo processStartInfo = new ProcessStartInfo { FileName = exe, Arguments = args, WorkingDirectory = workingDir, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; try { processStartInfo.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; processStartInfo.EnvironmentVariables["PYTHONUTF8"] = "1"; } catch { } using Process process = new Process { StartInfo = processStartInfo }; Stopwatch stopwatch = Stopwatch.StartNew(); process.Start(); try { process.PriorityClass = ProcessPriorityClass.BelowNormal; process.PriorityBoostEnabled = false; } catch { } Task<string> task = process.StandardOutput.ReadToEndAsync(); Task<string> task2 = process.StandardError.ReadToEndAsync(); if (!process.WaitForExit(timeoutMs)) { try { process.Kill(); } catch { } stopwatch.Stop(); return new ProcessResult(-1, task.GetAwaiter().GetResult(), "시간 초과", stopwatch.ElapsedMilliseconds); } stopwatch.Stop(); return new ProcessResult(process.ExitCode, task.GetAwaiter().GetResult(), task2.GetAwaiter().GetResult(), stopwatch.ElapsedMilliseconds); } private static string Q(string value) { return "\"" + value.Replace("\"", "\\\"") + "\""; } private static string ShortError(string text) { if (string.IsNullOrWhiteSpace(text)) { return "알 수 없는 오류"; } string[] array = text.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); return string.Join(" | ", array.TakeLast(Math.Min(4, array.Length))); } private static string RedactUrl(string text) { return text; } } internal readonly struct ToolCheckResult { internal readonly bool Ok; internal readonly string Message; private ToolCheckResult(bool ok, string message) { Ok = ok; Message = message; } internal static ToolCheckResult Success() { return new ToolCheckResult(ok: true, string.Empty); } internal static ToolCheckResult Fail(string message) { return new ToolCheckResult(ok: false, message); } } internal readonly struct TitleResult { internal readonly bool Ok; internal readonly string Title; internal readonly string Message; private TitleResult(bool ok, string title, string message) { Ok = ok; Title = title ?? string.Empty; Message = message ?? string.Empty; } internal static TitleResult Success(string title) { return new TitleResult(ok: true, title, string.Empty); } internal static TitleResult Fail(string message) { return new TitleResult(ok: false, string.Empty, message); } } internal readonly struct PipelineResult { internal readonly bool Ok; internal readonly string Message; internal readonly string OutputPath; internal readonly string Title; private PipelineResult(bool ok, string message, string outputPath, string title) { Ok = ok; Message = message; OutputPath = outputPath; Title = title; } internal static PipelineResult Success(string path, string title) { return new PipelineResult(ok: true, string.Empty, path, title ?? string.Empty); } internal static PipelineResult Fail(string message) { return new PipelineResult(ok: false, message, string.Empty, string.Empty); } } public sealed class Plugin : MonoBehaviour { private enum NetworkRequestType : byte { AddUrl = 1, Skip, Clear, RequestSnapshot, RemoveQueueItem, ReplayHistoryItem, RemoveHistoryItem, ShuffleQueue } private sealed class RemoteQueueState { internal int Id; internal string Url = string.Empty; internal string Title = string.Empty; } private sealed class QueuedVideo { internal int Id; internal string Url = string.Empty; internal string Title = "제목을 불러오는 중..."; internal string CachedPath = string.Empty; internal string PreloadStatus = "대기"; internal string PreloadError = string.Empty; internal bool PreloadStarted; internal bool Ready; internal bool Removed; } private sealed class SyncedAudioDevice { internal Component Owner; internal string Label = string.Empty; internal AudioSource VanillaAudio; internal AudioSource AuxAudio; internal AudioSource CustomAudio; internal string LoadedPath = string.Empty; internal bool Prepared; internal bool VanillaStateCaptured; internal bool VanillaMuteBefore; internal float VanillaVolumeBefore; internal bool AuxStateCaptured; internal bool AuxMuteBefore; internal float AuxVolumeBefore; } private sealed class WavData { internal int Channels; internal int SampleRate; internal float[] Samples; internal int FrameCount; } private const int NetworkProtocol = 2; private const string MsgHello = "gj.lethaltubetv.hello"; private const string MsgHelloAck = "gj.lethaltubetv.helloAck"; private const string MsgSnapshot = "gj.lethaltubetv.snapshot"; private const string MsgRequest = "gj.lethaltubetv.request"; private const float HandshakeTimeoutSeconds = 5f; private const float SnapshotIntervalSeconds = 0.4f; private const int MaxNetworkQueueItems = 64; private const int MaxNetworkUrlLength = 2048; private NetworkManager _networkManager; private CustomMessagingManager _customMessaging; private bool _networkHandlersRegistered; private bool _networkSessionWasActive; private float _nextNetworkHelloAt; private bool _networkHandshakeAccepted; private bool _networkSnapshotReceived; private float _nextNetworkSnapshotAt; private long _networkSnapshotRevision; private readonly Dictionary<ulong, float> _pendingModHandshakes = new Dictionary<ulong, float>(); private readonly HashSet<ulong> _verifiedModClients = new HashSet<ulong>(); private string _remoteCurrentUrl = string.Empty; private string _remoteCurrentTitle = string.Empty; private bool _remotePlaybackRunning; private double _remotePlaybackTime; private double _remoteSnapshotServerTime; private long _remoteSnapshotRevision; private string _pendingRemoteCurrentUrl = string.Empty; private string _pendingRemoteCurrentTitle = string.Empty; private string _networkFailedCurrentUrl = string.Empty; private float _networkFailedCurrentRetryAt; public const string PluginGuid = "gj.lethaltubetv"; public const string PluginName = "LethalTubeTV"; public const string PluginVersion = "0.3.1"; internal static ManualLogSource Log; internal static Plugin Instance; private static int _suppressQuickMenuThroughFrame = -1; private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>(); private readonly List<QueuedVideo> _queue = new List<QueuedVideo>(); private readonly List<QueuedVideo> _history = new List<QueuedVideo>(); private readonly Dictionary<int, SyncedAudioDevice> _boomboxSync = new Dictionary<int, SyncedAudioDevice>(); private readonly Dictionary<int, SyncedAudioDevice> _cruiserSync = new Dictionary<int, SyncedAudioDevice>(); private int _nextQueueItemId; private float _nextDeviceSyncSearchTime; private FieldInfo _cruiserRadioOnField; private bool _preloadWorkerRunning; private int _waitingForNextReadyId = -1; private int _skipWhenReadyId = -1; private string _currentLocalPath = string.Empty; private AudioClip _syncedAudioClip; private string _syncedAudioClipSourcePath = string.Empty; private bool _syncedAudioClipLoading; private int _syncedAudioClipLoadToken; private double _sharedPlaybackTime; private float _sharedPlaybackAnchorUnscaledTime; private bool _sharedPlaybackRunning; private bool _handlingSharedTrackEnd; private ConfigEntry<KeyCode> _openKey; private ConfigEntry<float> _interactionDistance; private ConfigEntry<int> _tvResolution; private ConfigEntry<bool> _useVanillaBoombox; private ConfigEntry<float> _youtubeVolume; private ConfigEntry<float> _boomboxVolume; private ConfigEntry<float> _cruiserVolume; private ConfigEntry<float> _cruiserMaxDistance; private ConfigEntry<bool> _showNowPlayingHud; private ConfigEntry<float> _nowPlayingHudDistance; private TVScript _tv; private float _nextTvSearchTime; private bool _showWindow; private bool _busy; private bool _ownsPlayback; private bool _loggedFirstFrame; private bool _mediaReady; private bool _haveObservedTvPower; private bool _lastObservedTvOn; private string _url = string.Empty; private string _currentUrl = string.Empty; private string _currentTitle = string.Empty; private int _loadToken; private string _status = "준비됨"; private Rect _windowRect = new Rect(0f, 0f, 720f, 420f); private CursorLockMode _previousCursorLock; private bool _previousCursorVisible; private PlayerControllerB _uiPlayer; private FieldInfo _disableLookInputField; private bool _previousDisableLookInput; private bool _capturedLookInput; private FieldInfo _disableMoveInputField; private bool _previousDisableMoveInput; private bool _capturedMoveInput; private bool _previousIsTypingChat; private bool _capturedTypingChat; private object _gameInputTarget; private MethodInfo _deactivateGameInputMethod; private MethodInfo _activateGameInputMethod; private bool _gameInputDeactivated; private bool _gameInputWasActive = true; private bool _clearGuiFocusOnNextDraw; private bool _focusUrlOnNextRepaint; private bool _showQueuePanel; private Vector2 _queueScroll; private GUIStyle _panelStyle; private GUIStyle _headerStyle; private GUIStyle _titleStyle; private GUIStyle _subtitleStyle; private GUIStyle _labelStyle; private GUIStyle _statusStyle; private GUIStyle _inputStyle; private GUIStyle _buttonStyle; private GUIStyle _dangerButtonStyle; private GUIStyle _smallDangerButtonStyle; private GUIStyle _queueItemStyle; private GUIStyle _playingQueueItemStyle; private GUIStyle _nowPlayingStyle; private GUIStyle _nowPlayingShadowStyle; private Texture2D _panelTexture; private Texture2D _headerTexture; private Texture2D _fieldTexture; private Texture2D _statusTexture; private Texture2D _playingTexture; private Texture2D _buttonTexture; private Texture2D _buttonHoverTexture; private Texture2D _dangerTexture; private Texture2D _dangerHoverTexture; private GameObject _videoPlayerHost; private VideoPlayer _customPlayer; private VideoPlayer _originalPlayer; private RenderTexture _tvScreenTargetTexture; private RenderTexture _vanillaVideoSinkTexture; private Texture2D _idleLogoTexture; private bool _idleLogoApplied; private bool _originalVideoStateCaptured; private bool _originalVideoEnabledBefore; private RenderTexture _originalVideoTargetBefore; private VideoAudioOutputMode _originalVideoAudioModeBefore; private long _lastCustomVideoFrame = long.MinValue; private float _lastCustomVideoFrameAdvanceTime; private float _customVideoSeekGraceUntil; private float _nextCustomVideoResyncTime; private bool _customVideoAwaitingSeekFrame; private long _customVideoFrameBeforeSeek = long.MinValue; private int _customVideoSeekRecoveryAttempts; private AudioSource _tvAudio; private AudioSource _customAudio; private bool _vanillaAudioStateCaptured; private bool _vanillaAudioMuteBefore; private float _vanillaAudioVolumeBefore; private FieldInfo _videoField; private MethodInfo _setTvScreenMaterialMethod; private MediaPipeline _pipeline; private bool _toolSetupStarted; private bool _toolSetupFinished; private string _toolSetupError = string.Empty; private bool NetworkSessionAvailable { get { if ((Object)(object)_networkManager != (Object)null) { return _networkManager.IsListening; } return false; } } private bool IsNetworkClientNonHost { get { if (NetworkSessionAvailable && _networkManager.IsClient) { return !_networkManager.IsHost; } return false; } } private bool UseRemoteNetworkAuthority { get { if (IsNetworkClientNonHost && _networkHandshakeAccepted) { return _networkSnapshotReceived; } return false; } } private ManualLogSource Logger => BootstrapPlugin.SharedLog ?? Log; private ConfigFile Config => BootstrapPlugin.SharedConfig; internal static bool UseVanillaBoombox => Instance?._useVanillaBoombox?.Value == true; internal static bool UiOpen { get { if ((Object)(object)Instance != (Object)null) { return Instance._showWindow; } return false; } } internal static bool BlockGameplayInput => UiOpen; private bool TryValidateReceivedYouTubeUrl(string rawUrl, out string normalizedUrl, string source) { normalizedUrl = string.Empty; if (string.IsNullOrWhiteSpace(rawUrl) || rawUrl.Length > 2048) { Logger.LogWarning((object)("Rejected invalid network URL from " + source + ": empty or too long.")); return false; } if (!MediaPipeline.IsSupportedYouTubeUrl(rawUrl, out normalizedUrl, out var reason)) { Logger.LogWarning((object)("Rejected invalid network URL from " + source + ": " + reason)); return false; } return true; } private void UpdateNetworkSync() { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsListening) { if ((Object)(object)_networkManager != (Object)null) { ShutdownNetworkSync(); } if (_networkSessionWasActive) { _networkSessionWasActive = false; ResetForLobbyOrSessionChange("network session ended"); } return; } if (_networkSessionWasActive && (Object)(object)_networkManager != (Object)null && _networkManager != singleton) { ResetForLobbyOrSessionChange("network session changed"); ShutdownNetworkSync(); } _networkSessionWasActive = true; if (_networkManager != singleton || !_networkHandlersRegistered) { InitializeNetworkSync(singleton); } if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsListening) { return; } if (_networkManager.IsHost) { EnforceRequiredModHandshake(); if (Time.unscaledTime >= _nextNetworkSnapshotAt) { _nextNetworkSnapshotAt = Time.unscaledTime + 0.4f; BroadcastAuthoritativeSnapshot(); } } else if (_networkManager.IsClient && !_networkHandshakeAccepted && Time.unscaledTime >= _nextNetworkHelloAt) { SendHelloToHost(); } if (UseRemoteNetworkAuthority) { TryApplyPendingRemoteCurrent(); } } private void InitializeNetworkSync(NetworkManager nm) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Expected O, but got Unknown ShutdownNetworkSync(); _networkManager = nm; _customMessaging = nm.CustomMessagingManager; if (_customMessaging == null) { return; } _customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.hello", new HandleNamedMessageDelegate(OnNetworkHello)); _customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.helloAck", new HandleNamedMessageDelegate(OnNetworkHelloAck)); _customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.snapshot", new HandleNamedMessageDelegate(OnNetworkSnapshot)); _customMessaging.RegisterNamedMessageHandler("gj.lethaltubetv.request", new HandleNamedMessageDelegate(OnNetworkRequest)); _networkHandlersRegistered = true; nm.OnClientConnectedCallback += OnNetworkClientConnected; nm.OnClientDisconnectCallback += OnNetworkClientDisconnected; _nextNetworkHelloAt = Time.unscaledTime; _networkHandshakeAccepted = nm.IsHost; _networkSnapshotReceived = nm.IsHost; _nextNetworkSnapshotAt = Time.unscaledTime + 0.15f; if (nm.IsHost) { _verifiedModClients.Clear(); _verifiedModClients.Add(nm.LocalClientId); foreach (ulong connectedClientsId in nm.ConnectedClientsIds) { if (connectedClientsId != nm.LocalClientId) { _pendingModHandshakes[connectedClientsId] = Time.unscaledTime + 5f; } } Logger.LogInfo((object)"Multiplayer sync initialized as HOST. LethalTubeTV is required for every remote client."); } else if (nm.IsClient) { SendHelloToHost(); Logger.LogInfo((object)"Multiplayer sync initialized as CLIENT. Waiting for host handshake."); } } private void ShutdownNetworkSync() { try { if (_networkHandlersRegistered && _customMessaging != null) { _customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.hello"); _customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.helloAck"); _customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.snapshot"); _customMessaging.UnregisterNamedMessageHandler("gj.lethaltubetv.request"); } if ((Object)(object)_networkManager != (Object)null) { _networkManager.OnClientConnectedCallback -= OnNetworkClientConnected; _networkManager.OnClientDisconnectCallback -= OnNetworkClientDisconnected; } } catch { } _networkManager = null; _customMessaging = null; _networkHandlersRegistered = false; _nextNetworkHelloAt = 0f; _networkHandshakeAccepted = false; _networkSnapshotReceived = false; _networkSnapshotRevision = 0L; _remoteSnapshotRevision = 0L; _pendingModHandshakes.Clear(); _verifiedModClients.Clear(); _remoteCurrentUrl = string.Empty; _remoteCurrentTitle = string.Empty; _pendingRemoteCurrentUrl = string.Empty; _pendingRemoteCurrentTitle = string.Empty; } private void OnNetworkClientConnected(ulong clientId) { if ((Object)(object)_networkManager == (Object)null) { return; } if (_networkManager.IsHost) { if (clientId != _networkManager.LocalClientId) { _pendingModHandshakes[clientId] = Time.unscaledTime + 5f; Logger.LogInfo((object)string.Format("Client {0} connected; waiting for LethalTubeTV {1} handshake.", clientId, "0.3.1")); } } else if (_networkManager.IsClient && clientId == _networkManager.LocalClientId) { SendHelloToHost(); } } private void OnNetworkClientDisconnected(ulong clientId) { _pendingModHandshakes.Remove(clientId); _verifiedModClients.Remove(clientId); if (!((Object)(object)_networkManager != (Object)null) || _networkManager.IsHost || clientId != _networkManager.LocalClientId) { return; } _networkHandshakeAccepted = false; _networkSnapshotReceived = false; if (_networkSessionWasActive) { _networkSessionWasActive = false; _mainThreadActions.Enqueue(delegate { ResetForLobbyOrSessionChange("local client disconnected"); }); } } private void EnforceRequiredModHandshake() { if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || _pendingModHandshakes.Count == 0) { return; } float unscaledTime = Time.unscaledTime; KeyValuePair<ulong, float>[] array = _pendingModHandshakes.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<ulong, float> keyValuePair = array[i]; if (unscaledTime < keyValuePair.Value) { continue; } ulong key = keyValuePair.Key; _pendingModHandshakes.Remove(key); if (!_verifiedModClients.Contains(key)) { Logger.LogWarning((object)string.Format("Disconnecting client {0}: LethalTubeTV {1} handshake not received.", key, "0.3.1")); try { _networkManager.DisconnectClient(key, "LethalTubeTV 0.3.1 모드가 필요합니다. 같은 버전을 설치한 뒤 다시 접속해주세요."); } catch (Exception ex) { Logger.LogWarning((object)("Failed to disconnect client without LethalTubeTV: " + ex.Message)); } } } } private unsafe void SendHelloToHost() { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_networkManager == (Object)null || _customMessaging == null || !_networkManager.IsClient || _networkManager.IsHost) { return; } try { FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(256, (Allocator)2, 2048); try { int num = 2; ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe("0.3.1", false); _customMessaging.SendNamedMessage("gj.lethaltubetv.hello", 0uL, val, (NetworkDelivery)3); _nextNetworkHelloAt = Time.unscaledTime + 1f; Logger.LogInfo((object)string.Format("Sent multiplayer hello to host. protocol={0}, version={1}", 2, "0.3.1")); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Logger.LogWarning((object)("Failed to send multiplayer hello: " + ex.Message)); _nextNetworkHelloAt = Time.unscaledTime + 1f; } } private void OnNetworkHello(ulong senderClientId, FastBufferReader reader) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || senderClientId == _networkManager.LocalClientId) { return; } try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives)); string text = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false); if (num != 2 || !string.Equals(text, "0.3.1", StringComparison.Ordinal)) { _pendingModHandshakes.Remove(senderClientId); Logger.LogWarning((object)$"Rejecting client {senderClientId}: incompatible LethalTubeTV protocol/version {num}/{text}."); _networkManager.DisconnectClient(senderClientId, "LethalTubeTV 버전이 맞지 않습니다. 방장은 0.3.1 버전을 사용 중입니다."); } else { _pendingModHandshakes.Remove(senderClientId); _verifiedModClients.Add(senderClientId); SendHelloAck(senderClientId); SendAuthoritativeSnapshot(senderClientId); Logger.LogInfo((object)$"Client {senderClientId} verified for LethalTubeTV multiplayer sync."); } } catch (Exception ex) { Logger.LogWarning((object)$"Invalid LethalTubeTV hello from client {senderClientId}: {ex.Message}"); } } private unsafe void SendHelloAck(ulong clientId) { //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_0048: Unknown result type (might be due to invalid IL or missing references) if (_customMessaging == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(256, (Allocator)2, 2048); try { int num = 2; ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe("0.3.1", false); _customMessaging.SendNamedMessage("gj.lethaltubetv.helloAck", clientId, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private void OnNetworkHelloAck(ulong senderClientId, FastBufferReader reader) { //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) if ((Object)(object)_networkManager == (Object)null || _networkManager.IsHost || senderClientId != 0L) { return; } try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives)); string text = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false); if (num != 2 || !string.Equals(text, "0.3.1", StringComparison.Ordinal)) { _status = "방장과 LethalTubeTV 버전이 달라. 방장: " + text + " / 나: 0.3.1"; Logger.LogWarning((object)_status); return; } _networkHandshakeAccepted = true; _status = "멀티 동기화 연결됨 · 방장 재생 상태를 따라가는 중"; Logger.LogInfo((object)"LethalTubeTV multiplayer handshake accepted by host."); SendRequest(NetworkRequestType.RequestSnapshot, string.Empty); } catch (Exception ex) { Logger.LogWarning((object)("Invalid hello acknowledgement from host: " + ex.Message)); } } private bool TrySendNetworkAddRequest(string normalizedUrl) { if (!IsNetworkClientNonHost) { return false; } if (!_networkHandshakeAccepted) { _status = "방장과 멀티 동기화 연결을 기다리는 중입니다."; return true; } SendRequest(NetworkRequestType.AddUrl, normalizedUrl); return true; } private bool TrySendNetworkSkipRequest() { if (!IsNetworkClientNonHost) { return false; } if (!_networkHandshakeAccepted) { _status = "방장과 멀티 동기화 연결을 기다리는 중입니다."; return true; } SendRequest(NetworkRequestType.Skip, string.Empty); return true; } private bool TrySendNetworkClearRequest() { if (!IsNetworkClientNonHost) { return false; } if (!_networkHandshakeAccepted) { _status = "방장과 멀티 동기화 연결을 기다리는 중입니다."; return true; } SendRequest(NetworkRequestType.Clear, string.Empty); return true; } private bool TrySendNetworkRemoveQueueItemRequest(int itemId) { if (!IsNetworkClientNonHost) { return false; } if (!_networkHandshakeAccepted) { _status = "방장과 멀티 동기화 연결을 기다리는 중입니다."; return true; } SendRequest(NetworkRequestType.RemoveQueueItem, itemId.ToString()); return true; } private bool TrySendNetworkReplayHistoryRequest(int itemId) { if (!IsNetworkClientNonHost) { return false; } if (!_networkHandshakeAccepted) { _status = "방장과 멀티 동기화 연결을 기다리는 중입니다."; return true; } SendRequest(NetworkRequestType.ReplayHistoryItem, itemId.ToString()); return true; } private bool TrySendNetworkRemoveHistoryRequest(int itemId) { if (!IsNetworkClientNonHost) { return false; } if (!_networkHandshakeAccepted) { _status = "방장과 멀티 동기화 연결을 기다리는 중입니다."; return true; } SendRequest(NetworkRequestType.RemoveHistoryItem, itemId.ToString()); return true; } private bool TrySendNetworkShuffleRequest() { if (!IsNetworkClientNonHost) { return false; } if (!_networkHandshakeAccepted) { _status = "방장과 멀티 동기화 연결을 기다리는 중입니다."; return true; } SendRequest(NetworkRequestType.ShuffleQueue, string.Empty); return true; } private unsafe void SendRequest(NetworkRequestType requestType, string payload) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) if (_customMessaging == null || (Object)(object)_networkManager == (Object)null || !_networkManager.IsClient || _networkManager.IsHost) { return; } try { FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(512, (Allocator)2, 8192); try { byte b = (byte)requestType; ((FastBufferWriter)(ref val)).WriteValueSafe<byte>(ref b, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(payload ?? string.Empty, false); _customMessaging.SendNamedMessage("gj.lethaltubetv.request", 0uL, val, (NetworkDelivery)3); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Logger.LogWarning((object)("Failed to send LethalTubeTV request: " + ex.Message)); } } private void OnNetworkRequest(ulong senderClientId, FastBufferReader reader) { //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || senderClientId == _networkManager.LocalClientId) { return; } if (!_verifiedModClients.Contains(senderClientId)) { Logger.LogWarning((object)$"Ignoring LethalTubeTV request from unverified client {senderClientId}."); return; } try { byte b = default(byte); ((FastBufferReader)(ref reader)).ReadValueSafe<byte>(ref b, default(ForPrimitives)); string text = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false); switch ((NetworkRequestType)b) { default: return; case NetworkRequestType.AddUrl: { if (!TryValidateReceivedYouTubeUrl(text, out var normalizedUrl, $"client {senderClientId} AddUrl request")) { return; } SubmitUrl(normalizedUrl); break; } case NetworkRequestType.Skip: SkipToNext(); break; case NetworkRequestType.Clear: ClearQueueAndReturnToVanilla(); break; case NetworkRequestType.RequestSnapshot: SendAuthoritativeSnapshot(senderClientId); return; case NetworkRequestType.RemoveQueueItem: { if (!int.TryParse(text, out var result3)) { return; } RemoveQueuedVideoLocal(result3); break; } case NetworkRequestType.ReplayHistoryItem: { if (!int.TryParse(text, out var result2)) { return; } ReplayHistoryVideoLocal(result2); break; } case NetworkRequestType.RemoveHistoryItem: { if (!int.TryParse(text, out var result)) { return; } RemoveHistoryVideoLocal(result); break; } case NetworkRequestType.ShuffleQueue: ShuffleQueueLocal(); break; } BroadcastAuthoritativeSnapshot(); } catch (Exception ex) { Logger.LogWarning((object)$"Failed to process LethalTubeTV request from {senderClientId}: {ex.Message}"); } } private void BroadcastAuthoritativeSnapshot() { if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || _customMessaging == null) { return; } ulong[] array = _verifiedModClients.ToArray(); foreach (ulong num in array) { if (num != _networkManager.LocalClientId) { if (!_networkManager.ConnectedClientsIds.Contains(num)) { _verifiedModClients.Remove(num); } else { SendAuthoritativeSnapshot(num); } } } } private unsafe void SendAuthoritativeSnapshot(ulong clientId) { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Unknown result type (might be due to invalid IL or missing references) //IL_01f4: Unknown result type (might be due to invalid IL or missing references) //IL_021a: Unknown result type (might be due to invalid IL or missing references) //IL_0220: Unknown result type (might be due to invalid IL or missing references) //IL_026f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsHost || _customMessaging == null) { return; } try { FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(4096, (Allocator)2, 262144); try { bool flag = _busy || _ownsPlayback || _mediaReady || !string.IsNullOrWhiteSpace(_currentUrl); double num = (flag ? GetSharedPlaybackTime() : 0.0); NetworkTime serverTime = _networkManager.ServerTime; double time = ((NetworkTime)(ref serverTime)).Time; int num2 = Math.Min(_queue.Count, 64); int num3 = Math.Min(_history.Count, 64); int num4 = 2; ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num4, default(ForPrimitives)); long num5 = ++_networkSnapshotRevision; ((FastBufferWriter)(ref val)).WriteValueSafe<long>(ref num5, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<double>(ref time, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref flag, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(_currentUrl ?? string.Empty, false); ((FastBufferWriter)(ref val)).WriteValueSafe(_currentTitle ?? string.Empty, false); ((FastBufferWriter)(ref val)).WriteValueSafe<bool>(ref _sharedPlaybackRunning, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<double>(ref num, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num2, default(ForPrimitives)); for (int i = 0; i < num2; i++) { QueuedVideo queuedVideo = _queue[i]; ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref queuedVideo.Id, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo.Url ?? string.Empty, false); ((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo.Title ?? string.Empty, false); } ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref num3, default(ForPrimitives)); for (int j = 0; j < num3; j++) { QueuedVideo queuedVideo2 = _history[j]; ((FastBufferWriter)(ref val)).WriteValueSafe<int>(ref queuedVideo2.Id, default(ForPrimitives)); ((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo2.Url ?? string.Empty, false); ((FastBufferWriter)(ref val)).WriteValueSafe(queuedVideo2.Title ?? string.Empty, false); } _customMessaging.SendNamedMessage("gj.lethaltubetv.snapshot", clientId, val, (NetworkDelivery)4); } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception ex) { Logger.LogWarning((object)$"Failed to send LethalTubeTV snapshot to client {clientId}: {ex.Message}"); } } private void OnNetworkSnapshot(ulong senderClientId, FastBufferReader reader) { //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_0042: 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_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_networkManager == (Object)null || _networkManager.IsHost || senderClientId != 0L) { return; } try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num, default(ForPrimitives)); if (num != 2) { return; } long num2 = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe<long>(ref num2, default(ForPrimitives)); double remoteSnapshotServerTime = default(double); ((FastBufferReader)(ref reader)).ReadValueSafe<double>(ref remoteSnapshotServerTime, default(ForPrimitives)); bool flag = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe<bool>(ref flag, default(ForPrimitives)); string rawUrl = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref rawUrl, false); string text = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false); bool remotePlaybackRunning = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe<bool>(ref remotePlaybackRunning, default(ForPrimitives)); double val = default(double); ((FastBufferReader)(ref reader)).ReadValueSafe<double>(ref val, default(ForPrimitives)); int val2 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref val2, default(ForPrimitives)); if (num2 <= _remoteSnapshotRevision) { return; } if (flag) { if (!TryValidateReceivedYouTubeUrl(rawUrl, out var normalizedUrl, "host snapshot current track")) { _status = "방장으로부터 유효하지 않은 YouTube URL을 받아 무시했습니다."; return; } rawUrl = normalizedUrl; } else { rawUrl = string.Empty; } val2 = Math.Max(0, Math.Min(val2, 64)); List<RemoteQueueState> list = new List<RemoteQueueState>(val2); int num3 = default(int); string rawUrl2 = default(string); string text2 = default(string); for (int i = 0; i < val2; i++) { ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num3, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe(ref rawUrl2, false); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text2, false); if (TryValidateReceivedYouTubeUrl(rawUrl2, out var normalizedUrl2, $"host snapshot queue item {num3}")) { list.Add(new RemoteQueueState { Id = num3, Url = normalizedUrl2, Title = (text2 ?? string.Empty) }); } } int val3 = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref val3, default(ForPrimitives)); val3 = Math.Max(0, Math.Min(val3, 64)); List<RemoteQueueState> list2 = new List<RemoteQueueState>(val3); int num4 = default(int); string rawUrl3 = default(string); string text3 = default(string); for (int j = 0; j < val3; j++) { ((FastBufferReader)(ref reader)).ReadValueSafe<int>(ref num4, default(ForPrimitives)); ((FastBufferReader)(ref reader)).ReadValueSafe(ref rawUrl3, false); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text3, false); if (TryValidateReceivedYouTubeUrl(rawUrl3, out var normalizedUrl3, $"host snapshot history item {num4}")) { list2.Add(new RemoteQueueState { Id = num4, Url = normalizedUrl3, Title = (text3 ?? string.Empty) }); } } _networkHandshakeAccepted = true; _networkSnapshotReceived = true; _remoteSnapshotRevision = num2; _remoteCurrentUrl = rawUrl ?? string.Empty; _remoteCurrentTitle = text ?? string.Empty; _remotePlaybackRunning = remotePlaybackRunning; _remotePlaybackTime = Math.Max(0.0, val); _remoteSnapshotServerTime = remoteSnapshotServerTime; ApplyRemoteCurrentState(flag, _remoteCurrentUrl, _remoteCurrentTitle); ReconcileRemoteQueue(list); ReconcileRemoteHistory(list2); } catch (Exception ex) { Logger.LogWarning((object)("Failed to read LethalTubeTV host snapshot: " + ex.Message)); } } private void ApplyRemoteNetworkClock() { //IL_002c: 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) if (!UseRemoteNetworkAuthority || (Object)(object)_networkManager == (Object)null) { return; } double num = _remotePlaybackTime; if (_remotePlaybackRunning) { NetworkTime serverTime = _networkManager.ServerTime; double num2 = ((NetworkTime)(ref serverTime)).Time - _remoteSnapshotServerTime; if (num2 > 0.0) { num += num2; } } _sharedPlaybackTime = Math.Max(0.0, num); _sharedPlaybackAnchorUnscaledTime = Time.unscaledTime; _sharedPlaybackRunning = _remotePlaybackRunning; _handlingSharedTrackEnd = false; } private void ApplyRemoteCurrentState(bool hasSession, string currentUrl, string currentTitle) { if (hasSession) { if (!TryValidateReceivedYouTubeUrl(currentUrl, out var normalizedUrl, "remote current-state apply")) { _status = "방장으로부터 유효하지 않은 YouTube URL을 받아 무시했습니다."; return; } currentUrl = normalizedUrl; } if (!hasSession || string.IsNullOrWhiteSpace(currentUrl)) { _pendingRemoteCurrentUrl = string.Empty; _pendingRemoteCurrentTitle = string.Empty; _networkFailedCurrentUrl = string.Empty; if (_busy || _ownsPlayback || _mediaReady || !string.IsNullOrWhiteSpace(_currentUrl)) { StopPlayback(); } _status = "방장 대기열 비어 있음 · 바닐라 장치 사용 중"; } else if (string.Equals(_currentUrl, currentUrl, StringComparison.Ordinal)) { if (!string.IsNullOrWhiteSpace(currentTitle) && currentTitle != "제목을 불러오는 중...") { _currentTitle = currentTitle; } } else { _pendingRemoteCurrentUrl = currentUrl; _pendingRemoteCurrentTitle = (string.IsNullOrWhiteSpace(currentTitle) ? "제목을 불러오는 중..." : currentTitle); TryApplyPendingRemoteCurrent(); } } private void TryApplyPendingRemoteCurrent() { if (!UseRemoteNetworkAuthority || string.IsNullOrWhiteSpace(_pendingRemoteCurrentUrl)) { return; } string pendingRemoteCurrentUrl = _pendingRemoteCurrentUrl; string pendingRemoteCurrentTitle = _pendingRemoteCurrentTitle; if (!TryValidateReceivedYouTubeUrl(pendingRemoteCurrentUrl, out var normalizedUrl, "pending remote track download")) { _pendingRemoteCurrentUrl = string.Empty; _pendingRemoteCurrentTitle = string.Empty; _status = "유효하지 않은 원격 YouTube URL을 차단했습니다."; return; } pendingRemoteCurrentUrl = normalizedUrl; if (string.Equals(_currentUrl, pendingRemoteCurrentUrl, StringComparison.Ordinal)) { _pendingRemoteCurrentUrl = string.Empty; _pendingRemoteCurrentTitle = string.Empty; if (!string.IsNullOrWhiteSpace(pendingRemoteCurrentTitle) && pendingRemoteCurrentTitle != "제목을 불러오는 중...") { _currentTitle = pendingRemoteCurrentTitle; } } else { if (_busy || (string.Equals(_networkFailedCurrentUrl, pendingRemoteCurrentUrl, StringComparison.Ordinal) && Time.unscaledTime < _networkFailedCurrentRetryAt)) { return; } _networkFailedCurrentUrl = string.Empty; QueuedVideo queuedVideo = null; for (int i = 0; i < _queue.Count; i++) { QueuedVideo queuedVideo2 = _queue[i]; if (!queuedVideo2.Removed && queuedVideo2.Ready && string.Equals(queuedVideo2.Url, pendingRemoteCurrentUrl, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(queuedVideo2.CachedPath) && File.Exists(queuedVideo2.CachedPath)) { queuedVideo = queuedVideo2; break; } } if (_ownsPlayback || _mediaReady || !string.IsNullOrWhiteSpace(_currentLocalPath)) { ArchiveCurrentTrackForHistory(); StopCurrentMediaForTransition(preserveCurrentFile: true); } _mediaReady = false; _loggedFirstFrame = false; _currentUrl = pendingRemoteCurrentUrl; _currentTitle = (string.IsNullOrWhiteSpace(pendingRemoteCurrentTitle) ? "제목을 불러오는 중..." : pendingRemoteCurrentTitle); _pendingRemoteCurrentUrl = string.Empty; _pendingRemoteCurrentTitle = string.Empty; if (queuedVideo != null) { _queue.Remove(queuedVideo); string cachedPath = queuedVideo.CachedPath; queuedVideo.CachedPath = string.Empty; queuedVideo.Removed = true; Logger.LogInfo((object)("Client switching instantly to preloaded host track: " + queuedVideo.Title)); PlayLocalFile(cachedPath); TryStartNextPreload(); } else { _currentUrl = string.Empty; _currentTitle = string.Empty; Logger.LogInfo((object)("Client downloading host current track: " + pendingRemoteCurrentUrl)); StartDownload(pendingRemoteCurrentUrl, closeWindow: false, pendingRemoteCurrentTitle); } } } private bool HasDifferentPendingRemoteCurrent(string justDownloadedUrl) { if (UseRemoteNetworkAuthority && !string.IsNullOrWhiteSpace(_pendingRemoteCurrentUrl)) { return !string.Equals(_pendingRemoteCurrentUrl, justDownloadedUrl, StringComparison.Ordinal); } return false; } private void NotifyNetworkCurrentDownloadFailed(string url) { if (UseRemoteNetworkAuthority && !string.IsNullOrWhiteSpace(url)) { _networkFailedCurrentUrl = url; _networkFailedCurrentRetryAt = Time.unscaledTime + 15f; _status = "현재 동기화 영상 다운로드에 실패했습니다. 15초 뒤 자동으로 다시 시도합니다."; } } private void ReconcileRemoteQueue(List<RemoteQueueState> incoming) { if (!UseRemoteNetworkAuthority) { return; } Dictionary<int, QueuedVideo> dictionary = _queue.ToDictionary((QueuedVideo q) => q.Id, (QueuedVideo q) => q); HashSet<int> hashSet = new HashSet<int>(incoming.Select((RemoteQueueState q) => q.Id)); QueuedVideo[] array = _queue.ToArray(); foreach (QueuedVideo queuedVideo in array) { if (!hashSet.Contains(queuedVideo.Id)) { queuedVideo.Removed = true; if (!string.IsNullOrWhiteSpace(queuedVideo.CachedPath)) { _pipeline.DeleteCachedFile(queuedVideo.CachedPath); } } } List<QueuedVideo> list = new List<QueuedVideo>(incoming.Count); foreach (RemoteQueueState item in incoming) { if (TryValidateReceivedYouTubeUrl(item.Url, out var normalizedUrl, $"remote queue reconcile item {item.Id}")) { item.Url = normalizedUrl; if (!dictionary.TryGetValue(item.Id, out var value) || !string.Equals(value.Url, item.Url, StringComparison.Ordinal)) { value = new QueuedVideo { Id = item.Id, Url = item.Url, Title = (string.IsNullOrWhiteSpace(item.Title) ? "제목을 불러오는 중..." : item.Title), PreloadStatus = "미리 받기 대기" }; } else if (!string.IsNullOrWhiteSpace(item.Title) && item.Title != "제목을 불러오는 중...") { value.Title = item.Title; } value.Removed = false; list.Add(value); } } _queue.Clear(); _queue.AddRange(list); if (incoming.Count > 0) { _nextQueueItemId = Math.Max(_nextQueueItemId, incoming.Max((RemoteQueueState x) => x.Id)); } TryStartNextPreload(); } private void ReconcileRemoteHistory(List<RemoteQueueState> incoming) { if (!UseRemoteNetworkAuthority) { return; } QueuedVideo[] array = _history.ToArray(); List<QueuedVideo> list = new List<QueuedVideo>(incoming.Count); foreach (RemoteQueueState item in incoming) { if (!TryValidateReceivedYouTubeUrl(item.Url, out var normalizedRemoteUrl, $"remote history reconcile item {item.Id}")) { continue; } QueuedVideo queuedVideo = array.FirstOrDefault((QueuedVideo h) => !h.Removed && string.Equals(h.Url, normalizedRemoteUrl, StringComparison.Ordinal)); if (queuedVideo == null) { queuedVideo = new QueuedVideo { Id = item.Id, Url = normalizedRemoteUrl, Title = (string.IsNullOrWhiteSpace(item.Title) ? "YouTube 영상" : item.Title), Ready = false, PreloadStarted = true, PreloadStatus = "재생 완료" }; } else { queuedVideo.Id = item.Id; queuedVideo.Url = normalizedRemoteUrl; if (!string.IsNullOrWhiteSpace(item.Title)) { queuedVideo.Title = item.Title; } queuedVideo.Removed = false; queuedVideo.PreloadStatus = "재생 완료"; } list.Add(queuedVideo); } HashSet<string> hashSet = new HashSet<string>(from x in list where !string.IsNullOrWhiteSpace(x.CachedPath) select x.CachedPath, StringComparer.OrdinalIgnoreCase); QueuedVideo[] array2 = array; foreach (QueuedVideo queuedVideo2 in array2) { if (!string.IsNullOrWhiteSpace(queuedVideo2.CachedPath) && !hashSet.Contains(queuedVideo2.CachedPath) && !string.Equals(queuedVideo2.CachedPath, _currentLocalPath, StringComparison.OrdinalIgnoreCase)) { _pipeline.DeleteCachedFile(queuedVideo2.CachedPath); } } _history.Clear(); _history.AddRange(list); if (incoming.Count > 0) { _nextQueueItemId = Math.Max(_nextQueueItemId, incoming.Max((RemoteQueueState x) => x.Id)); } } internal static bool EnsureCreated() { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown if ((Object)(object)Instance == (Object)null) { try { Plugin[] array = Resources.FindObjectsOfTypeAll<Plugin>(); foreach (Plugin plugin in array) { if (!((Object)(object)plugin == (Object)null) && !((Object)(object)((Component)plugin).gameObject == (Object)null)) { Instance = plugin; break; } } } catch { } } if ((Object)(object)Instance != (Object)null) { return false; } GameObject val = new GameObject("LethalTubeTV.PersistentRuntime"); Object.DontDestroyOnLoad((Object)val); val.AddComponent<Plugin>(); return true; } internal static bool TryHandleTvPowerOverride(TVScript tv, bool on) { Plugin instance = Instance; if ((Object)(object)instance == (Object)null || (Object)(object)tv == (Object)null) { return false; } try { instance._tv = tv; instance.EnsureCustomPlayer(); instance.IsolateVanillaVideoPlayer(); instance.SuppressVanillaTvAudio(); tv.tvOn = on; Plugin plugin = instance; if ((object)plugin._setTvScreenMaterialMethod == null) { plugin._setTvScreenMaterialMethod = typeof(TVScript).GetMethod("SetTVScreenMaterial", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } instance._setTvScreenMaterialMethod?.Invoke(tv, new object[1] { on }); if (!on) { instance._idleLogoApplied = false; try { if ((Object)(object)instance._customPlayer != (Object)null && instance._customPlayer.isPlaying) { instance._customPlayer.Pause(); } if ((Object)(object)instance._customAudio != (Object)null && instance._customAudio.isPlaying) { instance._customAudio.Pause(); } } catch { } } else if (!instance._ownsPlayback || !instance._mediaReady) { instance.ShowIdleLogo(force: true); } ManualLogSource logger = instance.Logger; if (logger != null) { logger.LogInfo((object)("TV power handled by LethalTubeTV override: " + (on ? "ON" : "OFF"))); } return true; } catch (Exception ex) { ManualLogSource logger2 = instance.Logger; if (logger2 != null) { logger2.LogWarning((object)("TV power override warning: " + ex.Message)); } try { tv.tvOn = on; instance._setTvScreenMaterialMethod?.Invoke(tv, new object[1] { on }); } catch { } return true; } } internal static bool ShouldBlockQuickMenu() { if (!UiOpen) { return Time.frameCount <= _suppressQuickMenuThroughFrame; } return true; } internal static void SuppressQuickMenuForFrames(int frames = 2) { _suppressQuickMenuThroughFrame = Mathf.Max(_suppressQuickMenuThroughFrame, Time.frameCount + Mathf.Max(1, frames)); } private void Awake() { //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Invalid comparison between Unknown and I4 //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Expected O, but got Unknown //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Expected O, but got Unknown //IL_01f3: Unknown result type (might be due to invalid IL or missing references) //IL_01fd: Expected O, but got Unknown //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_023b: Expected O, but got Unknown //IL_026f: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Expected O, but got Unknown //IL_02ce: Unknown result type (might be due to invalid IL or missing references) //IL_02d8: Expected O, but got Unknown //IL_0338: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Instance != (Object)null && (Object)(object)Instance != (Object)(object)this) { ManualLogSource logger = Logger; if (logger != null) { logger.LogWarning((object)"Duplicate LethalTubeTV runtime detected; destroying duplicate."); } Object.Destroy((Object)(object)((Component)this).gameObject); return; } Instance = this; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); Log = BootstrapPlugin.SharedLog ?? Log; if (Logger == null || Config == null) { ManualLogSource log = Log; if (log != null) { log.LogError((object)"LethalTubeTV runtime started without bootstrap logger/config; destroying runtime."); } if (Instance == this) { Instance = null; } Object.Destroy((Object)(object)((Component)this).gameObject); return; } _openKey = Config.Bind<KeyCode>("General", "OpenKey", (KeyCode)104, "TV / 붐박스 / 크루저 근처에서 YouTube 제어창을 여는 키"); if ((int)_openKey.Value == 287) { _openKey.Value = (KeyCode)104; Config.Save(); Logger.LogInfo((object)"Migrated OpenKey from F6 to H."); } _interactionDistance = Config.Bind<float>("General", "InteractionDistance", 4.5f, "TV / 붐박스 / 크루저 제어창을 열 수 있는 최대 거리"); _tvResolution = Config.Bind<int>("Video", "TVResolution", 480, new ConfigDescription("TV용 YouTube 영상 해상도입니다. 기본값은 480p이며 360/480/720 중 선택할 수 있습니다.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[3] { 360, 480, 720 }), Array.Empty<object>())); _useVanillaBoombox = Config.Bind<bool>("Boombox", "UseVanillaBoombox", false, "기본값(false): 붐박스 배터리 무한, 추가 무게 0kg, 다른 아이템으로 바꿔도 음악이 계속 재생됩니다. true로 변경하면 붐박스 배터리, 무게, 아이템 전환 동작을 바닐라 방식으로 사용합니다."); _youtubeVolume = Config.Bind<float>("Audio", "YouTubeVolume", 0.25f, new ConfigDescription("TV에서 재생되는 YouTube 볼륨. 0.0 = 음소거, 1.0 = 최대", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); _boomboxVolume = Config.Bind<float>("Audio", "BoomboxVolume", 0.6f, new ConfigDescription("붐박스에서 재생되는 YouTube 볼륨", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); _cruiserVolume = Config.Bind<float>("Audio", "CruiserVolume", 0.7f, new ConfigDescription("크루저 라디오에서 재생되는 YouTube 볼륨", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); _cruiserMaxDistance = Config.Bind<float>("Audio", "CruiserMaxDistance", 22f, new ConfigDescription("크루저 YouTube 오디오가 들리는 최대 거리. 크루저 오디오는 항상 완전한 3D 거리 감쇠를 사용함.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 60f), Array.Empty<object>())); _showNowPlayingHud = Config.Bind<bool>("UI", "ShowNowPlayingHud", true, "H 메뉴를 닫았을 때 재생 중인 장치 근처에서 현재 YouTube 제목을 표시"); _nowPlayingHudDistance = Config.Bind<float>("UI", "NowPlayingHudDistance", 10f, new ConfigDescription("현재 재생 HUD가 보이는 장치와의 최대 거리", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 30f), Array.Empty<object>())); string pluginDir = BootstrapPlugin.SharedPluginDir ?? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? Paths.PluginPath; _pipeline = new MediaPipeline(pluginDir, Logger); StartAutomaticToolSetup(); Logger.LogInfo((object)"LethalTubeTV 0.3.1 runtime initialized (target: Lethal Company v81)"); Logger.LogInfo((object)$"Press {_openKey.Value} near the TV, a boombox, or the cruiser radio to open the YouTube controls."); } private void StartAutomaticToolSetup() { if (_toolSetupStarted || _pipeline == null) { return; } _toolSetupStarted = true; _toolSetupFinished = false; _toolSetupError = string.Empty; Task.Run(delegate { ToolSetupResult result; try { result = _pipeline.EnsureTools(delegate(string message) { Logger.LogInfo((object)("[ToolSetup] " + message)); _mainThreadActions.Enqueue(delegate { if (!_busy && !_ownsPlayback && !_mediaReady) { _status = message; } }); }); } catch (Exception ex) { result = ToolSetupResult.Fail(ex.Message); } _mainThreadActions.Enqueue(delegate { _toolSetupFinished = true; if (result.Ok) { _toolSetupError = string.Empty; if (!_busy && !_ownsPlayback && !_mediaReady) { _status = "필수 도구 준비 완료 · YouTube 링크를 입력해 주세요."; } Logger.LogInfo((object)"LethalTubeTV automatic tool setup finished successfully."); } else { _toolSetupError = result.Message; _status = result.Message; Logger.LogError((object)("LethalTubeTV automatic tool setup failed: " + result.Message)); } }); }); } private TVScript FindTvIncludingInactive() { //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) try { TVScript val = Object.FindObjectOfType<TVScript>(); if ((Object)(object)val != (Object)null) { return val; } TVScript[] array = Resources.FindObjectsOfTypeAll<TVScript>(); foreach (TVScript val2 in array) { if (!((Object)(object)val2 == (Object)null) && !((Object)(object)((Component)val2).gameObject == (Object)null)) { Scene scene = ((Component)val2).gameObject.scene; if (((Scene)(ref scene)).IsValid() && ((Scene)(ref scene)).isLoaded) { return val2; } } } } catch (Exception ex) { Logger.LogDebug((object)("TV discovery warning: " + ex.Message)); } return null; } private void Update() { Action result; while (_mainThreadActions.TryDequeue(out result)) { try { result(); } catch (Exception ex) { Logger.LogError((object)ex); } } UpdateNetworkSync(); if (_showWindow) { KeepUiInputCaptured(); } if (Time.unscaledTime >= _nextTvSearchTime) { _nextTvSearchTime = Time.unscaledTime + 2f; if ((Object)(object)_tv == (Object)null) { _tv = FindTvIncludingInactive(); } } if ((Object)(object)_tv != (Object)null) { try { EnsureCustomPlayer(); IsolateVanillaVideoPlayer(); SuppressVanillaTvAudio(); if ((Object)(object)_customAudio != (Object)null) { _customAudio.volume = Mathf.Clamp01(_youtubeVolume.Value); } if (!_haveObservedTvPower) { _lastObservedTvOn = _tv.tvOn; _haveObservedTvPower = true; } bool tvOn = _tv.tvOn; if (tvOn != _lastObservedTvOn) { _lastObservedTvOn = tvOn; _idleLogoApplied = false; Logger.LogInfo((object)("TV power changed by game/player: " + (tvOn ? "ON" : "OFF"))); } if (!_ownsPlayback || !_mediaReady) { if (tvOn) { _setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { true }); ShowIdleLogo(); } else { _setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { false }); _idleLogoApplied = false; } } else { _idleLogoApplied = false; } } catch (Exception ex2) { Logger.LogDebug((object)("TV presentation maintenance warning: " + ex2.Message)); } } UpdateSyncedPlaybackDevices(); } private void UpdateSyncedPlaybackDevices() { if (!_ownsPlayback || !_mediaReady || string.IsNullOrWhiteSpace(_currentLocalPath)) { if (_sharedPlaybackRunning) { PauseSharedPlaybackClock(); } return; } if (Time.unscaledTime >= _nextDeviceSyncSearchTime) { _nextDeviceSyncSearchTime = Time.unscaledTime + 0.75f; DiscoverSyncedDevices(); } bool flag = (Object)(object)_tv != (Object)null && _tv.tvOn; bool flag2 = false; KeyValuePair<int, SyncedAudioDevice>[] array = _boomboxSync.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<int, SyncedAudioDevice> keyValuePair = array[i]; SyncedAudioDevice value = keyValuePair.Value; if ((Object)(object)value?.Owner == (Object)null) { _boomboxSync.Remove(keyValuePair.Key); continue; } Component owner = value.Owner; BoomboxItem val = (BoomboxItem)(object)((owner is BoomboxItem) ? owner : null); if (val != null && val.isPlayingMusic) { flag2 = true; } } bool flag3 = false; array = _cruiserSync.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<int, SyncedAudioDevice> keyValuePair2 = array[i]; SyncedAudioDevice value2 = keyValuePair2.Value; if ((Object)(object)value2?.Owner == (Object)null) { _cruiserSync.Remove(keyValuePair2.Key); continue; } Component owner2 = value2.Owner; VehicleController val2 = (VehicleController)(object)((owner2 is VehicleController) ? owner2 : null); if (val2 != null && IsCruiserRadioOn(val2)) { flag3 = true; } } bool flag4 = (Object)(object)_syncedAudioClip != (Object)null && string.Equals(_syncedAudioClipSourcePath, _currentLocalPath, StringComparison.OrdinalIgnoreCase); bool num = flag && (Object)(object)_customPlayer != (Object)null && _customPlayer.isPrepared && flag4; bool flag5 = flag2 && flag4; bool flag6 = flag3 && flag4; bool flag7 = num || flag5 || flag6; if (UseRemoteNetworkAuthority) { ApplyRemoteNetworkClock(); } else if (flag7) { StartSharedPlaybackClock(); } else { PauseSharedPlaybackClock(); } double sharedPlaybackTime = GetSharedPlaybackTime(); double currentMediaDuration = GetCurrentMediaDuration(); if (!UseRemoteNetworkAuthority && _sharedPlaybackRunning && !_handlingSharedTrackEnd && currentMediaDuration > 0.1 && sharedPlaybackTime >= currentMediaDuration - 0.04) { _handlingSharedTrackEnd = true; PauseSharedPlaybackClock(); _sharedPlaybackTime = currentMediaDuration; try { Logger.LogInfo((object)$"Shared track finished. time={sharedPlaybackTime:0.00}s, duration={currentMediaDuration:0.00}s, queueCount={_queue.Count}"); HandleVideoFinished(); return; } finally { _handlingSharedTrackEnd = false; } } SyncTvDevice(flag, _sharedPlaybackRunning, sharedPlaybackTime); array = _boomboxSync.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<int, SyncedAudioDevice> keyValuePair3 = array[i]; SyncedAudioDevice value3 = keyValuePair3.Value; if ((Object)(object)value3?.Owner == (Object)null) { _boomboxSync.Remove(keyValuePair3.Key); continue; } Component owner3 = value3.Owner; BoomboxItem val3 = (BoomboxItem)(object)((owner3 is BoomboxItem) ? owner3 : null); bool desiredOn = (Object)(object)val3 != (Object)null && val3.isPlayingMusic; SyncAudioDevice(value3, desiredOn, _sharedPlaybackRunning, sharedPlaybackTime); } array = _cruiserSync.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<int, SyncedAudioDevice> keyValuePair4 = array[i]; SyncedAudioDevice value4 = keyValuePair4.Value; if ((Object)(object)value4?.Owner == (Object)null) { _cruiserSync.Remove(keyValuePair4.Key); continue; } Component owner4 = value4.Owner; VehicleController cruiser = (VehicleController)(object)((owner4 is VehicleController) ? owner4 : null); bool desiredOn2 = IsCruiserRadioOn(cruiser); SyncAudioDevice(value4, desiredOn2, _sharedPlaybackRunning, sharedPlaybackTime); } } private double GetSharedPlaybackTime() { if (!_sharedPlaybackRunning) { return Math.Max(0.0, _sharedPlaybackTime); } return Math.Max(0.0, _sharedPlaybackTime + (double)(Time.unscaledTime - _sharedPlaybackAnchorUnscaledTime)); } private void StartSharedPlaybackClock() { if (!_sharedPlaybackRunning) { _sharedPlaybackAnchorUnscaledTime = Time.unscaledTime; _sharedPlaybackRunning = true; Logger.LogInfo((object)$"Shared playback clock START. time={_sharedPlaybackTime:0.00}s"); } } private void PauseSharedPlaybackClock() { if (_sharedPlaybackRunning) { _sharedPlaybackTime = GetSharedPlaybackTime(); _sharedPlaybackRunning = false; Logger.LogInfo((object)$"Shared playback clock PAUSE. time={_sharedPlaybackTime:0.00}s"); } } private void ResetSharedPlaybackClock(double time = 0.0) { _sharedPlaybackTime = Math.Max(0.0, time); _sharedPlaybackAnchorUnscaledTime = Time.unscaledTime; _sharedPlaybackRunning = false; _handlingSharedTrackEnd = false; } private double GetCurrentMediaDuration() { try { if ((Object)(object)_syncedAudioClip != (Object)null && _syncedAudioClip.length > 0.1f) { return _syncedAudioClip.length; } } catch { } try { if ((Object)(object)_customPlayer != (Object)null && _customPlayer.isPrepared && _customPlayer.length > 0.1) { return _customPlayer.length; } } catch { } return 0.0; } private void SyncTvDevice(bool desiredOn, bool sharedPlaying, double sharedTime) { if ((Object)(object)_tv == (Object)null || (Object)(object)_customPlayer == (Object)null) { return; } try { if (!desiredOn) { if (_customPlayer.isPlaying) { _customPlayer.Pause(); } if ((Object)(object)_customAudio != (Object)null && _customAudio.isPlaying) { _customAudio.Pause(); } _customVideoAwaitingSeekFrame = false; _customVideoSeekRecoveryAttempts = 0; _customVideoSeekGraceUntil = 0f; _setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { false }); return; } SuppressVanillaTvAudio(); _setTvScreenMaterialMethod?.Invoke(_tv, new object[1] { true }); if (!EnsureCustomVideoPlayerActive()) { return; } if ((Object)(object)_tvScreenTargetTexture != (Object)null && (Object)(object)_customPlayer.targetTexture != (Object)(object)_tvScreenTargetTexture) { _customPlayer.targetTexture = _tvScreenTargetTexture; } if (!_customPlayer.isPrepared) { return; } if ((Object)(object)_syncedAudioClip == (Object)null || !string.Equals(_syncedAudioClipSourcePath, _currentLocalPath, StringComparison.OrdinalIgnoreCase)) { BeginLoadSyncedAudioClip(_currentLocalPath); return; } if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)(object)_syncedAudioClip) { _customAudio.Stop(); _customAudio.clip = _syncedAudioClip; _customAudio.loop = false; Logger.LogInfo((object)($"TV synchronized AudioClip attached. length={_syncedAudioClip.length:0.00}s, " + $"channels={_syncedAudioClip.channels}, frequency={_syncedAudioClip.frequency}")); } if ((Object)(object)_customAudio != (Object)null) { ((Behaviour)_customAudio).enabled = true; _customAudio.mute = false; _customAudio.volume = Mathf.Clamp01(_youtubeVolume.Value); } double num = Math.Max(0.01, _customPlayer.length); double num2 = (((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)null) ? Math.Max(0.01, _customAudio.clip.length) : num); double num3 = Math.Max(0.0, Math.Min(sharedTime, num - 0.01)); double num4 = Math.Max(0.0, Math.Min(sharedTime, num2 - 0.01)); _customPlayer.timeReference = (VideoTimeReference)0; _customPlayer.playbackSpeed = 1f; if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)null && Math.Abs((double)_customAudio.time - num4) > 0.06) { _customAudio.time = (float)num4; } if (!sharedPlaying) { if (_customPlayer.isPlaying) { _customPlayer.Pause(); } if ((Object)(object)_customAudio != (Object)null && _customAudio.isPlaying) { _customAudio.Pause(); } return; } float unscaledTime = Time.unscaledTime; if (!_customPlayer.isPlaying) { if (!EnsureCustomVideoPlayerActive()) { Logger.LogWarning((object)"TV VideoPlayer could not be activated; keeping shared audio playback alive."); return; } _customVideoFrameBeforeSeek = _customPlayer.frame; if (_customPlayer.canSetTime) { _customPlayer.time = num3; } _customPlayer.Play(); _customVideoAwaitingSeekFrame = true; _customVideoSeekGraceUntil = unscaledTime + 2.5f; _nextCustomVideoResyncTime = unscaledTime + 3f; _customVideoSeekRecoveryAttempts = 0; Logger.LogInfo((object)($"TV video join requested (single seek). target={num3:0.00}s, " + $"actual={_customPlayer.time:0.00}s, frame={_customPlayer.frame}, " + $"isPlaying={_customPlayer.isPlaying}, enabled={((Behaviour)_customPlayer).enabled}, " + $"active={((Component)_customPlayer).gameObject.activeInHierarchy}")); } long frame = _customPlayer.frame; if (frame != _lastCustomVideoFrame) { _lastCustomVideoFrame = frame; _lastCustomVideoFrameAdvanceTime = unscaledTime; if (_customVideoAwaitingSeekFrame && frame >= 0 && frame != _customVideoFrameBeforeSeek) { _customVideoAwaitingSeekFrame = false; _customVideoSeekRecoveryAttempts = 0; _nextCustomVideoResyncTime = unscaledTime + 3f; Logger.LogInfo((object)($"TV video seek caught up. frame={frame}, actual={_customPlayer.time:0.00}s, " + $"target={num3:0.00}s")); } } if (_customVideoAwaitingSeekFrame) { if (unscaledTime >= _customVideoSeekGraceUntil && unscaledTime >= _nextCustomVideoResyncTime && _customPlayer.canSetTime && _customVideoSeekRecoveryAttempts < 2) { _customVideoSeekRecoveryAttempts++; _customVideoFrameBeforeSeek = _customPlayer.frame; _customPlayer.Pause(); _customPlayer.time = num3; _customPlayer.Play(); _customVideoSeekGraceUntil = unscaledTime + 2.5f; _nextCustomVideoResyncTime = unscaledTime + 3f; Logger.LogWarning((object)($"TV video seek retry {_customVideoSeekRecoveryAttempts}/2. target={num3:0.00}s, " + $"frame={_customPlayer.frame}, isPlaying={_customPlayer.isPlaying}")); } } else if (unscaledTime >= _nextCustomVideoResyncTime && _customPlayer.canSetTime) { double num5 = Math.Abs(_customPlayer.time - num3); if (num5 > 1.25) { _customVideoFrameBeforeSeek = _customPlayer.frame; _customPlayer.time = num3; _customVideoAwaitingSeekFrame = true; _customVideoSeekGraceUntil = unscaledTime + 2f; _nextCustomVideoResyncTime = unscaledTime + 3f; _customVideoSeekRecoveryAttempts = 0; Logger.LogInfo((object)($"TV video drift resync requested. drift={num5:0.00}s, " + $"target={num3:0.00}s, actual={_customPlayer.time:0.00}s")); } else { _nextCustomVideoResyncTime = unscaledTime + 1f; } } if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip != (Object)null && !_customAudio.isPlaying) { _customAudio.time = (float)num4; _customAudio.Play(); Logger.LogInfo((object)($"TV AudioClip joined shared clock. target={num4:0.00}s, " + $"actual={_customAudio.time:0.00}s")); } } catch (Exception ex) { Logger.LogDebug((object)("TV shared sync warning: " + ex.Message)); } } private void DiscoverSyncedDevices() { try { BoomboxItem[] array = Object.FindObjectsOfType<BoomboxItem>(); foreach (BoomboxItem val in array) { if (!((Object)(object)val == (Object)null) && !((Object)(object)val.boomboxAudio == (Object)null)) { int instanceID = ((Object)val).GetInstanceID(); if (!_boomboxSync.ContainsKey(instanceID)) { _boomboxSync[instanceID] = CreateSyncedAudioDevice((Component)(object)val, val.boomboxAudio, null, "Boombox"); Logger.LogInfo((object)$"Discovered boombox for synchronized YouTube audio. id={instanceID}"); } } } VehicleController[] array2 = Object.FindObjectsOfType<VehicleController>(); foreach (VehicleController val2 in array2) { if ((Object)(object)val2 == (Object)null || (Object)(object)val2.radioAudio == (Object)null) { continue; } int instanceID2 = ((Object)val2).GetInstanceID(); if (!_cruiserSync.ContainsKey(instanceID2)) { AudioSource auxAudio = null; try { auxAudio = val2.radioInterference; } catch { } _cruiserSync[instanceID2] = CreateSyncedAudioDevice((Component)(object)val2, val2.radioAudio, auxAudio, "Cruiser radio"); Logger.LogInfo((object)$"Discovered cruiser radio for synchronized YouTube audio. id={instanceID2}"); } } } catch (Exception ex) { Logger.LogDebug((object)("Device discovery warning: " + ex.Message)); } } private SyncedAudioDevice CreateSyncedAudioDevice(Component owner, AudioSource vanillaAudio, AudioSource auxAudio, string label) { SyncedAudioDevice syncedAudioDevice = new SyncedAudioDevice { Owner = owner, Label = label, VanillaAudio = vanillaAudio, AuxAudio = auxAudio }; GameObject val = (((Object)(object)vanillaAudio != (Object)null) ? ((Component)vanillaAudio).gameObject : owner.gameObject); AudioSource val2 = val.AddComponent<AudioSource>(); val2.playOnAwake = false; val2.loop = false; val2.mute = true; CopyAudioSourceSettings(vanillaAudio, val2); if (owner is VehicleController) { val2.spatialBlend = 1f; val2.rolloffMode = (AudioRolloffMode)0; val2.minDistance = 2f; val2.maxDistance = Mathf.Max(5f, _cruiserMaxDistance.Value); val2.dopplerLevel = 0f; val2.spread = 0f; } val2.volume = GetSyncedDeviceVolume(syncedAudioDevice); syncedAudioDevice.CustomAudio = val2; Logger.LogInfo((object)("Created " + label + " synchronized AudioSource. host=" + ((Object)val).name + ", " + $"volume={val2.volume:0.###}, spatialBlend={val2.spatialBlend:0.###}, " + $"minDistance={val2.minDistance:0.##}, maxDistance={val2.maxDistance:0.##}")); return syncedAudioDevice; } private static void CopyAudioSourceSettings(AudioSource source, AudioSource destination) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)source == (Object)null) && !((Object)(object)destination == (Object)null)) { destination.outputAudioMixerGroup = source.outputAudioMixerGroup; destination.pitch = source.pitch; destination.priority = source.priority; destination.spatialBlend = source.spatialBlend; destination.dopplerLevel = source.dopplerLevel; destination.spread = source.spread; destination.rolloffMode = source.rolloffMode; destination.minDistance = source.minDistance; destination.maxDistance = source.maxDistance; destination.bypassEffects = source.bypassEffects; destination.bypassListenerEffects = source.bypassListenerEffects; destination.bypassReverbZones = source.bypassReverbZones; } } private float GetSyncedDeviceVolume(SyncedAudioDevice target) { if (target?.Owner is BoomboxItem) { return Mathf.Clamp01(_boomboxVolume.Value); } if (target?.Owner is VehicleController) { return Mathf.Clamp01(_cruiserVolume.Value); } return Mathf.Clamp01(_youtubeVolume.Value); } private void ApplySyncedDeviceVolumes() { foreach (SyncedAudioDevice value in _boomboxSync.Values) { if ((Object)(object)value?.CustomAudio != (Object)null) { value.CustomAudio.volume = Mathf.Clamp01(_boomboxVolume.Value); } } foreach (SyncedAudioDevice value2 in _cruiserSync.Values) { if ((Object)(object)value2?.CustomAudio != (Object)null) { value2.CustomAudio.volume = Mathf.Clamp01(_cruiserVolume.Value); value2.CustomAudio.spatialBlend = 1f; value2.CustomAudio.rolloffMode = (AudioRolloffMode)0; value2.CustomAudio.minDistance = 2f; value2.CustomAudio.maxDistance = Mathf.Max(5f, _cruiserMaxDistance.Value); value2.CustomAudio.dopplerLevel = 0f; } } } private bool IsCruiserRadioOn(VehicleController cruiser) { if ((Object)(object)cruiser == (Object)null) { return false; } try { if ((object)_cruiserRadioOnField == null) { _cruiserRadioOnField = typeof(VehicleController).GetField("radioOn", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } if (_cruiserRadioOnField != null && _cruiserRadioOnField.GetValue(cruiser) is bool result) { return result; } } catch { } try { return (Object)(object)cruiser.radioAudio != (Object)null && cruiser.radioAudio.isPlaying; } catch { return false; } } private void SyncAudioDevice(SyncedAudioDevice target, bool desiredOn, bool masterPlaying, double masterTime) { if (target == null || (Object)(object)target.Owner == (Object)null || (Object)(object)target.CustomAudio == (Object)null) { return; } if (!desiredOn) { if (target.CustomAudio.isPlaying) { target.CustomAudio.Pause(); } target.CustomAudio.mute = true; RestoreDeviceVanillaAudio(target, resumeIfOn: false); return; } SuppressDeviceVanillaAudio(target); if (target.Owner is VehicleController) { target.CustomAudio.spatialBlend = 1f; target.CustomAudio.rolloffMode = (AudioRolloffMode)0; target.CustomAudio.minDistance = 2f; target.CustomAudio.maxDistance = Mathf.Max(5f, _cruiserMaxDistance.Value); target.CustomAudio.dopplerLevel = 0f; } if ((Object)(object)_syncedAudioClip == (Object)null || !string.Equals(_syncedAudioClipSourcePath, _currentLocalPath, StringComparison.OrdinalIgnoreCase)) { BeginLoadSyncedAudioClip(_currentLocalPath); return; } if (!string.Equals(target.LoadedPath, _currentLocalPath, StringComparison.OrdinalIgnoreCase) || (Object)(object)target.CustomAudio.clip != (Object)(object)_syncedAudioClip) { PrepareSyncedAudioDevice(target, _currentLocalPath); } if (!target.Prepared || (Object)(object)target.CustomAudio.clip == (Object)null) { return; } ((Behaviour)target.CustomAudio).enabled = true; target.CustomAudio.volume = GetSyncedDeviceVolume(target); if (!masterPlaying) { if (target.CustomAudio.isPlaying) { target.CustomAudio.Pause(); } target.CustomAudio.mute = true; return; } try { target.CustomAudio.mute = false; double num = Math.Max(0.01, target.CustomAudio.clip.length); double num2 = Math.Max(0.0, Math.Min(masterTime, num - 0.01)); if (Math.Abs((double)target.CustomAudio.time - num2) > 0.2) { target.CustomAudio.time = (float)num2; } if (!target.CustomAudio.isPlaying) { target.CustomAudio.Play(); Logger.LogInfo((object)$"{target.Label} synchronized AudioClip playback started. time={num2:0.00}s, clip={((Object)target.CustomAudio.clip).name}"); } } catch (Exception ex) { Logger.LogDebug((object)(target.Label + " sync warning: " + ex.Message)); } } private void PrepareSyncedAudioDevice(SyncedAudioDevice target, string filePath) { if (target == null || (Object)(object)target.CustomAudio == (Object)null || (Object)(object)_syncedAudioClip == (Object)null) { return; } try { target.CustomAudio.Stop(); target.CustomAudio.clip = _syncedAudioClip; target.CustomAudio.loop = false; target.CustomAudio.volume = GetSyncedDeviceVolume(target); target.CustomAudio.mute = false; target.LoadedPath = filePath; target.Prepared = true; Logger.LogInfo((object)$"{target.Label} synchronized AudioClip prepared. clip={((Object)_syncedAudioClip).name}, length={_syncedAudioClip.length:0.00}s, channels={_syncedAudioClip.channels}, frequency={_syncedAudioClip.frequency}"); } catch (Exception ex) { target.Prepared = false; Logger.LogWarning((object)("Failed to prepare " + target.Label + " AudioClip: " + ex.Message)); } } private void ResetSyncedAudioClipForNewTrack() { _syncedAudioClipLoadToken++; _syncedAudioClipLoading = false; _syncedAudioClipSourcePath = string.Empty; try { if ((Object)(object)_customAudio != (Object)null && (Object)(object)_customAudio.clip == (Object)(object)_syncedAudioClip) { _customAudio.Stop(); _customAudio.clip = null; } foreach (SyncedAudioDevice item in _boomboxSync.Values.Concat(_cruiserSync.Values)) { if (!((Object)(object)item?.CustomAudio == (Object)null)) { if ((Object)(object)item.CustomAudio.clip == (Object)(object)_syncedAudioClip) { item.CustomAudio.Stop(); item.CustomAudio.clip = null; } item.Prepared = false; item.LoadedPath = string.Empty; } } if ((Object)(object)_syncedAudioClip != (Object)null) { Object.Destroy((Object)(object)_syncedAudioClip); _syncedAudioClip = null; } } catch (Exception ex) { Logger.LogDebug((object)("Synchronized AudioClip reset warning: " + ex.Message)); } } private void BeginLoadSyncedAudioClip(string mp4Path) { if (string.IsNullOrWhiteSpace(mp4Path) || ((Object)(object)_syncedAudioClip != (Object)null && string.Equals(_syncedAudioClipSourcePath, mp4Path, StringComparison.OrdinalI