Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 96 additions & 11 deletions LobbyCompatibility/Features/LobbyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using HarmonyLib;
using LobbyCompatibility.Enums;
using LobbyCompatibility.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Steamworks;
using Steamworks.Data;

Expand All @@ -22,6 +24,30 @@ public static class LobbyHelper
private static List<PluginInfoRecord>? _clientPlugins;
public static LobbyDiff LatestLobbyDiff { get; private set; } = new(new List<PluginDiff>());
private static Dictionary<ulong, LobbyDiff> LobbyDiffCache { get; } = new();

/// <summary>
/// The absolute maximum string size for ALL steam lobby metadata is 8192 (2^13).
/// We want to give a large enough margin for the checksum, vanilla game metadata, and any other modded metadata.
/// </summary>
private const int MaxPluginMetadataLength = 7800;

/// <summary>
/// The average json string size of a plugin.
/// </summary>
/// <remarks> Calculated from `{"i":"BMX.LobbyCompatibility","v":"1.4.1","c":null,"s":null}` </remarks>
private const int AveragePluginJsonLength = 60;

/// <summary>
/// The current maximum string size for the plugin field in lobby metadata.
/// </summary>
/// <remarks> WARNING: The absolute maximum string size for ALL steam lobby metadata is 8192 (2^13). </remarks>
public static int CurrentMaxPluginMetadataLength { get; private set; } = MaxPluginMetadataLength;

/// <summary>
/// The maximum string size for ALL steam lobby metadata is 8192 (2^13).
/// We want to give a large margin for the checksum, vanilla game metadata, and any other modded metadata.
/// </summary>
private static int AverageMaxLobbyMetadataModCount => CurrentMaxPluginMetadataLength / AveragePluginJsonLength;

/// <summary>
/// Get a <see cref="LobbyDiff" /> from a <see cref="Lobby" />.
Expand All @@ -33,10 +59,20 @@ public static class LobbyHelper
/// <summary>
/// Get a <see cref="LobbyDiff" /> from a <see cref="Lobby" />.
/// </summary>
/// <param name="lobby"> The lobby to get the diff from. </param>
/// <param name="lobbyData"> The lobby to get the diff from. </param>
/// <returns> The <see cref="LobbyDiff" /> from the <see cref="Lobby" />. </returns>
public static LobbyDiff GetLobbyDiff(IEnumerable<KeyValuePair<string, string>> lobbyData) => GetLobbyDiff(null, null, lobbyData);

/// <summary>
/// Reserve space in the lobby metadata.
/// Useful if you need larger space for lobby metadata for your mod.
/// </summary>
/// <param name="length"> The amount to metadata space in bytes space to reserve. The amound of bytes LobbyCompatibility will use is reduced by this amount. </param>
public static void ReserveLobbyMetadataSpace(int length)
{
CurrentMaxPluginMetadataLength -= length;
}

/// <summary>
/// Get a <see cref="LobbyDiff" /> from a <see cref="Lobby" /> or <see cref="IEnumerable{String}" />.
/// </summary>
Expand All @@ -54,8 +90,11 @@ internal static LobbyDiff GetLobbyDiff(Lobby? lobby, string? lobbyPluginString,

var lobbyDataList = lobbyData?.ToList();

var lobbyPlugins = PluginHelper
.ParseLobbyPluginsMetadata(lobbyPluginString ?? (lobby.HasValue ? GetLobbyPlugins(lobby.Value) : (lobbyDataList != null ? GetLobbyPlugins(lobbyDataList) : string.Empty))).ToList();
var lobbyPlugins = ParseLobbyPluginsMetadata(lobbyPluginString ?? (lobby.HasValue
? GetLobbyPlugins(lobby.Value)
: lobbyDataList != null
? GetLobbyPlugins(lobbyDataList)
: string.Empty)).ToList();
_clientPlugins = PluginHelper.GetAllPluginInfo().CalculateCompatibilityLevel(lobby, lobbyDataList);

var pluginDiffs = new List<PluginDiff>();
Expand Down Expand Up @@ -143,18 +182,12 @@ internal static LobbyDiff GetLobbyDiff(Lobby? lobby, string? lobbyPluginString,
/// <returns> A json <see cref="string" /> from the <see cref="Lobby" />. </returns>
internal static string GetLobbyPlugins(Lobby lobby)
{
var lobbyPluginStrings = new List<string>();

if (GameNetworkManager.Instance && !GameNetworkManager.Instance.disableSteam)
{
var i = 0;
do lobbyPluginStrings.Insert(i, lobby.GetData($"{LobbyMetadata.Plugins}{i}"));
while (lobbyPluginStrings[i++].Contains("@"));
return lobby.GetData($"{LobbyMetadata.Plugins}0");
}

return lobbyPluginStrings
.Join(delimiter: string.Empty)
.Replace("@", string.Empty);
return string.Empty;
}

/// <summary>
Expand All @@ -177,6 +210,58 @@ internal static string GetLobbyPlugins(List<KeyValuePair<string, string>> lobbyD
.Join(delimiter: string.Empty)
.Replace("@", string.Empty);
}

/// <summary>
/// Creates a json string containing the metadata of the maximum amount of plugins, sorted by highest compatibility requirement first.
/// </summary>
/// <returns> A json strings containing the maximum allowed mod count, as dictated by <see cref="CurrentMaxPluginMetadataLength"/>. </returns>
internal static string GetLobbyPluginsMetadata(List<PluginInfoRecord>? plugins = null)
{
plugins ??= PluginHelper.GetAllPluginInfo().ToList();

plugins.Sort();

var allowedPluginInfoRecords = plugins.Take(AverageMaxLobbyMetadataModCount).ToList();

if (allowedPluginInfoRecords.Sum(record => record.JsonLength) + 1 + allowedPluginInfoRecords.Count > CurrentMaxPluginMetadataLength)
{
do
{
allowedPluginInfoRecords.RemoveAt(allowedPluginInfoRecords.Count - 1);
} while (allowedPluginInfoRecords.Sum(record => record.JsonLength) + 1 + allowedPluginInfoRecords.Count > CurrentMaxPluginMetadataLength);

return JsonConvert.SerializeObject(allowedPluginInfoRecords, new VersionConverter());
}

do
{
allowedPluginInfoRecords.Add(plugins[allowedPluginInfoRecords.Count]);
} while (allowedPluginInfoRecords.Sum(record => record.JsonLength) + 1 + allowedPluginInfoRecords.Count <= CurrentMaxPluginMetadataLength);

allowedPluginInfoRecords.RemoveAt(allowedPluginInfoRecords.Count - 1);

return JsonConvert.SerializeObject(allowedPluginInfoRecords, new VersionConverter());
}

/// <summary>
/// Parses a json string containing the metadata of all plugins.
/// </summary>
/// <param name="json"> The json string to parse. </param>
/// <returns> A list of plugins in the APIPluginInfo format. </returns>
internal static IEnumerable<PluginInfoRecord> ParseLobbyPluginsMetadata(string json)
{
try
{
return JsonConvert.DeserializeObject<List<PluginInfoRecord>>(json, new VersionConverter()) ??
new List<PluginInfoRecord>();
}
catch (Exception e)
{
LobbyCompatibilityPlugin.Logger?.LogError("Failed to parse lobby plugins metadata.");
LobbyCompatibilityPlugin.Logger?.LogDebug(e);
throw;
}
}

public static string GetCompatibilityHeader(PluginDiffResult pluginDiffResult)
{
Expand Down
44 changes: 1 addition & 43 deletions LobbyCompatibility/Features/PluginHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
using LobbyCompatibility.Attributes;
using LobbyCompatibility.Enums;
using LobbyCompatibility.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Steamworks.Data;

namespace LobbyCompatibility.Features;
Expand All @@ -21,6 +19,7 @@ namespace LobbyCompatibility.Features;
/// </summary>
public static class PluginHelper
{

/// <summary>
/// PluginInfos registered through the register command, rather than found using the attribute.
/// </summary>
Expand Down Expand Up @@ -128,47 +127,6 @@ internal static IEnumerable<PluginInfoRecord> GetAllPluginInfo()
return pluginInfos.Concat(RegisteredPluginInfoRecords);
}

/// <summary>
/// Creates a list of json strings containing the metadata of all plugins, to add to the lobby.
/// </summary>
/// <returns> A list of json strings containing the metadata of all plugins. </returns>
internal static IEnumerable<string> GetLobbyPluginsMetadata(List<PluginInfoRecord>? plugins = null)
{
var json = JsonConvert.SerializeObject(plugins ?? GetAllPluginInfo().ToList(), new VersionConverter());

// The maximum string size for steam lobby metadata is 8192 (2^13).
// We want one less than the maximum to allow space for a delimiter
var maxChunkLength = 8191;

for (var i = 0; i < json.Length; i += maxChunkLength)
{
if (maxChunkLength + i > json.Length)
maxChunkLength = json.Length - i;

yield return json.Substring(i, maxChunkLength);
}
}

/// <summary>
/// Parses a json string containing the metadata of all plugins.
/// </summary>
/// <param name="json"> The json string to parse. </param>
/// <returns> A list of plugins in the APIPluginInfo format. </returns>
internal static IEnumerable<PluginInfoRecord> ParseLobbyPluginsMetadata(string json)
{
try
{
return JsonConvert.DeserializeObject<List<PluginInfoRecord>>(json, new VersionConverter()) ??
new List<PluginInfoRecord>();
}
catch (Exception e)
{
LobbyCompatibilityPlugin.Logger?.LogError("Failed to parse lobby plugins metadata.");
LobbyCompatibilityPlugin.Logger?.LogDebug(e);
throw;
}
}

/// <summary>
/// Checks if a plugin matches the version of a target, according to the source's version strictness.
/// </summary>
Expand Down
34 changes: 33 additions & 1 deletion LobbyCompatibility/Models/PluginInfoRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,36 @@ public record PluginInfoRecord(

[property:JsonIgnore]
VariableCompatibilityCheckDelegate? VariableCompatibilityCheck = null
);
) : IComparable<PluginInfoRecord>
{
[JsonIgnore]
private int? _jsonLength;

/// <summary>
/// The calculated length of the json string.
/// </summary>
[JsonIgnore]
public int JsonLength => _jsonLength ??= 25 + GUID.Length + Version.ToString().Length +
(CompatibilityLevel.HasValue ? 1 : 4) + (VersionStrictness.HasValue ? 1 : 4);

public int CompareTo(PluginInfoRecord other)
{
if (CompatibilityLevel != other.CompatibilityLevel)
{
if (CompatibilityLevel is null) return 1;
if (other.CompatibilityLevel is null) return -1;

return (other.CompatibilityLevel ?? 0) - (CompatibilityLevel ?? 0);
}

if (VersionStrictness != other.VersionStrictness)
{
if (VersionStrictness is null) return 1;
if (other.VersionStrictness is null) return -1;

return (other.VersionStrictness ?? 0) - (VersionStrictness ?? 0);
}

return string.Compare(GUID, other.GUID, StringComparison.Ordinal);
}
}
2 changes: 1 addition & 1 deletion LobbyCompatibility/Patches/LobbyDataIsJoinablePostfix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private static bool Postfix(bool isJoinable, ref Lobby lobby)
}

var matchesPluginRequirements =
PluginHelper.MatchesTargetRequirements(PluginHelper.ParseLobbyPluginsMetadata(lobbyPluginString));
PluginHelper.MatchesTargetRequirements(LobbyHelper.ParseLobbyPluginsMetadata(lobbyPluginString));

if (!matchesPluginRequirements)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ private static void Postfix(Result result, ref Lobby lobby)
// Modded is flagged as true, since we're using mods
lobby.SetData(LobbyMetadata.Modded, "true");

// Add paginated plugin metadata to the lobby so clients can check if they have the required plugins
var pluginsString = PluginHelper.GetLobbyPluginsMetadata(pluginInfo).ToArray();
// Add each page - with a delimiter if there's another page
for (var i = 0; i < pluginsString.Length; i++)
lobby.SetData($"{LobbyMetadata.Plugins}{i}", $"{pluginsString[i]}{(i < pluginsString.Length - 1 ? "@" : string.Empty)}");
// Add plugin metadata to the lobby so clients can check if they have the required plugins
var pluginsString = LobbyHelper.GetLobbyPluginsMetadata(pluginInfo);

lobby.SetData($"{LobbyMetadata.Plugins}0", pluginsString);
Comment thread
MaxWasUnavailable marked this conversation as resolved.

// Set the joinable modded metadata to the same value as the original joinable metadata, in case it wasn't originally joinable
lobby.SetData(LobbyMetadata.JoinableModded, lobby.GetData(LobbyMetadata.Joinable));
Expand Down