Initial refactor to C# from PS

This commit is contained in:
Dork Normalize 2025-03-26 01:23:06 -07:00
parent 77a3c936fd
commit a08de59a73
15 changed files with 855 additions and 300 deletions

5
.gitignore vendored
View file

@ -360,4 +360,7 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
### Rider ###
.idea/

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,10 @@
using System.Text.RegularExpressions;
namespace AutoTrackR2.LogEventHandlers;
public interface ILogEventHandler
{
Regex Pattern { get; }
void Handle(LogEntry entry);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

101
AutoTrackR2/LogHandler.cs Normal file
View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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()

102
AutoTrackR2/WebHandler.cs Normal file
View file

@ -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"));
}
}