diff --git a/SecretAPI/Features/UserSettings/CustomButtonSetting.cs b/SecretAPI/Features/UserSettings/CustomButtonSetting.cs new file mode 100644 index 0000000..9e12c26 --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomButtonSetting.cs @@ -0,0 +1,61 @@ +namespace SecretAPI.Features.UserSettings +{ + using System; + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + + /// + /// Wraps . + /// + public abstract class CustomButtonSetting : CustomSetting, ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The button base. + protected CustomButtonSetting(SSButton button) + : base(button) + { + Base = button; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the button. + /// The setting's label. + /// The button text. + /// The time to hold. + /// The hint to show. + protected CustomButtonSetting(int? id, string label, string buttonText, float? holdTimeSeconds = null, string? hint = null) + : this(new SSButton(id, label, buttonText, holdTimeSeconds, hint)) + { + } + + /// + public new SSButton Base { get; } + + /// + /// Gets the of the last press. + /// + public TimeSpan LastPress => Base.SyncLastPress.Elapsed; + + /// + /// Gets or sets the text of the button. + /// + public string Text + { + get => Base.ButtonText; + set => Base.ButtonText = value; + } + + /// + /// Gets or sets the amount of time to hold the button in seconds. + /// + public float HoldTime + { + get => Base.HoldTimeSeconds; + set => Base.HoldTimeSeconds = value; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomDropdownSetting.cs b/SecretAPI/Features/UserSettings/CustomDropdownSetting.cs new file mode 100644 index 0000000..7f7d3e1 --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomDropdownSetting.cs @@ -0,0 +1,58 @@ +namespace SecretAPI.Features.UserSettings +{ + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + + /// + /// Custom wrapper. + /// + public abstract class CustomDropdownSetting : CustomSetting, ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The base setting to create the wrapper with. + protected CustomDropdownSetting(SSDropdownSetting setting) + : base(setting) + { + Base = setting; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the setting. + /// The setting's label. + /// The array of string options to give. + /// The default option (int index). + /// The entry type. + /// The hint to show. + protected CustomDropdownSetting( + int? id, + string label, + string[] options, + int defaultOptionIndex = 0, + SSDropdownSetting.DropdownEntryType entryType = SSDropdownSetting.DropdownEntryType.Regular, + string? hint = null) + : this(new SSDropdownSetting(id, label, options, defaultOptionIndex, entryType, hint)) + { + } + + /// + public new SSDropdownSetting Base { get; } + + /// + /// Gets or sets the options. + /// + public string[] Options + { + get => Base.Options; + set => Base.Options = value; + } + + /// + /// Gets the selected option as string. + /// + public string SelectedOption => Base.SyncSelectionText; + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomHeader.cs b/SecretAPI/Features/UserSettings/CustomHeader.cs new file mode 100644 index 0000000..8fe2437 --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomHeader.cs @@ -0,0 +1,35 @@ +namespace SecretAPI.Features.UserSettings +{ + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + + /// + /// Wraps . + /// + public class CustomHeader : ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The label to show. + /// Reduced padding. + /// Hint displayed. + public CustomHeader(string label, bool reducedPadding = false, string? hint = null) + { + Base = new SSGroupHeader(label, reducedPadding, hint); + } + + /// + /// Gets a for Gameplay purposes. + /// + public static CustomHeader Gameplay { get; } = new("Gameplay"); + + /// + /// Gets a for Example purposes. + /// + public static CustomHeader Examples { get; } = new("Examples"); + + /// + public SSGroupHeader Base { get; } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomKeybindSetting.cs b/SecretAPI/Features/UserSettings/CustomKeybindSetting.cs new file mode 100644 index 0000000..2a78ba6 --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomKeybindSetting.cs @@ -0,0 +1,43 @@ +namespace SecretAPI.Features.UserSettings +{ + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + using UnityEngine; + + /// + /// Wrapper for . + /// + public abstract class CustomKeybindSetting : CustomSetting, ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The setting to wrap. + protected CustomKeybindSetting(SSKeybindSetting setting) + : base(setting) + { + Base = setting; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the setting. + /// The setting's label. + /// The suggested key. + /// Whether to prevent interaction in a GUI. + /// The hint to show. + protected CustomKeybindSetting( + int? id, + string label, + KeyCode suggestedKey = KeyCode.None, + bool preventInteractionOnGui = true, + string? hint = null) + : this(new SSKeybindSetting(id, label, suggestedKey, preventInteractionOnGui, hint)) + { + } + + /// + public new SSKeybindSetting Base { get; } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomPlainTextSetting.cs b/SecretAPI/Features/UserSettings/CustomPlainTextSetting.cs new file mode 100644 index 0000000..619f0ca --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomPlainTextSetting.cs @@ -0,0 +1,81 @@ +namespace SecretAPI.Features.UserSettings +{ + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + using TMPro; + + /// + /// Wrapper for . + /// + public abstract class CustomPlainTextSetting : CustomSetting, ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The setting to create wrapper from. + protected CustomPlainTextSetting(SSPlaintextSetting setting) + : base(setting) + { + Base = setting; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the setting. + /// The setting's label. + /// The placeholder to use for the setting. + /// The max allowed characters. + /// The content type. + /// The hint to display for the setting. + protected CustomPlainTextSetting( + int? id, + string label, + string placeholder = "...", + int characterLimit = 64, + TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard, + string? hint = null) + : this(new SSPlaintextSetting(id, label, placeholder, characterLimit, contentType, hint)) + { + } + + /// + public new SSPlaintextSetting Base { get; } + + /// + /// Gets or sets the synced input text. + /// + public string InputText + { + get => Base.SyncInputText; + set => Base.SyncInputText = value; + } + + /// + /// Gets or sets the content type. + /// + public TMP_InputField.ContentType ContentType + { + get => Base.ContentType; + set => Base.ContentType = value; + } + + /// + /// Gets or sets the placeholder. + /// + public string Placeholder + { + get => Base.Placeholder; + set => Base.Placeholder = value; + } + + /// + /// Gets or sets the character limit. + /// + public int CharacterLimit + { + get => Base.CharacterLimit; + set => Base.CharacterLimit = value; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomSetting.cs b/SecretAPI/Features/UserSettings/CustomSetting.cs new file mode 100644 index 0000000..ce44a21 --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomSetting.cs @@ -0,0 +1,167 @@ +namespace SecretAPI.Features.UserSettings +{ + using System; + using System.Collections.Generic; + using System.Linq; + using global::UserSettings.ServerSpecific; + using LabApi.Features.Wrappers; + using Mirror; + using SecretAPI.Extensions; + using SecretAPI.Interfaces; + + /// + /// Wraps . + /// + public abstract class CustomSetting : ISetting + { + private static readonly Dictionary> ReceivedPlayerSettings = []; + + static CustomSetting() + { + SecretApi.Harmony?.PatchCategory(nameof(CustomSetting)); + + ServerSpecificSettingsSync.SendOnJoinFilter = null; + LabApi.Events.Handlers.PlayerEvents.Joined += ev => SendSettingsToPlayer(ev.Player); + LabApi.Events.Handlers.PlayerEvents.Left += ev => RemoveStoredPlayer(ev.Player); + LabApi.Events.Handlers.PlayerEvents.GroupChanged += ev => SendSettingsToPlayer(ev.Player); + ServerSpecificSettingsSync.ServerOnSettingValueReceived += OnSettingsUpdated; + } + + /// + /// Initializes a new instance of the class. + /// + /// The setting to use for custom setting. + protected CustomSetting(ServerSpecificSettingBase setting) + { + Base = setting; + } + + /// + /// Gets the registered custom settings. + /// + public static List CustomSettings { get; } = []; + + /// + /// Gets a dictionary of player to their received custom settings. + /// + public static IReadOnlyDictionary> PlayerSettings => ReceivedPlayerSettings; + + /// + public ServerSpecificSettingBase Base { get; } + + /// + /// Gets the of the setting. + /// + public abstract CustomHeader Header { get; } + + /// + /// Gets or sets the current label. + /// + public string Label + { + get => Base.Label; + set => Base.Label = value; + } + + /// + /// Gets or sets the current id. + /// + public int Id + { + get => Base.SettingId; + set => Base.SettingId = value; + } + + /// + /// Registers a collection of settings. + /// + /// The settings to register. + public static void Register(params CustomSetting[] settings) => CustomSettings.AddRange(settings); + + /// + /// Registers a collection of settings. + /// + /// The settings to register. + public static void Register(IEnumerable settings) => CustomSettings.AddRange(settings); + + /// + /// Gets a , used for validation. + /// + /// The type of the base setting. + /// The id of the setting. + /// The found matching the params, otherwise null. + public static CustomSetting? Get(Type type, int id) + => CustomSettings.FirstOrDefault(s => s.Base.SettingId == id && s.Base.GetType() == type); + + /// + /// Checks if a player is able to view a setting. + /// + /// The player to check. + /// A value indicating whether a player is able to view the setting. + protected virtual bool CanView(Player player) => true; + + /// + /// Creates a duplicate of the current setting. Used to properly sync values and implement . + /// + /// The duplicate setting created. + protected abstract CustomSetting CreateDuplicate(); + + /// + /// Handles the updating of a setting. + /// + /// The player to update. + protected abstract void HandleSettingUpdate(Player player); + + private static void RemoveStoredPlayer(Player player) => ReceivedPlayerSettings.Remove(player); + + private static void SendSettingsToPlayer(Player player, int version = 1) + { + IEnumerable hasAccess = CustomSettings.Where(s => s.CanView(player)); + List ordered = []; + foreach (IGrouping grouping in hasAccess.GroupBy(setting => setting.Header)) + { + ordered.Add(grouping.Key.Base); + ordered.AddRange(grouping.Select(setting => setting.Base)); + } + + ordered.AddRange(ServerSpecificSettingsSync.DefinedSettings); + + ServerSpecificSettingsSync.SendToPlayer(player.ReferenceHub, [.. ordered], version); + } + + private static void OnSettingsUpdated(ReferenceHub hub, ServerSpecificSettingBase settingBase) + { + if (hub.IsHost) + return; + + Player player = Player.Get(hub); + + CustomSetting? setting = CustomSettings.FirstOrDefault(s => s.Base.SettingId == settingBase.SettingId); + if (setting == null || !setting.CanView(player)) + return; + + CustomSetting newSettingPlayer = EnsurePlayerSpecificSetting(player, setting); + + NetworkWriter entryWriter = new(); + NetworkWriter valueWriter = new(); + settingBase.SerializeEntry(entryWriter); + settingBase.SerializeValue(valueWriter); + newSettingPlayer.Base.DeserializeEntry(new NetworkReader(entryWriter.buffer)); + newSettingPlayer.Base.DeserializeValue(new NetworkReader(valueWriter.buffer)); + newSettingPlayer.HandleSettingUpdate(player); + } + + private static CustomSetting EnsurePlayerSpecificSetting(Player player, CustomSetting toMatch) + { + List settings = ReceivedPlayerSettings.GetOrAdd(player, () => []); + CustomSetting? currentSetting = settings.FirstOrDefault(s => s.Id == toMatch.Id); + if (currentSetting == null) + { + currentSetting = toMatch.CreateDuplicate(); + settings.Add(currentSetting); + } + + return currentSetting; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomSliderSetting.cs b/SecretAPI/Features/UserSettings/CustomSliderSetting.cs new file mode 100644 index 0000000..2b53a1f --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomSliderSetting.cs @@ -0,0 +1,87 @@ +namespace SecretAPI.Features.UserSettings +{ + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + + /// + /// Wrapper for . + /// + public abstract class CustomSliderSetting : CustomSetting, ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The setting to wrap. + protected CustomSliderSetting(SSSliderSetting setting) + : base(setting) + { + Base = setting; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the setting. + /// The setting's label. + /// The slider's minimum value. + /// The slider's maximum value. + /// The default value for the slider. + /// Whether it should be an integer (false for float). + /// Value to string format. + /// The final display format. + /// The hint to display. + protected CustomSliderSetting( + int? id, + string label, + float minValue, + float maxValue, + float defaultValue = 0.0f, + bool integer = false, + string valueToStringFormat = "0.##", + string finalDisplayFormat = "{0}", + string? hint = null) + : this(new SSSliderSetting(id, label, minValue, maxValue, defaultValue, integer, valueToStringFormat, finalDisplayFormat, hint)) + { + } + + /// + public new SSSliderSetting Base { get; } + + /// + /// Gets the synced value selected as a float. + /// + public float SelectedValueFloat => Base.SyncFloatValue; + + /// + /// Gets the synced value selected as an integer. + /// + public int SelectedValueInt => Base.SyncIntValue; + + /// + /// Gets or sets the minimum value of the setting. + /// + public float MinimumValue + { + get => Base.MinValue; + set => Base.MinValue = value; + } + + /// + /// Gets or sets the maximum value of the setting. + /// + public float MaximumValue + { + get => Base.MaxValue; + set => Base.MaxValue = value; + } + + /// + /// Gets or sets the default value of the setting. + /// + public float DefaultValue + { + get => Base.DefaultValue; + set => Base.DefaultValue = value; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomTextAreaSetting.cs b/SecretAPI/Features/UserSettings/CustomTextAreaSetting.cs new file mode 100644 index 0000000..1a5c536 --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomTextAreaSetting.cs @@ -0,0 +1,43 @@ +namespace SecretAPI.Features.UserSettings +{ + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + using TMPro; + + /// + /// Wrapper for . + /// + public abstract class CustomTextAreaSetting : CustomSetting, ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The setting to wrap. + protected CustomTextAreaSetting(SSTextArea setting) + : base(setting) + { + Base = setting; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the setting. + /// The content of the setting. + /// The foldout mode. + /// The collapsed text. + /// The align for the text. + protected CustomTextAreaSetting( + int? id, + string content, + SSTextArea.FoldoutMode foldoutMode = SSTextArea.FoldoutMode.NotCollapsable, + string? collapsedText = null, + TextAlignmentOptions textAlignment = TextAlignmentOptions.TopLeft) + : this(new SSTextArea(id, content, foldoutMode, collapsedText, textAlignment)) + { + } + + /// + public new SSTextArea Base { get; } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/CustomTwoButtonSetting.cs b/SecretAPI/Features/UserSettings/CustomTwoButtonSetting.cs new file mode 100644 index 0000000..73fb40e --- /dev/null +++ b/SecretAPI/Features/UserSettings/CustomTwoButtonSetting.cs @@ -0,0 +1,53 @@ +namespace SecretAPI.Features.UserSettings +{ + using global::UserSettings.ServerSpecific; + using SecretAPI.Interfaces; + + /// + /// Wrapper for . + /// + public abstract class CustomTwoButtonSetting : CustomSetting, ISetting + { + /// + /// Initializes a new instance of the class. + /// + /// The setting to wrap. + protected CustomTwoButtonSetting(SSTwoButtonsSetting button) + : base(button) + { + Base = button; + } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the setting. + /// The setting's label. + /// The first option. + /// The second option. + /// Whether the second option should be default. Default: false. + /// The hint to show. + protected CustomTwoButtonSetting(int? id, string label, string optionA, string optionB, bool defaultIsB = false, string? hint = null) + : this(new SSTwoButtonsSetting(id, label, optionA, optionB, defaultIsB, hint)) + { + } + + /// + public new SSTwoButtonsSetting Base { get; } + + /// + /// Gets a value indicating whether the selected option is currently the first. + /// + public bool IsOptionA => Base.SyncIsA; + + /// + /// Gets a value indicating whether the selected option is currently the second. + /// + public bool IsOptionB => Base.SyncIsB; + + /// + /// Gets a value indicating whether the selected option is currently the default. + /// + public bool IsDefault => Base.DefaultIsB ? IsOptionB : IsOptionA; + } +} \ No newline at end of file diff --git a/SecretAPI/Features/UserSettings/ExampleSetting.cs b/SecretAPI/Features/UserSettings/ExampleSetting.cs new file mode 100644 index 0000000..51d4843 --- /dev/null +++ b/SecretAPI/Features/UserSettings/ExampleSetting.cs @@ -0,0 +1,31 @@ +namespace SecretAPI.Features.UserSettings +{ + using LabApi.Features.Wrappers; + using UnityEngine; + + /// + /// Example setting to use during testing. + /// + internal class ExampleSetting : CustomKeybindSetting + { + /// + /// Initializes a new instance of the class. + /// + public ExampleSetting() + : base(900, "Example Kill Button", KeyCode.G) + { + } + + /// + public override CustomHeader Header { get; } = CustomHeader.Examples; + + /// + protected override CustomSetting CreateDuplicate() => new ExampleSetting(); + + /// + protected override void HandleSettingUpdate(Player player) + { + player.Kill(); + } + } +} \ No newline at end of file diff --git a/SecretAPI/Interfaces/ISetting.cs b/SecretAPI/Interfaces/ISetting.cs new file mode 100644 index 0000000..18e9889 --- /dev/null +++ b/SecretAPI/Interfaces/ISetting.cs @@ -0,0 +1,17 @@ +namespace SecretAPI.Interfaces +{ + using UserSettings.ServerSpecific; + + /// + /// Handles . + /// + /// The setting being wrapped. + public interface ISetting + where T : ServerSpecificSettingBase + { + /// + /// Gets the base of the setting. + /// + public T Base { get; } + } +} \ No newline at end of file diff --git a/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs b/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs new file mode 100644 index 0000000..08c2ad5 --- /dev/null +++ b/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs @@ -0,0 +1,25 @@ +namespace SecretAPI.Patches.Features +{ + using HarmonyLib; + using SecretAPI.Attribute; + using SecretAPI.Features.UserSettings; + using UserSettings.ServerSpecific; + + /// + /// Fixes on custom settings. + /// + [HarmonyPatchCategory(nameof(CustomSetting))] + [HarmonyPatch(typeof(ServerSpecificSettingBase), nameof(ServerSpecificSettingBase.OriginalDefinition), MethodType.Getter)] + internal static class SettingsOriginalDefinitionFix + { +#pragma warning disable SA1313 + private static void Postfix(ServerSpecificSettingBase __instance, ref ServerSpecificSettingBase __result) +#pragma warning restore SA1313 + { + if (__result != null) + return; + + __result = CustomSetting.Get(__instance.GetType(), __instance.SettingId)?.Base ?? null!; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs b/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs new file mode 100644 index 0000000..ae2d5a5 --- /dev/null +++ b/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs @@ -0,0 +1,25 @@ +namespace SecretAPI.Patches.Features +{ + using HarmonyLib; + using SecretAPI.Attribute; + using SecretAPI.Features.UserSettings; + using UserSettings.ServerSpecific; + + /// + /// Fixes validation for . + /// + [HarmonyPatchCategory(nameof(CustomSetting))] + [HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.ServerPrevalidateClientResponse))] + internal static class SettingsSyncValidateFix + { +#pragma warning disable SA1313 + private static void Postfix(SSSClientResponse msg, ref bool __result) +#pragma warning restore SA1313 + { + if (__result) + return; + + __result = CustomSetting.Get(msg.SettingType, msg.Id) != null; + } + } +} \ No newline at end of file diff --git a/SecretAPI/SecretAPI.csproj b/SecretAPI/SecretAPI.csproj index 1063db9..0251372 100644 --- a/SecretAPI/SecretAPI.csproj +++ b/SecretAPI/SecretAPI.csproj @@ -5,6 +5,7 @@ latest enable 0.3.0 + true @@ -43,6 +44,7 @@ +