using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using Microsoft.CodeAnalysis; using NuageReport.Core.Model; [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("TheoHay, GangDesNuages")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+f7dcda716f6ccd2361b1be02417714f4cb5ee1f9")] [assembly: AssemblyProduct("NuageReport.Core")] [assembly: AssemblyTitle("NuageReport.Core")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/TheoHay/NuageREPO")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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 NuageReport.Core.Reporting { public sealed class DisplayReportFormatter { public string Format(RunReport report, DisplayReportMode mode) { if (report == null) { throw new ArgumentNullException("report"); } RunReportSummary summary = report.Summary; GlobalReportSummary global = summary.Global; StringBuilder stringBuilder = new StringBuilder(); AppendSection(stringBuilder, "Global data"); AppendCountAndValue(stringBuilder, "Objects in map", global.ObjectsAtStart, global.ObjectsAtStartValue); AppendCountAndValue(stringBuilder, "Objects extracted", global.ObjectsExtracted, global.ObjectsExtractedValue); stringBuilder.Append("Objects detected: ").Append(global.ObjectsDetected.ToString(CultureInfo.InvariantCulture)).Append(" / ") .Append(global.ObjectsAtStart.ToString(CultureInfo.InvariantCulture)) .Append(" (") .Append(global.ObjectsDetectedPercent.ToString("0.##", CultureInfo.InvariantCulture)) .AppendLine("%)"); stringBuilder.Append("Objects missed: ").AppendLine(global.ObjectsMissed.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append("Objects damaged: ").AppendLine(global.ObjectsDamaged.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append("Objects destroyed: ").AppendLine(global.ObjectsDestroyed.ToString(CultureInfo.InvariantCulture)); AppendValue(stringBuilder, "Total value lost", global.ObjectsValueLost); stringBuilder.AppendLine(); AppendCountAndValue(stringBuilder, "Monster loot orbs collected", global.OrbsCollected, global.OrbsExtractedValue); stringBuilder.Append("Monster loot orbs destroyed: ").Append(global.OrbsDestroyed.ToString(CultureInfo.InvariantCulture)).Append(" ($") .Append(global.OrbsLostValue.ToString(CultureInfo.InvariantCulture)) .AppendLine(" lost)"); stringBuilder.AppendLine(); stringBuilder.Append("Damage dealt to monsters: ").AppendLine(global.MonsterDamageTaken.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append("Monsters killed: ").AppendLine(global.MonstersKilled.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append("Damage taken by players: ").AppendLine(global.PlayerDamageTaken.ToString(CultureInfo.InvariantCulture)); AppendSection(stringBuilder, "Player data"); if (summary.Players.Count == 0) { stringBuilder.AppendLine("No player-specific stats recorded yet."); } else { foreach (PlayerReportSummary player in summary.Players) { string value = (string.IsNullOrWhiteSpace(player.DisplayName) ? player.PlayerId : player.DisplayName); stringBuilder.AppendLine(value); stringBuilder.Append(" Cart/cashout objects: ").Append(player.ObjectsPutInCartOrCashout.ToString(CultureInfo.InvariantCulture)).Append(" ($") .Append(player.ObjectsPutInCartOrCashoutValue.ToString(CultureInfo.InvariantCulture)) .AppendLine(")"); stringBuilder.Append(" Extracted weight: ").Append(player.ExtractedObjectWeight.ToString("0.##", CultureInfo.InvariantCulture)).Append(" total, ") .Append(player.AverageExtractedObjectWeight.ToString("0.##", CultureInfo.InvariantCulture)) .AppendLine(" avg/object"); stringBuilder.Append(" Average extracted value: $").Append(player.AverageExtractedObjectValue.ToString(CultureInfo.InvariantCulture)).AppendLine("/object"); stringBuilder.Append(" Objects detected: ").Append(player.ObjectsDetected.ToString(CultureInfo.InvariantCulture)).Append(" ($") .Append(player.ObjectsDetectedValue.ToString(CultureInfo.InvariantCulture)) .AppendLine(")"); stringBuilder.Append(" Damage taken: ").AppendLine(player.DamageTaken.ToString(CultureInfo.InvariantCulture)); stringBuilder.AppendLine(); } } return stringBuilder.ToString().TrimEnd(); } private static void AppendSection(StringBuilder builder, string title) { if (builder.Length > 0) { builder.AppendLine(); } builder.AppendLine(title.ToUpperInvariant()); builder.AppendLine("----------------------------------------"); } private static void AppendValue(StringBuilder builder, string label, int value) { builder.Append(label).Append(": $").AppendLine(value.ToString(CultureInfo.InvariantCulture)); } private static void AppendCountAndValue(StringBuilder builder, string label, int count, int value) { builder.Append(label).Append(": ").Append(count.ToString(CultureInfo.InvariantCulture)) .Append(" ($") .Append(value.ToString(CultureInfo.InvariantCulture)) .AppendLine(")"); } } public enum DisplayReportMode { Summary, Live } public sealed class JsonReportFormatter { public string Format(RunReport report) { if (report == null) { throw new ArgumentNullException("report"); } RunReportSummary summary = report.Summary; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("{"); AppendProperty(stringBuilder, 1, "schemaVersion", "1", comma: true); AppendProperty(stringBuilder, 1, "runId", report.RunId, comma: true); AppendProperty(stringBuilder, 1, "startedAt", report.StartedAt.ToString("O", CultureInfo.InvariantCulture), comma: true); AppendProperty(stringBuilder, 1, "endedAt", report.EndedAt?.ToString("O", CultureInfo.InvariantCulture), comma: true); AppendGlobal(stringBuilder, summary.Global); stringBuilder.AppendLine(","); AppendPlayers(stringBuilder, summary); stringBuilder.AppendLine(","); AppendCosmeticObjects(stringBuilder, summary); stringBuilder.AppendLine(); stringBuilder.AppendLine("}"); return stringBuilder.ToString(); } private static void AppendGlobal(StringBuilder builder, GlobalReportSummary global) { Indent(builder, 1).AppendLine("\"global\": {"); AppendProperty(builder, 2, "objectsAtStart", global.ObjectsAtStart, comma: true); AppendProperty(builder, 2, "objectsAtStartValue", global.ObjectsAtStartValue, comma: true); AppendProperty(builder, 2, "objectsExtracted", global.ObjectsExtracted, comma: true); AppendProperty(builder, 2, "objectsExtractedValue", global.ObjectsExtractedValue, comma: true); AppendProperty(builder, 2, "objectsDetected", global.ObjectsDetected, comma: true); AppendProperty(builder, 2, "objectsDetectedPercent", global.ObjectsDetectedPercent, comma: true); AppendProperty(builder, 2, "objectsMissed", global.ObjectsMissed, comma: true); AppendProperty(builder, 2, "objectsDamaged", global.ObjectsDamaged, comma: true); AppendProperty(builder, 2, "objectsDestroyed", global.ObjectsDestroyed, comma: true); AppendProperty(builder, 2, "objectsValueLost", global.ObjectsValueLost, comma: true); AppendProperty(builder, 2, "orbsCollected", global.OrbsCollected, comma: true); AppendProperty(builder, 2, "orbsDestroyed", global.OrbsDestroyed, comma: true); AppendProperty(builder, 2, "orbsExtractedValue", global.OrbsExtractedValue, comma: true); AppendProperty(builder, 2, "orbsLostValue", global.OrbsLostValue, comma: true); AppendProperty(builder, 2, "cosmeticObjectsExisting", global.CosmeticObjectsExisting, comma: true); AppendProperty(builder, 2, "cosmeticObjectsFound", global.CosmeticObjectsFound, comma: true); AppendProperty(builder, 2, "monsterDamageTaken", global.MonsterDamageTaken, comma: true); AppendProperty(builder, 2, "monstersKilled", global.MonstersKilled, comma: true); AppendProperty(builder, 2, "playerDamageTaken", global.PlayerDamageTaken, comma: false); Indent(builder, 1).Append('}'); } private static void AppendPlayers(StringBuilder builder, RunReportSummary summary) { Indent(builder, 1).AppendLine("\"players\": ["); for (int i = 0; i < summary.Players.Count; i++) { PlayerReportSummary playerReportSummary = summary.Players[i]; Indent(builder, 2).AppendLine("{"); AppendProperty(builder, 3, "playerId", playerReportSummary.PlayerId, comma: true); AppendProperty(builder, 3, "displayName", playerReportSummary.DisplayName, comma: true); AppendProperty(builder, 3, "objectsPutInCartOrCashout", playerReportSummary.ObjectsPutInCartOrCashout, comma: true); AppendProperty(builder, 3, "objectsPutInCartOrCashoutValue", playerReportSummary.ObjectsPutInCartOrCashoutValue, comma: true); AppendProperty(builder, 3, "extractedObjectWeight", playerReportSummary.ExtractedObjectWeight, comma: true); AppendProperty(builder, 3, "averageExtractedObjectWeight", playerReportSummary.AverageExtractedObjectWeight, comma: true); AppendProperty(builder, 3, "averageExtractedObjectValue", playerReportSummary.AverageExtractedObjectValue, comma: true); AppendProperty(builder, 3, "objectsDetected", playerReportSummary.ObjectsDetected, comma: true); AppendProperty(builder, 3, "objectsDetectedValue", playerReportSummary.ObjectsDetectedValue, comma: true); AppendProperty(builder, 3, "damageDealt", playerReportSummary.DamageDealt, comma: true); AppendProperty(builder, 3, "damageTaken", playerReportSummary.DamageTaken, comma: false); Indent(builder, 2).Append('}'); if (i < summary.Players.Count - 1) { builder.Append(','); } builder.AppendLine(); } Indent(builder, 1).Append(']'); } private static void AppendCosmeticObjects(StringBuilder builder, RunReportSummary summary) { Indent(builder, 1).AppendLine("\"cosmeticObjects\": {"); AppendValuableList(builder, 2, "existing", summary.CosmeticObjectsExisting, comma: true); AppendValuableList(builder, 2, "found", summary.CosmeticObjectsFound, comma: false); Indent(builder, 1).Append('}'); } private static void AppendValuableList(StringBuilder builder, int depth, string name, IReadOnlyList values, bool comma) { Indent(builder, depth).Append('"').Append(Escape(name)).AppendLine("\": ["); for (int i = 0; i < values.Count; i++) { ValuableRef valuableRef = values[i]; Indent(builder, depth + 1).Append("{ "); AppendInlineProperty(builder, "runtimeId", valuableRef.RuntimeId, comma: true); AppendInlineProperty(builder, "name", valuableRef.Name, comma: true); AppendInlineProperty(builder, "valueAtEvent", valuableRef.ValueAtEvent, comma: true); AppendInlineProperty(builder, "weightAtEvent", valuableRef.WeightAtEvent, comma: false); builder.Append(" }"); if (i < values.Count - 1) { builder.Append(','); } builder.AppendLine(); } Indent(builder, depth).Append(']'); if (comma) { builder.Append(','); } builder.AppendLine(); } private static void AppendProperty(StringBuilder builder, int depth, string name, string? value, bool comma) { Indent(builder, depth).Append('"').Append(Escape(name)).Append("\": "); if (value == null) { builder.Append("null"); } else { builder.Append('"').Append(Escape(value)).Append('"'); } AppendLineEnding(builder, comma); } private static void AppendProperty(StringBuilder builder, int depth, string name, int? value, bool comma) { Indent(builder, depth).Append('"').Append(Escape(name)).Append("\": "); builder.Append(value.HasValue ? value.Value.ToString(CultureInfo.InvariantCulture) : "null"); AppendLineEnding(builder, comma); } private static void AppendProperty(StringBuilder builder, int depth, string name, int value, bool comma) { Indent(builder, depth).Append('"').Append(Escape(name)).Append("\": ") .Append(value.ToString(CultureInfo.InvariantCulture)); AppendLineEnding(builder, comma); } private static void AppendProperty(StringBuilder builder, int depth, string name, double value, bool comma) { Indent(builder, depth).Append('"').Append(Escape(name)).Append("\": ") .Append(value.ToString("0.##", CultureInfo.InvariantCulture)); AppendLineEnding(builder, comma); } private static void AppendProperty(StringBuilder builder, int depth, string name, float value, bool comma) { Indent(builder, depth).Append('"').Append(Escape(name)).Append("\": ") .Append(value.ToString("0.###", CultureInfo.InvariantCulture)); AppendLineEnding(builder, comma); } private static void AppendProperty(StringBuilder builder, int depth, string name, bool value, bool comma) { Indent(builder, depth).Append('"').Append(Escape(name)).Append("\": ") .Append(value ? "true" : "false"); AppendLineEnding(builder, comma); } private static void AppendInlineProperty(StringBuilder builder, string name, string? value, bool comma) { builder.Append('"').Append(Escape(name)).Append("\": "); if (value == null) { builder.Append("null"); } else { builder.Append('"').Append(Escape(value)).Append('"'); } if (comma) { builder.Append(", "); } } private static void AppendInlineProperty(StringBuilder builder, string name, int? value, bool comma) { builder.Append('"').Append(Escape(name)).Append("\": "); builder.Append(value.HasValue ? value.Value.ToString(CultureInfo.InvariantCulture) : "null"); if (comma) { builder.Append(", "); } } private static void AppendInlineProperty(StringBuilder builder, string name, float? value, bool comma) { builder.Append('"').Append(Escape(name)).Append("\": "); builder.Append(value.HasValue ? value.Value.ToString("0.###", CultureInfo.InvariantCulture) : "null"); if (comma) { builder.Append(", "); } } private static void AppendLineEnding(StringBuilder builder, bool comma) { if (comma) { builder.Append(','); } builder.AppendLine(); } private static StringBuilder Indent(StringBuilder builder, int depth) { return builder.Append(' ', depth * 2); } private static string Escape(string value) { StringBuilder stringBuilder = new StringBuilder(value.Length + 8); foreach (char c in value) { switch (c) { case '\\': stringBuilder.Append("\\\\"); continue; case '"': stringBuilder.Append("\\\""); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (char.IsControl(c)) { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } } public sealed class LiveReportPayloadFormatter { public string Format(RunReport report) { if (report == null) { throw new ArgumentNullException("report"); } return new JsonReportFormatter().Format(report); } } public sealed class TextReportFormatter { public string Format(RunReport report) { if (report == null) { throw new ArgumentNullException("report"); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Run: " + report.RunId); stringBuilder.AppendLine($"Started: {report.StartedAt:u}"); stringBuilder.AppendLine("Ended: " + (report.EndedAt.HasValue ? report.EndedAt.Value.ToString("u") : "active")); stringBuilder.AppendLine($"Objects at start: {report.Summary.Global.ObjectsAtStart} (${report.Summary.Global.ObjectsAtStartValue})"); stringBuilder.AppendLine($"Discovered valuables: {report.Summary.Global.ObjectsDetected} ({report.Summary.Global.ObjectsDetectedPercent:0.##}%)"); stringBuilder.AppendLine($"First carted valuables: {report.FirstCartedCount}"); stringBuilder.AppendLine($"Extracted value: ${report.Summary.Global.ObjectsExtractedValue}"); stringBuilder.AppendLine($"Lost/destroyed value: ${report.Summary.Global.ObjectsValueLost}"); stringBuilder.AppendLine($"Monster damage taken: {report.Summary.Global.MonsterDamageTaken}"); stringBuilder.AppendLine($"Player damage taken: {report.Summary.Global.PlayerDamageTaken}"); stringBuilder.AppendLine(); stringBuilder.AppendLine("Events:"); foreach (RunEvent @event in report.Events) { stringBuilder.Append("- "); stringBuilder.Append(@event.Timestamp.ToString("u")); stringBuilder.Append(' '); stringBuilder.Append(@event.Type); stringBuilder.Append(" ["); stringBuilder.Append(@event.Confidence); stringBuilder.Append(']'); if (@event.Valuable != null) { stringBuilder.Append(" valuable="); stringBuilder.Append(@event.Valuable); } if (@event.Player != null) { stringBuilder.Append(" player="); stringBuilder.Append(@event.Player); } if (!string.IsNullOrWhiteSpace(@event.Message)) { stringBuilder.Append(" — "); stringBuilder.Append(@event.Message); } stringBuilder.AppendLine(); } return stringBuilder.ToString(); } } } namespace NuageReport.Core.Recording { public interface IClock { DateTimeOffset UtcNow { get; } } public interface IRunEventSink { void Record(RunEvent runEvent); } public sealed class RunRecorder : IRunEventSink { private readonly IClock _clock; private readonly List _events = new List(); private readonly Dictionary _spawnedValuables = new Dictionary(StringComparer.Ordinal); private readonly HashSet _discoveredValuables = new HashSet(StringComparer.Ordinal); private readonly HashSet _firstCartedValuables = new HashSet(StringComparer.Ordinal); private readonly HashSet _extractedValuables = new HashSet(StringComparer.Ordinal); private readonly HashSet _destroyedValuables = new HashSet(StringComparer.Ordinal); private readonly Dictionary _lockedValuableAttribution = new Dictionary(StringComparer.Ordinal); private readonly HashSet _spawnedOrbs = new HashSet(StringComparer.Ordinal); private readonly HashSet _extractedOrbs = new HashSet(StringComparer.Ordinal); private readonly HashSet _destroyedOrbs = new HashSet(StringComparer.Ordinal); private readonly HashSet _cosmeticObjectsExisting = new HashSet(StringComparer.Ordinal); private readonly HashSet _cosmeticObjectsFound = new HashSet(StringComparer.Ordinal); private readonly HashSet _killedMonsters = new HashSet(StringComparer.Ordinal); private string? _activeRunId; private DateTimeOffset _startedAt; private DateTimeOffset? _endedAt; public bool IsActive => _activeRunId != null; public string? ActiveRunId => _activeRunId; public int EventVersion { get; private set; } public event Action? EventRecorded; public RunRecorder(IClock clock) { _clock = clock ?? throw new ArgumentNullException("clock"); } public void StartRun(string runId, string? levelName) { if (string.IsNullOrWhiteSpace(runId)) { throw new ArgumentException("Run id is required.", "runId"); } _activeRunId = runId; _startedAt = _clock.UtcNow; _endedAt = null; EventVersion = 0; _events.Clear(); _spawnedValuables.Clear(); _discoveredValuables.Clear(); _firstCartedValuables.Clear(); _extractedValuables.Clear(); _destroyedValuables.Clear(); _lockedValuableAttribution.Clear(); _spawnedOrbs.Clear(); _extractedOrbs.Clear(); _destroyedOrbs.Clear(); _cosmeticObjectsExisting.Clear(); _cosmeticObjectsFound.Clear(); _killedMonsters.Clear(); Record(new RunEvent(RunEventType.LevelStarted, _startedAt, ConfidenceLevel.High) { LevelName = levelName, Message = "Level started." }); } public bool TryRecordValuableSpawned(ValuableRef valuable, string? levelName = null) { if (valuable == null) { throw new ArgumentNullException("valuable"); } if (!IsActive) { return false; } if (_spawnedValuables.TryGetValue(valuable.RuntimeId, out ValuableRef value)) { _spawnedValuables[valuable.RuntimeId] = Merge(value, valuable); if ((valuable.ValueAtEvent.HasValue && value.ValueAtEvent != valuable.ValueAtEvent) || (valuable.WeightAtEvent.HasValue && value.WeightAtEvent != valuable.WeightAtEvent)) { Record(new RunEvent(RunEventType.ValuableValueUpdated, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = _spawnedValuables[valuable.RuntimeId], Message = "Valuable value/weight updated." }); } return false; } _spawnedValuables.Add(valuable.RuntimeId, valuable); Record(new RunEvent(RunEventType.ValuableSpawned, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = valuable, Message = "Valuable spawned." }); return true; } public bool TryRecordValuableDiscovered(ValuableRef valuable, PlayerRef? player = null, ConfidenceLevel confidence = ConfidenceLevel.High, string? levelName = null) { if (valuable == null) { throw new ArgumentNullException("valuable"); } if (!IsActive || !_discoveredValuables.Add(valuable.RuntimeId)) { return false; } Record(new RunEvent(RunEventType.ValuableDiscovered, _clock.UtcNow, confidence) { LevelName = levelName, Valuable = valuable, Player = player, Message = ((player == null) ? "Valuable discovered." : "Valuable discovered by player.") }); return true; } public bool TryRecordFirstCarted(ValuableRef valuable, string? levelName = null) { return TryRecordFirstCarted(valuable, null, ConfidenceLevel.High, levelName); } public bool TryRecordFirstCarted(ValuableRef valuable, PlayerRef? player, ConfidenceLevel confidence = ConfidenceLevel.Medium, string? levelName = null) { if (valuable == null) { throw new ArgumentNullException("valuable"); } if (!IsActive || !_firstCartedValuables.Add(valuable.RuntimeId)) { return false; } LockAttributionIfAvailable(valuable.RuntimeId, player); Record(new RunEvent(RunEventType.ValuableFirstCarted, _clock.UtcNow, confidence) { LevelName = levelName, Valuable = valuable, Player = (GetLockedAttribution(valuable.RuntimeId) ?? player), Message = "Valuable entered a cart or cashout zone for the first confirmed time." }); return true; } public bool TryRecordValuableExtracted(ValuableRef valuable, PlayerRef? player = null, ConfidenceLevel confidence = ConfidenceLevel.High, string? levelName = null) { if (valuable == null) { throw new ArgumentNullException("valuable"); } if (!IsActive || !_extractedValuables.Add(valuable.RuntimeId)) { return false; } LockAttributionIfAvailable(valuable.RuntimeId, player); Record(new RunEvent(RunEventType.ValuableExtracted, _clock.UtcNow, confidence) { LevelName = levelName, Valuable = valuable, Player = (GetLockedAttribution(valuable.RuntimeId) ?? player), Message = ((player == null) ? "Valuable extracted." : "Valuable extracted with player attribution.") }); return true; } public void RecordValuableDamaged(ValuableRef valuable, int valueLost, string? levelName = null) { if (valuable == null) { throw new ArgumentNullException("valuable"); } if (IsActive) { int num = Math.Max(0, valueLost); Record(new RunEvent(RunEventType.ValuableDamaged, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = valuable, DeltaValue = ((num != 0) ? (-num) : 0), Message = ((num == 0) ? "Valuable damaged." : "Valuable lost value from damage.") }); } } public bool TryRecordValuableDestroyed(ValuableRef valuable, string? levelName = null) { if (valuable == null) { throw new ArgumentNullException("valuable"); } if (!IsActive || !_destroyedValuables.Add(valuable.RuntimeId)) { return false; } Record(new RunEvent(RunEventType.ValuableDestroyed, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = valuable, Message = "Valuable destroyed." }); return true; } public bool TryRecordOrbSpawned(ValuableRef orb, string? rarity = null, string? levelName = null) { if (orb == null) { throw new ArgumentNullException("orb"); } if (!IsActive || !_spawnedOrbs.Add(orb.RuntimeId)) { return false; } RunEvent runEvent = new RunEvent(RunEventType.OrbSpawned, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = orb, Message = "Monster loot orb spawned." }; AddTag(runEvent, "rarity", rarity); Record(runEvent); return true; } public bool TryRecordOrbExtracted(ValuableRef orb, string? rarity = null, string? levelName = null) { if (orb == null) { throw new ArgumentNullException("orb"); } if (!IsActive || !_extractedOrbs.Add(orb.RuntimeId)) { return false; } RunEvent runEvent = new RunEvent(RunEventType.OrbExtracted, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = orb, Message = "Monster loot orb extracted." }; AddTag(runEvent, "rarity", rarity); Record(runEvent); return true; } public bool TryRecordOrbDestroyed(ValuableRef orb, string? rarity = null, string? levelName = null) { if (orb == null) { throw new ArgumentNullException("orb"); } if (!IsActive || !_destroyedOrbs.Add(orb.RuntimeId)) { return false; } RunEvent runEvent = new RunEvent(RunEventType.OrbDestroyed, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = orb, Message = "Monster loot orb destroyed." }; AddTag(runEvent, "rarity", rarity); Record(runEvent); return true; } public bool TryRecordCosmeticObjectExisting(ValuableRef cosmeticObject, string? rarity = null, string? levelName = null) { if (cosmeticObject == null) { throw new ArgumentNullException("cosmeticObject"); } if (!IsActive || !_cosmeticObjectsExisting.Add(cosmeticObject.RuntimeId)) { return false; } RunEvent runEvent = new RunEvent(RunEventType.CosmeticObjectExisting, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = cosmeticObject, Message = "Cosmetic object existed in the level." }; AddTag(runEvent, "rarity", rarity); Record(runEvent); return true; } public bool TryRecordCosmeticObjectFound(ValuableRef cosmeticObject, string? rarity = null, string? levelName = null) { if (cosmeticObject == null) { throw new ArgumentNullException("cosmeticObject"); } if (!IsActive || !_cosmeticObjectsFound.Add(cosmeticObject.RuntimeId)) { return false; } RunEvent runEvent = new RunEvent(RunEventType.CosmeticObjectFound, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Valuable = cosmeticObject, Message = "Cosmetic object was found/extracted." }; AddTag(runEvent, "rarity", rarity); Record(runEvent); return true; } public void RecordMonsterDamaged(string monsterId, string monsterName, int damageAmount, string? levelName = null) { if (!string.IsNullOrWhiteSpace(monsterId) && damageAmount > 0 && IsActive) { RunEvent runEvent = new RunEvent(RunEventType.MonsterDamaged, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, DamageAmount = damageAmount, Message = "Monster took damage." }; AddTag(runEvent, "monsterId", monsterId); AddTag(runEvent, "monsterName", string.IsNullOrWhiteSpace(monsterName) ? monsterId : monsterName); Record(runEvent); } } public bool TryRecordMonsterKilled(string monsterId, string monsterName, string? levelName = null) { if (string.IsNullOrWhiteSpace(monsterId) || !IsActive || !_killedMonsters.Add(monsterId)) { return false; } RunEvent runEvent = new RunEvent(RunEventType.MonsterKilled, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Message = "Monster killed." }; AddTag(runEvent, "monsterId", monsterId); AddTag(runEvent, "monsterName", string.IsNullOrWhiteSpace(monsterName) ? monsterId : monsterName); Record(runEvent); return true; } public void RecordPlayerDamaged(PlayerRef player, int damageAmount, string? levelName = null) { if (player == null) { throw new ArgumentNullException("player"); } if (damageAmount > 0 && IsActive) { Record(new RunEvent(RunEventType.PlayerDamaged, _clock.UtcNow, ConfidenceLevel.High) { LevelName = levelName, Player = player, DamageAmount = damageAmount, Message = "Player took damage." }); } } public void Record(RunEvent runEvent) { if (runEvent == null) { throw new ArgumentNullException("runEvent"); } _events.Add(runEvent); EventVersion++; this.EventRecorded?.Invoke(runEvent); } public RunReport EndRun(string? levelName = null) { if (_activeRunId == null) { throw new InvalidOperationException("Cannot end a run before StartRun has been called."); } _endedAt = _clock.UtcNow; Record(new RunEvent(RunEventType.LevelEnded, _endedAt.Value, ConfidenceLevel.High) { LevelName = levelName, Message = "Level ended." }); RunReport result = Snapshot(); _activeRunId = null; return result; } public RunReport Snapshot() { if (_activeRunId == null) { throw new InvalidOperationException("No active run is available."); } return new RunReport(_activeRunId, _startedAt, _endedAt, _events.ToArray()); } private void LockAttributionIfAvailable(string runtimeId, PlayerRef? player) { if (player != null && !_lockedValuableAttribution.ContainsKey(runtimeId)) { _lockedValuableAttribution.Add(runtimeId, player); } } private PlayerRef? GetLockedAttribution(string runtimeId) { if (!_lockedValuableAttribution.TryGetValue(runtimeId, out PlayerRef value)) { return null; } return value; } private static ValuableRef Merge(ValuableRef previous, ValuableRef next) { return new ValuableRef(previous.RuntimeId, string.IsNullOrWhiteSpace(next.Name) ? previous.Name : next.Name, next.ValueAtEvent ?? previous.ValueAtEvent, next.WeightAtEvent ?? previous.WeightAtEvent); } private static void AddTag(RunEvent runEvent, string key, string? value) { if (!string.IsNullOrWhiteSpace(value)) { runEvent.Tags[key] = value; } } } public sealed class SystemClock : IClock { public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; } } namespace NuageReport.Core.Model { public enum ConfidenceLevel { Low, Medium, High } public sealed class GlobalReportSummary { public int ObjectsAtStart { get; set; } public int ObjectsAtStartValue { get; set; } public int ObjectsExtracted { get; set; } public int ObjectsExtractedValue { get; set; } public int ObjectsDetected { get; set; } public double ObjectsDetectedPercent { get; set; } public int ObjectsMissed { get; set; } public int ObjectsDamaged { get; set; } public int ObjectsDestroyed { get; set; } public int ObjectsValueLost { get; set; } public int OrbsCollected { get; set; } public int OrbsDestroyed { get; set; } public int OrbsExtractedValue { get; set; } public int OrbsLostValue { get; set; } public int CosmeticObjectsExisting { get; set; } public int CosmeticObjectsFound { get; set; } public int MonsterDamageTaken { get; set; } public int MonstersKilled { get; set; } public int PlayerDamageTaken { get; set; } } public sealed class PlayerRef { public string Id { get; } public string DisplayName { get; } public PlayerRef(string id, string displayName) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentException("Player id is required.", "id"); } Id = id; DisplayName = (string.IsNullOrWhiteSpace(displayName) ? id : displayName); } public override string ToString() { return DisplayName; } } public sealed class PlayerReportSummary { public string PlayerId { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public int ObjectsPutInCartOrCashout { get; set; } public int ObjectsPutInCartOrCashoutValue { get; set; } public float ExtractedObjectWeight { get; set; } public float AverageExtractedObjectWeight { get; set; } public int AverageExtractedObjectValue { get; set; } public int ObjectsDetected { get; set; } public int ObjectsDetectedValue { get; set; } public int DamageDealt { get; set; } public int DamageTaken { get; set; } } public sealed class RunEvent { public RunEventType Type { get; } public DateTimeOffset Timestamp { get; } public ConfidenceLevel Confidence { get; } public string? LevelName { get; set; } public string? Message { get; set; } public ValuableRef? Valuable { get; set; } public PlayerRef? Player { get; set; } public int? DeltaValue { get; set; } public int? DamageAmount { get; set; } public IDictionary Tags { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public RunEvent(RunEventType type, DateTimeOffset timestamp, ConfidenceLevel confidence) { Type = type; Timestamp = timestamp; Confidence = confidence; } } public enum RunEventType { Unknown, LevelStarted, LevelEnded, ValuableSpawned, ValuableValueUpdated, ValuableDiscovered, ValuableFirstCarted, ValuableExtracted, ValuableDamaged, ValuableDestroyed, OrbSpawned, OrbExtracted, OrbDestroyed, CosmeticObjectExisting, CosmeticObjectFound, MonsterDamaged, MonsterKilled, PlayerDamaged, PlayerDied, PlayerDisconnected, ShopEntered, QuotaEvaluated } public sealed class RunReport { public string RunId { get; } public DateTimeOffset StartedAt { get; } public DateTimeOffset? EndedAt { get; } public IReadOnlyList Events { get; } public RunReportSummary Summary => RunReportSummary.Build(this); public int DiscoveredCount => Events.Count((RunEvent e) => e.Type == RunEventType.ValuableDiscovered); public int FirstCartedCount => Events.Count((RunEvent e) => e.Type == RunEventType.ValuableFirstCarted); public int ExtractedValue => Events.Where((RunEvent e) => e.Type == RunEventType.ValuableExtracted).Sum((RunEvent e) => e.Valuable?.ValueAtEvent ?? e.DeltaValue.GetValueOrDefault()); public int LostOrDestroyedValue => (from e in Events where e.Type == RunEventType.ValuableDestroyed || e.Type == RunEventType.ValuableDamaged where e.DeltaValue.HasValue && e.DeltaValue.Value < 0 select e).Sum((RunEvent e) => -e.DeltaValue.Value); public RunReport(string runId, DateTimeOffset startedAt, DateTimeOffset? endedAt, IReadOnlyList events) { if (string.IsNullOrWhiteSpace(runId)) { throw new ArgumentException("Run id is required.", "runId"); } RunId = runId; StartedAt = startedAt; EndedAt = endedAt; Events = events ?? throw new ArgumentNullException("events"); } } public sealed class RunReportSummary { private sealed class PlayerAccumulator { public PlayerRef Player { get; } public HashSet CartOrCashoutValuables { get; } = new HashSet(StringComparer.Ordinal); public HashSet ExtractedValuables { get; } = new HashSet(StringComparer.Ordinal); public Dictionary DetectedValuables { get; } = new Dictionary(StringComparer.Ordinal); public int DamageTaken { get; set; } public PlayerAccumulator(PlayerRef player) { Player = player; } public PlayerReportSummary ToSummary(Dictionary latestValuables) { Dictionary latestValuables2 = latestValuables; int objectsPutInCartOrCashoutValue = CartOrCashoutValuables.Sum((string id) => ValueFor(latestValuables2, id)); int num = ExtractedValuables.Sum((string id) => ValueFor(latestValuables2, id)); float num2 = ExtractedValuables.Sum((string id) => WeightFor(latestValuables2, id)); return new PlayerReportSummary { PlayerId = Player.Id, DisplayName = Player.DisplayName, ObjectsPutInCartOrCashout = CartOrCashoutValuables.Count, ObjectsPutInCartOrCashoutValue = objectsPutInCartOrCashoutValue, ExtractedObjectWeight = num2, AverageExtractedObjectWeight = ((ExtractedValuables.Count == 0) ? 0f : (num2 / (float)ExtractedValuables.Count)), AverageExtractedObjectValue = ((ExtractedValuables.Count != 0) ? (num / ExtractedValuables.Count) : 0), ObjectsDetected = DetectedValuables.Count, ObjectsDetectedValue = DetectedValuables.Values.Sum((ValuableRef v) => v.ValueAtEvent ?? ValueFor(latestValuables2, v.RuntimeId)), DamageDealt = 0, DamageTaken = DamageTaken }; } } public GlobalReportSummary Global { get; } public IReadOnlyList Players { get; } public IReadOnlyList CosmeticObjectsExisting { get; } public IReadOnlyList CosmeticObjectsFound { get; } private RunReportSummary(GlobalReportSummary global, IReadOnlyList players, IReadOnlyList cosmeticObjectsExisting, IReadOnlyList cosmeticObjectsFound) { Global = global; Players = players; CosmeticObjectsExisting = cosmeticObjectsExisting; CosmeticObjectsFound = cosmeticObjectsFound; } public static RunReportSummary Build(RunReport report) { if (report == null) { throw new ArgumentNullException("report"); } Dictionary initialValuables = new Dictionary(StringComparer.Ordinal); Dictionary latestValuables = new Dictionary(StringComparer.Ordinal); HashSet hashSet = new HashSet(StringComparer.Ordinal); HashSet hashSet2 = new HashSet(StringComparer.Ordinal); HashSet hashSet3 = new HashSet(StringComparer.Ordinal); HashSet hashSet4 = new HashSet(StringComparer.Ordinal); HashSet hashSet5 = new HashSet(StringComparer.Ordinal); HashSet hashSet6 = new HashSet(StringComparer.Ordinal); Dictionary dictionary = new Dictionary(StringComparer.Ordinal); Dictionary dictionary2 = new Dictionary(StringComparer.Ordinal); HashSet hashSet7 = new HashSet(StringComparer.Ordinal); Dictionary dictionary3 = new Dictionary(StringComparer.Ordinal); Dictionary dictionary4 = new Dictionary(StringComparer.Ordinal); Dictionary dictionary5 = new Dictionary(StringComparer.Ordinal); Dictionary dictionary6 = new Dictionary(StringComparer.Ordinal); int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; foreach (RunEvent @event in report.Events) { if (@event.Valuable != null) { latestValuables[@event.Valuable.RuntimeId] = Merge(latestValuables, @event.Valuable); } switch (@event.Type) { case RunEventType.ValuableSpawned: if (@event.Valuable != null) { initialValuables[@event.Valuable.RuntimeId] = Merge(initialValuables, @event.Valuable); } break; case RunEventType.ValuableValueUpdated: if (@event.Valuable != null && initialValuables.ContainsKey(@event.Valuable.RuntimeId)) { initialValuables[@event.Valuable.RuntimeId] = Merge(initialValuables, @event.Valuable); } break; case RunEventType.ValuableDiscovered: if (@event.Valuable != null) { hashSet2.Add(@event.Valuable.RuntimeId); if (@event.Player != null) { GetPlayer(dictionary3, @event.Player).DetectedValuables[@event.Valuable.RuntimeId] = @event.Valuable; } } break; case RunEventType.ValuableFirstCarted: if (@event.Valuable != null && @event.Player != null) { dictionary4[@event.Valuable.RuntimeId] = @event.Player; } break; case RunEventType.ValuableExtracted: if (@event.Valuable != null) { hashSet.Add(@event.Valuable.RuntimeId); if (@event.Player != null) { dictionary5[@event.Valuable.RuntimeId] = @event.Player; } } break; case RunEventType.ValuableDamaged: if (@event.Valuable != null) { hashSet3.Add(@event.Valuable.RuntimeId); } if (@event.DeltaValue.HasValue && @event.DeltaValue.Value < 0) { AddValueLost(dictionary6, @event.Valuable?.RuntimeId, -@event.DeltaValue.Value); } break; case RunEventType.ValuableDestroyed: if (@event.Valuable != null) { hashSet4.Add(@event.Valuable.RuntimeId); AddValueLost(dictionary6, @event.Valuable.RuntimeId, ValueFor(latestValuables, @event.Valuable.RuntimeId)); } break; case RunEventType.OrbExtracted: if (@event.Valuable != null && hashSet5.Add(@event.Valuable.RuntimeId)) { num += @event.Valuable.ValueAtEvent.GetValueOrDefault(); } break; case RunEventType.OrbDestroyed: if (@event.Valuable != null && hashSet6.Add(@event.Valuable.RuntimeId)) { num2 += @event.Valuable.ValueAtEvent.GetValueOrDefault(); } break; case RunEventType.CosmeticObjectExisting: if (@event.Valuable != null) { dictionary[@event.Valuable.RuntimeId] = Merge(dictionary, @event.Valuable); } break; case RunEventType.CosmeticObjectFound: if (@event.Valuable != null) { dictionary2[@event.Valuable.RuntimeId] = Merge(dictionary2, @event.Valuable); } break; case RunEventType.MonsterDamaged: num3 += @event.DamageAmount.GetValueOrDefault(); break; case RunEventType.MonsterKilled: hashSet7.Add(TagValue(@event, "monsterId", @event.Message ?? @event.Timestamp.ToUnixTimeMilliseconds().ToString())); break; case RunEventType.PlayerDamaged: num4 += @event.DamageAmount.GetValueOrDefault(); if (@event.Player != null) { GetPlayer(dictionary3, @event.Player).DamageTaken += @event.DamageAmount.GetValueOrDefault(); } break; } } foreach (KeyValuePair item in dictionary4) { GetPlayer(dictionary3, item.Value).CartOrCashoutValuables.Add(item.Key); } foreach (KeyValuePair item2 in dictionary5) { PlayerRef value; PlayerRef playerRef = (dictionary4.TryGetValue(item2.Key, out value) ? value : item2.Value); PlayerAccumulator player = GetPlayer(dictionary3, playerRef); player.ExtractedValuables.Add(item2.Key); if (!dictionary4.ContainsKey(item2.Key)) { player.CartOrCashoutValuables.Add(item2.Key); } } GlobalReportSummary global = new GlobalReportSummary { ObjectsAtStart = initialValuables.Count, ObjectsAtStartValue = initialValuables.Values.Sum((ValuableRef v) => v.ValueAtEvent.GetValueOrDefault()), ObjectsExtracted = hashSet.Count, ObjectsExtractedValue = hashSet.Sum((string id) => ValueFor(latestValuables, id)), ObjectsDetected = hashSet2.Count, ObjectsDetectedPercent = ((initialValuables.Count == 0) ? 0.0 : Math.Round((double)hashSet2.Count / (double)initialValuables.Count * 100.0, 2)), ObjectsMissed = Math.Max(0, initialValuables.Count - hashSet2.Count), ObjectsDamaged = hashSet3.Count, ObjectsDestroyed = hashSet4.Count, ObjectsValueLost = dictionary6.Sum((KeyValuePair pair) => Math.Min(pair.Value, (ValueFor(initialValuables, pair.Key) == 0) ? pair.Value : ValueFor(initialValuables, pair.Key))), OrbsCollected = hashSet5.Count, OrbsDestroyed = hashSet6.Count, OrbsExtractedValue = num, OrbsLostValue = num2, CosmeticObjectsExisting = dictionary.Count, CosmeticObjectsFound = dictionary2.Count, MonsterDamageTaken = num3, MonstersKilled = hashSet7.Count, PlayerDamageTaken = num4 }; List players = (from p in dictionary3.Values.OrderBy((PlayerAccumulator p) => p.Player.DisplayName, StringComparer.OrdinalIgnoreCase) select p.ToSummary(latestValuables)).ToList(); return new RunReportSummary(global, players, dictionary.Values.OrderBy((ValuableRef v) => v.Name, StringComparer.OrdinalIgnoreCase).ToList(), dictionary2.Values.OrderBy((ValuableRef v) => v.Name, StringComparer.OrdinalIgnoreCase).ToList()); } private static void AddValueLost(Dictionary values, string? runtimeId, int valueLost) { if (!string.IsNullOrWhiteSpace(runtimeId) && valueLost > 0) { values[runtimeId] = (values.TryGetValue(runtimeId, out var value) ? (value + valueLost) : valueLost); } } private static PlayerAccumulator GetPlayer(Dictionary players, PlayerRef playerRef) { if (!players.TryGetValue(playerRef.Id, out PlayerAccumulator value)) { value = new PlayerAccumulator(playerRef); players.Add(playerRef.Id, value); } return value; } private static ValuableRef Merge(Dictionary refs, ValuableRef next) { if (!refs.TryGetValue(next.RuntimeId, out ValuableRef value)) { return next; } return new ValuableRef(next.RuntimeId, string.IsNullOrWhiteSpace(next.Name) ? value.Name : next.Name, next.ValueAtEvent ?? value.ValueAtEvent, next.WeightAtEvent ?? value.WeightAtEvent); } private static int ValueFor(Dictionary refs, string runtimeId) { if (!refs.TryGetValue(runtimeId, out ValuableRef value)) { return 0; } return value.ValueAtEvent.GetValueOrDefault(); } private static float WeightFor(Dictionary refs, string runtimeId) { if (!refs.TryGetValue(runtimeId, out ValuableRef value)) { return 0f; } return value.WeightAtEvent.GetValueOrDefault(); } private static string TagValue(RunEvent runEvent, string key, string fallback) { if (!runEvent.Tags.TryGetValue(key, out string value) || string.IsNullOrWhiteSpace(value)) { return fallback; } return value; } } public sealed class ValuableRef { public string RuntimeId { get; } public string Name { get; } public int? ValueAtEvent { get; } public float? WeightAtEvent { get; } public ValuableRef(string runtimeId, string name, int? valueAtEvent = null, float? weightAtEvent = null) { if (string.IsNullOrWhiteSpace(runtimeId)) { throw new ArgumentException("Runtime id is required.", "runtimeId"); } RuntimeId = runtimeId; Name = (string.IsNullOrWhiteSpace(name) ? runtimeId : name); ValueAtEvent = valueAtEvent; WeightAtEvent = weightAtEvent; } public override string ToString() { if (!ValueAtEvent.HasValue) { return Name; } return $"{Name} (${ValueAtEvent.Value})"; } } }