diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index 99a4467..883f760 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -217,20 +217,6 @@ namespace Bloxstrap richPresence.MonitorGameActivity(); shouldWait = true; - - // probably not the most ideal way to do this - //string? placeId = Utilities.GetKeyValue(LaunchCommandLine, "placeId=", '&'); - - //if (placeId is not null) - //{ - // richPresence = new DiscordRichPresence(); - // bool presenceSet = await richPresence.SetPresence(placeId); - - // if (presenceSet) - // shouldWait = true; - // else - // richPresence.Dispose(); - //} } if (!shouldWait) diff --git a/Bloxstrap/Helpers/Integrations/DiscordRichPresence.cs b/Bloxstrap/Helpers/Integrations/DiscordRichPresence.cs index 31cb89f..141864a 100644 --- a/Bloxstrap/Helpers/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Helpers/Integrations/DiscordRichPresence.cs @@ -9,209 +9,186 @@ using DiscordRPC; namespace Bloxstrap.Helpers.Integrations { - class DiscordRichPresence : IDisposable - { - readonly DiscordRpcClient RichPresence = new("1005469189907173486"); + class DiscordRichPresence : IDisposable + { + readonly DiscordRpcClient RichPresence = new("1005469189907173486"); - const string GameJoiningEntry = "[FLog::Output] ! Joining game"; - const string GameJoinedEntry = "[FLog::Network] serverId:"; - const string GameDisconnectedEntry = "[FLog::Network] Client:Disconnect"; + const string GameJoiningEntry = "[FLog::Output] ! Joining game"; + const string GameJoinedEntry = "[FLog::Network] serverId:"; + const string GameDisconnectedEntry = "[FLog::Network] Client:Disconnect"; const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|([0-9]+)"; - // these are values to use assuming the player isn't currently in a game + // these are values to use assuming the player isn't currently in a game bool ActivityInGame = false; - long ActivityPlaceId = 0; - string ActivityJobId = ""; - string ActivityMachineAddress = ""; // we're only really using this to confirm a place join + long ActivityPlaceId = 0; + string ActivityJobId = ""; + string ActivityMachineAddress = ""; // we're only really using this to confirm a place join. todo: maybe this could be used to see server location/ping? - public DiscordRichPresence() - { + public DiscordRichPresence() + { RichPresence.Initialize(); } - private static IEnumerable GetLog() - { - Debug.WriteLine("[DiscordRichPresence] Reading log file..."); + private async Task ExamineLogEntry(string entry) + { + Debug.WriteLine(entry); + + if (entry.Contains(GameJoiningEntry) && !ActivityInGame && ActivityPlaceId == 0) + { + Match match = Regex.Match(entry, GameJoiningEntryPattern); + + if (match.Groups.Count != 4) + return; + + ActivityInGame = false; + ActivityPlaceId = Int64.Parse(match.Groups[2].Value); + ActivityJobId = match.Groups[1].Value; + ActivityMachineAddress = match.Groups[3].Value; + + Debug.WriteLine($"[DiscordRichPresence] Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + } + else if (entry.Contains(GameJoinedEntry) && !ActivityInGame && ActivityPlaceId != 0) + { + Match match = Regex.Match(entry, GameJoinedEntryPattern); + + if (match.Groups.Count != 3 || match.Groups[1].Value != ActivityMachineAddress) + return; + + Debug.WriteLine($"[DiscordRichPresence] Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + + ActivityInGame = true; + await SetPresence(); + } + else if (entry.Contains(GameDisconnectedEntry) && ActivityInGame && ActivityPlaceId != 0) + { + Debug.WriteLine($"[DiscordRichPresence] Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + + ActivityInGame = false; + ActivityPlaceId = 0; + ActivityJobId = ""; + ActivityMachineAddress = ""; + await SetPresence(); + } + + } + + public async void MonitorGameActivity() + { + // okay, here's the process: + // + // - read the latest log file from %localappdata%\roblox\logs approx every 30 sec or so + // - 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 'Client:Disconnect' 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(Program.LocalAppData, "Roblox\\logs"); if (!Directory.Exists(logDirectory)) - return Enumerable.Empty(); + return; FileInfo logFileInfo = new DirectoryInfo(logDirectory).GetFiles().OrderByDescending(f => f.LastWriteTime).First(); - List log = new(); + FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - // we just want to read the last 3500 lines of the log file - // this should typically more than cover the last 30 seconds of logs - // it has to be last 3500 lines (~360KB) because voice chat outputs a loooot of logs :') - - ReverseLineReader rlr = new(() => logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); - log = rlr.Take(3500).ToList(); - - Debug.WriteLine("[DiscordRichPresence] Finished reading log file"); - - return log; - } - - private async Task ExamineLog(List log) - { - Debug.WriteLine("[DiscordRichPresence] Examining log file..."); - - foreach (string entry in log) + AutoResetEvent logUpdatedEvent = new(false); + FileSystemWatcher logWatcher = new() { - if (entry.Contains(GameJoiningEntry) && !ActivityInGame && ActivityPlaceId == 0) + Path = logDirectory, + Filter = Path.GetFileName(logFileInfo.FullName), + EnableRaisingEvents = true + }; + logWatcher.Changed += (s, e) => logUpdatedEvent.Set(); + + using (StreamReader sr = new(logFileStream)) + { + string? log = null; + + while (true) { - Match match = Regex.Match(entry, GameJoiningEntryPattern); + log = await sr.ReadLineAsync(); - if (match.Groups.Count != 4) - continue; - - ActivityInGame = false; - ActivityPlaceId = Int64.Parse(match.Groups[2].Value); - ActivityJobId = match.Groups[1].Value; - ActivityMachineAddress = match.Groups[3].Value; - - Debug.WriteLine($"[DiscordRichPresence] Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); - - // examine log again to check for immediate changes - await Task.Delay(1000); - MonitorGameActivity(false); - break; - } - else if (entry.Contains(GameJoinedEntry) && !ActivityInGame && ActivityPlaceId != 0) - { - Match match = Regex.Match(entry, GameJoinedEntryPattern); - - if (match.Groups.Count != 3 || match.Groups[1].Value != ActivityMachineAddress) - continue; - - Debug.WriteLine($"[DiscordRichPresence] Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); - - ActivityInGame = true; - await SetPresence(); - - // examine log again to check for immediate changes - await Task.Delay(1000); - MonitorGameActivity(false); - break; - } - //else if (entry.Contains(GameDisconnectedEntry) && ActivityInGame && ActivityPlaceId != 0) - else if (entry.Contains(GameDisconnectedEntry)) - { - // for this one, we want to break as soon as we see this entry - // or else it'll match a game join entry and think we're joining again - if (ActivityInGame && ActivityPlaceId != 0) - { - Debug.WriteLine($"[DiscordRichPresence] Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); - - ActivityInGame = false; - ActivityPlaceId = 0; - ActivityJobId = ""; - ActivityMachineAddress = ""; - await SetPresence(); - - // examine log again to check for immediate changes - await Task.Delay(1000); - MonitorGameActivity(false); + if (String.IsNullOrEmpty(log)) + { + logUpdatedEvent.WaitOne(1000); + } + else + { + //Debug.WriteLine(log); + await ExamineLogEntry(log); } - - break; } } - Debug.WriteLine("[DiscordRichPresence] Finished examining log file"); - } - - public async void MonitorGameActivity(bool loop = true) - { - // okay, here's the process: - // - // - read the latest log file from %localappdata%\roblox\logs approx every 30 sec or so - // - 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 'Client:Disconnect' entry - // - // we'll read the log file from bottom-to-top and find which line meets the criteria - // the processes for reading and examining the log files are separated since the log may have to be examined multiple times - - // read log file - List log = GetLog().ToList(); - - // and now let's get the current status from the log - await ExamineLog(log); - - if (!loop) - return; - - await Task.Delay(ActivityInGame ? 30000 : 10000); - MonitorGameActivity(); + // no need to close the event, its going to be finished with when the program closes anyway + // ...rr im too lazy to fix the event still be updating when its closed... lol } public async Task SetPresence() - { - if (!ActivityInGame) - { - RichPresence.ClearPresence(); - return true; - } + { + if (!ActivityInGame) + { + RichPresence.ClearPresence(); + return true; + } - string placeThumbnail = "roblox"; + string placeThumbnail = "roblox"; - var placeInfo = await Utilities.GetJson($"https://economy.roblox.com/v2/assets/{ActivityPlaceId}/details"); + var placeInfo = await Utilities.GetJson($"https://economy.roblox.com/v2/assets/{ActivityPlaceId}/details"); - if (placeInfo is null || placeInfo.Creator is null) - return false; + if (placeInfo is null || placeInfo.Creator is null) + return false; - var thumbnailInfo = await Utilities.GetJson($"https://thumbnails.roblox.com/v1/places/gameicons?placeIds={ActivityPlaceId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false"); + var thumbnailInfo = await Utilities.GetJson($"https://thumbnails.roblox.com/v1/places/gameicons?placeIds={ActivityPlaceId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false"); - if (thumbnailInfo is not null) - placeThumbnail = thumbnailInfo.Data![0].ImageUrl!; + if (thumbnailInfo is not null) + placeThumbnail = thumbnailInfo.Data![0].ImageUrl!; - DiscordRPC.Button[]? buttons = null; + DiscordRPC.Button[]? buttons = null; - if (!Program.Settings.HideRPCButtons) - { - buttons = new DiscordRPC.Button[] - { - new DiscordRPC.Button() - { - Label = "Join", - Url = $"https://www.roblox.com/games/start?placeId={ActivityPlaceId}&gameInstanceId={ActivityJobId}&launchData=%7B%7D" - }, + if (!Program.Settings.HideRPCButtons) + { + buttons = new DiscordRPC.Button[] + { + new DiscordRPC.Button() + { + Label = "Join", + Url = $"https://www.roblox.com/games/start?placeId={ActivityPlaceId}&gameInstanceId={ActivityJobId}&launchData=%7B%7D" + }, - new DiscordRPC.Button() - { - Label = "See Details", - Url = $"https://www.roblox.com/games/{ActivityPlaceId}" - } - }; - } + new DiscordRPC.Button() + { + Label = "See Details", + Url = $"https://www.roblox.com/games/{ActivityPlaceId}" + } + }; + } - RichPresence.SetPresence(new RichPresence() - { - Details = placeInfo.Name, - State = $"by {placeInfo.Creator.Name}", - Timestamps = new Timestamps() { Start = DateTime.UtcNow }, - Buttons = buttons, - Assets = new Assets() - { - LargeImageKey = placeThumbnail, - LargeImageText = placeInfo.Name, - SmallImageKey = "roblox", - SmallImageText = "Roblox" - } - }); + RichPresence.SetPresence(new RichPresence() + { + Details = placeInfo.Name, + State = $"by {placeInfo.Creator.Name}", + Timestamps = new Timestamps() { Start = DateTime.UtcNow }, + Buttons = buttons, + Assets = new Assets() + { + LargeImageKey = placeThumbnail, + LargeImageText = placeInfo.Name, + SmallImageKey = "roblox", + SmallImageText = "Roblox" + } + }); - return true; - } + return true; + } - public void Dispose() - { - RichPresence.Dispose(); - } - } + public void Dispose() + { + RichPresence.Dispose(); + } + } }