diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index 9d013db8..94b68550 100644 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -1,32 +1,34 @@ -using System.Web; - -namespace Bloxstrap.Integrations +namespace Bloxstrap.Integrations { public class ActivityWatcher : 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 GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer"; - private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer"; - private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:"; - 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 GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport"; - private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]"; - private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal"; - - private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; - private const string GameJoiningUniversePattern = @"universeid:([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 const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)"; + private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]"; + private const string GameJoiningEntry = "[FLog::Output] ! Joining game"; + + // these entries are technically volatile! + // they only get printed depending on their configured FLog level, which could change at any time + // while levels being changed is fairly rare, please limit the number of varying number of FLog types you have to use, if possible + + private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer"; + private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer"; + private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:"; + 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 GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport"; + private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal"; + + private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; + private const string GameJoiningPrivateServerPattern = @"""accessCode"":""([0-9a-f\-]{36})"""; + private const string GameJoiningUniversePattern = @"universeid:([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 const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)"; private int _logEntriesRead = 0; private bool _teleportMarker = false; private bool _reservedTeleportMarker = false; - + public event EventHandler? OnLogEntry; public event EventHandler? OnGameJoin; public event EventHandler? OnGameLeave; @@ -39,20 +41,14 @@ public class ActivityWatcher : IDisposable public string LogLocation = null!; - // these are values to use assuming the player isn't currently in a game - // hmm... do i move this to a model? - public DateTime ActivityTimeJoined; - public bool ActivityInGame = false; - public long ActivityPlaceId = 0; - public long ActivityUniverseId = 0; - public string ActivityJobId = ""; - public string ActivityMachineAddress = ""; - public bool ActivityMachineUDMUX = false; - public bool ActivityIsTeleport = false; - public string ActivityLaunchData = ""; - public ServerType ActivityServerType = ServerType.Public; - - public List ActivityHistory = new(); + public bool InGame = false; + + public ActivityData Data { get; private set; } = new(); + + /// + /// Ordered by newest to oldest + /// + public List History = new(); public bool IsDisposed = false; @@ -127,16 +123,6 @@ public async void Start() } } - public string GetActivityDeeplink() - { - string deeplink = $"roblox://experiences/start?placeId={ActivityPlaceId}&gameInstanceId={ActivityJobId}"; - - if (!String.IsNullOrEmpty(ActivityLaunchData)) - deeplink += "&launchData=" + HttpUtility.UrlEncode(ActivityLaunchData); - - return deeplink; - } - // TODO: i need to double check how this handles failed game joins (connection error, invalid permissions, etc) private void ReadLogEntry(string entry) { @@ -156,14 +142,26 @@ private void ReadLogEntry(string entry) if (entry.Contains(GameLeavingEntry)) OnAppClose?.Invoke(this, new EventArgs()); - if (!ActivityInGame && ActivityPlaceId == 0) + if (!InGame && Data.PlaceId == 0) { // We are not in a game, nor are in the process of joining one if (entry.Contains(GameJoiningPrivateServerEntry)) { // we only expect to be joining a private server if we're not already in a game - ActivityServerType = ServerType.Private; + + Data.ServerType = ServerType.Private; + + var match = Regex.Match(entry, GameJoiningPrivateServerPattern); + + if (match.Groups.Count != 2) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join private server entry"); + App.Logger.WriteLine(LOG_IDENT, entry); + return; + } + + Data.AccessCode = match.Groups[1].Value; } else if (entry.Contains(GameJoiningEntry)) { @@ -176,27 +174,27 @@ private void ReadLogEntry(string entry) return; } - ActivityInGame = false; - ActivityPlaceId = long.Parse(match.Groups[2].Value); - ActivityJobId = match.Groups[1].Value; - ActivityMachineAddress = match.Groups[3].Value; + InGame = false; + Data.PlaceId = long.Parse(match.Groups[2].Value); + Data.JobId = match.Groups[1].Value; + Data.MachineAddress = match.Groups[3].Value; if (_teleportMarker) { - ActivityIsTeleport = true; + Data.IsTeleport = true; _teleportMarker = false; } if (_reservedTeleportMarker) { - ActivityServerType = ServerType.Reserved; + Data.ServerType = ServerType.Reserved; _reservedTeleportMarker = false; } - App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({Data.PlaceId}/{Data.JobId}/{Data.MachineAddress})"); } } - else if (!ActivityInGame && ActivityPlaceId != 0) + else if (!InGame && Data.PlaceId != 0) { // We are not confirmed to be in a game, but we are in the process of joining one @@ -211,79 +209,70 @@ private void ReadLogEntry(string entry) return; } - ActivityUniverseId = long.Parse(match.Groups[1].Value); + Data.UniverseId = long.Parse(match.Groups[1].Value); + + if (History.Any()) + { + var lastActivity = History.First(); + + if (lastActivity is not null && Data.UniverseId == lastActivity.UniverseId && Data.IsTeleport) + Data.RootActivity = lastActivity.RootActivity ?? lastActivity; + } } else if (entry.Contains(GameJoiningUDMUXEntry)) { - Match match = Regex.Match(entry, GameJoiningUDMUXPattern); + var match = Regex.Match(entry, GameJoiningUDMUXPattern); - if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress) + if (match.Groups.Count != 3 || match.Groups[2].Value != Data.MachineAddress) { App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join UDMUX entry"); App.Logger.WriteLine(LOG_IDENT, entry); return; } - ActivityMachineAddress = match.Groups[1].Value; - ActivityMachineUDMUX = true; + Data.MachineAddress = match.Groups[1].Value; - App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({Data.PlaceId}/{Data.JobId}/{Data.MachineAddress})"); } else if (entry.Contains(GameJoinedEntry)) { Match match = Regex.Match(entry, GameJoinedEntryPattern); - if (match.Groups.Count != 2 || match.Groups[1].Value != ActivityMachineAddress) + if (match.Groups.Count != 2 || match.Groups[1].Value != Data.MachineAddress) { App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry"); App.Logger.WriteLine(LOG_IDENT, entry); return; } - App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({Data.PlaceId}/{Data.JobId}/{Data.MachineAddress})"); - ActivityInGame = true; - ActivityTimeJoined = DateTime.Now; + InGame = true; + Data.TimeJoined = DateTime.Now; OnGameJoin?.Invoke(this, new EventArgs()); } } - else if (ActivityInGame && ActivityPlaceId != 0) + else if (InGame && Data.PlaceId != 0) { // We are confirmed to be in a game if (entry.Contains(GameDisconnectedEntry)) { - App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({Data.PlaceId}/{Data.JobId}/{Data.MachineAddress})"); - // TODO: should this be including launchdata? - if (ActivityServerType != ServerType.Reserved) - { - ActivityHistory.Insert(0, new ActivityHistoryEntry - { - PlaceId = ActivityPlaceId, - UniverseId = ActivityUniverseId, - JobId = ActivityJobId, - TimeJoined = ActivityTimeJoined, - TimeLeft = DateTime.Now - }); - } + Data.TimeLeft = DateTime.Now; + History.Insert(0, Data); + + InGame = false; - ActivityInGame = false; - ActivityPlaceId = 0; - ActivityUniverseId = 0; - ActivityJobId = ""; - ActivityMachineAddress = ""; - ActivityMachineUDMUX = false; - ActivityIsTeleport = false; - ActivityLaunchData = ""; - ActivityServerType = ServerType.Public; + Data = new(); OnGameLeave?.Invoke(this, new EventArgs()); } else if (entry.Contains(GameTeleportingEntry)) { - App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({Data.PlaceId}/{Data.JobId}/{Data.MachineAddress})"); _teleportMarker = true; } else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry)) @@ -361,7 +350,7 @@ private void ReadLogEntry(string entry) return; } - ActivityLaunchData = data; + Data.RPCLaunchData = data; } OnRPCMessage?.Invoke(this, message); @@ -375,13 +364,13 @@ public async Task GetServerLocation() { const string LOG_IDENT = "ActivityWatcher::GetServerLocation"; - if (GeolocationCache.ContainsKey(ActivityMachineAddress)) - return GeolocationCache[ActivityMachineAddress]; + if (GeolocationCache.ContainsKey(Data.MachineAddress)) + return GeolocationCache[Data.MachineAddress]; try { string location = ""; - var ipInfo = await Http.GetJson($"https://ipinfo.io/{ActivityMachineAddress}/json"); + var ipInfo = await Http.GetJson($"https://ipinfo.io/{Data.MachineAddress}/json"); if (ipInfo is null) return $"? ({Strings.ActivityTracker_LookupFailed})"; @@ -393,16 +382,16 @@ public async Task GetServerLocation() else location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}"; - if (!ActivityInGame) + if (!InGame) return $"? ({Strings.ActivityTracker_LeftGame})"; - GeolocationCache[ActivityMachineAddress] = location; + GeolocationCache[Data.MachineAddress] = location; return location; } catch (Exception ex) { - App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {ActivityMachineAddress}"); + App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {Data.MachineAddress}"); App.Logger.WriteException(LOG_IDENT, ex); return $"? ({Strings.ActivityTracker_LookupFailed})"; diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs index fed19e44..6721f67d 100644 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Integrations/DiscordRichPresence.cs @@ -12,8 +12,6 @@ public class DiscordRichPresence : IDisposable private Queue _messageQueue = new(); private bool _visible = true; - private long _currentUniverseId; - private DateTime? _timeStartedUniverse; public DiscordRichPresence(ActivityWatcher activityWatcher) { @@ -70,7 +68,7 @@ public void ProcessRPCMessage(Message message, bool implicitUpdate = true) if (!buttonQuery.Any()) return; - buttonQuery.First().Url = _activityWatcher.GetActivityDeeplink(); + buttonQuery.First().Url = _activityWatcher.Data.GetInviteDeeplink(); } else if (message.Command == "SetRichPresence") { @@ -185,7 +183,7 @@ public async Task SetCurrentGame() { const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame"; - if (!_activityWatcher.ActivityInGame) + if (!_activityWatcher.InGame) { App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence"); @@ -197,50 +195,42 @@ public async Task SetCurrentGame() } string icon = "roblox"; - long placeId = _activityWatcher.ActivityPlaceId; - App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}"); - - // TODO: move this to its own function under the activity watcher? - // TODO: show error if information cannot be queried instead of silently failing + var activity = _activityWatcher.Data; + long placeId = activity.PlaceId; - long universeId = _activityWatcher.ActivityUniverseId; + App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}"); // preserve time spent playing if we're teleporting between places in the same universe - if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId) - _timeStartedUniverse = DateTime.UtcNow; + var timeStarted = activity.TimeJoined; - _currentUniverseId = universeId; + if (activity.RootActivity is not null) + timeStarted = activity.RootActivity.TimeJoined; - var gameDetailResponse = await Http.GetJson>($"https://games.roblox.com/v1/games?universeIds={universeId}"); - if (gameDetailResponse is null || !gameDetailResponse.Data.Any()) + if (activity.UniverseDetails is null) { - App.Logger.WriteLine(LOG_IDENT, "Could not get Universe info!"); - return false; + await UniverseDetails.FetchSingle(activity.UniverseId); + activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId); } - GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0]; - App.Logger.WriteLine(LOG_IDENT, "Got Universe details"); + var universeDetails = activity.UniverseDetails; - var universeThumbnailResponse = await Http.GetJson>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false"); - if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any()) - { - App.Logger.WriteLine(LOG_IDENT, "Could not get Universe thumbnail info!"); - } - else + if (universeDetails is null) { - icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl; - App.Logger.WriteLine(LOG_IDENT, $"Got Universe thumbnail as {icon}"); + Frontend.ShowMessageBox(Strings.ActivityTracker_RichPresenceLoadFailed, System.Windows.MessageBoxImage.Warning); + return false; } + icon = universeDetails.Thumbnail.ImageUrl; + List