diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs index a4cf2bae..4ac72170 100644 --- a/Bloxstrap/Bootstrapper.cs +++ b/Bloxstrap/Bootstrapper.cs @@ -327,6 +327,7 @@ private void StartRoblox() int gameClientPid; bool startEventSignalled; + // TODO: figure out why this is causing roblox to block for some users using (var startEvent = new EventWaitHandle(false, EventResetMode.ManualReset, AppData.StartEvent)) { startEvent.Reset(); @@ -387,7 +388,7 @@ private void StartRoblox() if (App.Settings.Prop.EnableActivityTracking || autoclosePids.Any()) { - using var ipl = new InterProcessLock("Watcher"); + using var ipl = new InterProcessLock("Watcher", TimeSpan.FromSeconds(5)); if (ipl.IsAcquired) Process.Start(Paths.Process, $"-watcher \"{args}\""); diff --git a/Bloxstrap/Integrations/ActivityWatcher.cs b/Bloxstrap/Integrations/ActivityWatcher.cs index 36d4bcf0..9d013db8 100644 --- a/Bloxstrap/Integrations/ActivityWatcher.cs +++ b/Bloxstrap/Integrations/ActivityWatcher.cs @@ -9,6 +9,7 @@ public class ActivityWatcher : IDisposable 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:"; @@ -17,6 +18,7 @@ public class ActivityWatcher : IDisposable 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\] (.*)"; @@ -39,8 +41,10 @@ public class ActivityWatcher : IDisposable // 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; @@ -48,6 +52,8 @@ public class ActivityWatcher : IDisposable public string ActivityLaunchData = ""; public ServerType ActivityServerType = ServerType.Public; + public List ActivityHistory = new(); + public bool IsDisposed = false; public async void Start() @@ -131,6 +137,7 @@ public string GetActivityDeeplink() return deeplink; } + // TODO: i need to double check how this handles failed game joins (connection error, invalid permissions, etc) private void ReadLogEntry(string entry) { const string LOG_IDENT = "ActivityWatcher::ReadLogEntry"; @@ -151,6 +158,8 @@ private void ReadLogEntry(string entry) if (!ActivityInGame && ActivityPlaceId == 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 @@ -189,13 +198,28 @@ private void ReadLogEntry(string entry) } else if (!ActivityInGame && ActivityPlaceId != 0) { - if (entry.Contains(GameJoiningUDMUXEntry)) + // We are not confirmed to be in a game, but we are in the process of joining one + + if (entry.Contains(GameJoiningUniverseEntry)) + { + var match = Regex.Match(entry, GameJoiningUniversePattern); + + if (match.Groups.Count != 2) + { + App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join universe entry"); + App.Logger.WriteLine(LOG_IDENT, entry); + return; + } + + ActivityUniverseId = long.Parse(match.Groups[1].Value); + } + else if (entry.Contains(GameJoiningUDMUXEntry)) { Match match = Regex.Match(entry, GameJoiningUDMUXPattern); if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress) { - App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join UDMUX entry"); + App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join UDMUX entry"); App.Logger.WriteLine(LOG_IDENT, entry); return; } @@ -219,17 +243,35 @@ private void ReadLogEntry(string entry) App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); ActivityInGame = true; + ActivityTimeJoined = DateTime.Now; + OnGameJoin?.Invoke(this, new EventArgs()); } } else if (ActivityInGame && ActivityPlaceId != 0) { + // We are confirmed to be in a game + if (entry.Contains(GameDisconnectedEntry)) { App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); + // 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 + }); + } + ActivityInGame = false; ActivityPlaceId = 0; + ActivityUniverseId = 0; ActivityJobId = ""; ActivityMachineAddress = ""; ActivityMachineUDMUX = false; diff --git a/Bloxstrap/Integrations/DiscordRichPresence.cs b/Bloxstrap/Integrations/DiscordRichPresence.cs index 9dfc6249..fed19e44 100644 --- a/Bloxstrap/Integrations/DiscordRichPresence.cs +++ b/Bloxstrap/Integrations/DiscordRichPresence.cs @@ -203,15 +203,8 @@ public async Task SetCurrentGame() // TODO: move this to its own function under the activity watcher? // TODO: show error if information cannot be queried instead of silently failing - var universeIdResponse = await Http.GetJson($"https://apis.roblox.com/universes/v1/places/{placeId}/universe"); - if (universeIdResponse is null) - { - App.Logger.WriteLine(LOG_IDENT, "Could not get Universe ID!"); - return false; - } - long universeId = universeIdResponse.UniverseId; - App.Logger.WriteLine(LOG_IDENT, $"Got Universe ID as {universeId}"); + 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) @@ -247,7 +240,7 @@ public async Task SetCurrentGame() buttons.Add(new Button { Label = "Join server", - Url = $"roblox://experiences/start?placeId={placeId}&gameInstanceId={_activityWatcher.ActivityJobId}" + Url = _activityWatcher.GetActivityDeeplink() }); } diff --git a/Bloxstrap/LaunchHandler.cs b/Bloxstrap/LaunchHandler.cs index a7f54067..0c00a4cb 100644 --- a/Bloxstrap/LaunchHandler.cs +++ b/Bloxstrap/LaunchHandler.cs @@ -215,7 +215,7 @@ public static void LaunchRoblox() App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); if (t.Exception is not null) - App.FinalizeExceptionHandling(t.Exception, false); + App.FinalizeExceptionHandling(t.Exception); } App.Terminate(); diff --git a/Bloxstrap/Models/ActivityHistoryEntry.cs b/Bloxstrap/Models/ActivityHistoryEntry.cs new file mode 100644 index 00000000..1e05640e --- /dev/null +++ b/Bloxstrap/Models/ActivityHistoryEntry.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.Mvvm.Input; +using System.Windows.Input; + +namespace Bloxstrap.Models +{ + public class ActivityHistoryEntry + { + public long UniverseId { get; set; } + + public long PlaceId { get; set; } + + public string JobId { get; set; } = String.Empty; + + public DateTime TimeJoined { get; set; } + + public DateTime TimeLeft { get; set; } + + public string TimeJoinedFriendly => String.Format("{0} - {1}", TimeJoined.ToString("h:mm tt"), TimeLeft.ToString("h:mm tt")); + + public bool DetailsLoaded = false; + + public string GameName { get; set; } = String.Empty; + + public string GameThumbnail { get; set; } = String.Empty; + + public ICommand RejoinServerCommand => new RelayCommand(RejoinServer); + + private void RejoinServer() + { + string playerPath = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe"); + string deeplink = $"roblox://experiences/start?placeId={PlaceId}&gameInstanceId={JobId}"; + + // start RobloxPlayerBeta.exe directly since Roblox can reuse the existing window + // ideally, i'd like to find out how roblox is doing it + Process.Start(playerPath, deeplink); + } + } +} diff --git a/Bloxstrap/Resources/Strings.Designer.cs b/Bloxstrap/Resources/Strings.Designer.cs index 1610dda4..06650509 100644 --- a/Bloxstrap/Resources/Strings.Designer.cs +++ b/Bloxstrap/Resources/Strings.Designer.cs @@ -666,20 +666,29 @@ public static string ContextMenu_CopyDeeplinkInvite { } /// - /// Looks up a localized string similar to Roblox is still launching. A log file will only be available once Roblox launches.. + /// Looks up a localized string similar to Rejoin. /// - public static string ContextMenu_RobloxNotRunning { + public static string ContextMenu_GameHistory_Rejoin { get { - return ResourceManager.GetString("ContextMenu.RobloxNotRunning", resourceCulture); + return ResourceManager.GetString("ContextMenu.GameHistory.Rejoin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Game history. + /// + public static string ContextMenu_GameHistory_Title { + get { + return ResourceManager.GetString("ContextMenu.GameHistory.Title", resourceCulture); } } /// - /// Looks up a localized string similar to See server details. + /// Looks up a localized string similar to Roblox is still launching. A log file will only be available once Roblox launches.. /// - public static string ContextMenu_SeeServerDetails { + public static string ContextMenu_RobloxNotRunning { get { - return ResourceManager.GetString("ContextMenu.SeeServerDetails", resourceCulture); + return ResourceManager.GetString("ContextMenu.RobloxNotRunning", resourceCulture); } } diff --git a/Bloxstrap/Resources/Strings.resx b/Bloxstrap/Resources/Strings.resx index 35350a4c..e536f1f4 100644 --- a/Bloxstrap/Resources/Strings.resx +++ b/Bloxstrap/Resources/Strings.resx @@ -268,9 +268,6 @@ Your ReShade configuration files will still be saved, and you can locate them by Copy invite deeplink - - See server details - Copy Instance ID @@ -1168,4 +1165,10 @@ Are you sure you want to continue? Your Fast Flags could not be loaded. They have been reset to the default configuration. + + Game history + + + Rejoin + \ No newline at end of file diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml index 88a73803..d62976a2 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml @@ -57,7 +57,19 @@ - + + + + + + + + + + + + + diff --git a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs index bd908444..615067ab 100644 --- a/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs +++ b/Bloxstrap/UI/Elements/ContextMenu/MenuContainer.xaml.cs @@ -80,6 +80,7 @@ public void ActivityWatcher_OnGameJoin(object? sender, EventArgs e) public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e) { Dispatcher.Invoke(() => { + JoinLastServerMenuItem.Visibility = Visibility.Visible; InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed; ServerDetailsMenuItem.Visibility = Visibility.Collapsed; @@ -129,5 +130,13 @@ private void CloseRobloxMenuItem_Click(object sender, RoutedEventArgs e) _watcher.KillRobloxProcess(); } + + private void JoinLastServerMenuItem_Click(object sender, RoutedEventArgs e) + { + if (_activityWatcher is null) + throw new ArgumentNullException(nameof(_activityWatcher)); + + new ServerHistory(_activityWatcher).ShowDialog(); + } } } diff --git a/Bloxstrap/UI/Elements/ContextMenu/ServerHistory.xaml b/Bloxstrap/UI/Elements/ContextMenu/ServerHistory.xaml new file mode 100644 index 00000000..c1b57513 --- /dev/null +++ b/Bloxstrap/UI/Elements/ContextMenu/ServerHistory.xaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +