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.Enums;
global using Bloxstrap.Extensions; global using Bloxstrap.Extensions;
global using Bloxstrap.Models; global using Bloxstrap.Models;
global using Bloxstrap.Models.BloxstrapRPC;
global using Bloxstrap.Models.Attributes; global using Bloxstrap.Models.Attributes;
global using Bloxstrap.Models.RobloxApi; global using Bloxstrap.Models.RobloxApi;
global using Bloxstrap.UI; global using Bloxstrap.UI;

View File

@ -11,7 +11,7 @@
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] [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 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]+"; 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<string>? OnLogEntry;
public event EventHandler? OnGameJoin; public event EventHandler? OnGameJoin;
public event EventHandler? OnGameLeave; public event EventHandler? OnGameLeave;
public event EventHandler<GameMessage>? OnGameMessage; public event EventHandler<Message>? OnRPCMessage;
private Dictionary<string, string> GeolocationCache = new(); private Dictionary<string, string> GeolocationCache = new();
@ -233,13 +233,13 @@
else if (entry.Contains(GameMessageEntry)) else if (entry.Contains(GameMessageEntry))
{ {
string messagePlain = entry.Substring(entry.IndexOf(GameMessageEntry) + GameMessageEntry.Length + 1); string messagePlain = entry.Substring(entry.IndexOf(GameMessageEntry) + GameMessageEntry.Length + 1);
GameMessage? message; Message? message;
App.Logger.WriteLine(LOG_IDENT, $"Received message: '{messagePlain}'"); App.Logger.WriteLine(LOG_IDENT, $"Received message: '{messagePlain}'");
try try
{ {
message = JsonSerializer.Deserialize<GameMessage>(messagePlain); message = JsonSerializer.Deserialize<Message>(messagePlain);
} }
catch (Exception) catch (Exception)
{ {
@ -259,7 +259,7 @@
return; 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 DiscordRpcClient _rpcClient = new("1005469189907173486");
private readonly ActivityWatcher _activityWatcher; private readonly ActivityWatcher _activityWatcher;
private RichPresence? _currentPresence; private DiscordRPC.RichPresence? _currentPresence;
private bool _visible = true; private bool _visible = true;
private string? _initialStatus;
private long _currentUniverseId; private long _currentUniverseId;
private DateTime? _timeStartedUniverse; private DateTime? _timeStartedUniverse;
public DiscordRichPresence(ActivityWatcher activityWatcher) public DiscordRichPresence(ActivityWatcher activityWatcher)
{ {
const string LOG_IDENT = "DiscordRichPresence::<construct>"; const string LOG_IDENT = "DiscordRichPresence::DiscordRichPresence";
_activityWatcher = activityWatcher; _activityWatcher = activityWatcher;
_activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame()); _activityWatcher.OnGameJoin += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame()); _activityWatcher.OnGameLeave += (_, _) => Task.Run(() => SetCurrentGame());
_activityWatcher.OnGameMessage += (_, message) => OnGameMessage(message); _activityWatcher.OnRPCMessage += (_, message) => ProcessRPCMessage(message);
_rpcClient.OnReady += (_, e) => _rpcClient.OnReady += (_, e) =>
App.Logger.WriteLine(LOG_IDENT, $"Received ready from user {e.User} ({e.User.ID})"); App.Logger.WriteLine(LOG_IDENT, $"Received ready from user {e.User} ({e.User.ID})");
@ -45,71 +44,12 @@ namespace Bloxstrap.Integrations
_rpcClient.Initialize(); _rpcClient.Initialize();
} }
public void OnGameMessage(GameMessage message) public void ProcessRPCMessage(Message message)
{ {
switch (message.Command) const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage";
{
case "SetPresenceStatus":
SetStatus(message.Data);
break;
case "SetPresenceTimestamp": if (message.Command != "SetRichPresence")
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");
return; 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) if (_currentPresence is null)
{ {
@ -117,62 +57,66 @@ namespace Bloxstrap.Integrations
return; return;
} }
if (String.IsNullOrEmpty(data)) Models.BloxstrapRPC.RichPresence? presence;
{
App.Logger.WriteLine(LOG_IDENT, "Clearing timestamp");
_currentPresence.Timestamps.Start = null; try
_currentPresence.Timestamps.End = null; {
presence = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
} }
else catch (Exception)
{ {
var parameters = data.Split(';'); App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
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");
return; return;
} }
if (String.IsNullOrEmpty(data)) if (presence is null)
{ {
_currentPresence.Assets.SmallImageKey = "roblox"; App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
_currentPresence.Assets.SmallImageText = "Roblox"; 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)) if (presence.State is not null)
return; {
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) if (presence.TimestampStart is not null)
{ _currentPresence.Timestamps.StartUnixMilliseconds = presence.TimestampStart * 1000;
App.Logger.WriteLine(LOG_IDENT, $"Icon text cannot be longer than 128 characters, aborting");
return;
}
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={assetId}"; if (presence.TimestampEnd is not null)
_currentPresence.Assets.SmallImageText = parameters[1]; _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(); UpdatePresence();
} }
#endregion
public void SetVisibility(bool visible) public void SetVisibility(bool visible)
{ {
@ -194,7 +138,6 @@ namespace Bloxstrap.Integrations
{ {
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence"); App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
_currentPresence = null; _currentPresence = null;
_initialStatus = null;
UpdatePresence(); UpdatePresence();
return true; return true;
} }
@ -271,7 +214,7 @@ namespace Bloxstrap.Integrations
_ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""), _ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""),
}; };
_currentPresence = new RichPresence _currentPresence = new DiscordRPC.RichPresence
{ {
Details = $"Playing {universeDetails.Name}", Details = $"Playing {universeDetails.Name}",
State = status, 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!;
}
}