mirror of
https://github.com/bloxstraplabs/bloxstrap.git
synced 2025-04-21 18:11:27 -07:00
Merge branch 'main' into user-pfp-discord-rpc
This commit is contained in:
commit
abb08f11b6
@ -1,12 +1,14 @@
|
|||||||
using System.Web;
|
namespace Bloxstrap.Integrations
|
||||||
|
|
||||||
namespace Bloxstrap.Integrations
|
|
||||||
{
|
{
|
||||||
public class ActivityWatcher : IDisposable
|
public class ActivityWatcher : IDisposable
|
||||||
{
|
{
|
||||||
// i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence,
|
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
|
||||||
// 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 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 GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer";
|
||||||
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
|
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
|
||||||
private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
|
private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
|
||||||
@ -14,12 +16,12 @@ namespace Bloxstrap.Integrations
|
|||||||
private const string GameJoinedEntry = "[FLog::Network] serverId:";
|
private const string GameJoinedEntry = "[FLog::Network] serverId:";
|
||||||
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
|
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
|
||||||
private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport";
|
private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport";
|
||||||
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
|
|
||||||
private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal";
|
private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal";
|
||||||
private const string GameJoinLoadTimeEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
|
private const string GameJoinLoadTimeEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
|
||||||
|
|
||||||
private const string GameJoinLoadTimeEntryPattern = ", userid:([0-9]+)";
|
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 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 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 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 GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
|
||||||
@ -41,21 +43,14 @@ namespace Bloxstrap.Integrations
|
|||||||
|
|
||||||
public string LogLocation = null!;
|
public string LogLocation = null!;
|
||||||
|
|
||||||
// these are values to use assuming the player isn't currently in a game
|
public bool InGame = false;
|
||||||
// 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 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;
|
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)
|
// TODO: i need to double check how this handles failed game joins (connection error, invalid permissions, etc)
|
||||||
private void ReadLogEntry(string entry)
|
private void ReadLogEntry(string entry)
|
||||||
{
|
{
|
||||||
@ -159,7 +144,12 @@ namespace Bloxstrap.Integrations
|
|||||||
if (entry.Contains(GameLeavingEntry))
|
if (entry.Contains(GameLeavingEntry))
|
||||||
OnAppClose?.Invoke(this, new EventArgs());
|
OnAppClose?.Invoke(this, new EventArgs());
|
||||||
|
|
||||||
if (ActivityUserId == "" && entry.Contains(GameJoinLoadTimeEntry))
|
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);
|
Match match = Regex.Match(entry, GameJoinLoadTimeEntryPattern);
|
||||||
|
|
||||||
@ -170,16 +160,25 @@ namespace Bloxstrap.Integrations
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityUserId = match.Groups[1].Value;
|
Data.ActivityUserId = match.Groups[1].Value;
|
||||||
}
|
}
|
||||||
if (!ActivityInGame && ActivityPlaceId == 0)
|
|
||||||
{
|
|
||||||
// We are not in a game, nor are in the process of joining one
|
|
||||||
|
|
||||||
if (entry.Contains(GameJoiningPrivateServerEntry))
|
if (entry.Contains(GameJoiningPrivateServerEntry))
|
||||||
{
|
{
|
||||||
// we only expect to be joining a private server if we're not already in a game
|
// 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))
|
else if (entry.Contains(GameJoiningEntry))
|
||||||
{
|
{
|
||||||
@ -192,27 +191,27 @@ namespace Bloxstrap.Integrations
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityInGame = false;
|
InGame = false;
|
||||||
ActivityPlaceId = long.Parse(match.Groups[2].Value);
|
Data.PlaceId = long.Parse(match.Groups[2].Value);
|
||||||
ActivityJobId = match.Groups[1].Value;
|
Data.JobId = match.Groups[1].Value;
|
||||||
ActivityMachineAddress = match.Groups[3].Value;
|
Data.MachineAddress = match.Groups[3].Value;
|
||||||
|
|
||||||
if (_teleportMarker)
|
if (_teleportMarker)
|
||||||
{
|
{
|
||||||
ActivityIsTeleport = true;
|
Data.IsTeleport = true;
|
||||||
_teleportMarker = false;
|
_teleportMarker = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_reservedTeleportMarker)
|
if (_reservedTeleportMarker)
|
||||||
{
|
{
|
||||||
ActivityServerType = ServerType.Reserved;
|
Data.ServerType = ServerType.Reserved;
|
||||||
_reservedTeleportMarker = false;
|
_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
|
// 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;
|
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))
|
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, "Failed to assert format for game join UDMUX entry");
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
App.Logger.WriteLine(LOG_IDENT, entry);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityMachineAddress = match.Groups[1].Value;
|
Data.MachineAddress = match.Groups[1].Value;
|
||||||
ActivityMachineUDMUX = true;
|
|
||||||
|
|
||||||
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))
|
else if (entry.Contains(GameJoinedEntry))
|
||||||
{
|
{
|
||||||
Match match = Regex.Match(entry, GameJoinedEntryPattern);
|
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, $"Failed to assert format for game joined entry");
|
||||||
App.Logger.WriteLine(LOG_IDENT, entry);
|
App.Logger.WriteLine(LOG_IDENT, entry);
|
||||||
return;
|
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;
|
InGame = true;
|
||||||
ActivityTimeJoined = DateTime.Now;
|
Data.TimeJoined = DateTime.Now;
|
||||||
|
|
||||||
OnGameJoin?.Invoke(this, new EventArgs());
|
OnGameJoin?.Invoke(this, new EventArgs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (ActivityInGame && ActivityPlaceId != 0)
|
else if (InGame && Data.PlaceId != 0)
|
||||||
{
|
{
|
||||||
// We are confirmed to be in a game
|
// We are confirmed to be in a game
|
||||||
|
|
||||||
if (entry.Contains(GameDisconnectedEntry))
|
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?
|
Data.TimeLeft = DateTime.Now;
|
||||||
if (ActivityServerType != ServerType.Reserved)
|
History.Insert(0, Data);
|
||||||
{
|
|
||||||
ActivityHistory.Insert(0, new ActivityHistoryEntry
|
|
||||||
{
|
|
||||||
PlaceId = ActivityPlaceId,
|
|
||||||
UniverseId = ActivityUniverseId,
|
|
||||||
JobId = ActivityJobId,
|
|
||||||
TimeJoined = ActivityTimeJoined,
|
|
||||||
TimeLeft = DateTime.Now
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityInGame = false;
|
InGame = false;
|
||||||
ActivityPlaceId = 0;
|
Data = new();
|
||||||
ActivityUniverseId = 0;
|
|
||||||
ActivityJobId = "";
|
|
||||||
ActivityMachineAddress = "";
|
|
||||||
ActivityMachineUDMUX = false;
|
|
||||||
ActivityIsTeleport = false;
|
|
||||||
ActivityLaunchData = "";
|
|
||||||
ActivityServerType = ServerType.Public;
|
|
||||||
ActivityUserId = "";
|
|
||||||
|
|
||||||
OnGameLeave?.Invoke(this, new EventArgs());
|
OnGameLeave?.Invoke(this, new EventArgs());
|
||||||
}
|
}
|
||||||
else if (entry.Contains(GameTeleportingEntry))
|
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;
|
_teleportMarker = true;
|
||||||
}
|
}
|
||||||
else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry))
|
else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry))
|
||||||
@ -378,7 +366,7 @@ namespace Bloxstrap.Integrations
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityLaunchData = data;
|
Data.RPCLaunchData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
OnRPCMessage?.Invoke(this, message);
|
OnRPCMessage?.Invoke(this, message);
|
||||||
@ -392,13 +380,13 @@ namespace Bloxstrap.Integrations
|
|||||||
{
|
{
|
||||||
const string LOG_IDENT = "ActivityWatcher::GetServerLocation";
|
const string LOG_IDENT = "ActivityWatcher::GetServerLocation";
|
||||||
|
|
||||||
if (GeolocationCache.ContainsKey(ActivityMachineAddress))
|
if (GeolocationCache.ContainsKey(Data.MachineAddress))
|
||||||
return GeolocationCache[ActivityMachineAddress];
|
return GeolocationCache[Data.MachineAddress];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string location = "";
|
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)
|
if (ipInfo is null)
|
||||||
return $"? ({Strings.ActivityTracker_LookupFailed})";
|
return $"? ({Strings.ActivityTracker_LookupFailed})";
|
||||||
@ -410,16 +398,16 @@ namespace Bloxstrap.Integrations
|
|||||||
else
|
else
|
||||||
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
|
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
|
||||||
|
|
||||||
if (!ActivityInGame)
|
if (!InGame)
|
||||||
return $"? ({Strings.ActivityTracker_LeftGame})";
|
return $"? ({Strings.ActivityTracker_LeftGame})";
|
||||||
|
|
||||||
GeolocationCache[ActivityMachineAddress] = location;
|
GeolocationCache[Data.MachineAddress] = location;
|
||||||
|
|
||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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);
|
App.Logger.WriteException(LOG_IDENT, ex);
|
||||||
|
|
||||||
return $"? ({Strings.ActivityTracker_LookupFailed})";
|
return $"? ({Strings.ActivityTracker_LookupFailed})";
|
||||||
|
@ -6,14 +6,12 @@ namespace Bloxstrap.Integrations
|
|||||||
{
|
{
|
||||||
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
|
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
|
||||||
private readonly ActivityWatcher _activityWatcher;
|
private readonly ActivityWatcher _activityWatcher;
|
||||||
|
private readonly Queue<Message> _messageQueue = new();
|
||||||
|
|
||||||
private DiscordRPC.RichPresence? _currentPresence;
|
private DiscordRPC.RichPresence? _currentPresence;
|
||||||
private DiscordRPC.RichPresence? _currentPresenceCopy;
|
private DiscordRPC.RichPresence? _originalPresence;
|
||||||
private Queue<Message> _messageQueue = new();
|
|
||||||
|
|
||||||
private bool _visible = true;
|
private bool _visible = true;
|
||||||
private long _currentUniverseId;
|
|
||||||
private DateTime? _timeStartedUniverse;
|
|
||||||
|
|
||||||
public DiscordRichPresence(ActivityWatcher activityWatcher)
|
public DiscordRichPresence(ActivityWatcher activityWatcher)
|
||||||
{
|
{
|
||||||
@ -54,7 +52,7 @@ namespace Bloxstrap.Integrations
|
|||||||
if (message.Command != "SetRichPresence" && message.Command != "SetLaunchData")
|
if (message.Command != "SetRichPresence" && message.Command != "SetLaunchData")
|
||||||
return;
|
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");
|
App.Logger.WriteLine(LOG_IDENT, "Presence is not set, enqueuing message");
|
||||||
_messageQueue.Enqueue(message);
|
_messageQueue.Enqueue(message);
|
||||||
@ -65,12 +63,7 @@ namespace Bloxstrap.Integrations
|
|||||||
|
|
||||||
if (message.Command == "SetLaunchData")
|
if (message.Command == "SetLaunchData")
|
||||||
{
|
{
|
||||||
var buttonQuery = _currentPresence.Buttons.Where(x => x.Label == "Join server");
|
_currentPresence.Buttons = GetButtons();
|
||||||
|
|
||||||
if (!buttonQuery.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
buttonQuery.First().Url = _activityWatcher.GetActivityDeeplink();
|
|
||||||
}
|
}
|
||||||
else if (message.Command == "SetRichPresence")
|
else if (message.Command == "SetRichPresence")
|
||||||
{
|
{
|
||||||
@ -97,7 +90,7 @@ namespace Bloxstrap.Integrations
|
|||||||
if (presenceData.Details.Length > 128)
|
if (presenceData.Details.Length > 128)
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
|
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
|
||||||
else if (presenceData.Details == "<reset>")
|
else if (presenceData.Details == "<reset>")
|
||||||
_currentPresence.Details = _currentPresenceCopy.Details;
|
_currentPresence.Details = _originalPresence.Details;
|
||||||
else
|
else
|
||||||
_currentPresence.Details = presenceData.Details;
|
_currentPresence.Details = presenceData.Details;
|
||||||
}
|
}
|
||||||
@ -107,7 +100,7 @@ namespace Bloxstrap.Integrations
|
|||||||
if (presenceData.State.Length > 128)
|
if (presenceData.State.Length > 128)
|
||||||
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
|
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
|
||||||
else if (presenceData.State == "<reset>")
|
else if (presenceData.State == "<reset>")
|
||||||
_currentPresence.State = _currentPresenceCopy.State;
|
_currentPresence.State = _originalPresence.State;
|
||||||
else
|
else
|
||||||
_currentPresence.State = presenceData.State;
|
_currentPresence.State = presenceData.State;
|
||||||
}
|
}
|
||||||
@ -130,8 +123,8 @@ namespace Bloxstrap.Integrations
|
|||||||
}
|
}
|
||||||
else if (presenceData.SmallImage.Reset)
|
else if (presenceData.SmallImage.Reset)
|
||||||
{
|
{
|
||||||
_currentPresence.Assets.SmallImageText = _currentPresenceCopy.Assets.SmallImageText;
|
_currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
|
||||||
_currentPresence.Assets.SmallImageKey = _currentPresenceCopy.Assets.SmallImageKey;
|
_currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -151,8 +144,8 @@ namespace Bloxstrap.Integrations
|
|||||||
}
|
}
|
||||||
else if (presenceData.LargeImage.Reset)
|
else if (presenceData.LargeImage.Reset)
|
||||||
{
|
{
|
||||||
_currentPresence.Assets.LargeImageText = _currentPresenceCopy.Assets.LargeImageText;
|
_currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
|
||||||
_currentPresence.Assets.LargeImageKey = _currentPresenceCopy.Assets.LargeImageKey;
|
_currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -185,11 +178,11 @@ namespace Bloxstrap.Integrations
|
|||||||
{
|
{
|
||||||
const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame";
|
const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame";
|
||||||
|
|
||||||
if (!_activityWatcher.ActivityInGame)
|
if (!_activityWatcher.InGame)
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
|
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
|
||||||
|
|
||||||
_currentPresence = _currentPresenceCopy = null;
|
_currentPresence = _originalPresence = null;
|
||||||
_messageQueue.Clear();
|
_messageQueue.Clear();
|
||||||
|
|
||||||
UpdatePresence();
|
UpdatePresence();
|
||||||
@ -200,46 +193,37 @@ namespace Bloxstrap.Integrations
|
|||||||
string smallImageText = "Roblox";
|
string smallImageText = "Roblox";
|
||||||
string smallImage = "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}");
|
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
|
// preserve time spent playing if we're teleporting between places in the same universe
|
||||||
if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId)
|
var timeStarted = activity.TimeJoined;
|
||||||
_timeStartedUniverse = DateTime.UtcNow;
|
|
||||||
|
|
||||||
_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 (activity.UniverseDetails is null)
|
||||||
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
|
|
||||||
{
|
{
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0];
|
icon = universeDetails.Thumbnail.ImageUrl;
|
||||||
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}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (App.Settings.Prop.AccountShownOnProfile)
|
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())
|
if (userPfpResponse is null || !userPfpResponse.Data.Any())
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Could not get user thumbnail info!");
|
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}");
|
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)
|
if (userInfoResponse is null)
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Could not get user info!");
|
App.Logger.WriteLine(LOG_IDENT, "Could not get user info!");
|
||||||
@ -262,46 +246,30 @@ namespace Bloxstrap.Integrations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_activityWatcher.InGame || placeId != activity.PlaceId)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed");
|
App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
string status = _activityWatcher.ActivityServerType switch
|
string status = _activityWatcher.Data.ServerType switch
|
||||||
{
|
{
|
||||||
ServerType.Private => "In a private server",
|
ServerType.Private => "In a private server",
|
||||||
ServerType.Reserved => "In a reserved 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)
|
string universeName = universeDetails.Data.Name;
|
||||||
universeDetails.Name = $"{universeDetails.Name}\x2800\x2800\x2800";
|
|
||||||
|
if (universeName.Length < 2)
|
||||||
|
universeName = $"{universeName}\x2800\x2800\x2800";
|
||||||
|
|
||||||
_currentPresence = new DiscordRPC.RichPresence
|
_currentPresence = new DiscordRPC.RichPresence
|
||||||
{
|
{
|
||||||
Details = $"Playing {universeDetails.Name}",
|
Details = $"Playing {universeName}",
|
||||||
State = status,
|
State = status,
|
||||||
Timestamps = new Timestamps { Start = _timeStartedUniverse },
|
Timestamps = new Timestamps { Start = timeStarted.ToUniversalTime() },
|
||||||
Buttons = buttons.ToArray(),
|
Buttons = GetButtons(),
|
||||||
Assets = new Assets
|
Assets = new Assets
|
||||||
{
|
{
|
||||||
LargeImageKey = icon,
|
LargeImageKey = icon,
|
||||||
@ -312,7 +280,7 @@ namespace Bloxstrap.Integrations
|
|||||||
};
|
};
|
||||||
|
|
||||||
// this is used for configuration from BloxstrapRPC
|
// this is used for configuration from BloxstrapRPC
|
||||||
_currentPresenceCopy = _currentPresence.Clone();
|
_originalPresence = _currentPresence.Clone();
|
||||||
|
|
||||||
if (_messageQueue.Any())
|
if (_messageQueue.Any())
|
||||||
{
|
{
|
||||||
@ -325,6 +293,40 @@ namespace Bloxstrap.Integrations
|
|||||||
return true;
|
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()
|
public void UpdatePresence()
|
||||||
{
|
{
|
||||||
const string LOG_IDENT = "DiscordRichPresence::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>
|
/// <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..
|
/// 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>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Rejoin.
|
/// Looks up a localized string similar to Rejoin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1229,7 +1247,7 @@ namespace Bloxstrap.Resources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Private.
|
/// Looks up a localized string similar to Private server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Enums_ServerType_Private {
|
public static string Enums_ServerType_Private {
|
||||||
get {
|
get {
|
||||||
@ -1238,7 +1256,7 @@ namespace Bloxstrap.Resources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Public.
|
/// Looks up a localized string similar to Public server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Enums_ServerType_Public {
|
public static string Enums_ServerType_Public {
|
||||||
get {
|
get {
|
||||||
@ -1247,7 +1265,7 @@ namespace Bloxstrap.Resources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Reserved.
|
/// Looks up a localized string similar to Reserved server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Enums_ServerType_Reserved {
|
public static string Enums_ServerType_Reserved {
|
||||||
get {
|
get {
|
||||||
|
@ -398,13 +398,13 @@ If not, then please report this exception through a [GitHub issue]({1}) along wi
|
|||||||
<value>Direct3D 11</value>
|
<value>Direct3D 11</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Enums.ServerType.Private" xml:space="preserve">
|
<data name="Enums.ServerType.Private" xml:space="preserve">
|
||||||
<value>Private</value>
|
<value>Private server</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Enums.ServerType.Public" xml:space="preserve">
|
<data name="Enums.ServerType.Public" xml:space="preserve">
|
||||||
<value>Public</value>
|
<value>Public server</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Enums.ServerType.Reserved" xml:space="preserve">
|
<data name="Enums.ServerType.Reserved" xml:space="preserve">
|
||||||
<value>Reserved</value>
|
<value>Reserved server</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Enums.Theme.Dark" xml:space="preserve">
|
<data name="Enums.Theme.Dark" xml:space="preserve">
|
||||||
<value>Dark</value>
|
<value>Dark</value>
|
||||||
@ -1177,4 +1177,10 @@ Are you sure you want to continue?</value>
|
|||||||
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
|
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
|
||||||
<value>Rejoin</value>
|
<value>Rejoin</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</MenuItem.Header>
|
</MenuItem.Header>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem x:Name="JoinLastServerMenuItem" Visibility="Collapsed" Click="JoinLastServerMenuItem_Click">
|
<MenuItem Click="JoinLastServerMenuItem_Click">
|
||||||
<MenuItem.Header>
|
<MenuItem.Header>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
@ -70,7 +70,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Dispatcher.Invoke(() => {
|
Dispatcher.Invoke(() => {
|
||||||
if (_activityWatcher.ActivityServerType == ServerType.Public)
|
if (_activityWatcher.Data.ServerType == ServerType.Public)
|
||||||
InviteDeeplinkMenuItem.Visibility = Visibility.Visible;
|
InviteDeeplinkMenuItem.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
ServerDetailsMenuItem.Visibility = Visibility.Visible;
|
ServerDetailsMenuItem.Visibility = Visibility.Visible;
|
||||||
@ -80,7 +80,6 @@ namespace Bloxstrap.UI.Elements.ContextMenu
|
|||||||
public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
|
public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => {
|
Dispatcher.Invoke(() => {
|
||||||
JoinLastServerMenuItem.Visibility = Visibility.Visible;
|
|
||||||
InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed;
|
InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed;
|
||||||
ServerDetailsMenuItem.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 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();
|
private void ServerDetailsMenuItem_Click(object sender, RoutedEventArgs e) => ShowServerInformationWindow();
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
WindowStartupLocation="CenterScreen">
|
WindowStartupLocation="CenterScreen">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<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" />
|
<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>
|
<Border.Style>
|
||||||
<Style TargetType="Border">
|
<Style TargetType="Border">
|
||||||
<Style.Triggers>
|
<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" />
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
@ -42,11 +45,11 @@
|
|||||||
<ui:ProgressRing Grid.Row="1" IsIndeterminate="True" />
|
<ui:ProgressRing Grid.Row="1" IsIndeterminate="True" />
|
||||||
</Border>
|
</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>
|
<ListView.Style>
|
||||||
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
|
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
|
||||||
<Style.Triggers>
|
<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" />
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
@ -65,14 +68,15 @@
|
|||||||
|
|
||||||
<Border Grid.Column="0" Width="84" Height="84" CornerRadius="4">
|
<Border Grid.Column="0" Width="84" Height="84" CornerRadius="4">
|
||||||
<Border.Background>
|
<Border.Background>
|
||||||
<ImageBrush ImageSource="{Binding GameThumbnail, IsAsync=True}" />
|
<ImageBrush ImageSource="{Binding UniverseDetails.Thumbnail.ImageUrl, IsAsync=True}" />
|
||||||
</Border.Background>
|
</Border.Background>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
|
||||||
<TextBlock Text="{Binding GameName}" FontSize="18" FontWeight="Medium" />
|
<TextBlock Text="{Binding UniverseDetails.Data.Name}" FontSize="18" FontWeight="Medium" />
|
||||||
<TextBlock Text="{Binding TimeJoinedFriendly}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
<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" Icon="Play28" IconFilled="True" />
|
<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>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ui:Card>
|
</ui:Card>
|
||||||
@ -80,7 +84,7 @@
|
|||||||
</ListView.ItemTemplate>
|
</ListView.ItemTemplate>
|
||||||
</ListView>
|
</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">
|
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
|
||||||
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
|
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
@ -60,7 +60,7 @@ namespace Bloxstrap.UI
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
string serverLocation = await _activityWatcher.GetServerLocation();
|
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.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public,
|
||||||
ServerType.Private => Strings.ContextMenu_ServerInformation_Notification_Title_Private,
|
ServerType.Private => Strings.ContextMenu_ServerInformation_Notification_Title_Private,
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Windows.Input;
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Bloxstrap.Integrations;
|
using Bloxstrap.Integrations;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
@ -10,7 +8,7 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
|||||||
{
|
{
|
||||||
private readonly ActivityWatcher _activityWatcher;
|
private readonly ActivityWatcher _activityWatcher;
|
||||||
|
|
||||||
public List<ActivityHistoryEntry>? ActivityHistory { get; private set; }
|
public List<ActivityData>? GameHistory { get; private set; }
|
||||||
|
|
||||||
public ICommand CloseWindowCommand => new RelayCommand(RequestClose);
|
public ICommand CloseWindowCommand => new RelayCommand(RequestClose);
|
||||||
|
|
||||||
@ -27,32 +25,45 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
|||||||
|
|
||||||
private async void LoadData()
|
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())
|
if (entries.Any())
|
||||||
{
|
{
|
||||||
|
// TODO: this will duplicate universe ids
|
||||||
string universeIds = String.Join(',', entries.Select(x => x.UniverseId));
|
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 (!await UniverseDetails.FetchBulk(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;
|
return;
|
||||||
|
|
||||||
foreach (var entry in entries)
|
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)
|
||||||
{
|
{
|
||||||
entry.GameName = gameDetailResponse.Data.Where(x => x.Id == entry.UniverseId).Select(x => x.Name).First();
|
if (entry.RootActivity is not null)
|
||||||
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(GameHistory));
|
||||||
OnPropertyChanged(nameof(ActivityHistory));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty);
|
private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty);
|
||||||
|
@ -9,9 +9,9 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
|
|||||||
{
|
{
|
||||||
private readonly ActivityWatcher _activityWatcher;
|
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;
|
public string ServerLocation { get; private set; } = Strings.ContextMenu_ServerInformation_Loading;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user