using System.Diagnostics; 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; } } enum GameProcessState { NotRunning, Running, Unknown } public class LogHandler { private string _logPath; private FileStream? _fileStream; private StreamReader? _reader; private Thread? _monitorThread; private CancellationTokenSource? _cancellationTokenSource; private GameProcessState _gameProcessState = GameProcessState.NotRunning; private bool _isMonitoring = false; public bool IsMonitoring => _isMonitoring; // 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(), new JumpDriveStateChangedEvent(), new RequestJumpFailedEvent() ]; public LogHandler(string? logPath) { if (string.IsNullOrEmpty(logPath)) { throw new ArgumentNullException(nameof(logPath), "Log path cannot be null or empty"); } _logPath = logPath; } 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()); StartMonitoring(); } public void StartMonitoring() { if (_isMonitoring) return; _cancellationTokenSource = new CancellationTokenSource(); _monitorThread = new Thread(() => MonitorLog(_cancellationTokenSource.Token)); _monitorThread.Start(); _isMonitoring = true; } public void StopMonitoring() { if (!_isMonitoring) return; _cancellationTokenSource?.Cancel(); _monitorThread?.Join(); _reader?.Close(); _fileStream?.Close(); _isMonitoring = false; } // Parse a single line of the log file and run matching handlers private void HandleLogEntry(string line) { // Console.WriteLine(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 == null || _fileStream == null) { break; } CheckGameProcessState(); List<string> lines = new List<string>(); while (_reader.ReadLine() is { } line) { lines.Add(line); } foreach (var line in lines) { // start new thread to handle log entry var thread = new Thread(() => HandleLogEntry(line)); thread.Start(); // Console.WriteLine(line); } { // Wait for new lines to be written to the log file Thread.Sleep(1000); } } Console.WriteLine("Monitor thread stopped"); } private void CheckGameProcessState() { // Check if the game process is running by window name var process = Process.GetProcesses().FirstOrDefault(p => p.MainWindowTitle == "Star Citizen"); GameProcessState newGameProcessState = process != null ? GameProcessState.Running : GameProcessState.NotRunning; if (newGameProcessState == GameProcessState.Running && _gameProcessState == GameProcessState.NotRunning) { // Game process went from NotRunning to Running, so reload the Game.log file Console.WriteLine("Game process started, reloading log file"); _reader?.Close(); _fileStream?.Close(); _fileStream = new FileStream(_logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); _reader = new StreamReader(_fileStream); } _gameProcessState = newGameProcessState; } }