diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index f11464c..611d625 100644 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -1,34 +1,36 @@ -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 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 GameJoinLoadTimeEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:"; - + private const string GameJoinLoadTimeEntryPattern = ", userid:([0-9]+)"; - 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 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; @@ -41,21 +43,14 @@ namespace Bloxstrap.Integrations 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 ActivityUserId = ""; - public string ActivityMachineAddress = ""; - public bool ActivityMachineUDMUX = false; - public bool ActivityIsTeleport = false; - public string ActivityLaunchData = ""; - public ServerType ActivityServerType = ServerType.Public; + public bool InGame = false; + + public ActivityData Data { get; private set; } = new(); - public List ActivityHistory = new(); + /// + /// Ordered by newest to oldest + /// + public List History = new(); public bool IsDisposed = false; @@ -130,16 +125,6 @@ namespace Bloxstrap.Integrations } } - 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) { @@ -159,27 +144,41 @@ namespace Bloxstrap.Integrations if (entry.Contains(GameLeavingEntry)) OnAppClose?.Invoke(this, new EventArgs()); - if (ActivityUserId == "" && entry.Contains(GameJoinLoadTimeEntry)) - { - Match match = Regex.Match(entry, GameJoinLoadTimeEntryPattern); - - if (match.Groups.Count != 2) - { - App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join load time entry"); - App.Logger.WriteLine(LOG_IDENT, entry); - return; - } - - ActivityUserId = match.Groups[1].Value; - } - if (!ActivityInGame && ActivityPlaceId == 0) + if (!InGame && Data.PlaceId == 0) { // We are not in a game, nor are in the process of joining one + + //gameJoinLoadTime is written to the log file regardless of the server being private or not + if (entry.Contains(GameJoinLoadTimeEntry)) + { + Match match = Regex.Match(entry, GameJoinLoadTimeEntryPattern); + if (match.Groups.Count != 2) + { + App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join load time entry"); + App.Logger.WriteLine(LOG_IDENT, entry); + return; + } + + Data.ActivityUserId = match.Groups[1].Value; + } + 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)) { @@ -192,27 +191,27 @@ namespace Bloxstrap.Integrations 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 @@ -227,80 +226,69 @@ namespace Bloxstrap.Integrations 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; + Data = new(); - ActivityInGame = false; - ActivityPlaceId = 0; - ActivityUniverseId = 0; - ActivityJobId = ""; - ActivityMachineAddress = ""; - ActivityMachineUDMUX = false; - ActivityIsTeleport = false; - ActivityLaunchData = ""; - ActivityServerType = ServerType.Public; - ActivityUserId = ""; - 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)) @@ -378,7 +366,7 @@ namespace Bloxstrap.Integrations return; } - ActivityLaunchData = data; + Data.RPCLaunchData = data; } OnRPCMessage?.Invoke(this, message); @@ -392,13 +380,13 @@ namespace Bloxstrap.Integrations { 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})"; @@ -410,16 +398,16 @@ namespace Bloxstrap.Integrations 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 d66b958..becd447 100644 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Integrations/DiscordRichPresence.cs @@ -6,14 +6,12 @@ namespace Bloxstrap.Integrations { private readonly DiscordRpcClient _rpcClient = new("1005469189907173486"); private readonly ActivityWatcher _activityWatcher; - + private readonly Queue _messageQueue = new(); + private DiscordRPC.RichPresence? _currentPresence; - private DiscordRPC.RichPresence? _currentPresenceCopy; - private Queue _messageQueue = new(); + private DiscordRPC.RichPresence? _originalPresence; private bool _visible = true; - private long _currentUniverseId; - private DateTime? _timeStartedUniverse; public DiscordRichPresence(ActivityWatcher activityWatcher) { @@ -54,7 +52,7 @@ namespace Bloxstrap.Integrations if (message.Command != "SetRichPresence" && message.Command != "SetLaunchData") return; - if (_currentPresence is null || _currentPresenceCopy is null) + if (_currentPresence is null || _originalPresence is null) { App.Logger.WriteLine(LOG_IDENT, "Presence is not set, enqueuing message"); _messageQueue.Enqueue(message); @@ -65,12 +63,7 @@ namespace Bloxstrap.Integrations if (message.Command == "SetLaunchData") { - var buttonQuery = _currentPresence.Buttons.Where(x => x.Label == "Join server"); - - if (!buttonQuery.Any()) - return; - - buttonQuery.First().Url = _activityWatcher.GetActivityDeeplink(); + _currentPresence.Buttons = GetButtons(); } else if (message.Command == "SetRichPresence") { @@ -97,7 +90,7 @@ namespace Bloxstrap.Integrations if (presenceData.Details.Length > 128) App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters"); else if (presenceData.Details == "") - _currentPresence.Details = _currentPresenceCopy.Details; + _currentPresence.Details = _originalPresence.Details; else _currentPresence.Details = presenceData.Details; } @@ -107,7 +100,7 @@ namespace Bloxstrap.Integrations if (presenceData.State.Length > 128) App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters"); else if (presenceData.State == "") - _currentPresence.State = _currentPresenceCopy.State; + _currentPresence.State = _originalPresence.State; else _currentPresence.State = presenceData.State; } @@ -130,8 +123,8 @@ namespace Bloxstrap.Integrations } else if (presenceData.SmallImage.Reset) { - _currentPresence.Assets.SmallImageText = _currentPresenceCopy.Assets.SmallImageText; - _currentPresence.Assets.SmallImageKey = _currentPresenceCopy.Assets.SmallImageKey; + _currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText; + _currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey; } else { @@ -151,8 +144,8 @@ namespace Bloxstrap.Integrations } else if (presenceData.LargeImage.Reset) { - _currentPresence.Assets.LargeImageText = _currentPresenceCopy.Assets.LargeImageText; - _currentPresence.Assets.LargeImageKey = _currentPresenceCopy.Assets.LargeImageKey; + _currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText; + _currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey; } else { @@ -185,11 +178,11 @@ namespace Bloxstrap.Integrations { const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame"; - if (!_activityWatcher.ActivityInGame) + if (!_activityWatcher.InGame) { App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence"); - _currentPresence = _currentPresenceCopy = null; + _currentPresence = _originalPresence = null; _messageQueue.Clear(); UpdatePresence(); @@ -200,46 +193,37 @@ namespace Bloxstrap.Integrations string smallImageText = "Roblox"; string smallImage = "roblox"; - long placeId = _activityWatcher.ActivityPlaceId; - string userId = _activityWatcher.ActivityUserId; - + + var activity = _activityWatcher.Data; + long placeId = activity.PlaceId; + 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 - - long universeId = _activityWatcher.ActivityUniverseId; - // 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!"); + await UniverseDetails.FetchSingle(activity.UniverseId); + activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId); + } + + var universeDetails = activity.UniverseDetails; + + if (universeDetails is null) + { + Frontend.ShowMessageBox(Strings.ActivityTracker_RichPresenceLoadFailed, System.Windows.MessageBoxImage.Warning); return false; } - GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0]; - App.Logger.WriteLine(LOG_IDENT, "Got Universe details"); - - 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 - { - icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl; - App.Logger.WriteLine(LOG_IDENT, $"Got Universe thumbnail as {icon}"); - } + icon = universeDetails.Thumbnail.ImageUrl; if (App.Settings.Prop.AccountShownOnProfile) { - var userPfpResponse = await Http.GetJson>($"https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds={userId}&size=180x180&format=Png&isCircular=false"); //we can remove '-headshot' from the url if we want a full avatar picture + var userPfpResponse = await Http.GetJson>($"https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds={activity.userId}&size=180x180&format=Png&isCircular=false"); //we can remove '-headshot' from the url if we want a full avatar picture if (userPfpResponse is null || !userPfpResponse.Data.Any()) { App.Logger.WriteLine(LOG_IDENT, "Could not get user thumbnail info!"); @@ -250,7 +234,7 @@ namespace Bloxstrap.Integrations App.Logger.WriteLine(LOG_IDENT, $"Got user thumbnail as {smallImage}"); } - var userInfoResponse = await Http.GetJson($"https://users.roblox.com/v1/users/{userId}"); + var userInfoResponse = await Http.GetJson($"https://users.roblox.com/v1/users/{activity.userId}"); if (userInfoResponse is null) { App.Logger.WriteLine(LOG_IDENT, "Could not get user info!"); @@ -262,46 +246,30 @@ namespace Bloxstrap.Integrations } } - - List