Draft: game history (+ other minor fixes)

This commit is contained in:
pizzaboxer 2024-09-03 01:24:52 +01:00
parent b4a9710177
commit dfcd5b6777
No known key found for this signature in database
GPG Key ID: 59D4A1DBAD0F2BA8
13 changed files with 312 additions and 27 deletions

View File

@ -327,6 +327,7 @@ namespace Bloxstrap
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 @@ namespace Bloxstrap
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}\"");

View File

@ -9,6 +9,7 @@ namespace Bloxstrap.Integrations
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 @@ namespace Bloxstrap.Integrations
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 @@ namespace Bloxstrap.Integrations
// 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 @@ namespace Bloxstrap.Integrations
public string ActivityLaunchData = "";
public ServerType ActivityServerType = ServerType.Public;
public List<ActivityHistoryEntry> ActivityHistory = new();
public bool IsDisposed = false;
public async void Start()
@ -131,6 +137,7 @@ namespace Bloxstrap.Integrations
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 @@ namespace Bloxstrap.Integrations
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 @@ namespace Bloxstrap.Integrations
}
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 @@ namespace Bloxstrap.Integrations
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;

View File

@ -203,15 +203,8 @@ namespace Bloxstrap.Integrations
// 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<UniverseIdResponse>($"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 @@ namespace Bloxstrap.Integrations
buttons.Add(new Button
{
Label = "Join server",
Url = $"roblox://experiences/start?placeId={placeId}&gameInstanceId={_activityWatcher.ActivityJobId}"
Url = _activityWatcher.GetActivityDeeplink()
});
}

View File

@ -215,7 +215,7 @@ namespace Bloxstrap
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();

View File

@ -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);
}
}
}

View File

@ -665,6 +665,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Rejoin.
/// </summary>
public static string ContextMenu_GameHistory_Rejoin {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Rejoin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game history.
/// </summary>
public static string ContextMenu_GameHistory_Title {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox is still launching. A log file will only be available once Roblox launches..
/// </summary>
@ -674,15 +692,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to See server details.
/// </summary>
public static string ContextMenu_SeeServerDetails {
get {
return ResourceManager.GetString("ContextMenu.SeeServerDetails", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy Instance ID.
/// </summary>

View File

@ -268,9 +268,6 @@ Your ReShade configuration files will still be saved, and you can locate them by
<data name="ContextMenu.CopyDeeplinkInvite" xml:space="preserve">
<value>Copy invite deeplink</value>
</data>
<data name="ContextMenu.SeeServerDetails" xml:space="preserve">
<value>See server details</value>
</data>
<data name="ContextMenu.ServerInformation.CopyInstanceId" xml:space="preserve">
<value>Copy Instance ID</value>
</data>
@ -1168,4 +1165,10 @@ Are you sure you want to continue?</value>
<data name="JsonManager.FastFlagsLoadFailed" xml:space="preserve">
<value>Your Fast Flags could not be loaded. They have been reset to the default configuration.</value>
</data>
<data name="ContextMenu.GameHistory.Title" xml:space="preserve">
<value>Game history</value>
</data>
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
<value>Rejoin</value>
</data>
</root>

View File

@ -57,7 +57,19 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="Info28"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_SeeServerDetails}" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Title}" />
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem x:Name="JoinLastServerMenuItem" Visibility="Collapsed" Click="JoinLastServerMenuItem_Click">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="History24"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" />
</Grid>
</MenuItem.Header>
</MenuItem>

View File

@ -80,6 +80,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
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 @@ namespace Bloxstrap.UI.Elements.ContextMenu
_watcher.KillRobloxProcess();
}
private void JoinLastServerMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_activityWatcher is null)
throw new ArgumentNullException(nameof(_activityWatcher));
new ServerHistory(_activityWatcher).ShowDialog();
}
}
}

View File

@ -0,0 +1,89 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.ContextMenu.ServerHistory"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.ContextMenu"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
d:DataContext="{d:DesignInstance Type=models:ServerHistoryViewModel}"
mc:Ignorable="d"
Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}"
MinWidth="420"
MinHeight="420"
Width="580"
Height="420"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" x:Name="RootTitleBar" Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<Border Grid.Row="1">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding ActivityHistory, Mode=OneWay}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Border.Style>
<ui:ProgressRing Grid.Row="1" IsIndeterminate="True" />
</Border>
<ListView Grid.Row="1" ItemsSource="{Binding ActivityHistory, Mode=OneWay}" Margin="8">
<ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<DataTrigger Binding="{Binding ActivityHistory, Mode=OneWay}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Visible" />
</Style>
</ListView.Style>
<ListView.ItemTemplate>
<DataTemplate>
<ui:Card Padding="12">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="84" Height="84" CornerRadius="4">
<Border.Background>
<ImageBrush ImageSource="{Binding GameThumbnail, IsAsync=True}" />
</Border.Background>
</Border>
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{Binding GameName}" FontSize="18" FontWeight="Medium" />
<TextBlock Text="{Binding TimeJoinedFriendly}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:Button Margin="0,8,0,0" Content="{x:Static resources:Strings.ContextMenu_GameHistory_Rejoin}" Command="{Binding RejoinServerCommand}" Appearance="Success" Icon="Play28" IconFilled="True" />
</StackPanel>
</Grid>
</ui:Card>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,21 @@
using Bloxstrap.Integrations;
using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
{
/// <summary>
/// Interaction logic for ServerInformation.xaml
/// </summary>
public partial class ServerHistory
{
public ServerHistory(ActivityWatcher watcher)
{
var viewModel = new ServerHistoryViewModel(watcher);
viewModel.RequestCloseEvent += (_, _) => Close();
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -0,0 +1,60 @@
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Windows.Input;
using Bloxstrap.Integrations;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.UI.ViewModels.ContextMenu
{
internal class ServerHistoryViewModel : NotifyPropertyChangedViewModel
{
private readonly ActivityWatcher _activityWatcher;
public List<ActivityHistoryEntry>? ActivityHistory { get; private set; }
public ICommand CloseWindowCommand => new RelayCommand(RequestClose);
public EventHandler? RequestCloseEvent;
public ServerHistoryViewModel(ActivityWatcher activityWatcher)
{
_activityWatcher = activityWatcher;
_activityWatcher.OnGameLeave += (_, _) => LoadData();
LoadData();
}
private async void LoadData()
{
var entries = _activityWatcher.ActivityHistory.Where(x => !x.DetailsLoaded);
if (entries.Any())
{
string universeIds = String.Join(',', entries.Select(x => x.UniverseId));
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={universeIds}");
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
return;
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeIds}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false");
if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any())
return;
foreach (var entry in entries)
{
entry.GameName = gameDetailResponse.Data.Where(x => x.Id == entry.UniverseId).Select(x => x.Name).First();
entry.GameThumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == entry.UniverseId).Select(x => x.ImageUrl).First();
entry.DetailsLoaded = true;
}
}
ActivityHistory = new(_activityWatcher.ActivityHistory);
OnPropertyChanged(nameof(ActivityHistory));
}
private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty);
}
}

View File

@ -17,7 +17,15 @@ namespace Bloxstrap.Utility
public InterProcessLock(string name, TimeSpan timeout)
{
Mutex = new Mutex(false, "Bloxstrap-" + name);
IsAcquired = Mutex.WaitOne(timeout);
try
{
IsAcquired = Mutex.WaitOne(timeout);
}
catch (AbandonedMutexException)
{
IsAcquired = true;
}
}
public void Dispose()