Decompiled source of RecordingArchiver v1.6.2
BepInEx/plugins/RecordingArchiver/RecordingArchiver.Core.dll
Decompiled a day agousing 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
The result has been truncated due to the large size, download it to view full contents!
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