diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs index b150e01a44..0af6302754 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs @@ -28,12 +28,28 @@ public class ButtonSetting : SettingBase, IWrapper /// /// /// - public ButtonSetting(int id, string label, string buttonText, float holdTime = 0.0f, string hintDescription = null, HeaderSetting header = null, Action onChanged = null) + [Obsolete("Use the constructor without the Header instead.")] + public ButtonSetting(int id, string label, string buttonText, float holdTime, string hintDescription, HeaderSetting header, Action onChanged) : base(new SSButton(id, label, buttonText, holdTime, hintDescription), header, onChanged) { Base = (SSButton)base.Base; } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + public ButtonSetting(int id, string label, string buttonText, float holdTime = 0.0f, string hintDescription = null, Action onChanged = null) + : base(new SSButton(id, label, buttonText, holdTime, hintDescription), onChanged) + { + Base = (SSButton)base.Base; + } + /// /// Initializes a new instance of the class. /// diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs index 88886839bd..557cee49a3 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs @@ -30,6 +30,31 @@ public class DropdownSetting : SettingBase, IWrapper /// /// /// + [Obsolete("Use the constructor without the Header instead.")] + public DropdownSetting( + int id, + string label, + IEnumerable options, + int defaultOptionIndex, + SSDropdownSetting.DropdownEntryType dropdownEntryType, + string hintDescription, + HeaderSetting header, + Action onChanged) + : base(new SSDropdownSetting(id, label, options.ToArray(), defaultOptionIndex, dropdownEntryType, hintDescription), header, onChanged) + { + Base = (SSDropdownSetting)base.Base; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// public DropdownSetting( int id, string label, @@ -37,9 +62,8 @@ public DropdownSetting( int defaultOptionIndex = 0, SSDropdownSetting.DropdownEntryType dropdownEntryType = SSDropdownSetting.DropdownEntryType.Regular, string hintDescription = null, - HeaderSetting header = null, Action onChanged = null) - : base(new SSDropdownSetting(id, label, options.ToArray(), defaultOptionIndex, dropdownEntryType, hintDescription), header, onChanged) + : base(new SSDropdownSetting(id, label, options.ToArray(), defaultOptionIndex, dropdownEntryType, hintDescription), onChanged) { Base = (SSDropdownSetting)base.Base; } diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs index cfb6b3bbca..6272b40a50 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs @@ -28,12 +28,28 @@ public class KeybindSetting : SettingBase, IWrapper /// /// /// - public KeybindSetting(int id, string label, KeyCode suggested, bool preventInteractionOnGUI = false, string hintDescription = "", HeaderSetting header = null, Action onChanged = null) + [Obsolete("Use the constructor without the Header instead.")] + public KeybindSetting(int id, string label, KeyCode suggested, bool preventInteractionOnGUI, string hintDescription, HeaderSetting header, Action onChanged) : base(new SSKeybindSetting(id, label, suggested, preventInteractionOnGUI, hintDescription), header, onChanged) { Base = (SSKeybindSetting)base.Base; } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + public KeybindSetting(int id, string label, KeyCode suggested, bool preventInteractionOnGUI = false, string hintDescription = "", Action onChanged = null) + : base(new SSKeybindSetting(id, label, suggested, preventInteractionOnGUI, hintDescription), onChanged) + { + Base = (SSKeybindSetting)base.Base; + } + /// /// Initializes a new instance of the class. /// diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs b/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs index 2cbc85e645..6ceb6ea94e 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs @@ -31,12 +31,18 @@ public class SettingBase : TypeCastObject, IWrapper internal static readonly List Settings = new(); + /// + /// A collection that contains all groups of settings that will be sent to clients. + /// + internal static readonly List Groups = new() { SettingGroup.ObsoleteGroup }; + /// /// Initializes a new instance of the class. /// /// A instance. /// /// + [Obsolete("Use the constructor without the Header instead.")] internal SettingBase(ServerSpecificSettingBase settingBase, HeaderSetting header, Action onChanged) { Base = settingBase; @@ -45,6 +51,18 @@ internal SettingBase(ServerSpecificSettingBase settingBase, HeaderSetting header OnChanged = onChanged; } + /// + /// Initializes a new instance of the class. + /// + /// A instance. + /// + internal SettingBase(ServerSpecificSettingBase settingBase, Action onChanged) + { + Base = settingBase; + + OnChanged = onChanged; + } + /// /// Initializes a new instance of the class. /// @@ -55,7 +73,9 @@ internal SettingBase(ServerSpecificSettingBase settingBase) if (OriginalDefinition != null) { +#pragma warning disable CS0618 // Type or member is obsolete Header = OriginalDefinition.Header; +#pragma warning restore CS0618 // Type or member is obsolete OnChanged = OriginalDefinition.OnChanged; Label = OriginalDefinition.Label; HintDescription = OriginalDefinition.HintDescription; @@ -73,9 +93,15 @@ public static IReadOnlyDictionary> Synce /// public static IReadOnlyCollection List => Settings; + /// + /// Gets the list of setting groups that are registered. + /// + public static IReadOnlyCollection AllGroups => Groups; + /// /// Gets or sets the predicate for syncing this setting when a player joins. /// + [Obsolete("Use SettingGroup::Viewers instead of SettingBase.SyncOnJoin")] public static Predicate SyncOnJoin { get; set; } /// @@ -123,6 +149,7 @@ public string HintDescription /// Gets or sets the header of this setting. /// /// Can be null. + [Obsolete("Use SettingGroup instead")] public HeaderSetting Header { get; set; } /// @@ -197,7 +224,13 @@ public static T Create(ServerSpecificSettingBase settingBase) /// /// Syncs setting with all players. /// - public static void SendToAll() => ServerSpecificSettingsSync.SendToAll(); + public static void SendToAll() + { + foreach (Player player in Player.List) + { + SendToPlayer(player); + } + } /// /// Syncs setting with all players according to the specified predicate. @@ -216,7 +249,11 @@ public static void SendToAll(Func predicate) /// Syncs setting with the specified target. /// /// Target player. - public static void SendToPlayer(Player player) => ServerSpecificSettingsSync.SendToPlayer(player.ReferenceHub); + public static void SendToPlayer(Player player) + { + ServerSpecificSettingBase[] sorted = Groups.OrderByDescending(group => group.Priority).SelectMany(group => group.GetViewableSettingsOrdered(player)).Select(setting => setting.Base).ToArray(); + ServerSpecificSettingsSync.SendToPlayer(player.ReferenceHub, sorted); + } /// /// Syncs specific settings with the specified target. @@ -233,6 +270,7 @@ public static void SendToPlayer(Player player, IEnumerable settings /// A requirement to meet when sending settings to players. /// A of instances that were successfully registered. /// This method is used to sync new settings with players. + [Obsolete("Use RegisterGroups instead")] public static IEnumerable Register(IEnumerable settings, Func predicate = null) { IEnumerable> grouped = settings.Where(s => s != null).GroupBy(s => s.Header); @@ -250,6 +288,7 @@ public static IEnumerable Register(IEnumerable setting ServerSpecificSettingsSync.DefinedSettings = (ServerSpecificSettingsSync.DefinedSettings ?? Array.Empty()).Concat(result.Select(s => s.Base)).ToArray(); Settings.AddRange(result); + SettingGroup.ObsoleteGroup.Settings.AddRange(result); if (predicate == null) SendToAll(); @@ -259,6 +298,39 @@ public static IEnumerable Register(IEnumerable setting return result; } + /// + /// Registers all settings from the specified collection. + /// + /// A collection of groups of settings to register. + /// A requirement to meet when sending settings to players after registering all groups. + /// A of instances that were successfully registered. + /// This method is used to sync new settings with players. + public static IEnumerable RegisterGroups(IEnumerable settings, Predicate predicate = null) + { + settings = settings.Where(group => group != null).ToArray(); + SettingBase[] sorted = settings.OrderByDescending(group => group.Priority).SelectMany(group => group.GetAllSettings()).ToArray(); + ServerSpecificSettingBase[] sent = sorted.Select(setting => setting.Base).ToArray(); + ServerSpecificSettingsSync.DefinedSettings = (ServerSpecificSettingsSync.DefinedSettings ?? Array.Empty()).Concat(sent).ToArray(); + Settings.AddRange(sorted); + Groups.AddRange(settings); + if (predicate == null) + { + SendToAll(); + } + else + { + foreach (Player player in Player.List) + { + if (predicate(player)) + { + SendToPlayer(player); + } + } + } + + return sorted; + } + /// /// Registers all settings from the specified collection to player. /// @@ -266,6 +338,7 @@ public static IEnumerable Register(IEnumerable setting /// A collection of settings to register. /// A of instances that were successfully registered. /// This method is used to sync new settings with players. + [Obsolete("Use RegisterGroups instead")] public static IEnumerable Register(Player player, IEnumerable settings) { IEnumerable> grouped = settings.Where(s => s != null).GroupBy(s => s.Header); @@ -283,6 +356,7 @@ public static IEnumerable Register(Player player, IEnumerable()).Concat(result.Select(s => s.Base)).ToArray(); Settings.AddRange(result); + SettingGroup.ObsoleteGroup.Settings.AddRange(result); SendToPlayer(player); @@ -296,10 +370,42 @@ public static IEnumerable Register(Player player, IEnumerableSettings to remove. If null, all settings will be removed. /// A of instances that were successfully removed. /// This method is used to unsync settings from players. Using it with provides an opportunity to update synced settings. + [Obsolete("Use SettingGroups instead")] public static IEnumerable Unregister(Func predicate = null, IEnumerable settings = null) { List list = ListPool.Pool.Get(ServerSpecificSettingsSync.DefinedSettings); List list2 = new((settings ?? Settings).Where(setting => list.Remove(setting.Base))); + SettingGroup.ObsoleteGroup.Settings.RemoveAll(setting => (settings ?? Settings).Contains(setting)); + + ServerSpecificSettingsSync.DefinedSettings = list.ToArray(); + + if (predicate == null) + SendToAll(); + else + SendToAll(predicate); + + ListPool.Pool.Return(list); + + return list2; + } + + /// + /// Removes settings from players. + /// + /// Determines which players will receive this update. + /// Groups to remove. If null, all settings will be removed. + /// A of instances that were successfully removed. + /// This method is used to unsync settings from players. Using it with provides an opportunity to update synced settings. + public static IEnumerable UnregisterGroups(Func predicate = null, IEnumerable groups = null) + { + List list = ListPool.Pool.Get(ServerSpecificSettingsSync.DefinedSettings); + SettingGroup[] settingGroups = groups?.ToArray(); + List list2 = new((settingGroups?.SelectMany(group => group.GetAllSettings()) ?? Settings).Where(setting => list.Remove(setting.Base))); + + if (groups == null) + Groups.Clear(); + else + Groups.RemoveAll(settingGroups.Contains); ServerSpecificSettingsSync.DefinedSettings = list.ToArray(); @@ -319,11 +425,13 @@ public static IEnumerable Unregister(Func predicate = /// Determines which player will receive this update. /// Settings to remove. If null, all settings will be removed. /// A of instances that were successfully removed. - /// This method is used to unsync settings from players. Using it with provides an opportunity to update synced settings. + /// /// This method is used to unsync settings from players. Using it with provides an opportunity to update synced settings. + [Obsolete("Use SettingGroups instead")] public static IEnumerable Unregister(Player player, IEnumerable settings = null) { List list = ListPool.Pool.Get(ServerSpecificSettingsSync.DefinedSettings); List list2 = new((settings ?? Settings).Where(setting => list.Remove(setting.Base))); + SettingGroup.ObsoleteGroup.Settings.RemoveAll(setting => (settings ?? Settings).Contains(setting)); ServerSpecificSettingsSync.DefinedSettings = list.ToArray(); @@ -340,7 +448,9 @@ public static IEnumerable Unregister(Player player, IEnumerableA string in human-readable format. public override string ToString() { +#pragma warning disable CS0618 // Type or member is obsolete return $"{Id} ({Label}) [{HintDescription}] {{{ResponseMode}}} ^{Header}^"; +#pragma warning restore CS0618 // Type or member is obsolete } /// diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/SettingGroup.cs b/EXILED/Exiled.API/Features/Core/UserSettings/SettingGroup.cs new file mode 100644 index 0000000000..1b544b8e0d --- /dev/null +++ b/EXILED/Exiled.API/Features/Core/UserSettings/SettingGroup.cs @@ -0,0 +1,163 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Core.UserSettings +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Pools; + + /// + /// Represents a group of SettingBase. + /// + public class SettingGroup + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public SettingGroup(IEnumerable settings, int priority = 0, Predicate viewers = null, IEnumerable subgroups = null) + { + Settings = settings.ToList(); + Priority = priority; + Viewers = viewers; + SubGroups = subgroups?.ToList(); + } + + /// + /// Gets or sets the settings within this group. + /// + public List Settings { get; set; } + + /// + /// Gets or sets the priority of this group when sent to players. + /// + public int Priority { get; set; } + + /// + /// Gets or sets the predicate which determines who can see this group of settings. + /// + public Predicate Viewers { get; set; } + + /// + /// Gets or sets the subgroups within this group. + /// + public List SubGroups { get; set; } + + /// + /// Gets a SettingGroup that all values from obsolete SettingBase methods register to. Helps track unregistering settings. + /// + internal static SettingGroup ObsoleteGroup { get; } = new(new SettingBase[] { }); + + /// + /// Handles constructing a group containing a singular setting. Useful for standalone settings. + /// + /// A instance. + /// A SettingGroup containing the singular setting. + public static implicit operator SettingGroup(SettingBase setting) => new(new[] { setting }); + + // These two methods are required to avoid implicit IEnumerable conversions. They are used to make the registration of settings easier. The true use of these will likely only be utilized once the obsolete code is removed. + + /// + /// Handles implicitly converting a List of SettingBase into a SettingGroup. + /// + /// A of . + /// A SettingGroup containing all the SettingBases. + public static implicit operator SettingGroup(List settings) => new(settings); + + /// + /// Handles implicitly converting an Array of SettingBase into a SettingGroup. + /// + /// An of . + /// A SettingGroup containing all the SettingBases. + public static implicit operator SettingGroup(SettingBase[] settings) => new(settings); + + /// + /// Recursively gets all instances of contained within this 's Settings and SubGroups. + /// + /// An of . + public IEnumerable GetAllSettings() + { + List settings = new(); + List previousGroups = ListPool.Pool.Get(1); + InternalGetAllSettings(settings, previousGroups); + ListPool.Pool.Return(previousGroups); + return settings; + } + + /// + /// Recursively gets all instances of contained within this 's Settings and SubGroups that can be viewed by a specified player. + /// + /// + /// An of . + public IEnumerable GetViewableSettingsOrdered(Player viewer) + { + if (viewer == null) + return Array.Empty(); + List settings = new(); + List previousGroups = ListPool.Pool.Get(1); + InternalGetViewableSettingsOrdered(settings, previousGroups, viewer); + ListPool.Pool.Return(previousGroups); + return settings; + } + + /// + /// Returns a string representation of this . + /// + /// A string in human-readable format. + public override string ToString() => $"{Priority} ({Viewers}) [{string.Join(", ", Settings.Select(s => s.ToString()))}]"; + + /// + /// An internal recursive method to get all settings within a SettingGroup. + /// + /// A of to add new settings to. + /// A of used to verify no recursive loops. + internal void InternalGetAllSettings(List current, List previousGroups) + { + current.AddRange(Settings); + if (SubGroups == null) + return; + previousGroups.Add(this); + foreach (SettingGroup group in SubGroups) + { + if (previousGroups.Contains(group)) + throw new InvalidOperationException("SettingGroups cannot reference themselves within subgroups."); + group.InternalGetAllSettings(current, previousGroups); + } + + previousGroups.Remove(this); + } + + /// + /// An internal recursive method to get all settings viewable by a specified player, ordered, within a SettingGroup. + /// + /// A of to add new settings to. + /// A of used to verify no recursive loops. + /// A that filters the settings. + internal void InternalGetViewableSettingsOrdered(List current, List previousGroups, Player viewer) + { + if (Viewers == null || Viewers(viewer)) + current.AddRange(Settings); + if (SubGroups == null) + return; + previousGroups.Add(this); + foreach (SettingGroup group in SubGroups.Where(group => group.Viewers == null || group.Viewers(viewer)).OrderByDescending(group => group.Priority)) + { + if (previousGroups.Contains(group)) + throw new InvalidOperationException("SettingGroups cannot reference themselves within subgroups."); + group.InternalGetViewableSettingsOrdered(current, previousGroups, viewer); + } + + previousGroups.Remove(this); + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs index 69c7875ad1..31cf34ba2a 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs @@ -28,15 +28,37 @@ public class TextInputSetting : SettingBase, IWrapper /// /// /// + [Obsolete("Use the constructor without the Header instead.")] + public TextInputSetting( + int id, + string label, + SSTextArea.FoldoutMode foldoutMode, + TextAlignmentOptions alignment, + string hintDescription, + HeaderSetting header, + Action onChanged) + : base(new SSTextArea(id, label, foldoutMode, hintDescription, alignment), header, onChanged) + { + Base = (SSTextArea)base.Base; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// public TextInputSetting( int id, string label, SSTextArea.FoldoutMode foldoutMode = SSTextArea.FoldoutMode.NotCollapsable, TextAlignmentOptions alignment = TextAlignmentOptions.TopLeft, string hintDescription = null, - HeaderSetting header = null, Action onChanged = null) - : base(new SSTextArea(id, label, foldoutMode, hintDescription, alignment), header, onChanged) + : base(new SSTextArea(id, label, foldoutMode, hintDescription, alignment), onChanged) { Base = (SSTextArea)base.Base; } diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs index 52c596ae76..42ea36e74b 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs @@ -28,12 +28,29 @@ public class TwoButtonsSetting : SettingBase, IWrapper /// /// /// - public TwoButtonsSetting(int id, string label, string firstOption, string secondOption, bool defaultIsSecond = false, string hintDescription = "", HeaderSetting header = null, Action onChanged = null) + [Obsolete("Use the constructor without the Header instead.")] + public TwoButtonsSetting(int id, string label, string firstOption, string secondOption, bool defaultIsSecond, string hintDescription, HeaderSetting header, Action onChanged) : base(new SSTwoButtonsSetting(id, label, firstOption, secondOption, defaultIsSecond, hintDescription), header, onChanged) { Base = (SSTwoButtonsSetting)base.Base; } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + public TwoButtonsSetting(int id, string label, string firstOption, string secondOption, bool defaultIsSecond = false, string hintDescription = "", Action onChanged = null) + : base(new SSTwoButtonsSetting(id, label, firstOption, secondOption, defaultIsSecond, hintDescription), onChanged) + { + Base = (SSTwoButtonsSetting)base.Base; + } + /// /// Initializes a new instance of the class. /// diff --git a/EXILED/Exiled.Events/Handlers/Internal/Round.cs b/EXILED/Exiled.Events/Handlers/Internal/Round.cs index 99212af062..5a77da2a49 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/Round.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/Round.cs @@ -97,8 +97,12 @@ public static void OnVerified(VerifiedEventArgs ev) { RoleAssigner.CheckLateJoin(ev.Player.ReferenceHub, ClientInstanceMode.ReadyClient); +#pragma warning disable CS0618 // Type or member is obsolete if (SettingBase.SyncOnJoin != null && SettingBase.SyncOnJoin(ev.Player)) SettingBase.SendToPlayer(ev.Player); + else if (SettingBase.SyncOnJoin == null) + SettingBase.SendToPlayer(ev.Player); +#pragma warning restore CS0618 // Type or member is obsolete // TODO: Remove if this has been fixed for https://git.scpslgame.com/northwood-qa/scpsl-bug-reporting/-/issues/52 foreach (Room room in Room.List.Where(current => current.AreLightsOff))