Decompiled source of RecordingArchiver v1.6.2

BepInEx/plugins/RecordingArchiver/RecordingArchiver.Core.dll

Decompiled a day ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;

[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("RecordingArchiver.Core")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.6.2.0")]
[assembly: AssemblyInformationalVersion("1.6.2")]
[assembly: AssemblyProduct("RecordingArchiver.Core")]
[assembly: AssemblyTitle("RecordingArchiver.Core")]
[assembly: AssemblyVersion("1.6.2.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace RecordingArchiver.Core
{
	public sealed class ArchiveEngine : IDisposable
	{
		private sealed class SourceFile
		{
			public string Path = "";

			public string Relative = "";

			public string Root = "";

			public string RetentionId = "";

			public ArchiveContext? Context;

			public bool ProvenContext;
		}

		private sealed class Observation
		{
			public long Length;

			public long WriteTicks;

			public DateTime StableSince;

			public DateTime FirstSeen;

			public DateTime LastSnapshot;

			public DateTime RetryAfter;

			public int Failures;
		}

		private readonly ArchiveOptions options;

		private readonly Action<string> reportError;

		private readonly FileStream processLock;

		private readonly string statePath;

		private readonly string sessionId;

		private readonly Dictionary<string, Observation> observations = new Dictionary<string, Observation>(StringComparer.OrdinalIgnoreCase);

		private readonly HashSet<string> verified = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		private readonly HashSet<RecordingMetadata> changedRecordings = new HashSet<RecordingMetadata>();

		private readonly List<string> completedJournals = new List<string>();

		private readonly HashSet<string> persistedRecordings = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		private readonly HashSet<RecordingMetadata> unpersistedEvidence = new HashSet<RecordingMetadata>();

		private readonly Dictionary<string, RecordingMetadata> recordings;

		private readonly ArchiveState state;

		private bool dirty;

		private ArchiveContext? currentContext;

		private Dictionary<string, ArchiveContext> liveContexts = new Dictionary<string, ArchiveContext>();

		public ArchiveState State => state;

		public ArchiveEngine(ArchiveOptions options, Action<string> reportError)
		{
			this.options = options;
			this.reportError = reportError;
			options.SourceDirectory = ArchivePaths.NormalizeRoot(options.SourceDirectory);
			options.OutputDirectory = ArchivePaths.NormalizeRoot(options.OutputDirectory);
			options.RetentionDirectory = ArchivePaths.NormalizeRoot(string.IsNullOrEmpty(options.RetentionDirectory) ? (options.SourceDirectory + "_RA") : options.RetentionDirectory);
			new SourceRetention(options.SourceDirectory, options.RetentionDirectory);
			if (options.RetentionDirectory.Equals(options.OutputDirectory, StringComparison.OrdinalIgnoreCase) || ArchivePaths.Contains(options.RetentionDirectory, options.OutputDirectory) || ArchivePaths.Contains(options.OutputDirectory, options.RetentionDirectory))
			{
				throw new ArgumentException("Output and retention directories must not overlap.");
			}
			if (options.SourceDirectory.Equals(options.OutputDirectory, StringComparison.OrdinalIgnoreCase) || ArchivePaths.Contains(options.SourceDirectory, options.OutputDirectory) || ArchivePaths.Contains(options.OutputDirectory, options.SourceDirectory))
			{
				throw new ArgumentException("Source and output directories must not overlap.");
			}
			if (options.StabilityPeriod <= TimeSpan.Zero || options.SnapshotAfter < options.StabilityPeriod)
			{
				throw new ArgumentException("Invalid stabilization periods.");
			}
			ArchivePaths.RejectLinks(options.SourceDirectory, options.SourceDirectory);
			ArchivePaths.RejectLinks(options.OutputDirectory, options.OutputDirectory);
			Directory.CreateDirectory(options.OutputDirectory);
			processLock = new FileStream(ArchivePaths.Resolve(options.OutputDirectory, ".recordingarchiver.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
			try
			{
				statePath = ArchivePaths.Resolve(options.OutputDirectory, ".recordingarchiver-state.json");
				if (!File.Exists(statePath) && File.Exists(statePath + ".ra-tmp"))
				{
					JsonStore.Read<ArchiveState>(statePath + ".ra-tmp");
					File.Move(statePath + ".ra-tmp", statePath);
				}
				state = (File.Exists(statePath) ? JsonStore.Read<ArchiveState>(statePath) : new ArchiveState
				{
					SourceRoot = options.SourceDirectory
				});
				if ((state.SchemaVersion != 1 && state.SchemaVersion != 2) || state.Recordings == null || !string.Equals(state.SourceRoot, options.SourceDirectory, StringComparison.OrdinalIgnoreCase))
				{
					throw new InvalidDataException("State is incompatible with this source. Use a separate output directory.");
				}
				recordings = new Dictionary<string, RecordingMetadata>(StringComparer.OrdinalIgnoreCase);
				state.SchemaVersion = 2;
				foreach (RecordingMetadata recording in state.Recordings)
				{
					recording.Context?.Validate();
					if (string.IsNullOrEmpty(recording.Key))
					{
						recording.Key = recording.OriginalFolderId;
					}
					ArchivePaths.Resolve(options.OutputDirectory, recording.RelativeDirectory);
					if (recording.Files == null || recording.Clips == null)
					{
						throw new InvalidDataException("Invalid recording state.");
					}
					foreach (ArchivedFile file in recording.Files)
					{
						ArchivePaths.Resolve(options.OutputDirectory, file.Destination);
						ArchivePaths.Resolve(options.SourceDirectory, file.SourceRelativePath);
					}
					recordings.Add(recording.Key, recording);
					persistedRecordings.Add(recording.Key);
				}
				sessionId = DateTime.UtcNow.ToString("yyyy-MM-dd_HH-mm-ss") + "_" + Guid.NewGuid().ToString("N").Substring(0, 12);
				changedRecordings.UnionWith(state.Recordings);
				dirty = state.Recordings.Count > 0;
				RecoverCopies();
			}
			catch
			{
				processLock.Dispose();
				throw;
			}
		}

		public void Observe(ClipEvidence evidence)
		{
			if (!Guid.TryParse(evidence.VideoId, out var result) || result == Guid.Empty || !Guid.TryParse(evidence.ClipId, out var result2) || result2 == Guid.Empty || evidence.Order < 0)
			{
				throw new ArgumentException("Invalid recording evidence.");
			}
			evidence.VideoId = result.ToString();
			evidence.ClipId = result2.ToString();
			evidence.Context = evidence.Context ?? currentContext;
			RecordingMetadata recording = GetRecording(evidence.VideoId, evidence.Context);
			ClipEvidence clipEvidence = recording.Clips.FirstOrDefault((ClipEvidence c) => c.ClipId == evidence.ClipId);
			if (clipEvidence != null)
			{
				evidence.CameraOwner = evidence.CameraOwner ?? clipEvidence.CameraOwner;
				evidence.Holder = evidence.Holder ?? clipEvidence.Holder;
				evidence.Encoder = clipEvidence.Encoder ?? evidence.Encoder;
				if (evidence.CameraId.Length == 0)
				{
					evidence.CameraId = clipEvidence.CameraId;
				}
				if (SameEvidence(clipEvidence, evidence))
				{
					return;
				}
				recording.Clips.Remove(clipEvidence);
			}
			recording.Clips.Add(evidence);
			recording.Clips.Sort((ClipEvidence a, ClipEvidence b) => a.Order.CompareTo(b.Order));
			recording.OwnerConflict = (from c in recording.Clips
				where c.CameraOwner != null
				select c.CameraOwner.Id).Distinct().Skip(1).Any();
			dirty = true;
			changedRecordings.Add(recording);
			unpersistedEvidence.Add(recording);
		}

		private static bool SameEvidence(ClipEvidence a, ClipEvidence b)
		{
			if (a.Order == b.Order && a.CameraId == b.CameraId && a.IsRecording == b.IsRecording && a.IsEncoded == b.IsEncoded && a.IsReceived == b.IsReceived && a.IsValid == b.IsValid && SameIdentity(a.CameraOwner, b.CameraOwner) && SameIdentity(a.Holder, b.Holder))
			{
				return SameIdentity(a.Encoder, b.Encoder);
			}
			return false;
		}

		private static bool SameIdentity(PlayerIdentity? a, PlayerIdentity? b)
		{
			if (a?.Id == b?.Id && a?.Name == b?.Name)
			{
				return a?.Evidence == b?.Evidence;
			}
			return false;
		}

		public void SetContext(ArchiveContext? context)
		{
			context?.Validate();
			currentContext = context;
		}

		public void SetSourceContexts(Dictionary<string, ArchiveContext> contexts)
		{
			liveContexts = contexts;
		}

		public void Scan(DateTime utcNow, CancellationToken cancellation = default(CancellationToken))
		{
			HashSet<string> present = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (SourceFile item in EnumerateSources())
			{
				cancellation.ThrowIfCancellationRequested();
				string path = item.Path;
				string relative = item.Relative;
				string text = path;
				present.Add(text);
				try
				{
					ArchivePaths.RejectLinks(item.Root, path);
					ArchivePaths.Resolve(options.SourceDirectory, relative);
					FileInfo fileInfo = new FileInfo(path);
					long length = fileInfo.Length;
					long writeTicks = fileInfo.LastWriteTimeUtc.Ticks;
					if (!observations.TryGetValue(text, out Observation value))
					{
						value = new Observation
						{
							Length = length,
							WriteTicks = writeTicks,
							StableSince = utcNow,
							FirstSeen = utcNow
						};
						observations.Add(text, value);
					}
					if (value.Length != length || value.WriteTicks != writeTicks)
					{
						value.Length = length;
						value.WriteTicks = writeTicks;
						value.StableSince = utcNow;
						value.Failures = 0;
						value.RetryAfter = DateTime.MinValue;
					}
					if (utcNow < value.RetryAfter)
					{
						continue;
					}
					int num;
					if (utcNow - value.StableSince >= options.StabilityPeriod)
					{
						num = ((utcNow - fileInfo.LastWriteTimeUtc >= options.StabilityPeriod) ? 1 : 0);
						if (num != 0)
						{
							goto IL_01e6;
						}
					}
					else
					{
						num = 0;
					}
					if (!(utcNow - value.FirstSeen >= options.SnapshotAfter))
					{
						goto IL_01e6;
					}
					int num2 = ((utcNow - value.LastSnapshot >= options.SnapshotAfter) ? 1 : 0);
					goto IL_01e7;
					IL_01e6:
					num2 = 0;
					goto IL_01e7;
					IL_01e7:
					bool flag = (byte)num2 != 0;
					if (num == 0 && !flag)
					{
						continue;
					}
					string[] array = relative.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
					if (!((array.Length == 2 && array[1].Equals("fullRecording.webm", StringComparison.OrdinalIgnoreCase)) ? (!options.ArchiveFullRecordings) : (!options.ArchiveFragments)))
					{
						string folder = ((array.Length > 1) ? array[0] : "_unclassified");
						RecordingMetadata recordingMetadata = ResolveRecording(folder, array, relative, length, writeTicks, item);
						ArchivedFile archivedFile = recordingMetadata.Files.LastOrDefault((ArchivedFile f) => f.SourceRelativePath == relative && f.Length == length && f.SourceWriteTicks == writeTicks && f.Status == "byte-verified-media-unverified");
						if (archivedFile == null || !VerifyExisting(archivedFile))
						{
							Copy(path, relative, array, recordingMetadata, length, writeTicks, flag, utcNow, cancellation, item.RetentionId);
							value.LastSnapshot = utcNow;
							value.Failures = 0;
							value.RetryAfter = DateTime.MinValue;
						}
					}
				}
				catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is SecurityException)
				{
					if (observations.TryGetValue(text, out Observation value2))
					{
						value2.Failures++;
						value2.RetryAfter = utcNow.AddSeconds((value2.Failures <= 5) ? Math.Min(60.0, Math.Pow(2.0, value2.Failures)) : 300.0);
						int num3 = ex.HResult & 0xFFFF;
						string text2 = ((num3 == 112 || num3 == 39) ? "Destination disk is full; free space before retrying." : "Check destination permissions, free space and path length.");
						if (value2.Failures <= 5)
						{
							reportError($"Archive copy failed ({ex.GetType().Name}, code {num3}, retry {value2.Failures}/5; then 5-minute cooldown). {text2} Originals remain untouched.");
						}
					}
					else
					{
						reportError("Cannot inspect recording (" + ex.GetType().Name + "). Originals remain untouched.");
					}
				}
			}
			string[] array2 = observations.Keys.Where((string k) => !present.Contains(k)).ToArray();
			foreach (string key in array2)
			{
				observations.Remove(key);
			}
			if (dirty)
			{
				Persist();
			}
		}

		private RecordingMetadata ResolveRecording(string folder, string[] parts, string relative, long length, long ticks, SourceFile source)
		{
			bool provenContext = source.ProvenContext;
			RecordingMetadata recordingMetadata = state.Recordings.LastOrDefault((RecordingMetadata r) => (!provenContext || r.Context?.ScopeKey == source.Context.ScopeKey) && r.OriginalFolderId.Equals(folder, StringComparison.OrdinalIgnoreCase) && r.Files.Any((ArchivedFile f) => f.SourceRelativePath == relative && f.Length == length && f.SourceWriteTicks == ticks));
			if (source.RetentionId.Length > 0 && recordingMetadata != null)
			{
				return recordingMetadata;
			}
			if (parts.Length >= 3 && Guid.TryParse(parts[1], out var _))
			{
				List<RecordingMetadata> list = state.Recordings.Where((RecordingMetadata r) => (!provenContext || r.Context?.ScopeKey == source.Context.ScopeKey) && r.OriginalFolderId.Equals(folder, StringComparison.OrdinalIgnoreCase) && r.Clips.Any((ClipEvidence c) => c.ClipId.Equals(parts[1], StringComparison.OrdinalIgnoreCase))).ToList();
				RecordingMetadata recordingMetadata2 = list.LastOrDefault((RecordingMetadata r) => r.Context?.ScopeKey == source.Context?.ScopeKey);
				if (recordingMetadata2 != null)
				{
					return recordingMetadata2;
				}
				if (list.Count == 1)
				{
					return list[0];
				}
			}
			return recordingMetadata ?? GetRecording(folder, source.Context);
		}

		private IEnumerable<SourceFile> EnumerateSources()
		{
			foreach (string item in EnumerateFiles(options.SourceDirectory))
			{
				string relativePath = Path.GetRelativePath(options.SourceDirectory, item);
				ArchiveContext archiveContext = ContextForPath(liveContexts, relativePath);
				yield return new SourceFile
				{
					Path = item,
					Root = options.SourceDirectory,
					Relative = relativePath,
					Context = (archiveContext ?? currentContext),
					ProvenContext = (archiveContext != null)
				};
			}
			string[] array;
			try
			{
				ArchivePaths.RejectLinks(options.RetentionDirectory, options.RetentionDirectory);
				array = (Directory.Exists(options.RetentionDirectory) ? Directory.GetDirectories(options.RetentionDirectory) : Array.Empty<string>());
			}
			catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
			{
				reportError("Cannot scan retained sources; originals retained for retry (" + ex.GetType().Name + ").");
				yield break;
			}
			string[] array2 = array;
			foreach (string text in array2)
			{
				RetainedBatch manifest;
				string data;
				try
				{
					string path = ArchivePaths.Resolve(options.RetentionDirectory, Path.Combine(Path.GetFileName(text), "retention.json"));
					if (!File.Exists(path))
					{
						continue;
					}
					manifest = JsonStore.Read<RetainedBatch>(path);
					if (manifest.Id != Path.GetFileName(text) || !string.Equals(manifest.SourceRoot, options.SourceDirectory, StringComparison.OrdinalIgnoreCase))
					{
						throw new InvalidDataException("Invalid retention provenance.");
					}
					if (manifest.OriginalRelativePath != ".")
					{
						ArchivePaths.Resolve(options.SourceDirectory, manifest.OriginalRelativePath);
					}
					else if (manifest.IsFile)
					{
						throw new InvalidDataException("Invalid retained file path.");
					}
					manifest.Context?.Validate();
					data = ArchivePaths.Resolve(options.RetentionDirectory, Path.Combine(manifest.Id, "data"));
					goto IL_033e;
				}
				catch (Exception ex2) when (ex2 is IOException || ex2 is UnauthorizedAccessException || ex2 is SerializationException || ex2 is ArgumentException)
				{
					reportError("Cannot read a retention manifest; source left untouched (" + ex2.GetType().Name + ").");
				}
				continue;
				IL_033e:
				if (manifest.IsFile)
				{
					if (File.Exists(data))
					{
						ArchiveContext archiveContext2 = RetainedContext(manifest, manifest.OriginalRelativePath);
						yield return new SourceFile
						{
							Path = data,
							Root = text,
							Relative = manifest.OriginalRelativePath,
							Context = archiveContext2,
							ProvenContext = (archiveContext2 != null),
							RetentionId = manifest.Id
						};
					}
					continue;
				}
				foreach (string item2 in EnumerateFiles(data))
				{
					string relativePath2 = Path.GetRelativePath(data, item2);
					string relative = ((manifest.OriginalRelativePath == ".") ? relativePath2 : Path.Combine(manifest.OriginalRelativePath, relativePath2));
					ArchiveContext archiveContext3 = RetainedContext(manifest, relative);
					yield return new SourceFile
					{
						Path = item2,
						Root = data,
						Relative = relative,
						Context = archiveContext3,
						ProvenContext = (archiveContext3 != null),
						RetentionId = manifest.Id
					};
				}
			}
		}

		private static ArchiveContext? RetainedContext(RetainedBatch manifest, string relative)
		{
			return ContextForPath(manifest.FileContexts ?? new Dictionary<string, ArchiveContext>(), relative) ?? manifest.Context;
		}

		private static ArchiveContext? ContextForPath(Dictionary<string, ArchiveContext> contexts, string relative)
		{
			ArchiveContext result = null;
			int num = -1;
			foreach (KeyValuePair<string, ArchiveContext> context in contexts)
			{
				if (context.Key.Length > num && (relative.Equals(context.Key, StringComparison.OrdinalIgnoreCase) || relative.StartsWith(context.Key + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
				{
					context.Value.Validate();
					result = context.Value;
					num = context.Key.Length;
				}
			}
			return result;
		}

		private IEnumerable<string> EnumerateFiles(string root)
		{
			Stack<string> pending = new Stack<string>();
			pending.Push(root);
			while (pending.Count > 0)
			{
				string path = pending.Pop();
				string[] files;
				string[] directories;
				try
				{
					if (!Directory.Exists(path))
					{
						continue;
					}
					ArchivePaths.RejectLinks(root, path);
					files = Directory.GetFiles(path);
					directories = Directory.GetDirectories(path);
					goto IL_00cd;
				}
				catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
				{
					reportError("Cannot scan a recording directory (" + ex.GetType().Name + "). Will retry next scan.");
				}
				continue;
				IL_00cd:
				string[] array = directories;
				foreach (string item in array)
				{
					pending.Push(item);
				}
				string[] array2 = files;
				for (int j = 0; j < array2.Length; j++)
				{
					yield return array2[j];
				}
			}
		}

		private RecordingMetadata GetRecording(string folderId, ArchiveContext? context)
		{
			context?.Validate();
			string text = ((context == null) ? folderId : (context.ScopeKey + "/" + folderId));
			if (recordings.TryGetValue(text, out RecordingMetadata value))
			{
				return value;
			}
			string path = ((context != null) ? (options.GroupBySession ? ((string.IsNullOrEmpty(context.MatchDateLocal) ? "Undated" : context.MatchDateLocal) + "_M_" + ArchivePaths.HashText(context.MatchId).Substring(0, 12)) : "Matches_merged") : (options.GroupBySession ? sessionId : "Sessions_merged"));
			if (context != null && options.GroupBySession)
			{
				RecordingMetadata recordingMetadata = state.Recordings.FirstOrDefault((RecordingMetadata r) => r.Context?.MatchId == context.MatchId);
				if (recordingMetadata != null)
				{
					path = recordingMetadata.RelativeDirectory.Split(Path.DirectorySeparatorChar)[0];
				}
			}
			string text2 = ArchivePaths.HashText(text).Substring(0, 16);
			RecordingMetadata recordingMetadata2 = new RecordingMetadata
			{
				Key = text,
				Context = context,
				OriginalFolderId = folderId,
				RelativeDirectory = ((context == null) ? Path.Combine(path, "R_" + text2) : Path.Combine(path, "I_" + context.IncursionNumber.ToString("D3") + "_" + ArchivePaths.HashText(context.IncursionId).Substring(0, 8), "R_" + text2)),
				SessionId = (context?.MatchId ?? sessionId),
				PlayerDirectoryEvidence = "per-clip camera owner; full recording has no inferred single owner"
			};
			state.Recordings.Add(recordingMetadata2);
			recordings.Add(text, recordingMetadata2);
			dirty = true;
			changedRecordings.Add(recordingMetadata2);
			unpersistedEvidence.Add(recordingMetadata2);
			return recordingMetadata2;
		}

		private bool VerifyExisting(ArchivedFile file)
		{
			string text = ArchivePaths.Resolve(options.OutputDirectory, file.Destination);
			if (!File.Exists(text) || new FileInfo(text).Length != file.Length)
			{
				return false;
			}
			if (verified.Contains(file.Destination))
			{
				return true;
			}
			if (HashFile(text) != file.Sha256)
			{
				return false;
			}
			verified.Add(file.Destination);
			return true;
		}

		private void Copy(string source, string relative, string[] parts, RecordingMetadata recording, long length, long writeTicks, bool snapshot, DateTime utcNow, CancellationToken cancellation, string retentionId)
		{
			Guid result;
			string text = ((parts.Length == 2 && parts[1].Equals("fullRecording.webm", StringComparison.OrdinalIgnoreCase)) ? "full" : ((parts.Length >= 3 && Guid.TryParse(parts[1], out result)) ? "segment" : "unknown"));
			PlayerIdentity owner = ((!(text == "segment")) ? null : recording.Clips.FirstOrDefault((ClipEvidence c) => c.ClipId.Equals(parts[1], StringComparison.OrdinalIgnoreCase))?.CameraOwner);
			string path = (options.GroupByPlayer ? ArchivePaths.PlayerDirectory(owner, options.UseSteamId) : "AllPlayers");
			string path2 = ((text == "full") ? "original" : ((text == "segment") ? Path.Combine("segments", path, ArchivePaths.HashText(parts[1]).Substring(0, 16)) : "unknown"));
			string text2 = Path.Combine(recording.RelativeDirectory, path2);
			Directory.CreateDirectory(ArchivePaths.Resolve(options.OutputDirectory, text2));
			string text3 = ArchivePaths.Resolve(options.OutputDirectory, Path.Combine(text2, ".ra-" + Guid.NewGuid().ToString("N").Substring(0, 16) + ".partial"));
			if (!persistedRecordings.Contains(recording.Key) || unpersistedEvidence.Contains(recording))
			{
				Persist();
			}
			CopyJournal copyJournal = new CopyJournal
			{
				RecordingId = recording.Key,
				File = new ArchivedFile
				{
					SourceRelativePath = relative,
					RetentionId = retentionId,
					Length = length,
					SourceWriteTicks = writeTicks
				}
			};
			string text4 = text3 + ".ra-journal";
			JsonStore.Write(text4, copyJournal);
			string text5;
			using (FileStream fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, (!snapshot) ? FileShare.Read : FileShare.ReadWrite, 131072, FileOptions.SequentialScan))
			{
				using FileStream fileStream2 = new FileStream(text3, FileMode.CreateNew, FileAccess.Write, FileShare.None, 131072, FileOptions.SequentialScan);
				using SHA256 sHA = SHA256.Create();
				byte[] array = new byte[131072];
				long num = length;
				while (num > 0)
				{
					cancellation.ThrowIfCancellationRequested();
					int num2 = fileStream.Read(array, 0, (int)Math.Min(array.Length, num));
					if (num2 == 0)
					{
						throw new IOException("Source shortened during copy.");
					}
					fileStream2.Write(array, 0, num2);
					sHA.TransformBlock(array, 0, num2, null, 0);
					num -= num2;
				}
				sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
				text5 = ArchivePaths.Hex(sHA.Hash);
				fileStream2.Flush(flushToDisk: true);
				FileInfo fileInfo = new FileInfo(source);
				if (!snapshot && (fileInfo.Length != length || fileInfo.LastWriteTimeUtc.Ticks != writeTicks))
				{
					throw new IOException("Source changed during copy.");
				}
			}
			if (new FileInfo(text3).Length != length || HashFile(text3) != text5)
			{
				throw new IOException("Archive verification failed.");
			}
			string path3 = parts[parts.Length - 1];
			string text6 = ArchivePaths.SafeName(Path.GetFileNameWithoutExtension(path3));
			if (text6.Length > 24)
			{
				text6 = text6.Substring(0, 24);
			}
			string text7 = ArchivePaths.SafeName(Path.GetExtension(path3));
			if (Path.GetExtension(path3).Length == 0)
			{
				text7 = "";
			}
			if (text7.Length > 12)
			{
				text7 = ".bin";
			}
			string text8 = text6 + "." + ArchivePaths.HashText(relative).Substring(0, 8) + "." + text5.Substring(0, 16) + (snapshot ? ".snapshot" : "") + text7;
			string destination = Path.Combine(text2, text8);
			string text9 = ArchivePaths.Resolve(options.OutputDirectory, destination);
			if (File.Exists(text9) && (new FileInfo(text9).Length != length || HashFile(text9) != text5))
			{
				destination = Path.Combine(text2, Guid.NewGuid().ToString("N") + "_" + text8);
				text9 = ArchivePaths.Resolve(options.OutputDirectory, destination);
			}
			ArchivedFile item = (copyJournal.File = new ArchivedFile
			{
				SourceRelativePath = relative,
				RetentionId = retentionId,
				Destination = destination,
				Sha256 = text5,
				Length = length,
				SourceWriteTicks = writeTicks,
				ArchivedUtc = utcNow.ToString("O"),
				Kind = text,
				Status = (snapshot ? "incomplete-snapshot-media-unverified" : "byte-verified-media-unverified")
			});
			copyJournal.ByteVerified = true;
			JsonStore.Write(text4, copyJournal);
			if (File.Exists(text9))
			{
				File.Delete(text3);
			}
			else
			{
				File.Move(text3, text9);
			}
			completedJournals.Add(text4);
			recording.Files.RemoveAll((ArchivedFile f) => f.SourceRelativePath == relative && f.Destination == destination);
			recording.Files.Add(item);
			if (text == "full")
			{
				recording.FullRecordingStatus = (snapshot ? "incomplete-snapshot" : "original-copied-media-unverified");
			}
			verified.Add(destination);
			dirty = true;
			changedRecordings.Add(recording);
		}

		private static string HashFile(string path)
		{
			using FileStream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072, FileOptions.SequentialScan);
			using SHA256 sHA = SHA256.Create();
			return ArchivePaths.Hex(sHA.ComputeHash(inputStream));
		}

		private void RecoverCopies()
		{
			foreach (string item in from p in EnumerateFiles(options.OutputDirectory)
				where Path.GetFileName(p).StartsWith(".ra-", StringComparison.Ordinal) && p.EndsWith(".partial.ra-journal", StringComparison.Ordinal)
				select p)
			{
				ArchivePaths.RejectLinks(options.OutputDirectory, item);
				CopyJournal copyJournal = JsonStore.Read<CopyJournal>(item);
				if (!copyJournal.ByteVerified)
				{
					continue;
				}
				if (!recordings.TryGetValue(copyJournal.RecordingId, out RecordingMetadata value) || copyJournal.File == null)
				{
					throw new InvalidDataException("Copy journal has no known recording.");
				}
				ArchivedFile entry = copyJournal.File;
				string text = ArchivePaths.Resolve(options.OutputDirectory, entry.Destination);
				string root = ArchivePaths.Resolve(options.OutputDirectory, value.RelativeDirectory);
				ArchivePaths.Resolve(options.SourceDirectory, entry.SourceRelativePath);
				if (!ArchivePaths.Contains(root, text) || entry.Length < 0 || entry.Sha256.Length != 64)
				{
					throw new InvalidDataException("Invalid copy journal.");
				}
				string text2 = item.Substring(0, item.Length - ".ra-journal".Length);
				if (!File.Exists(text))
				{
					ArchivePaths.RejectLinks(options.OutputDirectory, text2);
					if (!File.Exists(text2) || new FileInfo(text2).Length != entry.Length || HashFile(text2) != entry.Sha256)
					{
						reportError("Interrupted copy could not be verified; partial and journal retained for recovery.");
						continue;
					}
					File.Move(text2, text);
				}
				if (new FileInfo(text).Length != entry.Length || HashFile(text) != entry.Sha256)
				{
					reportError("Interrupted copy destination differs; retained without overwriting.");
					continue;
				}
				value.Files.RemoveAll((ArchivedFile f) => f.SourceRelativePath == entry.SourceRelativePath && f.Destination == entry.Destination);
				value.Files.Add(entry);
				if (entry.Kind == "full")
				{
					value.FullRecordingStatus = (entry.Status.StartsWith("incomplete", StringComparison.Ordinal) ? "incomplete-snapshot" : "original-copied-media-unverified");
				}
				changedRecordings.Add(value);
				completedJournals.Add(item);
				dirty = true;
			}
		}

		private void Persist()
		{
			JsonStore.Write(statePath, state);
			foreach (RecordingMetadata recording in state.Recordings)
			{
				persistedRecordings.Add(recording.Key);
			}
			foreach (RecordingMetadata changedRecording in changedRecordings)
			{
				string path = ArchivePaths.Resolve(options.OutputDirectory, Path.Combine(changedRecording.RelativeDirectory, "metadata.json"));
				Directory.CreateDirectory(Path.GetDirectoryName(path));
				JsonStore.Write(path, changedRecording);
				string path2 = changedRecording.RelativeDirectory.Split(Path.DirectorySeparatorChar)[0];
				string path3 = ArchivePaths.Resolve(options.OutputDirectory, Path.Combine(path2, "session_" + changedRecording.SessionId + ".json"));
				if (!File.Exists(path3))
				{
					JsonStore.Write(path3, new SessionMetadata
					{
						SessionId = changedRecording.SessionId,
						StartedUtc = (changedRecording.Context?.StartedUtc ?? DateTime.UtcNow.ToString("O")),
						SourceRoot = options.SourceDirectory,
						SessionMeaning = ((changedRecording.Context == null) ? "unassigned/legacy archiver process" : "room visit; incursion ordinals count locally observed underground arrivals")
					});
				}
			}
			changedRecordings.Clear();
			unpersistedEvidence.Clear();
			foreach (string completedJournal in completedJournals)
			{
				File.Delete(completedJournal);
			}
			completedJournals.Clear();
			dirty = false;
		}

		public void Dispose()
		{
			processLock.Dispose();
		}
	}
	public static class ArchivePaths
	{
		public static string NormalizeRoot(string path)
		{
			if (string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path))
			{
				throw new ArgumentException("An absolute directory is required.", "path");
			}
			string fullPath = Path.GetFullPath(path);
			if (string.Equals(fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), Path.GetPathRoot(fullPath)?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))
			{
				throw new ArgumentException("A filesystem root is not an archive directory.", "path");
			}
			return fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
		}

		public static bool Contains(string root, string path)
		{
			return Path.GetFullPath(path).StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
		}

		public static string Resolve(string root, string relative)
		{
			if (string.IsNullOrWhiteSpace(relative) || Path.IsPathRooted(relative))
			{
				throw new InvalidDataException("Invalid relative archive path.");
			}
			string fullPath = Path.GetFullPath(Path.Combine(root, relative));
			if (!Contains(root, fullPath))
			{
				throw new InvalidDataException("Path escapes its root.");
			}
			RejectLinks(root, fullPath);
			return fullPath;
		}

		public static void RejectLinks(string root, string path)
		{
			for (string text = path; text != null; text = Path.GetDirectoryName(text))
			{
				if ((Directory.Exists(text) || File.Exists(text)) && (File.GetAttributes(text) & FileAttributes.ReparsePoint) != FileAttributes.None)
				{
					throw new IOException("Reparse points are not supported in archive paths.");
				}
			}
		}

		public static string SafeName(string name)
		{
			char[] invalid = Path.GetInvalidFileNameChars();
			string text = new string(name.Select((char c) => (!char.IsControl(c) && !invalid.Contains(c) && !"<>:\"/\\|?*".Contains(c)) ? c : '_').ToArray());
			text = text.Trim().TrimEnd('.', ' ');
			text = TruncateName(text, 48);
			if (text.Length == 0 || text == "." || text == "..")
			{
				text = "Unknown";
			}
			string value = text.Split('.')[0].TrimEnd(' ').ToUpperInvariant();
			if (new string[28]
			{
				"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6",
				"COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7",
				"LPT8", "LPT9", "COM¹", "COM²", "COM³", "LPT¹", "LPT²", "LPT³"
			}.Contains(value))
			{
				text = "_" + text;
			}
			return text;
		}

		private static string TruncateName(string name, int length)
		{
			int num = Math.Min(name.Length, length);
			if (num > 0 && char.IsHighSurrogate(name[num - 1]))
			{
				num--;
			}
			return name.Substring(0, num).TrimEnd('.', ' ');
		}

		public static string SafePlayerName(string name)
		{
			StringBuilder stringBuilder = new StringBuilder();
			int num;
			for (int i = 0; i < name.Length; i += num)
			{
				num = ((!char.IsHighSurrogate(name[i]) || i + 1 >= name.Length || !char.IsLowSurrogate(name[i + 1])) ? 1 : 2);
				int num2 = ((num == 2) ? char.ConvertToUtf32(name[i], name[i + 1]) : name[i]);
				UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(name, i);
				if (unicodeCategory == UnicodeCategory.OtherSymbol || unicodeCategory == UnicodeCategory.MathSymbol || unicodeCategory == UnicodeCategory.CurrencySymbol || unicodeCategory == UnicodeCategory.ModifierSymbol || unicodeCategory == UnicodeCategory.Control || unicodeCategory == UnicodeCategory.Format || unicodeCategory == UnicodeCategory.Surrogate || unicodeCategory == UnicodeCategory.EnclosingMark || (num2 >= 65024 && num2 <= 65039) || (num2 >= 917760 && num2 <= 917999))
				{
					if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '_')
					{
						stringBuilder.Append('_');
					}
				}
				else
				{
					stringBuilder.Append(name, i, num);
				}
			}
			return SafeName(stringBuilder.ToString().Normalize(NormalizationForm.FormC).Trim('_', ' '));
		}

		public static string HashText(string text)
		{
			using SHA256 sHA = SHA256.Create();
			return Hex(sHA.ComputeHash(Encoding.UTF8.GetBytes(text)));
		}

		public static string Hex(byte[] bytes)
		{
			return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
		}

		public static string PlayerDirectory(PlayerIdentity? owner, bool exposeId)
		{
			if (owner == null || string.IsNullOrWhiteSpace(owner.Id))
			{
				return "Unknown";
			}
			string text = (exposeId ? SafeName(owner.Id.StartsWith("steam:", StringComparison.Ordinal) ? owner.Id.Substring(6) : owner.Id) : HashText(owner.Id).Substring(0, 12));
			string text2 = TruncateName(SafePlayerName(owner.Name), 16);
			if (text2.Length == 0)
			{
				text2 = "Unknown";
			}
			return text2 + "_" + text;
		}
	}
	public static class IdentityResolver
	{
		public static bool IsNumericId(string? value)
		{
			if (ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var result))
			{
				return result != 0;
			}
			return false;
		}

		public static PlayerIdentity FromPhoton(string? userId, string? platform, string? steamId, string? nickname, int actor, string sessionScope)
		{
			string id;
			string evidence;
			if (IsNumericId(steamId))
			{
				id = "steam:" + steamId;
				evidence = "Photon.CustomProperties.SteamID";
			}
			else if (IsNumericId(userId) && !string.IsNullOrWhiteSpace(platform))
			{
				id = "platform:" + platform + ":" + userId;
				evidence = "Photon.CustomProperties.PlatformFamily+UserID";
			}
			else
			{
				id = "actor:" + sessionScope + ":" + actor.ToString(CultureInfo.InvariantCulture);
				evidence = "Photon.ActorNumber; session-scoped";
			}
			return new PlayerIdentity
			{
				Id = id,
				Name = (string.IsNullOrWhiteSpace(nickname) ? "Unknown" : nickname),
				Evidence = evidence
			};
		}

		public static PlayerIdentity? FromCameraOwner(string? steamId, string? name)
		{
			if (!IsNumericId(steamId))
			{
				return null;
			}
			return new PlayerIdentity
			{
				Id = "steam:" + steamId,
				Name = (string.IsNullOrWhiteSpace(name) ? "Unknown" : name),
				Evidence = "ContentPOVs.POVCamera.plrID"
			};
		}
	}
	public static class JsonStore
	{
		public static T Read<T>(string path) where T : class
		{
			using FileStream stream = File.OpenRead(path);
			return (T)(new DataContractJsonSerializer(typeof(T)).ReadObject(stream) ?? throw new InvalidDataException("Empty JSON document."));
		}

		public static void Write<T>(string path, T value)
		{
			string text = path + ".ra-tmp";
			ArchivePaths.RejectLinks(Path.GetDirectoryName(path), path);
			ArchivePaths.RejectLinks(Path.GetDirectoryName(path), text);
			using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None))
			{
				new DataContractJsonSerializer(typeof(T)).WriteObject(fileStream, value);
				fileStream.Flush(flushToDisk: true);
			}
			if (File.Exists(path))
			{
				File.Replace(text, path, null);
			}
			else
			{
				File.Move(text, path);
			}
		}
	}
	public sealed class ArchiveOptions
	{
		public string SourceDirectory { get; set; } = "";

		public string OutputDirectory { get; set; } = "";

		public string RetentionDirectory { get; set; } = "";

		public bool ArchiveFragments { get; set; } = true;

		public bool ArchiveFullRecordings { get; set; } = true;

		public bool GroupBySession { get; set; } = true;

		public bool GroupByPlayer { get; set; } = true;

		public bool UseSteamId { get; set; } = true;

		public TimeSpan StabilityPeriod { get; set; } = TimeSpan.FromSeconds(5.0);

		public TimeSpan SnapshotAfter { get; set; } = TimeSpan.FromMinutes(2.0);
	}
	public sealed class PlayerIdentity
	{
		public string Id { get; set; } = "";

		public string Name { get; set; } = "Unknown";

		public string Evidence { get; set; } = "";
	}
	public sealed class ClipEvidence
	{
		public string VideoId { get; set; } = "";

		public string ClipId { get; set; } = "";

		public string CameraId { get; set; } = "";

		public int Order { get; set; }

		public ArchiveContext? Context { get; set; }

		public PlayerIdentity? CameraOwner { get; set; }

		public PlayerIdentity? Holder { get; set; }

		public PlayerIdentity? Encoder { get; set; }

		public bool IsRecording { get; set; }

		public bool IsEncoded { get; set; }

		public bool IsReceived { get; set; }

		public bool IsValid { get; set; }
	}
	public sealed class ArchivedFile
	{
		public string SourceRelativePath { get; set; } = "";

		public string RetentionId { get; set; } = "";

		public string Destination { get; set; } = "";

		public string Sha256 { get; set; } = "";

		public long Length { get; set; }

		public long SourceWriteTicks { get; set; }

		public string ArchivedUtc { get; set; } = "";

		public string Kind { get; set; } = "unknown";

		public string Status { get; set; } = "byte-verified-media-unverified";
	}
	public sealed class RecordingMetadata
	{
		public string Key { get; set; } = "";

		public ArchiveContext? Context { get; set; }

		public string OriginalFolderId { get; set; } = "";

		public string RelativeDirectory { get; set; } = "";

		public string SessionId { get; set; } = "";

		public string PlayerDirectoryEvidence { get; set; } = "Unknown";

		public bool OwnerConflict { get; set; }

		public string FullRecordingStatus { get; set; } = "not-observed";

		public List<ClipEvidence> Clips { get; set; } = new List<ClipEvidence>();

		public List<ArchivedFile> Files { get; set; } = new List<ArchivedFile>();

		public string FullRecordingScopeMeaning { get; set; } = "extraction context; may include clips from earlier incursions";
	}
	public sealed class ArchiveContext
	{
		public string MatchId { get; set; } = "";

		public string IncursionId { get; set; } = "";

		public int IncursionNumber { get; set; }

		public int GameDay { get; set; }

		public string SaveId { get; set; } = "";

		public string Evidence { get; set; } = "";

		public string StartedUtc { get; set; } = "";

		public string MatchDateLocal { get; set; } = "";

		public string ScopeKey => MatchId + "/" + IncursionId;

		public void Validate()
		{
			if (!Guid.TryParse(MatchId, out var result) || !Guid.TryParse(IncursionId, out result) || IncursionNumber < 0 || GameDay < 0)
			{
				throw new InvalidDataException("Invalid match/incursion context.");
			}
			if (!string.IsNullOrEmpty(MatchDateLocal) && !DateTime.TryParseExact(MatchDateLocal, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var _))
			{
				throw new InvalidDataException("Invalid local match date.");
			}
		}
	}
	public sealed class RetainedBatch
	{
		public string Id { get; set; } = "";

		public string SourceRoot { get; set; } = "";

		public string OriginalRelativePath { get; set; } = "";

		public bool IsFile { get; set; }

		public ArchiveContext? Context { get; set; }

		public Dictionary<string, ArchiveContext> FileContexts { get; set; } = new Dictionary<string, ArchiveContext>();
	}
	public sealed class CopyJournal
	{
		public string RecordingId { get; set; } = "";

		public bool ByteVerified { get; set; }

		public ArchivedFile File { get; set; } = new ArchivedFile();
	}
	public sealed class ArchiveState
	{
		public int SchemaVersion { get; set; } = 1;

		public string SourceRoot { get; set; } = "";

		public List<RecordingMetadata> Recordings { get; set; } = new List<RecordingMetadata>();
	}
	public sealed class SessionMetadata
	{
		public string SessionId { get; set; } = "";

		public string StartedUtc { get; set; } = "";

		public string SourceRoot { get; set; } = "";

		public string SessionMeaning { get; set; } = "archiver-process; existing video IDs retain their previous session";
	}
	public static class PlaybackTimeline
	{
		public static float Rebase(float eventTime, float sourceOffset, float playbackOffset, float duration)
		{
			float[] array = new float[4] { eventTime, sourceOffset, playbackOffset, duration };
			foreach (float f in array)
			{
				if (float.IsNaN(f) || float.IsInfinity(f))
				{
					throw new InvalidDataException("Invalid recording timeline.");
				}
			}
			if (sourceOffset < 0f || playbackOffset < 0f || duration < 0f)
			{
				throw new InvalidDataException("Negative recording timeline.");
			}
			return playbackOffset + Math.Max(0f, Math.Min(duration, eventTime - sourceOffset));
		}
	}
	public sealed class PlaybackWorkspace
	{
		private const string Marker = "RecordingArchiver playback workspace v1\n";

		private readonly FileStream owner;

		private readonly Action<Exception> report;

		public string Root { get; }

		public PlaybackWorkspace(string parent, Action<Exception> report)
		{
			this.report = report;
			parent = ArchivePaths.NormalizeRoot(parent);
			ArchivePaths.RejectLinks(parent, parent);
			string text = Guid.NewGuid().ToString("N");
			Root = ArchivePaths.Resolve(parent, text);
			Directory.CreateDirectory(Root);
			owner = new FileStream(Path.Combine(Root, "owner.lock"), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
			try
			{
				File.WriteAllText(Path.Combine(Root, "workspace.marker"), "RecordingArchiver playback workspace v1\n" + text);
			}
			catch
			{
				owner.Dispose();
				throw;
			}
		}

		public Task RemoveEpoch(string epoch, Task[] pending)
		{
			TerminalProtocol.RequireGuid(epoch);
			string path = ArchivePaths.Resolve(Root, Guid.Parse(epoch).ToString("N"));
			return After(pending, delegate
			{
				DeleteWithRetry(path);
			});
		}

		public Task Close(Task[] pending)
		{
			return After(pending, delegate
			{
				owner.Dispose();
				DeleteWithRetry(Root);
			});
		}

		private async Task After(Task[] pending, Action cleanup)
		{
			try
			{
				await Task.WhenAll(pending).ConfigureAwait(continueOnCapturedContext: false);
			}
			catch (OperationCanceledException)
			{
			}
			catch (Exception obj)
			{
				report(obj);
			}
			try
			{
				await Task.Run(cleanup).ConfigureAwait(continueOnCapturedContext: false);
			}
			catch (Exception obj2)
			{
				report(obj2);
			}
		}

		private static void DeleteWithRetry(string path)
		{
			int num = 0;
			while (true)
			{
				try
				{
					if (Directory.Exists(path))
					{
						ValidateTree(path);
						Directory.Delete(path, recursive: true);
					}
					break;
				}
				catch (IOException) when (num < 3)
				{
					Thread.Sleep(200 * (num + 1));
				}
				num++;
			}
		}

		private static void ValidateTree(string root)
		{
			Stack<string> stack = new Stack<string>();
			stack.Push(root);
			while (stack.Count > 0)
			{
				string path = stack.Pop();
				ArchivePaths.RejectLinks(root, path);
				string[] files = Directory.GetFiles(path);
				foreach (string path2 in files)
				{
					ArchivePaths.RejectLinks(root, path2);
				}
				files = Directory.GetDirectories(path);
				foreach (string item in files)
				{
					stack.Push(item);
				}
			}
		}

		public static void RecoverAbandoned(string parent, Action<Exception> report)
		{
			parent = ArchivePaths.NormalizeRoot(parent);
			try
			{
				ArchivePaths.RejectLinks(parent, parent);
				if (!Directory.Exists(parent))
				{
					return;
				}
				string[] directories = Directory.GetDirectories(parent);
				foreach (string path in directories)
				{
					string fileName = Path.GetFileName(path);
					if (!Guid.TryParseExact(fileName, "N", out var _))
					{
						continue;
					}
					try
					{
						string text = ArchivePaths.Resolve(parent, Path.Combine(fileName, "workspace.marker"));
						string path2 = ArchivePaths.Resolve(parent, Path.Combine(fileName, "owner.lock"));
						if (!File.Exists(text) || new FileInfo(text).Length > 128 || File.ReadAllText(text) != "RecordingArchiver playback workspace v1\n" + fileName || !File.Exists(path2))
						{
							continue;
						}
						try
						{
							using (File.Open(path2, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
							{
							}
						}
						catch (IOException)
						{
							goto end_IL_0048;
						}
						DeleteWithRetry(path);
						end_IL_0048:;
					}
					catch (Exception ex2) when (ex2 is IOException || ex2 is UnauthorizedAccessException)
					{
						report(ex2);
					}
				}
			}
			catch (Exception ex3) when (ex3 is IOException || ex3 is UnauthorizedAccessException)
			{
				report(ex3);
			}
		}
	}
	public enum RewardPhase
	{
		Quota = 1,
		Money
	}
	public sealed class RewardTicket
	{
		public string Epoch { get; set; } = "";

		public string SourceKey { get; set; } = "";

		public string Token { get; set; } = "";

		public bool PayPending { get; set; } = true;

		public bool AllowRepeat { get; set; }
	}
	public sealed class RewardLedgerState
	{
		public string Epoch { get; set; } = "";

		public List<string> Sources { get; set; } = new List<string>();

		public List<string> Repeats { get; set; } = new List<string>();
	}
	public sealed class RewardLedger
	{
		private readonly Dictionary<string, int> sources = new Dictionary<string, int>(StringComparer.Ordinal);

		private readonly Dictionary<string, int> repeats = new Dictionary<string, int>(StringComparer.Ordinal);

		public string Epoch { get; }

		public RewardLedger(string epoch)
		{
			TerminalProtocol.RequireGuid(epoch);
			Epoch = epoch;
		}

		public bool CanApply(RewardTicket ticket, RewardPhase phase)
		{
			ValidateTicket(ticket);
			if (!string.Equals(ticket.Epoch, Epoch, StringComparison.Ordinal))
			{
				return false;
			}
			if (!ticket.PayPending)
			{
				return false;
			}
			if (phase != RewardPhase.Quota && phase != RewardPhase.Money)
			{
				throw new ArgumentOutOfRangeException("phase");
			}
			if (ticket.AllowRepeat)
			{
				if (repeats.TryGetValue(ticket.Token + ":" + ticket.SourceKey, out var value))
				{
					return ((uint)value & (uint)phase) == 0;
				}
				return true;
			}
			if (sources.TryGetValue(ticket.SourceKey, out var value2))
			{
				return ((uint)value2 & (uint)phase) == 0;
			}
			return true;
		}

		public bool TryApply(RewardTicket ticket, RewardPhase phase)
		{
			if (!CanApply(ticket, phase))
			{
				return false;
			}
			if (!sources.ContainsKey(ticket.SourceKey) && sources.Count >= 4096)
			{
				throw new InvalidDataException("Reward ledger capacity reached.");
			}
			if (ticket.AllowRepeat)
			{
				string key = ticket.Token + ":" + ticket.SourceKey;
				if (!repeats.ContainsKey(key) && repeats.Count >= 32)
				{
					throw new InvalidDataException("Additional reward capacity reached.");
				}
				repeats.TryGetValue(key, out var value);
				repeats[key] = value | (int)phase;
			}
			sources.TryGetValue(ticket.SourceKey, out var value2);
			sources[ticket.SourceKey] = value2 | (int)phase;
			return true;
		}

		public int PaidPhases(string source)
		{
			if (!sources.TryGetValue(source, out var value))
			{
				return 0;
			}
			return value;
		}

		public RewardLedgerState Export()
		{
			return new RewardLedgerState
			{
				Epoch = Epoch,
				Sources = sources.Select<KeyValuePair<string, int>, string>((KeyValuePair<string, int> p) => p.Key + ":" + p.Value).ToList(),
				Repeats = repeats.Select<KeyValuePair<string, int>, string>((KeyValuePair<string, int> p) => p.Key + ":" + p.Value).ToList()
			};
		}

		public static RewardLedger Restore(RewardLedgerState state)
		{
			RewardLedger rewardLedger = new RewardLedger(state.Epoch);
			if (state.Sources == null || state.Repeats == null || state.Sources.Count > 4096 || state.Repeats.Count > 32)
			{
				throw new InvalidDataException("Invalid reward ledger.");
			}
			foreach (string source in state.Sources)
			{
				string[] array = (source ?? throw new InvalidDataException("Null reward receipt.")).Split(':');
				if (array.Length != 2 || !IsHash(array[0]) || !ValidMask(array[1], out var mask) || rewardLedger.sources.ContainsKey(array[0]))
				{
					throw new InvalidDataException("Invalid reward receipt.");
				}
				rewardLedger.sources.Add(array[0], mask);
			}
			foreach (string repeat in state.Repeats)
			{
				string[] array2 = (repeat ?? throw new InvalidDataException("Null repeated reward receipt.")).Split(':');
				if (array2.Length != 3 || array2[0].Length != 32 || !Guid.TryParseExact(array2[0], "N", out var _) || !IsHash(array2[1]) || !ValidMask(array2[2], out var mask2))
				{
					throw new InvalidDataException("Invalid repeated reward receipt.");
				}
				string key = array2[0] + ":" + array2[1];
				if (rewardLedger.repeats.ContainsKey(key))
				{
					throw new InvalidDataException("Duplicate reward receipt.");
				}
				rewardLedger.repeats.Add(key, mask2);
				if (!rewardLedger.sources.TryGetValue(array2[1], out var value) || (value & mask2) != mask2)
				{
					throw new InvalidDataException("Inconsistent repeated reward receipt.");
				}
			}
			return rewardLedger;
		}

		private static bool ValidMask(string text, out int mask)
		{
			if (int.TryParse(text, out mask) && mask >= 1)
			{
				return mask <= 3;
			}
			return false;
		}

		private static bool IsHash(string value)
		{
			if (value != null && value.Length == 64)
			{
				return value.All((char c) => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'));
			}
			return false;
		}

		private static void ValidateTicket(RewardTicket ticket)
		{
			TerminalProtocol.RequireGuid(ticket.Epoch);
			if (!IsHash(ticket.SourceKey) || ticket.Token == null || ticket.Token.Length != 32 || !Guid.TryParseExact(ticket.Token, "N", out var result) || result == Guid.Empty || (ticket.AllowRepeat && !ticket.PayPending))
			{
				throw new InvalidDataException("Invalid reward ticket.");
			}
		}
	}
	public sealed class SessionTracker
	{
		private string lastUndergroundScene = "";

		private bool enteredUnderground;

		public ArchiveContext? Current { get; private set; }

		public ArchiveContext BeginMatch(string saveId, string evidence)
		{
			lastUndergroundScene = "";
			enteredUnderground = false;
			ArchiveContext obj = new ArchiveContext
			{
				MatchId = Guid.NewGuid().ToString("N")
			};
			Guid result = Guid.NewGuid();
			obj.IncursionId = result.ToString("N");
			obj.IncursionNumber = 1;
			obj.SaveId = (Guid.TryParse(saveId, out result) ? saveId : "");
			obj.Evidence = evidence + "; surface-before-first-observed-dive";
			obj.StartedUtc = DateTime.UtcNow.ToString("O");
			obj.MatchDateLocal = DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
			ArchiveContext result2 = obj;
			Current = obj;
			return result2;
		}

		public ArchiveContext EnterUnderground(string sceneToken, int day)
		{
			if (Current == null)
			{
				BeginMatch("", "late-observation");
			}
			if (enteredUnderground && lastUndergroundScene == sceneToken)
			{
				return Current;
			}
			bool flag = !enteredUnderground;
			enteredUnderground = true;
			lastUndergroundScene = sceneToken;
			return Current = new ArchiveContext
			{
				MatchId = Current.MatchId,
				IncursionId = (flag ? Current.IncursionId : Guid.NewGuid().ToString("N")),
				IncursionNumber = (flag ? Current.IncursionNumber : (Current.IncursionNumber + 1)),
				GameDay = Math.Max(0, day),
				SaveId = Current.SaveId,
				Evidence = "PhotonGameLobbyHandler.Awake; underground scene loaded; locally observed ordinal",
				StartedUtc = (flag ? Current.StartedUtc : DateTime.UtcNow.ToString("O")),
				MatchDateLocal = Current.MatchDateLocal
			};
		}
	}
	public sealed class SourceRetention
	{
		private readonly string sourceRoot;

		private readonly object gate = new object();

		private readonly Dictionary<string, ArchiveContext> origins = new Dictionary<string, ArchiveContext>(StringComparer.OrdinalIgnoreCase);

		public string Root { get; }

		public SourceRetention(string sourceRoot, string retentionRoot)
		{
			this.sourceRoot = ArchivePaths.NormalizeRoot(sourceRoot);
			Root = ArchivePaths.NormalizeRoot(retentionRoot);
			if (this.sourceRoot.Equals(Root, StringComparison.OrdinalIgnoreCase) || ArchivePaths.Contains(this.sourceRoot, Root) || ArchivePaths.Contains(Root, this.sourceRoot))
			{
				throw new ArgumentException("Retention must be outside the source tree.");
			}
			if (!string.Equals(Path.GetPathRoot(this.sourceRoot), Path.GetPathRoot(Root), StringComparison.OrdinalIgnoreCase))
			{
				throw new ArgumentException("Atomic retention requires the same volume.");
			}
		}

		public void Remember(string path, ArchiveContext? context)
		{
			if (context == null)
			{
				return;
			}
			context.Validate();
			string relativePath = Path.GetRelativePath(sourceRoot, path);
			ArchivePaths.Resolve(sourceRoot, relativePath);
			lock (gate)
			{
				origins[relativePath] = context;
			}
		}

		public Dictionary<string, ArchiveContext> SnapshotContexts()
		{
			lock (gate)
			{
				return new Dictionary<string, ArchiveContext>(origins, StringComparer.OrdinalIgnoreCase);
			}
		}

		public void Preserve(string path, bool isFile, ArchiveContext? context)
		{
			lock (gate)
			{
				string text = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
				if (!text.Equals(sourceRoot, StringComparison.OrdinalIgnoreCase) && !ArchivePaths.Contains(sourceRoot, text))
				{
					throw new IOException("Refusing to retain a path outside Rec.");
				}
				ArchivePaths.RejectLinks(sourceRoot, text);
				ArchivePaths.RejectLinks(Root, Root);
				if (isFile ? (!File.Exists(text)) : (!Directory.Exists(text)))
				{
					return;
				}
				context?.Validate();
				string text2 = Guid.NewGuid().ToString("N").Substring(0, 16);
				string text3 = ArchivePaths.Resolve(Root, text2);
				Directory.CreateDirectory(text3);
				RetainedBatch retainedBatch = new RetainedBatch
				{
					Id = text2,
					SourceRoot = sourceRoot,
					OriginalRelativePath = Path.GetRelativePath(sourceRoot, text),
					IsFile = isFile,
					Context = context
				};
				string originalRelativePath = retainedBatch.OriginalRelativePath;
				foreach (KeyValuePair<string, ArchiveContext> origin in origins)
				{
					if (originalRelativePath == "." || origin.Key.Equals(originalRelativePath, StringComparison.OrdinalIgnoreCase) || origin.Key.StartsWith(originalRelativePath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
					{
						retainedBatch.FileContexts.Add(origin.Key, origin.Value);
					}
				}
				JsonStore.Write(Path.Combine(text3, "retention.json"), retainedBatch);
				if (isFile)
				{
					File.Move(text, Path.Combine(text3, "data"));
				}
				else
				{
					Directory.Move(text, Path.Combine(text3, "data"));
				}
				foreach (string key in retainedBatch.FileContexts.Keys)
				{
					origins.Remove(key);
				}
			}
		}

		public void CopyPreserving(string source, string destination, ArchiveContext? context)
		{
			lock (gate)
			{
				Preserve(destination, isFile: true, null);
				File.Copy(source, destination, overwrite: false);
				Remember(destination, context);
			}
		}
	}
	public sealed class TerminalClipEntry
	{
		public string MatchId { get; set; } = "";

		public string VideoId { get; set; } = "";

		public string ClipId { get; set; } = "";

		public int Incursion { get; set; }

		public string PlayerId { get; set; } = "";

		public string PlayerName { get; set; } = "";

		public int Order { get; set; }
	}
	public sealed class TerminalSelection
	{
		public List<TerminalClipEntry> Clips { get; set; } = new List<TerminalClipEntry>();

		public bool IsSequence { get; set; }

		public int ClipNumber { get; set; }

		public string Key => TerminalProtocol.SourceKey(Clips[0].VideoId, Clips.Select((TerminalClipEntry c) => c.ClipId));
	}
	public sealed class TerminalLibrary
	{
		private readonly Dictionary<string, TerminalClipEntry> clips = new Dictionary<string, TerminalClipEntry>();

		public void Clear()
		{
			clips.Clear();
		}

		public bool Observe(TerminalClipEntry entry)
		{
			TerminalProtocol.RequireGuid(entry.VideoId);
			TerminalProtocol.RequireGuid(entry.ClipId);
			if (string.IsNullOrWhiteSpace(entry.MatchId) || string.IsNullOrWhiteSpace(entry.PlayerId) || entry.Incursion < 0 || entry.Order < 0)
			{
				throw new InvalidDataException("Missing clip scope or player identity.");
			}
			string key = TerminalProtocol.SourceKey(entry.VideoId, new string[1] { entry.ClipId });
			if (clips.TryGetValue(key, out TerminalClipEntry value) && (value.MatchId != entry.MatchId || value.Incursion != entry.Incursion))
			{
				return false;
			}
			if (!clips.ContainsKey(key) && clips.Count >= 4096)
			{
				throw new InvalidDataException("Terminal clip capacity reached.");
			}
			clips[key] = new TerminalClipEntry
			{
				MatchId = entry.MatchId,
				VideoId = Guid.Parse(entry.VideoId).ToString(),
				ClipId = Guid.Parse(entry.ClipId).ToString(),
				Incursion = entry.Incursion,
				PlayerId = entry.PlayerId,
				PlayerName = entry.PlayerName,
				Order = (value?.Order ?? entry.Order)
			};
			return true;
		}

		public IEnumerable<TerminalSelection> Selections(string match)
		{
			foreach (var item in (from c in clips.Values
				where c.MatchId == match
				group c by new { c.Incursion, c.PlayerId, c.VideoId } into g
				orderby g.Key.Incursion
				select g).ThenBy(g => g.First().PlayerName, StringComparer.CurrentCulture).ThenBy(g => g.Key.VideoId, StringComparer.Ordinal))
			{
				List<TerminalClipEntry> ordered = item.OrderBy((TerminalClipEntry c) => c.Order).ThenBy<TerminalClipEntry, string>((TerminalClipEntry c) => c.ClipId, StringComparer.Ordinal).ToList();
				if (ordered.Count <= 128)
				{
					yield return new TerminalSelection
					{
						Clips = ordered,
						IsSequence = true
					};
				}
				for (int i = 0; i < ordered.Count; i++)
				{
					yield return new TerminalSelection
					{
						Clips = new List<TerminalClipEntry> { ordered[i] },
						ClipNumber = i + 1
					};
				}
			}
		}

		public TerminalSelection? Resolve(string match, DiscDefinition disc)
		{
			disc.Validate();
			List<TerminalClipEntry> selected = new List<TerminalClipEntry>();
			foreach (string clipId in disc.ClipIds)
			{
				if (!clips.TryGetValue(TerminalProtocol.SourceKey(disc.SourceVideoId, new string[1] { clipId }), out TerminalClipEntry value) || value.MatchId != match)
				{
					return null;
				}
				selected.Add(value);
			}
			if (selected.Any((TerminalClipEntry c) => c.Incursion != selected[0].Incursion || c.PlayerId != selected[0].PlayerId))
			{
				return null;
			}
			if (!selected.Select((TerminalClipEntry c) => c.ClipId).SequenceEqual(from c in selected.OrderBy((TerminalClipEntry c) => c.Order).ThenBy<TerminalClipEntry, string>((TerminalClipEntry c) => c.ClipId, StringComparer.Ordinal)
				select c.ClipId))
			{
				return null;
			}
			return new TerminalSelection
			{
				Clips = selected,
				IsSequence = (selected.Count > 1)
			};
		}
	}
	public sealed class PlaybackFile
	{
		public string ClipId { get; set; } = "";

		public string SourceRelativePath { get; set; } = "";

		public long Length { get; set; }

		public long WriteTicks { get; set; }
	}
	public static class TerminalMedia
	{
		public static string Prepare(DiscDefinition disc, IReadOnlyList<PlaybackFile> files, string sourceRoot, string outputRoot, string playbackRoot, CancellationToken cancellation, PlaybackFile? fullRecording = null)
		{
			disc.Validate();
			sourceRoot = ArchivePaths.NormalizeRoot(sourceRoot);
			outputRoot = ArchivePaths.NormalizeRoot(outputRoot);
			playbackRoot = ArchivePaths.NormalizeRoot(playbackRoot);
			if (files.Count != disc.ClipIds.Count || files.Count == 0)
			{
				throw new InvalidDataException("Incomplete playback snapshot.");
			}
			if (playbackRoot.Equals(sourceRoot, StringComparison.OrdinalIgnoreCase) || ArchivePaths.Contains(sourceRoot, playbackRoot) || ArchivePaths.Contains(playbackRoot, sourceRoot) || playbackRoot.Equals(outputRoot, StringComparison.OrdinalIgnoreCase) || ArchivePaths.Contains(playbackRoot, outputRoot))
			{
				throw new InvalidDataException("Unsafe playback directory.");
			}
			string result = ArchivePaths.Resolve(playbackRoot, Guid.Parse(disc.AliasId).ToString("N"));
			ArchiveState archiveState = null;
			string path = ArchivePaths.Resolve(outputRoot, ".recordingarchiver-state.json");
			if (File.Exists(path))
			{
				archiveState = JsonStore.Read<ArchiveState>(path);
				if (!string.Equals(archiveState.SourceRoot, sourceRoot, StringComparison.OrdinalIgnoreCase) || archiveState.Recordings == null || archiveState.Recordings.Any((RecordingMetadata r) => r == null || r.Files == null))
				{
					throw new InvalidDataException("Archive index is invalid or belongs to another source.");
				}
			}
			for (int num = 0; num < files.Count; num++)
			{
				cancellation.ThrowIfCancellationRequested();
				PlaybackFile playbackFile = files[num];
				TerminalProtocol.RequireGuid(playbackFile.ClipId);
				string value = Path.Combine(Guid.Parse(disc.SourceVideoId).ToString(), Guid.Parse(playbackFile.ClipId).ToString(), "output.webm");
				if (!Guid.Parse(playbackFile.ClipId).Equals(Guid.Parse(disc.ClipIds[num])) || !playbackFile.SourceRelativePath.Equals(value, StringComparison.OrdinalIgnoreCase) || playbackFile.Length < 4 || playbackFile.WriteTicks <= 0)
				{
					throw new InvalidDataException("Invalid playback provenance.");
				}
				string source = FindSource(playbackFile, sourceRoot, outputRoot, archiveState);
				string text = ArchivePaths.Resolve(playbackRoot, Path.Combine(Guid.Parse(disc.AliasId).ToString("N"), Guid.Parse(playbackFile.ClipId).ToString(), "output.webm"));
				Directory.CreateDirectory(Path.GetDirectoryName(text));
				CopyVerified(source, text, playbackFile.Length, cancellation);
			}
			string target = ArchivePaths.Resolve(playbackRoot, Path.Combine(Guid.Parse(disc.AliasId).ToString("N"), "fullRecording.webm"));
			if (fullRecording != null)
			{
				string text2 = Path.Combine(Guid.Parse(disc.SourceVideoId).ToString(), "fullRecording.webm");
				if (fullRecording.SourceRelativePath != text2 || fullRecording.Length < 4 || fullRecording.WriteTicks <= 0)
				{
					throw new InvalidDataException("Invalid full recording provenance.");
				}
				CopyVerified(FindSource(fullRecording, sourceRoot, outputRoot, archiveState), target, fullRecording.Length, cancellation);
			}
			else if (files.Count == 1)
			{
				CopyVerified(ArchivePaths.Resolve(playbackRoot, Path.Combine(Guid.Parse(disc.AliasId).ToString("N"), Guid.Parse(files[0].ClipId).ToString(), "output.webm")), target, files[0].Length, cancellation);
			}
			return result;
		}

		private static string FindSource(PlaybackFile file, string sourceRoot, string outputRoot, ArchiveState? state)
		{
			string text = ArchivePaths.Resolve(sourceRoot, file.SourceRelativePath);
			if (Matches(text, file))
			{
				return text;
			}
			if (state != null)
			{
				foreach (ArchivedFile item in from f in state.Recordings.SelectMany((RecordingMetadata r) => r.Files ?? new List<ArchivedFile>())
					where f != null && string.Equals(f.SourceRelativePath, file.SourceRelativePath, StringComparison.OrdinalIgnoreCase) && f.Length == file.Length && f.SourceWriteTicks == file.WriteTicks && f.Status == "byte-verified-media-unverified"
					select f)
				{
					string text2 = ArchivePaths.Resolve(outputRoot, item.Destination);
					if (File.Exists(text2) && new FileInfo(text2).Length == file.Length && Hash(text2) == item.Sha256)
					{
						return text2;
					}
				}
			}
			string text3 = sourceRoot + "_RA";
			ArchivePaths.RejectLinks(text3, text3);
			if (Directory.Exists(text3))
			{
				string[] directories = Directory.GetDirectories(text3);
				foreach (string path in directories)
				{
					string path2 = ArchivePaths.Resolve(text3, Path.Combine(Path.GetFileName(path), "retention.json"));
					if (!File.Exists(path2))
					{
						continue;
					}
					RetainedBatch retainedBatch = JsonStore.Read<RetainedBatch>(path2);
					if (retainedBatch.Id != Path.GetFileName(path) || !string.Equals(retainedBatch.SourceRoot, sourceRoot, StringComparison.OrdinalIgnoreCase))
					{
						throw new InvalidDataException("Invalid retained playback provenance.");
					}
					string originalRelativePath = retainedBatch.OriginalRelativePath;
					if (originalRelativePath != ".")
					{
						ArchivePaths.Resolve(sourceRoot, originalRelativePath);
					}
					string relative;
					if (retainedBatch.IsFile)
					{
						if (!originalRelativePath.Equals(file.SourceRelativePath, StringComparison.OrdinalIgnoreCase))
						{
							continue;
						}
						relative = Path.Combine(retainedBatch.Id, "data");
					}
					else
					{
						if (originalRelativePath != "." && !file.SourceRelativePath.StartsWith(originalRelativePath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
						{
							continue;
						}
						relative = Path.Combine(retainedBatch.Id, "data", (originalRelativePath == ".") ? file.SourceRelativePath : file.SourceRelativePath.Substring(originalRelativePath.Length + 1));
					}
					string text4 = ArchivePaths.Resolve(text3, relative);
					if (Matches(text4, file))
					{
						return text4;
					}
				}
			}
			throw new FileNotFoundException("A clip is unavailable in live, archived and retained sources.");
		}

		private static bool Matches(string path, PlaybackFile file)
		{
			if (File.Exists(path) && new FileInfo(path).Length == file.Length)
			{
				return File.GetLastWriteTimeUtc(path).Ticks == file.WriteTicks;
			}
			return false;
		}

		private static void CopyVerified(string source, string target, long length, CancellationToken cancellation)
		{
			string text = Path.Combine(Path.GetDirectoryName(target), ".p" + Guid.NewGuid().ToString("N").Substring(0, 8));
			long ticks = File.GetLastWriteTimeUtc(source).Ticks;
			string text2;
			using (FileStream fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.Read, 131072, FileOptions.SequentialScan))
			{
				using FileStream fileStream2 = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None, 131072);
				using SHA256 sHA = SHA256.Create();
				byte[] array = new byte[131072];
				if (fileStream.Read(array, 0, 4) != 4 || array[0] != 26 || array[1] != 69 || array[2] != 223 || array[3] != 163)
				{
					throw new InvalidDataException("Clip has no WebM/EBML header.");
				}
				fileStream.Position = 0L;
				long num = 0L;
				int num2;
				while ((num2 = fileStream.Read(array, 0, array.Length)) > 0)
				{
					cancellation.ThrowIfCancellationRequested();
					if (num + num2 > length)
					{
						throw new IOException("Clip grew during preparation.");
					}
					fileStream2.Write(array, 0, num2);
					sHA.TransformBlock(array, 0, num2, null, 0);
					num += num2;
				}
				sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
				text2 = ArchivePaths.Hex(sHA.Hash);
				fileStream2.Flush(flushToDisk: true);
				if (num != length || File.GetLastWriteTimeUtc(source).Ticks != ticks)
				{
					throw new IOException("Clip changed during preparation.");
				}
			}
			if (Hash(text) != text2)
			{
				throw new IOException("Playback copy verification failed.");
			}
			if (File.Exists(target))
			{
				if (Hash(target) != text2)
				{
					throw new IOException("Playback target already exists with different bytes.");
				}
				File.Delete(text);
			}
			else
			{
				File.Move(text, target);
			}
		}

		private static string Hash(string path)
		{
			using FileStream inputStream = File.OpenRead(path);
			using SHA256 sHA = SHA256.Create();
			return ArchivePaths.Hex(sHA.ComputeHash(inputStream));
		}
	}
	public sealed class DiscDefinition
	{
		public string AliasId { get; set; } = "";

		public string SourceVideoId { get; set; } = "";

		public List<string> ClipIds { get; set; } = new List<string>();

		public bool PayPending { get; set; } = true;

		public bool AllowRepeat { get; set; }

		public string SourceKey => TerminalProtocol.SourceKey(SourceVideoId, ClipIds);

		public string PaymentKey => TerminalProtocol.PaymentKey(SourceVideoId);

		public void Validate()
		{
			TerminalProtocol.RequireGuid(AliasId);
			TerminalProtocol.RequireGuid(SourceVideoId);
			if (Guid.Parse(AliasId) == Guid.Parse(SourceVideoId) || ClipIds == null || ClipIds.Count == 0 || ClipIds.Count > 128 || (AllowRepeat && !PayPending))
			{
				throw new InvalidDataException("Invalid disc definition.");
			}
			foreach (string clipId in ClipIds)
			{
				TerminalProtocol.RequireGuid(clipId);
			}
			if (ClipIds.Select(Guid.Parse).Distinct().Count() != ClipIds.Count)
			{
				throw new InvalidDataException("Duplicate clip identity.");
			}
		}
	}
	public sealed class DiscRoomState
	{
		public int Version { get; set; } = 1;

		public string Epoch { get; set; } = "";

		public int AuthorActor { get; set; }

		public List<DiscDefinition> Discs { get; set; } = new List<DiscDefinition>();

		public void Validate()
		{
			TerminalProtocol.RequireGuid(Epoch);
			if (Version != 1 || AuthorActor <= 0 || Discs == null || Discs.Count > 32)
			{
				throw new InvalidDataException("Invalid terminal room state.");
			}
			foreach (DiscDefinition disc in Discs)
			{
				(disc ?? throw new InvalidDataException("Null disc definition.")).Validate();
			}
			if (Discs.Select((DiscDefinition d) => Guid.Parse(d.AliasId)).Distinct().Count() != Discs.Count)
			{
				throw new InvalidDataException("Duplicate disc identity.");
			}
		}
	}
	public static class TerminalProtocol
	{
		public const int MaxPayloadLength = 524288;

		public static void RequireGuid(string id)
		{
			if (id == null || id.Length > 36 || !Guid.TryParse(id, out var result) || result == Guid.Empty)
			{
				throw new InvalidDataException("Invalid video identifier.");
			}
		}

		public static string SourceKey(string video, IEnumerable<string> clips)
		{
			RequireGuid(video);
			string[] array = clips.Select(delegate(string id)
			{
				RequireGuid(id);
				return Guid.Parse(id).ToString("N");
			}).ToArray();
			if (array.Length > 128)
			{
				throw new InvalidDataException("Too many clips.");
			}
			return ArchivePaths.HashText(Guid.Parse(video).ToString("N") + "|" + string.Join("|", array));
		}

		public static string PaymentKey(string video)
		{
			RequireGuid(video);
			return ArchivePaths.HashText("native-video:" + Guid.Parse(video).ToString("N"));
		}

		public static string Encode<T>(T value)
		{
			using MemoryStream memoryStream = new MemoryStream();
			new DataContractJsonSerializer(typeof(T)).WriteObject(memoryStream, value);
			if (memoryStream.Length > 524288)
			{
				throw new InvalidDataException("Terminal metadata limit reached.");
			}
			return Encoding.UTF8.GetString(memoryStream.ToArray());
		}

		public static T Decode<T>(string text) where T : class
		{
			if (text == null || text.Length > 524288)
			{
				throw new InvalidDataException("Invalid terminal metadata size.");
			}
			byte[] bytes = Encoding.UTF8.GetBytes(text);
			if (bytes.Length > 524288)
			{
				throw new InvalidDataException("Invalid terminal metadata size.");
			}
			using MemoryStream stream = new MemoryStream(bytes);
			return (T)(new DataContractJsonSerializer(typeof(T)).ReadObject(stream) ?? throw new InvalidDataException("Empty terminal metadata."));
		}
	}
}

BepInEx/plugins/RecordingArchiver/RecordingArchiver.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using EPOOutline;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using RecordingArchiver.Core;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Zorro.Core;
using Zorro.Recorder;

[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("RecordingArchiver")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.6.2.0")]
[assembly: AssemblyInformationalVersion("1.6.2")]
[assembly: AssemblyProduct("RecordingArchiver")]
[assembly: AssemblyTitle("RecordingArchiver")]
[assembly: AssemblyVersion("1.6.2.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace RecordingArchiver
{
	public sealed class ArchiveTerminal : MonoBehaviourPunCallbacks
	{
		private sealed class PreparedAlias
		{
			internal DiscDefinition Disc;

			internal string Directory = "";

			internal CameraRecording Recording;
		}

		private sealed class Preparation
		{
			internal DiscDefinition Disc;

			internal TerminalSnapshot Snapshot;

			internal Task<string> Task;
		}

		internal const string DefinitionsKey = "cwra.terminal.discs.v1";

		internal const string LedgerKey = "cwra.terminal.receipts.v1";

		private const string VersionKey = "cwra.terminal.version";

		private const string ReadyKey = "cwra.terminal.ready.v1";

		internal static ArchiveTerminal? Instance;

		private readonly TerminalCatalog catalog = new TerminalCatalog();

		private readonly Dictionary<Guid, PreparedAlias> aliases = new Dictionary<Guid, PreparedAlias>();

		private readonly Dictionary<Guid, Preparation> preparations = new Dictionary<Guid, Preparation>();

		private readonly HashSet<Guid> failedPreparations = new HashSet<Guid>();

		private CancellationTokenSource cancellation = new CancellationTokenSource();

		private DiscRoomState? roomState;

		private RewardLedger? ledger;

		private string sourceRoot = "";

		private string outputRoot = "";

		private string playbackRoot = "";

		private PlaybackWorkspace? workspace;

		private readonly List<Task> cleanups = new List<Task>();

		private bool initialized;

		private float nextTick;

		private Guid? waitingDisc;

		private float waitingUntil;

		private string status = "Abre una partida para consultar sus grabaciones.";

		private UploadVideoStation? station;

		private WorldArchiveTerminal? worldTerminal;

		private bool isOpen;

		private int closedFrame = -1;

		private bool payPending = true;

		private bool allowRepeat;

		private int selectedIncursion = -1;

		private string selectedKey = "";

		private string selectedPlayer = "";

		private GameObject? uiRoot;

		private GameObject? panel;

		private GameObject? ownedEventSystem;

		private TMP_Text? hint;

		private TMP_Text? statusLabel;

		private TMP_Text? detailLabel;

		private TMP_Text? paymentLabel;

		private TMP_Text? summaryLabel;

		private RectTransform? incursionList;

		private RectTransform? recordingList;

		private Toggle? pendingToggle;

		private Toggle? repeatToggle;

		private Button? prepareButton;

		private Button? closeButton;

		private Modal? nativeModal;

		private GameObject? nativeButton;

		private string renderedRows = "";

		private string renderedIncursions = "";

		private string renderedDetails = "";

		private Color Ink;

		private Color Paper;

		private Color Accent;

		internal bool RewardsReady
		{
			get
			{
				if (ledger != null)
				{
					return roomState != null;
				}
				return false;
			}
		}

		internal string RewardEpoch
		{
			get
			{
				DiscRoomState? obj = roomState;
				return ((obj != null) ? obj.Epoch : null) ?? Guid.NewGuid().ToString("N");
			}
		}

		internal bool IsOpen => isOpen;

		internal bool BlocksInput
		{
			get
			{
				if (!isOpen)
				{
					return Time.frameCount == closedFrame;
				}
				return true;
			}
		}

		internal bool CanOpenTerminal
		{
			get
			{
				if (initialized && !isOpen && Time.frameCount != closedFrame)
				{
					return NearStation();
				}
				return false;
			}
		}

		internal void Initialize(string source, string output)
		{
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Expected O, but got Unknown
			Instance = this;
			sourceRoot = ArchivePaths.NormalizeRoot(source);
			outputRoot = ArchivePaths.NormalizeRoot(output);
			string parent = Path.Combine(Path.GetTempPath(), "CWRA_Terminal");
			Plugin logger = Plugin.Instance;
			Action<Exception> report = delegate(Exception ex)
			{
				logger?.ReportTerminalError(ex);
			};
			workspace = new PlaybackWorkspace(parent, report);
			playbackRoot = workspace.Root;
			cleanups.Add(Task.Run(delegate
			{
				PlaybackWorkspace.RecoverAbandoned(parent, report);
			}));
			initialized = true;
			if (PhotonNetwork.InRoom)
			{
				Joined();
			}
		}

		internal bool IsAlias(Guid id)
		{
			if (!aliases.ContainsKey(id))
			{
				DiscRoomState? obj = roomState;
				if (obj == null)
				{
					return false;
				}
				return obj.Discs.Any((DiscDefinition d) => Guid.Parse(d.AliasId) == id);
			}
			return true;
		}

		internal DiscDefinition? Definition(Guid id)
		{
			if (!aliases.TryGetValue(id, out PreparedAlias value))
			{
				DiscRoomState? obj = roomState;
				if (obj == null)
				{
					return null;
				}
				return ((IEnumerable<DiscDefinition>)obj.Discs).FirstOrDefault((Func<DiscDefinition, bool>)((DiscDefinition d) => Guid.Parse(d.AliasId) == id));
			}
			return value.Disc;
		}

		internal string? AliasDirectory(Guid id)
		{
			if (!aliases.TryGetValue(id, out PreparedAlias value))
			{
				return null;
			}
			return value.Directory;
		}

		internal void Capture(CameraRecording recording, bool fullProduced = false)
		{
			catalog.Capture(recording, fullProduced);
		}

		internal void FolderOpenResult(string message)
		{
			status = message;
		}

		internal void MatchStarted(string evidence)
		{
			Close();
			selectedIncursion = -1;
			selectedPlayer = "";
			selectedKey = "";
			allowRepeat = false;
			payPending = true;
			catalog.Clear();
			if (evidence.StartsWith("RPC_QuotaFailed", StringComparison.Ordinal) && PhotonNetwork.IsMasterClient)
			{
				ResetLocal();
				CreateRoomState();
			}
		}

		public override void OnJoinedRoom()
		{
			if (initialized)
			{
				Joined();
			}
		}

		public override void OnLeftRoom()
		{
			ResetLocal();
			roomState = null;
			ledger = null;
		}

		private void Joined()
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Expected O, but got Unknown
			PhotonNetwork.LocalPlayer.SetCustomProperties(new Hashtable
			{
				[(object)"cwra.terminal.version"] = 3,
				[(object)"cwra.terminal.ready.v1"] = Array.Empty<string>()
			}, (Hashtable)null, (WebFlags)null);
			if (PhotonNetwork.IsMasterClient && !(((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties[(object)"cwra.terminal.discs.v1"] is string))
			{
				CreateRoomState();
			}
			else
			{
				ReadRoomState(changingMaster: true);
			}
			nextTick = 0f;
		}

		private void CreateRoomState()
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Expected O, but got Unknown
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Expected O, but got Unknown
			roomState = new DiscRoomState
			{
				Epoch = Guid.NewGuid().ToString("N"),
				AuthorActor = PhotonNetwork.LocalPlayer.ActorNumber
			};
			ledger = new RewardLedger(roomState.Epoch);
			PublishDefinitions();
		}

		private void ResetLocal()
		{
			//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
			Close();
			RemoveWorldTerminal();
			selectedIncursion = -1;
			selectedPlayer = "";
			selectedKey = "";
			allowRepeat = false;
			payPending = true;
			Task[] array = ((IEnumerable<Preparation>)preparations.Values).Select((Func<Preparation, Task>)((Preparation p) => p.Task)).ToArray();
			cancellation.Cancel();
			cancellation.Dispose();
			cancellation = new CancellationTokenSource();
			foreach (PreparedAlias value in aliases.Values)
			{
				if ((Object)(object)value.Recording != (Object)null)
				{
					if (PhotonNetwork.InRoom)
					{
						RecordingsHandler.GetRecordings().Remove(value.Recording.videoHandle);
					}
					Object.Destroy((Object)(object)((Component)value.Recording).gameObject);
				}
			}
			if (roomState != null && workspace != null)
			{
				cleanups.Add(workspace.RemoveEpoch(roomState.Epoch, array));
			}
			aliases.Clear();
			preparations.Clear();
			failedPreparations.Clear();
			catalog.Clear();
			waitingDisc = null;
			status = "Sin grabaciones preparadas en esta visita a la sala.";
		}

		public override void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged)
		{
			if (initialized && PhotonNetwork.InRoom)
			{
				if (!PhotonNetwork.IsMasterClient && (((Dictionary<object, object>)(object)propertiesThatChanged).ContainsKey((object)"cwra.terminal.discs.v1") || ((Dictionary<object, object>)(object)propertiesThatChanged).ContainsKey((object)"cwra.terminal.receipts.v1")))
				{
					ReadRoomState(changingMaster: false);
				}
				nextTick = 0f;
			}
		}

		public override void OnMasterClientSwitched(Player newMasterClient)
		{
			if (initialized && PhotonNetwork.InRoom)
			{
				ReadRoomState(changingMaster: true);
				if (newMasterClient.IsLocal && roomState != null)
				{
					roomState.AuthorActor = newMasterClient.ActorNumber;
					PublishDefinitions();
				}
			}
		}

		private void ReadRoomState(bool changingMaster)
		{
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Expected O, but got Unknown
			try
			{
				if (!(((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties[(object)"cwra.terminal.discs.v1"] is string text))
				{
					return;
				}
				DiscRoomState val = TerminalProtocol.Decode<DiscRoomState>(text);
				val.Validate();
				if (!changingMaster && val.AuthorActor != PhotonNetwork.MasterClient.ActorNumber)
				{
					return;
				}
				if (roomState != null && val.Epoch != roomState.Epoch)
				{
					ResetLocal();
				}
				roomState = val;
				if (((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties[(object)"cwra.terminal.receipts.v1"] is string text2)
				{
					RewardLedger val2 = RewardLedger.Restore(TerminalProtocol.Decode<RewardLedgerState>(text2));
					if (val2.Epoch != val.Epoch)
					{
						throw new InvalidDataException("Reward epoch mismatch.");
					}
					ledger = val2;
				}
				else
				{
					ledger = new RewardLedger(val.Epoch);
				}
			}
			catch (Exception ex) when (ex is IOException || ex is ArgumentException || ex is SerializationException)
			{
				status = "No se pudo validar la sincronización del terminal. No se prepararán discos.";
				Plugin.Instance?.ReportTerminalError(ex);
				roomState = null;
				ledger = null;
			}
		}

		private bool PublishDefinitions()
		{
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: 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)
			//IL_007f: Expected O, but got Unknown
			if (!PhotonNetwork.IsMasterClient || roomState == null || ledger == null)
			{
				return false;
			}
			roomState.AuthorActor = PhotonNetwork.LocalPlayer.ActorNumber;
			roomState.Validate();
			bool num = PhotonNetwork.CurrentRoom.SetCustomProperties(new Hashtable
			{
				[(object)"cwra.terminal.discs.v1"] = TerminalProtocol.Encode<DiscRoomState>(roomState),
				[(object)"cwra.terminal.receipts.v1"] = TerminalProtocol.Encode<RewardLedgerState>(ledger.Export())
			}, (Hashtable)null, (WebFlags)null);
			if (!num)
			{
				status = "No se pudo enviar la preparación del disco. Reintenta cuando vuelva la conexión.";
			}
			return num;
		}

		internal void AttachRewardReceipt(Hashtable properties)
		{
			if (ledger != null && roomState != null)
			{
				properties[(object)"cwra.terminal.receipts.v1"] = TerminalProtocol.Encode<RewardLedgerState>(ledger.Export());
			}
		}

		internal bool ApplyReward(RewardTicket ticket, RewardPhase phase)
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			if (ledger == null)
			{
				return Definition(Guid.Parse(ticket.Token)) == null;
			}
			try
			{
				return ledger.TryApply(ticket, phase);
			}
			catch (Exception ex) when (ex is IOException || ex is ArgumentException)
			{
				status = "No se pudo registrar el cobro; se ha impedido para evitar duplicarlo.";
				Plugin.Instance?.ReportTerminalError(ex);
				return false;
			}
		}

		internal bool CanReward(RewardTicket ticket, RewardPhase phase)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			RewardLedger? obj = ledger;
			if (obj == null)
			{
				return false;
			}
			return obj.CanApply(ticket, phase);
		}

		private void Update()
		{
			if (!initialized)
			{
				return;
			}
			HandleInput();
			if (!PhotonNetwork.InRoom)
			{
				if (isOpen)
				{
					Close();
				}
			}
			else if (!(Time.unscaledTime < nextTick))
			{
				nextTick = Time.unscaledTime + 1f;
				try
				{
					station = Object.FindFirstObjectByType<UploadVideoStation>();
					catalog.Refresh();
					PollPreparations();
					PrepareDefinitions();
					FinishDisc();
					ReconcileWorldTerminal();
				}
				catch (Exception ex) when (ex is IOException || ex is InvalidOperationException || ex is ArgumentException || ex is SerializationException)
				{
					status = "No se pudo actualizar el terminal. Revisa el registro y pulsa Actualizar.";
					Plugin.Instance?.ReportTerminalError(ex);
				}
			}
		}

		private void PrepareDefinitions()
		{
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			if (roomState == null)
			{
				return;
			}
			foreach (DiscDefinition disc in roomState.Discs)
			{
				Guid guid = Guid.Parse(disc.AliasId);
				if (aliases.TryGetValue(guid, out PreparedAlias value))
				{
					if (!RecordingsHandler.GetRecordings().ContainsKey(value.Recording.videoHandle))
					{
						RecordingsHandler.GetRecordings().Add(value.Recording.videoHandle, value.Recording);
					}
				}
				else
				{
					if (preparations.ContainsKey(guid) || failedPreparations.Contains(guid))
					{
						continue;
					}
					TerminalSnapshot snapshot = catalog.Find(disc);
					if (snapshot != null)
					{
						string workspace = Path.Combine(playbackRoot, Guid.Parse(roomState.Epoch).ToString("N"));
						CancellationToken token = cancellation.Token;
						Preparation preparation = new Preparation
						{
							Disc = disc,
							Snapshot = snapshot
						};
						preparation.Task = Task.Run(() => TerminalMedia.Prepare(disc, (IReadOnlyList<PlaybackFile>)snapshot.Files, sourceRoot, outputRoot, workspace, token, snapshot.FullRecording), token);
						preparations.Add(guid, preparation);
					}
				}
			}
		}

		internal void ReconcileWorldTerminal()
		{
			if (!PhotonNetwork.InRoom || !PhotonGameLobbyHandler.IsSurface || (Object)(object)station == (Object)null)
			{
				RemoveWorldTerminal();
			}
			else if (!((Object)(object)worldTerminal != (Object)null) || !((Object)(object)worldTerminal.Station == (Object)(object)station))
			{
				RemoveWorldTerminal();
				worldTerminal = WorldArchiveTerminal.Create(this, station);
			}
		}

		private void RemoveWorldTerminal()
		{
			if ((Object)(object)worldTerminal != (Object)null)
			{
				((Component)worldTerminal).gameObject.SetActive(false);
				Object.Destroy((Object)(object)((Component)worldTerminal).gameObject);
			}
			worldTerminal = null;
		}

		private void PollPreparations()
		{
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_0183: Expected O, but got Unknown
			bool flag = false;
			KeyValuePair<Guid, Preparation>[] array = preparations.ToArray();
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<Guid, Preparation> keyValuePair = array[i];
				if (keyValuePair.Value.Task.IsCompleted)
				{
					preparations.Remove(keyValuePair.Key);
					try
					{
						Preparation value = keyValuePair.Value;
						string result = value.Task.GetAwaiter().GetResult();
						aliases.Add(keyValuePair.Key, new PreparedAlias
						{
							Disc = value.Disc,
							Directory = result,
							Recording = TerminalCatalog.Register(value.Disc, value.Snapshot, result)
						});
						flag = true;
					}
					catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is OperationCanceledException || ex is InvalidOperationException)
					{
						failedPreparations.Add(keyValuePair.Key);
						status = "No se pudo preparar un clip; el disco no se entregará hasta que todos tengan los archivos.";
						Plugin.Instance?.ReportTerminalError(ex);
					}
				}
			}
			if (flag)
			{
				PhotonNetwork.LocalPlayer.SetCustomProperties(new Hashtable { [(object)"cwra.terminal.ready.v1"] = aliases.Keys.Select((Guid id) => id.ToString("N")).ToArray() }, (Hashtable)null, (WebFlags)null);
			}
		}

		private bool AllClientsCompatible()
		{
			return PhotonNetwork.PlayerList.All((Player p) => p.CustomProperties[(object)"cwra.terminal.version"] is int num && num == 3);
		}

		private void StartDisc(TerminalSnapshot snapshot)
		{
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: 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_00c7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fc: 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_0120: Expected O, but got Unknown
			if (!PhotonNetwork.IsMasterClient || roomState == null || ledger == null || waitingDisc.HasValue)
			{
				return;
			}
			if (!AllClientsCompatible())
			{
				status = "Todos los jugadores deben tener Recording Archiver 1.6.1 con el terminal activado.";
				return;
			}
			if (roomState.Discs.Count >= 32)
			{
				status = "Esta partida ya tiene 32 discos del terminal. No se crearán más definiciones.";
				return;
			}
			PlayerInventory val = default(PlayerInventory);
			InventorySlot val2 = default(InventorySlot);
			if ((Object)(object)Player.localPlayer == (Object)null || !Player.localPlayer.TryGetInventory(ref val) || !val.TryGetSlot(3, ref val2) || (Object)(object)val2.ItemInSlot.item != (Object)null)
			{
				status = "Deja libre la ranura del disco antes de prepararlo.";
				return;
			}
			DiscDefinition val3 = new DiscDefinition
			{
				AliasId = Guid.NewGuid().ToString("N"),
				SourceVideoId = snapshot.VideoId,
				ClipIds = snapshot.Files.Select((PlaybackFile f) => f.ClipId).ToList(),
				PayPending = payPending,
				AllowRepeat = (payPending && allowRepeat)
			};
			val3.Validate();
			roomState.Discs.Add(val3);
			if (!PublishDefinitions())
			{
				roomState.Discs.Remove(val3);
				return;
			}
			waitingDisc = Guid.Parse(val3.AliasId);
			waitingUntil = Time.unscaledTime + 60f;
			status = "Preparando clips y esperando la confirmación de todos los jugadores…";
			nextTick = 0f;
		}

		private void FinishDisc()
		{
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d2: Expected O, but got Unknown
			//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00db: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ea: Expected O, but got Unknown
			//IL_00f3: Unknown result type (might be due to invalid IL or missing references)
			if (!waitingDisc.HasValue || !PhotonNetwork.IsMasterClient)
			{
				return;
			}
			Guid value = waitingDisc.Value;
			string token = value.ToString("N");
			if (!PhotonNetwork.PlayerList.All((Player p) => p.CustomProperties[(object)"cwra.terminal.ready.v1"] is string[] source && source.Contains(token)) || !aliases.ContainsKey(value))
			{
				if (Time.unscaledTime >= waitingUntil)
				{
					waitingDisc = null;
					status = "Preparación cancelada: faltan clips o confirmaciones de otros clientes. No se ha entregado ningún disco.";
				}
				return;
			}
			ExtractVideoMachine val = Object.FindFirstObjectByType<ExtractVideoMachine>();
			PlayerInventory val2 = default(PlayerInventory);
			if ((Object)(object)val == (Object)null || (Object)(object)Player.localPlayer == (Object)null || !Player.localPlayer.TryGetInventory(ref val2))
			{
				waitingDisc = null;
				status = "No se encontró el equipo de extracción o el inventario.";
				return;
			}
			ItemInstanceData val3 = new ItemInstanceData(Guid.NewGuid());
			val3.AddDataEntry((ItemDataEntry)new FlashcardEntry
			{
				videoID = new VideoHandle(value)
			});
			InventorySlot val4 = default(InventorySlot);
			if (!val2.TryAddItem(new ItemDescriptor(val.m_flashCardItem, val3), ref val4))
			{
				waitingDisc = null;
				status = "La ranura del disco se ha ocupado. Libérala y vuelve a preparar un disco.";
				return;
			}
			Player.localPlayer.refs.view.RPC("RPC_SelectSlot", Player.localPlayer.refs.view.Owner, new object[1] { val4.SlotID });
			waitingDisc = null;
			allowRepeat = false;
			status = "Disco preparado en tu inventario. Llévalo al equipo de subida para reproducirlo y evaluar sus visitas originales.";
		}

		private void OnDestroy()
		{
			Close();
			RemoveWorldTerminal();
			cancellation.Cancel();
			cancellation.Dispose();
			Task[] array = ((IEnumerable<Preparation>)preparations.Values).Select((Func<Preparation, Task>)((Preparation p) => p.Task)).Concat(cleanups).ToArray();
			if (workspace != null)
			{
				workspace.Close(array);
			}
			if ((Object)(object)uiRoot != (Object)null)
			{
				Object.Destroy((Object)(object)uiRoot);
			}
			if ((Object)(object)ownedEventSystem != (Object)null)
			{
				Object.Destroy((Object)(object)ownedEventSystem);
			}
			if (Instance == this)
			{
				Instance = null;
			}
		}

		private bool NearStation()
		{
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			if (PhotonNetwork.InRoom && PhotonGameLobbyHandler.IsSurface && (Object)(object)station != (Object)null && (Object)(object)Player.localPlayer != (Object)null && !Player.localPlayer.data.dead)
			{
				if (!(Vector3.Distance(Player.localPlayer.HeadPosition(), ((Component)station).transform.position) <= 4f))
				{
					if ((Object)(object)worldTerminal != (Object)null)
					{
						return Vector3.Distance(Player.localPlayer.HeadPosition(), ((Component)worldTerminal).transform.position) <= 4f;
					}
					return false;
				}
				return true;
			}
			return false;
		}

		private void HandleInput()
		{
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			if (isOpen && (!NearStation() || Input.GetKeyDown((KeyCode)27)))
			{
				Close();
				return;
			}
			if (isOpen && Input.GetKeyDown((KeyCode)9))
			{
				MoveFocus((!Input.GetKey((KeyCode)304) && !Input.GetKey((KeyCode)303)) ? 1 : (-1));
			}
			KeyboardShortcut terminalKey = Plugin.Instance.TerminalKey;
			if (((KeyboardShortcut)(ref terminalKey)).IsDown())
			{
				if (isOpen)
				{
					Close();
				}
				else
				{
					OpenTerminal();
				}
			}
		}

		internal void OpenTerminal()
		{
			if (CanOpenTerminal)
			{
				EscapeMenu val = Object.FindFirstObjectByType<EscapeMenu>();
				if ((!((Object)(object)val != (Object)null) || !val.Open) && (!((Object)(object)nativeModal != (Object)null) || !nativeModal.Open))
				{
					isOpen = true;
					payPending = true;
					allowRepeat = false;
					nextTick = 0f;
					renderedDetails = "";
					status = "Selecciona una incursión, un jugador y sus clips.";
				}
			}
		}

		private void MoveFocus(int direction)
		{
			//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0101: Unknown result type (might be due to invalid IL or missing references)
			//IL_0106: Unknown result type (might be due to invalid IL or missing references)
			//IL_0115: Unknown result type (might be due to invalid IL or missing references)
			//IL_011a: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)panel == (Object)null || (Object)(object)EventSystem.current == (Object)null)
			{
				return;
			}
			Selectable[] array = (from s in panel.GetComponentsInChildren<Selectable>()
				where s.IsInteractable()
				select s).ToArray();
			if (array.Length == 0)
			{
				return;
			}
			int num = Array.FindIndex(array, (Selectable s) => (Object)(object)((Component)s).gameObject == (Object)(object)EventSystem.current.currentSelectedGameObject);
			Selectable val = array[(num + direction + array.Length) % array.Length];
			val.Select();
			EscapeMenuButton component = ((Component)val).GetComponent<EscapeMenuButton>();
			if (component != null)
			{
				component.OnSelected();
			}
			ScrollRect componentInParent = ((Component)val).GetComponentInParent<ScrollRect>();
			if ((Object)(object)componentInParent != (Object)null)
			{
				Rect rect = componentInParent.content.rect;
				float height = ((Rect)(ref rect)).height;
				rect = componentInParent.viewport.rect;
				if (height > ((Rect)(ref rect)).height)
				{
					float num2 = 0f - ((RectTransform)((Component)val).transform).anchoredPosition.y;
					rect = componentInParent.content.rect;
					float height2 = ((Rect)(ref rect)).height;
					rect = componentInParent.viewport.rect;
					componentInParent.verticalNormalizedPosition = 1f - Mathf.Clamp01(num2 / (height2 - ((Rect)(ref rect)).height));
				}
			}
		}

		private void Close()
		{
			if (isOpen)
			{
				closedFrame = Time.frameCount;
			}
			isOpen = false;
			if ((Object)(object)panel != (Object)null)
			{
				panel.SetActive(false);
			}
		}

		internal void CloseFromGameMenu()
		{
			Close();
		}

		private void LateUpdate()
		{
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0172: 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_00d5: Unknown result type (might be due to invalid IL or missing references)
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			//IL_014e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0227: Unknown result type (might be due to invalid IL or missing references)
			//IL_0231: Expected O, but got Unknown
			if (!initialized)
			{
				return;
			}
			if ((Object)(object)uiRoot == (Object)null)
			{
				BuildUi();
			}
			if (isOpen && nativeModal.Open)
			{
				Close();
			}
			((Component)hint).gameObject.SetActive(!isOpen && NearStation());
			UploadCompleteUI val = (((Object)(object)station != (Object)null) ? station.m_uploadCompleteUI : null);
			TelevisionPlayback televisionPlayback = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent<TelevisionPlayback>() : null);
			hint.text = ((televisionPlayback == null || !televisionPlayback.Active) ? (Plugin.ShortcutLabel(Plugin.Instance.TerminalKey) + " · Archivo de grabaciones") : ((!televisionPlayback.AllClientsCompatible) ? ("Controles de la tele: todos necesitan Recording Archiver 1.3 · " + Plugin.ShortcutLabel(Plugin.Instance.TerminalKey) + " Archivo") : (televisionPlayback.CanControl ? (Plugin.ShortcutLabel(Plugin.Instance.PauseKey) + " · " + (televisionPlayback.Paused ? "Reanudar" : "Pausar") + "   " + Plugin.ShortcutLabel(Plugin.Instance.FinishKey) + " · Terminar   Interactuar · Terminar\n" + Plugin.ShortcutLabel(Plugin.Instance.TerminalKey) + " · Archivo de grabaciones") : ("El anfitrión controla la reproducción · " + Plugin.ShortcutLabel(Plugin.Instance.TerminalKey) + " Archivo"))));
			TMP_Text? obj = hint;
			obj.text += " · v1.6.2";
			bool flag = isOpen && !panel.activeSelf;
			panel.SetActive(isOpen);
			if (!isOpen)
			{
				return;
			}
			Cursor.visible = true;
			Cursor.lockState = (CursorLockMode)0;
			if ((Object)(object)EventSystem.current == (Object)null && (Object)(object)ownedEventSystem == (Object)null)
			{
				ownedEventSystem = new GameObject("RecordingArchiver EventSystem", new Type[2]
				{
					typeof(EventSystem),
					typeof(StandaloneInputModule)
				});
				Object.DontDestroyOnLoad((Object)(object)ownedEventSystem);
			}
			if (flag)
			{
				renderedDetails = "";
				((Selectable)closeButton).Select();
				EscapeMenuButton component = ((Component)closeButton).GetComponent<EscapeMenuButton>();
				if (component != null)
				{
					component.OnSelected();
				}
			}
			RenderUi();
		}

		private RectTransform Rect(Transform parent, string name, float x, float y, float width, float height)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: 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)
			RectTransform component = new GameObject(name, new Type[1] { typeof(RectTransform) }).GetComponent<RectTransform>();
			((Transform)component).SetParent(parent, false);
			Vector2 val = default(Vector2);
			((Vector2)(ref val))..ctor(0f, 1f);
			component.anchorMax = val;
			component.anchorMin = val;
			component.pivot = new Vector2(0f, 1f);
			component.anchoredPosition = new Vector2(x, 0f - y);
			component.sizeDelta = new Vector2(width, height);
			return component;
		}

		private TMP_Text Label(Transform parent, string text, float x, float y, float width, float height, int size = 18)
		{
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			TextMeshProUGUI obj = ((Component)Rect(parent, "Text", x, y, width, height)).gameObject.AddComponent<TextMeshProUGUI>();
			((TMP_Text)obj).font = ((TMP_Text)nativeModal.BodyText).font;
			((TMP_Text)obj).fontSharedMaterial = ((TMP_Text)nativeModal.BodyText).fontSharedMaterial;
			((TMP_Text)obj).text = text;
			((TMP_Text)obj).fontSize = size;
			((Graphic)obj).color = Ink;
			((TMP_Text)obj).richText = false;
			((Graphic)obj).raycastTarget = false;
			((TMP_Text)obj).overflowMode = (TextOverflowModes)1;
			return (TMP_Text)(object)obj;
		}

		private Button Button(Transform parent, string text, float x, float y, float width, float height, Action action, string? controlName = null)
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: 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_0099: Expected O, but got Unknown
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_009f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_0118: Unknown result type (might be due to invalid IL or missing references)
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_023f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0249: Expected O, but got Unknown
			//IL_01ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_0206: Unknown result type (might be due to invalid IL or missing references)
			//IL_020b: Unknown result type (might be due to invalid IL or missing references)
			//IL_020c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0213: Unknown result type (might be due to invalid IL or missing references)
			//IL_0198: 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_01b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01de: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = Object.Instantiate<GameObject>(nativeButton, parent, false);
			((Object)val).name = controlName ?? text;
			RectTransform val2 = (RectTransform)val.transform;
			Vector2 val3 = default(Vector2);
			((Vector2)(ref val3))..ctor(0f, 1f);
			val2.anchorMax = val3;
			val2.anchorMin = val3;
			val2.pivot = new Vector2(0f, 1f);
			val2.anchoredPosition = new Vector2(x, 0f - y);
			val2.sizeDelta = new Vector2(width, height);
			Button component = val.GetComponent<Button>();
			component.onClick = new ButtonClickedEvent();
			Navigation navigation = ((Selectable)component).navigation;
			((Navigation)(ref navigation)).mode = (Mode)3;
			((Selectable)component).navigation = navigation;
			TMP_Text componentInChildren = val.GetComponentInChildren<TMP_Text>(true);
			componentInChildren.text = text;
			componentInChildren.richText = false;
			componentInChildren.fontSize = 19f;
			componentInChildren.enableAutoSizing = false;
			componentInChildren.alignment = (TextAlignmentOptions)4097;
			componentInChildren.overflowMode = (TextOverflowModes)1;
			componentInChildren.rectTransform.anchorMin = Vector2.zero;
			componentInChildren.rectTransform.anchorMax = Vector2.one;
			componentInChildren.rectTransform.offsetMin = new Vector2(12f, 4f);
			componentInChildren.rectTransform.offsetMax = new Vector2(-30f, -4f);
			Image[] componentsInChildren = val.GetComponentsInChildren<Image>(true);
			foreach (Image val4 in componentsInChildren)
			{
				RectTransform rectTransform = ((Graphic)val4).rectTransform;
				if ((Object)(object)val4.sprite != (Object)null && ((Object)val4.sprite).name == "arrow_right")
				{
					((Vector2)(ref val3))..ctor(1f, 0.5f);
					rectTransform.anchorMax = val3;
					rectTransform.anchorMin = val3;
					rectTransform.pivot = new Vector2(1f, 0.5f);
					rectTransform.anchoredPosition = new Vector2(-10f, 0f);
					rectTransform.sizeDelta = new Vector2(16f, 16f);
				}
				else
				{
					rectTransform.anchorMin = Vector2.zero;
					rectTransform.anchorMax = Vector2.one;
					val3 = (rectTransform.offsetMin = (rectTransform.offsetMax = Vector2.zero));
				}
			}
			val.AddComponent<CanvasGroup>();
			((UnityEvent)component.onClick).AddListener((UnityAction)delegate
			{
				action();
			});
			return component;
		}

		private void MarkSelected(Button button)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			Transform transform = ((Component)button).transform;
			Rect rect = ((RectTransform)((Component)button).transform).rect;
			Image obj = ((Component)Rect(transform, "Current selection", 0f, 0f, 4f, ((Rect)(ref rect)).height)).gameObject.AddComponent<Image>();
			((Graphic)obj).color = Accent;
			((Graphic)obj).raycastTarget = false;
		}

		private Toggle Check(Transform parent, string text, float y, bool initial, Action<bool> changed)
		{
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			RectTransform val = Rect(parent, text, 0f, y, 416f, 46f);
			((Graphic)((Component)val).gameObject.AddComponent<Image>()).color = Color.clear;
			Image val2 = ((Component)Rect((Transform)(object)val, "Box", 0f, 10f, 24f, 24f)).gameObject.AddComponent<Image>();
			((Graphic)val2).color = Ink;
			((Graphic)((Component)Rect(((Component)val2).transform, "Inside", 2f, 2f, 20f, 20f)).gameObject.AddComponent<Image>()).color = Paper;
			TMP_Text val3 = Label(((Component)val2).transform, "X", 2f, 1f, 20f, 22f, 20);
			val3.alignment = (TextAlignmentOptions)514;
			Toggle obj = ((Component)val).gameObject.AddComponent<Toggle>();
			((Selectable)obj).targetGraphic = (Graphic)(object)val2;
			obj.graphic = (Graphic)(object)val3;
			obj.isOn = initial;
			Label((Transform)(object)val, text, 34f, 0f, 382f, 46f, 20);
			((Component)val).gameObject.AddComponent<CanvasGroup>();
			((UnityEvent<bool>)(object)obj.onValueChanged).AddListener((UnityAction<bool>)delegate(bool value)
			{
				changed(value);
			});
			return obj;
		}

		private RectTransform Scroll(Transform parent, string name, float x, float y, float width, float height)
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Expected O, but got Unknown
			RectTransform val = Rect(parent, name, x, y, width, height);
			((Graphic)((Component)val).gameObject.AddComponent<Image>()).color = new Color(1f, 1f, 1f, 0.025f);
			((Component)val).gameObject.AddComponent<Mask>().showMaskGraphic = true;
			RectTransform val2 = Rect((Transform)(object)val, "Content", 0f, 0f, width - 4f, 0f);
			VerticalLayoutGroup obj = ((Component)val2).gameObject.AddComponent<VerticalLayoutGroup>();
			((HorizontalOrVerticalLayoutGroup)obj).spacing = 6f;
			((LayoutGroup)obj).padding = new RectOffset(4, 4, 4, 4);
			((HorizontalOrVerticalLayoutGroup)obj).childControlHeight = false;
			((HorizontalOrVerticalLayoutGroup)obj).childControlWidth = true;
			((HorizontalOrVerticalLayoutGroup)obj).childForceExpandHeight = false;
			((Component)val2).gameObject.AddComponent<ContentSizeFitter>().verticalFit = (FitMode)2;
			ScrollRect obj2 = ((Component)val).gameObject.AddComponent<ScrollRect>();
			obj2.viewport = val;
			obj2.content = val2;
			obj2.horizontal = false;
			obj2.movementType = (MovementType)2;
			return val2;
		}

		private void BuildUi()
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: 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)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Expected O, but got Unknown
			//IL_0113: Unknown result type (might be due to invalid IL or missing references)
			//IL_0162: Unknown result type (might be due to invalid IL or missing references)
			//IL_0169: Unknown result type (might be due to invalid IL or missing references)
			//IL_016b: Unknown result type (might be due to invalid IL or missing references)
			//IL_016c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0172: 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_01f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0206: Unknown result type (might be due to invalid IL or missing references)
			//IL_022f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0335: Unknown result type (might be due to invalid IL or missing references)
			//IL_059c: Unknown result type (might be due to invalid IL or missing references)
			//IL_05f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0631: Unknown result type (might be due to invalid IL or missing references)
			//IL_0642: Unknown result type (might be due to invalid IL or missing references)
			//IL_0647: Unknown result type (might be due to invalid IL or missing references)
			nativeModal = RetrievableResourceSingleton<Modal>.Instance;
			nativeButton = nativeModal.m_buttonPrefab;
			Ink = SingletonAsset<UIColorDatabase>.Instance.WhiteColor;
			Paper = SingletonAsset<UIColorDatabase>.Instance.BlackColor;
			Image[] componentsInChildren = nativeButton.GetComponentsInChildren<Image>(true);
			Image? obj = ((IEnumerable<Image>)componentsInChildren).FirstOrDefault((Func<Image, bool>)((Image image) => ((Object)image).name == "SELECT"));
			Accent = ((obj != null) ? ((Graphic)obj).color : Ink);
			uiRoot = new GameObject("RecordingArchiver Terminal", new Type[4]
			{
				typeof(RectTransform),
				typeof(Canvas),
				typeof(CanvasScaler),
				typeof(GraphicRaycaster)
			});
			Object.DontDestroyOnLoad((Object)(object)uiRoot);
			Canvas component = uiRoot.GetComponent<Canvas>();
			component.renderMode = (RenderMode)0;
			component.sortingOrder = 30000;
			CanvasScaler component2 = uiRoot.GetComponent<CanvasScaler>();
			component2.uiScaleMode = (ScaleMode)1;
			component2.referenceResolution = new Vector2(1000f, 740f);
			component2.screenMatchMode = (ScreenMatchMode)1;
			RectTransform val = Rect(uiRoot.transform, "Panel", 0f, 0f, 960f, 700f);
			Vector2 val2 = default(Vector2);
			((Vector2)(ref val2))..ctor(0.5f, 0.5f);
			val.pivot = val2;
			Vector2 anchorMin = (val.anchorMax = val2);
			val.anchorMin = anchorMin;
			val.anchoredPosition = Vector2.zero;
			panel = ((Component)val).gameObject;
			Image[] array = (from image in ((Component)nativeModal).GetComponentsInChildren<Image>(true)
				where !((Component)image).transform.IsChildOf(nativeModal.ButtonContainer)
				select image).ToArray();
			foreach (Image val4 in array)
			{
				Image obj2 = ((Component)Rect((Transform)(object)val, "Native background", 0f, 0f, 960f, 700f)).gameObject.AddComponent<Image>();
				obj2.sprite = val4.sprite;
				obj2.type = val4.type;
				((Graphic)obj2).color = ((Graphic)val4).color;
				((Graphic)obj2).raycastTarget = false;
			}
			((Graphic)panel.AddComponent<Image>()).color = Color.clear;
			TMP_Text obj3 = Label((Transform)(object)val, "Archivo de grabaciones", 28f, 22f, 730f, 56f, 42);
			obj3.font = ((TMP_Text)nativeModal.TitleText).font;
			obj3.fontSharedMaterial = ((TMP_Text)nativeModal.TitleText).fontSharedMaterial;
			summaryLabel = Label((Transform)(object)val, "Partida actual", 28f, 88f, 850f, 28f);
			closeButton = Button((Transform)(object)val, "Cerrar · Esc", 785f, 28f, 150f, 42f, Close);
			((Component)closeButton).GetComponentInChildren<TMP_Text>().text = "Cerrar";
			Image obj4 = ((Component)Rect((Transform)(object)val, "Divider", 28f, 128f, 904f, 1f)).gameObject.AddComponent<Image>();
			((Graphic)obj4).color = new Color(1f, 1f, 1f, 0.3f);
			((Graphic)obj4).raycastTarget = false;
			Label((Transform)(object)val, "Incursión", 28f, 143f, 145f, 30f, 20);
			Label((Transform)(object)val, "Jugadores y clips", 196f, 143f, 298f, 30f, 20);
			Label((Transform)(object)val, "Preparar disco", 516f, 143f, 416f, 30f, 20);
			incursionList = Scroll((Transform)(object)val, "Incursiones", 26f, 180f, 145f, 380f);
			recordingList = Scroll((Transform)(object)val, "Grabaciones", 188f, 180f, 306f, 380f);
			RectTransform parent = Rect((Transform)(object)val, "Detalles", 516f, 180f, 416f, 380f);
			detailLabel = Label((Transform)(object)parent, "", 0f, 0f, 416f, 68f, 20);
			paymentLabel = Label((Transform)(object)parent, "", 0f, 76f, 416f, 30f, 20);
			pendingToggle = Check((Transform)(object)parent, "Cobrar visitas e ingresos pendientes", 112f, initial: true, delegate(bool value)
			{
				payPending = value;
				if (!value)
				{
					allowRepeat = false;
					repeatToggle.SetIsOnWithoutNotify(false);
				}
			});
			Label((Transform)(object)parent, "Clips y secuencias de una cámara comparten el mismo pago original.", 0f, 163f, 416f, 48f, 17);
			repeatToggle = Check((Transform)(object)parent, "Permitir un cobro adicional con este disco", 216f, initial: false, delegate(bool value)
			{
				allowRepeat = value && payPending;
			});
			Label((Transform)(object)parent, "Autoriza otro pago, una vez por disco. Reintentar el mismo disco no vuelve a cobrar.", 0f, 267f, 416f, 48f, 17);
			prepareButton = Button((Transform)(object)parent, "Preparar disco en el inventario", 0f, 330f, 416f, 48f, delegate
			{
				TerminalSnapshot terminalSnapshot = catalog.Snapshots.FirstOrDefault((TerminalSnapshot s) => s.SelectionKey == selectedKey);
				if (terminalSnapshot != null)
				{
					StartDisc(terminalSnapshot);
				}
			});
			statusLabel = Label((Transform)(object)val, "", 28f, 578f, 904f, 48f);
			((Component)Button((Transform)(object)val, "Actualizar grabaciones", 28f, 642f, 250f, 38f, delegate
			{
				failedPreparations.Clear();
				nextTick = 0f;
				status = "Actualizando grabaciones y archivos disponibles…";
			})).GetComponentInChildren<TMP_Text>().text = "Actualizar";
			Button((Transform)(object)val, "Carpeta de partidas · " + Plugin.ShortcutLabel(Plugin.Instance.FolderKey), 300f, 642f, 340f, 38f, delegate
			{
				Plugin.Instance?.OpenArchiveFolder();
			});
			hint = Label(uiRoot.transform, Plugin.ShortcutLabel(Plugin.Instance.TerminalKey) + " · Archivo de grabaciones", 20f, 20f, 940f, 64f);
			((Graphic)hint).color = Color.white;
			hint.outlineColor = Color32.op_Implicit(Paper);
			hint.outlineWidth = 0.2f;
			panel.SetActive(false);
		}

		private static void ClearChildren(RectTransform parent)
		{
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			foreach (Transform item in (Transform)parent)
			{
				((Component)item).gameObject.SetActive(false);
				Object.Destroy((Object)(object)((Component)item).gameObject);
			}
		}

		private void RenderUi()
		{
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			TerminalSnapshot[] source = catalog.Snapshots.ToArray();
			int num = source.Count((TerminalSnapshot s) => !s.IsSequence);
			summaryLabel.text = "Partida actual · " + num + ((num == 1) ? " clip" : " clips") + " · " + Plugin.ShortcutLabel(Plugin.Instance.FolderKey) + " Carpeta de partidas";
			int[] array = (from n in source.Select((TerminalSnapshot s) => s.Incursion).Distinct()
				orderby n
				select n).ToArray();
			if (!array.Contains(selectedIncursion))
			{
				selectedIncursion = ((array.Length == 0) ? (-1) : array[0]);
				selectedPlayer = "";
				selectedKey = "";
			}
			string text = selectedIncursion + ":" + string.Join(",", array);
			if (renderedIncursions != text)
			{
				renderedIncursions = text;
				ClearChildren(incursionList);
				int[] array2 = array;
				for (int num2 = 0; num2 < array2.Length; num2++)
				{
					int number = array2[num2];
					Button button = Button((Transform)(object)incursionList, (number == 0) ? "I_000\nSuperficie" : ("I_" + number.ToString("D3")), 0f, 0f, 140f, (number == 0) ? 66 : 44, delegate
					{
						selectedIncursion = number;
						selectedPlayer = "";
						selectedKey = "";
						allowRepeat = false;
					});
					if (number == selectedIncursion)
					{
						MarkSelected(button);
					}
				}
			}
			TerminalSnapshot[] array3 = source.Where((TerminalSnapshot s) => s.Incursion == selectedIncursion).ToArray();
			if (!array3.Any((TerminalSnapshot s) => s.PlayerId == selectedPlayer))
			{
				selectedPlayer = "";
			}
			string text2 = selectedIncursion + ":" + selectedPlayer + ":" + selectedKey + ":" + string.Join(",", array3.Select((TerminalSnapshot s) => s.SelectionKey + s.PlayerId + s.PlayerName));
			if (renderedRows != text2)
			{
				renderedRows = text2;
				ClearChildren(recordingList);
				if (array3.Length == 0)
				{
					Label((Transform)(object)recordingList, "Todavía no hay clips listos. Espera a su codificación y recepción.", 0f, 0f, 270f, 130f);
				}
				if (selectedPlayer.Length == 0)
				{
					foreach (IGrouping<string, TerminalSnapshot> item in from s in array3
						group s by s.PlayerId)
					{
						string id = item.Key;
						int num3 = item.Count((TerminalSnapshot s) => !s.IsSequence);
						Button((Transform)(object)recordingList, item.First().PlayerName + "\n" + num3 + ((num3 == 1) ? " clip" : " clips"), 0f, 0f, 280f, 76f, delegate
						{
							selectedPlayer = id;
							selectedKey = "";
							allowRepeat = false;
						}, "Player");
					}
				}
				else
				{
					Button((Transform)(object)recordingList, "Volver a jugadores", 0f, 0f, 280f, 44f, delegate
					{
						selectedPlayer = "";
						selectedKey = "";
					}, "BackToPlayers");
					TerminalSnapshot[] array4 = array3.Where((TerminalSnapshot s) => s.PlayerId == selectedPlayer).ToArray();
					Label((Transform)(object)recordingList, array4[0].PlayerName, 0f, 0f, 280f, 38f, 20);
					string[] array5 = array4.Select((TerminalSnapshot s) => s.VideoId).Distinct().ToArray();
					if ((from s in array4
						group s by s.VideoId).Any((IGrouping<string, TerminalSnapshot> g) => g.All((TerminalSnapshot s) => !s.IsSequence)))
					{
						Label((Transform)(object)recordingList, "Más de 128 clips en una grabación: prepara clips individuales.", 0f, 0f, 280f, 80f, 17);
					}
					TerminalSnapshot[] array6 = array4;
					foreach (TerminalSnapshot row in array6)
					{
						string text3 = ((array5.Length > 1) ? ("Grabación " + (Array.IndexOf(array5, row.VideoId) + 1) + " · ") : "");
						string text4 = (row.IsSequence ? ("Todos seguidos · " + row.Clips.Count + ((row.Clips.Count == 1) ? " clip" : " clips")) : ("Clip " + row.ClipNumber));
						Button button2 = Button((Transform)(object)recordingList, text3 + text4, 0f, 0f, 280f, 62f, delegate
						{
							selectedKey = row.SelectionKey;
							allowRepeat = false;
							payPending = true;
							renderedDetails = "";
						}, "Recording");
						if (row.SelectionKey == selectedKey)
						{
							MarkSelected(button2);
						}
					}
				}
			}
			TerminalSnapshot terminalSnapshot = source.FirstOrDefault((TerminalSnapshot s) => s.SelectionKey == selectedKey && s.Incursion == selectedIncursion && s.PlayerId == selectedPlayer);
			int num4;
			if (terminalSnapshot != null)
			{
				RewardLedger? obj = ledger;
				num4 = ((obj != null) ? obj.PaidPhases(TerminalProtocol.PaymentKey(terminalSnapshot.VideoId)) : 0);
			}
			else
			{
				num4 = 0;
			}
			int num5 = num4;
			string text5 = selectedKey + ":" + num5 + ":" + payPending + ":" + allowRepeat;
			if (renderedDetails != text5)
			{
				renderedDetails = text5;
				string text6 = ((terminalSnapshot == null) ? "" : ((terminalSnapshot.Title.Length > 110) ? (terminalSnapshot.Title.Substring(0, 107) + "…") : terminalSnapshot.Title));
				detailLabel.text = ((terminalSnapshot == null) ? "Selecciona una incursión, un jugador y sus clips." : (text6 + "\nI_" + terminalSnapshot.Incursion.ToString("D3") + " · " + (terminalSnapshot.IsSequence ? ("Todos seguidos · " + terminalSnapshot.Clips.Count + " clips") : ("Clip " + terminalSnapshot.ClipNumber))));
				paymentLabel.text = ((terminalSnapshot == null) ? "" : (num5 switch
				{
					0 => "Pago original: pendiente", 
					3 => "Pago original: realizado", 
					_ => "Pago original: parcialmente realizado", 
				}));
				pendingToggle.SetIsOnWithoutNotify(payPending);
				repeatToggle.SetIsOnWithoutNotify(allowRepeat && payPending);
			}
			((Selectable)pendingToggle).interactable = terminalSnapshot != null;
			((Selectable)repeatToggle).interactable = terminalSnapshot != null && payPending;
			((Component)pendingToggle).GetComponent<CanvasGroup>().alpha = (((Selectable)pendingToggle).interactable ? 1f : 0.45f);
			((Component)repeatToggle).GetComponent<CanvasGroup>().alpha = (((Selectable)repeatToggle).interactable ? 1f : 0.45f);
			((Selectable)prepareButton).interactable = terminalSnapshot != null && PhotonNetwork.IsMasterClient && roomState != null && !waitingDisc.HasValue;
			((Component)prepareButton).GetComponent<CanvasGroup>().alpha = (((Selectable)prepareButton).interactable ? 1f : 0.45f);
			statusLabel.text = ((!PhotonNetwork.IsMasterClient) ? ("El anfitrión prepara los discos. Todos necesitan el mod y los clips.\n" + status) : status);
		}
	}
	internal static class GameEvidence
	{
		private sealed class ClipScope
		{
			internal ArchiveContext? Context;

			internal ClipEvidence? Evidence;
		}

		private static string actorScope = Guid.NewGuid().ToString("N");

		private static Room? scopedRoom;

		private static readonly ConditionalWeakTable<Clip, ClipScope> ClipScopes = new ConditionalWeakTable<Clip, ClipScope>();

		private static readonly FieldInfo? HolderField = AccessTools.Field(typeof(VideoCamera), "m_playerView");

		private static readonly FieldInfo? DataField = AccessTools.Field(typeof(VideoCamera), "m_instanceData");

		internal static void VerifyHooks()
		{
			if (AccessTools.Method(typeof(RecordingsHandler), "OnStartRecording", new Type[1] { typeof(StartRecordingCommandPackage) }, (Type[])null) == null || HolderField == null || DataField == null)
			{
				throw new MissingMemberException("Recording API changed.");
			}
		}

		private static PlayerIdentity? Identity(Player? player)
		{
			if (player == null)
			{
				return null;
			}
			return IdentityResolver.FromPhoton(player.CustomProperties[(object)"UserID"]?.ToString(), player.CustomProperties[(object)"PlatformFamily"]?.ToString(), player.CustomProperties[(object)"SteamID"]?.ToString(), player.NickName, player.ActorNumber, Scope());
		}

		private static string Scope()
		{
			if (scopedRoom != PhotonNetwork.CurrentRoom)
			{
				scopedRoom = PhotonNetwork.CurrentRoom;
				actorScope = Guid.NewGuid().ToString("N");
			}
			return actorScope;
		}

		internal static void Capture(StartRecordingCommandPackage package)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			CameraRecording val = default(CameraRecording);
			if (!RecordingsHandler.TryGetRecording(package.VideoID, ref val))
			{
				return;
			}
			Clip val2 = ((IEnumerable<Clip>)val.GetAllClips()).FirstOrDefault((Func<Clip, bool>)((Clip c) => ((ClipID)(ref c.clipID)).Equals(package.ClipID)));
			if (val2 == null)
			{
				return;
			}
			ClipEvidence evidence = Status(val2, val.GetAllClips().IndexOf(val2));
			evidence.CameraId = package.CameraDataGuid.ToString();
			VideoCamera obj = default(VideoCamera);
			if (CameraHandler.TryGetCamera(package.CameraDataGuid, ref obj))
			{
				object? obj2 = HolderField?.GetValue(obj);
				PhotonView val3 = (PhotonView)((obj2 is PhotonView) ? obj2 : null);
				evidence.Holder = Identity(((Object)(object)val3 != (Object)null) ? val3.Owner : null);
				object? obj3 = DataField?.GetValue(obj);
				ItemDataEntry val4 = ((IEnumerable<ItemDataEntry>)((ItemInstanceData)(((obj3 is ItemInstanceData) ? obj3 : null)?)).m_dataEntries).FirstOrDefault((Func<ItemDataEntry, bool>)((ItemDataEntry e) => ((object)e).GetType().FullName == "ContentPOVs.POVCamera"));
				string ownerId = ((object)val4)?.GetType().GetField("plrID", BindingFlags.Instance | BindingFlags.Public)?.GetValue(val4) as string;
				Player val5 = ((IEnumerable<Player>)PhotonNetwork.PlayerList).FirstOrDefault((Func<Player, bool>)delegate(Player p)
				{
					if (!(p.CustomProperties[(object)"SteamID"]?.ToString() == ownerId))
					{
						object obj4 = p.CustomProperties[(object)"PlatformFamily"];
						if (obj4 != null && obj4.ToString()?.IndexOf("Steam", StringComparison.OrdinalIgnoreCase) >= 0)
						{
							return p.CustomProperties[(object)"UserID"]?.ToString() == ownerId;
						}
						return false;
					}
					return true;
				});
				evidence.CameraOwner = IdentityResolver.FromCameraOwner(ownerId, (val5 != null) ? val5.NickName : null);
			}
			ClipScopes.GetValue(val2, (Clip _) => new ClipScope
			{
				Context = evidence.Context
			}).Evidence = evidence;
			Plugin.Instance?.Enqueue(evidence);
		}

		private static ClipEvidence Status(Clip clip, int index)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: 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_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: 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_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Expected O, but got Unknown
			ClipEvidence val = new ClipEvidence
			{
				VideoId = clip.m_recording.videoHandle.id.ToString(),
				ClipId = clip.clipID.id.ToString(),
				Order = index,
				Context = ContextFor(clip)
			};
			Room currentRoom = PhotonNetwork.CurrentRoom;
			val.Encoder = Identity((currentRoom != null) ? currentRoom.GetPlayer(clip.ownerID, false) : null) ?? IdentityResolver.FromPhoton((string)null, (string)null, (string)null, (string)null, clip.ownerID, Scope());
			val.IsRecording = clip.isRecording;
			val.IsEncoded = clip.encoded;
			val.IsReceived = clip.hasBeenRecieved;
			val.IsValid = clip.Valid;
			return val;
		}

		private static ArchiveContext? ContextFor(Clip clip)
		{
			ArchiveContext context = ClipScopes.GetValue(clip, (Clip _) => new ClipScope
			{
				Context = Plugin.Instance?.CurrentContext
			}).Context;
			Plugin.Instance?.RememberClip(clip.GetClipDirectory(), context);
			return context;
		}

		internal static ClipEvidence Describe(Clip clip, int index)
		{
			ClipEvidence result = Status(clip, index);
			ClipEvidence evidence = ClipScopes.GetValue(clip, (Clip _) => new ClipScope
			{
				Context = result.Context
			}).Evidence;
			result.CameraOwner = ((evidence != null) ? evidence.CameraOwner : null);
			result.Holder = ((evidence != null) ? evidence.Holder : null);
			return result;
		}

		internal static void Refresh()
		{
			if (!PhotonNetwork.InRoom)
			{
				return;
			}
			foreach (CameraRecording value in RecordingsHandler.GetRecordings().Values)
			{
				ArchiveTerminal? instance = ArchiveTerminal.Instance;
				if (instance == null || !instance.IsAlias(value.videoHandle.id))
				{
					for (int i = 0; i < value.ClipCount; i++)
					{
						Plugin.Instance?.Enqueue(Status(value.GetClip(i), i));
					}
				}
			}
		}
	}
	[HarmonyPatch(typeof(RecordingsHandler), "OnStartRecording", new Type[] { typeof(StartRecordingCommandPackage) })]
	internal static class RecordingStartedPatch
	{
		[HarmonyPostfix]
		private static void Postfix(StartRecordingCommandPackage package)
		{
			try
			{
				GameEvidence.Capture(package);
			}
			catch (Exception ex)
			{
				Plugin.Instance?.ReportIdentityError(ex);
			}
		}
	}
	[HarmonyPatch(typeof(InRoomState), "Enter")]
	internal static class MatchEnteredPatch
	{
		[HarmonyPrefix]
		private static void Prefix()
		{
			Plugin.Instance?.BeginMatch("InRoomState.Enter; room visit");
		}
	}
	[HarmonyPatch(typeof(PhotonGameLobbyHandler), "Awake")]
	internal static class IncursionArrivedPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			Plugin.Instance?.SceneArrived();
		}
	}
	[HarmonyPatch(typeof(SurfaceNetworkHandler), "RPC_QuotaFailed")]
	internal static class CampaignResetPatch
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			Plugin.Instance?.BeginMatch("RPC_QuotaFailed; campaign reset");
		}
	}
	[BepInPlugin("local.contentwarning.recordingarchiver", "Recording Archiver", "1.6.2")]
	[BepInProcess("Content Warning.exe")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Version = "1.6.2";

		public const string Id = "local.contentwarning.recordingarchiver";

		internal static Plugin? Instance;

		private readonly ConcurrentDictionary<string, ClipEvidence> pending = new ConcurrentDictionary<string, ClipEvidence>();

		private readonly CancellationTokenSource cancellation = new CancellationTokenSource();

		private readonly AutoResetEvent wake = new AutoResetEvent(initialState: false);

		private readonly SessionTracker sessions = new SessionTracker();

		private SourceRetention? retention;

		private readonly ConcurrentDictionary<string, ArchiveContext> clipContexts = new ConcurrentDictionary<string, ArchiveContext>(StringComparer.OrdinalIgnoreCase);

		private Task? worker;

		private Harmony? harmony;

		private float nextRefresh;

		private bool debug;

		private bool quitting;

		private string archiveDirectory = "";

		internal ArchiveContext? CurrentContext => sessions.Current;

		internal KeyboardShortcut FolderKey { get; private set; }

		internal KeyboardShortcut TerminalKey { get; private set; }

		internal KeyboardShortcut PauseKey { get; private set; }

		internal KeyboardShortcut FinishKey { get; private set; }

		internal bool HandlesPath(string path)
		{
			string text = ArchivePaths.NormalizeRoot(RecordingsHandler.GetDirectory());
			string text2 = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
			if (!text2.Equals(text, StringComparison.OrdinalIgnoreCase))
			{
				return ArchivePaths.Contains(text, text2);
			}
			return true;
		}

		internal unsafe static string ShortcutLabel(KeyboardShortcut shortcut)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Expected I4, but got Unknown
			return string.Join("+", from key in ((KeyboardShortcut)(ref shortcut)).Modifiers.Concat((IEnumerable<KeyCode>)(object)new KeyCode[1] { (KeyCode)(int)((KeyboardShortcut)(ref shortcut)).MainKey })
				select ((int)key != 306 && (int)key != 305) ? (((int)key != 304 && (int)key != 303) ? (((int)key != 308 && (int)key != 307) ? ((object)(*(KeyCode*)(&key))/*cast due to .constrained prefix*/).ToString() : "Alt") : "Shift") : "Ctrl");
		}

		private void Awake()
		{
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0091: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
			//IL_010f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0135: Unknown result type (might be due to invalid IL or missing references)
			//IL_015b: Unknown result type (might be due to invalid IL or missing references)
			//IL_018d: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c6: Expected O, but got Unknown
			//IL_0291: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_02cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_02da: Unknown result type (might be due to invalid IL or missing references)
			//IL_0305: Unknown result type (might be due to invalid IL or missing references)
			//IL_0314: Unknown result type (might be due to invalid IL or missing references)
			//IL_033f: Unknown result type (might be due to invalid IL or missing references)
			//IL_034e: Unknown result type (might be due to invalid IL or missing references)
			//IL_03b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_03c1: Expected O, but got Unknown
			//IL_03cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_03d6: Expected O, but got Unknown
			Instance = this;
			bool value = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Activar el archivador. Reiniciar el juego tras cambiar la configuración.").Value;
			string text = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos);
			if (string.IsNullOrEmpty(text))
			{
				text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Videos");
			}
			string value2 = ((BaseUnityPlugin)this).Config.Bind<string>("General", "OutputDirectory", Path.Combine(text, "ContentWarning"), "Ruta absoluta persistente, fuera de Rec.").Value;
			ArchiveOptions options = new ArchiveOptions
			{
				SourceDirectory = RecordingsHandler.GetDirectory(),
				OutputDirectory = Environment.ExpandEnvironmentVariables(value2),
				ArchiveFragments = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ArchiveFragments", true, "Conservar clips y archivos auxiliares, incluidos formatos desconocidos.").Value,
				ArchiveFullRecordings = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ArchiveFullRecordings", true, "Conservar fullRecording.webm.").Value,
				GroupBySession = ((BaseUnityPlugin)this).Config.Bind<bool>("Organization", "GroupBySession", true, "Agrupar por partida (visita a sala) e incursión observada.").Value,
				GroupByPlayer = ((BaseUnityPlugin)this).Config.Bind<bool>("Organization", "GroupByPlayer", true, "Agrupar por dueño demostrado de cámara; Unknown si no existe evidencia.").Value,
				UseSteamId = ((BaseUnityPlugin)this).Config.Bind<bool>("Organization", "UseSteamId", true, "Incluir ID en el nombre de carpeta; false usa una huella. Los metadatos conservan el ID.").Value,
				StabilityPeriod = TimeSpan.FromSeconds(Math.Max(1, ((BaseUnityPlugin)this).Config.Bind<int>("Processing", "StabilitySeconds", 5, "Tiempo sin cambios antes de copiar con exclusión de escritura.").Value)),
				SnapshotAfter = TimeSpan.FromSeconds(Math.Max(30, ((BaseUnityPlugin)this).Config.Bind<int>("Processing", "SnapshotAfterSeconds", 120, "Intervalo mínimo de copias parciales para archivos en escritura prolongada.").Value))
			};
			double scanSeconds = Math.Max(1, ((BaseUnityPlugin)this).Config.Bind<int>("Processing", "ScanIntervalSeconds", 2, "Reconciliación periódica en segundo plano.").Value);
			if (options.SnapshotAfter < options.StabilityPeriod)
			{
				options.SnapshotAfter = options.StabilityPeriod;
			}
			debug = ((BaseUnityPlugin)this).Config.Bind<bool>("Logging", "EnableDebugLogging", false, "Registrar contadores de archivado, sin nombres ni identificadores personales.").Value;
			if (!value)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Recording Archiver disabled by configuration.");
				return;
			}
			archiveDirectory = options.OutputDirectory;
			FolderKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Shortcuts", "OpenArchiveFolderShortcut", new KeyboardShortcut((KeyCode)111, (KeyCode[])(object)new KeyCode[1] { (KeyCode)306 }), "Abrir la carpeta de partidas. Ejemplo: O + LeftControl. None desactiva el atajo.").Value;
			TerminalKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Shortcuts", "OpenTerminalShortcut", new KeyboardShortcut((KeyCode)117, (KeyCode[])(object)new KeyCode[1] { (KeyCode)306 }), "Abrir o cerrar el archivo cerca de la tele.").Value;
			PauseKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Shortcuts", "PauseTelevisionShortcut", new KeyboardShortcut((KeyCode)112, (KeyCode[])(object)new KeyCode[1] { (KeyCode)306 }), "Pausar o reanudar la tele.").Value;
			FinishKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Shortcuts", "FinishTelevisionShortcut", new KeyboardShortcut((KeyCode)106, (KeyCode[])(object)new KeyCode[1] { (KeyCode)306 }), "Terminar la reproducción de la tele.").Value;
			bool value3 = ((BaseUnityPlugin)this).Config.Bind<bool>("Terminal", "Enabled", true, "Activar terminal junto al equipo de subida. Todos los jugadores necesitan esta versión para sus discos compartidos.").Value;
			string text2 = "validation";
			try
			{
				options.RetentionDirectory = options.SourceDirectory + "_RA";
				retention = new SourceRetention(options.SourceDirectory, options.RetentionDirectory);
				GameEvidence.VerifyHooks();
				harmony = new Harmony("local.contentwarning.recordingarchiver");
				Type[] types = typeof(Plugin).Assembly.GetTypes();
				foreach (Type type in types)
				{
					if (Attribute.IsDefined(type, typeof(HarmonyPatch), inherit: false))
					{
						text2 = type.Name;
						harmony.CreateClassProcessor(type).Patch();
					}
				}
			}
			catch (Exception ex)
			{
				Harmony? obj = harmony;
				if (obj != null)
				{
					obj.UnpatchSelf();
				}
				retention = null;
				((BaseUnityPlugin)this).Logger.LogError((object)("Lifecycle/protection hooks unavailable (" + ex.GetType().Name + "); periodic archiving continues WITHOUT cleanup/overwrite protection."));
				if (ex is HarmonyException)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)("Hook diagnostic: " + text2 + "; " + ex.Message + " Cause: " + ex.GetBaseException().Message));
				}
			}
			worker = Task.Run(delegate
			{
				RunWorker(options, scanSeconds, cancellation.Token);
			});
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Recording Archiver 1.6.2 loaded; Unity " + Application.unityVersion + ". Worker startup will be logged separately."));
			if (retention != null)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Pre-delete and pre-overwrite retention hooks active.");
			}
			if (value3 && retention != null)
			{
				((Component)this).gameObject.AddComponent<ArchiveTerminal>().Initialize(options.SourceDirectory, options.OutputDirectory);
			}
		}

		internal void Enqueue(ClipEvidence evidence)
		{
			if (worker == null || worker.IsCompleted || cancellation.IsCancellationRequested)
			{
				return;
			}
			ConcurrentDictionary<string, ClipEvidence> concurrentDictionary = pending;
			string[] array = new string[5];
			ArchiveContext context = evidence.Context;
			array[0] = ((context != null) ? context.ScopeKey : null);
			array[1] = "/";
			array[2] = evidence.VideoId;
			array[3] = "/";
			array[4] = evidence.ClipId;
			concurrentDictionary.AddOrUpdate(string.Concat(array), evidence, delegate(string _, ClipEvidence previous)
			{
				evidence.CameraOwner = evidence.CameraOwner ?? previous.CameraOwner;
				evidence.Holder = evidence.Holder ?? previous.Holder;
				evidence.Encoder = previous.Encoder ?? evidence.Encoder;
				if (evidence.CameraId.Length == 0)
				{
					evidence.CameraId = previous.CameraId;
				}
				return evidence;
			});
			wake.Set();
		}

		internal void BeginMatch(string evidence)
		{
			string text = (SaveSystem.HaveCurrentSave ? SaveSystem.CurrentSave.GetSaveID().ToString() : "");
			sessions.BeginMatch(text, evidence);
			ArchiveTerminal.Instance?.MatchStarted(evidence);
			wake.Set();
		}

		internal void SceneArrived()
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			if (sessions.Current == null)
			{
				BeginMatch("scene-observed-after-join");
			}
			if (!PhotonGameLobbyHandler.IsSurface)
			{
				SessionTracker obj = sessions;
				Scene activeScene = SceneManager.GetActiveScene();
				string text = ((Scene)(ref activeScene)).handle.ToString();
				RoomStatsHolder roomStats = SurfaceNetworkHandler.RoomStats;
				obj.EnterUnderground(text, (roomStats != null) ? roomStats.CurrentDay : 0);
			}
			wake.Set();
		}

		internal void Preserve(string path, bool file)
		{
			if (retention == null)
			{
				throw new IOException("Retention unavailable; destructive recording operation refused.");
			}
			try
			{
				retention.Preserve(path, file, (ArchiveContext)null);
				if (!file && !path.TrimEnd(Path.DirectorySeparatorChar).Equals(RecordingsHandler.GetDirectory().TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))
				{
					retention.Remember(path, (ArchiveContext)(clipContexts.TryGetValue(path, out ArchiveContext value) ? ((object)value) : ((object)CurrentContext)));
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Pre-delete/overwrite retention failed (" + ex.GetType().Name + "); destructive operation refused. Originals remain at their source."));
				throw;
			}
			wake.Set();
		}

		internal void RememberClip(string directory, ArchiveContext? context)
		{
			if (context == null)
			{
				return;
			}
			clipContexts[directory] = context;
			if (!Directory.Exists(directory))
			{
				SourceRetention? obj = retention;
				if (obj != null)
				{
					obj.Remember(directory, context);
				}
			}
		}

		internal void RememberSource(string path, ArchiveContext? context)
		{
			SourceRetention? obj = retention;
			if (obj != null)
			{
				obj.Remember(path, context);
			}
		}

		internal void RecordingDirectoryReady(string path)
		{
			if (HandlesPath(path))
			{
				SourceRetention? obj = retention;
				if (obj != null)
				{
					obj.Remember(path, (ArchiveContext)(clipContexts.TryGetValue(path, out ArchiveContext value) ? ((object)value) : ((object)CurrentContext)));
				}
			}
		}

		internal void CopyPreserving(string source, string destination)
		{
			if (retention == null)
			{
				throw new IOException("Retention unavailable.");
			}
			retention.CopyPreserving(source, destination, CurrentContext);
			wake.Set();
		}

		internal void ReportIdentityError(Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Recording identity unavailable (" + ex.GetType().Name + "); files remain eligible for Unknown archiving."));
		}

		internal void ReportExtractionError(Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)("Extraction stopped (" + ex.GetType().Name + "); previous recording preserved. Check permissions, locks and free space."));
		}

		internal void ReportTerminalError(Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Recording terminal operation failed (" + ex.GetType().Name + "). Check availability, synchronization and free space."));
		}

		internal void OpenArchiveFolder()
		{
			try
			{
				string text = ArchivePaths.NormalizeRoot(archiveDirectory);
				ArchivePaths.RejectLinks(text, text);
				Directory.CreateDirectory(text);
				using (Process.Start(new ProcessStartInfo
				{
					FileName = text,
					UseShellExecute = true
				}))
				{
				}
				ArchiveTerminal.Instance?.FolderOpenResult("Carpeta de partidas abierta.");
			}
			catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is ArgumentException || ex is InvalidOperationException || ex is Win32Exception)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("No se pudo abrir la carpeta de grabaciones (" + ex.GetType().Name + "). Comprueba el destino configurado y sus permisos."));
				ArchiveTerminal.Instance?.FolderOpenResult("No se pudo abrir la carpeta. Comprueba el destino configurado y sus permisos.");
			}
		}

		private void Update()
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			if (!string.IsNullOrEmpty(archiveDirectory))
			{
				KeyboardShortcut folderKey = FolderKey;
				if (((KeyboardShortcut)(ref folderKey)).IsDown())
				{
					OpenArchiveFolder();
				}
			}
			if (worker == null || worker.IsCompleted || Time.unscaledTime < nextRefresh)
			{
				return;
			}
			nextRefresh = Time.unscaledTime + 5f;
			try
			{
				GameEvidence.Refresh();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Recording status refresh failed (" + ex.GetType().Name + "); archiving continues."));
			}
		}

		private void RunWorker(ArchiveOptions options, double interval, CancellationToken token)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Expected O, but got Unknown
			try
			{
				ArchiveEngine val = new ArchiveEngine(options, (Action<string>)delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				});
				try
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)"Archive worker ready; output directory locked successfully.");
					int num = 0;
					while (!token.IsCancellationRequested)
					{
						try
						{
							val.SetContext(CurrentContext);
							if (retention != null)
							{
								val.SetSourceContexts(retention.SnapshotContexts());
							}
							foreach (string key in pending.Keys)
							{
								if (pending.TryRemove(key, out ClipEvidence value))
								{
									val.Observe(value);
								}
							}
							val.Scan(DateTime.UtcNow, token);
							if (debug)
							{
								int num2 = 0;
								foreach (RecordingMetadata recording in val.State.Recordings)
								{
									num2 += recording.Files.Count;
								}
								if (num2 != num)
								{
									((BaseUnityPlugin)this).Logger.LogDebug((object)$"Archived file versions: {num2}.");
								}
								num = num2;
							}
						}
						catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is ArgumentException || ex is SerializationException)
						{
							((BaseUnityPlugin)this).Logger.LogError((object)("Archive scan/persistence failed (" + ex.GetType().Name + "); retrying. Check destination permissions, free space and state JSON. Originals remain untouched."));
						}
						WaitHandle.WaitAny(new WaitHandle[2] { token.WaitHandle, wake }, TimeSpan.FromSeconds(interval));
					}
				}
				finally
				{
					((IDisposable)val)?.Dispose();
				}
			}
			catch (OperationCanceledException) when (token.IsCancellationRequested)
			{
			}
			catch (Exception ex3)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Archive worker stopped (" + ex3.GetType().Name + "). Check output path, permissions, state JSON and concurrent instances; restart after correcting the problem. Originals remain untouched."));
			}
		}

		private void OnDestroy()
		{
			if (!quitting)
			{
				RetainOnShutdown();
			}
			Instance = null;
			cancellation.Cancel();
			if (!quitting)
			{
				Harmony? obj = harmony;
				if (obj != null)
				{
					obj.UnpatchSelf();
				}
			}
			if (worker != null)
			{
				worker.ContinueWith(delegate
				{
					cancellation.Dispose();
					wake.Dispose();
				}, TaskScheduler.Default);
			}
			else
			{
				cancellation.Dispose();
				wake.Dispose();
			}
		}

		private void OnApplicationQuit()
		{
			quitting = true;
			RetainOnShutdown();
		}

		private void RetainOnShutdown()
		{
			if (retention == null)
			{
				return;
			}
			try
			{
				retention.Preserve(RecordingsHandler.GetDirectory(), false, (ArchiveContext)null);
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Shutdown retention deferred (" + ex.GetType().Name + "); deletion guards keep remaining sources in Rec for the next launch."));
			}
		}
	}
	internal static class Preservation
	{
		internal static void DeleteDirectory(DirectoryInfo directory, bool recursive)
		{
			Plugin plugin = Plugin.Instance ?? throw new IOException("Archiver unavailable; deletion refused.");
			if (!plugin.HandlesPath(directory.FullName))
			{
				directory.Delete(recursive);
			}
			else
			{
				plugin.Preserve(directory.FullName, file: false);
			}
		}

		internal static void Copy(string source, string destination, bool overwrite)
		{
			Plugin plugin = Plugin.Instance ?? throw new IOException("Archiver unavailable; overwrite refused.");
			if (!plugin.HandlesPath(destination))
			{
				File.Copy(source, destination, overwrite);
			}
			else
			{
				plugin.CopyPreserving(source, destination);
			}
		}

		internal static void SaveReceived(PhotonSendVideoHandler receiver, VideoChunk chunk)
		{
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//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)
			Plugin plugin = Plugin.Instance ?? throw new IOException("Archiver unavailable; overwrite refused.");
			string path = Path.Combine((string)(AccessTools.Field(typeof(PhotonSendVideoHandler), "PATH_TO_VIDEO").GetValue(receiver) ?? throw new IOException("Receive directory unavailable.")), chunk.VideoID.id.ToString(), chunk.ClipID.id.ToString(), "output.webm");
			if (plugin.HandlesPath(path))
			{
				try
				{
					plugin.Preserve(path, file: true);
				}
				catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
				{
					return;
				}
			}
			try
			{
				AccessTools.Method(typeof(PhotonSendVideoHandler), "TempSaveReceivedVideo", (Type[])null, (Type[])null).Invoke(receiver, new object[1] { chunk });
			}
			catch (TargetInvocationException ex2) when (ex2.InnerException != null)
			{
				ExceptionDispatchInfo.Capture(ex2.InnerException).Throw();
			}
			if (plugin.HandlesPath(path) && File.Exists(path))
			{
				plugin.RecordingDirectoryReady(Path.GetDirectoryName(path));
			}
		}

		internal static IEnumerable<CodeInstruction> Replace(IEnumerable<CodeInstruction> instructions, MethodInfo original, MethodInfo replacement)
		{
			int count = 0;
			foreach (CodeInstruction instruction in instructions)
			{
				if (CodeInstructionExtensions.Calls(instruction, original))
				{
					instruction.opcode = OpCodes.Call;
					instruction.operand = replacement;
					count++;
				}
				yield return instruction;
			}
			if (count != 1)
			{
				throw new MissingMethodException("Expected exactly one verified recording filesystem operation.");
			}
		}
	}
	[HarmonyPatch]
	internal static class RecordingDeletionPatch
	{
		[HarmonyTargetMethods]
		private static IEnumerable<MethodBase> Targets()
		{
			yield return AccessTools.Method(typeof(RecordingsHandler), "OnCreated", (Type[])null, (Type[])null);
			yield return AccessTools.Method(typeof(RecordingsHandler), "DeleteDirectory", (Type[])null, (Type[])null);
			yield return AccessTools.Method(typeof(MainMenuHandler), "DeleteTempFolder", (Type[])null, (Type[])null);
			yield return AccessTools.Constructor(typeof(RecordingPipe), new Type[3]
			{
				typeof(int),
				typeof(int),
				typeof(string)
			}, false);
		}

		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpile(IEnumerable<CodeInstruction> instructions)
		{
			return Preservation.Replace(instructions, AccessTools.Method(typeof(DirectoryInfo), "Delete", new Type[1] { typeof(bool) }, (Type[])null), AccessTools.Method(typeof(Preservation), "DeleteDirectory", (Type[])null, (Type[])null));
		}
	}
	[HarmonyPatch]
	internal static class ExtractionCopyPatch
	{
		[HarmonyTargetMethod]
		private static MethodBase Target()
		{
			return AccessTools.EnumeratorMoveNext((MethodBase)AccessTools.Method(typeof(RecordingsHandler), "ExtractRecording", (Type[])null, (Type[])null));
		}

		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpile(IEnumerable<CodeInstruction> instructions)
		{
			return Preservation.Replace(instructions, AccessTools.Method(typeof(File), "Copy", new Type[3]
			{
				typeof(string),
				typeof(string),
				typeof(bool)
			}, (Type[])null), AccessTools.Method(typeof(Preservation), "Copy", (Type[])null, (Type[])null));
		}
	}
	[HarmonyPatch(typeof(RecordingsHandler), "ExtractRecording")]
	internal static class ExtractionFailurePatch
	{
		[HarmonyPrefix]
		private static void Prefix(CameraRecording recording, ref Action<bool> callback)
		{
			Action<bool> original = callback;
			callback = delegate(bool success)
			{
				if (success)
				{
					try
					{
						ArchiveTerminal.Instance?.Capture(recording, fullProduced: true);
					}
					catch (Exception ex)
					{
						Plugin.Instance?.ReportTerminalError(ex);
					}
				}
				original(success);
			};
		}

		[HarmonyPostfix]
		private static void Postfix(Action<bool> callback, ref IEnumerator __result)
		{
			__result = Guard(__result, callback);
		}

		private static IEnumerator Guard(IEnumerator original, Action<bool> callback)
		{
			try
			{
				while (true)
				{
					bool flag = false;
					Exception ex = null;
					try
					{
						flag = original.MoveNext();
					}
					catch (Exception ex2) when (ex2 is IOException || ex2 is UnauthorizedAccessException)
					{
						ex = ex2;
					}
					if (ex != null)
					{
						Plugin.Instance?.ReportExtractionError(ex);
						callback(obj: false);
						break;
					}
					if (!flag)
					{
						break;
					}
					yield return original.Current;
				}
			}
			finally
			{
				(original as IDisposable)?.Dispose();
			}
		}
	}
	[HarmonyPatch(/*Could not decode attribute arguments.*/)]
	internal static class RecordingPipeReadyPatch
	{
		[HarmonyPostfix]
		private static void Postfix(string directory)
		{
			Plugin.Instance?.RecordingDirectoryReady(directory);
		}
	}
	[HarmonyPatch(typeof(PhotonSendVideoHandler), "RecieveClipChunk")]
	internal static class ReceivedClipWritePatch
	{
		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpile(IEnumerable<CodeInstruction> instructions)
		{
			return Preservation.Replace(instructions, AccessTools.Method(typeof(PhotonSendVideoHandler), "TempSaveReceivedVideo", (Type[])null, (Type[])null), AccessTools.Method(typeof(Preservation), "SaveReceived", (Type[])null, (Type[])null));
		}
	}
	[HarmonyPatch(typeof(LibVEStitcher), "StitchVideos")]
	internal static class StitchOverwritePatch
	{
		[HarmonyPrefix]
		private static bool Prefix(string outputDirectory, ref bool __result, out ArchiveContext? __state)
		{
			Plugin plugin = Plugin.Instance ?? throw new IOException("Archiver unavailable; stitching refused.");
			__state = plugin.CurrentContext;
			if (!plugin.HandlesPath(outputDirectory))
			{
				__state = null;
				return true;
			}
			try
			{
				plugin.Preserve(Path.Combine(outputDirectory, "fullRecording.webm"), file: true);
				plugin.Preserve(Path.Combine(outputDirectory, "videoList.txt"), file: true);
				return true;
			}
			catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
			{
				__state = null;
				__result = false;
				return false;
			}
		}

		[HarmonyPostfix]
		private static void Postfix(string outputDirectory, bool __result, ArchiveContext? __state)
		{
			if (__state != null)
			{
				Plugin.Instance?.RememberSource(Path.Combine(outputDirectory, "fullRecording.webm"), __state);
			}
		}
	}
	[HarmonyPatch(typeof(UploadCompleteUI), "Awake")]
	internal static class TelevisionCreatedPatch
	{
		[HarmonyPostfix]
		private static void Postfix(UploadCompleteUI __instance)
		{
			TelevisionPlayback.Get(__instance);
		}
	}
	[HarmonyPatch(typeof(UploadCompleteUI), "PlayVideosAsync")]
	internal static class TelevisionPlaybackPatch
	{
		[HarmonyPostfix]
		private static void Postfix(UploadCompleteUI __instance, CameraRecording playableVideo, ref IEnumerator __result)
		{
			__result = TelevisionPlayback.Get(__instance).Begin(playableVideo, __result);
		}
	}
	[HarmonyPatch(typeof(UploadCompleteUI), "DisplayVideoEval")]
	internal static class TelevisionEvaluationPatch
	{
		[HarmonyPostfix]
		private static void Postfix(UploadCompleteUI __instance, ref IEnumerator __result)
		{
			__result = TelevisionPlayback.Get(__instance).Evaluation(__result);
		}
	}
	[HarmonyPatch(typeof(UploadCompleteUI), "Replay")]
	internal static class TelevisionReplayPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(UploadCompleteUI __instance)
		{
			TelevisionPlayback component = ((Component)__instance).GetComponent<TelevisionPlayback>();
			if ((Object)(object)component == (Object)null || !component.HasRecording || !component.AllClientsCompatible)
			{
				return true;
			}
			if (component.Active)
			{
				component.RequestFinish();
			}
			else
			{
				component.RequestReplay();
			}
			return false;
		}
	}
	[HarmonyPatch(typeof(UploadCompleteUI), "PlayVideo")]
	internal static class TelevisionLostFootagePatch
	{
		[HarmonyPrefix]
		private static void Prefix(UploadCompleteUI __instance)
		{
			((Component)__instance).GetComponent<TelevisionPlayback>()?.Forget();
		}
	}
	[HarmonyPatch(typeof(UploadVideoStation), "IsValid")]
	internal static class TelevisionInteractionAvailablePatch
	{
		[HarmonyPostfix]
		private static void Postfix(UploadVideoStation __instance, ref bool __result)
		{
			UploadCompleteUI uploadCompleteUI = __instance.m_uploadCompleteUI;
			TelevisionPlayback televisionPlayback = ((uploadCompleteUI != null) ? ((Component)uploadCompleteUI).GetComponent<TelevisionPlayback>() : null);
			if (televisionPlayback != null && televisionPlayback.Active && televisionPlayback.CanControl)
			{
				__result = true;
			}
		}
	}
	[HarmonyPatch(typeof(UploadVideoStation), "Interact")]
	internal static class TelevisionFinishInteractionPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(UploadVideoStation __instance)
		{
			UploadCompleteUI uploadCompleteUI = __instance.m_uploadCompleteUI;
			TelevisionPlayback televisionPlayback = ((uploadCompleteUI != null) ? ((Component)uploadCompleteUI).GetComponent<TelevisionPlayback>() : null);
			if (televisionPlayback == null || !televisionPlayback.Active || !televisionPlayback.AllClientsCompatible)
			{
				return true;
			}
			televisionPlayback.RequestFinish();
			return false;
		}
	}
	[HarmonyPatch(typeof(Interactable), "get_hoverText")]
	internal static class TelevisionInteractionTextPatch
	{
		[HarmonyPostfix]
		private static void Postfix(Interactable __instance, ref string __result)
		{
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			UploadCompleteUI obj = ((UploadVideoStation)(((__instance is UploadVideoStation) ? __instance : null)?)).m_uploadCompleteUI ?? ((ReplayVideoInteractable)(((__instance is ReplayVideoInteractable) ? __instance : null)?)).m_uploadCompleteUI;
			TelevisionPlayback televisionPlayback = ((obj != null) ? ((Component)obj).GetComponent<TelevisionPlayback>() : null);
			if (televisionPlayback != null && televisionPlayback.Active && televisionPlayback.CanControl)
			{
				__result = "Terminar reproducción · " + Plugin.ShortcutLabel(Plugin.Instance.FinishKey);
			}
		}
	}
	[HarmonyPatch(typeof(Interactable), "IsValid")]
	internal static class TelevisionReplayAvailablePatch
	{
		[HarmonyPostfix]
		private static void Postfix(Interactable __instance, ref bool __result)
		{
			ReplayVideoInteractable val = (ReplayVideoInteractable)(object)((__instance is ReplayVideoInteractable) ? __instance : null);
			if (val != null)
			{
				UploadCompleteUI uploadCompleteUI = val.m_uploadCompleteUI;
				TelevisionPlayback televisionPlayback = ((uploadCompleteUI != null) ? ((Component)uploadCompleteUI).GetComponent<TelevisionPlayback>() : null);
				if (televisionPlayback != null && televisionPlayback.HasRecording && televisionPlayback.AllClientsCompatible)
				{
					__result &= televisionPlayback.CanControl;
				}
			}
		}
	}
	public sealed class TelevisionPlayback : MonoBehaviourPun
	{
		private sealed class Playback
		{
			internal Guid Video;

			internal bool Active = true;

			internal bool Paused;

			internal bool Terminated;

			internal int Generation;

			internal int Revision;
		}

		private UploadCompleteUI ui;

		private UploadVideoStation? station;

		private Playback? playback;

		private int issuedRevision;

		private const string VersionKey = "cwra.tv.version";

		private const byte Pause = 1;

		private const byte Resume = 2;

		private const byte Finish = 3;

		private const byte Replay = 4;

		internal bool Active => playback?.Active ?? false;

		internal bool HasRecording => playback != null;

		internal bool Paused => playback?.Paused ?? false;

		internal bool AllClientsCompatible
		{
			get
			{
				if (PhotonNetwork.InRoom)
				{
					return PhotonNetwork.PlayerList.All((Player p) => p.CustomProperties[(object)"cwra.tv.version"] is int num && num == 1);
				}
				return false;
			}
		}

		internal bool CanControl
		{
			get
			{
				//IL_008e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0094: Unknown result type (might be due to invalid IL or missing references)
				if (!AllClientsCompatible || !PhotonNetwork.IsMasterClient || (Object)(object)Player.localPlayer == (Object)null || Player.localPlayer.data.dead || !PhotonGameLobbyHandler.IsSurface)
				{
					return false;
				}
				if ((Object)(object)station == (Object)null)
				{
					station = Object.FindFirstObjectByType<UploadVideoStation>();
				}
				Transform val = (((Object)(object)station != (Object)null && (Object)(object)station.m_uploadCompleteUI == (Object)(object)ui) ? ((Component)station).transform : ((Component)this).transform);
				return Vector3.Distance(Player.localPlayer.HeadPosition(), val.position) <= 4f;
			}
		}

		internal static TelevisionPlayback Get(UploadCompleteUI screen)
		{
			//IL_0050: 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_006f: Expected O, but got Unknown
			TelevisionPlayback obj = ((Component)screen).GetComponent<TelevisionPlayback>() ?? ((Component)screen).gameObject.AddComponent<TelevisionPlayback>();
			obj.ui = screen;
			if (PhotonNetwork.InRoom && (!(PhotonNetwork.LocalPlayer.CustomProperties[(object)"cwra.tv.version"] is int num) || num != 1))
			{
				PhotonNetwork.LocalPlayer.SetCustomProperties(new Hashtable { [(object)"cwra.tv.version"] = 1 }, (Hashtable)null, (WebFlags)null);
			}
			PhotonView component = ((Component)screen).GetComponent<PhotonView>();
			if (component != null)
			{
				component.RefreshRpcMonoBehaviourCache();
				return obj;
			}
			return obj;
		}

		internal IEnumerator Begin(CameraRecording recording, IEnumerator original)
		{
			int generation = ((playback == null || !(playback.Video == recording.videoHandle.id)) ? 1 : checked(playback.Generation + 1));
			if (playback != null)
			{
				playback.Terminated = true;
			}
			((MonoBehaviour)ui).StopAllCoroutines();
			Playback current = (playback = new Playback
			{
				Video = recording.videoHandle.id,
				Generation = generation
			});
			issuedRevision = 0;
			return Drive(original, current, main: true);
		}

		internal IEnumerator Evaluation(IEnumerator original)
		{
			if (playback != null)
			{
				return Drive(original, playback, main: false);
			}
			return original;
		}

		internal void Forget()
		{
			if (playback != null)
			{
				playback.Terminated = true;
			}
			playback = null;
		}

		private static IEnumerator Drive(IEnumerator original, Playback current, bool main)
		{
			try
			{
				while (!current.Terminated)
				{
					if (current.Paused)
					{
						yield return null;
						continue;
					}
					if (original.MoveNext())
					{
						yield return original.Current;
						continue;
					}
					break;
				}
			}
			finally
			{
				(original as IDisposable)?.Dispose();
				if (main)
				{
					current.Active = false;
				}
			}
		}

		private void Update()
		{
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			if (!Active)
			{
				return;
			}
			if (Paused && ui.m_videoPlayer.isPlaying)
			{
				ui.m_videoPlayer.Pause();
			}
			if (!CanControl)
			{
				return;
			}
			ArchiveTerminal? instance = ArchiveTerminal.Instance;
			if (instance == null || !instance.IsOpen)
			{
				KeyboardShortcut val = Plugin.Instance.PauseKey;
				if (((KeyboardShortcut)(ref val)).IsDown())
				{
					Request((byte)((!Paused) ? 1 : 2));
				}
				val = Plugin.Instance.FinishKey;
				if (((KeyboardShortcut)(ref val)).IsDown())
				{
					RequestFinish();
				}
			}
		}

		internal void RequestFinish()
		{
			if (Active)
			{
				Request(3);
			}
		}

		internal void RequestReplay()
		{
			if (!Active && playback != null)
			{
				Request(4);
			}
		}

		private void Request(byte command)
		{
			if (CanControl && playback != null && issuedRevision != int.MaxValue)
			{
				issuedRevision = Math.Max(issuedRevision, playback.Revision) + 1;
				((MonoBehaviourPun)this).photonView.RPC("RPC_ControlRecording", (RpcTarget)5, new object[4]
				{
					command,
					playback.Generation,
					issuedRevision,
					playback.Video.ToByteArray()
				});
			}
		}

		[PunRPC]
		private void RPC_ControlRecording(byte command, int generation, int revision, byte[] video, PhotonMessageInfo info)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			Playback playback = this.playback;
			if (!PhotonNetwork.InRoom || info.Sender != PhotonNetwork.MasterClient || playback == null || video == null || video.Length != 16 || playback.Video != new Guid(video) || generation != playback.Generation || revision <= playback.Revision)
			{
				return;
			}
			switch (command)
			{
			case 4:
				if (!playback.Active)
				{
					playback.Revision = revision;
					AccessTools.Method(typeof(UploadCompleteUI), "RPCA_Replay", (Type[])null, (Type[])null).Invoke(ui, Array.Empty<object>());
				}
				break