From a08de59a73ca3a2a76cde4ed28bfa257fe956afd Mon Sep 17 00:00:00 2001 From: Dork Normalize <nope> Date: Wed, 26 Mar 2025 01:23:06 -0700 Subject: [PATCH] Initial refactor to C# from PS --- .gitignore | 5 +- AutoTrackR2/HomePage.xaml.cs | 585 +++++++++--------- AutoTrackR2/LocalPlayerData.cs | 17 + .../LogEventHandlers/ActorDeathEvent.cs | 59 ++ .../LogEventHandlers/GameVersionEvent.cs | 21 + .../LogEventHandlers/ILogEventHandler.cs | 10 + .../LogEventHandlers/InArenaCommanderEvent.cs | 22 + .../InPersistentUniverseEvent.cs | 22 + .../InstancedInteriorEvent.cs | 77 +++ AutoTrackR2/LogEventHandlers/LoginEvent.cs | 24 + .../VehicleDestructionEvent.cs | 59 ++ AutoTrackR2/LogHandler.cs | 101 +++ AutoTrackR2/TrackREventDispatcher.cs | 49 ++ AutoTrackR2/UpdatePage.xaml.cs | 2 +- AutoTrackR2/WebHandler.cs | 102 +++ 15 files changed, 855 insertions(+), 300 deletions(-) create mode 100644 AutoTrackR2/LocalPlayerData.cs create mode 100644 AutoTrackR2/LogEventHandlers/ActorDeathEvent.cs create mode 100644 AutoTrackR2/LogEventHandlers/GameVersionEvent.cs create mode 100644 AutoTrackR2/LogEventHandlers/ILogEventHandler.cs create mode 100644 AutoTrackR2/LogEventHandlers/InArenaCommanderEvent.cs create mode 100644 AutoTrackR2/LogEventHandlers/InPersistentUniverseEvent.cs create mode 100644 AutoTrackR2/LogEventHandlers/InstancedInteriorEvent.cs create mode 100644 AutoTrackR2/LogEventHandlers/LoginEvent.cs create mode 100644 AutoTrackR2/LogEventHandlers/VehicleDestructionEvent.cs create mode 100644 AutoTrackR2/LogHandler.cs create mode 100644 AutoTrackR2/TrackREventDispatcher.cs create mode 100644 AutoTrackR2/WebHandler.cs diff --git a/.gitignore b/.gitignore index 9491a2f..ec1bbfc 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +### Rider ### +.idea/ \ No newline at end of file diff --git a/AutoTrackR2/HomePage.xaml.cs b/AutoTrackR2/HomePage.xaml.cs index f65af75..73918ca 100644 --- a/AutoTrackR2/HomePage.xaml.cs +++ b/AutoTrackR2/HomePage.xaml.cs @@ -3,309 +3,313 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Effects; -using System.IO; using System.Windows.Documents; using System.Globalization; using System.Windows.Media.Imaging; +using AutoTrackR2.LogEventHandlers; -namespace AutoTrackR2 +namespace AutoTrackR2; + +public struct PlayerData { - public partial class HomePage : UserControl + public string? PFPURL; + public string? UEERecord; + public string? OrgURL; + public string? OrgName; + public string? JoinDate; +} + +public partial class HomePage : UserControl +{ + public HomePage() { - public HomePage() + InitializeComponent(); + + // Get the current month + string currentMonth = DateTime.Now.ToString("MMMM", CultureInfo.InvariantCulture); + + // Set the TextBlock text + KillTallyTitle.Text = $"Kill Tally - {currentMonth}"; + } + + private Process runningProcess; // Field to store the running process + private LogHandler _logHandler; + private bool _UIEventsRegistered = false; + + + // Update Start/Stop button states based on the isRunning flag + public void UpdateButtonState(bool isRunning) + { + var accentColor = (Color)Application.Current.Resources["AccentColor"]; + + if (isRunning) { - InitializeComponent(); + // Set Start button to "Running..." and apply glow effect + StartButton.Content = "Running..."; + StartButton.IsEnabled = false; // Disable Start button + StartButton.Style = (Style)FindResource("DisabledButtonStyle"); - // Get the current month - string currentMonth = DateTime.Now.ToString("MMMM", CultureInfo.InvariantCulture); - - // Set the TextBlock text - KillTallyTitle.Text = $"Kill Tally - {currentMonth}"; - } - - private Process runningProcess; // Field to store the running process - - // Update Start/Stop button states based on the isRunning flag - public void UpdateButtonState(bool isRunning) - { - var accentColor = (Color)Application.Current.Resources["AccentColor"]; - - if (isRunning) + // Add glow effect to the Start button + StartButton.Effect = new DropShadowEffect { - // Set Start button to "Running..." and apply glow effect - StartButton.Content = "Running..."; - StartButton.IsEnabled = false; // Disable Start button - StartButton.Style = (Style)FindResource("DisabledButtonStyle"); + Color = accentColor, + BlurRadius = 30, // Adjust blur radius for desired glow intensity + ShadowDepth = 0, // Set shadow depth to 0 for a pure glow effect + Opacity = 1, // Set opacity for glow visibility + Direction = 0 // Direction doesn't matter for glow + }; - // Add glow effect to the Start button - StartButton.Effect = new DropShadowEffect - { - Color = accentColor, - BlurRadius = 30, // Adjust blur radius for desired glow intensity - ShadowDepth = 0, // Set shadow depth to 0 for a pure glow effect - Opacity = 1, // Set opacity for glow visibility - Direction = 0 // Direction doesn't matter for glow - }; - - StopButton.Style = (Style)FindResource("ButtonStyle"); - StopButton.IsEnabled = true; // Enable Stop button - } - else - { - // Reset Start button back to its original state - StartButton.Content = "Start"; - StartButton.IsEnabled = true; // Enable Start button - - // Remove the glow effect from Start button - StartButton.Effect = null; - - StopButton.Style = (Style)FindResource("DisabledButtonStyle"); - StartButton.Style = (Style)FindResource("ButtonStyle"); - StopButton.IsEnabled = false; // Disable Stop button - } + StopButton.Style = (Style)FindResource("ButtonStyle"); + StopButton.IsEnabled = true; // Enable Stop button } - - public void StartButton_Click(object sender, RoutedEventArgs e) + else { - UpdateButtonState(true); - string scriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "KillTrackR_MainScript.ps1"); - TailFileAsync(scriptPath); + // Reset Start button back to its original state + StartButton.Content = "Start"; + StartButton.IsEnabled = true; // Enable Start button + + // Remove the glow effect from Start button + StartButton.Effect = null; + + StopButton.Style = (Style)FindResource("DisabledButtonStyle"); + StartButton.Style = (Style)FindResource("ButtonStyle"); + StopButton.IsEnabled = false; // Disable Stop button } + + RegisterUIEventHandlers(); + } - private async void TailFileAsync(string scriptPath) - { - await Task.Run(() => + public void StartButton_Click(object sender, RoutedEventArgs e) + { + UpdateButtonState(true); + //string scriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "KillTrackR_MainScript.ps1"); + // TailFileAsync(scriptPath); + + // _logHandler = new LogHandler(@"U:\\StarCitizen\\StarCitizen\\LIVE\\Game.log"); + _logHandler = new LogHandler(ConfigManager.LogFile); + _logHandler.Initialize(); + } + + private void RegisterUIEventHandlers() + { + if (_UIEventsRegistered) + return; + + // Username + TrackREventDispatcher.PlayerLoginEvent += (username) => { + Dispatcher.Invoke(() => { - try - { - ProcessStartInfo psi = new ProcessStartInfo - { - FileName = "powershell.exe", - Arguments = $"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\"", - WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - runningProcess = new Process { StartInfo = psi }; // Store the process in the field - - runningProcess.OutputDataReceived += (s, e) => - { - if (!string.IsNullOrEmpty(e.Data)) - { - Dispatcher.Invoke(() => - { - // Parse and display key-value pairs in the OutputTextBox - if (e.Data.Contains("PlayerName=")) - { - string pilotName = e.Data.Split('=')[1].Trim(); - PilotNameTextBox.Text = pilotName; // Update the Button's Content - AdjustFontSize(PilotNameTextBox); - } - else if (e.Data.Contains("PlayerShip=")) - { - string playerShip = e.Data.Split('=')[1].Trim(); - PlayerShipTextBox.Text = playerShip; - AdjustFontSize(PlayerShipTextBox); - } - else if (e.Data.Contains("GameMode=")) - { - string gameMode = e.Data.Split('=')[1].Trim(); - GameModeTextBox.Text = gameMode; - AdjustFontSize(GameModeTextBox); - } - else if (e.Data.Contains("KillTally=")) - { - string killTally = e.Data.Split('=')[1].Trim(); - KillTallyTextBox.Text = killTally; - AdjustFontSize(KillTallyTextBox); - } - else if (e.Data.Contains("NewKill=")) - { - // Parse the kill data - var killData = e.Data.Split('=')[1].Trim(); // Assume the kill data follows after "NewKill=" - var killParts = killData.Split(','); - - // Fetch the dynamic resource for AltTextColor - var altTextColorBrush = new SolidColorBrush((Color)Application.Current.Resources["AltTextColor"]); - var accentColorBrush = new SolidColorBrush((Color)Application.Current.Resources["AccentColor"]); - - // Fetch the Orbitron FontFamily from resources - var orbitronFontFamily = (FontFamily)Application.Current.Resources["Orbitron"]; - var gemunuFontFamily = (FontFamily)Application.Current.Resources["Gemunu"]; - - // Create a new TextBlock for each kill - var killTextBlock = new TextBlock - { - Margin = new Thickness(0, 10, 0, 10), - Style = (Style)Application.Current.Resources["RoundedTextBlock"], // Apply style for text - FontSize = 14, - FontWeight = FontWeights.Bold, - FontFamily = gemunuFontFamily, - }; - - // Add styled content using Run elements - killTextBlock.Inlines.Add(new Run("Victim Name: ") - { - Foreground = altTextColorBrush, - FontFamily = orbitronFontFamily, - }); - killTextBlock.Inlines.Add(new Run($"{killParts[1]}\n")); - - // Repeat for other lines - killTextBlock.Inlines.Add(new Run("Victim Ship: ") - { - Foreground = altTextColorBrush, - FontFamily = orbitronFontFamily, - }); - killTextBlock.Inlines.Add(new Run($"{killParts[2]}\n")); - - killTextBlock.Inlines.Add(new Run("Victim Org: ") - { - Foreground = altTextColorBrush, - FontFamily = orbitronFontFamily, - }); - killTextBlock.Inlines.Add(new Run($"{killParts[3]}\n")); - - killTextBlock.Inlines.Add(new Run("Join Date: ") - { - Foreground = altTextColorBrush, - FontFamily = orbitronFontFamily, - }); - killTextBlock.Inlines.Add(new Run($"{killParts[4]}\n")); - - killTextBlock.Inlines.Add(new Run("UEE Record: ") - { - Foreground = altTextColorBrush, - FontFamily = orbitronFontFamily, - }); - killTextBlock.Inlines.Add(new Run($"{killParts[5]}\n")); - - killTextBlock.Inlines.Add(new Run("Kill Time: ") - { - Foreground = altTextColorBrush, - FontFamily = orbitronFontFamily, - }); - killTextBlock.Inlines.Add(new Run($"{killParts[6]}")); - - // Create a Border and apply the RoundedTextBlockWithBorder style - var killBorder = new Border - { - Style = (Style)Application.Current.Resources["RoundedTextBlockWithBorder"], // Apply border style - }; - - // Create a Grid to hold the TextBlock and the Image - var killGrid = new Grid - { - Width = 400, // Adjust the width of the Grid - Height = 130, // Adjust the height as needed - }; - - // Define two columns in the Grid: one for the text and one for the image - killGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3, GridUnitType.Star) }); // Text column - killGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) }); // Image column - - // Add the TextBlock to the first column of the Grid - Grid.SetColumn(killTextBlock, 0); - killGrid.Children.Add(killTextBlock); - - // Create the Image for the profile - var profileImage = new Image - { - Source = new BitmapImage(new Uri(killParts[7])), // Assuming the 8th part contains the profile image URL - Width = 90, - Height = 90, - Stretch = Stretch.Fill, // Adjust how the image fits - }; - - // Create a Border around the Image - var imageBorder = new Border - { - BorderBrush = accentColorBrush, // Set the border color - BorderThickness = new Thickness(2), // Set the border thickness - Padding = new Thickness(0), // Optional padding inside the border - CornerRadius = new CornerRadius(5), - Margin = new Thickness(10,18,15,18), - Child = profileImage // Set the Image as the content of the Border - }; - - // Add the Border (with the image inside) to the Grid - Grid.SetColumn(imageBorder, 1); - killGrid.Children.Add(imageBorder); - - // Set the Grid as the child of the Border - killBorder.Child = killGrid; - - // Add the new Border to the StackPanel inside the Border - KillFeedStackPanel.Children.Insert(0, killBorder); - } - - else - { - DebugPanel.AppendText(e.Data + Environment.NewLine); - } - }); - } - }; - - runningProcess.ErrorDataReceived += (s, e) => - { - if (!string.IsNullOrEmpty(e.Data)) - { - Dispatcher.Invoke(() => - { - DebugPanel.AppendText(e.Data + Environment.NewLine); - }); - } - }; - - runningProcess.Start(); - runningProcess.BeginOutputReadLine(); - runningProcess.BeginErrorReadLine(); - - runningProcess.WaitForExit(); - } - catch (Exception ex) - { - Dispatcher.Invoke(() => - { - MessageBox.Show($"Error running script: {ex.Message}"); - }); - } + PilotNameTextBox.Text = username; + AdjustFontSize(PilotNameTextBox); + LocalPlayerData.Username = username; }); - } - - public void StopButton_Click(object sender, RoutedEventArgs e) - { - if (runningProcess != null && !runningProcess.HasExited) + }; + + // Ship + TrackREventDispatcher.InstancedInteriorEvent += (data) => { + if (data.OwnerGEID == LocalPlayerData.Username && data.Ship != null) { - // Kill the running process - runningProcess.Kill(); - runningProcess = null; // Clear the reference to the process + Dispatcher.Invoke(() => + { + PlayerShipTextBox.Text = data.Ship; + AdjustFontSize(PlayerShipTextBox); + LocalPlayerData.PlayerShip = data.Ship; + }); } + }; + + // Game Mode + TrackREventDispatcher.PlayerChangedGameModeEvent += (mode) => { + Dispatcher.Invoke(() => + { + GameModeTextBox.Text = mode.ToString(); + AdjustFontSize(GameModeTextBox); + LocalPlayerData.CurrentGameMode = mode; + }); + }; + + // Game Version + TrackREventDispatcher.GameVersionEvent += (version) => { + LocalPlayerData.GameVersion = version; + }; + + // Actor Death + TrackREventDispatcher.ActorDeathEvent += async (data) => { + if (data.VictimPilot != LocalPlayerData.Username) + { + var playerData = await WebHandler.GetPlayerData(data.VictimPilot); - // Clear the text boxes - System.Threading.Thread.Sleep(200); - PilotNameTextBox.Text = string.Empty; - PlayerShipTextBox.Text = string.Empty; - GameModeTextBox.Text = string.Empty; - KillTallyTextBox.Text = string.Empty; - KillFeedStackPanel.Children.Clear(); - } + if (playerData != null) + { + Dispatcher.Invoke(() => { AddKillToScreen(data, playerData); }); + await WebHandler.SubmitKill(data, playerData); + } + } + }; + + _UIEventsRegistered = true; + } - private void AdjustFontSize(TextBlock textBlock) + private void AddKillToScreen(ActorDeathData deathData, PlayerData? playerData) + { + // Fetch the dynamic resource for AltTextColor + var altTextColorBrush = new SolidColorBrush((Color)Application.Current.Resources["AltTextColor"]); + var accentColorBrush = new SolidColorBrush((Color)Application.Current.Resources["AccentColor"]); + + // Fetch the Orbitron FontFamily from resources + var orbitronFontFamily = (FontFamily)Application.Current.Resources["Orbitron"]; + var gemunuFontFamily = (FontFamily)Application.Current.Resources["Gemunu"]; + + // Create a new TextBlock for each kill + var killTextBlock = new TextBlock { - // Set a starting font size - double fontSize = 14; - double maxWidth = textBlock.Width; + Margin = new Thickness(0, 10, 0, 10), + Style = (Style)Application.Current.Resources["RoundedTextBlock"], // Apply style for text + FontSize = 14, + FontWeight = FontWeights.Bold, + FontFamily = gemunuFontFamily, + }; - if (string.IsNullOrEmpty(textBlock.Text) || double.IsNaN(maxWidth)) - return; + // Add styled content using Run elements + killTextBlock.Inlines.Add(new Run("Victim Name: ") + { + Foreground = altTextColorBrush, + FontFamily = orbitronFontFamily, + }); + killTextBlock.Inlines.Add(new Run($"{deathData.VictimPilot}\n")); - // Measure the rendered width of the text - FormattedText formattedText = new FormattedText( + // Repeat for other lines + killTextBlock.Inlines.Add(new Run("Victim Ship: ") + { + Foreground = altTextColorBrush, + FontFamily = orbitronFontFamily, + }); + killTextBlock.Inlines.Add(new Run($"{deathData.VictimShip}\n")); + + killTextBlock.Inlines.Add(new Run("Victim Org: ") + { + Foreground = altTextColorBrush, + FontFamily = orbitronFontFamily, + }); + killTextBlock.Inlines.Add(new Run($"{playerData?.OrgName}\n")); + + killTextBlock.Inlines.Add(new Run("Join Date: ") + { + Foreground = altTextColorBrush, + FontFamily = orbitronFontFamily, + }); + killTextBlock.Inlines.Add(new Run($"{playerData?.JoinDate}\n")); + + killTextBlock.Inlines.Add(new Run("UEE Record: ") + { + Foreground = altTextColorBrush, + FontFamily = orbitronFontFamily, + }); + + + const string dateFormatString = "dd MMM yyyy HH:mm"; + var currentTime = DateTime.UtcNow.ToString(dateFormatString); + + killTextBlock.Inlines.Add(new Run("Kill Time: ") + { + Foreground = altTextColorBrush, + FontFamily = orbitronFontFamily, + }); + killTextBlock.Inlines.Add(new Run($"{currentTime}")); + + // Create a Border and apply the RoundedTextBlockWithBorder style + var killBorder = new Border + { + Style = (Style)Application.Current.Resources["RoundedTextBlockWithBorder"], // Apply border style + }; + + // Create a Grid to hold the TextBlock and the Image + var killGrid = new Grid + { + Width = 400, // Adjust the width of the Grid + Height = 130, // Adjust the height as needed + }; + + // Define two columns in the Grid: one for the text and one for the image + killGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3, GridUnitType.Star) }); // Text column + killGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) }); // Image column + + // Add the TextBlock to the first column of the Grid + Grid.SetColumn(killTextBlock, 0); + killGrid.Children.Add(killTextBlock); + + // Create the Image for the profile + var profileImage = new Image + { + Source = new BitmapImage(new Uri(playerData?.PFPURL)), // Assuming the 8th part contains the profile image URL + Width = 90, + Height = 90, + Stretch = Stretch.Fill, // Adjust how the image fits + }; + + // Create a Border around the Image + var imageBorder = new Border + { + BorderBrush = accentColorBrush, // Set the border color + BorderThickness = new Thickness(2), // Set the border thickness + Padding = new Thickness(0), // Optional padding inside the border + CornerRadius = new CornerRadius(5), + Margin = new Thickness(10,18,15,18), + Child = profileImage // Set the Image as the content of the Border + }; + + // Add the Border (with the image inside) to the Grid + Grid.SetColumn(imageBorder, 1); + killGrid.Children.Add(imageBorder); + + // Set the Grid as the child of the Border + killBorder.Child = killGrid; + + // Add the new Border to the StackPanel inside the Border + Dispatcher.Invoke(() => + { + KillFeedStackPanel.Children.Insert(0, killBorder); + }); + } + + public void StopButton_Click(object sender, RoutedEventArgs e) + { + _logHandler.Stop(); + + // Clear the text boxes + System.Threading.Thread.Sleep(200); + PilotNameTextBox.Text = string.Empty; + PlayerShipTextBox.Text = string.Empty; + GameModeTextBox.Text = string.Empty; + KillTallyTextBox.Text = string.Empty; + KillFeedStackPanel.Children.Clear(); + } + + private void AdjustFontSize(TextBlock textBlock) + { + // Set a starting font size + double fontSize = 14; + double maxWidth = textBlock.Width; + + if (string.IsNullOrEmpty(textBlock.Text) || double.IsNaN(maxWidth)) + return; + + // Measure the rendered width of the text + FormattedText formattedText = new FormattedText( + textBlock.Text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(textBlock.FontFamily, textBlock.FontStyle, textBlock.FontWeight, textBlock.FontStretch), + fontSize, + textBlock.Foreground, + VisualTreeHelper.GetDpi(this).PixelsPerDip + ); + + // Reduce font size until text fits within the width + while (formattedText.Width > maxWidth && fontSize > 6) + { + fontSize -= 0.5; + formattedText = new FormattedText( textBlock.Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, @@ -314,24 +318,9 @@ namespace AutoTrackR2 textBlock.Foreground, VisualTreeHelper.GetDpi(this).PixelsPerDip ); - - // Reduce font size until text fits within the width - while (formattedText.Width > maxWidth && fontSize > 6) - { - fontSize -= 0.5; - formattedText = new FormattedText( - textBlock.Text, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - new Typeface(textBlock.FontFamily, textBlock.FontStyle, textBlock.FontWeight, textBlock.FontStretch), - fontSize, - textBlock.Foreground, - VisualTreeHelper.GetDpi(this).PixelsPerDip - ); - } - - // Apply the adjusted font size - textBlock.FontSize = fontSize; } + + // Apply the adjusted font size + textBlock.FontSize = fontSize; } } diff --git a/AutoTrackR2/LocalPlayerData.cs b/AutoTrackR2/LocalPlayerData.cs new file mode 100644 index 0000000..668ebcd --- /dev/null +++ b/AutoTrackR2/LocalPlayerData.cs @@ -0,0 +1,17 @@ +namespace AutoTrackR2; + + +public enum GameMode +{ + ArenaCommander, + PersistentUniverse +} + +public static class LocalPlayerData +{ + public static string? Username; + public static string? PlayerShip; + public static string? GameVersion; + public static GameMode CurrentGameMode; + public static string? LastSeenVehicleLocation; +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/ActorDeathEvent.cs b/AutoTrackR2/LogEventHandlers/ActorDeathEvent.cs new file mode 100644 index 0000000..5dee7f3 --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/ActorDeathEvent.cs @@ -0,0 +1,59 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +public struct ActorDeathData +{ + public string VictimPilot; + public string VictimShip; + public string Player; + public string Weapon; + public string Class; + public string DamageType; + public string Timestamp; +} + +public class ActorDeathEvent : ILogEventHandler +{ + public Regex Pattern { get; } + public ActorDeathEvent() + { + Pattern = new Regex(@"<Actor Death> CActor::Kill: '(?<EnemyPilot>[^']+)' \[\d+\] in zone '(?<EnemyShip>[^']+)' killed by '(?<Player>[^']+)' \[[^']+\] using '(?<Weapon>[^']+)' \[Class (?<Class>[^\]]+)\] with damage type '(?<DamageType>[^']+)"); + } + + Regex cleanUpPattern = new Regex(@"^(.+?)_\d+$"); + + public void Handle(LogEntry entry) + { + if (entry.Message is null) return; + + var match = Pattern.Match(entry.Message); + if (!match.Success) return; + + var data = new ActorDeathData { + VictimPilot = match.Groups["EnemyPilot"].Value, + VictimShip = match.Groups["EnemyShip"].Value, + Player = match.Groups["Player"].Value, + Weapon = match.Groups["Weapon"].Value, + Class = match.Groups["Class"].Value, + DamageType = match.Groups["DamageType"].Value, + Timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss") + }; + + if (cleanUpPattern.IsMatch(data.VictimShip)) + { + data.VictimShip = cleanUpPattern.Match(data.VictimShip).Groups[1].Value; + } + + if (cleanUpPattern.IsMatch(data.Weapon)) + { + data.Weapon = cleanUpPattern.Match(data.Weapon).Groups[1].Value; + } + + + TrackREventDispatcher.OnActorDeathEvent(data); + + } + + +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/GameVersionEvent.cs b/AutoTrackR2/LogEventHandlers/GameVersionEvent.cs new file mode 100644 index 0000000..abeca73 --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/GameVersionEvent.cs @@ -0,0 +1,21 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +public class GameVersionEvent : ILogEventHandler +{ + public Regex Pattern { get; } + + public GameVersionEvent() + { + Pattern = new Regex(@"--system-trace-env-id='pub-sc-alpha-(?<GameVersion>\d{3,4}-\d{7})'"); + } + public void Handle(LogEntry entry) + { + if (entry.Message is null) return; + var match = Pattern.Match(entry.Message); + if (!match.Success) return; + + TrackREventDispatcher.OnGameVersionEvent(match.Groups["GameVersion"].Value); + } +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/ILogEventHandler.cs b/AutoTrackR2/LogEventHandlers/ILogEventHandler.cs new file mode 100644 index 0000000..adaf1ba --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/ILogEventHandler.cs @@ -0,0 +1,10 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +public interface ILogEventHandler +{ + Regex Pattern { get; } + void Handle(LogEntry entry); + +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/InArenaCommanderEvent.cs b/AutoTrackR2/LogEventHandlers/InArenaCommanderEvent.cs new file mode 100644 index 0000000..defdd4c --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/InArenaCommanderEvent.cs @@ -0,0 +1,22 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +public class InArenaCommanderEvent : ILogEventHandler +{ + public Regex Pattern { get; } + + public InArenaCommanderEvent() + { + Pattern = new Regex("Requesting Mode Change"); + } + + public void Handle(LogEntry entry) + { + if (entry.Message is null) return; + var match = Pattern.Match(entry.Message); + if (!match.Success) return; + + TrackREventDispatcher.OnPlayerChangedGameModeEvent(GameMode.ArenaCommander); + } +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/InPersistentUniverseEvent.cs b/AutoTrackR2/LogEventHandlers/InPersistentUniverseEvent.cs new file mode 100644 index 0000000..2f7c53e --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/InPersistentUniverseEvent.cs @@ -0,0 +1,22 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +public class InPersistentUniverseEvent : ILogEventHandler +{ + public Regex Pattern { get; } + + public InPersistentUniverseEvent() + { + Pattern = new Regex(@"<\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z> \[Notice\] <ContextEstablisherTaskFinished> establisher=""CReplicationModel"" message=""CET completed"" taskname=""StopLoadingScreen"" state=[^\s()]+\(\d+\) status=""Finished"" runningTime=\d+\.\d+ numRuns=\d+ map=""megamap"" gamerules=""SC_Default"" sessionId=""[a-f0-9\-]+"" \[Team_Network\]\[Network\]\[Replication\]\[Loading\]\[Persistence\]"); + } + + public void Handle(LogEntry entry) + { + if (entry.Message is null) return; + var match = Pattern.Match(entry.Message); + if (!match.Success) return; + + TrackREventDispatcher.OnPlayerChangedGameModeEvent(GameMode.PersistentUniverse); + } +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/InstancedInteriorEvent.cs b/AutoTrackR2/LogEventHandlers/InstancedInteriorEvent.cs new file mode 100644 index 0000000..62ab732 --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/InstancedInteriorEvent.cs @@ -0,0 +1,77 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +public struct InstancedInteriorData +{ + public string Entity; + public string OwnerGEID; + public string ManagerGEID; + public string InstancedInterior; + public string? Ship; +} + +// A ship loadout has been changed +public class InstancedInteriorEvent : ILogEventHandler +{ + public Regex Pattern { get; } + + private Regex _shipManufacturerPattern; + private Regex _cleanUpPattern = new Regex(@"(.+?)_\d+$"); + + private List<string> _shipManufacturers = new List<string> + { + "ORIG", + "CRUS", + "RSI", + "AEGS", + "VNCL", + "DRAK", + "ANVL", + "BANU", + "MISC", + "CNOU", + "XIAN", + "GAMA", + "TMBL", + "ESPR", + "KRIG", + "GRIN", + "XNAA", + "MRAI" + }; + + public InstancedInteriorEvent() + { + Pattern = new Regex(@"\[InstancedInterior\] OnEntityLeaveZone - InstancedInterior \[(?<InstancedInterior>[^\]]+)\] \[\d+\] -> Entity \[(?<Entity>[^\]]+)\] \[\d+\] -- m_openDoors\[\d+\], m_managerGEID\[(?<ManagerGEID>\d+)\], m_ownerGEID\[(?<OwnerGEID>[^\[]+)\]"); + _shipManufacturerPattern = new Regex($"^({string.Join("|", _shipManufacturers)})"); + } + + public void Handle(LogEntry entry) + { + if (entry.Message is null) return; + var match = Pattern.Match(entry.Message); + if (!match.Success) return; + + var data = new InstancedInteriorData { + Entity = match.Groups["Entity"].Value, + OwnerGEID = match.Groups["OwnerGEID"].Value, + ManagerGEID = match.Groups["ManagerGEID"].Value, + InstancedInterior = match.Groups["InstancedInterior"].Value, + }; + + match = _shipManufacturerPattern.Match(data.Entity); + if (match.Success) + { + match = _cleanUpPattern.Match(data.Entity); + if (match.Success) + { + data.Ship = match.Groups[1].Value; + } + } + + TrackREventDispatcher.OnInstancedInteriorEvent(data); + + } + +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/LoginEvent.cs b/AutoTrackR2/LogEventHandlers/LoginEvent.cs new file mode 100644 index 0000000..e6f9cc4 --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/LoginEvent.cs @@ -0,0 +1,24 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +// Local player has logged in +public class LoginEvent : ILogEventHandler +{ + public Regex Pattern { get; } + + public LoginEvent() + { + Pattern = new Regex(@"\[Notice\] <Legacy login response> \[CIG-net\] User Login Success - Handle\[(?<Player>[A-Za-z0-9_-]+)\]"); + } + + public void Handle(LogEntry entry) + { + if (entry.Message is null) return; + + var match = Pattern.Match(entry.Message); + if (!match.Success) return; + + TrackREventDispatcher.OnPlayerLoginEvent(match.Groups["Player"].Value); + } +} \ No newline at end of file diff --git a/AutoTrackR2/LogEventHandlers/VehicleDestructionEvent.cs b/AutoTrackR2/LogEventHandlers/VehicleDestructionEvent.cs new file mode 100644 index 0000000..041859a --- /dev/null +++ b/AutoTrackR2/LogEventHandlers/VehicleDestructionEvent.cs @@ -0,0 +1,59 @@ +using System.Text.RegularExpressions; + +namespace AutoTrackR2.LogEventHandlers; + +public struct VehicleDestructionData +{ + public string Vehicle { get; set; } + public string VehicleZone { get; set; } + public float PosX { get; set; } + public float PosY { get; set; } + public float PosZ { get; set; } + public string Driver { get; set; } + public int DestroyLevelFrom { get; set; } + public int DestroyLevelTo { get; set; } + public string CausedBy { get; set; } + public string DamageType { get; set; } +} + +public class VehicleDestructionEvent : ILogEventHandler +{ + public Regex Pattern { get; } + + public VehicleDestructionEvent() + { + Pattern = new Regex(""" + "<(?<timestamp>[^>]+)> \[Notice\] <Vehicle Destruction> CVehicle::OnAdvanceDestroyLevel: " + + "Vehicle '(?<vehicle>[^']+)' \[\d+\] in zone '(?<vehicle_zone>[^']+)' " + + "\[pos x: (?<pos_x>[-\d\.]+), y: (?<pos_y>[-\d\.]+), z: (?<pos_z>[-\d\.]+) " + + "vel x: [^,]+, y: [^,]+, z: [^\]]+\] driven by '(?<driver>[^']+)' \[\d+\] " + + "advanced from destroy level (?<destroy_level_from>\d+) to (?<destroy_level_to>\d+) " + + "caused by '(?<caused_by>[^']+)' \[\d+\] with '(?<damage_type>[^']+)'" + """); + } + + public void Handle(LogEntry entry) + { + var match = Pattern.Match(entry.Message); + if (!match.Success) + { + return; + } + + var data = new VehicleDestructionData + { + Vehicle = match.Groups["vehicle"].Value, + VehicleZone = match.Groups["vehicle_zone"].Value, + PosX = float.Parse(match.Groups["pos_x"].Value), + PosY = float.Parse(match.Groups["pos_y"].Value), + PosZ = float.Parse(match.Groups["pos_z"].Value), + Driver = match.Groups["driver"].Value, + DestroyLevelFrom = int.Parse(match.Groups["destroy_level_from"].Value), + DestroyLevelTo = int.Parse(match.Groups["destroy_level_to"].Value), + CausedBy = match.Groups["caused_by"].Value, + DamageType = match.Groups["damage_type"].Value, + }; + + TrackREventDispatcher.OnVehicleDestructionEvent(data); + } +} \ No newline at end of file diff --git a/AutoTrackR2/LogHandler.cs b/AutoTrackR2/LogHandler.cs new file mode 100644 index 0000000..0bff5dc --- /dev/null +++ b/AutoTrackR2/LogHandler.cs @@ -0,0 +1,101 @@ +using System.IO; +using System.Text.RegularExpressions; +using AutoTrackR2.LogEventHandlers; + +namespace AutoTrackR2; + + +// Represents a single log entry +// This is the object that will be passed to each handler, mostly for convenience +public class LogEntry +{ + public DateTime Timestamp { get; set; } + public required string? Message { get; set; } + +} +public class LogHandler(string logPath) +{ + private readonly string? _logPath = logPath; + private FileStream? _fileStream; + private StreamReader? _reader; + + private CancellationTokenSource cancellationToken = new CancellationTokenSource(); + Thread? monitorThread; + + // Handlers that should be run on every log entry + // Overlap with _startupEventHandlers is fine + private readonly List<ILogEventHandler> _eventHandlers = [ + new LoginEvent(), + new InstancedInteriorEvent(), + new InArenaCommanderEvent(), + new InPersistentUniverseEvent(), + new GameVersionEvent(), + ]; + + // Initialize the LogHandler and run all startup handlers + public void Initialize() + { + if (!File.Exists(_logPath)) + { + throw new FileNotFoundException("Log file not found", _logPath); + } + + _fileStream = new FileStream(_logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + _reader = new StreamReader(_fileStream); + + while (_reader.ReadLine() is { } line) + { + HandleLogEntry(line); + } + + // Ensures that any deaths already in log aren't sent to the APIs until the monitor thread is running + _eventHandlers.Add(new ActorDeathEvent()); + + monitorThread = new Thread(() => MonitorLog(cancellationToken.Token)); + monitorThread.Start(); + } + + public void Stop() + { + // Stop the monitor thread + cancellationToken?.Cancel(); + _reader?.Close(); + _fileStream?.Close(); + } + + // Parse a single line of the log file and run matching handlers + private void HandleLogEntry(string line) + { + foreach (var handler in _eventHandlers) + { + var match = handler.Pattern.Match(line); + if (!match.Success) continue; + + var entry = new LogEntry + { + Timestamp = DateTime.Now, + Message = line + }; + handler.Handle(entry); + break; + } + } + + private void MonitorLog(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + if (_reader?.ReadLine() is { } line) + { + HandleLogEntry(line); + Console.WriteLine(line); + } + else + { + // Wait for new lines to be written to the log file + Thread.Sleep(1000); + } + } + Console.WriteLine("Monitor thread stopped"); + } +} \ No newline at end of file diff --git a/AutoTrackR2/TrackREventDispatcher.cs b/AutoTrackR2/TrackREventDispatcher.cs new file mode 100644 index 0000000..8b3f12d --- /dev/null +++ b/AutoTrackR2/TrackREventDispatcher.cs @@ -0,0 +1,49 @@ +using AutoTrackR2.LogEventHandlers; + +namespace AutoTrackR2; + +public static class TrackREventDispatcher +{ + // Local Player Login + public static event Action<string>? PlayerLoginEvent; + public static void OnPlayerLoginEvent(string playerName) + { + PlayerLoginEvent?.Invoke(playerName); + } + + // An instanced interior has changed + // Example: Player enters/leaves a ship + public static event Action<InstancedInteriorData>? InstancedInteriorEvent; + public static void OnInstancedInteriorEvent(InstancedInteriorData data) + { + InstancedInteriorEvent?.Invoke(data); + } + + // Player changed GameMode (AC or PU) + public static event Action<GameMode>? PlayerChangedGameModeEvent; + public static void OnPlayerChangedGameModeEvent(GameMode mode) + { + PlayerChangedGameModeEvent?.Invoke(mode); + } + + // Game version has been detected + public static event Action<string>? GameVersionEvent; + public static void OnGameVersionEvent(string value) + { + GameVersionEvent?.Invoke(value); + } + + // Actor has died + public static event Action<ActorDeathData>? ActorDeathEvent; + public static void OnActorDeathEvent(ActorDeathData data) + { + ActorDeathEvent?.Invoke(data); + } + + // Vehicle has been destroyed + public static event Action<VehicleDestructionData>? VehicleDestructionEvent; + public static void OnVehicleDestructionEvent(VehicleDestructionData data) + { + VehicleDestructionEvent?.Invoke(data); + } +} \ No newline at end of file diff --git a/AutoTrackR2/UpdatePage.xaml.cs b/AutoTrackR2/UpdatePage.xaml.cs index 11f4d24..75d9e70 100644 --- a/AutoTrackR2/UpdatePage.xaml.cs +++ b/AutoTrackR2/UpdatePage.xaml.cs @@ -9,7 +9,7 @@ namespace AutoTrackR2 { public partial class UpdatePage : UserControl { - private string currentVersion = "v2.07"; + public static string currentVersion = "v2.08"; private string latestVersion; public UpdatePage() diff --git a/AutoTrackR2/WebHandler.cs b/AutoTrackR2/WebHandler.cs new file mode 100644 index 0000000..73740e3 --- /dev/null +++ b/AutoTrackR2/WebHandler.cs @@ -0,0 +1,102 @@ +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using AutoTrackR2.LogEventHandlers; + +namespace AutoTrackR2; + +public static class WebHandler +{ + class APIKillData + { + public string? victim_ship { get; set; } + public string? victim{ get; set; } + public string? enlisted{ get; set; } + public string? rsi{ get; set; } + public string? weapon{ get; set; } + public string? method{ get; set; } + public string? loadout_ship{ get; set; } + public string? game_version{ get; set; } + public string? gamemode{ get; set; } + public string? trackr_version{ get; set; } + public string? location{ get; set; } + } + + public static async Task<PlayerData?> GetPlayerData(string enemyPilot) + { + var joinDataPattern = new Regex("<span class=\"label\">Enlisted</span>\\s*<strong class=\"value\">([^<]+)</strong>"); + var ueePattern = new Regex("<p class=\"entry citizen-record\">\\s*<span class=\"label\">UEE Citizen Record<\\/span>\\s*<strong class=\"value\">#?(n\\/a|\\d+)<\\/strong>\\s*<\\/p>"); + var orgPattern = new Regex("\\/orgs\\/(?<OrgURL>[A-z0-9]+)\" .*\\>(?<OrgName>.*)<"); + var pfpPattern = new Regex("/media/(.*)\""); + + // Make web request to check player data + var playerData = new PlayerData(); + var httpClient = new HttpClient(); + var response = await httpClient.GetAsync($"https://robertsspaceindustries.com/en/citizens/{enemyPilot}"); + + if (response.StatusCode != HttpStatusCode.OK) + { + return null; + } + + var content = await response.Content.ReadAsStringAsync(); + var joinDataMatch = joinDataPattern.Match(content); + if (joinDataMatch.Success) + { + playerData.JoinDate = joinDataMatch.Groups[1].Value; + } + + var ueeMatch = ueePattern.Match(content); + if (ueeMatch.Success) + { + playerData.UEERecord = ueeMatch.Groups[1].Value; + } + + var orgMatch = orgPattern.Match(content); + if (orgMatch.Success) + { + playerData.OrgName = orgMatch.Groups["OrgName"].Value; + playerData.OrgURL = "https://robertsspaceindustries.com/en/orgs/" + orgMatch.Groups["OrgURL"].Value; + } + + var pfpMatch = pfpPattern.Match(content); + if (pfpMatch.Success) + { + var match = pfpMatch.Groups[1].Value; + if (match.Contains("heap_thumb")) + { + playerData.PFPURL = "https://cdn.robertsspaceindustries.com/static/images/account/avatar_default_big.jpg"; + } + else + { + playerData.PFPURL = "https://robertsspaceindustries.com/media/" + pfpMatch.Groups[1].Value; + } + } + + return playerData; + } + + public static async Task SubmitKill(ActorDeathData deathData, PlayerData? enemyPlayerData) + { + var killData = new APIKillData + { + victim_ship = deathData.VictimShip, + victim = deathData.VictimPilot, + enlisted = enemyPlayerData?.JoinDate, + rsi = enemyPlayerData?.UEERecord, + weapon = deathData.Weapon, + method = deathData.DamageType, + loadout_ship = LocalPlayerData.PlayerShip ?? "Unknown", + game_version = LocalPlayerData.GameVersion ?? "Unknown", + gamemode = LocalPlayerData.CurrentGameMode.ToString() ?? "Unknown", + trackr_version = UpdatePage.currentVersion ?? "Unknown", + location = LocalPlayerData.LastSeenVehicleLocation ?? "Unknown" + }; + + var httpClient = new HttpClient(); + string jsonData = JsonSerializer.Serialize(killData); + await httpClient.PostAsync(ConfigManager.ApiUrl + "/register-kill", new StringContent(jsonData, Encoding.UTF8, "application/json")); + } +} \ No newline at end of file