mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 10:01:27 -07:00
Add integration for notifying server details
might just be the coolest integration yet
This commit is contained in:
parent
bbd2534f94
commit
e72202f6f8
@ -337,6 +337,8 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Logger.WriteLine($"[App::OnStartup] Successfully reached end of main thread. Terminating...");
|
||||||
|
|
||||||
Terminate();
|
Terminate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,7 +333,9 @@ namespace Bloxstrap
|
|||||||
|
|
||||||
Process gameClient = Process.Start(Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"), _launchCommandLine);
|
Process gameClient = Process.Start(Path.Combine(_versionFolder, "RobloxPlayerBeta.exe"), _launchCommandLine);
|
||||||
List<Process> autocloseProcesses = new();
|
List<Process> autocloseProcesses = new();
|
||||||
|
GameActivityWatcher? activityWatcher = null;
|
||||||
DiscordRichPresence? richPresence = null;
|
DiscordRichPresence? richPresence = null;
|
||||||
|
ServerNotifier? serverNotifier = null;
|
||||||
|
|
||||||
App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClient.Id})");
|
App.Logger.WriteLine($"[Bootstrapper::StartRoblox] Started Roblox (PID {gameClient.Id})");
|
||||||
|
|
||||||
@ -366,10 +368,19 @@ namespace Bloxstrap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (App.Settings.Prop.UseDiscordRichPresence || App.Settings.Prop.ShowServerDetails)
|
||||||
|
activityWatcher = new();
|
||||||
|
|
||||||
if (App.Settings.Prop.UseDiscordRichPresence)
|
if (App.Settings.Prop.UseDiscordRichPresence)
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence");
|
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using Discord Rich Presence");
|
||||||
richPresence = new DiscordRichPresence();
|
richPresence = new(activityWatcher!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (App.Settings.Prop.ShowServerDetails)
|
||||||
|
{
|
||||||
|
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Using server details notifier");
|
||||||
|
serverNotifier = new(activityWatcher!);
|
||||||
shouldWait = true;
|
shouldWait = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +413,7 @@ namespace Bloxstrap
|
|||||||
if (!shouldWait)
|
if (!shouldWait)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
richPresence?.MonitorGameActivity();
|
activityWatcher?.StartWatcher();
|
||||||
|
|
||||||
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close");
|
App.Logger.WriteLine("[Bootstrapper::StartRoblox] Waiting for Roblox to close");
|
||||||
await gameClient.WaitForExitAsync();
|
await gameClient.WaitForExitAsync();
|
||||||
|
182
Bloxstrap/Helpers/GameActivityWatcher.cs
Normal file
182
Bloxstrap/Helpers/GameActivityWatcher.cs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
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 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;
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
|
||||||
|
OnGameLeave?.Invoke(this, new EventArgs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
IsDisposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,28 +16,15 @@ namespace Bloxstrap.Integrations
|
|||||||
class DiscordRichPresence : IDisposable
|
class DiscordRichPresence : IDisposable
|
||||||
{
|
{
|
||||||
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
|
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
|
||||||
|
private readonly GameActivityWatcher _activityWatcher;
|
||||||
|
|
||||||
// i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence,
|
public DiscordRichPresence(GameActivityWatcher activityWatcher)
|
||||||
// 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;
|
|
||||||
|
|
||||||
// these are values to use assuming the player isn't currently in a game
|
|
||||||
private bool _activityInGame = false;
|
|
||||||
private long _activityPlaceId = 0;
|
|
||||||
private string _activityJobId = "";
|
|
||||||
private string _activityMachineAddress = "";
|
|
||||||
|
|
||||||
public DiscordRichPresence()
|
|
||||||
{
|
{
|
||||||
|
_activityWatcher = activityWatcher;
|
||||||
|
|
||||||
|
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetPresence());
|
||||||
|
_activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetPresence());
|
||||||
|
|
||||||
_rpcClient.OnReady += (_, e) =>
|
_rpcClient.OnReady += (_, e) =>
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Received ready from user {e.User.Username} ({e.User.ID})");
|
App.Logger.WriteLine($"[DiscordRichPresence::DiscordRichPresence] Received ready from user {e.User.Username} ({e.User.ID})");
|
||||||
|
|
||||||
@ -57,159 +44,20 @@ namespace Bloxstrap.Integrations
|
|||||||
_rpcClient.Initialize();
|
_rpcClient.Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExamineLogEntry(string entry)
|
|
||||||
{
|
|
||||||
// App.Logger.WriteLine(entry);
|
|
||||||
_logEntriesRead += 1;
|
|
||||||
|
|
||||||
// debug stats to ensure that the log reader is working correctly
|
|
||||||
// if more than 5000 log entries have been read, only log per 100 to save on spam
|
|
||||||
if (_logEntriesRead <= 5000 && _logEntriesRead % 50 == 0)
|
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::ExamineLogEntry] Read {_logEntriesRead} log entries");
|
|
||||||
else if (_logEntriesRead % 100 == 0)
|
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::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($"[DiscordRichPresence::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($"[DiscordRichPresence::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($"[DiscordRichPresence::ExamineLogEntry] Failed to assert format for game join UDMUX entry");
|
|
||||||
App.Logger.WriteLine(entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_activityMachineAddress = match.Groups[1].Value;
|
|
||||||
|
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::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($"[DiscordRichPresence::ExamineLogEntry] Failed to assert format for game joined entry");
|
|
||||||
App.Logger.WriteLine(entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::ExamineLogEntry] Joined Game ({_activityPlaceId}/{_activityJobId}/{_activityMachineAddress})");
|
|
||||||
|
|
||||||
_activityInGame = true;
|
|
||||||
await SetPresence();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_activityInGame && _activityPlaceId != 0 && entry.Contains(GameDisconnectedEntry))
|
|
||||||
{
|
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::ExamineLogEntry] Disconnected from Game ({_activityPlaceId}/{_activityJobId}/{_activityMachineAddress})");
|
|
||||||
|
|
||||||
_activityInGame = false;
|
|
||||||
_activityPlaceId = 0;
|
|
||||||
_activityJobId = "";
|
|
||||||
_activityMachineAddress = "";
|
|
||||||
await SetPresence();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void MonitorGameActivity()
|
|
||||||
{
|
|
||||||
// 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("[DiscordRichPresence::MonitorGameActivity] 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($"[DiscordRichPresence::MonitorGameActivity] 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($"[DiscordRichPresence::MonitorGameActivity] 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 (true)
|
|
||||||
{
|
|
||||||
string? log = await sr.ReadLineAsync();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(log))
|
|
||||||
logUpdatedEvent.WaitOne(1000);
|
|
||||||
else
|
|
||||||
await ExamineLogEntry(log);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need to close the event, its going to be finished with when the program closes anyway
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SetPresence()
|
public async Task<bool> SetPresence()
|
||||||
{
|
{
|
||||||
if (!_activityInGame)
|
if (!_activityWatcher.ActivityInGame)
|
||||||
{
|
{
|
||||||
|
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Clearing presence");
|
||||||
_rpcClient.ClearPresence();
|
_rpcClient.ClearPresence();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
string icon = "roblox";
|
string icon = "roblox";
|
||||||
|
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Fetching data for Place ID {_activityPlaceId}");
|
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Setting presence for Place ID {_activityWatcher.ActivityPlaceId}");
|
||||||
|
|
||||||
var universeIdResponse = await Utilities.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{_activityPlaceId}/universe");
|
var universeIdResponse = await Utilities.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{_activityWatcher.ActivityPlaceId}/universe");
|
||||||
if (universeIdResponse is null)
|
if (universeIdResponse is null)
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe ID!");
|
App.Logger.WriteLine($"[DiscordRichPresence::SetPresence] Could not get Universe ID!");
|
||||||
@ -245,7 +93,7 @@ namespace Bloxstrap.Integrations
|
|||||||
new Button
|
new Button
|
||||||
{
|
{
|
||||||
Label = "See Details",
|
Label = "See Details",
|
||||||
Url = $"https://www.roblox.com/games/{_activityPlaceId}"
|
Url = $"https://www.roblox.com/games/{_activityWatcher.ActivityPlaceId}"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -254,7 +102,7 @@ namespace Bloxstrap.Integrations
|
|||||||
buttons.Insert(0, new Button
|
buttons.Insert(0, new Button
|
||||||
{
|
{
|
||||||
Label = "Join",
|
Label = "Join",
|
||||||
Url = $"roblox://experiences/start?placeId={_activityPlaceId}&gameInstanceId={_activityJobId}"
|
Url = $"roblox://experiences/start?placeId={_activityWatcher.ActivityPlaceId}&gameInstanceId={_activityWatcher.ActivityJobId}"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
68
Bloxstrap/Integrations/ServerNotifier.cs
Normal file
68
Bloxstrap/Integrations/ServerNotifier.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
using Bloxstrap.Helpers;
|
||||||
|
using Bloxstrap.Properties;
|
||||||
|
|
||||||
|
namespace Bloxstrap.Integrations
|
||||||
|
{
|
||||||
|
public class ServerNotifier
|
||||||
|
{
|
||||||
|
private readonly GameActivityWatcher _activityWatcher;
|
||||||
|
|
||||||
|
public ServerNotifier(GameActivityWatcher activityWatcher)
|
||||||
|
{
|
||||||
|
_activityWatcher = activityWatcher;
|
||||||
|
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => Notify());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Notify()
|
||||||
|
{
|
||||||
|
string machineAddress = _activityWatcher.ActivityMachineAddress;
|
||||||
|
string message = "";
|
||||||
|
|
||||||
|
App.Logger.WriteLine($"[ServerNotifier::Notify] Getting server information for {machineAddress}");
|
||||||
|
|
||||||
|
// basically nobody has a free public access geolocation api that's accurate,
|
||||||
|
// the ones that do require an api key which isn't suitable for a client-side application like this
|
||||||
|
// so, hopefully this is reliable enough?
|
||||||
|
string locationCity = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{machineAddress}/city");
|
||||||
|
string locationRegion = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{machineAddress}/region");
|
||||||
|
string locationCountry = await App.HttpClient.GetStringAsync($"https://ipinfo.io/{machineAddress}/country");
|
||||||
|
|
||||||
|
locationCity = locationCity.ReplaceLineEndings("");
|
||||||
|
locationRegion = locationRegion.ReplaceLineEndings("");
|
||||||
|
locationCountry = locationCountry.ReplaceLineEndings("");
|
||||||
|
|
||||||
|
if (locationCity == locationRegion)
|
||||||
|
message = $"Location: {locationRegion}, {locationCountry}\n";
|
||||||
|
else
|
||||||
|
message = $"Location: {locationCity}, {locationRegion}, {locationCountry}\n";
|
||||||
|
|
||||||
|
PingReply ping = await new Ping().SendPingAsync(machineAddress);
|
||||||
|
|
||||||
|
// UDMUX protected servers reject ICMP packets and so the ping fails
|
||||||
|
// we could get around this by doing a UDP ping but ehhhhhhhhhhhhh
|
||||||
|
if (ping.Status == IPStatus.Success)
|
||||||
|
message += $"Latency: ~{ping.RoundtripTime}ms";
|
||||||
|
else
|
||||||
|
message += "Latency: N/A (server may be UDMUX protected)";
|
||||||
|
|
||||||
|
App.Logger.WriteLine($"[ServerNotifier::Notify] {message}");
|
||||||
|
|
||||||
|
NotifyIcon notification = new()
|
||||||
|
{
|
||||||
|
Icon = Resources.IconBloxstrap,
|
||||||
|
Text = "Bloxstrap",
|
||||||
|
Visible = true,
|
||||||
|
BalloonTipTitle = "Connected to server",
|
||||||
|
BalloonTipText = message
|
||||||
|
};
|
||||||
|
|
||||||
|
notification.ShowBalloonTip(10);
|
||||||
|
await Task.Delay(10000);
|
||||||
|
notification.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ namespace Bloxstrap.Models
|
|||||||
public bool RFUAutoclose { get; set; } = false;
|
public bool RFUAutoclose { get; set; } = false;
|
||||||
public bool UseReShade { get; set; } = true;
|
public bool UseReShade { get; set; } = true;
|
||||||
public bool UseReShadeExtraviPresets { get; set; } = true;
|
public bool UseReShadeExtraviPresets { get; set; } = true;
|
||||||
// ideally should be List<CustomIntegration> but wpf moment so blehhhhh :P
|
public bool ShowServerDetails { get; set; } = false;
|
||||||
public ObservableCollection<CustomIntegration> CustomIntegrations { get; set; } = new();
|
public ObservableCollection<CustomIntegration> CustomIntegrations { get; set; } = new();
|
||||||
|
|
||||||
// mod preset configuration
|
// mod preset configuration
|
||||||
|
@ -114,6 +114,12 @@ namespace Bloxstrap.ViewModels
|
|||||||
set => App.Settings.Prop.RFUAutoclose = value;
|
set => App.Settings.Prop.RFUAutoclose = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ShowServerDetailsEnabled
|
||||||
|
{
|
||||||
|
get => App.Settings.Prop.ShowServerDetails;
|
||||||
|
set => App.Settings.Prop.ShowServerDetails = value;
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<CustomIntegration> CustomIntegrations
|
public ObservableCollection<CustomIntegration> CustomIntegrations
|
||||||
{
|
{
|
||||||
get => App.Settings.Prop.CustomIntegrations;
|
get => App.Settings.Prop.CustomIntegrations;
|
||||||
|
@ -115,6 +115,17 @@
|
|||||||
</ui:CardControl>
|
</ui:CardControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="Miscellaneous" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" />
|
||||||
|
<ui:CardControl Margin="0,8,0,0" Padding="16,13,16,12" IsEnabled="{Binding IsChecked, ElementName=RbxFpsUnlockerEnabledToggle, Mode=OneWay}">
|
||||||
|
<ui:CardControl.Header>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock FontSize="14" Text="Show server details of current game" />
|
||||||
|
<TextBlock Margin="0,2,0,0" FontSize="12" Text="Be notified of useful server details (e.g. location and ping) whenever you join a game." Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:CardControl.Header>
|
||||||
|
<ui:ToggleSwitch IsChecked="{Binding ShowServerDetailsEnabled, Mode=TwoWay}" />
|
||||||
|
</ui:CardControl>
|
||||||
|
|
||||||
<TextBlock Text="Custom Integrations" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" />
|
<TextBlock Text="Custom Integrations" FontSize="16" FontWeight="Medium" Margin="0,16,0,0" />
|
||||||
<TextBlock Margin="0,4,0,0" Text="Here, you can have other programs launch with Roblox automatically like how rbxfpsunlocker does." TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
<TextBlock Margin="0,4,0,0" Text="Here, you can have other programs launch with Roblox automatically like how rbxfpsunlocker does." TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||||
<Grid Margin="0,8,0,0">
|
<Grid Margin="0,8,0,0">
|
||||||
|
Loading…
Reference in New Issue
Block a user