mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 10:01:27 -07:00
Merge branch 'main' into user-pfp-discord-rpc
This commit is contained in:
commit
abb08f11b6
@ -1,29 +1,31 @@
|
||||
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;
|
||||
@ -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 List<ActivityHistoryEntry> ActivityHistory = new();
|
||||
public ActivityData Data { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Ordered by newest to oldest
|
||||
/// </summary>
|
||||
public List<ActivityData> 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);
|
||||
|
||||
ActivityInGame = false;
|
||||
ActivityPlaceId = 0;
|
||||
ActivityUniverseId = 0;
|
||||
ActivityJobId = "";
|
||||
ActivityMachineAddress = "";
|
||||
ActivityMachineUDMUX = false;
|
||||
ActivityIsTeleport = false;
|
||||
ActivityLaunchData = "";
|
||||
ActivityServerType = ServerType.Public;
|
||||
ActivityUserId = "";
|
||||
InGame = false;
|
||||
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))
|
||||
@ -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<IPInfoResponse>($"https://ipinfo.io/{ActivityMachineAddress}/json");
|
||||
var ipInfo = await Http.GetJson<IPInfoResponse>($"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})";
|
||||
|
@ -6,14 +6,12 @@ namespace Bloxstrap.Integrations
|
||||
{
|
||||
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
|
||||
private readonly ActivityWatcher _activityWatcher;
|
||||
private readonly Queue<Message> _messageQueue = new();
|
||||
|
||||
private DiscordRPC.RichPresence? _currentPresence;
|
||||
private DiscordRPC.RichPresence? _currentPresenceCopy;
|
||||
private Queue<Message> _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 == "<reset>")
|
||||
_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 == "<reset>")
|
||||
_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<ApiArrayResponse<GameDetailResponse>>($"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<ApiArrayResponse<ThumbnailResponse>>($"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<ApiArrayResponse<ThumbnailResponse>>($"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<ApiArrayResponse<ThumbnailResponse>>($"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<UserInfoResponse>($"https://users.roblox.com/v1/users/{userId}");
|
||||
var userInfoResponse = await Http.GetJson<UserInfoResponse>($"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<Button> buttons = new();
|
||||
|
||||
if (!App.Settings.Prop.HideRPCButtons && _activityWatcher.ActivityServerType == ServerType.Public)
|
||||
{
|
||||
buttons.Add(new Button
|
||||
{
|
||||
Label = "Join server",
|
||||
Url = _activityWatcher.GetActivityDeeplink()
|
||||
});
|
||||
}
|
||||
|
||||
buttons.Add(new Button
|
||||
{
|
||||
Label = "See game page",
|
||||
Url = $"https://www.roblox.com/games/{placeId}"
|
||||
});
|
||||
|
||||
if (!_activityWatcher.ActivityInGame || placeId != _activityWatcher.ActivityPlaceId)
|
||||
if (!_activityWatcher.InGame || placeId != activity.PlaceId)
|
||||
{
|
||||
App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed");
|
||||
return false;
|
||||
}
|
||||
|
||||
string status = _activityWatcher.ActivityServerType switch
|
||||
string status = _activityWatcher.Data.ServerType switch
|
||||
{
|
||||
ServerType.Private => "In a private server",
|
||||
ServerType.Reserved => "In a reserved server",
|
||||
_ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""),
|
||||
_ => $"by {universeDetails.Data.Creator.Name}" + (universeDetails.Data.Creator.HasVerifiedBadge ? " ☑️" : ""),
|
||||
};
|
||||
|
||||
if (universeDetails.Name.Length < 2)
|
||||
universeDetails.Name = $"{universeDetails.Name}\x2800\x2800\x2800";
|
||||
string universeName = universeDetails.Data.Name;
|
||||
|
||||
if (universeName.Length < 2)
|
||||
universeName = $"{universeName}\x2800\x2800\x2800";
|
||||
|
||||
_currentPresence = new DiscordRPC.RichPresence
|
||||
{
|
||||
Details = $"Playing {universeDetails.Name}",
|
||||
Details = $"Playing {universeName}",
|
||||
State = status,
|
||||
Timestamps = new Timestamps { Start = _timeStartedUniverse },
|
||||
Buttons = buttons.ToArray(),
|
||||
Timestamps = new Timestamps { Start = timeStarted.ToUniversalTime() },
|
||||
Buttons = GetButtons(),
|
||||
Assets = new Assets
|
||||
{
|
||||
LargeImageKey = icon,
|
||||
@ -312,7 +280,7 @@ namespace Bloxstrap.Integrations
|
||||
};
|
||||
|
||||
// this is used for configuration from BloxstrapRPC
|
||||
_currentPresenceCopy = _currentPresence.Clone();
|
||||
_originalPresence = _currentPresence.Clone();
|
||||
|
||||
if (_messageQueue.Any())
|
||||
{
|
||||
@ -325,6 +293,40 @@ namespace Bloxstrap.Integrations
|
||||
return true;
|
||||
}
|
||||
|
||||
public Button[] GetButtons()
|
||||
{
|
||||
var buttons = new List<Button>();
|
||||
|
||||
var data = _activityWatcher.Data;
|
||||
|
||||
if (!App.Settings.Prop.HideRPCButtons)
|
||||
{
|
||||
bool show = false;
|
||||
|
||||
if (data.ServerType == ServerType.Public)
|
||||
show = true;
|
||||
else if (data.ServerType == ServerType.Reserved && !String.IsNullOrEmpty(data.RPCLaunchData))
|
||||
show = true;
|
||||
|
||||
if (show)
|
||||
{
|
||||
buttons.Add(new Button
|
||||
{
|
||||
Label = "Join server",
|
||||
Url = data.GetInviteDeeplink()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buttons.Add(new Button
|
||||
{
|
||||
Label = "See game page",
|
||||
Url = $"https://www.roblox.com/games/{data.PlaceId}"
|
||||
});
|
||||
|
||||
return buttons.ToArray();
|
||||
}
|
||||
|
||||
public void UpdatePresence()
|
||||
{
|
||||
const string LOG_IDENT = "DiscordRichPresence::UpdatePresence";
|
||||
|
93
Bloxstrap/Models/ActivityData.cs
Normal file
93
Bloxstrap/Models/ActivityData.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System.Web;
|
||||
using System.Windows.Input;
|
||||
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Bloxstrap.Models
|
||||
{
|
||||
public class ActivityData
|
||||
{
|
||||
private long _universeId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// If the current activity stems from an in-universe teleport, then this will be
|
||||
/// set to the activity that corresponds to the initial game join
|
||||
/// </summary>
|
||||
public ActivityData? RootActivity;
|
||||
|
||||
public long UniverseId
|
||||
{
|
||||
get => _universeId;
|
||||
set
|
||||
{
|
||||
_universeId = value;
|
||||
UniverseDetails.LoadFromCache(value);
|
||||
}
|
||||
}
|
||||
|
||||
public long PlaceId { get; set; } = 0;
|
||||
|
||||
public string JobId { get; set; } = String.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// This will be empty unless the server joined is a private server
|
||||
/// </summary>
|
||||
public string AccessCode { get; set; } = String.Empty;
|
||||
|
||||
public string MachineAddress { get; set; } = String.Empty;
|
||||
|
||||
public bool IsTeleport { get; set; } = false;
|
||||
|
||||
public ServerType ServerType { get; set; } = ServerType.Public;
|
||||
|
||||
public DateTime TimeJoined { get; set; }
|
||||
|
||||
public DateTime? TimeLeft { get; set; }
|
||||
|
||||
// everything below here is optional strictly for bloxstraprpc, discord rich presence, or game history
|
||||
|
||||
/// <summary>
|
||||
/// This is intended only for other people to use, i.e. context menu invite link, rich presence joining
|
||||
/// </summary>
|
||||
public string RPCLaunchData { get; set; } = String.Empty;
|
||||
|
||||
public UniverseDetails? UniverseDetails { get; set; }
|
||||
|
||||
public string GameHistoryDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
string desc = String.Format("{0} • {1} - {2}", UniverseDetails?.Data.Creator.Name, TimeJoined.ToString("h:mm tt"), TimeLeft?.ToString("h:mm tt"));
|
||||
|
||||
if (ServerType != ServerType.Public)
|
||||
desc += " • " + ServerType.ToTranslatedString();
|
||||
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand RejoinServerCommand => new RelayCommand(RejoinServer);
|
||||
|
||||
public string GetInviteDeeplink(bool launchData = true)
|
||||
{
|
||||
string deeplink = $"roblox://experiences/start?placeId={PlaceId}";
|
||||
|
||||
if (ServerType == ServerType.Private)
|
||||
deeplink += "&accessCode=" + AccessCode;
|
||||
else
|
||||
deeplink += "&gameInstanceId=" + JobId;
|
||||
|
||||
if (launchData && !String.IsNullOrEmpty(RPCLaunchData))
|
||||
deeplink += "&launchData=" + HttpUtility.UrlEncode(RPCLaunchData);
|
||||
|
||||
return deeplink;
|
||||
}
|
||||
|
||||
private void RejoinServer()
|
||||
{
|
||||
string playerPath = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe");
|
||||
|
||||
Process.Start(playerPath, GetInviteDeeplink(false));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
50
Bloxstrap/Models/UniverseDetails.cs
Normal file
50
Bloxstrap/Models/UniverseDetails.cs
Normal file
@ -0,0 +1,50 @@
|
||||
namespace Bloxstrap.Models
|
||||
{
|
||||
public class UniverseDetails
|
||||
{
|
||||
private static List<UniverseDetails> _cache { get; set; } = new();
|
||||
|
||||
public GameDetailResponse Data { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Returns data for a 128x128 icon
|
||||
/// </summary>
|
||||
public ThumbnailResponse Thumbnail { get; set; } = null!;
|
||||
|
||||
public static UniverseDetails? LoadFromCache(long id)
|
||||
{
|
||||
var cacheQuery = _cache.Where(x => x.Data?.Id == id);
|
||||
|
||||
if (cacheQuery.Any())
|
||||
return cacheQuery.First();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Task<bool> FetchSingle(long id) => FetchBulk(id.ToString());
|
||||
|
||||
public static async Task<bool> FetchBulk(string ids)
|
||||
{
|
||||
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={ids}");
|
||||
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
|
||||
return false;
|
||||
|
||||
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={ids}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false");
|
||||
if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any())
|
||||
return false;
|
||||
|
||||
foreach (string strId in ids.Split(','))
|
||||
{
|
||||
long id = long.Parse(strId);
|
||||
|
||||
_cache.Add(new UniverseDetails
|
||||
{
|
||||
Data = gameDetailResponse.Data.Where(x => x.Id == id).First(),
|
||||
Thumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == id).First(),
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
24
Bloxstrap/Resources/Strings.Designer.cs
generated
24
Bloxstrap/Resources/Strings.Designer.cs
generated
@ -123,6 +123,15 @@ namespace Bloxstrap.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your current game will not show on your Discord presence because an error occurred when loading the game information..
|
||||
/// </summary>
|
||||
public static string ActivityTracker_RichPresenceLoadFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("ActivityTracker.RichPresenceLoadFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website..
|
||||
/// </summary>
|
||||
@ -665,6 +674,15 @@ namespace Bloxstrap.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them..
|
||||
/// </summary>
|
||||
public static string ContextMenu_GameHistory_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("ContextMenu.GameHistory.Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Rejoin.
|
||||
/// </summary>
|
||||
@ -1229,7 +1247,7 @@ namespace Bloxstrap.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Private.
|
||||
/// Looks up a localized string similar to Private server.
|
||||
/// </summary>
|
||||
public static string Enums_ServerType_Private {
|
||||
get {
|
||||
@ -1238,7 +1256,7 @@ namespace Bloxstrap.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Public.
|
||||
/// Looks up a localized string similar to Public server.
|
||||
/// </summary>
|
||||
public static string Enums_ServerType_Public {
|
||||
get {
|
||||
@ -1247,7 +1265,7 @@ namespace Bloxstrap.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Reserved.
|
||||
/// Looks up a localized string similar to Reserved server.
|
||||
/// </summary>
|
||||
public static string Enums_ServerType_Reserved {
|
||||
get {
|
||||
|
@ -398,13 +398,13 @@ If not, then please report this exception through a [GitHub issue]({1}) along wi
|
||||
<value>Direct3D 11</value>
|
||||
</data>
|
||||
<data name="Enums.ServerType.Private" xml:space="preserve">
|
||||
<value>Private</value>
|
||||
<value>Private server</value>
|
||||
</data>
|
||||
<data name="Enums.ServerType.Public" xml:space="preserve">
|
||||
<value>Public</value>
|
||||
<value>Public server</value>
|
||||
</data>
|
||||
<data name="Enums.ServerType.Reserved" xml:space="preserve">
|
||||
<value>Reserved</value>
|
||||
<value>Reserved server</value>
|
||||
</data>
|
||||
<data name="Enums.Theme.Dark" xml:space="preserve">
|
||||
<value>Dark</value>
|
||||
@ -1177,4 +1177,10 @@ Are you sure you want to continue?</value>
|
||||
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
|
||||
<value>Rejoin</value>
|
||||
</data>
|
||||
<data name="ActivityTracker.RichPresenceLoadFailed" xml:space="preserve">
|
||||
<value>Your current game will not show on your Discord presence because an error occurred when loading the game information.</value>
|
||||
</data>
|
||||
<data name="ContextMenu.GameHistory.Description" xml:space="preserve">
|
||||
<value>Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
@ -61,7 +61,7 @@
|
||||
</Grid>
|
||||
</MenuItem.Header>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="JoinLastServerMenuItem" Visibility="Collapsed" Click="JoinLastServerMenuItem_Click">
|
||||
<MenuItem Click="JoinLastServerMenuItem_Click">
|
||||
<MenuItem.Header>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
|
@ -70,7 +70,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
return;
|
||||
|
||||
Dispatcher.Invoke(() => {
|
||||
if (_activityWatcher.ActivityServerType == ServerType.Public)
|
||||
if (_activityWatcher.Data.ServerType == ServerType.Public)
|
||||
InviteDeeplinkMenuItem.Visibility = Visibility.Visible;
|
||||
|
||||
ServerDetailsMenuItem.Visibility = Visibility.Visible;
|
||||
@ -80,7 +80,6 @@ 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;
|
||||
|
||||
@ -105,7 +104,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
||||
|
||||
private void RichPresenceMenuItem_Click(object sender, RoutedEventArgs e) => _watcher.RichPresence?.SetVisibility(((MenuItem)sender).IsChecked);
|
||||
|
||||
private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetDataObject(_activityWatcher?.GetActivityDeeplink());
|
||||
private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetDataObject(_activityWatcher?.Data.GetInviteDeeplink());
|
||||
|
||||
private void ServerDetailsMenuItem_Click(object sender, RoutedEventArgs e) => ShowServerInformationWindow();
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
@ -27,11 +28,13 @@
|
||||
|
||||
<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">
|
||||
<TextBlock Grid.Row="1" Margin="16,8,16,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Description}" TextWrapping="Wrap" />
|
||||
|
||||
<Border Grid.Row="2">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ActivityHistory, Mode=OneWay}" Value="{x:Null}">
|
||||
<DataTrigger Binding="{Binding GameHistory, Mode=OneWay}" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
@ -42,11 +45,11 @@
|
||||
<ui:ProgressRing Grid.Row="1" IsIndeterminate="True" />
|
||||
</Border>
|
||||
|
||||
<ListView Grid.Row="1" ItemsSource="{Binding ActivityHistory, Mode=OneWay}" Margin="8">
|
||||
<ListView Grid.Row="2" ItemsSource="{Binding GameHistory, 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}">
|
||||
<DataTrigger Binding="{Binding GameHistory, Mode=OneWay}" Value="{x:Null}">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
@ -65,14 +68,15 @@
|
||||
|
||||
<Border Grid.Column="0" Width="84" Height="84" CornerRadius="4">
|
||||
<Border.Background>
|
||||
<ImageBrush ImageSource="{Binding GameThumbnail, IsAsync=True}" />
|
||||
<ImageBrush ImageSource="{Binding UniverseDetails.Thumbnail.ImageUrl, 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" />
|
||||
<TextBlock Text="{Binding UniverseDetails.Data.Name}" FontSize="18" FontWeight="Medium" />
|
||||
<TextBlock Text="{Binding GameHistoryDescription}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
<ui:Button Margin="0,8,0,0" Content="{x:Static resources:Strings.ContextMenu_GameHistory_Rejoin}" Command="{Binding RejoinServerCommand}"
|
||||
Appearance="Success" Foreground="White" IconForeground="White" Icon="Play28" IconFilled="True" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ui:Card>
|
||||
@ -80,7 +84,7 @@
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
|
||||
<Border Grid.Row="3" 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>
|
||||
|
@ -60,7 +60,7 @@ namespace Bloxstrap.UI
|
||||
return;
|
||||
|
||||
string serverLocation = await _activityWatcher.GetServerLocation();
|
||||
string title = _activityWatcher.ActivityServerType switch
|
||||
string title = _activityWatcher.Data.ServerType switch
|
||||
{
|
||||
ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public,
|
||||
ServerType.Private => Strings.ContextMenu_ServerInformation_Notification_Title_Private,
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Input;
|
||||
using Bloxstrap.Integrations;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
@ -10,7 +8,7 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
||||
{
|
||||
private readonly ActivityWatcher _activityWatcher;
|
||||
|
||||
public List<ActivityHistoryEntry>? ActivityHistory { get; private set; }
|
||||
public List<ActivityData>? GameHistory { get; private set; }
|
||||
|
||||
public ICommand CloseWindowCommand => new RelayCommand(RequestClose);
|
||||
|
||||
@ -27,32 +25,45 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
||||
|
||||
private async void LoadData()
|
||||
{
|
||||
var entries = _activityWatcher.ActivityHistory.Where(x => !x.DetailsLoaded);
|
||||
var entries = _activityWatcher.History.Where(x => x.UniverseDetails is null);
|
||||
|
||||
if (entries.Any())
|
||||
{
|
||||
// TODO: this will duplicate universe ids
|
||||
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())
|
||||
if (!await UniverseDetails.FetchBulk(universeIds))
|
||||
return;
|
||||
|
||||
foreach (var entry in entries)
|
||||
entry.UniverseDetails = UniverseDetails.LoadFromCache(entry.UniverseId);
|
||||
}
|
||||
|
||||
GameHistory = new(_activityWatcher.History);
|
||||
|
||||
var consolidatedJobIds = new List<ActivityData>();
|
||||
|
||||
// consolidate activity entries from in-universe teleports
|
||||
// the time left of the latest activity gets moved to the root activity
|
||||
// the job id of the latest public server activity gets moved to the root activity
|
||||
foreach (var entry in _activityWatcher.History)
|
||||
{
|
||||
if (entry.RootActivity is not null)
|
||||
{
|
||||
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;
|
||||
if (entry.RootActivity.TimeLeft < entry.TimeLeft)
|
||||
entry.RootActivity.TimeLeft = entry.TimeLeft;
|
||||
|
||||
if (entry.ServerType == ServerType.Public && !consolidatedJobIds.Contains(entry))
|
||||
{
|
||||
entry.RootActivity.JobId = entry.JobId;
|
||||
consolidatedJobIds.Add(entry);
|
||||
}
|
||||
|
||||
GameHistory.Remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
ActivityHistory = new(_activityWatcher.ActivityHistory);
|
||||
OnPropertyChanged(nameof(ActivityHistory));
|
||||
OnPropertyChanged(nameof(GameHistory));
|
||||
}
|
||||
|
||||
private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty);
|
||||
|
@ -9,9 +9,9 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
||||
{
|
||||
private readonly ActivityWatcher _activityWatcher;
|
||||
|
||||
public string InstanceId => _activityWatcher.ActivityJobId;
|
||||
public string InstanceId => _activityWatcher.Data.JobId;
|
||||
|
||||
public string ServerType => Strings.ResourceManager.GetStringSafe($"Enums.ServerType.{_activityWatcher.ActivityServerType}");
|
||||
public string ServerType => _activityWatcher.Data.ServerType.ToTranslatedString();
|
||||
|
||||
public string ServerLocation { get; private set; } = Strings.ContextMenu_ServerInformation_Loading;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user