mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-10 15:25:42 -07:00
186 lines
8.2 KiB
C#
186 lines
8.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Bloxstrap.Helpers
|
|
{
|
|
public class GameActivityWatcher : IDisposable
|
|
{
|
|
// i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence,
|
|
// like checking the ping and region of the current connected server. maybe that's something to add?
|
|
private const string GameJoiningEntry = "[FLog::Output] ! Joining game";
|
|
private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = ";
|
|
private const string GameJoinedEntry = "[FLog::Network] serverId:";
|
|
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
|
|
|
|
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
|
|
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
|
|
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
|
|
|
|
private int _logEntriesRead = 0;
|
|
|
|
public event EventHandler? OnGameJoin;
|
|
public event EventHandler? OnGameLeave;
|
|
|
|
// these are values to use assuming the player isn't currently in a game
|
|
public bool ActivityInGame = false;
|
|
public long ActivityPlaceId = 0;
|
|
public string ActivityJobId = "";
|
|
public string ActivityMachineAddress = "";
|
|
public bool ActivityMachineUDMUX = false;
|
|
|
|
public bool IsDisposed = false;
|
|
|
|
public async void StartWatcher()
|
|
{
|
|
// okay, here's the process:
|
|
//
|
|
// - tail the latest log file from %localappdata%\roblox\logs
|
|
// - check for specific lines to determine player's game activity as shown below:
|
|
//
|
|
// - get the place id, job id and machine address from '! Joining game '{{JOBID}}' place {{PLACEID}} at {{MACHINEADDRESS}}' entry
|
|
// - confirm place join with 'serverId: {{MACHINEADDRESS}}|{{MACHINEPORT}}' entry
|
|
// - check for leaves/disconnects with 'Time to disconnect replication data: {{TIME}}' entry
|
|
//
|
|
// we'll tail the log file continuously, monitoring for any log entries that we need to determine the current game activity
|
|
|
|
string logDirectory = Path.Combine(Directories.LocalAppData, "Roblox\\logs");
|
|
|
|
if (!Directory.Exists(logDirectory))
|
|
return;
|
|
|
|
FileInfo logFileInfo;
|
|
|
|
// we need to make sure we're fetching the absolute latest log file
|
|
// if roblox doesn't start quickly enough, we can wind up fetching the previous log file
|
|
// good rule of thumb is to find a log file that was created in the last 15 seconds or so
|
|
|
|
App.Logger.WriteLine("[GameActivityWatcher::StartWatcher] Opening Roblox log file...");
|
|
|
|
while (true)
|
|
{
|
|
logFileInfo = new DirectoryInfo(logDirectory).GetFiles().OrderByDescending(x => x.CreationTime).First();
|
|
|
|
if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now)
|
|
break;
|
|
|
|
App.Logger.WriteLine($"[GameActivityWatcher::StartWatcher] Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})");
|
|
await Task.Delay(1000);
|
|
}
|
|
|
|
FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
App.Logger.WriteLine($"[GameActivityWatcher::StartWatcher] Opened {logFileInfo.Name}");
|
|
|
|
AutoResetEvent logUpdatedEvent = new(false);
|
|
FileSystemWatcher logWatcher = new()
|
|
{
|
|
Path = logDirectory,
|
|
Filter = Path.GetFileName(logFileInfo.FullName),
|
|
EnableRaisingEvents = true
|
|
};
|
|
logWatcher.Changed += (s, e) => logUpdatedEvent.Set();
|
|
|
|
using StreamReader sr = new(logFileStream);
|
|
|
|
while (!IsDisposed)
|
|
{
|
|
string? log = await sr.ReadLineAsync();
|
|
|
|
if (string.IsNullOrEmpty(log))
|
|
logUpdatedEvent.WaitOne(1000);
|
|
else
|
|
ExamineLogEntry(log);
|
|
}
|
|
}
|
|
|
|
private void ExamineLogEntry(string entry)
|
|
{
|
|
// App.Logger.WriteLine(entry);
|
|
_logEntriesRead += 1;
|
|
|
|
// debug stats to ensure that the log reader is working correctly
|
|
// if more than 1000 log entries have been read, only log per 100 to save on spam
|
|
if (_logEntriesRead <= 1000 && _logEntriesRead % 50 == 0)
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Read {_logEntriesRead} log entries");
|
|
else if (_logEntriesRead % 100 == 0)
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Read {_logEntriesRead} log entries");
|
|
|
|
if (!ActivityInGame && ActivityPlaceId == 0 && entry.Contains(GameJoiningEntry))
|
|
{
|
|
Match match = Regex.Match(entry, GameJoiningEntryPattern);
|
|
|
|
if (match.Groups.Count != 4)
|
|
{
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Failed to assert format for game join entry");
|
|
App.Logger.WriteLine(entry);
|
|
return;
|
|
}
|
|
|
|
ActivityInGame = false;
|
|
ActivityPlaceId = long.Parse(match.Groups[2].Value);
|
|
ActivityJobId = match.Groups[1].Value;
|
|
ActivityMachineAddress = match.Groups[3].Value;
|
|
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
|
|
}
|
|
else if (!ActivityInGame && ActivityPlaceId != 0)
|
|
{
|
|
if (entry.Contains(GameJoiningUDMUXEntry))
|
|
{
|
|
Match match = Regex.Match(entry, GameJoiningUDMUXPattern);
|
|
|
|
if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress)
|
|
{
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Failed to assert format for game join UDMUX entry");
|
|
App.Logger.WriteLine(entry);
|
|
return;
|
|
}
|
|
|
|
ActivityMachineAddress = match.Groups[1].Value;
|
|
ActivityMachineUDMUX = true;
|
|
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
|
|
}
|
|
else if (entry.Contains(GameJoinedEntry))
|
|
{
|
|
Match match = Regex.Match(entry, GameJoinedEntryPattern);
|
|
|
|
if (match.Groups.Count != 2 || match.Groups[1].Value != ActivityMachineAddress)
|
|
{
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Failed to assert format for game joined entry");
|
|
App.Logger.WriteLine(entry);
|
|
return;
|
|
}
|
|
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
|
|
|
|
ActivityInGame = true;
|
|
OnGameJoin?.Invoke(this, new EventArgs());
|
|
}
|
|
}
|
|
else if (ActivityInGame && ActivityPlaceId != 0 && entry.Contains(GameDisconnectedEntry))
|
|
{
|
|
App.Logger.WriteLine($"[GameActivityWatcher::ExamineLogEntry] Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
|
|
|
|
ActivityInGame = false;
|
|
ActivityPlaceId = 0;
|
|
ActivityJobId = "";
|
|
ActivityMachineAddress = "";
|
|
ActivityMachineUDMUX = false;
|
|
|
|
OnGameLeave?.Invoke(this, new EventArgs());
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
IsDisposed = true;
|
|
}
|
|
}
|
|
}
|