/ Framework / OverwatchTranscript / TranscriptReader.cs
TranscriptReader.cs
  1  using Newtonsoft.Json;
  2  using System.IO;
  3  using System;
  4  using System.IO.Compression;
  5  using System.Linq;
  6  using System.Collections.Generic;
  7  using System.Collections.Concurrent;
  8  
  9  namespace OverwatchTranscript
 10  {
 11      public interface ITranscriptReader
 12      {
 13          OverwatchCommonHeader Header { get; }
 14          T GetHeader<T>(string key);
 15          void AddMomentHandler(Action<ActivateMoment> handler);
 16          void AddEventHandler<T>(Action<ActivateEvent<T>> handler);
 17          bool Next();
 18          void Close();
 19      }
 20  
 21      public class TranscriptReader : ITranscriptReader
 22      {
 23          private readonly object handlersLock = new object();
 24          private readonly string transcriptFile;
 25          private readonly string artifactsFolder;
 26          private readonly List<Action<ActivateMoment>> momentHandlers = new List<Action<ActivateMoment>>();
 27          private readonly Dictionary<string, List<Action<ActivateMoment, string>>> eventHandlers = new Dictionary<string, List<Action<ActivateMoment, string>>>();
 28          private readonly string workingDir;
 29          private readonly OverwatchTranscript model;
 30          private bool closed;
 31          private long momentCounter;
 32          private readonly ConcurrentQueue<OverwatchMoment> queue = new ConcurrentQueue<OverwatchMoment>();
 33          private readonly Task queueFiller;
 34  
 35          public TranscriptReader(string workingDir, string inputFilename)
 36          {
 37              closed = false;
 38              this.workingDir = workingDir;
 39              transcriptFile = Path.Combine(workingDir, TranscriptConstants.TranscriptFilename);
 40              artifactsFolder = Path.Combine(workingDir, TranscriptConstants.ArtifactFolderName);
 41  
 42              if (!Directory.Exists(workingDir)) Directory.CreateDirectory(workingDir);
 43              if (File.Exists(transcriptFile) || Directory.Exists(artifactsFolder)) throw new Exception("workingdir not clean");
 44  
 45              model = LoadModel(inputFilename);
 46  
 47              queueFiller = Task.Run(() => FillQueue(model, workingDir));
 48          }
 49  
 50          public OverwatchCommonHeader Header
 51          {
 52              get
 53              {
 54                  CheckClosed();
 55                  return model.Header.Common;
 56              }
 57          }
 58  
 59          public T GetHeader<T>(string key)
 60          {
 61              CheckClosed();
 62              var value = model.Header.Entries.First(e => e.Key == key).Value;
 63              return JsonConvert.DeserializeObject<T>(value)!;
 64          }
 65  
 66          public void AddMomentHandler(Action<ActivateMoment> handler)
 67          {
 68              CheckClosed();
 69              lock (handlersLock)
 70              {
 71                  momentHandlers.Add(handler);
 72              }
 73          }
 74  
 75          public void AddEventHandler<T>(Action<ActivateEvent<T>> handler)
 76          {
 77              CheckClosed();
 78  
 79              var typeName = typeof(T).FullName;
 80              if (string.IsNullOrEmpty(typeName)) throw new Exception("Empty typename for payload");
 81  
 82              lock (handlersLock)
 83              {
 84                  if (eventHandlers.ContainsKey(typeName))
 85                  {
 86                      eventHandlers[typeName].Add(CreateEventAction(handler));
 87                  }
 88                  else
 89                  {
 90                      eventHandlers.Add(typeName, new List<Action<ActivateMoment, string>>
 91                      {
 92                          CreateEventAction(handler)
 93                      });
 94                  }
 95              }
 96          }
 97  
 98          private readonly object nextLock = new object();
 99          private OverwatchMoment? moment = null;
100          private OverwatchMoment? next = null;
101  
102          public bool Next()
103          {
104              CheckClosed();
105  
106              OverwatchMoment? m = null;
107              TimeSpan? duration = null;
108              lock (nextLock)
109              {
110                  if (next == null)
111                  {
112                      if (!queue.TryDequeue(out moment)) return false;
113                      queue.TryDequeue(out next);
114                  }
115                  else
116                  {
117                      moment = next;
118                      next = null;
119                      queue.TryDequeue(out next);
120                  }
121  
122                  m = moment;
123                  duration = GetMomentDuration();
124              }
125  
126              ActivateMoment(moment, duration);
127  
128              return true;
129          }
130  
131          public void Close()
132          {
133              CheckClosed();
134              closed = true;
135  
136              queueFiller.Wait();
137  
138              Directory.Delete(workingDir, true);
139          }
140  
141          private Action<ActivateMoment, string> CreateEventAction<T>(Action<ActivateEvent<T>> handler)
142          {
143              return (m, s) =>
144              {
145                  handler(new ActivateEvent<T>(m, JsonConvert.DeserializeObject<T>(s)!));
146              };
147          }
148  
149          private void FillQueue(OverwatchTranscript model, string workingDir)
150          {
151              var reader = new MomentReader(model, workingDir);
152  
153              while (true)
154              {
155                  if (closed)
156                  {
157                      reader.Close();
158                      return;
159                  }
160  
161                  while (queue.Count < 10)
162                  {
163                      var moment = reader.Next();
164                      if (moment == null)
165                      {
166                          reader.Close();
167                          return;
168                      }
169                      queue.Enqueue(moment);
170                  }
171  
172                  Thread.Sleep(1);
173              }
174          }
175  
176          private TimeSpan? GetMomentDuration()
177          {
178              if (moment == null) return null;
179              if (next == null) return null;
180  
181              return next.Utc - moment.Utc;
182          }
183  
184          private void ActivateMoment(OverwatchMoment moment, TimeSpan? duration)
185          {
186              var m = new ActivateMoment(moment.Utc, duration, momentCounter);
187  
188              lock (handlersLock)
189              {
190                  ActivateMomentHandlers(m);
191  
192                  foreach (var @event in moment.Events)
193                  {
194                      ActivateEventHandlers(m, @event);
195                  }
196              }
197  
198              momentCounter++;
199          }
200  
201          private void ActivateMomentHandlers(ActivateMoment m)
202          {
203              foreach (var handler in momentHandlers)
204              {
205                  handler(m);
206              }
207          }
208  
209          private void ActivateEventHandlers(ActivateMoment m, OverwatchEvent @event)
210          {
211              if (!eventHandlers.ContainsKey(@event.Type)) return;
212              var handlers = eventHandlers[@event.Type];
213  
214              foreach (var handler in handlers)
215              {
216                  handler(m, @event.Payload);
217              }
218          }
219  
220          private OverwatchTranscript LoadModel(string inputFilename)
221          {
222              ZipFile.ExtractToDirectory(inputFilename, workingDir);
223  
224              if (!File.Exists(transcriptFile))
225              {
226                  closed = true;
227                  throw new Exception("Is not a transcript file. Unzipped to: " + workingDir);
228              }
229  
230              return JsonConvert.DeserializeObject<OverwatchTranscript>(File.ReadAllText(transcriptFile))!;
231          }
232  
233          private void CheckClosed()
234          {
235              if (closed) throw new Exception("Transcript has already been closed.");
236          }
237      }
238  
239      public class ActivateMoment
240      {
241          public ActivateMoment(DateTime utc, TimeSpan? duration, long index)
242          {
243              Utc = utc;
244              Duration = duration;
245              Index = index;
246          }
247  
248          public DateTime Utc { get; }
249          public TimeSpan? Duration { get; }
250          public long Index { get; }
251      }
252  
253      public class ActivateEvent<T>
254      {
255          public ActivateEvent(ActivateMoment moment, T payload)
256          {
257              Moment = moment;
258              Payload = payload;
259          }
260  
261          public ActivateMoment Moment { get; }
262          public T Payload { get; }
263      }
264  }