Last active
June 27, 2023 21:44
-
-
Save pjmagee/dbdaf311790e28b90f7590f992ce6e88 to your computer and use it in GitHub Desktop.
SWTOR Parser v2 Memory Spans
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public static readonly string Settings = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, System.Environment.SpecialFolderOption.None), "Star Wars - The Old Republic", "CombatLogs"); | |
| public static DirectoryInfo CombatLogsDirectory => new(Settings); | |
| void Main() | |
| { | |
| foreach (var file in EnumerateCombatLogs()) | |
| { | |
| foreach (var line in EnumerateCombatLogItems(file)) | |
| { | |
| if (line.Value is not null) | |
| if (line.Value.IsParry || line.Value.IsDodge || line.Value.IsMiss || line.Value.IsAbsorbed) | |
| new | |
| { | |
| line.Ability, | |
| line.Value, | |
| line.Threat | |
| }.Dump(); | |
| } | |
| } | |
| } | |
| public class CombatLog | |
| { | |
| public FileInfo FileInfo { get; set; } | |
| public CombatLog() | |
| { | |
| } | |
| } | |
| public class GameObject | |
| { | |
| public ReadOnlyMemory<char> Rom { get; } | |
| public GameObject(ReadOnlyMemory<char> rom) | |
| { | |
| Rom = rom; | |
| } | |
| public bool IsNested => Rom.Span.IndexOf('/') > -1; | |
| public virtual string? Name | |
| { | |
| get | |
| { | |
| if (IsNested) return Rom.Span.Slice(Rom.Span.IndexOf('/') + 1, Rom.Span.LastIndexOf('{') - Rom.Span.IndexOf('/') - 1).Trim().ToString(); | |
| if (Rom.Length > 0) return Rom.Span.Slice(0, Rom.Span.IndexOf('{') - 1).Trim().ToString(); | |
| return null; | |
| } | |
| } | |
| public virtual long? Id | |
| { | |
| get | |
| { | |
| if (IsNested) | |
| { | |
| var startIndex = Rom.Span.LastIndexOf('{'); | |
| var endIndex = Rom.Span.LastIndexOf('}'); | |
| return long.Parse(Rom.Span.Slice(startIndex + 1, endIndex - startIndex - 1)); | |
| } | |
| if (Rom.Length > 0) | |
| { | |
| var startIndex = Rom.Span.IndexOf('{'); | |
| var endIndex = Rom.Span.IndexOf('}'); | |
| return long.Parse(Rom.Span.Slice(startIndex + 1, endIndex - startIndex - 1)); | |
| } | |
| return null; | |
| } | |
| } | |
| } | |
| public class Action | |
| { | |
| public ReadOnlyMemory<char> Rom { get; } | |
| public Action(ReadOnlyMemory<char> rom) | |
| { | |
| Rom = rom; | |
| } | |
| public static Action? Parse(ReadOnlyMemory<char> memory) | |
| { | |
| if(memory.Span.IndexOf(':') != -1) | |
| return new Action(memory); | |
| return null; | |
| } | |
| private int Splitter => Rom.Span.IndexOf(':'); | |
| public GameObject Event => new GameObject(Rom.Slice(0, Splitter)); | |
| public GameObject Effect => new GameObject(Rom.Slice(Splitter + 1, Rom.Span.Length - Splitter - 1)); | |
| } | |
| public class Actor | |
| { | |
| public List<ReadOnlyMemory<char>> Roms { get; } | |
| public string? Name | |
| { | |
| get | |
| { | |
| try | |
| { | |
| if (IsPlayer) | |
| return Roms[0].Span.Slice(1, Roms[0].Span.IndexOf('#') - 1).ToString(); | |
| return Roms[0].Span.Slice(0, Roms[0].Span.IndexOf('{')).ToString(); | |
| } | |
| catch | |
| { | |
| return null; | |
| } | |
| } | |
| } | |
| public bool IsEmpty => Roms.Count == 0 || (Roms.Count == 1 && Roms[0].Length == 1 && Roms[0].Span[0] == '='); | |
| public bool IsNpc => !IsEmpty && Roms[0].Span[0] != '@'; | |
| public bool IsPlayer => Roms.Count > 0 && Roms[0].Span.Length > 0 && Roms[0].Span[0] == '@'; | |
| public bool IsCompanion => IsPlayer && Roms[0].Span.IndexOf('/') > 0; | |
| public int? HealthNow | |
| { | |
| get | |
| { | |
| if (Roms.Count == 3) | |
| { | |
| return int.Parse(Roms[2].Slice(1, Roms[2].Span.IndexOf('/') - 1).ToString()); | |
| } | |
| return null; | |
| } | |
| } | |
| public int? HealthMax | |
| { | |
| get | |
| { | |
| if (!IsEmpty) | |
| { | |
| var health = Roms[2].Span; | |
| int maxStart = health.IndexOf('/') + 1; | |
| int maxLength = health.Length - maxStart - 1; | |
| return int.Parse(Roms[2].Slice(maxStart, maxLength).ToString()); | |
| } | |
| return null; | |
| } | |
| } | |
| public (float X, float Y, float Z)? Position | |
| { | |
| get | |
| { | |
| if (!IsEmpty) | |
| { | |
| var coordinates = ExtractCoordinates(Roms[1].Span); | |
| return (coordinates[0], coordinates[1], coordinates[2]); | |
| } | |
| return null; | |
| } | |
| } | |
| public long? Id | |
| { | |
| get | |
| { | |
| try | |
| { | |
| if (IsPlayer) | |
| { | |
| var hash = Roms[0].Span.IndexOf('#'); | |
| var slash = Roms[0].Span.IndexOf('/'); | |
| if (IsCompanion) | |
| return long.Parse(Roms[0].Span.Slice(hash + 1, slash - hash - 1)); | |
| return long.Parse(Roms[0].Span.Slice(hash + 1, Roms[0].Span.Length - 1 - hash)); | |
| } | |
| if (IsNpc) | |
| { | |
| int openIndex = Roms[0].Span.IndexOf('{'); | |
| int closeIndex = Roms[0].Span.IndexOf('}'); | |
| return long.Parse(Roms[0].Span.Slice(openIndex + 1, closeIndex - openIndex - 1)); | |
| } | |
| } | |
| catch | |
| { | |
| } | |
| return null; | |
| } | |
| } | |
| public Actor(ReadOnlyMemory<char> rom) | |
| { | |
| Roms = GetSubSections(rom); | |
| } | |
| public static Actor? Parse(ReadOnlyMemory<char> rom) | |
| { | |
| if (rom.IsEmpty || (rom.Length == 1 && rom.Span[0] == '=')) return null; | |
| return new Actor(rom); | |
| } | |
| } | |
| public class ActorSection | |
| { | |
| public ReadOnlyMemory<char> Section { get; } | |
| public ActorSection(ReadOnlyMemory<char> section) | |
| { | |
| Section = section; | |
| } | |
| } | |
| public class Ability : GameObject | |
| { | |
| public Ability(ReadOnlyMemory<char> rom) : base(rom) | |
| { | |
| } | |
| public static Ability? Parse(ReadOnlyMemory<char> rom) | |
| { | |
| if(rom.Length == 0 || rom.IsEmpty) return null; | |
| return new Ability(rom); | |
| } | |
| } | |
| public class CombatLogItem | |
| { | |
| public ReadOnlyMemory<char> Rom { get; set; } | |
| public List<ReadOnlyMemory<char>> Roms { get; } | |
| public DateTime TimeStamp { get; } | |
| public Actor? Source { get; } | |
| public Actor? Target { get; } | |
| public Ability? Ability { get; } | |
| public Action? Action { get; } | |
| public Value? Value { get; } | |
| public Threat? Threat { get; } | |
| public CombatLogItem(ReadOnlyMemory<char> line) | |
| { | |
| Rom = line; | |
| Roms = GetSections(Rom); | |
| TimeStamp = DateTime.Parse(Roms[0].Span); | |
| Source = Actor.Parse(Roms[1]); | |
| Target = Actor.Parse(Roms[2]); | |
| Ability = Ability.Parse(Roms[3]); | |
| Action = Action.Parse(Roms[4]); | |
| Value = Value.Parse(Rom); | |
| Threat = Threat.Parse(Rom); | |
| } | |
| } | |
| public static class Constants | |
| { | |
| public static ReadOnlyMemory<char> Energy = new("energy".ToCharArray()); | |
| public static ReadOnlyMemory<char> Kinetic = new("kinetic".ToCharArray()); | |
| public static ReadOnlyMemory<char> Internal = new("internal".ToCharArray()); | |
| public static ReadOnlyMemory<char> Elemental = new("elemental".ToCharArray()); | |
| public static ReadOnlyMemory<char> Absorbed = new("absorbed".ToCharArray()); | |
| public static ReadOnlyMemory<char> Critical = new(new[] {'*'}); | |
| public static ReadOnlyMemory<char> Parry = new("-parry".ToArray()); | |
| public static ReadOnlyMemory<char> Miss = new("-miss".ToArray()); | |
| public static ReadOnlyMemory<char> Dodge = new("-dodge".ToArray()); | |
| public static ReadOnlyMemory<char> Tilde = new("~".ToArray()); | |
| public static ReadOnlyMemory<char> HeroEnginePrefix = new("he".ToArray()); | |
| } | |
| public class Value | |
| { | |
| public ReadOnlyMemory<char> Memory { get; } | |
| public Value(ReadOnlyMemory<char> memory) | |
| { | |
| this.Memory = memory; | |
| } | |
| public bool IsCritical => Memory.Span.Contains(Constants.Critical.Span, StringComparison.OrdinalIgnoreCase); | |
| private bool IsEnergy => Memory.Span.Contains(Constants.Energy.Span, StringComparison.OrdinalIgnoreCase); | |
| private bool IsKinetic => Memory.Span.Contains(Constants.Kinetic.Span, StringComparison.OrdinalIgnoreCase); | |
| private bool IsElemental => Memory.Span.Contains(Constants.Elemental.Span, StringComparison.OrdinalIgnoreCase); | |
| private bool IsInternal => Memory.Span.Contains(Constants.Internal.Span, StringComparison.OrdinalIgnoreCase); | |
| public bool IsAbsorbed => Memory.Span.Contains(Constants.Absorbed.Span, StringComparison.OrdinalIgnoreCase); | |
| public bool IsParry => Memory.Span.Contains(Constants.Parry.Span, StringComparison.OrdinalIgnoreCase); | |
| public bool IsMiss => Memory.Span.Contains(Constants.Miss.Span, StringComparison.OrdinalIgnoreCase); | |
| public bool IsDodge => Memory.Span.Contains(Constants.Dodge.Span, StringComparison.OrdinalIgnoreCase); | |
| private bool IsTilde => Memory.Span.Contains(Constants.Tilde.Span, StringComparison.OrdinalIgnoreCase); | |
| public SpecialValue? Special | |
| { | |
| get | |
| { | |
| var value = ExtractTildeValue(Memory); | |
| if(value is null) return null; | |
| if (IsElemental) return new SpecialValue(value.Value, SpecialType.Elemental); | |
| if (IsEnergy) return new SpecialValue(value.Value, SpecialType.Energy); | |
| if (IsInternal) return new SpecialValue(value.Value, SpecialType.Internal); | |
| if (IsKinetic) return new SpecialValue(value.Value, SpecialType.Kinetic); | |
| return null; | |
| } | |
| } | |
| public int? Initial | |
| { | |
| get | |
| { | |
| return ExtractFirstValue(Memory); | |
| } | |
| } | |
| public long? Id | |
| { | |
| get | |
| { | |
| var start = Memory.Span.IndexOf('{'); | |
| var end = Memory.Span.IndexOf('}'); | |
| if (start != -1) | |
| return long.Parse(Memory.Span.Slice(start + 1, end - start - 1)); | |
| return null; | |
| } | |
| } | |
| public static Value? Parse(ReadOnlyMemory<char> memory) | |
| { | |
| var start = memory.Span.LastIndexOf('('); | |
| var end = memory.Span.LastIndexOf(')'); | |
| var exists = start > memory.Span.LastIndexOf(']'); | |
| if (exists) | |
| { | |
| var scope = memory.Slice(start + 1, end - start - 1); | |
| if (!scope.Span.StartsWith(Constants.HeroEnginePrefix.Span, StringComparison.OrdinalIgnoreCase)) | |
| return new Value(scope); | |
| } | |
| return null; | |
| } | |
| public struct SpecialValue | |
| { | |
| public int Value { get; } | |
| public SpecialType Type { get; } | |
| public SpecialValue(int value, SpecialType type) | |
| { | |
| Value = value; | |
| Type = type; | |
| } | |
| } | |
| public enum SpecialType | |
| { | |
| Elemental, | |
| Internal, | |
| Energy, | |
| Kinetic | |
| } | |
| } | |
| public class Threat | |
| { | |
| public ReadOnlyMemory<char> Rom { get; } | |
| public bool IsPositive => Value >= 0; | |
| public bool IsNegative => Value < 0; | |
| public int Value => int.Parse(Rom.Span); | |
| public Threat(ReadOnlyMemory<char> rom) | |
| { | |
| this.Rom = rom; | |
| } | |
| public static Threat? Parse(ReadOnlyMemory<char> rom) | |
| { | |
| var start = rom.Span.LastIndexOf('<'); | |
| var end = rom.Span.LastIndexOf('>'); | |
| var exists = start > rom.Span.LastIndexOf(']'); | |
| if (exists) | |
| { | |
| var scope = rom.Slice(start + 1, end - start - 1); | |
| if (scope.Span[0] != 'v') | |
| return new Threat(scope); | |
| } | |
| return null; | |
| } | |
| } | |
| public static int? ExtractFirstValue(ReadOnlyMemory<char> rom) | |
| { | |
| int? value = null; | |
| int index = 0; | |
| // Ignore any leading whitespace | |
| while (index < rom.Length && char.IsWhiteSpace(rom.Span[index])) | |
| { | |
| index++; | |
| } | |
| // Extract the digits until a non-digit character or '~' is encountered | |
| while (index < rom.Length && (char.IsDigit(rom.Span[index]) || rom.Span[index] == '~')) | |
| { | |
| if (rom.Span[index] == '~') | |
| { | |
| break; // Stop extracting when '~' is encountered | |
| } | |
| value = ((value ?? 0) * 10) + (rom.Span[index] - '0'); | |
| index++; | |
| } | |
| return value; | |
| } | |
| public static int? ExtractTildeValue(ReadOnlyMemory<char> rom) | |
| { | |
| int? value = null; | |
| int index = 0; | |
| // Find the '~' character, if present | |
| while (index < rom.Length && rom.Span[index] != '~') | |
| { | |
| index++; | |
| } | |
| // Ignore any characters until a non-digit character is encountered | |
| while (index < rom.Length && !char.IsDigit(rom.Span[index])) | |
| { | |
| index++; | |
| } | |
| // Extract the digits until a non-digit character is encountered | |
| while (index < rom.Length && char.IsDigit(rom.Span[index])) | |
| { | |
| value = ((value?? 0) * 10) + (rom.Span[index] - '0'); | |
| index++; | |
| } | |
| return value; | |
| } | |
| public static List<float> ExtractCoordinates(ReadOnlySpan<char> span) | |
| { | |
| List<float> coordinates = new List<float>(); | |
| int start = 0; | |
| int end = 0; | |
| // Skip the opening parenthesis | |
| if (span[0] == '(') | |
| { | |
| start = 1; | |
| } | |
| // Exclude the closing parenthesis | |
| if (span[span.Length - 1] == ')') | |
| { | |
| end = span.Length - 1; | |
| } | |
| else | |
| { | |
| end = span.Length; | |
| } | |
| ReadOnlySpan<char> content = span.Slice(start, end - start); | |
| // Split the content by comma | |
| start = 0; | |
| for (int i = 0; i < content.Length; i++) | |
| { | |
| if (content[i] == ',') | |
| { | |
| float number; | |
| if (float.TryParse(content.Slice(start, i - start), NumberStyles.Float, CultureInfo.InvariantCulture, out number)) | |
| { | |
| coordinates.Add(number); | |
| } | |
| start = i + 1; | |
| } | |
| } | |
| // Handle the last number | |
| float lastNumber; | |
| if (float.TryParse(content.Slice(start, content.Length - start), NumberStyles.Float, CultureInfo.InvariantCulture, out lastNumber)) | |
| { | |
| coordinates.Add(lastNumber); | |
| } | |
| return coordinates; | |
| } | |
| public static IEnumerable<CombatLog> EnumerateCombatLogs() | |
| { | |
| foreach (FileInfo file in CombatLogsDirectory.EnumerateFiles("*.txt", new EnumerationOptions() { RecurseSubdirectories = true })) | |
| { | |
| yield return new CombatLog | |
| { | |
| FileInfo = file | |
| }; | |
| } | |
| } | |
| public static List<ReadOnlyMemory<char>> GetSubSections(ReadOnlyMemory<char> section) | |
| { | |
| List<ReadOnlyMemory<char>> subSections = new List<ReadOnlyMemory<char>>(); | |
| int start = 0; | |
| for (int i = 0; i < section.Length; i++) | |
| { | |
| if (section.Span[i] == '|') | |
| { | |
| if (i > start) | |
| { | |
| subSections.Add(new ReadOnlyMemory<char>(section.Slice(start, i - start).ToArray())); | |
| } | |
| start = i + 1; | |
| } | |
| } | |
| if (section.Length > start) | |
| { | |
| subSections.Add(new ReadOnlyMemory<char>(section.Slice(start, section.Length - start).ToArray())); | |
| } | |
| return subSections; | |
| } | |
| public static List<ReadOnlyMemory<char>> GetSections(ReadOnlyMemory<char> span) | |
| { | |
| List<ReadOnlyMemory<char>> sections = new List<ReadOnlyMemory<char>>(); | |
| int start = -1; | |
| int end = -1; | |
| for (int i = 0; i < span.Length; i++) | |
| { | |
| if (span.Span[i] == '[') | |
| { | |
| start = i + 1; | |
| } | |
| else if (span.Span[i] == ']') | |
| { | |
| end = i; | |
| if (start != -1) | |
| { | |
| sections.Add(span.Slice(start, end - start)); | |
| start = -1; | |
| } | |
| } | |
| } | |
| return sections; | |
| } | |
| public static IEnumerable<CombatLogItem> EnumerateCombatLogItems(CombatLog combatLog) | |
| { | |
| using (var stream = combatLog.FileInfo.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) | |
| { | |
| using (var reader = new StreamReader(stream)) | |
| { | |
| while (!reader.EndOfStream) | |
| { | |
| var line = reader.ReadLine(); | |
| if (line is not null) | |
| yield return new CombatLogItem(line.AsMemory()); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment