diff --git a/LobbyCompatibility/Features/LobbyHelper.cs b/LobbyCompatibility/Features/LobbyHelper.cs index fc26bdf..c2eaae0 100644 --- a/LobbyCompatibility/Features/LobbyHelper.cs +++ b/LobbyCompatibility/Features/LobbyHelper.cs @@ -5,6 +5,8 @@ using HarmonyLib; using LobbyCompatibility.Enums; using LobbyCompatibility.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Steamworks; using Steamworks.Data; @@ -22,6 +24,30 @@ public static class LobbyHelper private static List? _clientPlugins; public static LobbyDiff LatestLobbyDiff { get; private set; } = new(new List()); private static Dictionary LobbyDiffCache { get; } = new(); + + /// + /// 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. + /// + private const int MaxPluginMetadataLength = 7800; + + /// + /// The average json string size of a plugin. + /// + /// Calculated from `{"i":"BMX.LobbyCompatibility","v":"1.4.1","c":null,"s":null}` + private const int AveragePluginJsonLength = 60; + + /// + /// The current maximum string size for the plugin field in lobby metadata. + /// + /// WARNING: The absolute maximum string size for ALL steam lobby metadata is 8192 (2^13). + public static int CurrentMaxPluginMetadataLength { get; private set; } = MaxPluginMetadataLength; + + /// + /// 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. + /// + private static int AverageMaxLobbyMetadataModCount => CurrentMaxPluginMetadataLength / AveragePluginJsonLength; /// /// Get a from a . @@ -33,10 +59,20 @@ public static class LobbyHelper /// /// Get a from a . /// - /// The lobby to get the diff from. + /// The lobby to get the diff from. /// The from the . public static LobbyDiff GetLobbyDiff(IEnumerable> lobbyData) => GetLobbyDiff(null, null, lobbyData); + /// + /// Reserve space in the lobby metadata. + /// Useful if you need larger space for lobby metadata for your mod. + /// + /// The amount to metadata space in bytes space to reserve. The amound of bytes LobbyCompatibility will use is reduced by this amount. + public static void ReserveLobbyMetadataSpace(int length) + { + CurrentMaxPluginMetadataLength -= length; + } + /// /// Get a from a or . /// @@ -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(); @@ -143,18 +182,12 @@ internal static LobbyDiff GetLobbyDiff(Lobby? lobby, string? lobbyPluginString, /// A json from the . internal static string GetLobbyPlugins(Lobby lobby) { - var lobbyPluginStrings = new List(); - 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; } /// @@ -177,6 +210,58 @@ internal static string GetLobbyPlugins(List> lobbyD .Join(delimiter: string.Empty) .Replace("@", string.Empty); } + + /// + /// Creates a json string containing the metadata of the maximum amount of plugins, sorted by highest compatibility requirement first. + /// + /// A json strings containing the maximum allowed mod count, as dictated by . + internal static string GetLobbyPluginsMetadata(List? 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()); + } + + /// + /// Parses a json string containing the metadata of all plugins. + /// + /// The json string to parse. + /// A list of plugins in the APIPluginInfo format. + internal static IEnumerable ParseLobbyPluginsMetadata(string json) + { + try + { + return JsonConvert.DeserializeObject>(json, new VersionConverter()) ?? + new List(); + } + catch (Exception e) + { + LobbyCompatibilityPlugin.Logger?.LogError("Failed to parse lobby plugins metadata."); + LobbyCompatibilityPlugin.Logger?.LogDebug(e); + throw; + } + } public static string GetCompatibilityHeader(PluginDiffResult pluginDiffResult) { diff --git a/LobbyCompatibility/Features/PluginHelper.cs b/LobbyCompatibility/Features/PluginHelper.cs index 56ac787..2332c7d 100644 --- a/LobbyCompatibility/Features/PluginHelper.cs +++ b/LobbyCompatibility/Features/PluginHelper.cs @@ -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; @@ -21,6 +19,7 @@ namespace LobbyCompatibility.Features; /// public static class PluginHelper { + /// /// PluginInfos registered through the register command, rather than found using the attribute. /// @@ -128,47 +127,6 @@ internal static IEnumerable GetAllPluginInfo() return pluginInfos.Concat(RegisteredPluginInfoRecords); } - /// - /// Creates a list of json strings containing the metadata of all plugins, to add to the lobby. - /// - /// A list of json strings containing the metadata of all plugins. - internal static IEnumerable GetLobbyPluginsMetadata(List? 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); - } - } - - /// - /// Parses a json string containing the metadata of all plugins. - /// - /// The json string to parse. - /// A list of plugins in the APIPluginInfo format. - internal static IEnumerable ParseLobbyPluginsMetadata(string json) - { - try - { - return JsonConvert.DeserializeObject>(json, new VersionConverter()) ?? - new List(); - } - catch (Exception e) - { - LobbyCompatibilityPlugin.Logger?.LogError("Failed to parse lobby plugins metadata."); - LobbyCompatibilityPlugin.Logger?.LogDebug(e); - throw; - } - } - /// /// Checks if a plugin matches the version of a target, according to the source's version strictness. /// diff --git a/LobbyCompatibility/Models/PluginInfoRecord.cs b/LobbyCompatibility/Models/PluginInfoRecord.cs index 276afcd..e32abda 100644 --- a/LobbyCompatibility/Models/PluginInfoRecord.cs +++ b/LobbyCompatibility/Models/PluginInfoRecord.cs @@ -33,4 +33,36 @@ public record PluginInfoRecord( [property:JsonIgnore] VariableCompatibilityCheckDelegate? VariableCompatibilityCheck = null -); \ No newline at end of file +) : IComparable +{ + [JsonIgnore] + private int? _jsonLength; + + /// + /// The calculated length of the json string. + /// + [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); + } +} \ No newline at end of file diff --git a/LobbyCompatibility/Patches/LobbyDataIsJoinablePostfix.cs b/LobbyCompatibility/Patches/LobbyDataIsJoinablePostfix.cs index bb34233..80c6d8d 100644 --- a/LobbyCompatibility/Patches/LobbyDataIsJoinablePostfix.cs +++ b/LobbyCompatibility/Patches/LobbyDataIsJoinablePostfix.cs @@ -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) { diff --git a/LobbyCompatibility/Patches/SteamMatchmakingOnLobbyCreatedPostfix.cs b/LobbyCompatibility/Patches/SteamMatchmakingOnLobbyCreatedPostfix.cs index 46ea4c7..57cefa4 100644 --- a/LobbyCompatibility/Patches/SteamMatchmakingOnLobbyCreatedPostfix.cs +++ b/LobbyCompatibility/Patches/SteamMatchmakingOnLobbyCreatedPostfix.cs @@ -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); // 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));