Revamp Bloxstrap game messaging, now BloxstrapRPC

support for nested and dynamically-typed json fields, and games now have greater liberty over how the rich presence can be configured
This commit is contained in:
pizzaboxer 2023-07-28 21:02:54 +01:00
parent cb4c813b8a
commit c8c49a2921
No known key found for this signature in database
GPG Key ID: 59D4A1DBAD0F2BA8
7 changed files with 101 additions and 124 deletions

View File

@ -17,6 +17,7 @@ global using System.Threading.Tasks;
global using Bloxstrap.Enums;
global using Bloxstrap.Extensions;
global using Bloxstrap.Models;
global using Bloxstrap.Models.BloxstrapRPC;
global using Bloxstrap.Models.Attributes;
global using Bloxstrap.Models.RobloxApi;
global using Bloxstrap.UI;

View File

@ -11,7 +11,7 @@
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] [SendBloxstrapMessage]";
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
@ -24,7 +24,7 @@
public event EventHandler<string>? OnLogEntry;
public event EventHandler? OnGameJoin;
public event EventHandler? OnGameLeave;
public event EventHandler<GameMessage>? OnGameMessage;
public event EventHandler<Message>? OnRPCMessage;
private Dictionary<string, string> GeolocationCache = new();
@ -233,13 +233,13 @@
else if (entry.Contains(GameMessageEntry))
{
string messagePlain = entry.Substring(entry.IndexOf(GameMessageEntry) + GameMessageEntry.Length + 1);
GameMessage? message;
Message? message;
App.Logger.WriteLine(LOG_IDENT, $"Received message: '{messagePlain}'");
try
{
message = JsonSerializer.Deserialize<GameMessage>(messagePlain);
message = JsonSerializer.Deserialize<Message>(messagePlain);
}
catch (Exception)
{
@ -259,7 +259,7 @@
return;
}
OnGameMessage?.Invoke(this, message);
OnRPCMessage?.Invoke(this, message);
}
}
}

View File

@ -7,21 +7,20 @@ namespace Bloxstrap.Integrations
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
private readonly ActivityWatcher _activityWatcher;
private RichPresence? _currentPresence;
private DiscordRPC.RichPresence? _currentPresence;
private bool _visible = true;
private string? _initialStatus;
private long _currentUniverseId;
private DateTime? _timeStartedUniverse;
public DiscordRichPresence(ActivityWatcher activityWatcher)
{
const string LOG_IDENT = "DiscordRichPresence::<construct>";
const string LOG_IDENT = "DiscordRichPresence::DiscordRichPresence";
_activityWatcher = activityWatcher;
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameMessage += (_, message) => OnGameMessage(message);
_activityWatcher.OnRPCMessage += (_, message) => ProcessRPCMessage(message);
_rpcClient.OnReady += (_, e) =>
App.Logger.WriteLine(LOG_IDENT, $"Received ready from user {e.User} ({e.User.ID})");
@ -45,71 +44,12 @@ namespace Bloxstrap.Integrations
_rpcClient.Initialize();
}
public void OnGameMessage(GameMessage message)
public void ProcessRPCMessage(Message message)
{
switch (message.Command)
{
case "SetPresenceStatus":
SetStatus(message.Data);
break;
const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage";
case "SetPresenceTimestamp":
SetTimestamp(message.Data);
break;
case "SetPresenceIcon":
SetIcon(message.Data);
break;
}
}
#region Game message commands
public void SetStatus(string status)
{
const string LOG_IDENT = "DiscordRichPresence::SetStatus";
App.Logger.WriteLine(LOG_IDENT, $"Setting status to '{status}'");
if (_currentPresence is null)
{
App.Logger.WriteLine(LOG_IDENT, $"Presence is not set, aborting");
if (message.Command != "SetRichPresence")
return;
}
if (status.Length > 128)
{
App.Logger.WriteLine(LOG_IDENT, $"Status cannot be longer than 128 characters, aborting");
return;
}
if (_initialStatus is null)
_initialStatus = _currentPresence.State;
string finalStatus;
if (String.IsNullOrEmpty(status))
{
App.Logger.WriteLine(LOG_IDENT, $"Status is empty, reverting to initial status");
finalStatus = _initialStatus;
}
else
{
finalStatus = status;
}
if (_currentPresence.State == finalStatus)
{
App.Logger.WriteLine(LOG_IDENT, $"Status is unchanged, aborting");
return;
}
_currentPresence.State = finalStatus;
UpdatePresence();
}
public void SetTimestamp(string data)
{
const string LOG_IDENT = "DiscordRichPresence::SetTimestamp";
if (_currentPresence is null)
{
@ -117,62 +57,66 @@ namespace Bloxstrap.Integrations
return;
}
if (String.IsNullOrEmpty(data))
{
App.Logger.WriteLine(LOG_IDENT, "Clearing timestamp");
Models.BloxstrapRPC.RichPresence? presence;
_currentPresence.Timestamps.Start = null;
_currentPresence.Timestamps.End = null;
try
{
presence = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
}
else
catch (Exception)
{
var parameters = data.Split(';');
if (parameters.Length >= 1 && ulong.TryParse(parameters[0], out ulong startTimestamp))
_currentPresence.Timestamps.StartUnixMilliseconds = startTimestamp * 1000;
if (parameters.Length >= 2 && ulong.TryParse(parameters[1], out ulong endTimestamp))
_currentPresence.Timestamps.EndUnixMilliseconds = endTimestamp * 1000;
}
UpdatePresence();
}
public void SetIcon(string data)
{
const string LOG_IDENT = "DiscordRichPresence::SetIcon";
if (_currentPresence is null)
{
App.Logger.WriteLine(LOG_IDENT, "Presence is not set, aborting");
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
return;
}
if (String.IsNullOrEmpty(data))
if (presence is null)
{
_currentPresence.Assets.SmallImageKey = "roblox";
_currentPresence.Assets.SmallImageText = "Roblox";
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
return;
}
else
if (presence.Details is not null)
{
var parameters = data.Split(';');
if (presence.Details.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
else
_currentPresence.Details = presence.Details;
}
if (parameters.Length < 2 || !ulong.TryParse(parameters[0], out ulong assetId))
return;
if (presence.State is not null)
{
if (presence.State.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
else
_currentPresence.State = presence.State;
}
if (parameters[1].Length > 128)
{
App.Logger.WriteLine(LOG_IDENT, $"Icon text cannot be longer than 128 characters, aborting");
return;
}
if (presence.TimestampStart is not null)
_currentPresence.Timestamps.StartUnixMilliseconds = presence.TimestampStart * 1000;
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={assetId}";
_currentPresence.Assets.SmallImageText = parameters[1];
if (presence.TimestampEnd is not null)
_currentPresence.Timestamps.EndUnixMilliseconds = presence.TimestampEnd * 1000;
if (presence.SmallImage is not null)
{
if (presence.SmallImage.AssetId is not null)
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presence.SmallImage.AssetId}";
if (presence.SmallImage.HoverText is not null)
_currentPresence.Assets.SmallImageText = presence.SmallImage.HoverText;
}
if (presence.LargeImage is not null)
{
if (presence.LargeImage.AssetId is not null)
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presence.LargeImage.AssetId}";
if (presence.LargeImage.HoverText is not null)
_currentPresence.Assets.LargeImageText = presence.LargeImage.HoverText;
}
UpdatePresence();
}
#endregion
public void SetVisibility(bool visible)
{
@ -194,7 +138,6 @@ namespace Bloxstrap.Integrations
{
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
_currentPresence = null;
_initialStatus = null;
UpdatePresence();
return true;
}
@ -271,7 +214,7 @@ namespace Bloxstrap.Integrations
_ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""),
};
_currentPresence = new RichPresence
_currentPresence = new DiscordRPC.RichPresence
{
Details = $"Playing {universeDetails.Name}",
State = status,

View File

@ -0,0 +1,10 @@
namespace Bloxstrap.Models.BloxstrapRPC;
public class Message
{
[JsonPropertyName("command")]
public string Command { get; set; } = null!;
[JsonPropertyName("data")]
public JsonElement Data { get; set; }
}

View File

@ -0,0 +1,23 @@
namespace Bloxstrap.Models.BloxstrapRPC
{
class RichPresence
{
[JsonPropertyName("details")]
public string? Details { get; set; }
[JsonPropertyName("state")]
public string? State { get; set; }
[JsonPropertyName("timestampStart")]
public ulong? TimestampStart { get; set; }
[JsonPropertyName("timestampEnd")]
public ulong? TimestampEnd { get; set; }
[JsonPropertyName("smallImage")]
public RichPresenceImage? SmallImage { get; set; }
[JsonPropertyName("largeImage")]
public RichPresenceImage? LargeImage { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models.BloxstrapRPC
{
class RichPresenceImage
{
[JsonPropertyName("assetId")]
public ulong? AssetId { get; set; }
[JsonPropertyName("hoverText")]
public string? HoverText { get; set; }
}
}

View File

@ -1,11 +0,0 @@
namespace Bloxstrap.Models
{
public class GameMessage
{
[JsonPropertyName("command")]
public string Command { get; set; } = null!;
[JsonPropertyName("data")]
public string Data { get; set; } = null!;
}
}