using Newtonsoft.Json; using System.IO; using System; using System.IO.Compression; using System.Linq; using System.Collections.Generic; using System.Collections.Concurrent; namespace OverwatchTranscript { public interface ITranscriptReader { OverwatchCommonHeader Header { get; } T GetHeader(string key); void AddMomentHandler(Action handler); void AddEventHandler(Action> handler); bool Next(); void Close(); } public class TranscriptReader : ITranscriptReader { private readonly object handlersLock = new object(); private readonly string transcriptFile; private readonly string artifactsFolder; private readonly List> momentHandlers = new List>(); private readonly Dictionary>> eventHandlers = new Dictionary>>(); private readonly string workingDir; private readonly OverwatchTranscript model; private bool closed; private long momentCounter; private readonly ConcurrentQueue queue = new ConcurrentQueue(); private readonly Task queueFiller; public TranscriptReader(string workingDir, string inputFilename) { closed = false; this.workingDir = workingDir; transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename); artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName); if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir); if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean"); model = LoadModel(inputFilename); queueFiller = Task.Run(() => FillQueue(model, workingDir)); } public OverwatchCommonHeader Header { get { CheckClosed(); return model.Header.Common; } } public T GetHeader(string key) { CheckClosed(); var value = model.Header.Entries.First(e => e.Key == key).Value; return JsonConvert.DeserializeObject(value)!; } public void AddMomentHandler(Action handler) { CheckClosed(); lock (handlersLock) { momentHandlers.Add(handler); } } public void AddEventHandler(Action> handler) { CheckClosed(); var typeName = typeof(T).FullName; if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload"); lock (handlersLock) { if (eventHandlers.ContainsKey(typeName)) { eventHandlers[typeName].Add(CreateEventAction(handler)); } else { eventHandlers.Add(typeName, new List> { CreateEventAction(handler) }); } } } private readonly object nextLock = new object(); private OverwatchMoment? moment = null; private OverwatchMoment? next = null; public bool Next() { CheckClosed(); OverwatchMoment? m = null; TimeSpan? duration = null; lock (nextLock) { if (next == null) { if (!queue.TryDequeue(out moment)) return false; queue.TryDequeue(out next); } else { moment = next; next = null; queue.TryDequeue(out next); } m = moment; duration = GetMomentDuration(); } ActivateMoment(moment, duration); return true; } public void Close() { CheckClosed(); closed = true; queueFiller.Wait(); Directory.Delete(workingDir, true); } private Action CreateEventAction(Action> handler) { return (m, s) => { handler(new ActivateEvent(m, JsonConvert.DeserializeObject(s)!)); }; } private void FillQueue(OverwatchTranscript model, string workingDir) { var reader = new MomentReader(model, workingDir); while (true) { if (closed) { reader.Close(); return; } while (queue.Count < 10) { var moment = reader.Next(); if (moment == null) { reader.Close(); return; } queue.Enqueue(moment); } Thread.Sleep(1); } } private TimeSpan? GetMomentDuration() { if (moment == null) return null; if (next == null) return null; return next.Utc - moment.Utc; } private void ActivateMoment(OverwatchMoment moment, TimeSpan? duration) { var m = new ActivateMoment(moment.Utc, duration, momentCounter); lock (handlersLock) { ActivateMomentHandlers(m); foreach (var @event in moment.Events) { ActivateEventHandlers(m, @event); } } momentCounter++; } private void ActivateMomentHandlers(ActivateMoment m) { foreach (var handler in momentHandlers) { handler(m); } } private void ActivateEventHandlers(ActivateMoment m, OverwatchEvent @event) { if (!eventHandlers.ContainsKey(@event.Type)) return; var handlers = eventHandlers[@event.Type]; foreach (var handler in handlers) { handler(m, @event.Payload); } } private OverwatchTranscript LoadModel(string inputFilename) { ZipFile.ExtractToDirectory(inputFilename, workingDir); if (!File.Exists(transcriptFile)) { closed = true; throw new Exception("Is not a transcript file. Unzipped to: " + workingDir); } return JsonConvert.DeserializeObject(File.ReadAllText(transcriptFile))!; } private void CheckClosed() { if (closed) throw new Exception("Transcript has already been closed."); } } public class ActivateMoment { public ActivateMoment(DateTime utc, TimeSpan? duration, long index) { Utc = utc; Duration = duration; Index = index; } public DateTime Utc { get; } public TimeSpan? Duration { get; } public long Index { get; } } public class ActivateEvent { public ActivateEvent(ActivateMoment moment, T payload) { Moment = moment; Payload = payload; } public ActivateMoment Moment { get; } public T Payload { get; } } }