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 }